aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.dev/.sasslintrc17
-rw-r--r--.dev/.stylelintrc.js15
-rw-r--r--.docker/nginx.conf7
-rw-r--r--.editorconfig2
-rw-r--r--.github/mailmap7
-rw-r--r--.htaccess23
-rw-r--r--.travis.yml29
-rw-r--r--AUTHORS31
-rw-r--r--CHANGELOG.md72
-rw-r--r--Dockerfile2
-rw-r--r--Makefile9
-rw-r--r--README.md6
-rw-r--r--application/History.php1
-rw-r--r--application/Languages.php3
-rw-r--r--application/Router.php184
-rw-r--r--application/Thumbnailer.php3
-rw-r--r--application/Utils.php84
-rw-r--r--application/api/ApiMiddleware.php21
-rw-r--r--application/api/ApiUtils.php17
-rw-r--r--application/api/controllers/ApiController.php3
-rw-r--r--application/api/controllers/Links.php23
-rw-r--r--application/bookmark/Bookmark.php202
-rw-r--r--application/bookmark/BookmarkArray.php23
-rw-r--r--application/bookmark/BookmarkFileService.php157
-rw-r--r--application/bookmark/BookmarkFilter.php175
-rw-r--r--application/bookmark/BookmarkIO.php49
-rw-r--r--application/bookmark/BookmarkInitializer.php87
-rw-r--r--application/bookmark/BookmarkServiceInterface.php109
-rw-r--r--application/bookmark/LinkUtils.php116
-rw-r--r--application/bookmark/exception/DatastoreNotInitializedException.php10
-rw-r--r--application/config/ConfigJson.php8
-rw-r--r--application/config/ConfigManager.php7
-rw-r--r--application/config/ConfigPlugin.php17
-rw-r--r--application/container/ContainerBuilder.php105
-rw-r--r--application/container/ShaarliContainer.php31
-rw-r--r--application/feed/Cache.php38
-rw-r--r--application/feed/FeedBuilder.php147
-rw-r--r--application/formatter/BookmarkDefaultFormatter.php154
-rw-r--r--application/formatter/BookmarkFormatter.php101
-rw-r--r--application/formatter/BookmarkMarkdownExtraFormatter.php24
-rw-r--r--application/formatter/BookmarkMarkdownFormatter.php10
-rw-r--r--application/formatter/FormatterFactory.php2
-rw-r--r--application/front/ShaarliAdminMiddleware.php27
-rw-r--r--application/front/ShaarliMiddleware.php83
-rw-r--r--application/front/controller/admin/ConfigureController.php126
-rw-r--r--application/front/controller/admin/ExportController.php80
-rw-r--r--application/front/controller/admin/ImportController.php82
-rw-r--r--application/front/controller/admin/LogoutController.php33
-rw-r--r--application/front/controller/admin/ManageTagController.php88
-rw-r--r--application/front/controller/admin/MetadataController.php29
-rw-r--r--application/front/controller/admin/PasswordController.php101
-rw-r--r--application/front/controller/admin/PluginsController.php85
-rw-r--r--application/front/controller/admin/ServerController.php87
-rw-r--r--application/front/controller/admin/SessionFilterController.php50
-rw-r--r--application/front/controller/admin/ShaareAddController.php34
-rw-r--r--application/front/controller/admin/ShaareManageController.php202
-rw-r--r--application/front/controller/admin/ShaarePublishController.php263
-rw-r--r--application/front/controller/admin/ShaarliAdminController.php71
-rw-r--r--application/front/controller/admin/ThumbnailsController.php65
-rw-r--r--application/front/controller/admin/TokenController.php26
-rw-r--r--application/front/controller/admin/ToolsController.php35
-rw-r--r--application/front/controller/visitor/BookmarkListController.php249
-rw-r--r--application/front/controller/visitor/DailyController.php205
-rw-r--r--application/front/controller/visitor/ErrorController.php42
-rw-r--r--application/front/controller/visitor/ErrorNotFoundController.php29
-rw-r--r--application/front/controller/visitor/FeedController.php58
-rw-r--r--application/front/controller/visitor/InstallController.php175
-rw-r--r--application/front/controller/visitor/LoginController.php153
-rw-r--r--application/front/controller/visitor/OpenSearchController.php27
-rw-r--r--application/front/controller/visitor/PictureWallController.php54
-rw-r--r--application/front/controller/visitor/PublicSessionFilterController.php46
-rw-r--r--application/front/controller/visitor/ShaarliVisitorController.php181
-rw-r--r--application/front/controller/visitor/TagCloudController.php121
-rw-r--r--application/front/controller/visitor/TagController.php118
-rw-r--r--application/front/controllers/LoginController.php48
-rw-r--r--application/front/controllers/ShaarliController.php69
-rw-r--r--application/front/exceptions/AlreadyInstalledException.php15
-rw-r--r--application/front/exceptions/CantLoginException.php10
-rw-r--r--application/front/exceptions/LoginBannedException.php2
-rw-r--r--application/front/exceptions/OpenShaarliPasswordException.php18
-rw-r--r--application/front/exceptions/ResourcePermissionException.php13
-rw-r--r--application/front/exceptions/ShaarliFrontException.php (renamed from application/front/exceptions/ShaarliException.php)4
-rw-r--r--application/front/exceptions/ThumbnailsDisabledException.php15
-rw-r--r--application/front/exceptions/UnauthorizedException.php15
-rw-r--r--application/front/exceptions/WrongTokenException.php18
-rw-r--r--application/helper/ApplicationUtils.php (renamed from application/ApplicationUtils.php)95
-rw-r--r--application/helper/DailyPageHelper.php208
-rw-r--r--application/helper/FileUtils.php (renamed from application/FileUtils.php)58
-rw-r--r--application/http/HttpAccess.php47
-rw-r--r--application/http/HttpUtils.php196
-rw-r--r--application/http/MetadataRetriever.php69
-rw-r--r--application/legacy/LegacyController.php162
-rw-r--r--application/legacy/LegacyLinkDB.php6
-rw-r--r--application/legacy/LegacyRouter.php63
-rw-r--r--application/legacy/LegacyUpdater.php7
-rw-r--r--application/legacy/UnknowLegacyRouteException.php9
-rw-r--r--application/netscape/NetscapeBookmarkUtils.php133
-rw-r--r--application/plugin/PluginManager.php31
-rw-r--r--application/render/PageBuilder.php114
-rw-r--r--application/render/PageCacheManager.php60
-rw-r--r--application/render/TemplatePage.php34
-rw-r--r--application/security/BanManager.php30
-rw-r--r--application/security/CookieManager.php33
-rw-r--r--application/security/LoginManager.php83
-rw-r--r--application/security/SessionManager.php111
-rw-r--r--application/updater/Updater.php75
-rw-r--r--assets/common/js/metadata.js107
-rw-r--r--assets/common/js/shaare-batch.js121
-rw-r--r--assets/common/js/thumbnails-update.js14
-rw-r--r--assets/default/js/base.js114
-rw-r--r--assets/default/scss/shaarli.scss234
-rw-r--r--assets/vintage/css/shaarli.css2
-rw-r--r--composer.json21
-rw-r--r--composer.lock1046
-rw-r--r--doc/md/3rd-party-libraries.md21
-rw-r--r--doc/md/Backup-and-restore.md11
-rw-r--r--doc/md/Browsing-and-searching.md37
-rw-r--r--doc/md/Community-and-related-software.md (renamed from doc/md/Community-&-Related-software.md)60
-rw-r--r--doc/md/Continuous-integration-tools.md32
-rw-r--r--doc/md/Development-guidelines.md13
-rw-r--r--doc/md/Directory-structure.md54
-rw-r--r--doc/md/Docker.md227
-rw-r--r--doc/md/Download-and-Installation.md124
-rw-r--r--doc/md/FAQ.md46
-rw-r--r--doc/md/Installation.md78
-rw-r--r--doc/md/Link-structure.md18
-rw-r--r--doc/md/Plugins.md61
-rw-r--r--doc/md/REST-API.md159
-rw-r--r--doc/md/RSS-feeds.md28
-rw-r--r--doc/md/Release-Shaarli.md161
-rw-r--r--doc/md/Reverse-proxy.md141
-rw-r--r--doc/md/Security.md25
-rw-r--r--doc/md/Server-configuration.md659
-rw-r--r--doc/md/Server-security.md76
-rw-r--r--doc/md/Shaarli-configuration.md218
-rw-r--r--doc/md/Sharing-content.md71
-rw-r--r--doc/md/Static-analysis.md13
-rw-r--r--doc/md/Troubleshooting.md140
-rw-r--r--doc/md/Unit-tests.md119
-rw-r--r--doc/md/Upgrade-and-migration.md144
-rw-r--r--doc/md/Usage.md111
-rw-r--r--doc/md/dev/Development.md179
-rw-r--r--doc/md/dev/GnuPG-signature.md (renamed from doc/md/GnuPG-signature.md)20
-rw-r--r--doc/md/dev/Plugin-system.md (renamed from doc/md/Plugin-System.md)165
-rw-r--r--doc/md/dev/Release-Shaarli.md145
-rw-r--r--doc/md/dev/Theming.md (renamed from doc/md/Theming.md)3
-rw-r--r--doc/md/dev/Translations.md (renamed from doc/md/Translations.md)89
-rw-r--r--doc/md/dev/Unit-tests.md133
-rw-r--r--doc/md/dev/Versioning.md (renamed from doc/md/Versioning-and-Branches.md)28
-rw-r--r--doc/md/dev/images/poedit-1.jpg (renamed from doc/md/images/poedit-1.jpg)bin72956 -> 72956 bytes
-rw-r--r--doc/md/docker/docker-101.md140
-rw-r--r--doc/md/docker/resources.md19
-rw-r--r--doc/md/docker/reverse-proxy-configuration.md123
-rw-r--r--doc/md/docker/shaarli-images.md118
-rw-r--r--doc/md/guides/backup-restore-import-export.md64
-rw-r--r--doc/md/guides/images/01-create-droplet-distro.jpgbin20909 -> 0 bytes
-rw-r--r--doc/md/guides/images/02-create-droplet-region.jpgbin21603 -> 0 bytes
-rw-r--r--doc/md/guides/images/03-create-droplet-size.jpgbin20860 -> 0 bytes
-rw-r--r--doc/md/guides/images/04-finalize.jpgbin28233 -> 0 bytes
-rw-r--r--doc/md/guides/images/05-droplet.jpgbin11977 -> 0 bytes
-rw-r--r--doc/md/guides/images/06-domain.jpgbin4499 -> 0 bytes
-rw-r--r--doc/md/guides/install-shaarli-with-debian9-and-docker.md257
-rw-r--r--doc/md/guides/various-hacks.md24
-rw-r--r--doc/md/images/07-installation.jpg (renamed from doc/md/guides/images/07-installation.jpg)bin42832 -> 42832 bytes
-rw-r--r--doc/md/images/bookmarklet.pngbin53346 -> 0 bytes
-rw-r--r--doc/md/images/firefoxshare.pngbin715 -> 0 bytes
-rw-r--r--doc/md/images/install-shaarli.pngbin33827 -> 0 bytes
-rw-r--r--doc/md/index.md127
-rw-r--r--docker-compose.yml2
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po1618
-rw-r--r--inc/languages/ja/LC_MESSAGES/shaarli.po1293
-rw-r--r--inc/languages/jp/LC_MESSAGES/shaarli.po1333
-rw-r--r--index.php1979
-rw-r--r--init.php86
-rw-r--r--mkdocs.yml46
-rw-r--r--package.json32
-rw-r--r--plugins/addlink_toolbar/addlink_toolbar.php6
-rw-r--r--plugins/archiveorg/archiveorg.html2
-rw-r--r--plugins/archiveorg/archiveorg.php7
-rw-r--r--plugins/default_colors/default_colors.php56
-rw-r--r--plugins/demo_plugin/demo_plugin.php10
-rw-r--r--plugins/isso/isso.php8
-rw-r--r--plugins/isso/isso_button.html5
-rw-r--r--plugins/playvideos/README.md9
-rw-r--r--plugins/playvideos/playvideos.php6
-rw-r--r--plugins/pubsubhubbub/pubsubhubbub.php8
-rw-r--r--plugins/qrcode/qrcode.php9
-rw-r--r--plugins/qrcode/shaarli-qrcode.js15
-rw-r--r--plugins/wallabag/README.md2
-rw-r--r--plugins/wallabag/wallabag.php4
-rw-r--r--tests/FileUtilsTest.php110
-rw-r--r--tests/HistoryTest.php25
-rw-r--r--tests/LanguagesTest.php4
-rw-r--r--tests/PluginManagerTest.php76
-rw-r--r--tests/RouterTest.php509
-rw-r--r--tests/TestCase.php77
-rw-r--r--tests/ThumbnailerTest.php5
-rw-r--r--tests/TimeZoneTest.php4
-rw-r--r--tests/UtilsTest.php44
-rw-r--r--tests/api/ApiMiddlewareTest.php65
-rw-r--r--tests/api/ApiUtilsTest.php76
-rw-r--r--tests/api/controllers/history/HistoryTest.php6
-rw-r--r--tests/api/controllers/info/InfoTest.php10
-rw-r--r--tests/api/controllers/links/DeleteLinkTest.php19
-rw-r--r--tests/api/controllers/links/GetLinkIdTest.php18
-rw-r--r--tests/api/controllers/links/GetLinksTest.php14
-rw-r--r--tests/api/controllers/links/PostLinkTest.php22
-rw-r--r--tests/api/controllers/links/PutLinkTest.php20
-rw-r--r--tests/api/controllers/tags/DeleteTagTest.php23
-rw-r--r--tests/api/controllers/tags/GetTagNameTest.php16
-rw-r--r--tests/api/controllers/tags/GetTagsTest.php10
-rw-r--r--tests/api/controllers/tags/PutTagTest.php22
-rw-r--r--tests/bookmark/BookmarkArrayTest.php38
-rw-r--r--tests/bookmark/BookmarkFileServiceTest.php373
-rw-r--r--tests/bookmark/BookmarkFilterTest.php74
-rw-r--r--tests/bookmark/BookmarkInitializerTest.php97
-rw-r--r--tests/bookmark/BookmarkTest.php94
-rw-r--r--tests/bookmark/LinkUtilsTest.php351
-rw-r--r--tests/bootstrap.php18
-rw-r--r--tests/config/ConfigJsonTest.php25
-rw-r--r--tests/config/ConfigManagerTest.php26
-rw-r--r--tests/config/ConfigPhpTest.php4
-rw-r--r--tests/config/ConfigPluginTest.php22
-rw-r--r--tests/container/ContainerBuilderTest.php54
-rw-r--r--tests/container/ShaarliTestContainer.php42
-rw-r--r--tests/feed/CachedPageTest.php12
-rw-r--r--tests/feed/FeedBuilderTest.php122
-rw-r--r--tests/formatter/BookmarkDefaultFormatterTest.php123
-rw-r--r--tests/formatter/BookmarkMarkdownExtraFormatterTest.php162
-rw-r--r--tests/formatter/BookmarkMarkdownFormatterTest.php8
-rw-r--r--tests/formatter/BookmarkRawFormatterTest.php4
-rw-r--r--tests/formatter/FormatterFactoryTest.php4
-rw-r--r--tests/front/ShaarliAdminMiddlewareTest.php100
-rw-r--r--tests/front/ShaarliMiddlewareTest.php163
-rw-r--r--tests/front/controller/LoginControllerTest.php178
-rw-r--r--tests/front/controller/ShaarliControllerTest.php116
-rw-r--r--tests/front/controller/admin/ConfigureControllerTest.php252
-rw-r--r--tests/front/controller/admin/ExportControllerTest.php163
-rw-r--r--tests/front/controller/admin/FrontAdminControllerMockHelper.php56
-rw-r--r--tests/front/controller/admin/ImportControllerTest.php148
-rw-r--r--tests/front/controller/admin/LogoutControllerTest.php50
-rw-r--r--tests/front/controller/admin/ManageTagControllerTest.php272
-rw-r--r--tests/front/controller/admin/PasswordControllerTest.php203
-rw-r--r--tests/front/controller/admin/PluginsControllerTest.php205
-rw-r--r--tests/front/controller/admin/ServerControllerTest.php184
-rw-r--r--tests/front/controller/admin/SessionFilterControllerTest.php177
-rw-r--r--tests/front/controller/admin/ShaareAddControllerTest.php97
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php418
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php380
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php145
-rw-r--r--tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php139
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php63
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php367
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php155
-rw-r--r--tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php369
-rw-r--r--tests/front/controller/admin/ShaarliAdminControllerTest.php184
-rw-r--r--tests/front/controller/admin/ThumbnailsControllerTest.php156
-rw-r--r--tests/front/controller/admin/TokenControllerTest.php41
-rw-r--r--tests/front/controller/admin/ToolsControllerTest.php69
-rw-r--r--tests/front/controller/visitor/BookmarkListControllerTest.php532
-rw-r--r--tests/front/controller/visitor/DailyControllerTest.php716
-rw-r--r--tests/front/controller/visitor/ErrorControllerTest.php70
-rw-r--r--tests/front/controller/visitor/ErrorNotFoundControllerTest.php81
-rw-r--r--tests/front/controller/visitor/FeedControllerTest.php151
-rw-r--r--tests/front/controller/visitor/FrontControllerMockHelper.php118
-rw-r--r--tests/front/controller/visitor/InstallControllerTest.php304
-rw-r--r--tests/front/controller/visitor/LoginControllerTest.php404
-rw-r--r--tests/front/controller/visitor/OpenSearchControllerTest.php44
-rw-r--r--tests/front/controller/visitor/PictureWallControllerTest.php123
-rw-r--r--tests/front/controller/visitor/PublicSessionFilterControllerTest.php122
-rw-r--r--tests/front/controller/visitor/ShaarliVisitorControllerTest.php246
-rw-r--r--tests/front/controller/visitor/TagCloudControllerTest.php381
-rw-r--r--tests/front/controller/visitor/TagControllerTest.php215
-rw-r--r--tests/helper/ApplicationUtilsTest.php (renamed from tests/ApplicationUtilsTest.php)86
-rw-r--r--tests/helper/DailyPageHelperTest.php262
-rw-r--r--tests/helper/FileUtilsTest.php197
-rw-r--r--tests/http/HttpUtils/ClientIpIdTest.php2
-rw-r--r--tests/http/HttpUtils/GetHttpUrlTest.php2
-rw-r--r--tests/http/HttpUtils/GetIpAdressFromProxyTest.php2
-rw-r--r--tests/http/HttpUtils/IndexUrlTest.php68
-rw-r--r--tests/http/HttpUtils/IndexUrlTestWithConstant.php51
-rw-r--r--tests/http/HttpUtils/IsHttpsTest.php2
-rw-r--r--tests/http/HttpUtils/PageUrlTest.php2
-rw-r--r--tests/http/HttpUtils/ServerUrlTest.php2
-rw-r--r--tests/http/MetadataRetrieverTest.php154
-rw-r--r--tests/http/UrlTest.php2
-rw-r--r--tests/http/UrlUtils/CleanupUrlTest.php2
-rw-r--r--tests/http/UrlUtils/GetUrlSchemeTest.php2
-rw-r--r--tests/http/UrlUtils/UnparseUrlTest.php2
-rw-r--r--tests/http/UrlUtils/WhitelistProtocolsTest.php2
-rw-r--r--tests/languages/fr/LanguagesFrTest.php6
-rw-r--r--tests/legacy/LegacyControllerTest.php101
-rw-r--r--tests/legacy/LegacyLinkDBTest.php41
-rw-r--r--tests/legacy/LegacyLinkFilterTest.php18
-rw-r--r--tests/legacy/LegacyUpdaterTest.php24
-rw-r--r--tests/netscape/BookmarkExportTest.php78
-rw-r--r--tests/netscape/BookmarkImportTest.php80
-rw-r--r--tests/plugins/PluginAddlinkTest.php14
-rw-r--r--tests/plugins/PluginArchiveorgTest.php37
-rw-r--r--tests/plugins/PluginDefaultColorsTest.php25
-rw-r--r--tests/plugins/PluginIssoTest.php23
-rw-r--r--tests/plugins/PluginPlayvideosTest.php10
-rw-r--r--tests/plugins/PluginPubsubhubbubTest.php10
-rw-r--r--tests/plugins/PluginQrcodeTest.php8
-rw-r--r--tests/plugins/PluginWallabagTest.php4
-rw-r--r--tests/plugins/WallabagInstanceTest.php4
-rw-r--r--tests/plugins/resources/hashtags.md10
-rw-r--r--tests/plugins/resources/hashtags.raw10
-rw-r--r--tests/plugins/resources/markdown.html33
-rw-r--r--tests/plugins/resources/markdown.md34
-rw-r--r--tests/plugins/test/test.php8
-rw-r--r--tests/render/PageCacheManagerTest.php (renamed from tests/feed/CacheTest.php)43
-rw-r--r--tests/render/ThemeUtilsTest.php2
-rw-r--r--tests/security/BanManagerTest.php9
-rw-r--r--tests/security/LoginManagerTest.php81
-rw-r--r--tests/security/SessionManagerTest.php91
-rw-r--r--tests/updater/DummyUpdater.php8
-rw-r--r--tests/updater/UpdaterTest.php79
-rw-r--r--tests/utils/FakeApplicationUtils.php2
-rw-r--r--tests/utils/FakeConfigManager.php10
-rw-r--r--tests/utils/ReferenceHistory.php2
-rw-r--r--tests/utils/ReferenceLinkDB.php10
-rw-r--r--tpl/default/404.html2
-rw-r--r--tpl/default/addlink.html58
-rw-r--r--tpl/default/changepassword.html2
-rw-r--r--tpl/default/changetag.html8
-rw-r--r--tpl/default/configure.html6
-rw-r--r--tpl/default/daily.html40
-rw-r--r--tpl/default/dailyrss.html51
-rw-r--r--tpl/default/editlink.batch.html32
-rw-r--r--tpl/default/editlink.html48
-rw-r--r--tpl/default/error.html2
-rw-r--r--tpl/default/export.html3
-rw-r--r--tpl/default/feed.atom.html2
-rw-r--r--tpl/default/feed.rss.html4
-rw-r--r--tpl/default/import.html2
-rw-r--r--tpl/default/includes.html21
-rw-r--r--tpl/default/install.html12
-rw-r--r--tpl/default/linklist.html54
-rw-r--r--tpl/default/linklist.paging.html59
-rw-r--r--tpl/default/opensearch.html4
-rw-r--r--tpl/default/page.footer.html9
-rw-r--r--tpl/default/page.header.html56
-rw-r--r--tpl/default/picwall.html86
-rw-r--r--tpl/default/pluginsadmin.html9
-rw-r--r--tpl/default/server.html129
-rw-r--r--tpl/default/server.requirements.html68
-rw-r--r--tpl/default/tag.cloud.html6
-rw-r--r--tpl/default/tag.list.html10
-rw-r--r--tpl/default/tag.sort.html8
-rw-r--r--tpl/default/thumbnails.html2
-rw-r--r--tpl/default/tools.html28
-rw-r--r--tpl/vintage/404.html2
-rw-r--r--tpl/vintage/addlink.html2
-rw-r--r--tpl/vintage/changepassword.html4
-rw-r--r--tpl/vintage/changetag.html2
-rw-r--r--tpl/vintage/configure.html6
-rw-r--r--tpl/vintage/daily.html20
-rw-r--r--tpl/vintage/dailyrss.html46
-rw-r--r--tpl/vintage/editlink.html12
-rw-r--r--tpl/vintage/error.html2
-rw-r--r--tpl/vintage/export.html5
-rw-r--r--tpl/vintage/feed.atom.html4
-rw-r--r--tpl/vintage/feed.rss.html2
-rw-r--r--tpl/vintage/import.html2
-rw-r--r--tpl/vintage/includes.html15
-rw-r--r--tpl/vintage/install.html2
-rw-r--r--tpl/vintage/linklist.html43
-rw-r--r--tpl/vintage/linklist.paging.html17
-rw-r--r--tpl/vintage/loginform.html2
-rw-r--r--tpl/vintage/opensearch.html4
-rw-r--r--tpl/vintage/page.footer.html6
-rw-r--r--tpl/vintage/page.header.html22
-rw-r--r--tpl/vintage/picwall.html2
-rw-r--r--tpl/vintage/pluginsadmin.html6
-rw-r--r--tpl/vintage/tag.cloud.html4
-rw-r--r--tpl/vintage/thumbnails.html2
-rw-r--r--tpl/vintage/tools.html14
-rw-r--r--webpack.config.js71
-rw-r--r--yarn.lock7574
380 files changed, 27166 insertions, 14171 deletions
diff --git a/.dev/.sasslintrc b/.dev/.sasslintrc
deleted file mode 100644
index 47c3145d..00000000
--- a/.dev/.sasslintrc
+++ /dev/null
@@ -1,17 +0,0 @@
1options:
2 max-warnings: 0
3rules:
4 property-sort-order:
5 - 0
6# Sort order rule does not work with CSS variables: https://github.com/sasstools/sass-lint/issues/1161
7# - 1
8# -
9# order: 'concentric'
10 no-important:
11 - 0
12 no-vendor-prefixes:
13 - 0 # this will be fixed with v2: see https://github.com/sasstools/sass-lint/pull/1137
14 nesting-depth:
15 - 1
16 -
17 max-depth: 4
diff --git a/.dev/.stylelintrc.js b/.dev/.stylelintrc.js
new file mode 100644
index 00000000..a754e33b
--- /dev/null
+++ b/.dev/.stylelintrc.js
@@ -0,0 +1,15 @@
1module.exports = {
2 extends: 'stylelint-config-standard',
3 plugins: [
4 "stylelint-scss"
5 ],
6 rules: {
7 "indentation": [2],
8 "number-leading-zero": null,
9 // Replace CSS @ with SASS ones
10 "at-rule-no-unknown": null,
11 "scss/at-rule-no-unknown": true,
12 // not compatible with SASS apparently
13 "no-descending-specificity": null
14 },
15}
diff --git a/.docker/nginx.conf b/.docker/nginx.conf
index 07fba33f..023f52c1 100644
--- a/.docker/nginx.conf
+++ b/.docker/nginx.conf
@@ -29,7 +29,7 @@ http {
29 log_not_found off; 29 log_not_found off;
30 deny all; 30 deny all;
31 } 31 }
32 32
33 location ~ ~$ { 33 location ~ ~$ {
34 # deny access to temp editor files, e.g. "script.php~" 34 # deny access to temp editor files, e.g. "script.php~"
35 access_log off; 35 access_log off;
@@ -65,6 +65,11 @@ http {
65 include fastcgi.conf; 65 include fastcgi.conf;
66 } 66 }
67 67
68 location ~ /doc/ {
69 default_type "text/html";
70 try_files $uri $uri/ $uri.html =404;
71 }
72
68 location ~ \.php$ { 73 location ~ \.php$ {
69 # deny access to all other PHP scripts 74 # deny access to all other PHP scripts
70 deny all; 75 deny all;
diff --git a/.editorconfig b/.editorconfig
index 34bd7994..c2ab80eb 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -14,7 +14,7 @@ indent_size = 4
14indent_size = 2 14indent_size = 2
15 15
16[*.php] 16[*.php]
17max_line_length = 100 17max_line_length = 120
18 18
19[Dockerfile] 19[Dockerfile]
20max_line_length = 80 20max_line_length = 80
diff --git a/.github/mailmap b/.github/mailmap
index 7633afcf..15a25e43 100644
--- a/.github/mailmap
+++ b/.github/mailmap
@@ -1,13 +1,18 @@
1ArthurHoaro <arthur@hoa.ro> 1ArthurHoaro <arthur@hoa.ro> <arthur.hoareau@wizacha.com>
2ArthurHoaro <arthur@hoa.ro> Arthur
2Florian Eula <eula.florian@gmail.com> feula 3Florian Eula <eula.florian@gmail.com> feula
3Florian Eula <eula.florian@gmail.com> <mr.pikzen@gmail.com> 4Florian Eula <eula.florian@gmail.com> <mr.pikzen@gmail.com>
4Immánuel Fodor <immanuelfactor+github@gmail.com> 5Immánuel Fodor <immanuelfactor+github@gmail.com>
6Immánuel Fodor <immanuelfactor+github@gmail.com> Immánuel! <21174107+immanuelfodor@users.noreply.github.com>
5kalvn <kalvnthereal@gmail.com> <kalvn@users.noreply.github.com> 7kalvn <kalvnthereal@gmail.com> <kalvn@users.noreply.github.com>
8kalvn <kalvnthereal@gmail.com> <kalvn@pm.me>
9Neros <contact@neros.fr> <NerosTie@users.noreply.github.com>
6Nicolas Danelon <hi@nicolasmd.com.ar> nicolasm 10Nicolas Danelon <hi@nicolasmd.com.ar> nicolasm
7Nicolas Danelon <hi@nicolasmd.com.ar> <nda@3818.com.ar> 11Nicolas Danelon <hi@nicolasmd.com.ar> <nda@3818.com.ar>
8Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@gmail.com> 12Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@gmail.com>
9Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@users.noreply.github.com> 13Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@users.noreply.github.com>
10Sébastien Sauvage <sebsauvage@sebsauvage.net> 14Sébastien Sauvage <sebsauvage@sebsauvage.net>
15Sébastien NOBILI <code@pipoprods.org> <s-code-github@pipoprods.org>
11Timo Van Neerden <fire@lehollandaisvolant.net> 16Timo Van Neerden <fire@lehollandaisvolant.net>
12Timo Van Neerden <fire@lehollandaisvolant.net> lehollandaisvolant <levoltigeurhollandais@gmail.com> 17Timo Van Neerden <fire@lehollandaisvolant.net> lehollandaisvolant <levoltigeurhollandais@gmail.com>
13VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com> 18VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com>
diff --git a/.htaccess b/.htaccess
index 4c004271..25fcfb03 100644
--- a/.htaccess
+++ b/.htaccess
@@ -7,31 +7,20 @@ RewriteEngine On
7RewriteRule ^(.git|doxygen|vendor) - [F] 7RewriteRule ^(.git|doxygen|vendor) - [F]
8 8
9# Forward the "Authorization" HTTP header 9# Forward the "Authorization" HTTP header
10# fixes JWT token not correctly forwarded on some Apache/FastCGI setups
10RewriteCond %{HTTP:Authorization} ^(.*) 11RewriteCond %{HTTP:Authorization} ^(.*)
11RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] 12RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
13# Alternative (if the 2 lines above don't work)
14# SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
12 15
13# REST API 16# REST API
17# Ionos Hosting needs RewriteBase /
18# RewriteBase /
14RewriteCond %{REQUEST_FILENAME} !-f 19RewriteCond %{REQUEST_FILENAME} !-f
15RewriteCond %{REQUEST_FILENAME} !-d 20RewriteCond %{REQUEST_FILENAME} !-d
16RewriteRule ^ index.php [QSA,L] 21RewriteRule ^ index.php [QSA,L]
17 22
18<Limit GET POST PUT DELETE OPTIONS> 23<LimitExcept GET POST PUT DELETE PATCH OPTIONS>
19 <IfModule version_module>
20 <IfVersion >= 2.4>
21 Require all granted
22 </IfVersion>
23 <IfVersion < 2.4>
24 Allow from all
25 Deny from none
26 </IfVersion>
27 </IfModule>
28
29 <IfModule !version_module>
30 Require all granted
31 </IfModule>
32</Limit>
33
34<LimitExcept GET POST PUT DELETE OPTIONS>
35 <IfModule version_module> 24 <IfModule version_module>
36 <IfVersion >= 2.4> 25 <IfVersion >= 2.4>
37 Require all denied 26 Require all denied
diff --git a/.travis.yml b/.travis.yml
index f466c317..d7460947 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,16 @@
1sudo: false 1dist: bionic
2dist: trusty
3 2
4matrix: 3matrix:
5 include: 4 include:
5 # jobs for each supported php version
6 - language: php
7 php: nightly # PHP 8.0
8 install:
9 - composer self-update --2
10 - composer update --ignore-platform-req=php
11 - composer remove --dev --ignore-platform-req=php phpunit/phpunit
12 - composer require --dev --ignore-platform-req=php phpunit/php-text-template ^2.0
13 - composer require --dev --ignore-platform-req=php phpunit/phpunit ^9.0
6 - language: php 14 - language: php
7 php: 7.4 15 php: 7.4
8 - language: php 16 - language: php
@@ -11,23 +19,22 @@ matrix:
11 php: 7.2 19 php: 7.2
12 - language: php 20 - language: php
13 php: 7.1 21 php: 7.1
22 # jobs for frontend builds
14 - language: node_js 23 - language: node_js
15 node_js: 8 24 node_js: 10
16 cache: 25 cache:
17 yarn: true 26 yarn: true
18 directories: 27 directories:
19 - $HOME/.cache/yarn 28 - $HOME/.cache/yarn
20
21 install: 29 install:
22 - yarn install 30 - yarn install
23
24 before_script: 31 before_script:
25 - PATH=${PATH//:\.\/node_modules\/\.bin/} 32 - PATH=${PATH//:\.\/node_modules\/\.bin/}
26
27 script: 33 script:
28 - yarn run build # Just to be sure that the build isn't broken 34 - yarn run build # verify successful frontend builds
29 - make eslint 35 - make eslint # javascript static analysis
30 - make sasslint 36 - make sasslint # linter for SASS syntax
37 # jobs for documentation builds
31 - language: python 38 - language: python
32 python: 3.6 39 python: 3.6
33 cache: 40 cache:
@@ -43,7 +50,9 @@ cache:
43 - $HOME/.composer/cache 50 - $HOME/.composer/cache
44 51
45install: 52install:
46 - composer install --prefer-dist 53 # install/update composer and php dependencies
54 - composer config --unset platform && composer config platform.php $TRAVIS_PHP_VERSION
55 - composer update
47 56
48before_script: 57before_script:
49 - PATH=${PATH//:\.\/node_modules\/\.bin/} 58 - PATH=${PATH//:\.\/node_modules\/\.bin/}
diff --git a/AUTHORS b/AUTHORS
index 50593218..0ec52acc 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,46 +1,56 @@
1 782 ArthurHoaro <arthur@hoa.ro> 1 991 ArthurHoaro <arthur@hoa.ro>
2 401 VirtualTam <virtualtam@flibidi.net> 2 402 VirtualTam <virtualtam@flibidi.net>
3 218 nodiscc <nodiscc@gmail.com> 3 294 nodiscc <nodiscc@gmail.com>
4 56 Sébastien Sauvage <sebsauvage@sebsauvage.net> 4 56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
5 16 Luce Carević <lcarevic@access42.net> 5 16 Luce Carević <lcarevic@access42.net>
6 15 Florian Eula <eula.florian@gmail.com> 6 15 Florian Eula <eula.florian@gmail.com>
7 13 Emilien Klein <emilien@klein.st> 7 13 Emilien Klein <emilien@klein.st>
8 12 Nicolas Danelon <hi@nicolasmd.com.ar> 8 12 Nicolas Danelon <hi@nicolasmd.com.ar>
9 9 Lucas Cimon <lucas.cimon@gmail.com>
9 9 Willi Eggeling <thewilli@gmail.com> 10 9 Willi Eggeling <thewilli@gmail.com>
10 8 Christophe HENRY <christophe.henry@sbgodin.fr> 11 8 Christophe HENRY <christophe.henry@sbgodin.fr>
11 6 B. van Berkum <dev@dotmpe.com> 12 6 B. van Berkum <dev@dotmpe.com>
13 6 Immánuel Fodor <immanuelfactor+github@gmail.com>
14 6 Keith Carangelo <mail@kcaran.com>
15 6 kalvn <kalvnthereal@gmail.com>
12 6 llune <llune@users.noreply.github.com> 16 6 llune <llune@users.noreply.github.com>
13 5 Lucas Cimon <lucas.cimon@gmail.com>
14 5 Mark Schmitz <kramred@gmail.com> 17 5 Mark Schmitz <kramred@gmail.com>
15 5 kalvn <kalvnthereal@gmail.com> 18 5 Sébastien NOBILI <code@pipoprods.org>
19 5 dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
16 4 Alexandre Alapetite <alexandre@alapetite.fr> 20 4 Alexandre Alapetite <alexandre@alapetite.fr>
17 4 David Sferruzza <david.sferruzza@gmail.com> 21 4 David Sferruzza <david.sferruzza@gmail.com>
18 4 Immánuel Fodor <immanuelfactor+github@gmail.com>
19 3 Agurato <mail.vmonot@gmail.com> 22 3 Agurato <mail.vmonot@gmail.com>
23 3 Christoph Stoettner <christoph.stoettner@stoeps.de>
20 3 Teromene <teromene@teromene.fr> 24 3 Teromene <teromene@teromene.fr>
21 2 Alexandre G.-Raymond <alex@ndre.gr> 25 2 Alexandre G.-Raymond <alex@ndre.gr>
22 2 Chris Kuethe <chris.kuethe@gmail.com> 26 2 Chris Kuethe <chris.kuethe@gmail.com>
23 2 Felix Bartels <felix@host-consultants.de> 27 2 Felix Bartels <felix@host-consultants.de>
28 2 Guillaume Virlet <github@virlet.org>
24 2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org> 29 2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
25 2 Mathieu Chabanon <git@matchab.fr> 30 2 Mathieu Chabanon <git@matchab.fr>
26 2 Miloš Jovanović <mjovanovic@gmail.com> 31 2 Miloš Jovanović <mjovanovic@gmail.com>
32 2 Neros <contact@neros.fr>
27 2 Qwerty <champlywood@free.fr> 33 2 Qwerty <champlywood@free.fr>
28 2 Stephen Muth <smuth4@gmail.com> 34 2 Stephen Muth <smuth4@gmail.com>
29 2 Timo Van Neerden <fire@lehollandaisvolant.net> 35 2 Timo Van Neerden <fire@lehollandaisvolant.net>
36 2 flow.gunso <flow.gunso@gmail.com>
30 2 julienCXX <software@chmodplusx.eu> 37 2 julienCXX <software@chmodplusx.eu>
31 2 philipp-r <philipp-r@users.noreply.github.com> 38 2 philipp-r <philipp-r@users.noreply.github.com>
32 2 pips <pips@e5150.fr> 39 2 pips <pips@e5150.fr>
33 2 trailjeep <trailjeep@gmail.com> 40 2 trailjeep <trailjeep@gmail.com>
41 2 yude <yudesleepy@gmail.com>
34 1 Adrien Oliva <adrien.oliva@yapbreak.fr> 42 1 Adrien Oliva <adrien.oliva@yapbreak.fr>
35 1 Adrien le Maire <adrien@alemaire.be> 43 1 Adrien le Maire <adrien@alemaire.be>
36 1 Alexis J <alexis@effingo.be> 44 1 Alexis J <alexis@effingo.be>
37 1 Angristan <angristan@users.noreply.github.com> 45 1 Angristan <angristan@users.noreply.github.com>
38 1 Bish Erbas <42714627+bisherbas@users.noreply.github.com> 46 1 Bish Erbas <42714627+bisherbas@users.noreply.github.com>
39 1 BoboTiG <bobotig@gmail.com> 47 1 BoboTiG <bobotig@gmail.com>
48 1 Brendan M. Sleight <bms.git@barwap.com>
40 1 Bronco <bronco@warriordudimanche.net> 49 1 Bronco <bronco@warriordudimanche.net>
41 1 Buster One <37770318+buster-one@users.noreply.github.com> 50 1 Buster One <37770318+buster-one@users.noreply.github.com>
42 1 D Low <daniellowtw@gmail.com> 51 1 D Low <daniellowtw@gmail.com>
43 1 Daniel Jakots <vigdis@chown.me> 52 1 Daniel Jakots <vigdis@chown.me>
53 1 David Foucher <dev@tyjak.net>
44 1 Dennis Verspuij <dennisverspuij@users.noreply.github.com> 54 1 Dennis Verspuij <dennisverspuij@users.noreply.github.com>
45 1 Dimtion <zizou.xena@gmail.com> 55 1 Dimtion <zizou.xena@gmail.com>
46 1 Fanch <fanch-github@qth.fr> 56 1 Fanch <fanch-github@qth.fr>
@@ -48,20 +58,25 @@
48 1 Florian Voigt <flvoigt@me.com> 58 1 Florian Voigt <flvoigt@me.com>
49 1 Franck Kerbiriou <FranckKe@users.noreply.github.com> 59 1 Franck Kerbiriou <FranckKe@users.noreply.github.com>
50 1 Gary Marigliano <gmarigliano93@gmail.com> 60 1 Gary Marigliano <gmarigliano93@gmail.com>
51 1 Guillaume Virlet <github@virlet.org>
52 1 Jonathan Amiez <jonathan.amiez@gmail.com> 61 1 Jonathan Amiez <jonathan.amiez@gmail.com>
53 1 Jonathan Druart <jonathan.druart@gmail.com> 62 1 Jonathan Druart <jonathan.druart@gmail.com>
54 1 Julien Pivotto <roidelapluie@inuits.eu> 63 1 Julien Pivotto <roidelapluie@inuits.eu>
55 1 Kevin Canévet <kevin@streamroot.io> 64 1 Kevin Canévet <kevin@streamroot.io>
65 1 Kevin Masson <kevin.masson@methodinthemadness.eu>
56 1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org> 66 1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
57 1 Lionel Martin <renarddesmers@gmail.com> 67 1 Lionel Martin <renarddesmers@gmail.com>
58 1 Mark Gerarts <mark.gerarts@gmail.com> 68 1 Mark Gerarts <mark.gerarts@gmail.com>
59 1 Marsup <marsup@gmail.com> 69 1 Marsup <marsup@gmail.com>
60 1 Neros <contact@neros.fr> 70 1 Paul van den Burg <github@paulvandenburg.nl>
61 1 Rajat Hans <rajathans9@gmail.com> 71 1 Rajat Hans <rajathans9@gmail.com>
62 1 Sbgodin <Sbgodin@users.noreply.github.com> 72 1 Sbgodin <Sbgodin@users.noreply.github.com>
73 1 Sebastien Wains <sebw@users.noreply.github.com>
63 1 TsT <tst2005@gmail.com> 74 1 TsT <tst2005@gmail.com>
64 1 agentcobra <agentcobra@free.fr> 75 1 agentcobra <agentcobra@free.fr>
76 1 aguy <aguytech@users.noreply.github.com>
65 1 dimtion <zizou.xena@gmail.com> 77 1 dimtion <zizou.xena@gmail.com>
66 1 durcheinandr <jochen@durcheinandr.de> 78 1 durcheinandr <jochen@durcheinandr.de>
67 1 lapineige <lapineige@users.noreply.github.com> 79 1 lapineige <lapineige@users.noreply.github.com>
80 1 owen bell <66233223+xfnw@users.noreply.github.com>
81 1 rfolo9li <50079896+rfolo9li@users.noreply.github.com>
82 1 sprak3000 <sprak3000+github@gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index abf802ea..f1686d67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,78 @@ All notable changes to this project will be documented in this file.
4The format is based on [Keep a Changelog](http://keepachangelog.com/) 4The format is based on [Keep a Changelog](http://keepachangelog.com/)
5and this project adheres to [Semantic Versioning](http://semver.org/). 5and this project adheres to [Semantic Versioning](http://semver.org/).
6 6
7## [v0.12.1]() - UNRELEASED
8
9## [v0.12.0](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-10-13
10
11**Save you `data/` folder before updating!**
12
13### Added
14- Thumbnailer: add soundcloud.com to list of common media domains
15- Markdown rendering is now integrated into Shaarli core
16- Add autofocus on tag cloud filter input
17- Japanese translations
18- Japanese translation: add language to admin configuration page
19- Support for PHP 8.0
20- Support for local anchor URL (starting with `#`)
21- LDAP authentication
22- Encapsulated PageCacheManager
23- Docs:
24 - add screenshots of all pages
25 - section about mkdocs
26 - Ulauncher extension
27- CI: run against PHP 7.4
28- Added $links_per_page variable to template and display on default
29- Inject BookmarkServiceInterface in plugins data
30- Add manual configuration for root URL
31- Added PATCH to the allowed Apache request methods.
32- REST API: compatibility with ionos Apache's headers
33
34### Changed
35- Introduce Bookmark object and Service layer
36 - Save bookmark as objects in the datastore
37 - Handle bookmark as objects across the whole codebase (except templates and plugins)
38- Process all Shaarli page through Slim controller, with proper URL rewriting (see #1516)
39- Docs: the entire documentation has been reviewed, updated and improved, thanks to @nodiscc!
40- ATOM feed: use instance name as author name instead of URL
41- Updated French translation
42- Default colors plugin: generate CSS file during initialization
43- Improve default bookmarks after install
44- Upgrade all front end dependencies and webpack build
45- Default theme: Make tag cloud/list views buttons more obvious
46
47### Fixed
48- Undefined index: thumbnail in daily page
49- Undefined index: thumbnail on OpenGraph headers
50- Undefined index: updated on linklist
51- Make sure that bookmark sort is consistent, even with equal timestamps
52- Code PHP version check as requirement bumped to PHP 7.1
53- Thumbnail images lazy loading
54- Markdown plugin: fix RSS feed direct link reverse
55- Fix RSS permalink included in Markdown bloc
56- Demo plugin: multiple typos
57- Makefile target for releases
58- Makefile target for html documentation
59- Session cookie setting being set while session is active
60- Deprecated use of implode
61- Division by zero in tag cloud
62- CI: deprecated linux distribution and sudo directive
63- Docker build: gcc is no longer included in python alpine image
64- Default template: display pin button in mobile view
65- Pinned bookmarks are not longer displayed first in ATOM/RSS feeds
66- Docs:
67 - Outdated Docker documentation for stable branch
68 - Outdated links
69 - Plugin description in meta files
70- docker-compose.yml: pin traefik image to 1.7-alpine
71
72### Removed
73- Markdown plugin
74- Docs:
75 - emojione & twemoji removed
76- Makefile: remove static_analysis_summary from all: target
77- doc/Makefile: remove references to composer update
78
7## [v0.11.1](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) - 2019-08-03 79## [v0.11.1](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) - 2019-08-03
8 80
9Release to fix broken Docker build on the latest version. 81Release to fix broken Docker build on the latest version.
diff --git a/Dockerfile b/Dockerfile
index f05cf3a4..f6120b71 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,6 +4,7 @@
4FROM python:3-alpine as docs 4FROM python:3-alpine as docs
5ADD . /usr/src/app/shaarli 5ADD . /usr/src/app/shaarli
6RUN cd /usr/src/app/shaarli \ 6RUN cd /usr/src/app/shaarli \
7 && apk add --no-cache gcc musl-dev \
7 && pip install --no-cache-dir mkdocs \ 8 && pip install --no-cache-dir mkdocs \
8 && mkdocs build --clean 9 && mkdocs build --clean
9 10
@@ -43,6 +44,7 @@ RUN apk --update --no-cache add \
43 php7-openssl \ 44 php7-openssl \
44 php7-session \ 45 php7-session \
45 php7-xml \ 46 php7-xml \
47 php7-simplexml \
46 php7-zlib \ 48 php7-zlib \
47 s6 49 s6
48 50
diff --git a/Makefile b/Makefile
index b52ba22f..7415887a 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
3 3
4BIN = vendor/bin 4BIN = vendor/bin
5 5
6all: static_analysis_summary check_permissions test 6all: check_permissions test
7 7
8## 8##
9# Docker test adapter 9# Docker test adapter
@@ -85,6 +85,10 @@ all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR
85 @# --text doesn't work with phpunit 4.* (v5 requires PHP 5.6) 85 @# --text doesn't work with phpunit 4.* (v5 requires PHP 5.6)
86 @#$(BIN)/phpcov merge --text coverage/txt coverage 86 @#$(BIN)/phpcov merge --text coverage/txt coverage
87 87
88### download 3rd-party PHP libraries, including dev dependencies
89composer_dependencies_dev: clean
90 composer install --prefer-dist
91
88## 92##
89# Custom release archive generation 93# Custom release archive generation
90# 94#
@@ -171,7 +175,8 @@ translate:
171eslint: 175eslint:
172 @yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/ 176 @yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/
173 @yarn run eslint -c .dev/.eslintrc.js assets/default/js/ 177 @yarn run eslint -c .dev/.eslintrc.js assets/default/js/
178 @yarn run eslint -c .dev/.eslintrc.js assets/common/js/
174 179
175### Run CSSLint check against Shaarli's SCSS files 180### Run CSSLint check against Shaarli's SCSS files
176sasslint: 181sasslint:
177 @yarn run sass-lint -c .dev/.sasslintrc 'assets/default/scss/*.scss' -v -q 182 @yarn run stylelint --config .dev/.stylelintrc.js 'assets/default/scss/*.scss'
diff --git a/README.md b/README.md
index 4fb0bfe0..46dda8d5 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,13 @@ _Do you want to share the links you discover?_
6_Shaarli is a minimalist link sharing service that you can install on your own server._ 6_Shaarli is a minimalist link sharing service that you can install on your own server._
7_It is designed to be personal (single-user), fast and handy._ 7_It is designed to be personal (single-user), fast and handy._
8 8
9[![](https://img.shields.io/badge/stable-v0.10.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4) 9[![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1)
10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) 10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
11&bull; 11&bull;
12[![](https://img.shields.io/badge/latest-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) 12[![](https://img.shields.io/badge/latest-v0.12.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0)
13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) 13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
14&bull; 14&bull;
15[![](https://img.shields.io/badge/master-v0.11.x-blue.svg)](https://github.com/shaarli/Shaarli) 15[![](https://img.shields.io/badge/master-v0.12.x-blue.svg)](https://github.com/shaarli/Shaarli)
16[![](https://img.shields.io/travis/shaarli/Shaarli.svg?label=master)](https://travis-ci.org/shaarli/Shaarli) 16[![](https://img.shields.io/travis/shaarli/Shaarli.svg?label=master)](https://travis-ci.org/shaarli/Shaarli)
17 17
18[![Join the chat at https://gitter.im/shaarli/Shaarli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/shaarli/Shaarli) 18[![Join the chat at https://gitter.im/shaarli/Shaarli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/shaarli/Shaarli)
diff --git a/application/History.php b/application/History.php
index 4fd2f294..bd5c1bf7 100644
--- a/application/History.php
+++ b/application/History.php
@@ -4,6 +4,7 @@ namespace Shaarli;
4use DateTime; 4use DateTime;
5use Exception; 5use Exception;
6use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Helper\FileUtils;
7 8
8/** 9/**
9 * Class History 10 * Class History
diff --git a/application/Languages.php b/application/Languages.php
index 5cda802e..d83e0765 100644
--- a/application/Languages.php
+++ b/application/Languages.php
@@ -179,9 +179,10 @@ class Languages
179 { 179 {
180 return [ 180 return [
181 'auto' => t('Automatic'), 181 'auto' => t('Automatic'),
182 'de' => t('German'),
182 'en' => t('English'), 183 'en' => t('English'),
183 'fr' => t('French'), 184 'fr' => t('French'),
184 'de' => t('German'), 185 'jp' => t('Japanese'),
185 ]; 186 ];
186 } 187 }
187} 188}
diff --git a/application/Router.php b/application/Router.php
deleted file mode 100644
index d7187487..00000000
--- a/application/Router.php
+++ /dev/null
@@ -1,184 +0,0 @@
1<?php
2namespace Shaarli;
3
4/**
5 * Class Router
6 *
7 * (only displayable pages here)
8 */
9class Router
10{
11 public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
12
13 public static $PAGE_LOGIN = 'login';
14
15 public static $PAGE_PICWALL = 'picwall';
16
17 public static $PAGE_TAGCLOUD = 'tagcloud';
18
19 public static $PAGE_TAGLIST = 'taglist';
20
21 public static $PAGE_DAILY = 'daily';
22
23 public static $PAGE_FEED_ATOM = 'atom';
24
25 public static $PAGE_FEED_RSS = 'rss';
26
27 public static $PAGE_TOOLS = 'tools';
28
29 public static $PAGE_CHANGEPASSWORD = 'changepasswd';
30
31 public static $PAGE_CONFIGURE = 'configure';
32
33 public static $PAGE_CHANGETAG = 'changetag';
34
35 public static $PAGE_ADDLINK = 'addlink';
36
37 public static $PAGE_EDITLINK = 'edit_link';
38
39 public static $PAGE_DELETELINK = 'delete_link';
40
41 public static $PAGE_CHANGE_VISIBILITY = 'change_visibility';
42
43 public static $PAGE_PINLINK = 'pin';
44
45 public static $PAGE_EXPORT = 'export';
46
47 public static $PAGE_IMPORT = 'import';
48
49 public static $PAGE_OPENSEARCH = 'opensearch';
50
51 public static $PAGE_LINKLIST = 'linklist';
52
53 public static $PAGE_PLUGINSADMIN = 'pluginadmin';
54
55 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
56
57 public static $PAGE_THUMBS_UPDATE = 'thumbs_update';
58
59 public static $GET_TOKEN = 'token';
60
61 /**
62 * Reproducing renderPage() if hell, to avoid regression.
63 *
64 * This highlights how bad this needs to be rewrite,
65 * but let's focus on plugins for now.
66 *
67 * @param string $query $_SERVER['QUERY_STRING'].
68 * @param array $get $_SERVER['GET'].
69 * @param bool $loggedIn true if authenticated user.
70 *
71 * @return string page found.
72 */
73 public static function findPage($query, $get, $loggedIn)
74 {
75 $loggedIn = ($loggedIn === true) ? true : false;
76
77 if (empty($query) && !isset($get['edit_link']) && !isset($get['post'])) {
78 return self::$PAGE_LINKLIST;
79 }
80
81 if (startsWith($query, 'do=' . self::$PAGE_LOGIN) && $loggedIn === false) {
82 return self::$PAGE_LOGIN;
83 }
84
85 if (startsWith($query, 'do=' . self::$PAGE_PICWALL)) {
86 return self::$PAGE_PICWALL;
87 }
88
89 if (startsWith($query, 'do=' . self::$PAGE_TAGCLOUD)) {
90 return self::$PAGE_TAGCLOUD;
91 }
92
93 if (startsWith($query, 'do=' . self::$PAGE_TAGLIST)) {
94 return self::$PAGE_TAGLIST;
95 }
96
97 if (startsWith($query, 'do=' . self::$PAGE_OPENSEARCH)) {
98 return self::$PAGE_OPENSEARCH;
99 }
100
101 if (startsWith($query, 'do=' . self::$PAGE_DAILY)) {
102 return self::$PAGE_DAILY;
103 }
104
105 if (startsWith($query, 'do=' . self::$PAGE_FEED_ATOM)) {
106 return self::$PAGE_FEED_ATOM;
107 }
108
109 if (startsWith($query, 'do=' . self::$PAGE_FEED_RSS)) {
110 return self::$PAGE_FEED_RSS;
111 }
112
113 if (startsWith($query, 'do=' . self::$PAGE_THUMBS_UPDATE)) {
114 return self::$PAGE_THUMBS_UPDATE;
115 }
116
117 if (startsWith($query, 'do=' . self::$AJAX_THUMB_UPDATE)) {
118 return self::$AJAX_THUMB_UPDATE;
119 }
120
121 // At this point, only loggedin pages.
122 if (!$loggedIn) {
123 return self::$PAGE_LINKLIST;
124 }
125
126 if (startsWith($query, 'do=' . self::$PAGE_TOOLS)) {
127 return self::$PAGE_TOOLS;
128 }
129
130 if (startsWith($query, 'do=' . self::$PAGE_CHANGEPASSWORD)) {
131 return self::$PAGE_CHANGEPASSWORD;
132 }
133
134 if (startsWith($query, 'do=' . self::$PAGE_CONFIGURE)) {
135 return self::$PAGE_CONFIGURE;
136 }
137
138 if (startsWith($query, 'do=' . self::$PAGE_CHANGETAG)) {
139 return self::$PAGE_CHANGETAG;
140 }
141
142 if (startsWith($query, 'do=' . self::$PAGE_ADDLINK)) {
143 return self::$PAGE_ADDLINK;
144 }
145
146 if (isset($get['edit_link']) || isset($get['post'])) {
147 return self::$PAGE_EDITLINK;
148 }
149
150 if (isset($get['delete_link'])) {
151 return self::$PAGE_DELETELINK;
152 }
153
154 if (isset($get[self::$PAGE_CHANGE_VISIBILITY])) {
155 return self::$PAGE_CHANGE_VISIBILITY;
156 }
157
158 if (startsWith($query, 'do=' . self::$PAGE_PINLINK)) {
159 return self::$PAGE_PINLINK;
160 }
161
162 if (startsWith($query, 'do=' . self::$PAGE_EXPORT)) {
163 return self::$PAGE_EXPORT;
164 }
165
166 if (startsWith($query, 'do=' . self::$PAGE_IMPORT)) {
167 return self::$PAGE_IMPORT;
168 }
169
170 if (startsWith($query, 'do=' . self::$PAGE_PLUGINSADMIN)) {
171 return self::$PAGE_PLUGINSADMIN;
172 }
173
174 if (startsWith($query, 'do=' . self::$PAGE_SAVE_PLUGINSADMIN)) {
175 return self::$PAGE_SAVE_PLUGINSADMIN;
176 }
177
178 if (startsWith($query, 'do=' . self::$GET_TOKEN)) {
179 return self::$GET_TOKEN;
180 }
181
182 return self::$PAGE_LINKLIST;
183 }
184}
diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php
index 314baf0d..5aec23c8 100644
--- a/application/Thumbnailer.php
+++ b/application/Thumbnailer.php
@@ -4,7 +4,6 @@ namespace Shaarli;
4 4
5use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
6use WebThumbnailer\Application\ConfigManager as WTConfigManager; 6use WebThumbnailer\Application\ConfigManager as WTConfigManager;
7use WebThumbnailer\Exception\WebThumbnailerException;
8use WebThumbnailer\WebThumbnailer; 7use WebThumbnailer\WebThumbnailer;
9 8
10/** 9/**
@@ -90,7 +89,7 @@ class Thumbnailer
90 89
91 try { 90 try {
92 return $this->wt->thumbnail($url); 91 return $this->wt->thumbnail($url);
93 } catch (WebThumbnailerException $e) { 92 } catch (\Throwable $e) {
94 // Exceptions are only thrown in debug mode. 93 // Exceptions are only thrown in debug mode.
95 error_log(get_class($e) . ': ' . $e->getMessage()); 94 error_log(get_class($e) . ': ' . $e->getMessage());
96 } 95 }
diff --git a/application/Utils.php b/application/Utils.php
index 4b7fc546..db046893 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -4,21 +4,23 @@
4 */ 4 */
5 5
6/** 6/**
7 * Logs a message to a text file 7 * Format log using provided data.
8 * 8 *
9 * The log format is compatible with fail2ban. 9 * @param string $message the message to log
10 * @param string|null $clientIp the client's remote IPv4/IPv6 address
10 * 11 *
11 * @param string $logFile where to write the logs 12 * @return string Formatted message to log
12 * @param string $clientIp the client's remote IPv4/IPv6 address
13 * @param string $message the message to log
14 */ 13 */
15function logm($logFile, $clientIp, $message) 14function format_log(string $message, string $clientIp = null): string
16{ 15{
17 file_put_contents( 16 $out = $message;
18 $logFile, 17
19 date('Y/m/d H:i:s').' - '.$clientIp.' - '.strval($message).PHP_EOL, 18 if (!empty($clientIp)) {
20 FILE_APPEND 19 // Note: we keep the first dash to avoid breaking fail2ban configs
21 ); 20 $out = '- ' . $clientIp . ' - ' . $out;
21 }
22
23 return $out;
22} 24}
23 25
24/** 26/**
@@ -87,18 +89,22 @@ function endsWith($haystack, $needle, $case = true)
87 * 89 *
88 * @param mixed $input Data to escape: a single string or an array of strings. 90 * @param mixed $input Data to escape: a single string or an array of strings.
89 * 91 *
90 * @return string escaped. 92 * @return string|array escaped.
91 */ 93 */
92function escape($input) 94function escape($input)
93{ 95{
94 if (is_bool($input)) { 96 if (null === $input) {
97 return null;
98 }
99
100 if (is_bool($input) || is_int($input) || is_float($input) || $input instanceof DateTimeInterface) {
95 return $input; 101 return $input;
96 } 102 }
97 103
98 if (is_array($input)) { 104 if (is_array($input)) {
99 $out = array(); 105 $out = array();
100 foreach ($input as $key => $value) { 106 foreach ($input as $key => $value) {
101 $out[$key] = escape($value); 107 $out[escape($key)] = escape($value);
102 } 108 }
103 return $out; 109 return $out;
104 } 110 }
@@ -294,15 +300,15 @@ function normalize_spaces($string)
294 * Requires php-intl to display international datetimes, 300 * Requires php-intl to display international datetimes,
295 * otherwise default format '%c' will be returned. 301 * otherwise default format '%c' will be returned.
296 * 302 *
297 * @param DateTime $date to format. 303 * @param DateTimeInterface $date to format.
298 * @param bool $time Displays time if true. 304 * @param bool $time Displays time if true.
299 * @param bool $intl Use international format if true. 305 * @param bool $intl Use international format if true.
300 * 306 *
301 * @return bool|string Formatted date, or false if the input is invalid. 307 * @return bool|string Formatted date, or false if the input is invalid.
302 */ 308 */
303function format_date($date, $time = true, $intl = true) 309function format_date($date, $time = true, $intl = true)
304{ 310{
305 if (! $date instanceof DateTime) { 311 if (! $date instanceof DateTimeInterface) {
306 return false; 312 return false;
307 } 313 }
308 314
@@ -321,6 +327,23 @@ function format_date($date, $time = true, $intl = true)
321} 327}
322 328
323/** 329/**
330 * Format the date month according to the locale.
331 *
332 * @param DateTimeInterface $date to format.
333 *
334 * @return bool|string Formatted date, or false if the input is invalid.
335 */
336function format_month(DateTimeInterface $date)
337{
338 if (! $date instanceof DateTimeInterface) {
339 return false;
340 }
341
342 return strftime('%B', $date->getTimestamp());
343}
344
345
346/**
324 * Check if the input is an integer, no matter its real type. 347 * Check if the input is an integer, no matter its real type.
325 * 348 *
326 * PHP is a bit messy regarding this: 349 * PHP is a bit messy regarding this:
@@ -448,14 +471,27 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
448 * Wrapper function for translation which match the API 471 * Wrapper function for translation which match the API
449 * of gettext()/_() and ngettext(). 472 * of gettext()/_() and ngettext().
450 * 473 *
451 * @param string $text Text to translate. 474 * @param string $text Text to translate.
452 * @param string $nText The plural message ID. 475 * @param string $nText The plural message ID.
453 * @param int $nb The number of items for plural forms. 476 * @param int $nb The number of items for plural forms.
454 * @param string $domain The domain where the translation is stored (default: shaarli). 477 * @param string $domain The domain where the translation is stored (default: shaarli).
478 * @param array $variables Associative array of variables to replace in translated text.
479 * @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables.
455 * 480 *
456 * @return string Text translated. 481 * @return string Text translated.
457 */ 482 */
458function t($text, $nText = '', $nb = 1, $domain = 'shaarli') 483function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false)
484{
485 $postFunction = $fixCase ? 'ucfirst' : function ($input) { return $input; };
486
487 return $postFunction(dn__($domain, $text, $nText, $nb, $variables));
488}
489
490/**
491 * Converts an exception into a printable stack trace string.
492 */
493function exception2text(Throwable $e): string
459{ 494{
460 return dn__($domain, $text, $nText, $nb); 495 return $e->getMessage() . PHP_EOL . $e->getFile() . $e->getLine() . PHP_EOL . $e->getTraceAsString();
461} 496}
497
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index 4745ac94..adc8b266 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2namespace Shaarli\Api; 2namespace Shaarli\Api;
3 3
4use malkusch\lock\mutex\FlockMutex;
4use Shaarli\Api\Exceptions\ApiAuthorizationException; 5use Shaarli\Api\Exceptions\ApiAuthorizationException;
5use Shaarli\Api\Exceptions\ApiException; 6use Shaarli\Api\Exceptions\ApiException;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
@@ -71,7 +72,14 @@ class ApiMiddleware
71 $response = $e->getApiResponse(); 72 $response = $e->getApiResponse();
72 } 73 }
73 74
74 return $response; 75 return $response
76 ->withHeader('Access-Control-Allow-Origin', '*')
77 ->withHeader(
78 'Access-Control-Allow-Headers',
79 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
80 )
81 ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
82 ;
75 } 83 }
76 84
77 /** 85 /**
@@ -100,7 +108,9 @@ class ApiMiddleware
100 */ 108 */
101 protected function checkToken($request) 109 protected function checkToken($request)
102 { 110 {
103 if (! $request->hasHeader('Authorization')) { 111 if (!$request->hasHeader('Authorization')
112 && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])
113 ) {
104 throw new ApiAuthorizationException('JWT token not provided'); 114 throw new ApiAuthorizationException('JWT token not provided');
105 } 115 }
106 116
@@ -108,7 +118,11 @@ class ApiMiddleware
108 throw new ApiAuthorizationException('Token secret must be set in Shaarli\'s administration'); 118 throw new ApiAuthorizationException('Token secret must be set in Shaarli\'s administration');
109 } 119 }
110 120
111 $authorization = $request->getHeaderLine('Authorization'); 121 if (isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])) {
122 $authorization = $this->container->environment['REDIRECT_HTTP_AUTHORIZATION'];
123 } else {
124 $authorization = $request->getHeaderLine('Authorization');
125 }
112 126
113 if (! preg_match('/^Bearer (.*)/i', $authorization, $matches)) { 127 if (! preg_match('/^Bearer (.*)/i', $authorization, $matches)) {
114 throw new ApiAuthorizationException('Invalid JWT header'); 128 throw new ApiAuthorizationException('Invalid JWT header');
@@ -130,6 +144,7 @@ class ApiMiddleware
130 $linkDb = new BookmarkFileService( 144 $linkDb = new BookmarkFileService(
131 $conf, 145 $conf,
132 $this->container->get('history'), 146 $this->container->get('history'),
147 new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
133 true 148 true
134 ); 149 );
135 $this->container['db'] = $linkDb; 150 $this->container['db'] = $linkDb;
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index 5156a5f7..eb1ca9bc 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -67,7 +67,7 @@ class ApiUtils
67 if (! $bookmark->isNote()) { 67 if (! $bookmark->isNote()) {
68 $out['url'] = $bookmark->getUrl(); 68 $out['url'] = $bookmark->getUrl();
69 } else { 69 } else {
70 $out['url'] = $indexUrl . $bookmark->getUrl(); 70 $out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/');
71 } 71 }
72 $out['shorturl'] = $bookmark->getShortUrl(); 72 $out['shorturl'] = $bookmark->getShortUrl();
73 $out['title'] = $bookmark->getTitle(); 73 $out['title'] = $bookmark->getTitle();
@@ -89,12 +89,12 @@ class ApiUtils
89 * If no URL is provided, it will generate a local note URL. 89 * If no URL is provided, it will generate a local note URL.
90 * If no title is provided, it will use the URL as title. 90 * If no title is provided, it will use the URL as title.
91 * 91 *
92 * @param array $input Request Link. 92 * @param array|null $input Request Link.
93 * @param bool $defaultPrivate Request Link. 93 * @param bool $defaultPrivate Setting defined if a bookmark is private by default.
94 * 94 *
95 * @return Bookmark instance. 95 * @return Bookmark instance.
96 */ 96 */
97 public static function buildLinkFromRequest($input, $defaultPrivate) 97 public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark
98 { 98 {
99 $bookmark = new Bookmark(); 99 $bookmark = new Bookmark();
100 $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; 100 $url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
@@ -110,6 +110,15 @@ class ApiUtils
110 $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); 110 $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
111 $bookmark->setPrivate($private); 111 $bookmark->setPrivate($private);
112 112
113 $created = \DateTime::createFromFormat(\DateTime::ATOM, $input['created'] ?? '');
114 if ($created instanceof \DateTimeInterface) {
115 $bookmark->setCreated($created);
116 }
117 $updated = \DateTime::createFromFormat(\DateTime::ATOM, $input['updated'] ?? '');
118 if ($updated instanceof \DateTimeInterface) {
119 $bookmark->setUpdated($updated);
120 }
121
113 return $bookmark; 122 return $bookmark;
114 } 123 }
115 124
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php
index c4b3d0c3..88a845eb 100644
--- a/application/api/controllers/ApiController.php
+++ b/application/api/controllers/ApiController.php
@@ -4,6 +4,7 @@ namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\BookmarkServiceInterface; 5use Shaarli\Bookmark\BookmarkServiceInterface;
6use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Shaarli\History;
7use Slim\Container; 8use Slim\Container;
8 9
9/** 10/**
@@ -31,7 +32,7 @@ abstract class ApiController
31 protected $bookmarkService; 32 protected $bookmarkService;
32 33
33 /** 34 /**
34 * @var HistoryController 35 * @var History
35 */ 36 */
36 protected $history; 37 protected $history;
37 38
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index 16fc8688..6bf529e4 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -96,11 +96,12 @@ class Links extends ApiController
96 */ 96 */
97 public function getLink($request, $response, $args) 97 public function getLink($request, $response, $args)
98 { 98 {
99 if (!$this->bookmarkService->exists($args['id'])) { 99 $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null;
100 if ($id === null || ! $this->bookmarkService->exists($id)) {
100 throw new ApiLinkNotFoundException(); 101 throw new ApiLinkNotFoundException();
101 } 102 }
102 $index = index_url($this->ci['environment']); 103 $index = index_url($this->ci['environment']);
103 $out = ApiUtils::formatLink($this->bookmarkService->get($args['id']), $index); 104 $out = ApiUtils::formatLink($this->bookmarkService->get($id), $index);
104 105
105 return $response->withJson($out, 200, $this->jsonStyle); 106 return $response->withJson($out, 200, $this->jsonStyle);
106 } 107 }
@@ -115,8 +116,8 @@ class Links extends ApiController
115 */ 116 */
116 public function postLink($request, $response) 117 public function postLink($request, $response)
117 { 118 {
118 $data = $request->getParsedBody(); 119 $data = (array) ($request->getParsedBody() ?? []);
119 $bookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); 120 $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links'));
120 // duplicate by URL, return 409 Conflict 121 // duplicate by URL, return 409 Conflict
121 if (! empty($bookmark->getUrl()) 122 if (! empty($bookmark->getUrl())
122 && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl())) 123 && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl()))
@@ -148,18 +149,19 @@ class Links extends ApiController
148 */ 149 */
149 public function putLink($request, $response, $args) 150 public function putLink($request, $response, $args)
150 { 151 {
151 if (! $this->bookmarkService->exists($args['id'])) { 152 $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null;
153 if ($id === null || !$this->bookmarkService->exists($id)) {
152 throw new ApiLinkNotFoundException(); 154 throw new ApiLinkNotFoundException();
153 } 155 }
154 156
155 $index = index_url($this->ci['environment']); 157 $index = index_url($this->ci['environment']);
156 $data = $request->getParsedBody(); 158 $data = $request->getParsedBody();
157 159
158 $requestBookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); 160 $requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links'));
159 // duplicate URL on a different link, return 409 Conflict 161 // duplicate URL on a different link, return 409 Conflict
160 if (! empty($requestBookmark->getUrl()) 162 if (! empty($requestBookmark->getUrl())
161 && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) 163 && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl()))
162 && $dup->getId() != $args['id'] 164 && $dup->getId() != $id
163 ) { 165 ) {
164 return $response->withJson( 166 return $response->withJson(
165 ApiUtils::formatLink($dup, $index), 167 ApiUtils::formatLink($dup, $index),
@@ -168,7 +170,7 @@ class Links extends ApiController
168 ); 170 );
169 } 171 }
170 172
171 $responseBookmark = $this->bookmarkService->get($args['id']); 173 $responseBookmark = $this->bookmarkService->get($id);
172 $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark); 174 $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark);
173 $this->bookmarkService->set($responseBookmark); 175 $this->bookmarkService->set($responseBookmark);
174 176
@@ -189,10 +191,11 @@ class Links extends ApiController
189 */ 191 */
190 public function deleteLink($request, $response, $args) 192 public function deleteLink($request, $response, $args)
191 { 193 {
192 if (! $this->bookmarkService->exists($args['id'])) { 194 $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null;
195 if ($id === null || !$this->bookmarkService->exists($id)) {
193 throw new ApiLinkNotFoundException(); 196 throw new ApiLinkNotFoundException();
194 } 197 }
195 $bookmark = $this->bookmarkService->get($args['id']); 198 $bookmark = $this->bookmarkService->get($id);
196 $this->bookmarkService->remove($bookmark); 199 $this->bookmarkService->remove($bookmark);
197 200
198 return $response->withStatus(204); 201 return $response->withStatus(204);
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php
index f9b21d3d..4810c5e6 100644
--- a/application/bookmark/Bookmark.php
+++ b/application/bookmark/Bookmark.php
@@ -1,8 +1,11 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
5use DateTime; 7use DateTime;
8use DateTimeInterface;
6use Shaarli\Bookmark\Exception\InvalidBookmarkException; 9use Shaarli\Bookmark\Exception\InvalidBookmarkException;
7 10
8/** 11/**
@@ -36,21 +39,24 @@ class Bookmark
36 /** @var array List of bookmark's tags */ 39 /** @var array List of bookmark's tags */
37 protected $tags; 40 protected $tags;
38 41
39 /** @var string Thumbnail's URL - false if no thumbnail could be found */ 42 /** @var string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */
40 protected $thumbnail; 43 protected $thumbnail;
41 44
42 /** @var bool Set to true if the bookmark is set as sticky */ 45 /** @var bool Set to true if the bookmark is set as sticky */
43 protected $sticky; 46 protected $sticky;
44 47
45 /** @var DateTime Creation datetime */ 48 /** @var DateTimeInterface Creation datetime */
46 protected $created; 49 protected $created;
47 50
48 /** @var DateTime Update datetime */ 51 /** @var DateTimeInterface datetime */
49 protected $updated; 52 protected $updated;
50 53
51 /** @var bool True if the bookmark can only be seen while logged in */ 54 /** @var bool True if the bookmark can only be seen while logged in */
52 protected $private; 55 protected $private;
53 56
57 /** @var mixed[] Available to store any additional content for a bookmark. Currently used for search highlight. */
58 protected $additionalContent = [];
59
54 /** 60 /**
55 * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. 61 * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
56 * 62 *
@@ -58,25 +64,25 @@ class Bookmark
58 * 64 *
59 * @return $this 65 * @return $this
60 */ 66 */
61 public function fromArray($data) 67 public function fromArray(array $data): Bookmark
62 { 68 {
63 $this->id = $data['id']; 69 $this->id = $data['id'] ?? null;
64 $this->shortUrl = $data['shorturl']; 70 $this->shortUrl = $data['shorturl'] ?? null;
65 $this->url = $data['url']; 71 $this->url = $data['url'] ?? null;
66 $this->title = $data['title']; 72 $this->title = $data['title'] ?? null;
67 $this->description = $data['description']; 73 $this->description = $data['description'] ?? null;
68 $this->thumbnail = isset($data['thumbnail']) ? $data['thumbnail'] : null; 74 $this->thumbnail = $data['thumbnail'] ?? null;
69 $this->sticky = isset($data['sticky']) ? $data['sticky'] : false; 75 $this->sticky = $data['sticky'] ?? false;
70 $this->created = $data['created']; 76 $this->created = $data['created'] ?? null;
71 if (is_array($data['tags'])) { 77 if (is_array($data['tags'])) {
72 $this->tags = $data['tags']; 78 $this->tags = $data['tags'];
73 } else { 79 } else {
74 $this->tags = preg_split('/\s+/', $data['tags'], -1, PREG_SPLIT_NO_EMPTY); 80 $this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY);
75 } 81 }
76 if (! empty($data['updated'])) { 82 if (! empty($data['updated'])) {
77 $this->updated = $data['updated']; 83 $this->updated = $data['updated'];
78 } 84 }
79 $this->private = $data['private'] ? true : false; 85 $this->private = ($data['private'] ?? false) ? true : false;
80 86
81 return $this; 87 return $this;
82 } 88 }
@@ -92,24 +98,28 @@ class Bookmark
92 * - the URL with the permalink 98 * - the URL with the permalink
93 * - the title with the URL 99 * - the title with the URL
94 * 100 *
101 * Also make sure that we do not save search highlights in the datastore.
102 *
95 * @throws InvalidBookmarkException 103 * @throws InvalidBookmarkException
96 */ 104 */
97 public function validate() 105 public function validate(): void
98 { 106 {
99 if ($this->id === null 107 if ($this->id === null
100 || ! is_int($this->id) 108 || ! is_int($this->id)
101 || empty($this->shortUrl) 109 || empty($this->shortUrl)
102 || empty($this->created) 110 || empty($this->created)
103 || ! $this->created instanceof DateTime
104 ) { 111 ) {
105 throw new InvalidBookmarkException($this); 112 throw new InvalidBookmarkException($this);
106 } 113 }
107 if (empty($this->url)) { 114 if (empty($this->url)) {
108 $this->url = '?'. $this->shortUrl; 115 $this->url = '/shaare/'. $this->shortUrl;
109 } 116 }
110 if (empty($this->title)) { 117 if (empty($this->title)) {
111 $this->title = $this->url; 118 $this->title = $this->url;
112 } 119 }
120 if (array_key_exists('search_highlight', $this->additionalContent)) {
121 unset($this->additionalContent['search_highlight']);
122 }
113 } 123 }
114 124
115 /** 125 /**
@@ -118,11 +128,11 @@ class Bookmark
118 * - created: with the current datetime 128 * - created: with the current datetime
119 * - shortUrl: with a generated small hash from the date and the given ID 129 * - shortUrl: with a generated small hash from the date and the given ID
120 * 130 *
121 * @param int $id 131 * @param int|null $id
122 * 132 *
123 * @return Bookmark 133 * @return Bookmark
124 */ 134 */
125 public function setId($id) 135 public function setId(?int $id): Bookmark
126 { 136 {
127 $this->id = $id; 137 $this->id = $id;
128 if (empty($this->created)) { 138 if (empty($this->created)) {
@@ -138,9 +148,9 @@ class Bookmark
138 /** 148 /**
139 * Get the Id. 149 * Get the Id.
140 * 150 *
141 * @return int 151 * @return int|null
142 */ 152 */
143 public function getId() 153 public function getId(): ?int
144 { 154 {
145 return $this->id; 155 return $this->id;
146 } 156 }
@@ -148,9 +158,9 @@ class Bookmark
148 /** 158 /**
149 * Get the ShortUrl. 159 * Get the ShortUrl.
150 * 160 *
151 * @return string 161 * @return string|null
152 */ 162 */
153 public function getShortUrl() 163 public function getShortUrl(): ?string
154 { 164 {
155 return $this->shortUrl; 165 return $this->shortUrl;
156 } 166 }
@@ -158,9 +168,9 @@ class Bookmark
158 /** 168 /**
159 * Get the Url. 169 * Get the Url.
160 * 170 *
161 * @return string 171 * @return string|null
162 */ 172 */
163 public function getUrl() 173 public function getUrl(): ?string
164 { 174 {
165 return $this->url; 175 return $this->url;
166 } 176 }
@@ -170,7 +180,7 @@ class Bookmark
170 * 180 *
171 * @return string 181 * @return string
172 */ 182 */
173 public function getTitle() 183 public function getTitle(): ?string
174 { 184 {
175 return $this->title; 185 return $this->title;
176 } 186 }
@@ -180,7 +190,7 @@ class Bookmark
180 * 190 *
181 * @return string 191 * @return string
182 */ 192 */
183 public function getDescription() 193 public function getDescription(): string
184 { 194 {
185 return ! empty($this->description) ? $this->description : ''; 195 return ! empty($this->description) ? $this->description : '';
186 } 196 }
@@ -188,9 +198,9 @@ class Bookmark
188 /** 198 /**
189 * Get the Created. 199 * Get the Created.
190 * 200 *
191 * @return DateTime 201 * @return DateTimeInterface
192 */ 202 */
193 public function getCreated() 203 public function getCreated(): ?DateTimeInterface
194 { 204 {
195 return $this->created; 205 return $this->created;
196 } 206 }
@@ -198,9 +208,9 @@ class Bookmark
198 /** 208 /**
199 * Get the Updated. 209 * Get the Updated.
200 * 210 *
201 * @return DateTime 211 * @return DateTimeInterface
202 */ 212 */
203 public function getUpdated() 213 public function getUpdated(): ?DateTimeInterface
204 { 214 {
205 return $this->updated; 215 return $this->updated;
206 } 216 }
@@ -208,11 +218,11 @@ class Bookmark
208 /** 218 /**
209 * Set the ShortUrl. 219 * Set the ShortUrl.
210 * 220 *
211 * @param string $shortUrl 221 * @param string|null $shortUrl
212 * 222 *
213 * @return Bookmark 223 * @return Bookmark
214 */ 224 */
215 public function setShortUrl($shortUrl) 225 public function setShortUrl(?string $shortUrl): Bookmark
216 { 226 {
217 $this->shortUrl = $shortUrl; 227 $this->shortUrl = $shortUrl;
218 228
@@ -222,14 +232,14 @@ class Bookmark
222 /** 232 /**
223 * Set the Url. 233 * Set the Url.
224 * 234 *
225 * @param string $url 235 * @param string|null $url
226 * @param array $allowedProtocols 236 * @param string[] $allowedProtocols
227 * 237 *
228 * @return Bookmark 238 * @return Bookmark
229 */ 239 */
230 public function setUrl($url, $allowedProtocols = []) 240 public function setUrl(?string $url, array $allowedProtocols = []): Bookmark
231 { 241 {
232 $url = trim($url); 242 $url = $url !== null ? trim($url) : '';
233 if (! empty($url)) { 243 if (! empty($url)) {
234 $url = whitelist_protocols($url, $allowedProtocols); 244 $url = whitelist_protocols($url, $allowedProtocols);
235 } 245 }
@@ -241,13 +251,13 @@ class Bookmark
241 /** 251 /**
242 * Set the Title. 252 * Set the Title.
243 * 253 *
244 * @param string $title 254 * @param string|null $title
245 * 255 *
246 * @return Bookmark 256 * @return Bookmark
247 */ 257 */
248 public function setTitle($title) 258 public function setTitle(?string $title): Bookmark
249 { 259 {
250 $this->title = trim($title); 260 $this->title = $title !== null ? trim($title) : '';
251 261
252 return $this; 262 return $this;
253 } 263 }
@@ -255,11 +265,11 @@ class Bookmark
255 /** 265 /**
256 * Set the Description. 266 * Set the Description.
257 * 267 *
258 * @param string $description 268 * @param string|null $description
259 * 269 *
260 * @return Bookmark 270 * @return Bookmark
261 */ 271 */
262 public function setDescription($description) 272 public function setDescription(?string $description): Bookmark
263 { 273 {
264 $this->description = $description; 274 $this->description = $description;
265 275
@@ -270,11 +280,11 @@ class Bookmark
270 * Set the Created. 280 * Set the Created.
271 * Note: you shouldn't set this manually except for special cases (like bookmark import) 281 * Note: you shouldn't set this manually except for special cases (like bookmark import)
272 * 282 *
273 * @param DateTime $created 283 * @param DateTimeInterface|null $created
274 * 284 *
275 * @return Bookmark 285 * @return Bookmark
276 */ 286 */
277 public function setCreated($created) 287 public function setCreated(?DateTimeInterface $created): Bookmark
278 { 288 {
279 $this->created = $created; 289 $this->created = $created;
280 290
@@ -284,11 +294,11 @@ class Bookmark
284 /** 294 /**
285 * Set the Updated. 295 * Set the Updated.
286 * 296 *
287 * @param DateTime $updated 297 * @param DateTimeInterface|null $updated
288 * 298 *
289 * @return Bookmark 299 * @return Bookmark
290 */ 300 */
291 public function setUpdated($updated) 301 public function setUpdated(?DateTimeInterface $updated): Bookmark
292 { 302 {
293 $this->updated = $updated; 303 $this->updated = $updated;
294 304
@@ -300,7 +310,7 @@ class Bookmark
300 * 310 *
301 * @return bool 311 * @return bool
302 */ 312 */
303 public function isPrivate() 313 public function isPrivate(): bool
304 { 314 {
305 return $this->private ? true : false; 315 return $this->private ? true : false;
306 } 316 }
@@ -308,11 +318,11 @@ class Bookmark
308 /** 318 /**
309 * Set the Private. 319 * Set the Private.
310 * 320 *
311 * @param bool $private 321 * @param bool|null $private
312 * 322 *
313 * @return Bookmark 323 * @return Bookmark
314 */ 324 */
315 public function setPrivate($private) 325 public function setPrivate(?bool $private): Bookmark
316 { 326 {
317 $this->private = $private ? true : false; 327 $this->private = $private ? true : false;
318 328
@@ -322,9 +332,9 @@ class Bookmark
322 /** 332 /**
323 * Get the Tags. 333 * Get the Tags.
324 * 334 *
325 * @return array 335 * @return string[]
326 */ 336 */
327 public function getTags() 337 public function getTags(): array
328 { 338 {
329 return is_array($this->tags) ? $this->tags : []; 339 return is_array($this->tags) ? $this->tags : [];
330 } 340 }
@@ -332,13 +342,13 @@ class Bookmark
332 /** 342 /**
333 * Set the Tags. 343 * Set the Tags.
334 * 344 *
335 * @param array $tags 345 * @param string[]|null $tags
336 * 346 *
337 * @return Bookmark 347 * @return Bookmark
338 */ 348 */
339 public function setTags($tags) 349 public function setTags(?array $tags): Bookmark
340 { 350 {
341 $this->setTagsString(implode(' ', $tags)); 351 $this->setTagsString(implode(' ', $tags ?? []));
342 352
343 return $this; 353 return $this;
344 } 354 }
@@ -346,7 +356,7 @@ class Bookmark
346 /** 356 /**
347 * Get the Thumbnail. 357 * Get the Thumbnail.
348 * 358 *
349 * @return string|bool 359 * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found
350 */ 360 */
351 public function getThumbnail() 361 public function getThumbnail()
352 { 362 {
@@ -356,11 +366,11 @@ class Bookmark
356 /** 366 /**
357 * Set the Thumbnail. 367 * Set the Thumbnail.
358 * 368 *
359 * @param string|bool $thumbnail 369 * @param string|bool|null $thumbnail Thumbnail's URL - false if no thumbnail could be found
360 * 370 *
361 * @return Bookmark 371 * @return Bookmark
362 */ 372 */
363 public function setThumbnail($thumbnail) 373 public function setThumbnail($thumbnail): Bookmark
364 { 374 {
365 $this->thumbnail = $thumbnail; 375 $this->thumbnail = $thumbnail;
366 376
@@ -368,11 +378,29 @@ class Bookmark
368 } 378 }
369 379
370 /** 380 /**
381 * Return true if:
382 * - the bookmark's thumbnail is not already set to false (= not found)
383 * - it's not a note
384 * - it's an HTTP(S) link
385 * - the thumbnail has not yet be retrieved (null) or its associated cache file doesn't exist anymore
386 *
387 * @return bool True if the bookmark's thumbnail needs to be retrieved.
388 */
389 public function shouldUpdateThumbnail(): bool
390 {
391 return $this->thumbnail !== false
392 && !$this->isNote()
393 && startsWith(strtolower($this->url), 'http')
394 && (null === $this->thumbnail || !is_file($this->thumbnail))
395 ;
396 }
397
398 /**
371 * Get the Sticky. 399 * Get the Sticky.
372 * 400 *
373 * @return bool 401 * @return bool
374 */ 402 */
375 public function isSticky() 403 public function isSticky(): bool
376 { 404 {
377 return $this->sticky ? true : false; 405 return $this->sticky ? true : false;
378 } 406 }
@@ -380,11 +408,11 @@ class Bookmark
380 /** 408 /**
381 * Set the Sticky. 409 * Set the Sticky.
382 * 410 *
383 * @param bool $sticky 411 * @param bool|null $sticky
384 * 412 *
385 * @return Bookmark 413 * @return Bookmark
386 */ 414 */
387 public function setSticky($sticky) 415 public function setSticky(?bool $sticky): Bookmark
388 { 416 {
389 $this->sticky = $sticky ? true : false; 417 $this->sticky = $sticky ? true : false;
390 418
@@ -394,7 +422,7 @@ class Bookmark
394 /** 422 /**
395 * @return string Bookmark's tags as a string, separated by a space 423 * @return string Bookmark's tags as a string, separated by a space
396 */ 424 */
397 public function getTagsString() 425 public function getTagsString(): string
398 { 426 {
399 return implode(' ', $this->getTags()); 427 return implode(' ', $this->getTags());
400 } 428 }
@@ -402,10 +430,10 @@ class Bookmark
402 /** 430 /**
403 * @return bool 431 * @return bool
404 */ 432 */
405 public function isNote() 433 public function isNote(): bool
406 { 434 {
407 // We check empty value to get a valid result if the link has not been saved yet 435 // We check empty value to get a valid result if the link has not been saved yet
408 return empty($this->url) || $this->url[0] === '?'; 436 return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
409 } 437 }
410 438
411 /** 439 /**
@@ -415,14 +443,14 @@ class Bookmark
415 * - multiple spaces will be removed 443 * - multiple spaces will be removed
416 * - trailing dash in tags will be removed 444 * - trailing dash in tags will be removed
417 * 445 *
418 * @param string $tags 446 * @param string|null $tags
419 * 447 *
420 * @return $this 448 * @return $this
421 */ 449 */
422 public function setTagsString($tags) 450 public function setTagsString(?string $tags): Bookmark
423 { 451 {
424 // Remove first '-' char in tags. 452 // Remove first '-' char in tags.
425 $tags = preg_replace('/(^| )\-/', '$1', $tags); 453 $tags = preg_replace('/(^| )\-/', '$1', $tags ?? '');
426 // Explode all tags separted by spaces or commas 454 // Explode all tags separted by spaces or commas
427 $tags = preg_split('/[\s,]+/', $tags); 455 $tags = preg_split('/[\s,]+/', $tags);
428 // Remove eventual empty values 456 // Remove eventual empty values
@@ -434,12 +462,50 @@ class Bookmark
434 } 462 }
435 463
436 /** 464 /**
465 * Get entire additionalContent array.
466 *
467 * @return mixed[]
468 */
469 public function getAdditionalContent(): array
470 {
471 return $this->additionalContent;
472 }
473
474 /**
475 * Set a single entry in additionalContent, by key.
476 *
477 * @param string $key
478 * @param mixed|null $value Any type of value can be set.
479 *
480 * @return $this
481 */
482 public function addAdditionalContentEntry(string $key, $value): self
483 {
484 $this->additionalContent[$key] = $value;
485
486 return $this;
487 }
488
489 /**
490 * Get a single entry in additionalContent, by key.
491 *
492 * @param string $key
493 * @param mixed|null $default
494 *
495 * @return mixed|null can be any type or even null.
496 */
497 public function getAdditionalContentEntry(string $key, $default = null)
498 {
499 return array_key_exists($key, $this->additionalContent) ? $this->additionalContent[$key] : $default;
500 }
501
502 /**
437 * Rename a tag in tags list. 503 * Rename a tag in tags list.
438 * 504 *
439 * @param string $fromTag 505 * @param string $fromTag
440 * @param string $toTag 506 * @param string $toTag
441 */ 507 */
442 public function renameTag($fromTag, $toTag) 508 public function renameTag(string $fromTag, string $toTag): void
443 { 509 {
444 if (($pos = array_search($fromTag, $this->tags)) !== false) { 510 if (($pos = array_search($fromTag, $this->tags)) !== false) {
445 $this->tags[$pos] = trim($toTag); 511 $this->tags[$pos] = trim($toTag);
@@ -451,7 +517,7 @@ class Bookmark
451 * 517 *
452 * @param string $tag 518 * @param string $tag
453 */ 519 */
454 public function deleteTag($tag) 520 public function deleteTag(string $tag): void
455 { 521 {
456 if (($pos = array_search($tag, $this->tags)) !== false) { 522 if (($pos = array_search($tag, $this->tags)) !== false) {
457 unset($this->tags[$pos]); 523 unset($this->tags[$pos]);
diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php
index d87d43b4..67bb3b73 100644
--- a/application/bookmark/BookmarkArray.php
+++ b/application/bookmark/BookmarkArray.php
@@ -1,5 +1,7 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
5use Shaarli\Bookmark\Exception\InvalidBookmarkException; 7use Shaarli\Bookmark\Exception\InvalidBookmarkException;
@@ -187,13 +189,13 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
187 /** 189 /**
188 * Returns a bookmark offset in bookmarks array from its unique ID. 190 * Returns a bookmark offset in bookmarks array from its unique ID.
189 * 191 *
190 * @param int $id Persistent ID of a bookmark. 192 * @param int|null $id Persistent ID of a bookmark.
191 * 193 *
192 * @return int Real offset in local array, or null if doesn't exist. 194 * @return int Real offset in local array, or null if doesn't exist.
193 */ 195 */
194 protected function getBookmarkOffset($id) 196 protected function getBookmarkOffset(?int $id): ?int
195 { 197 {
196 if (isset($this->ids[$id])) { 198 if ($id !== null && isset($this->ids[$id])) {
197 return $this->ids[$id]; 199 return $this->ids[$id];
198 } 200 }
199 return null; 201 return null;
@@ -205,7 +207,7 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
205 * 207 *
206 * @return int next ID. 208 * @return int next ID.
207 */ 209 */
208 public function getNextId() 210 public function getNextId(): int
209 { 211 {
210 if (!empty($this->ids)) { 212 if (!empty($this->ids)) {
211 return max(array_keys($this->ids)) + 1; 213 return max(array_keys($this->ids)) + 1;
@@ -214,11 +216,11 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
214 } 216 }
215 217
216 /** 218 /**
217 * @param $url 219 * @param string $url
218 * 220 *
219 * @return Bookmark|null 221 * @return Bookmark|null
220 */ 222 */
221 public function getByUrl($url) 223 public function getByUrl(string $url): ?Bookmark
222 { 224 {
223 if (! empty($url) 225 if (! empty($url)
224 && isset($this->urls[$url]) 226 && isset($this->urls[$url])
@@ -234,16 +236,17 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
234 * 236 *
235 * Also update the urls and ids mapping arrays. 237 * Also update the urls and ids mapping arrays.
236 * 238 *
237 * @param string $order ASC|DESC 239 * @param string $order ASC|DESC
240 * @param bool $ignoreSticky If set to true, sticky bookmarks won't be first
238 */ 241 */
239 public function reorder($order = 'DESC') 242 public function reorder(string $order = 'DESC', bool $ignoreSticky = false): void
240 { 243 {
241 $order = $order === 'ASC' ? -1 : 1; 244 $order = $order === 'ASC' ? -1 : 1;
242 // Reorder array by dates. 245 // Reorder array by dates.
243 usort($this->bookmarks, function ($a, $b) use ($order) { 246 usort($this->bookmarks, function ($a, $b) use ($order, $ignoreSticky) {
244 /** @var $a Bookmark */ 247 /** @var $a Bookmark */
245 /** @var $b Bookmark */ 248 /** @var $b Bookmark */
246 if ($a->isSticky() !== $b->isSticky()) { 249 if (false === $ignoreSticky && $a->isSticky() !== $b->isSticky()) {
247 return $a->isSticky() ? -1 : 1; 250 return $a->isSticky() ? -1 : 1;
248 } 251 }
249 return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order; 252 return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order;
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
index 9c59e139..3ea98a45 100644
--- a/application/bookmark/BookmarkFileService.php
+++ b/application/bookmark/BookmarkFileService.php
@@ -1,17 +1,21 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
3 4
4namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
5 6
6 7use DateTime;
7use Exception; 8use Exception;
9use malkusch\lock\mutex\Mutex;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 10use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
11use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
9use Shaarli\Bookmark\Exception\EmptyDataStoreException; 12use Shaarli\Bookmark\Exception\EmptyDataStoreException;
10use Shaarli\Config\ConfigManager; 13use Shaarli\Config\ConfigManager;
11use Shaarli\Formatter\BookmarkMarkdownFormatter; 14use Shaarli\Formatter\BookmarkMarkdownFormatter;
12use Shaarli\History; 15use Shaarli\History;
13use Shaarli\Legacy\LegacyLinkDB; 16use Shaarli\Legacy\LegacyLinkDB;
14use Shaarli\Legacy\LegacyUpdater; 17use Shaarli\Legacy\LegacyUpdater;
18use Shaarli\Render\PageCacheManager;
15use Shaarli\Updater\UpdaterUtils; 19use Shaarli\Updater\UpdaterUtils;
16 20
17/** 21/**
@@ -39,17 +43,25 @@ class BookmarkFileService implements BookmarkServiceInterface
39 /** @var History instance */ 43 /** @var History instance */
40 protected $history; 44 protected $history;
41 45
46 /** @var PageCacheManager instance */
47 protected $pageCacheManager;
48
42 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ 49 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
43 protected $isLoggedIn; 50 protected $isLoggedIn;
44 51
52 /** @var Mutex */
53 protected $mutex;
54
45 /** 55 /**
46 * @inheritDoc 56 * @inheritDoc
47 */ 57 */
48 public function __construct(ConfigManager $conf, History $history, $isLoggedIn) 58 public function __construct(ConfigManager $conf, History $history, Mutex $mutex, bool $isLoggedIn)
49 { 59 {
50 $this->conf = $conf; 60 $this->conf = $conf;
51 $this->history = $history; 61 $this->history = $history;
52 $this->bookmarksIO = new BookmarkIO($this->conf); 62 $this->mutex = $mutex;
63 $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
64 $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex);
53 $this->isLoggedIn = $isLoggedIn; 65 $this->isLoggedIn = $isLoggedIn;
54 66
55 if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { 67 if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
@@ -57,10 +69,16 @@ class BookmarkFileService implements BookmarkServiceInterface
57 } else { 69 } else {
58 try { 70 try {
59 $this->bookmarks = $this->bookmarksIO->read(); 71 $this->bookmarks = $this->bookmarksIO->read();
60 } catch (EmptyDataStoreException $e) { 72 } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
61 $this->bookmarks = new BookmarkArray(); 73 $this->bookmarks = new BookmarkArray();
62 if ($isLoggedIn) { 74
63 $this->save(); 75 if ($this->isLoggedIn) {
76 // Datastore file does not exists, we initialize it with default bookmarks.
77 if ($e instanceof DatastoreNotInitializedException) {
78 $this->initialize();
79 } else {
80 $this->save();
81 }
64 } 82 }
65 } 83 }
66 84
@@ -79,22 +97,25 @@ class BookmarkFileService implements BookmarkServiceInterface
79 /** 97 /**
80 * @inheritDoc 98 * @inheritDoc
81 */ 99 */
82 public function findByHash($hash) 100 public function findByHash(string $hash, string $privateKey = null): Bookmark
83 { 101 {
84 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); 102 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
85 // PHP 7.3 introduced array_key_first() to avoid this hack 103 // PHP 7.3 introduced array_key_first() to avoid this hack
86 $first = reset($bookmark); 104 $first = reset($bookmark);
87 if (! $this->isLoggedIn && $first->isPrivate()) { 105 if (!$this->isLoggedIn
88 throw new Exception('Not authorized'); 106 && $first->isPrivate()
107 && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
108 ) {
109 throw new BookmarkNotFoundException();
89 } 110 }
90 111
91 return $bookmark; 112 return $first;
92 } 113 }
93 114
94 /** 115 /**
95 * @inheritDoc 116 * @inheritDoc
96 */ 117 */
97 public function findByUrl($url) 118 public function findByUrl(string $url): ?Bookmark
98 { 119 {
99 return $this->bookmarks->getByUrl($url); 120 return $this->bookmarks->getByUrl($url);
100 } 121 }
@@ -102,19 +123,28 @@ class BookmarkFileService implements BookmarkServiceInterface
102 /** 123 /**
103 * @inheritDoc 124 * @inheritDoc
104 */ 125 */
105 public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false) 126 public function search(
106 { 127 array $request = [],
128 string $visibility = null,
129 bool $caseSensitive = false,
130 bool $untaggedOnly = false,
131 bool $ignoreSticky = false
132 ) {
107 if ($visibility === null) { 133 if ($visibility === null) {
108 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; 134 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
109 } 135 }
110 136
111 // Filter bookmark database according to parameters. 137 // Filter bookmark database according to parameters.
112 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : ''; 138 $searchTags = isset($request['searchtags']) ? $request['searchtags'] : '';
113 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : ''; 139 $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : '';
140
141 if ($ignoreSticky) {
142 $this->bookmarks->reorder('DESC', true);
143 }
114 144
115 return $this->bookmarkFilter->filter( 145 return $this->bookmarkFilter->filter(
116 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, 146 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
117 [$searchtags, $searchterm], 147 [$searchTags, $searchTerm],
118 $caseSensitive, 148 $caseSensitive,
119 $visibility, 149 $visibility,
120 $untaggedOnly 150 $untaggedOnly
@@ -124,7 +154,7 @@ class BookmarkFileService implements BookmarkServiceInterface
124 /** 154 /**
125 * @inheritDoc 155 * @inheritDoc
126 */ 156 */
127 public function get($id, $visibility = null) 157 public function get(int $id, string $visibility = null): Bookmark
128 { 158 {
129 if (! isset($this->bookmarks[$id])) { 159 if (! isset($this->bookmarks[$id])) {
130 throw new BookmarkNotFoundException(); 160 throw new BookmarkNotFoundException();
@@ -147,20 +177,17 @@ class BookmarkFileService implements BookmarkServiceInterface
147 /** 177 /**
148 * @inheritDoc 178 * @inheritDoc
149 */ 179 */
150 public function set($bookmark, $save = true) 180 public function set(Bookmark $bookmark, bool $save = true): Bookmark
151 { 181 {
152 if ($this->isLoggedIn !== true) { 182 if (true !== $this->isLoggedIn) {
153 throw new Exception(t('You\'re not authorized to alter the datastore')); 183 throw new Exception(t('You\'re not authorized to alter the datastore'));
154 } 184 }
155 if (! $bookmark instanceof Bookmark) {
156 throw new Exception(t('Provided data is invalid'));
157 }
158 if (! isset($this->bookmarks[$bookmark->getId()])) { 185 if (! isset($this->bookmarks[$bookmark->getId()])) {
159 throw new BookmarkNotFoundException(); 186 throw new BookmarkNotFoundException();
160 } 187 }
161 $bookmark->validate(); 188 $bookmark->validate();
162 189
163 $bookmark->setUpdated(new \DateTime()); 190 $bookmark->setUpdated(new DateTime());
164 $this->bookmarks[$bookmark->getId()] = $bookmark; 191 $this->bookmarks[$bookmark->getId()] = $bookmark;
165 if ($save === true) { 192 if ($save === true) {
166 $this->save(); 193 $this->save();
@@ -172,15 +199,12 @@ class BookmarkFileService implements BookmarkServiceInterface
172 /** 199 /**
173 * @inheritDoc 200 * @inheritDoc
174 */ 201 */
175 public function add($bookmark, $save = true) 202 public function add(Bookmark $bookmark, bool $save = true): Bookmark
176 { 203 {
177 if ($this->isLoggedIn !== true) { 204 if (true !== $this->isLoggedIn) {
178 throw new Exception(t('You\'re not authorized to alter the datastore')); 205 throw new Exception(t('You\'re not authorized to alter the datastore'));
179 } 206 }
180 if (! $bookmark instanceof Bookmark) { 207 if (!empty($bookmark->getId())) {
181 throw new Exception(t('Provided data is invalid'));
182 }
183 if (! empty($bookmark->getId())) {
184 throw new Exception(t('This bookmarks already exists')); 208 throw new Exception(t('This bookmarks already exists'));
185 } 209 }
186 $bookmark->setId($this->bookmarks->getNextId()); 210 $bookmark->setId($this->bookmarks->getNextId());
@@ -197,14 +221,11 @@ class BookmarkFileService implements BookmarkServiceInterface
197 /** 221 /**
198 * @inheritDoc 222 * @inheritDoc
199 */ 223 */
200 public function addOrSet($bookmark, $save = true) 224 public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark
201 { 225 {
202 if ($this->isLoggedIn !== true) { 226 if (true !== $this->isLoggedIn) {
203 throw new Exception(t('You\'re not authorized to alter the datastore')); 227 throw new Exception(t('You\'re not authorized to alter the datastore'));
204 } 228 }
205 if (! $bookmark instanceof Bookmark) {
206 throw new Exception('Provided data is invalid');
207 }
208 if ($bookmark->getId() === null) { 229 if ($bookmark->getId() === null) {
209 return $this->add($bookmark, $save); 230 return $this->add($bookmark, $save);
210 } 231 }
@@ -214,14 +235,11 @@ class BookmarkFileService implements BookmarkServiceInterface
214 /** 235 /**
215 * @inheritDoc 236 * @inheritDoc
216 */ 237 */
217 public function remove($bookmark, $save = true) 238 public function remove(Bookmark $bookmark, bool $save = true): void
218 { 239 {
219 if ($this->isLoggedIn !== true) { 240 if (true !== $this->isLoggedIn) {
220 throw new Exception(t('You\'re not authorized to alter the datastore')); 241 throw new Exception(t('You\'re not authorized to alter the datastore'));
221 } 242 }
222 if (! $bookmark instanceof Bookmark) {
223 throw new Exception(t('Provided data is invalid'));
224 }
225 if (! isset($this->bookmarks[$bookmark->getId()])) { 243 if (! isset($this->bookmarks[$bookmark->getId()])) {
226 throw new BookmarkNotFoundException(); 244 throw new BookmarkNotFoundException();
227 } 245 }
@@ -236,7 +254,7 @@ class BookmarkFileService implements BookmarkServiceInterface
236 /** 254 /**
237 * @inheritDoc 255 * @inheritDoc
238 */ 256 */
239 public function exists($id, $visibility = null) 257 public function exists(int $id, string $visibility = null): bool
240 { 258 {
241 if (! isset($this->bookmarks[$id])) { 259 if (! isset($this->bookmarks[$id])) {
242 return false; 260 return false;
@@ -259,7 +277,7 @@ class BookmarkFileService implements BookmarkServiceInterface
259 /** 277 /**
260 * @inheritDoc 278 * @inheritDoc
261 */ 279 */
262 public function count($visibility = null) 280 public function count(string $visibility = null): int
263 { 281 {
264 return count($this->search([], $visibility)); 282 return count($this->search([], $visibility));
265 } 283 }
@@ -267,21 +285,22 @@ class BookmarkFileService implements BookmarkServiceInterface
267 /** 285 /**
268 * @inheritDoc 286 * @inheritDoc
269 */ 287 */
270 public function save() 288 public function save(): void
271 { 289 {
272 if (!$this->isLoggedIn) { 290 if (true !== $this->isLoggedIn) {
273 // TODO: raise an Exception instead 291 // TODO: raise an Exception instead
274 die('You are not authorized to change the database.'); 292 die('You are not authorized to change the database.');
275 } 293 }
294
276 $this->bookmarks->reorder(); 295 $this->bookmarks->reorder();
277 $this->bookmarksIO->write($this->bookmarks); 296 $this->bookmarksIO->write($this->bookmarks);
278 invalidateCaches($this->conf->get('resource.page_cache')); 297 $this->pageCacheManager->invalidateCaches();
279 } 298 }
280 299
281 /** 300 /**
282 * @inheritDoc 301 * @inheritDoc
283 */ 302 */
284 public function bookmarksCountPerTag($filteringTags = [], $visibility = null) 303 public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
285 { 304 {
286 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); 305 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
287 $tags = []; 306 $tags = [];
@@ -291,6 +310,7 @@ class BookmarkFileService implements BookmarkServiceInterface
291 if (empty($tag) 310 if (empty($tag)
292 || (! $this->isLoggedIn && startsWith($tag, '.')) 311 || (! $this->isLoggedIn && startsWith($tag, '.'))
293 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG 312 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
313 || in_array($tag, $filteringTags, true)
294 ) { 314 ) {
295 continue; 315 continue;
296 } 316 }
@@ -316,45 +336,68 @@ class BookmarkFileService implements BookmarkServiceInterface
316 $keys = array_keys($tags); 336 $keys = array_keys($tags);
317 $tmpTags = array_combine($keys, $keys); 337 $tmpTags = array_combine($keys, $keys);
318 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); 338 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
339
319 return $tags; 340 return $tags;
320 } 341 }
321 342
322 /** 343 /**
323 * @inheritDoc 344 * @inheritDoc
324 */ 345 */
325 public function days() 346 public function findByDate(
326 { 347 \DateTimeInterface $from,
327 $bookmarkDays = []; 348 \DateTimeInterface $to,
328 foreach ($this->search() as $bookmark) { 349 ?\DateTimeInterface &$previous,
329 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; 350 ?\DateTimeInterface &$next
351 ): array {
352 $out = [];
353 $previous = null;
354 $next = null;
355
356 foreach ($this->search([], null, false, false, true) as $bookmark) {
357 if ($to < $bookmark->getCreated()) {
358 $next = $bookmark->getCreated();
359 } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
360 $out[] = $bookmark;
361 } else {
362 if ($previous !== null) {
363 break;
364 }
365 $previous = $bookmark->getCreated();
366 }
330 } 367 }
331 $bookmarkDays = array_keys($bookmarkDays);
332 sort($bookmarkDays);
333 368
334 return $bookmarkDays; 369 return $out;
335 } 370 }
336 371
337 /** 372 /**
338 * @inheritDoc 373 * @inheritDoc
339 */ 374 */
340 public function filterDay($request) 375 public function getLatest(): ?Bookmark
341 { 376 {
342 return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request); 377 foreach ($this->search([], null, false, false, true) as $bookmark) {
378 return $bookmark;
379 }
380
381 return null;
343 } 382 }
344 383
345 /** 384 /**
346 * @inheritDoc 385 * @inheritDoc
347 */ 386 */
348 public function initialize() 387 public function initialize(): void
349 { 388 {
350 $initializer = new BookmarkInitializer($this); 389 $initializer = new BookmarkInitializer($this);
351 $initializer->initialize(); 390 $initializer->initialize();
391
392 if (true === $this->isLoggedIn) {
393 $this->save();
394 }
352 } 395 }
353 396
354 /** 397 /**
355 * Handles migration to the new database format (BookmarksArray). 398 * Handles migration to the new database format (BookmarksArray).
356 */ 399 */
357 protected function migrate() 400 protected function migrate(): void
358 { 401 {
359 $bookmarkDb = new LegacyLinkDB( 402 $bookmarkDb = new LegacyLinkDB(
360 $this->conf->get('resource.datastore'), 403 $this->conf->get('resource.datastore'),
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php
index fd556679..c79386ea 100644
--- a/application/bookmark/BookmarkFilter.php
+++ b/application/bookmark/BookmarkFilter.php
@@ -1,5 +1,7 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
5use Exception; 7use Exception;
@@ -77,8 +79,13 @@ class BookmarkFilter
77 * 79 *
78 * @throws BookmarkNotFoundException 80 * @throws BookmarkNotFoundException
79 */ 81 */
80 public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false) 82 public function filter(
81 { 83 string $type,
84 $request,
85 bool $casesensitive = false,
86 string $visibility = 'all',
87 bool $untaggedonly = false
88 ) {
82 if (!in_array($visibility, ['all', 'public', 'private'])) { 89 if (!in_array($visibility, ['all', 'public', 'private'])) {
83 $visibility = 'all'; 90 $visibility = 'all';
84 } 91 }
@@ -115,7 +122,7 @@ class BookmarkFilter
115 return $this->filterTags($request, $casesensitive, $visibility); 122 return $this->filterTags($request, $casesensitive, $visibility);
116 } 123 }
117 case self::$FILTER_DAY: 124 case self::$FILTER_DAY:
118 return $this->filterDay($request); 125 return $this->filterDay($request, $visibility);
119 default: 126 default:
120 return $this->noFilter($visibility); 127 return $this->noFilter($visibility);
121 } 128 }
@@ -128,7 +135,7 @@ class BookmarkFilter
128 * 135 *
129 * @return Bookmark[] filtered bookmarks. 136 * @return Bookmark[] filtered bookmarks.
130 */ 137 */
131 private function noFilter($visibility = 'all') 138 private function noFilter(string $visibility = 'all')
132 { 139 {
133 if ($visibility === 'all') { 140 if ($visibility === 'all') {
134 return $this->bookmarks; 141 return $this->bookmarks;
@@ -151,11 +158,11 @@ class BookmarkFilter
151 * 158 *
152 * @param string $smallHash permalink hash. 159 * @param string $smallHash permalink hash.
153 * 160 *
154 * @return array $filtered array containing permalink data. 161 * @return Bookmark[] $filtered array containing permalink data.
155 * 162 *
156 * @throws \Shaarli\Bookmark\Exception\BookmarkNotFoundException if the smallhash doesn't match any link. 163 * @throws BookmarkNotFoundException if the smallhash doesn't match any link.
157 */ 164 */
158 private function filterSmallHash($smallHash) 165 private function filterSmallHash(string $smallHash)
159 { 166 {
160 foreach ($this->bookmarks as $key => $l) { 167 foreach ($this->bookmarks as $key => $l) {
161 if ($smallHash == $l->getShortUrl()) { 168 if ($smallHash == $l->getShortUrl()) {
@@ -186,15 +193,15 @@ class BookmarkFilter
186 * @param string $searchterms search query. 193 * @param string $searchterms search query.
187 * @param string $visibility Optional: return only all/private/public bookmarks. 194 * @param string $visibility Optional: return only all/private/public bookmarks.
188 * 195 *
189 * @return array search results. 196 * @return Bookmark[] search results.
190 */ 197 */
191 private function filterFulltext($searchterms, $visibility = 'all') 198 private function filterFulltext(string $searchterms, string $visibility = 'all')
192 { 199 {
193 if (empty($searchterms)) { 200 if (empty($searchterms)) {
194 return $this->noFilter($visibility); 201 return $this->noFilter($visibility);
195 } 202 }
196 203
197 $filtered = array(); 204 $filtered = [];
198 $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); 205 $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
199 $exactRegex = '/"([^"]+)"/'; 206 $exactRegex = '/"([^"]+)"/';
200 // Retrieve exact search terms. 207 // Retrieve exact search terms.
@@ -206,8 +213,8 @@ class BookmarkFilter
206 $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); 213 $explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
207 214
208 // Filter excluding terms and update andSearch. 215 // Filter excluding terms and update andSearch.
209 $excludeSearch = array(); 216 $excludeSearch = [];
210 $andSearch = array(); 217 $andSearch = [];
211 foreach ($explodedSearchAnd as $needle) { 218 foreach ($explodedSearchAnd as $needle) {
212 if ($needle[0] == '-' && strlen($needle) > 1) { 219 if ($needle[0] == '-' && strlen($needle) > 1) {
213 $excludeSearch[] = substr($needle, 1); 220 $excludeSearch[] = substr($needle, 1);
@@ -227,33 +234,38 @@ class BookmarkFilter
227 } 234 }
228 } 235 }
229 236
230 // Concatenate link fields to search across fields. 237 $lengths = [];
231 // Adds a '\' separator for exact search terms. 238 $content = $this->buildFullTextSearchableLink($link, $lengths);
232 $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\';
233 $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\';
234 $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\';
235 $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\';
236 239
237 // Be optimistic 240 // Be optimistic
238 $found = true; 241 $found = true;
242 $foundPositions = [];
239 243
240 // First, we look for exact term search 244 // First, we look for exact term search
241 for ($i = 0; $i < count($exactSearch) && $found; $i++) { 245 // Then iterate over keywords, if keyword is not found,
242 $found = strpos($content, $exactSearch[$i]) !== false;
243 }
244
245 // Iterate over keywords, if keyword is not found,
246 // no need to check for the others. We want all or nothing. 246 // no need to check for the others. We want all or nothing.
247 for ($i = 0; $i < count($andSearch) && $found; $i++) { 247 foreach ([$exactSearch, $andSearch] as $search) {
248 $found = strpos($content, $andSearch[$i]) !== false; 248 for ($i = 0; $i < count($search) && $found !== false; $i++) {
249 $found = mb_strpos($content, $search[$i]);
250 if ($found === false) {
251 break;
252 }
253
254 $foundPositions[] = ['start' => $found, 'end' => $found + mb_strlen($search[$i])];
255 }
249 } 256 }
250 257
251 // Exclude terms. 258 // Exclude terms.
252 for ($i = 0; $i < count($excludeSearch) && $found; $i++) { 259 for ($i = 0; $i < count($excludeSearch) && $found !== false; $i++) {
253 $found = strpos($content, $excludeSearch[$i]) === false; 260 $found = strpos($content, $excludeSearch[$i]) === false;
254 } 261 }
255 262
256 if ($found) { 263 if ($found !== false) {
264 $link->addAdditionalContentEntry(
265 'search_highlight',
266 $this->postProcessFoundPositions($lengths, $foundPositions)
267 );
268
257 $filtered[$id] = $link; 269 $filtered[$id] = $link;
258 } 270 }
259 } 271 }
@@ -268,7 +280,7 @@ class BookmarkFilter
268 * 280 *
269 * @return string generated regex fragment 281 * @return string generated regex fragment
270 */ 282 */
271 private static function tag2regex($tag) 283 private static function tag2regex(string $tag): string
272 { 284 {
273 $len = strlen($tag); 285 $len = strlen($tag);
274 if (!$len || $tag === "-" || $tag === "*") { 286 if (!$len || $tag === "-" || $tag === "*") {
@@ -314,13 +326,13 @@ class BookmarkFilter
314 * You can specify one or more tags, separated by space or a comma, e.g. 326 * You can specify one or more tags, separated by space or a comma, e.g.
315 * print_r($mydb->filterTags('linux programming')); 327 * print_r($mydb->filterTags('linux programming'));
316 * 328 *
317 * @param string $tags list of tags separated by commas or blank spaces. 329 * @param string|array $tags list of tags, separated by commas or blank spaces if passed as string.
318 * @param bool $casesensitive ignore case if false. 330 * @param bool $casesensitive ignore case if false.
319 * @param string $visibility Optional: return only all/private/public bookmarks. 331 * @param string $visibility Optional: return only all/private/public bookmarks.
320 * 332 *
321 * @return array filtered bookmarks. 333 * @return Bookmark[] filtered bookmarks.
322 */ 334 */
323 public function filterTags($tags, $casesensitive = false, $visibility = 'all') 335 public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all')
324 { 336 {
325 // get single tags (we may get passed an array, even though the docs say different) 337 // get single tags (we may get passed an array, even though the docs say different)
326 $inputTags = $tags; 338 $inputTags = $tags;
@@ -396,9 +408,9 @@ class BookmarkFilter
396 * 408 *
397 * @param string $visibility return only all/private/public bookmarks. 409 * @param string $visibility return only all/private/public bookmarks.
398 * 410 *
399 * @return array filtered bookmarks. 411 * @return Bookmark[] filtered bookmarks.
400 */ 412 */
401 public function filterUntagged($visibility) 413 public function filterUntagged(string $visibility)
402 { 414 {
403 $filtered = []; 415 $filtered = [];
404 foreach ($this->bookmarks as $key => $link) { 416 foreach ($this->bookmarks as $key => $link) {
@@ -425,21 +437,26 @@ class BookmarkFilter
425 * print_r($mydb->filterDay('20120125')); 437 * print_r($mydb->filterDay('20120125'));
426 * 438 *
427 * @param string $day day to filter. 439 * @param string $day day to filter.
428 * 440 * @param string $visibility return only all/private/public bookmarks.
429 * @return array all link matching given day. 441
442 * @return Bookmark[] all link matching given day.
430 * 443 *
431 * @throws Exception if date format is invalid. 444 * @throws Exception if date format is invalid.
432 */ 445 */
433 public function filterDay($day) 446 public function filterDay(string $day, string $visibility)
434 { 447 {
435 if (!checkDateFormat('Ymd', $day)) { 448 if (!checkDateFormat('Ymd', $day)) {
436 throw new Exception('Invalid date format'); 449 throw new Exception('Invalid date format');
437 } 450 }
438 451
439 $filtered = array(); 452 $filtered = [];
440 foreach ($this->bookmarks as $key => $l) { 453 foreach ($this->bookmarks as $key => $bookmark) {
441 if ($l->getCreated()->format('Ymd') == $day) { 454 if ($visibility === static::$PUBLIC && $bookmark->isPrivate()) {
442 $filtered[$key] = $l; 455 continue;
456 }
457
458 if ($bookmark->getCreated()->format('Ymd') == $day) {
459 $filtered[$key] = $bookmark;
443 } 460 }
444 } 461 }
445 462
@@ -455,9 +472,9 @@ class BookmarkFilter
455 * @param string $tags string containing a list of tags. 472 * @param string $tags string containing a list of tags.
456 * @param bool $casesensitive will convert everything to lowercase if false. 473 * @param bool $casesensitive will convert everything to lowercase if false.
457 * 474 *
458 * @return array filtered tags string. 475 * @return string[] filtered tags string.
459 */ 476 */
460 public static function tagsStrToArray($tags, $casesensitive) 477 public static function tagsStrToArray(string $tags, bool $casesensitive): array
461 { 478 {
462 // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) 479 // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
463 $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); 480 $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
@@ -465,4 +482,74 @@ class BookmarkFilter
465 482
466 return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); 483 return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
467 } 484 }
485
486 /**
487 * This method finalize the content of the foundPositions array,
488 * by associated all search results to their associated bookmark field,
489 * making sure that there is no overlapping results, etc.
490 *
491 * @param array $fieldLengths Start and end positions of every bookmark fields in the aggregated bookmark content.
492 * @param array $foundPositions Positions where the search results were found in the aggregated content.
493 *
494 * @return array Updated $foundPositions, by bookmark field.
495 */
496 protected function postProcessFoundPositions(array $fieldLengths, array $foundPositions): array
497 {
498 // Sort results by starting position ASC.
499 usort($foundPositions, function (array $entryA, array $entryB): int {
500 return $entryA['start'] > $entryB['start'] ? 1 : -1;
501 });
502
503 $out = [];
504 $currentMax = -1;
505 foreach ($foundPositions as $foundPosition) {
506 // we do not allow overlapping highlights
507 if ($foundPosition['start'] < $currentMax) {
508 continue;
509 }
510
511 $currentMax = $foundPosition['end'];
512 foreach ($fieldLengths as $part => $length) {
513 if ($foundPosition['start'] < $length['start'] || $foundPosition['start'] > $length['end']) {
514 continue;
515 }
516
517 $out[$part][] = [
518 'start' => $foundPosition['start'] - $length['start'],
519 'end' => $foundPosition['end'] - $length['start'],
520 ];
521 break;
522 }
523 }
524
525 return $out;
526 }
527
528 /**
529 * Concatenate link fields to search across fields. Adds a '\' separator for exact search terms.
530 * Also populate $length array with starting and ending positions of every bookmark field
531 * inside concatenated content.
532 *
533 * @param Bookmark $link
534 * @param array $lengths (by reference)
535 *
536 * @return string Lowercase concatenated fields content.
537 */
538 protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string
539 {
540 $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\';
541 $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\';
542 $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\';
543 $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\';
544
545 $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())];
546 $nextField = $lengths['title']['end'] + 1;
547 $lengths['description'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getDescription())];
548 $nextField = $lengths['description']['end'] + 1;
549 $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())];
550 $nextField = $lengths['url']['end'] + 1;
551 $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())];
552
553 return $content;
554 }
468} 555}
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php
index ae9ffcb4..f40fa476 100644
--- a/application/bookmark/BookmarkIO.php
+++ b/application/bookmark/BookmarkIO.php
@@ -1,7 +1,12 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
7use malkusch\lock\mutex\Mutex;
8use malkusch\lock\mutex\NoMutex;
9use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
5use Shaarli\Bookmark\Exception\EmptyDataStoreException; 10use Shaarli\Bookmark\Exception\EmptyDataStoreException;
6use Shaarli\Bookmark\Exception\NotWritableDataStoreException; 11use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
7use Shaarli\Config\ConfigManager; 12use Shaarli\Config\ConfigManager;
@@ -26,11 +31,14 @@ class BookmarkIO
26 */ 31 */
27 protected $conf; 32 protected $conf;
28 33
34
35 /** @var Mutex */
36 protected $mutex;
37
29 /** 38 /**
30 * string Datastore PHP prefix 39 * string Datastore PHP prefix
31 */ 40 */
32 protected static $phpPrefix = '<?php /* '; 41 protected static $phpPrefix = '<?php /* ';
33
34 /** 42 /**
35 * string Datastore PHP suffix 43 * string Datastore PHP suffix
36 */ 44 */
@@ -41,35 +49,46 @@ class BookmarkIO
41 * 49 *
42 * @param ConfigManager $conf instance 50 * @param ConfigManager $conf instance
43 */ 51 */
44 public function __construct($conf) 52 public function __construct(ConfigManager $conf, Mutex $mutex = null)
45 { 53 {
54 if ($mutex === null) {
55 // This should only happen with legacy classes
56 $mutex = new NoMutex();
57 }
46 $this->conf = $conf; 58 $this->conf = $conf;
47 $this->datastore = $conf->get('resource.datastore'); 59 $this->datastore = $conf->get('resource.datastore');
60 $this->mutex = $mutex;
48 } 61 }
49 62
50 /** 63 /**
51 * Reads database from disk to memory 64 * Reads database from disk to memory
52 * 65 *
53 * @return BookmarkArray instance 66 * @return Bookmark[]
54 * 67 *
55 * @throws NotWritableDataStoreException Data couldn't be loaded 68 * @throws NotWritableDataStoreException Data couldn't be loaded
56 * @throws EmptyDataStoreException Datastore doesn't exist 69 * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark
70 * @throws DatastoreNotInitializedException File does not exists
57 */ 71 */
58 public function read() 72 public function read()
59 { 73 {
60 if (! file_exists($this->datastore)) { 74 if (! file_exists($this->datastore)) {
61 throw new EmptyDataStoreException(); 75 throw new DatastoreNotInitializedException();
62 } 76 }
63 77
64 if (!is_writable($this->datastore)) { 78 if (!is_writable($this->datastore)) {
65 throw new NotWritableDataStoreException($this->datastore); 79 throw new NotWritableDataStoreException($this->datastore);
66 } 80 }
67 81
82 $content = null;
83 $this->mutex->synchronized(function () use (&$content) {
84 $content = file_get_contents($this->datastore);
85 });
86
68 // Note that gzinflate is faster than gzuncompress. 87 // Note that gzinflate is faster than gzuncompress.
69 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 88 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
70 $links = unserialize(gzinflate(base64_decode( 89 $links = unserialize(gzinflate(base64_decode(
71 substr(file_get_contents($this->datastore), 90 substr($content, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
72 strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); 91 )));
73 92
74 if (empty($links)) { 93 if (empty($links)) {
75 if (filesize($this->datastore) > 100) { 94 if (filesize($this->datastore) > 100) {
@@ -84,7 +103,7 @@ class BookmarkIO
84 /** 103 /**
85 * Saves the database from memory to disk 104 * Saves the database from memory to disk
86 * 105 *
87 * @param BookmarkArray $links instance. 106 * @param Bookmark[] $links
88 * 107 *
89 * @throws NotWritableDataStoreException the datastore is not writable 108 * @throws NotWritableDataStoreException the datastore is not writable
90 */ 109 */
@@ -98,11 +117,13 @@ class BookmarkIO
98 throw new NotWritableDataStoreException(dirname($this->datastore)); 117 throw new NotWritableDataStoreException(dirname($this->datastore));
99 } 118 }
100 119
101 file_put_contents( 120 $data = self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix;
102 $this->datastore,
103 self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix
104 );
105 121
106 invalidateCaches($this->conf->get('resource.page_cache')); 122 $this->mutex->synchronized(function () use ($data) {
123 file_put_contents(
124 $this->datastore,
125 $data
126 );
127 });
107 } 128 }
108} 129}
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php
index 9eee9a35..04b996f3 100644
--- a/application/bookmark/BookmarkInitializer.php
+++ b/application/bookmark/BookmarkInitializer.php
@@ -1,13 +1,14 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
4 6
5/** 7/**
6 * Class BookmarkInitializer 8 * Class BookmarkInitializer
7 * 9 *
8 * This class is used to initialized default bookmarks after a fresh install of Shaarli. 10 * This class is used to initialized default bookmarks after a fresh install of Shaarli.
9 * It is no longer call when the data store is empty, 11 * It should be only called if the datastore file does not exist(users might want to delete the default bookmarks).
10 * because user might want to delete default bookmarks after the install.
11 * 12 *
12 * To prevent data corruption, it does not overwrite existing bookmarks, 13 * To prevent data corruption, it does not overwrite existing bookmarks,
13 * even though there should not be any. 14 * even though there should not be any.
@@ -24,7 +25,7 @@ class BookmarkInitializer
24 * 25 *
25 * @param BookmarkServiceInterface $bookmarkService 26 * @param BookmarkServiceInterface $bookmarkService
26 */ 27 */
27 public function __construct($bookmarkService) 28 public function __construct(BookmarkServiceInterface $bookmarkService)
28 { 29 {
29 $this->bookmarkService = $bookmarkService; 30 $this->bookmarkService = $bookmarkService;
30 } 31 }
@@ -32,28 +33,80 @@ class BookmarkInitializer
32 /** 33 /**
33 * Initialize the data store with default bookmarks 34 * Initialize the data store with default bookmarks
34 */ 35 */
35 public function initialize() 36 public function initialize(): void
36 { 37 {
37 $bookmark = new Bookmark(); 38 $bookmark = new Bookmark();
38 $bookmark->setTitle(t('My secret stuff... - Pastebin.com')); 39 $bookmark->setTitle('quicksilver (loop) on Vimeo ' . t('(private bookmark with thumbnail demo)'));
39 $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []); 40 $bookmark->setUrl('https://vimeo.com/153493904');
40 $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.')); 41 $bookmark->setDescription(t(
41 $bookmark->setTagsString('secretstuff'); 42'Shaarli will automatically pick up the thumbnail for links to a variety of websites.
43
44Explore your new Shaarli instance by trying out controls and menus.
45Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli.
46
47Now you can edit or delete the default shaares.
48'
49 ));
50 $bookmark->setTagsString('shaarli help thumbnail');
51 $bookmark->setPrivate(true);
52 $this->bookmarkService->add($bookmark, false);
53
54 $bookmark = new Bookmark();
55 $bookmark->setTitle(t('Note: Shaare descriptions'));
56 $bookmark->setDescription(t(
57'Adding a shaare without entering a URL creates a text-only "note" post such as this one.
58This note is private, so you are the only one able to see it while logged in.
59
60You can use this to keep notes, post articles, code snippets, and much more.
61
62The Markdown formatting setting allows you to format your notes and bookmark description:
63
64### Title headings
65
66#### Multiple headings levels
67 * bullet lists
68 * _italic_ text
69 * **bold** text
70 * ~~strike through~~ text
71 * `code` blocks
72 * images
73 * [links](https://en.wikipedia.org/wiki/Markdown)
74
75Markdown also supports tables:
76
77| Name | Type | Color | Qty |
78| ------- | --------- | ------ | ----- |
79| Orange | Fruit | Orange | 126 |
80| Apple | Fruit | Any | 62 |
81| Lemon | Fruit | Yellow | 30 |
82| Carrot | Vegetable | Red | 14 |
83'
84 ));
85 $bookmark->setTagsString('shaarli help');
42 $bookmark->setPrivate(true); 86 $bookmark->setPrivate(true);
43 $this->bookmarkService->add($bookmark); 87 $this->bookmarkService->add($bookmark, false);
44 88
45 $bookmark = new Bookmark(); 89 $bookmark = new Bookmark();
46 $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); 90 $bookmark->setTitle(
47 $bookmark->setUrl('https://shaarli.readthedocs.io', []); 91 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service')
92 );
48 $bookmark->setDescription(t( 93 $bookmark->setDescription(t(
49 'Welcome to Shaarli! This is your first public bookmark. ' 94'Welcome to Shaarli!
50 . 'To edit or delete me, you must first login. 95
96Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately.
97You can add a description to your bookmarks, such as this one, and tag them.
98
99Create a new shaare by clicking the `+Shaare` button, or using any of the recommended tools (browser extension, mobile app, bookmarklet, REST API, etc.).
51 100
52To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. 101You 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`).
102Hashtags such as #shaarli #help are also supported.
103You can also filter the available [RSS feed](/feed/atom) and picture wall by tag or plaintext search.
53 104
54You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' 105We hope that you will enjoy using Shaarli, maintained with ❤️ by the community!
106Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if you have a suggestion or encounter an issue.
107'
55 )); 108 ));
56 $bookmark->setTagsString('opensource software'); 109 $bookmark->setTagsString('shaarli help');
57 $this->bookmarkService->add($bookmark); 110 $this->bookmarkService->add($bookmark, false);
58 } 111 }
59} 112}
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php
index 7b7a4f09..08cdbb4e 100644
--- a/application/bookmark/BookmarkServiceInterface.php
+++ b/application/bookmark/BookmarkServiceInterface.php
@@ -1,73 +1,73 @@
1<?php 1<?php
2 2
3namespace Shaarli\Bookmark; 3declare(strict_types=1);
4 4
5namespace Shaarli\Bookmark;
5 6
6use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 7use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7use Shaarli\Bookmark\Exception\NotWritableDataStoreException; 8use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Exceptions\IOException;
10use Shaarli\History;
11 9
12/** 10/**
13 * Class BookmarksService 11 * Class BookmarksService
14 * 12 *
15 * This is the entry point to manipulate the bookmark DB. 13 * This is the entry point to manipulate the bookmark DB.
14 *
15 * Regarding return types of a list of bookmarks, it can either be an array or an ArrayAccess implementation,
16 * so until PHP 8.0 is the minimal supported version with union return types it cannot be explicitly added.
16 */ 17 */
17interface BookmarkServiceInterface 18interface BookmarkServiceInterface
18{ 19{
19 /** 20 /**
20 * BookmarksService constructor.
21 *
22 * @param ConfigManager $conf instance
23 * @param History $history instance
24 * @param bool $isLoggedIn true if the current user is logged in
25 */
26 public function __construct(ConfigManager $conf, History $history, $isLoggedIn);
27
28 /**
29 * Find a bookmark by hash 21 * Find a bookmark by hash
30 * 22 *
31 * @param string $hash 23 * @param string $hash Bookmark's hash
24 * @param string|null $privateKey Optional key used to access private links while logged out
32 * 25 *
33 * @return mixed 26 * @return Bookmark
34 * 27 *
35 * @throws \Exception 28 * @throws \Exception
36 */ 29 */
37 public function findByHash($hash); 30 public function findByHash(string $hash, string $privateKey = null);
38 31
39 /** 32 /**
40 * @param $url 33 * @param $url
41 * 34 *
42 * @return Bookmark|null 35 * @return Bookmark|null
43 */ 36 */
44 public function findByUrl($url); 37 public function findByUrl(string $url): ?Bookmark;
45 38
46 /** 39 /**
47 * Search bookmarks 40 * Search bookmarks
48 * 41 *
49 * @param mixed $request 42 * @param array $request
50 * @param string $visibility 43 * @param ?string $visibility
51 * @param bool $caseSensitive 44 * @param bool $caseSensitive
52 * @param bool $untaggedOnly 45 * @param bool $untaggedOnly
46 * @param bool $ignoreSticky
53 * 47 *
54 * @return Bookmark[] 48 * @return Bookmark[]
55 */ 49 */
56 public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false); 50 public function search(
51 array $request = [],
52 string $visibility = null,
53 bool $caseSensitive = false,
54 bool $untaggedOnly = false,
55 bool $ignoreSticky = false
56 );
57 57
58 /** 58 /**
59 * Get a single bookmark by its ID. 59 * Get a single bookmark by its ID.
60 * 60 *
61 * @param int $id Bookmark ID 61 * @param int $id Bookmark ID
62 * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an 62 * @param ?string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
63 * exception 63 * exception
64 * 64 *
65 * @return Bookmark 65 * @return Bookmark
66 * 66 *
67 * @throws BookmarkNotFoundException 67 * @throws BookmarkNotFoundException
68 * @throws \Exception 68 * @throws \Exception
69 */ 69 */
70 public function get($id, $visibility = null); 70 public function get(int $id, string $visibility = null);
71 71
72 /** 72 /**
73 * Updates an existing bookmark (depending on its ID). 73 * Updates an existing bookmark (depending on its ID).
@@ -80,7 +80,7 @@ interface BookmarkServiceInterface
80 * @throws BookmarkNotFoundException 80 * @throws BookmarkNotFoundException
81 * @throws \Exception 81 * @throws \Exception
82 */ 82 */
83 public function set($bookmark, $save = true); 83 public function set(Bookmark $bookmark, bool $save = true): Bookmark;
84 84
85 /** 85 /**
86 * Adds a new bookmark (the ID must be empty). 86 * Adds a new bookmark (the ID must be empty).
@@ -92,7 +92,7 @@ interface BookmarkServiceInterface
92 * 92 *
93 * @throws \Exception 93 * @throws \Exception
94 */ 94 */
95 public function add($bookmark, $save = true); 95 public function add(Bookmark $bookmark, bool $save = true): Bookmark;
96 96
97 /** 97 /**
98 * Adds or updates a bookmark depending on its ID: 98 * Adds or updates a bookmark depending on its ID:
@@ -106,7 +106,7 @@ interface BookmarkServiceInterface
106 * 106 *
107 * @throws \Exception 107 * @throws \Exception
108 */ 108 */
109 public function addOrSet($bookmark, $save = true); 109 public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark;
110 110
111 /** 111 /**
112 * Deletes a bookmark. 112 * Deletes a bookmark.
@@ -116,65 +116,72 @@ interface BookmarkServiceInterface
116 * 116 *
117 * @throws \Exception 117 * @throws \Exception
118 */ 118 */
119 public function remove($bookmark, $save = true); 119 public function remove(Bookmark $bookmark, bool $save = true): void;
120 120
121 /** 121 /**
122 * Get a single bookmark by its ID. 122 * Get a single bookmark by its ID.
123 * 123 *
124 * @param int $id Bookmark ID 124 * @param int $id Bookmark ID
125 * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an 125 * @param ?string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
126 * exception 126 * exception
127 * 127 *
128 * @return bool 128 * @return bool
129 */ 129 */
130 public function exists($id, $visibility = null); 130 public function exists(int $id, string $visibility = null): bool;
131 131
132 /** 132 /**
133 * Return the number of available bookmarks for given visibility. 133 * Return the number of available bookmarks for given visibility.
134 * 134 *
135 * @param string $visibility public|private|all 135 * @param ?string $visibility public|private|all
136 * 136 *
137 * @return int Number of bookmarks 137 * @return int Number of bookmarks
138 */ 138 */
139 public function count($visibility = null); 139 public function count(string $visibility = null): int;
140 140
141 /** 141 /**
142 * Write the datastore. 142 * Write the datastore.
143 * 143 *
144 * @throws NotWritableDataStoreException 144 * @throws NotWritableDataStoreException
145 */ 145 */
146 public function save(); 146 public function save(): void;
147 147
148 /** 148 /**
149 * Returns the list tags appearing in the bookmarks with the given tags 149 * Returns the list tags appearing in the bookmarks with the given tags
150 * 150 *
151 * @param array $filteringTags tags selecting the bookmarks to consider 151 * @param array|null $filteringTags tags selecting the bookmarks to consider
152 * @param string $visibility process only all/private/public bookmarks 152 * @param string|null $visibility process only all/private/public bookmarks
153 * 153 *
154 * @return array tag => bookmarksCount 154 * @return array tag => bookmarksCount
155 */ 155 */
156 public function bookmarksCountPerTag($filteringTags = [], $visibility = 'all'); 156 public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array;
157 157
158 /** 158 /**
159 * Returns the list of days containing articles (oldest first) 159 * Return a list of bookmark matching provided period of time.
160 * It also update directly previous and next date outside of given period found in the datastore.
161 *
162 * @param \DateTimeInterface $from Starting date.
163 * @param \DateTimeInterface $to Ending date.
164 * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from.
165 * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to.
160 * 166 *
161 * @return array containing days (in format YYYYMMDD). 167 * @return array List of bookmarks matching provided period of time.
162 */ 168 */
163 public function days(); 169 public function findByDate(
170 \DateTimeInterface $from,
171 \DateTimeInterface $to,
172 ?\DateTimeInterface &$previous,
173 ?\DateTimeInterface &$next
174 ): array;
164 175
165 /** 176 /**
166 * Returns the list of articles for a given day. 177 * Returns the latest bookmark by creation date.
167 * 178 *
168 * @param string $request day to filter. Format: YYYYMMDD. 179 * @return Bookmark|null Found Bookmark or null if the datastore is empty.
169 *
170 * @return Bookmark[] list of shaare found.
171 *
172 * @throws BookmarkNotFoundException
173 */ 180 */
174 public function filterDay($request); 181 public function getLatest(): ?Bookmark;
175 182
176 /** 183 /**
177 * Creates the default database after a fresh install. 184 * Creates the default database after a fresh install.
178 */ 185 */
179 public function initialize(); 186 public function initialize(): void;
180} 187}
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index 88379430..faf5dbfd 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -3,112 +3,6 @@
3use Shaarli\Bookmark\Bookmark; 3use Shaarli\Bookmark\Bookmark;
4 4
5/** 5/**
6 * Get cURL callback function for CURLOPT_WRITEFUNCTION
7 *
8 * @param string $charset to extract from the downloaded page (reference)
9 * @param string $title to extract from the downloaded page (reference)
10 * @param string $description to extract from the downloaded page (reference)
11 * @param string $keywords to extract from the downloaded page (reference)
12 * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
13 * @param string $curlGetInfo Optionally overrides curl_getinfo function
14 *
15 * @return Closure
16 */
17function get_curl_download_callback(
18 &$charset,
19 &$title,
20 &$description,
21 &$keywords,
22 $retrieveDescription,
23 $curlGetInfo = 'curl_getinfo'
24) {
25 $isRedirected = false;
26 $currentChunk = 0;
27 $foundChunk = null;
28
29 /**
30 * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
31 *
32 * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
33 * Then we extract the title and the charset and stop the download when it's done.
34 *
35 * @param resource $ch cURL resource
36 * @param string $data chunk of data being downloaded
37 *
38 * @return int|bool length of $data or false if we need to stop the download
39 */
40 return function (&$ch, $data) use (
41 $retrieveDescription,
42 $curlGetInfo,
43 &$charset,
44 &$title,
45 &$description,
46 &$keywords,
47 &$isRedirected,
48 &$currentChunk,
49 &$foundChunk
50 ) {
51 $currentChunk++;
52 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
53 if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
54 $isRedirected = true;
55 return strlen($data);
56 }
57 if (!empty($responseCode) && $responseCode !== 200) {
58 return false;
59 }
60 // After a redirection, the content type will keep the previous request value
61 // until it finds the next content-type header.
62 if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
63 $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
64 }
65 if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
66 return false;
67 }
68 if (!empty($contentType) && empty($charset)) {
69 $charset = header_extract_charset($contentType);
70 }
71 if (empty($charset)) {
72 $charset = html_extract_charset($data);
73 }
74 if (empty($title)) {
75 $title = html_extract_title($data);
76 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
77 }
78 if ($retrieveDescription && empty($description)) {
79 $description = html_extract_tag('description', $data);
80 $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
81 }
82 if ($retrieveDescription && empty($keywords)) {
83 $keywords = html_extract_tag('keywords', $data);
84 if (! empty($keywords)) {
85 $foundChunk = $currentChunk;
86 // Keywords use the format tag1, tag2 multiple words, tag
87 // So we format them to match Shaarli's separator and glue multiple words with '-'
88 $keywords = implode(' ', array_map(function($keyword) {
89 return implode('-', preg_split('/\s+/', trim($keyword)));
90 }, explode(',', $keywords)));
91 }
92 }
93
94 // We got everything we want, stop the download.
95 // If we already found either the title, description or keywords,
96 // it's highly unlikely that we'll found the other metas further than
97 // in the same chunk of data or the next one. So we also stop the download after that.
98 if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
99 && (! $retrieveDescription
100 || $foundChunk < $currentChunk
101 || (!empty($title) && !empty($description) && !empty($keywords))
102 )
103 ) {
104 return false;
105 }
106
107 return strlen($data);
108 };
109}
110
111/**
112 * Extract title from an HTML document. 6 * Extract title from an HTML document.
113 * 7 *
114 * @param string $html HTML content where to look for a title. 8 * @param string $html HTML content where to look for a title.
@@ -132,7 +26,7 @@ function html_extract_title($html)
132 */ 26 */
133function header_extract_charset($header) 27function header_extract_charset($header)
134{ 28{
135 preg_match('/charset="?([^; ]+)/i', $header, $match); 29 preg_match('/charset=["\']?([^; "\']+)/i', $header, $match);
136 if (! empty($match[1])) { 30 if (! empty($match[1])) {
137 return strtolower(trim($match[1])); 31 return strtolower(trim($match[1]));
138 } 32 }
@@ -172,11 +66,13 @@ function html_extract_tag($tag, $html)
172{ 66{
173 $propertiesKey = ['property', 'name', 'itemprop']; 67 $propertiesKey = ['property', 'name', 'itemprop'];
174 $properties = implode('|', $propertiesKey); 68 $properties = implode('|', $propertiesKey);
69 // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
70 $orCondition = '["\']?(?:og:)?'. $tag .'["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
175 // Try to retrieve OpenGraph image. 71 // Try to retrieve OpenGraph image.
176 $ogRegex = '#<meta[^>]+(?:'. $properties .')=["\']?(?:og:)?'. $tag .'["\'\s][^>]*content=["\']?(.*?)["\'/>]#'; 72 $ogRegex = '#<meta[^>]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=["\'](.*?)["\'].*?>#';
177 // If the attributes are not in the order property => content (e.g. Github) 73 // If the attributes are not in the order property => content (e.g. Github)
178 // New regex to keep this readable... more or less. 74 // New regex to keep this readable... more or less.
179 $ogRegexReverse = '#<meta[^>]+content=["\']([^"\']+)[^>]+(?:'. $properties .')=["\']?(?:og)?:'. $tag .'["\'\s/>]#'; 75 $ogRegexReverse = '#<meta[^>]+content=["\'](.*?)["\'][^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#';
180 76
181 if (preg_match($ogRegex, $html, $matches) > 0 77 if (preg_match($ogRegex, $html, $matches) > 0
182 || preg_match($ogRegexReverse, $html, $matches) > 0 78 || preg_match($ogRegexReverse, $html, $matches) > 0
@@ -220,7 +116,7 @@ function hashtag_autolink($description, $indexUrl = '')
220 * \p{Mn} - any non marking space (accents, umlauts, etc) 116 * \p{Mn} - any non marking space (accents, umlauts, etc)
221 */ 117 */
222 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 118 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
223 $replacement = '$1<a href="'. $indexUrl .'?addtag=$2" title="Hashtag $2">#$2</a>'; 119 $replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>';
224 return preg_replace($regex, $replacement, $description); 120 return preg_replace($regex, $replacement, $description);
225} 121}
226 122
diff --git a/application/bookmark/exception/DatastoreNotInitializedException.php b/application/bookmark/exception/DatastoreNotInitializedException.php
new file mode 100644
index 00000000..f495049d
--- /dev/null
+++ b/application/bookmark/exception/DatastoreNotInitializedException.php
@@ -0,0 +1,10 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Bookmark\Exception;
6
7class DatastoreNotInitializedException extends \Exception
8{
9
10}
diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php
index 4509357c..23b22269 100644
--- a/application/config/ConfigJson.php
+++ b/application/config/ConfigJson.php
@@ -19,7 +19,7 @@ class ConfigJson implements ConfigIO
19 $data = file_get_contents($filepath); 19 $data = file_get_contents($filepath);
20 $data = str_replace(self::getPhpHeaders(), '', $data); 20 $data = str_replace(self::getPhpHeaders(), '', $data);
21 $data = str_replace(self::getPhpSuffix(), '', $data); 21 $data = str_replace(self::getPhpSuffix(), '', $data);
22 $data = json_decode($data, true); 22 $data = json_decode(trim($data), true);
23 if ($data === null) { 23 if ($data === null) {
24 $errorCode = json_last_error(); 24 $errorCode = json_last_error();
25 $error = sprintf( 25 $error = sprintf(
@@ -46,7 +46,7 @@ class ConfigJson implements ConfigIO
46 // JSON_PRETTY_PRINT is available from PHP 5.4. 46 // JSON_PRETTY_PRINT is available from PHP 5.4.
47 $print = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0; 47 $print = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
48 $data = self::getPhpHeaders() . json_encode($conf, $print) . self::getPhpSuffix(); 48 $data = self::getPhpHeaders() . json_encode($conf, $print) . self::getPhpSuffix();
49 if (!file_put_contents($filepath, $data)) { 49 if (empty($filepath) || !file_put_contents($filepath, $data)) {
50 throw new \Shaarli\Exceptions\IOException( 50 throw new \Shaarli\Exceptions\IOException(
51 $filepath, 51 $filepath,
52 t('Shaarli could not create the config file. '. 52 t('Shaarli could not create the config file. '.
@@ -73,7 +73,7 @@ class ConfigJson implements ConfigIO
73 */ 73 */
74 public static function getPhpHeaders() 74 public static function getPhpHeaders()
75 { 75 {
76 return '<?php /*'. PHP_EOL; 76 return '<?php /*';
77 } 77 }
78 78
79 /** 79 /**
@@ -85,6 +85,6 @@ class ConfigJson implements ConfigIO
85 */ 85 */
86 public static function getPhpSuffix() 86 public static function getPhpSuffix()
87 { 87 {
88 return PHP_EOL . '*/ ?>'; 88 return '*/ ?>';
89 } 89 }
90} 90}
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index e45bb4c3..fb085023 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -3,6 +3,7 @@ namespace Shaarli\Config;
3 3
4use Shaarli\Config\Exception\MissingFieldConfigException; 4use Shaarli\Config\Exception\MissingFieldConfigException;
5use Shaarli\Config\Exception\UnauthorizedConfigException; 5use Shaarli\Config\Exception\UnauthorizedConfigException;
6use Shaarli\Thumbnailer;
6 7
7/** 8/**
8 * Class ConfigManager 9 * Class ConfigManager
@@ -361,11 +362,12 @@ class ConfigManager
361 $this->setEmpty('security.open_shaarli', false); 362 $this->setEmpty('security.open_shaarli', false);
362 $this->setEmpty('security.allowed_protocols', ['ftp', 'ftps', 'magnet']); 363 $this->setEmpty('security.allowed_protocols', ['ftp', 'ftps', 'magnet']);
363 364
364 $this->setEmpty('general.header_link', '?'); 365 $this->setEmpty('general.header_link', '/');
365 $this->setEmpty('general.links_per_page', 20); 366 $this->setEmpty('general.links_per_page', 20);
366 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); 367 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
367 $this->setEmpty('general.default_note_title', 'Note: '); 368 $this->setEmpty('general.default_note_title', 'Note: ');
368 $this->setEmpty('general.retrieve_description', false); 369 $this->setEmpty('general.retrieve_description', true);
370 $this->setEmpty('general.enable_async_metadata', true);
369 371
370 $this->setEmpty('updates.check_updates', false); 372 $this->setEmpty('updates.check_updates', false);
371 $this->setEmpty('updates.check_updates_branch', 'stable'); 373 $this->setEmpty('updates.check_updates_branch', 'stable');
@@ -381,6 +383,7 @@ class ConfigManager
381 // default state of the 'remember me' checkbox of the login form 383 // default state of the 'remember me' checkbox of the login form
382 $this->setEmpty('privacy.remember_user_default', true); 384 $this->setEmpty('privacy.remember_user_default', true);
383 385
386 $this->setEmpty('thumbnails.mode', Thumbnailer::MODE_ALL);
384 $this->setEmpty('thumbnails.width', '125'); 387 $this->setEmpty('thumbnails.width', '125');
385 $this->setEmpty('thumbnails.height', '90'); 388 $this->setEmpty('thumbnails.height', '90');
386 389
diff --git a/application/config/ConfigPlugin.php b/application/config/ConfigPlugin.php
index dbb24937..ea8dfbda 100644
--- a/application/config/ConfigPlugin.php
+++ b/application/config/ConfigPlugin.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2 2
3use Shaarli\Config\Exception\PluginConfigOrderException; 3use Shaarli\Config\Exception\PluginConfigOrderException;
4use Shaarli\Plugin\PluginManager;
4 5
5/** 6/**
6 * Plugin configuration helper functions. 7 * Plugin configuration helper functions.
@@ -19,6 +20,20 @@ use Shaarli\Config\Exception\PluginConfigOrderException;
19 */ 20 */
20function save_plugin_config($formData) 21function save_plugin_config($formData)
21{ 22{
23 // We can only save existing plugins
24 $directories = str_replace(
25 PluginManager::$PLUGINS_PATH . '/',
26 '',
27 glob(PluginManager::$PLUGINS_PATH . '/*')
28 );
29 $formData = array_filter(
30 $formData,
31 function ($value, string $key) use ($directories) {
32 return startsWith($key, 'order') || in_array($key, $directories);
33 },
34 ARRAY_FILTER_USE_BOTH
35 );
36
22 // Make sure there are no duplicates in orders. 37 // Make sure there are no duplicates in orders.
23 if (!validate_plugin_order($formData)) { 38 if (!validate_plugin_order($formData)) {
24 throw new PluginConfigOrderException(); 39 throw new PluginConfigOrderException();
@@ -69,7 +84,7 @@ function validate_plugin_order($formData)
69 $orders = array(); 84 $orders = array();
70 foreach ($formData as $key => $value) { 85 foreach ($formData as $key => $value) {
71 // No duplicate order allowed. 86 // No duplicate order allowed.
72 if (in_array($value, $orders)) { 87 if (in_array($value, $orders, true)) {
73 return false; 88 return false;
74 } 89 }
75 90
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php
index e2c78ccc..d84418ad 100644
--- a/application/container/ContainerBuilder.php
+++ b/application/container/ContainerBuilder.php
@@ -4,14 +4,28 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Container; 5namespace Shaarli\Container;
6 6
7use malkusch\lock\mutex\FlockMutex;
8use Psr\Log\LoggerInterface;
7use Shaarli\Bookmark\BookmarkFileService; 9use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Bookmark\BookmarkServiceInterface; 10use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager; 11use Shaarli\Config\ConfigManager;
12use Shaarli\Feed\FeedBuilder;
13use Shaarli\Formatter\FormatterFactory;
14use Shaarli\Front\Controller\Visitor\ErrorController;
15use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
10use Shaarli\History; 16use Shaarli\History;
17use Shaarli\Http\HttpAccess;
18use Shaarli\Http\MetadataRetriever;
19use Shaarli\Netscape\NetscapeBookmarkUtils;
11use Shaarli\Plugin\PluginManager; 20use Shaarli\Plugin\PluginManager;
12use Shaarli\Render\PageBuilder; 21use Shaarli\Render\PageBuilder;
22use Shaarli\Render\PageCacheManager;
23use Shaarli\Security\CookieManager;
13use Shaarli\Security\LoginManager; 24use Shaarli\Security\LoginManager;
14use Shaarli\Security\SessionManager; 25use Shaarli\Security\SessionManager;
26use Shaarli\Thumbnailer;
27use Shaarli\Updater\Updater;
28use Shaarli\Updater\UpdaterUtils;
15 29
16/** 30/**
17 * Class ContainerBuilder 31 * Class ContainerBuilder
@@ -30,22 +44,43 @@ class ContainerBuilder
30 /** @var SessionManager */ 44 /** @var SessionManager */
31 protected $session; 45 protected $session;
32 46
47 /** @var CookieManager */
48 protected $cookieManager;
49
33 /** @var LoginManager */ 50 /** @var LoginManager */
34 protected $login; 51 protected $login;
35 52
36 public function __construct(ConfigManager $conf, SessionManager $session, LoginManager $login) 53 /** @var LoggerInterface */
37 { 54 protected $logger;
55
56 /** @var string|null */
57 protected $basePath = null;
58
59 public function __construct(
60 ConfigManager $conf,
61 SessionManager $session,
62 CookieManager $cookieManager,
63 LoginManager $login,
64 LoggerInterface $logger
65 ) {
38 $this->conf = $conf; 66 $this->conf = $conf;
39 $this->session = $session; 67 $this->session = $session;
40 $this->login = $login; 68 $this->login = $login;
69 $this->cookieManager = $cookieManager;
70 $this->logger = $logger;
41 } 71 }
42 72
43 public function build(): ShaarliContainer 73 public function build(): ShaarliContainer
44 { 74 {
45 $container = new ShaarliContainer(); 75 $container = new ShaarliContainer();
76
46 $container['conf'] = $this->conf; 77 $container['conf'] = $this->conf;
47 $container['sessionManager'] = $this->session; 78 $container['sessionManager'] = $this->session;
79 $container['cookieManager'] = $this->cookieManager;
48 $container['loginManager'] = $this->login; 80 $container['loginManager'] = $this->login;
81 $container['logger'] = $this->logger;
82 $container['basePath'] = $this->basePath;
83
49 $container['plugins'] = function (ShaarliContainer $container): PluginManager { 84 $container['plugins'] = function (ShaarliContainer $container): PluginManager {
50 return new PluginManager($container->conf); 85 return new PluginManager($container->conf);
51 }; 86 };
@@ -58,14 +93,20 @@ class ContainerBuilder
58 return new BookmarkFileService( 93 return new BookmarkFileService(
59 $container->conf, 94 $container->conf,
60 $container->history, 95 $container->history,
96 new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
61 $container->loginManager->isLoggedIn() 97 $container->loginManager->isLoggedIn()
62 ); 98 );
63 }; 99 };
64 100
101 $container['metadataRetriever'] = function (ShaarliContainer $container): MetadataRetriever {
102 return new MetadataRetriever($container->conf, $container->httpAccess);
103 };
104
65 $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder { 105 $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
66 return new PageBuilder( 106 return new PageBuilder(
67 $container->conf, 107 $container->conf,
68 $container->sessionManager->getSession(), 108 $container->sessionManager->getSession(),
109 $container->logger,
69 $container->bookmarkService, 110 $container->bookmarkService,
70 $container->sessionManager->generateToken(), 111 $container->sessionManager->generateToken(),
71 $container->loginManager->isLoggedIn() 112 $container->loginManager->isLoggedIn()
@@ -73,7 +114,65 @@ class ContainerBuilder
73 }; 114 };
74 115
75 $container['pluginManager'] = function (ShaarliContainer $container): PluginManager { 116 $container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
76 return new PluginManager($container->conf); 117 $pluginManager = new PluginManager($container->conf);
118
119 $pluginManager->load($container->conf->get('general.enabled_plugins'));
120
121 return $pluginManager;
122 };
123
124 $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
125 return new FormatterFactory(
126 $container->conf,
127 $container->loginManager->isLoggedIn()
128 );
129 };
130
131 $container['pageCacheManager'] = function (ShaarliContainer $container): PageCacheManager {
132 return new PageCacheManager(
133 $container->conf->get('resource.page_cache'),
134 $container->loginManager->isLoggedIn()
135 );
136 };
137
138 $container['feedBuilder'] = function (ShaarliContainer $container): FeedBuilder {
139 return new FeedBuilder(
140 $container->bookmarkService,
141 $container->formatterFactory->getFormatter(),
142 $container->environment,
143 $container->loginManager->isLoggedIn()
144 );
145 };
146
147 $container['thumbnailer'] = function (ShaarliContainer $container): Thumbnailer {
148 return new Thumbnailer($container->conf);
149 };
150
151 $container['httpAccess'] = function (): HttpAccess {
152 return new HttpAccess();
153 };
154
155 $container['netscapeBookmarkUtils'] = function (ShaarliContainer $container): NetscapeBookmarkUtils {
156 return new NetscapeBookmarkUtils($container->bookmarkService, $container->conf, $container->history);
157 };
158
159 $container['updater'] = function (ShaarliContainer $container): Updater {
160 return new Updater(
161 UpdaterUtils::read_updates_file($container->conf->get('resource.updates')),
162 $container->bookmarkService,
163 $container->conf,
164 $container->loginManager->isLoggedIn()
165 );
166 };
167
168 $container['notFoundHandler'] = function (ShaarliContainer $container): ErrorNotFoundController {
169 return new ErrorNotFoundController($container);
170 };
171 $container['errorHandler'] = function (ShaarliContainer $container): ErrorController {
172 return new ErrorController($container);
173 };
174 $container['phpErrorHandler'] = function (ShaarliContainer $container): ErrorController {
175 return new ErrorController($container);
77 }; 176 };
78 177
79 return $container; 178 return $container;
diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php
index 3fa9116e..3e5bd252 100644
--- a/application/container/ShaarliContainer.php
+++ b/application/container/ShaarliContainer.php
@@ -4,25 +4,50 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Container; 5namespace Shaarli\Container;
6 6
7use Psr\Log\LoggerInterface;
7use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
10use Shaarli\Feed\FeedBuilder;
11use Shaarli\Formatter\FormatterFactory;
9use Shaarli\History; 12use Shaarli\History;
13use Shaarli\Http\HttpAccess;
14use Shaarli\Http\MetadataRetriever;
15use Shaarli\Netscape\NetscapeBookmarkUtils;
10use Shaarli\Plugin\PluginManager; 16use Shaarli\Plugin\PluginManager;
11use Shaarli\Render\PageBuilder; 17use Shaarli\Render\PageBuilder;
18use Shaarli\Render\PageCacheManager;
19use Shaarli\Security\CookieManager;
12use Shaarli\Security\LoginManager; 20use Shaarli\Security\LoginManager;
13use Shaarli\Security\SessionManager; 21use Shaarli\Security\SessionManager;
22use Shaarli\Thumbnailer;
23use Shaarli\Updater\Updater;
14use Slim\Container; 24use Slim\Container;
15 25
16/** 26/**
17 * Extension of Slim container to document the injected objects. 27 * Extension of Slim container to document the injected objects.
18 * 28 *
29 * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`)
30 * @property BookmarkServiceInterface $bookmarkService
31 * @property CookieManager $cookieManager
19 * @property ConfigManager $conf 32 * @property ConfigManager $conf
20 * @property SessionManager $sessionManager 33 * @property mixed[] $environment $_SERVER automatically injected by Slim
21 * @property LoginManager $loginManager 34 * @property callable $errorHandler Overrides default Slim exception display
35 * @property FeedBuilder $feedBuilder
36 * @property FormatterFactory $formatterFactory
22 * @property History $history 37 * @property History $history
23 * @property BookmarkServiceInterface $bookmarkService 38 * @property HttpAccess $httpAccess
39 * @property LoginManager $loginManager
40 * @property LoggerInterface $logger
41 * @property MetadataRetriever $metadataRetriever
42 * @property NetscapeBookmarkUtils $netscapeBookmarkUtils
43 * @property callable $notFoundHandler Overrides default Slim exception display
24 * @property PageBuilder $pageBuilder 44 * @property PageBuilder $pageBuilder
45 * @property PageCacheManager $pageCacheManager
46 * @property callable $phpErrorHandler Overrides default Slim PHP error display
25 * @property PluginManager $pluginManager 47 * @property PluginManager $pluginManager
48 * @property SessionManager $sessionManager
49 * @property Thumbnailer $thumbnailer
50 * @property Updater $updater
26 */ 51 */
27class ShaarliContainer extends Container 52class ShaarliContainer extends Container
28{ 53{
diff --git a/application/feed/Cache.php b/application/feed/Cache.php
deleted file mode 100644
index e5d43e61..00000000
--- a/application/feed/Cache.php
+++ /dev/null
@@ -1,38 +0,0 @@
1<?php
2/**
3 * Cache utilities
4 */
5
6/**
7 * Purges all cached pages
8 *
9 * @param string $pageCacheDir page cache directory
10 *
11 * @return mixed an error string if the directory is missing
12 */
13function purgeCachedPages($pageCacheDir)
14{
15 if (! is_dir($pageCacheDir)) {
16 $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
17 error_log($error);
18 return $error;
19 }
20
21 array_map('unlink', glob($pageCacheDir.'/*.cache'));
22}
23
24/**
25 * Invalidates caches when the database is changed or the user logs out.
26 *
27 * @param string $pageCacheDir page cache directory
28 */
29function invalidateCaches($pageCacheDir)
30{
31 // Purge cache attached to session.
32 if (isset($_SESSION['tags'])) {
33 unset($_SESSION['tags']);
34 }
35
36 // Purge page cache shared by sessions.
37 purgeCachedPages($pageCacheDir);
38}
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php
index 40bd4f15..f70fce4f 100644
--- a/application/feed/FeedBuilder.php
+++ b/application/feed/FeedBuilder.php
@@ -43,22 +43,10 @@ class FeedBuilder
43 */ 43 */
44 protected $formatter; 44 protected $formatter;
45 45
46 /** 46 /** @var mixed[] $_SERVER */
47 * @var string RSS or ATOM feed.
48 */
49 protected $feedType;
50
51 /**
52 * @var array $_SERVER
53 */
54 protected $serverInfo; 47 protected $serverInfo;
55 48
56 /** 49 /**
57 * @var array $_GET
58 */
59 protected $userInput;
60
61 /**
62 * @var boolean True if the user is currently logged in, false otherwise. 50 * @var boolean True if the user is currently logged in, false otherwise.
63 */ 51 */
64 protected $isLoggedIn; 52 protected $isLoggedIn;
@@ -77,7 +65,6 @@ class FeedBuilder
77 * @var string server locale. 65 * @var string server locale.
78 */ 66 */
79 protected $locale; 67 protected $locale;
80
81 /** 68 /**
82 * @var DateTime Latest item date. 69 * @var DateTime Latest item date.
83 */ 70 */
@@ -88,37 +75,36 @@ class FeedBuilder
88 * 75 *
89 * @param BookmarkServiceInterface $linkDB LinkDB instance. 76 * @param BookmarkServiceInterface $linkDB LinkDB instance.
90 * @param BookmarkFormatter $formatter instance. 77 * @param BookmarkFormatter $formatter instance.
91 * @param string $feedType Type of feed.
92 * @param array $serverInfo $_SERVER. 78 * @param array $serverInfo $_SERVER.
93 * @param array $userInput $_GET.
94 * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise. 79 * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
95 */ 80 */
96 public function __construct($linkDB, $formatter, $feedType, $serverInfo, $userInput, $isLoggedIn) 81 public function __construct($linkDB, $formatter, $serverInfo, $isLoggedIn)
97 { 82 {
98 $this->linkDB = $linkDB; 83 $this->linkDB = $linkDB;
99 $this->formatter = $formatter; 84 $this->formatter = $formatter;
100 $this->feedType = $feedType;
101 $this->serverInfo = $serverInfo; 85 $this->serverInfo = $serverInfo;
102 $this->userInput = $userInput;
103 $this->isLoggedIn = $isLoggedIn; 86 $this->isLoggedIn = $isLoggedIn;
104 } 87 }
105 88
106 /** 89 /**
107 * Build data for feed templates. 90 * Build data for feed templates.
108 * 91 *
92 * @param string $feedType Type of feed (RSS/ATOM).
93 * @param array $userInput $_GET.
94 *
109 * @return array Formatted data for feeds templates. 95 * @return array Formatted data for feeds templates.
110 */ 96 */
111 public function buildData() 97 public function buildData(string $feedType, ?array $userInput)
112 { 98 {
113 // Search for untagged bookmarks 99 // Search for untagged bookmarks
114 if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { 100 if (isset($this->userInput['searchtags']) && empty($userInput['searchtags'])) {
115 $this->userInput['searchtags'] = false; 101 $userInput['searchtags'] = false;
116 } 102 }
117 103
118 // Optionally filter the results: 104 // Optionally filter the results:
119 $linksToDisplay = $this->linkDB->search($this->userInput); 105 $linksToDisplay = $this->linkDB->search($userInput ?? [], null, false, false, true);
120 106
121 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay)); 107 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
122 108
123 // Can't use array_keys() because $link is a LinkDB instance and not a real array. 109 // Can't use array_keys() because $link is a LinkDB instance and not a real array.
124 $keys = array(); 110 $keys = array();
@@ -130,15 +116,15 @@ class FeedBuilder
130 $this->formatter->addContextData('index_url', $pageaddr); 116 $this->formatter->addContextData('index_url', $pageaddr);
131 $linkDisplayed = array(); 117 $linkDisplayed = array();
132 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { 118 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
133 $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr); 119 $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr);
134 } 120 }
135 121
136 $data['language'] = $this->getTypeLanguage(); 122 $data['language'] = $this->getTypeLanguage($feedType);
137 $data['last_update'] = $this->getLatestDateFormatted(); 123 $data['last_update'] = $this->getLatestDateFormatted($feedType);
138 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn; 124 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
139 // Remove leading slash from REQUEST_URI. 125 // Remove leading path from REQUEST_URI (already contained in $pageaddr).
140 $data['self_link'] = escape(server_url($this->serverInfo)) 126 $requestUri = preg_replace('#(.*?/)(feed.*)#', '$2', escape($this->serverInfo['REQUEST_URI']));
141 . escape($this->serverInfo['REQUEST_URI']); 127 $data['self_link'] = $pageaddr . $requestUri;
142 $data['index_url'] = $pageaddr; 128 $data['index_url'] = $pageaddr;
143 $data['usepermalinks'] = $this->usePermalinks === true; 129 $data['usepermalinks'] = $this->usePermalinks === true;
144 $data['links'] = $linkDisplayed; 130 $data['links'] = $linkDisplayed;
@@ -147,17 +133,48 @@ class FeedBuilder
147 } 133 }
148 134
149 /** 135 /**
136 * Set this to true to use permalinks instead of direct bookmarks.
137 *
138 * @param boolean $usePermalinks true to force permalinks.
139 */
140 public function setUsePermalinks($usePermalinks)
141 {
142 $this->usePermalinks = $usePermalinks;
143 }
144
145 /**
146 * Set this to true to hide timestamps in feeds.
147 *
148 * @param boolean $hideDates true to enable.
149 */
150 public function setHideDates($hideDates)
151 {
152 $this->hideDates = $hideDates;
153 }
154
155 /**
156 * Set the locale. Used to show feed language.
157 *
158 * @param string $locale The locale (eg. 'fr_FR.UTF8').
159 */
160 public function setLocale($locale)
161 {
162 $this->locale = strtolower($locale);
163 }
164
165 /**
150 * Build a feed item (one per shaare). 166 * Build a feed item (one per shaare).
151 * 167 *
168 * @param string $feedType Type of feed (RSS/ATOM).
152 * @param Bookmark $link Single link array extracted from LinkDB. 169 * @param Bookmark $link Single link array extracted from LinkDB.
153 * @param string $pageaddr Index URL. 170 * @param string $pageaddr Index URL.
154 * 171 *
155 * @return array Link array with feed attributes. 172 * @return array Link array with feed attributes.
156 */ 173 */
157 protected function buildItem($link, $pageaddr) 174 protected function buildItem(string $feedType, $link, $pageaddr)
158 { 175 {
159 $data = $this->formatter->format($link); 176 $data = $this->formatter->format($link);
160 $data['guid'] = $pageaddr . '?' . $data['shorturl']; 177 $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl'];
161 if ($this->usePermalinks === true) { 178 if ($this->usePermalinks === true) {
162 $permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>'; 179 $permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
163 } else { 180 } else {
@@ -165,13 +182,13 @@ class FeedBuilder
165 } 182 }
166 $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink; 183 $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
167 184
168 $data['pub_iso_date'] = $this->getIsoDate($data['created']); 185 $data['pub_iso_date'] = $this->getIsoDate($feedType, $data['created']);
169 186
170 // atom:entry elements MUST contain exactly one atom:updated element. 187 // atom:entry elements MUST contain exactly one atom:updated element.
171 if (!empty($link->getUpdated())) { 188 if (!empty($link->getUpdated())) {
172 $data['up_iso_date'] = $this->getIsoDate($data['updated'], DateTime::ATOM); 189 $data['up_iso_date'] = $this->getIsoDate($feedType, $data['updated'], DateTime::ATOM);
173 } else { 190 } else {
174 $data['up_iso_date'] = $this->getIsoDate($data['created'], DateTime::ATOM); 191 $data['up_iso_date'] = $this->getIsoDate($feedType, $data['created'], DateTime::ATOM);
175 } 192 }
176 193
177 // Save the more recent item. 194 // Save the more recent item.
@@ -186,51 +203,23 @@ class FeedBuilder
186 } 203 }
187 204
188 /** 205 /**
189 * Set this to true to use permalinks instead of direct bookmarks.
190 *
191 * @param boolean $usePermalinks true to force permalinks.
192 */
193 public function setUsePermalinks($usePermalinks)
194 {
195 $this->usePermalinks = $usePermalinks;
196 }
197
198 /**
199 * Set this to true to hide timestamps in feeds.
200 *
201 * @param boolean $hideDates true to enable.
202 */
203 public function setHideDates($hideDates)
204 {
205 $this->hideDates = $hideDates;
206 }
207
208 /**
209 * Set the locale. Used to show feed language.
210 *
211 * @param string $locale The locale (eg. 'fr_FR.UTF8').
212 */
213 public function setLocale($locale)
214 {
215 $this->locale = strtolower($locale);
216 }
217
218 /**
219 * Get the language according to the feed type, based on the locale: 206 * Get the language according to the feed type, based on the locale:
220 * 207 *
221 * - RSS format: en-us (default: 'en-en'). 208 * - RSS format: en-us (default: 'en-en').
222 * - ATOM format: fr (default: 'en'). 209 * - ATOM format: fr (default: 'en').
223 * 210 *
211 * @param string $feedType Type of feed (RSS/ATOM).
212 *
224 * @return string The language. 213 * @return string The language.
225 */ 214 */
226 public function getTypeLanguage() 215 protected function getTypeLanguage(string $feedType)
227 { 216 {
228 // Use the locale do define the language, if available. 217 // Use the locale do define the language, if available.
229 if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) { 218 if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) {
230 $length = ($this->feedType === self::$FEED_RSS) ? 5 : 2; 219 $length = ($feedType === self::$FEED_RSS) ? 5 : 2;
231 return str_replace('_', '-', substr($this->locale, 0, $length)); 220 return str_replace('_', '-', substr($this->locale, 0, $length));
232 } 221 }
233 return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en'; 222 return ($feedType === self::$FEED_RSS) ? 'en-en' : 'en';
234 } 223 }
235 224
236 /** 225 /**
@@ -238,32 +227,35 @@ class FeedBuilder
238 * 227 *
239 * Return an empty string if invalid DateTime is passed. 228 * Return an empty string if invalid DateTime is passed.
240 * 229 *
230 * @param string $feedType Type of feed (RSS/ATOM).
231 *
241 * @return string Formatted date. 232 * @return string Formatted date.
242 */ 233 */
243 protected function getLatestDateFormatted() 234 protected function getLatestDateFormatted(string $feedType)
244 { 235 {
245 if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) { 236 if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) {
246 return ''; 237 return '';
247 } 238 }
248 239
249 $type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM; 240 $type = ($feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
250 return $this->latestDate->format($type); 241 return $this->latestDate->format($type);
251 } 242 }
252 243
253 /** 244 /**
254 * Get ISO date from DateTime according to feed type. 245 * Get ISO date from DateTime according to feed type.
255 * 246 *
247 * @param string $feedType Type of feed (RSS/ATOM).
256 * @param DateTime $date Date to format. 248 * @param DateTime $date Date to format.
257 * @param string|bool $format Force format. 249 * @param string|bool $format Force format.
258 * 250 *
259 * @return string Formatted date. 251 * @return string Formatted date.
260 */ 252 */
261 protected function getIsoDate(DateTime $date, $format = false) 253 protected function getIsoDate(string $feedType, DateTime $date, $format = false)
262 { 254 {
263 if ($format !== false) { 255 if ($format !== false) {
264 return $date->format($format); 256 return $date->format($format);
265 } 257 }
266 if ($this->feedType == self::$FEED_RSS) { 258 if ($feedType == self::$FEED_RSS) {
267 return $date->format(DateTime::RSS); 259 return $date->format(DateTime::RSS);
268 } 260 }
269 return $date->format(DateTime::ATOM); 261 return $date->format(DateTime::ATOM);
@@ -275,21 +267,22 @@ class FeedBuilder
275 * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. 267 * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
276 * If 'nb' is set to 'all', display all filtered bookmarks (max parameter). 268 * If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
277 * 269 *
278 * @param int $max maximum number of bookmarks to display. 270 * @param int $max maximum number of bookmarks to display.
271 * @param array $userInput $_GET.
279 * 272 *
280 * @return int number of bookmarks to display. 273 * @return int number of bookmarks to display.
281 */ 274 */
282 public function getNbLinks($max) 275 protected function getNbLinks($max, ?array $userInput)
283 { 276 {
284 if (empty($this->userInput['nb'])) { 277 if (empty($userInput['nb'])) {
285 return self::$DEFAULT_NB_LINKS; 278 return self::$DEFAULT_NB_LINKS;
286 } 279 }
287 280
288 if ($this->userInput['nb'] == 'all') { 281 if ($userInput['nb'] == 'all') {
289 return $max; 282 return $max;
290 } 283 }
291 284
292 $intNb = intval($this->userInput['nb']); 285 $intNb = intval($userInput['nb']);
293 if (!is_int($intNb) || $intNb == 0) { 286 if (!is_int($intNb) || $intNb == 0) {
294 return self::$DEFAULT_NB_LINKS; 287 return self::$DEFAULT_NB_LINKS;
295 } 288 }
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php
index c6c59064..d58a5e39 100644
--- a/application/formatter/BookmarkDefaultFormatter.php
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -12,10 +12,13 @@ namespace Shaarli\Formatter;
12 */ 12 */
13class BookmarkDefaultFormatter extends BookmarkFormatter 13class BookmarkDefaultFormatter extends BookmarkFormatter
14{ 14{
15 const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT';
16 const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|';
17
15 /** 18 /**
16 * @inheritdoc 19 * @inheritdoc
17 */ 20 */
18 public function formatTitle($bookmark) 21 protected function formatTitle($bookmark)
19 { 22 {
20 return escape($bookmark->getTitle()); 23 return escape($bookmark->getTitle());
21 } 24 }
@@ -23,10 +26,28 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
23 /** 26 /**
24 * @inheritdoc 27 * @inheritdoc
25 */ 28 */
26 public function formatDescription($bookmark) 29 protected function formatTitleHtml($bookmark)
30 {
31 $title = $this->tokenizeSearchHighlightField(
32 $bookmark->getTitle() ?? '',
33 $bookmark->getAdditionalContentEntry('search_highlight')['title'] ?? []
34 );
35
36 return $this->replaceTokens(escape($title));
37 }
38
39 /**
40 * @inheritdoc
41 */
42 protected function formatDescription($bookmark)
27 { 43 {
28 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; 44 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
29 return format_description(escape($bookmark->getDescription()), $indexUrl); 45 $description = $this->tokenizeSearchHighlightField(
46 $bookmark->getDescription() ?? '',
47 $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? []
48 );
49
50 return $this->replaceTokens(format_description(escape($description), $indexUrl));
30 } 51 }
31 52
32 /** 53 /**
@@ -40,7 +61,27 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
40 /** 61 /**
41 * @inheritdoc 62 * @inheritdoc
42 */ 63 */
43 public function formatTagString($bookmark) 64 protected function formatTagListHtml($bookmark)
65 {
66 if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) {
67 return $this->formatTagList($bookmark);
68 }
69
70 $tags = $this->tokenizeSearchHighlightField(
71 $bookmark->getTagsString(),
72 $bookmark->getAdditionalContentEntry('search_highlight')['tags']
73 );
74 $tags = $this->filterTagList(explode(' ', $tags));
75 $tags = escape($tags);
76 $tags = $this->replaceTokensArray($tags);
77
78 return $tags;
79 }
80
81 /**
82 * @inheritdoc
83 */
84 protected function formatTagString($bookmark)
44 { 85 {
45 return implode(' ', $this->formatTagList($bookmark)); 86 return implode(' ', $this->formatTagList($bookmark));
46 } 87 }
@@ -48,13 +89,12 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
48 /** 89 /**
49 * @inheritdoc 90 * @inheritdoc
50 */ 91 */
51 public function formatUrl($bookmark) 92 protected function formatUrl($bookmark)
52 { 93 {
53 if (! empty($this->contextData['index_url']) && ( 94 if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
54 startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') 95 return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
55 )) {
56 return $this->contextData['index_url'] . escape($bookmark->getUrl());
57 } 96 }
97
58 return escape($bookmark->getUrl()); 98 return escape($bookmark->getUrl());
59 } 99 }
60 100
@@ -63,19 +103,107 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
63 */ 103 */
64 protected function formatRealUrl($bookmark) 104 protected function formatRealUrl($bookmark)
65 { 105 {
66 if (! empty($this->contextData['index_url']) && ( 106 if ($bookmark->isNote()) {
67 startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') 107 if (isset($this->contextData['index_url'])) {
68 )) { 108 $prefix = rtrim($this->contextData['index_url'], '/') . '/';
69 return $this->contextData['index_url'] . escape($bookmark->getUrl()); 109 }
110
111 if (isset($this->contextData['base_path'])) {
112 $prefix = rtrim($this->contextData['base_path'], '/') . '/';
113 }
114
115 return escape($prefix ?? '') . escape(ltrim($bookmark->getUrl(), '/'));
70 } 116 }
117
71 return escape($bookmark->getUrl()); 118 return escape($bookmark->getUrl());
72 } 119 }
73 120
74 /** 121 /**
75 * @inheritdoc 122 * @inheritdoc
76 */ 123 */
124 protected function formatUrlHtml($bookmark)
125 {
126 $url = $this->tokenizeSearchHighlightField(
127 $bookmark->getUrl() ?? '',
128 $bookmark->getAdditionalContentEntry('search_highlight')['url'] ?? []
129 );
130
131 return $this->replaceTokens(escape($url));
132 }
133
134 /**
135 * @inheritdoc
136 */
77 protected function formatThumbnail($bookmark) 137 protected function formatThumbnail($bookmark)
78 { 138 {
79 return escape($bookmark->getThumbnail()); 139 return escape($bookmark->getThumbnail());
80 } 140 }
141
142 /**
143 * Insert search highlight token in provided field content based on a list of search result positions
144 *
145 * @param string $fieldContent
146 * @param array|null $positions List of of search results with 'start' and 'end' positions.
147 *
148 * @return string Updated $fieldContent.
149 */
150 protected function tokenizeSearchHighlightField(string $fieldContent, ?array $positions): string
151 {
152 if (empty($positions)) {
153 return $fieldContent;
154 }
155
156 $insertedTokens = 0;
157 $tokenLength = strlen(static::SEARCH_HIGHLIGHT_OPEN);
158 foreach ($positions as $position) {
159 $position = [
160 'start' => $position['start'] + ($insertedTokens * $tokenLength),
161 'end' => $position['end'] + ($insertedTokens * $tokenLength),
162 ];
163
164 $content = mb_substr($fieldContent, 0, $position['start']);
165 $content .= static::SEARCH_HIGHLIGHT_OPEN;
166 $content .= mb_substr($fieldContent, $position['start'], $position['end'] - $position['start']);
167 $content .= static::SEARCH_HIGHLIGHT_CLOSE;
168 $content .= mb_substr($fieldContent, $position['end']);
169
170 $fieldContent = $content;
171
172 $insertedTokens += 2;
173 }
174
175 return $fieldContent;
176 }
177
178 /**
179 * Replace search highlight tokens with HTML highlighted span.
180 *
181 * @param string $fieldContent
182 *
183 * @return string updated content.
184 */
185 protected function replaceTokens(string $fieldContent): string
186 {
187 return str_replace(
188 [static::SEARCH_HIGHLIGHT_OPEN, static::SEARCH_HIGHLIGHT_CLOSE],
189 ['<span class="search-highlight">', '</span>'],
190 $fieldContent
191 );
192 }
193
194 /**
195 * Apply replaceTokens to an array of content strings.
196 *
197 * @param string[] $fieldContents
198 *
199 * @return array
200 */
201 protected function replaceTokensArray(array $fieldContents): array
202 {
203 foreach ($fieldContents as &$entry) {
204 $entry = $this->replaceTokens($entry);
205 }
206
207 return $fieldContents;
208 }
81} 209}
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php
index a80d83fc..e1b7f705 100644
--- a/application/formatter/BookmarkFormatter.php
+++ b/application/formatter/BookmarkFormatter.php
@@ -2,15 +2,38 @@
2 2
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use DateTime; 5use DateTimeInterface;
6use Shaarli\Config\ConfigManager;
7use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigManager;
8 8
9/** 9/**
10 * Class BookmarkFormatter 10 * Class BookmarkFormatter
11 * 11 *
12 * Abstract class processing all bookmark attributes through methods designed to be overridden. 12 * Abstract class processing all bookmark attributes through methods designed to be overridden.
13 * 13 *
14 * List of available formatted fields:
15 * - id ID
16 * - shorturl Unique identifier, used in permalinks
17 * - url URL, can be altered in some way, e.g. passing through an HTTP reverse proxy
18 * - real_url (legacy) same as `url`
19 * - url_html URL to be displayed in HTML content (it can contain HTML tags)
20 * - title Title
21 * - title_html Title to be displayed in HTML content (it can contain HTML tags)
22 * - description Description content. It most likely contains HTML tags
23 * - thumbnail Thumbnail: path to local cache file, false if there is none, null if hasn't been retrieved
24 * - taglist List of tags (array)
25 * - taglist_urlencoded List of tags (array) URL encoded: it must be used to create a link to a URL containing a tag
26 * - taglist_html List of tags (array) to be displayed in HTML content (it can contain HTML tags)
27 * - tags Tags separated by a single whitespace
28 * - tags_urlencoded Tags separated by a single whitespace, URL encoded: must be used to create a link
29 * - sticky Is sticky (bool)
30 * - private Is private (bool)
31 * - class Additional CSS class
32 * - created Creation DateTime
33 * - updated Last edit DateTime
34 * - timestamp Creation timestamp
35 * - updated_timestamp Last edit timestamp
36 *
14 * @package Shaarli\Formatter 37 * @package Shaarli\Formatter
15 */ 38 */
16abstract class BookmarkFormatter 39abstract class BookmarkFormatter
@@ -55,11 +78,16 @@ abstract class BookmarkFormatter
55 $out['shorturl'] = $this->formatShortUrl($bookmark); 78 $out['shorturl'] = $this->formatShortUrl($bookmark);
56 $out['url'] = $this->formatUrl($bookmark); 79 $out['url'] = $this->formatUrl($bookmark);
57 $out['real_url'] = $this->formatRealUrl($bookmark); 80 $out['real_url'] = $this->formatRealUrl($bookmark);
81 $out['url_html'] = $this->formatUrlHtml($bookmark);
58 $out['title'] = $this->formatTitle($bookmark); 82 $out['title'] = $this->formatTitle($bookmark);
83 $out['title_html'] = $this->formatTitleHtml($bookmark);
59 $out['description'] = $this->formatDescription($bookmark); 84 $out['description'] = $this->formatDescription($bookmark);
60 $out['thumbnail'] = $this->formatThumbnail($bookmark); 85 $out['thumbnail'] = $this->formatThumbnail($bookmark);
61 $out['taglist'] = $this->formatTagList($bookmark); 86 $out['taglist'] = $this->formatTagList($bookmark);
87 $out['taglist_urlencoded'] = $this->formatTagListUrlEncoded($bookmark);
88 $out['taglist_html'] = $this->formatTagListHtml($bookmark);
62 $out['tags'] = $this->formatTagString($bookmark); 89 $out['tags'] = $this->formatTagString($bookmark);
90 $out['tags_urlencoded'] = $this->formatTagStringUrlEncoded($bookmark);
63 $out['sticky'] = $bookmark->isSticky(); 91 $out['sticky'] = $bookmark->isSticky();
64 $out['private'] = $bookmark->isPrivate(); 92 $out['private'] = $bookmark->isPrivate();
65 $out['class'] = $this->formatClass($bookmark); 93 $out['class'] = $this->formatClass($bookmark);
@@ -67,6 +95,7 @@ abstract class BookmarkFormatter
67 $out['updated'] = $this->formatUpdated($bookmark); 95 $out['updated'] = $this->formatUpdated($bookmark);
68 $out['timestamp'] = $this->formatCreatedTimestamp($bookmark); 96 $out['timestamp'] = $this->formatCreatedTimestamp($bookmark);
69 $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark); 97 $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark);
98
70 return $out; 99 return $out;
71 } 100 }
72 101
@@ -80,6 +109,8 @@ abstract class BookmarkFormatter
80 public function addContextData($key, $value) 109 public function addContextData($key, $value)
81 { 110 {
82 $this->contextData[$key] = $value; 111 $this->contextData[$key] = $value;
112
113 return $this;
83 } 114 }
84 115
85 /** 116 /**
@@ -128,7 +159,19 @@ abstract class BookmarkFormatter
128 */ 159 */
129 protected function formatRealUrl($bookmark) 160 protected function formatRealUrl($bookmark)
130 { 161 {
131 return $bookmark->getUrl(); 162 return $this->formatUrl($bookmark);
163 }
164
165 /**
166 * Format Url Html: to be displayed in HTML content, it can contains HTML tags.
167 *
168 * @param Bookmark $bookmark instance
169 *
170 * @return string formatted Url HTML
171 */
172 protected function formatUrlHtml($bookmark)
173 {
174 return $this->formatUrl($bookmark);
132 } 175 }
133 176
134 /** 177 /**
@@ -144,6 +187,18 @@ abstract class BookmarkFormatter
144 } 187 }
145 188
146 /** 189 /**
190 * Format Title HTML: to be displayed in HTML content, it can contains HTML tags.
191 *
192 * @param Bookmark $bookmark instance
193 *
194 * @return string formatted Title
195 */
196 protected function formatTitleHtml($bookmark)
197 {
198 return $bookmark->getTitle();
199 }
200
201 /**
147 * Format Description 202 * Format Description
148 * 203 *
149 * @param Bookmark $bookmark instance 204 * @param Bookmark $bookmark instance
@@ -180,6 +235,30 @@ abstract class BookmarkFormatter
180 } 235 }
181 236
182 /** 237 /**
238 * Format Url Encoded Tags
239 *
240 * @param Bookmark $bookmark instance
241 *
242 * @return array formatted Tags
243 */
244 protected function formatTagListUrlEncoded($bookmark)
245 {
246 return array_map('urlencode', $this->filterTagList($bookmark->getTags()));
247 }
248
249 /**
250 * Format Tags HTML: to be displayed in HTML content, it can contains HTML tags.
251 *
252 * @param Bookmark $bookmark instance
253 *
254 * @return array formatted Tags
255 */
256 protected function formatTagListHtml($bookmark)
257 {
258 return $this->formatTagList($bookmark);
259 }
260
261 /**
183 * Format TagString 262 * Format TagString
184 * 263 *
185 * @param Bookmark $bookmark instance 264 * @param Bookmark $bookmark instance
@@ -192,6 +271,18 @@ abstract class BookmarkFormatter
192 } 271 }
193 272
194 /** 273 /**
274 * Format TagString
275 *
276 * @param Bookmark $bookmark instance
277 *
278 * @return string formatted TagString
279 */
280 protected function formatTagStringUrlEncoded($bookmark)
281 {
282 return implode(' ', $this->formatTagListUrlEncoded($bookmark));
283 }
284
285 /**
195 * Format Class 286 * Format Class
196 * Used to add specific CSS class for a link 287 * Used to add specific CSS class for a link
197 * 288 *
@@ -209,7 +300,7 @@ abstract class BookmarkFormatter
209 * 300 *
210 * @param Bookmark $bookmark instance 301 * @param Bookmark $bookmark instance
211 * 302 *
212 * @return DateTime instance 303 * @return DateTimeInterface instance
213 */ 304 */
214 protected function formatCreated(Bookmark $bookmark) 305 protected function formatCreated(Bookmark $bookmark)
215 { 306 {
@@ -221,7 +312,7 @@ abstract class BookmarkFormatter
221 * 312 *
222 * @param Bookmark $bookmark instance 313 * @param Bookmark $bookmark instance
223 * 314 *
224 * @return DateTime instance 315 * @return DateTimeInterface instance
225 */ 316 */
226 protected function formatUpdated(Bookmark $bookmark) 317 protected function formatUpdated(Bookmark $bookmark)
227 { 318 {
diff --git a/application/formatter/BookmarkMarkdownExtraFormatter.php b/application/formatter/BookmarkMarkdownExtraFormatter.php
new file mode 100644
index 00000000..0694b23f
--- /dev/null
+++ b/application/formatter/BookmarkMarkdownExtraFormatter.php
@@ -0,0 +1,24 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Class BookmarkMarkdownExtraFormatter
9 *
10 * Format bookmark description into MarkdownExtra format.
11 *
12 * @see https://michelf.ca/projects/php-markdown/extra/
13 *
14 * @package Shaarli\Formatter
15 */
16class BookmarkMarkdownExtraFormatter extends BookmarkMarkdownFormatter
17{
18 public function __construct(ConfigManager $conf, bool $isLoggedIn)
19 {
20 parent::__construct($conf, $isLoggedIn);
21
22 $this->parsedown = new \ParsedownExtra();
23 }
24}
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
index 077e5312..f7714be9 100644
--- a/application/formatter/BookmarkMarkdownFormatter.php
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -56,7 +56,10 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
56 return parent::formatDescription($bookmark); 56 return parent::formatDescription($bookmark);
57 } 57 }
58 58
59 $processedDescription = $bookmark->getDescription(); 59 $processedDescription = $this->tokenizeSearchHighlightField(
60 $bookmark->getDescription() ?? '',
61 $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? []
62 );
60 $processedDescription = $this->filterProtocols($processedDescription); 63 $processedDescription = $this->filterProtocols($processedDescription);
61 $processedDescription = $this->formatHashTags($processedDescription); 64 $processedDescription = $this->formatHashTags($processedDescription);
62 $processedDescription = $this->reverseEscapedHtml($processedDescription); 65 $processedDescription = $this->reverseEscapedHtml($processedDescription);
@@ -65,6 +68,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
65 ->setBreaksEnabled(true) 68 ->setBreaksEnabled(true)
66 ->text($processedDescription); 69 ->text($processedDescription);
67 $processedDescription = $this->sanitizeHtml($processedDescription); 70 $processedDescription = $this->sanitizeHtml($processedDescription);
71 $processedDescription = $this->replaceTokens($processedDescription);
68 72
69 if (!empty($processedDescription)) { 73 if (!empty($processedDescription)) {
70 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; 74 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
@@ -114,7 +118,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
114 118
115 /** 119 /**
116 * Replace hashtag in Markdown links format 120 * Replace hashtag in Markdown links format
117 * E.g. `#hashtag` becomes `[#hashtag](?addtag=hashtag)` 121 * E.g. `#hashtag` becomes `[#hashtag](./add-tag/hashtag)`
118 * It includes the index URL if specified. 122 * It includes the index URL if specified.
119 * 123 *
120 * @param string $description 124 * @param string $description
@@ -133,7 +137,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
133 * \p{Mn} - any non marking space (accents, umlauts, etc) 137 * \p{Mn} - any non marking space (accents, umlauts, etc)
134 */ 138 */
135 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 139 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
136 $replacement = '$1[#$2]('. $indexUrl .'?addtag=$2)'; 140 $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)';
137 141
138 $descriptionLines = explode(PHP_EOL, $description); 142 $descriptionLines = explode(PHP_EOL, $description);
139 $descriptionOut = ''; 143 $descriptionOut = '';
diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php
index 5f282f68..a029579f 100644
--- a/application/formatter/FormatterFactory.php
+++ b/application/formatter/FormatterFactory.php
@@ -38,7 +38,7 @@ class FormatterFactory
38 * 38 *
39 * @return BookmarkFormatter instance. 39 * @return BookmarkFormatter instance.
40 */ 40 */
41 public function getFormatter(string $type = null) 41 public function getFormatter(string $type = null): BookmarkFormatter
42 { 42 {
43 $type = $type ? $type : $this->conf->get('formatter', 'default'); 43 $type = $type ? $type : $this->conf->get('formatter', 'default');
44 $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter'; 44 $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter';
diff --git a/application/front/ShaarliAdminMiddleware.php b/application/front/ShaarliAdminMiddleware.php
new file mode 100644
index 00000000..35ce4a3b
--- /dev/null
+++ b/application/front/ShaarliAdminMiddleware.php
@@ -0,0 +1,27 @@
1<?php
2
3namespace Shaarli\Front;
4
5use Slim\Http\Request;
6use Slim\Http\Response;
7
8/**
9 * Middleware used for controller requiring to be authenticated.
10 * It extends ShaarliMiddleware, and just make sure that the user is authenticated.
11 * Otherwise, it redirects to the login page.
12 */
13class ShaarliAdminMiddleware extends ShaarliMiddleware
14{
15 public function __invoke(Request $request, Response $response, callable $next): Response
16 {
17 $this->initBasePath($request);
18
19 if (true !== $this->container->loginManager->isLoggedIn()) {
20 $returnUrl = urlencode($this->container->environment['REQUEST_URI']);
21
22 return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
23 }
24
25 return parent::__invoke($request, $response, $next);
26 }
27}
diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php
index fa6c6467..d1aa1399 100644
--- a/application/front/ShaarliMiddleware.php
+++ b/application/front/ShaarliMiddleware.php
@@ -3,7 +3,7 @@
3namespace Shaarli\Front; 3namespace Shaarli\Front;
4 4
5use Shaarli\Container\ShaarliContainer; 5use Shaarli\Container\ShaarliContainer;
6use Shaarli\Front\Exception\ShaarliException; 6use Shaarli\Front\Exception\UnauthorizedException;
7use Slim\Http\Request; 7use Slim\Http\Request;
8use Slim\Http\Response; 8use Slim\Http\Response;
9 9
@@ -24,6 +24,8 @@ class ShaarliMiddleware
24 24
25 /** 25 /**
26 * Middleware execution: 26 * Middleware execution:
27 * - run updates
28 * - if not logged in open shaarli, redirect to login
27 * - execute the controller 29 * - execute the controller
28 * - return the response 30 * - return the response
29 * 31 *
@@ -35,23 +37,78 @@ class ShaarliMiddleware
35 * 37 *
36 * @return Response response. 38 * @return Response response.
37 */ 39 */
38 public function __invoke(Request $request, Response $response, callable $next) 40 public function __invoke(Request $request, Response $response, callable $next): Response
39 { 41 {
42 $this->initBasePath($request);
43
40 try { 44 try {
41 $response = $next($request, $response); 45 if (!is_file($this->container->conf->getConfigFileExt())
42 } catch (ShaarliException $e) { 46 && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
43 $this->container->pageBuilder->assign('message', $e->getMessage()); 47 ) {
44 if ($this->container->conf->get('dev.debug', false)) { 48 return $response->withRedirect($this->container->basePath . '/install');
45 $this->container->pageBuilder->assign(
46 'stacktrace',
47 nl2br(get_class($this) .': '. $e->getTraceAsString())
48 );
49 } 49 }
50 50
51 $response = $response->withStatus($e->getCode()); 51 $this->runUpdates();
52 $response = $response->write($this->container->pageBuilder->render('error')); 52 $this->checkOpenShaarli($request, $response, $next);
53
54 return $next($request, $response);
55 } catch (UnauthorizedException $e) {
56 $returnUrl = urlencode($this->container->environment['REQUEST_URI']);
57
58 return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
59 }
60 // Other exceptions are handled by ErrorController
61 }
62
63 /**
64 * Run the updater for every requests processed while logged in.
65 */
66 protected function runUpdates(): void
67 {
68 if ($this->container->loginManager->isLoggedIn() !== true) {
69 return;
70 }
71
72 $this->container->updater->setBasePath($this->container->basePath);
73 $newUpdates = $this->container->updater->update();
74 if (!empty($newUpdates)) {
75 $this->container->updater->writeUpdates(
76 $this->container->conf->get('resource.updates'),
77 $this->container->updater->getDoneUpdates()
78 );
79
80 $this->container->pageCacheManager->invalidateCaches();
81 }
82 }
83
84 /**
85 * Access is denied to most pages with `hide_public_links` + `force_login` settings.
86 */
87 protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool
88 {
89 if (// if the user isn't logged in
90 !$this->container->loginManager->isLoggedIn()
91 // and Shaarli doesn't have public content...
92 && $this->container->conf->get('privacy.hide_public_links')
93 // and is configured to enforce the login
94 && $this->container->conf->get('privacy.force_login')
95 // and the current page isn't already the login page
96 // and the user is not requesting a feed (which would lead to a different content-type as expected)
97 && !in_array($next->getName(), ['login', 'processLogin', 'atom', 'rss'], true)
98 ) {
99 throw new UnauthorizedException();
53 } 100 }
54 101
55 return $response; 102 return true;
103 }
104
105 /**
106 * Initialize the URL base path if it hasn't been defined yet.
107 */
108 protected function initBasePath(Request $request): void
109 {
110 if (null === $this->container->basePath) {
111 $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/');
112 }
56 } 113 }
57} 114}
diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php
new file mode 100644
index 00000000..0ed7ad81
--- /dev/null
+++ b/application/front/controller/admin/ConfigureController.php
@@ -0,0 +1,126 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Languages;
8use Shaarli\Render\TemplatePage;
9use Shaarli\Render\ThemeUtils;
10use Shaarli\Thumbnailer;
11use Slim\Http\Request;
12use Slim\Http\Response;
13use Throwable;
14
15/**
16 * Class ConfigureController
17 *
18 * Slim controller used to handle Shaarli configuration page (display + save new config).
19 */
20class ConfigureController extends ShaarliAdminController
21{
22 /**
23 * GET /admin/configure - Displays the configuration page
24 */
25 public function index(Request $request, Response $response): Response
26 {
27 $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli'));
28 $this->assignView('theme', $this->container->conf->get('resource.theme'));
29 $this->assignView(
30 'theme_available',
31 ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl'))
32 );
33 $this->assignView('formatter_available', ['default', 'markdown', 'markdownExtra']);
34 list($continents, $cities) = generateTimeZoneData(
35 timezone_identifiers_list(),
36 $this->container->conf->get('general.timezone')
37 );
38 $this->assignView('continents', $continents);
39 $this->assignView('cities', $cities);
40 $this->assignView('retrieve_description', $this->container->conf->get('general.retrieve_description', false));
41 $this->assignView('private_links_default', $this->container->conf->get('privacy.default_private_links', false));
42 $this->assignView(
43 'session_protection_disabled',
44 $this->container->conf->get('security.session_protection_disabled', false)
45 );
46 $this->assignView('enable_rss_permalinks', $this->container->conf->get('feed.rss_permalinks', false));
47 $this->assignView('enable_update_check', $this->container->conf->get('updates.check_updates', true));
48 $this->assignView('hide_public_links', $this->container->conf->get('privacy.hide_public_links', false));
49 $this->assignView('api_enabled', $this->container->conf->get('api.enabled', true));
50 $this->assignView('api_secret', $this->container->conf->get('api.secret'));
51 $this->assignView('languages', Languages::getAvailableLanguages());
52 $this->assignView('gd_enabled', extension_loaded('gd'));
53 $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
54 $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
55
56 return $response->write($this->render(TemplatePage::CONFIGURE));
57 }
58
59 /**
60 * POST /admin/configure - Update Shaarli's configuration
61 */
62 public function save(Request $request, Response $response): Response
63 {
64 $this->checkToken($request);
65
66 $continent = $request->getParam('continent');
67 $city = $request->getParam('city');
68 $tz = 'UTC';
69 if (null !== $continent && null !== $city && isTimeZoneValid($continent, $city)) {
70 $tz = $continent . '/' . $city;
71 }
72
73 $this->container->conf->set('general.timezone', $tz);
74 $this->container->conf->set('general.title', escape($request->getParam('title')));
75 $this->container->conf->set('general.header_link', escape($request->getParam('titleLink')));
76 $this->container->conf->set('general.retrieve_description', !empty($request->getParam('retrieveDescription')));
77 $this->container->conf->set('resource.theme', escape($request->getParam('theme')));
78 $this->container->conf->set(
79 'security.session_protection_disabled',
80 !empty($request->getParam('disablesessionprotection'))
81 );
82 $this->container->conf->set(
83 'privacy.default_private_links',
84 !empty($request->getParam('privateLinkByDefault'))
85 );
86 $this->container->conf->set('feed.rss_permalinks', !empty($request->getParam('enableRssPermalinks')));
87 $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
88 $this->container->conf->set('privacy.hide_public_links', !empty($request->getParam('hidePublicLinks')));
89 $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
90 $this->container->conf->set('api.secret', escape($request->getParam('apiSecret')));
91 $this->container->conf->set('formatter', escape($request->getParam('formatter')));
92
93 if (!empty($request->getParam('language'))) {
94 $this->container->conf->set('translation.language', escape($request->getParam('language')));
95 }
96
97 $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE;
98 if ($thumbnailsMode !== Thumbnailer::MODE_NONE
99 && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
100 ) {
101 $this->saveWarningMessage(
102 t('You have enabled or changed thumbnails mode.') .
103 '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>'
104 );
105 }
106 $this->container->conf->set('thumbnails.mode', $thumbnailsMode);
107
108 try {
109 $this->container->conf->write($this->container->loginManager->isLoggedIn());
110 $this->container->history->updateSettings();
111 $this->container->pageCacheManager->invalidateCaches();
112 } catch (Throwable $e) {
113 $this->assignView('message', t('Error while writing config file after configuration update.'));
114
115 if ($this->container->conf->get('dev.debug', false)) {
116 $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString());
117 }
118
119 return $response->write($this->render('error'));
120 }
121
122 $this->saveSuccessMessage(t('Configuration was saved.'));
123
124 return $this->redirect($response, '/admin/configure');
125 }
126}
diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php
new file mode 100644
index 00000000..2be957fa
--- /dev/null
+++ b/application/front/controller/admin/ExportController.php
@@ -0,0 +1,80 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use DateTime;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Render\TemplatePage;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13/**
14 * Class ExportController
15 *
16 * Slim controller used to display Shaarli data export page,
17 * and process the bookmarks export as a Netscape Bookmarks file.
18 */
19class ExportController extends ShaarliAdminController
20{
21 /**
22 * GET /admin/export - Display export page
23 */
24 public function index(Request $request, Response $response): Response
25 {
26 $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
27
28 return $response->write($this->render(TemplatePage::EXPORT));
29 }
30
31 /**
32 * POST /admin/export - Process export, and serve download file named
33 * bookmarks_(all|private|public)_datetime.html
34 */
35 public function export(Request $request, Response $response): Response
36 {
37 $this->checkToken($request);
38
39 $selection = $request->getParam('selection');
40
41 if (empty($selection)) {
42 $this->saveErrorMessage(t('Please select an export mode.'));
43
44 return $this->redirect($response, '/admin/export');
45 }
46
47 $prependNoteUrl = filter_var($request->getParam('prepend_note_url') ?? false, FILTER_VALIDATE_BOOLEAN);
48
49 try {
50 $formatter = $this->container->formatterFactory->getFormatter('raw');
51
52 $this->assignView(
53 'links',
54 $this->container->netscapeBookmarkUtils->filterAndFormat(
55 $formatter,
56 $selection,
57 $prependNoteUrl,
58 index_url($this->container->environment)
59 )
60 );
61 } catch (\Exception $exc) {
62 $this->saveErrorMessage($exc->getMessage());
63
64 return $this->redirect($response, '/admin/export');
65 }
66
67 $now = new DateTime();
68 $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
69 $response = $response->withHeader(
70 'Content-disposition',
71 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html'
72 );
73
74 $this->assignView('date', $now->format(DateTime::RFC822));
75 $this->assignView('eol', PHP_EOL);
76 $this->assignView('selection', $selection);
77
78 return $response->write($this->render(TemplatePage::NETSCAPE_EXPORT_BOOKMARKS));
79 }
80}
diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php
new file mode 100644
index 00000000..758d5ef9
--- /dev/null
+++ b/application/front/controller/admin/ImportController.php
@@ -0,0 +1,82 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Psr\Http\Message\UploadedFileInterface;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ImportController
14 *
15 * Slim controller used to display Shaarli data import page,
16 * and import bookmarks from Netscape Bookmarks file.
17 */
18class ImportController extends ShaarliAdminController
19{
20 /**
21 * GET /admin/import - Display import page
22 */
23 public function index(Request $request, Response $response): Response
24 {
25 $this->assignView(
26 'maxfilesize',
27 get_max_upload_size(
28 ini_get('post_max_size'),
29 ini_get('upload_max_filesize'),
30 false
31 )
32 );
33 $this->assignView(
34 'maxfilesizeHuman',
35 get_max_upload_size(
36 ini_get('post_max_size'),
37 ini_get('upload_max_filesize'),
38 true
39 )
40 );
41 $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
42
43 return $response->write($this->render(TemplatePage::IMPORT));
44 }
45
46 /**
47 * POST /admin/import - Process import file provided and create bookmarks
48 */
49 public function import(Request $request, Response $response): Response
50 {
51 $this->checkToken($request);
52
53 $file = ($request->getUploadedFiles() ?? [])['filetoupload'] ?? null;
54 if (!$file instanceof UploadedFileInterface) {
55 $this->saveErrorMessage(t('No import file provided.'));
56
57 return $this->redirect($response, '/admin/import');
58 }
59
60
61 // Import bookmarks from an uploaded file
62 if (0 === $file->getSize()) {
63 // The file is too big or some form field may be missing.
64 $msg = sprintf(
65 t(
66 'The file you are trying to upload is probably bigger than what this webserver can accept'
67 .' (%s). Please upload in smaller chunks.'
68 ),
69 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
70 );
71 $this->saveErrorMessage($msg);
72
73 return $this->redirect($response, '/admin/import');
74 }
75
76 $status = $this->container->netscapeBookmarkUtils->import($request->getParams(), $file);
77
78 $this->saveSuccessMessage($status);
79
80 return $this->redirect($response, '/admin/import');
81 }
82}
diff --git a/application/front/controller/admin/LogoutController.php b/application/front/controller/admin/LogoutController.php
new file mode 100644
index 00000000..28165129
--- /dev/null
+++ b/application/front/controller/admin/LogoutController.php
@@ -0,0 +1,33 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Security\CookieManager;
8use Shaarli\Security\LoginManager;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class LogoutController
14 *
15 * Slim controller used to logout the user.
16 * It invalidates page cache and terminate the user session. Then it redirects to the homepage.
17 */
18class LogoutController extends ShaarliAdminController
19{
20 public function index(Request $request, Response $response): Response
21 {
22 $this->container->pageCacheManager->invalidateCaches();
23 $this->container->sessionManager->logout();
24 $this->container->cookieManager->setCookieParameter(
25 CookieManager::STAY_SIGNED_IN,
26 'false',
27 0,
28 $this->container->basePath . '/'
29 );
30
31 return $this->redirect($response, '/');
32 }
33}
diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php
new file mode 100644
index 00000000..2065c3e2
--- /dev/null
+++ b/application/front/controller/admin/ManageTagController.php
@@ -0,0 +1,88 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ManageTagController
14 *
15 * Slim controller used to handle Shaarli manage tags page (rename and delete tags).
16 */
17class ManageTagController extends ShaarliAdminController
18{
19 /**
20 * GET /admin/tags - Displays the manage tags page
21 */
22 public function index(Request $request, Response $response): Response
23 {
24 $fromTag = $request->getParam('fromtag') ?? '';
25
26 $this->assignView('fromtag', escape($fromTag));
27 $this->assignView(
28 'pagetitle',
29 t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli')
30 );
31
32 return $response->write($this->render(TemplatePage::CHANGE_TAG));
33 }
34
35 /**
36 * POST /admin/tags - Update or delete provided tag
37 */
38 public function save(Request $request, Response $response): Response
39 {
40 $this->checkToken($request);
41
42 $isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag');
43
44 $fromTag = trim($request->getParam('fromtag') ?? '');
45 $toTag = trim($request->getParam('totag') ?? '');
46
47 if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) {
48 $this->saveWarningMessage(t('Invalid tags provided.'));
49
50 return $this->redirect($response, '/admin/tags');
51 }
52
53 // TODO: move this to bookmark service
54 $count = 0;
55 $bookmarks = $this->container->bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
56 foreach ($bookmarks as $bookmark) {
57 if (false === $isDelete) {
58 $bookmark->renameTag($fromTag, $toTag);
59 } else {
60 $bookmark->deleteTag($fromTag);
61 }
62
63 $this->container->bookmarkService->set($bookmark, false);
64 $this->container->history->updateLink($bookmark);
65 $count++;
66 }
67
68 $this->container->bookmarkService->save();
69
70 if (true === $isDelete) {
71 $alert = sprintf(
72 t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count),
73 $count
74 );
75 } else {
76 $alert = sprintf(
77 t('The tag was renamed in %d bookmark.', 'The tag was renamed in %d bookmarks.', $count),
78 $count
79 );
80 }
81
82 $this->saveSuccessMessage($alert);
83
84 $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag);
85
86 return $this->redirect($response, $redirect);
87 }
88}
diff --git a/application/front/controller/admin/MetadataController.php b/application/front/controller/admin/MetadataController.php
new file mode 100644
index 00000000..ff845944
--- /dev/null
+++ b/application/front/controller/admin/MetadataController.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Controller used to retrieve/update bookmark's metadata.
12 */
13class MetadataController extends ShaarliAdminController
14{
15 /**
16 * GET /admin/metadata/{url} - Attempt to retrieve the bookmark title from provided URL.
17 */
18 public function ajaxRetrieveTitle(Request $request, Response $response): Response
19 {
20 $url = $request->getParam('url');
21
22 // Only try to extract metadata from URL with HTTP(s) scheme
23 if (!empty($url) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
24 return $response->withJson($this->container->metadataRetriever->retrieve($url));
25 }
26
27 return $response->withJson([]);
28 }
29}
diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php
new file mode 100644
index 00000000..5ec0d24b
--- /dev/null
+++ b/application/front/controller/admin/PasswordController.php
@@ -0,0 +1,101 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Container\ShaarliContainer;
8use Shaarli\Front\Exception\OpenShaarliPasswordException;
9use Shaarli\Front\Exception\ShaarliFrontException;
10use Shaarli\Render\TemplatePage;
11use Slim\Http\Request;
12use Slim\Http\Response;
13use Throwable;
14
15/**
16 * Class PasswordController
17 *
18 * Slim controller used to handle passwords update.
19 */
20class PasswordController extends ShaarliAdminController
21{
22 public function __construct(ShaarliContainer $container)
23 {
24 parent::__construct($container);
25
26 $this->assignView(
27 'pagetitle',
28 t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli')
29 );
30 }
31
32 /**
33 * GET /admin/password - Displays the change password template
34 */
35 public function index(Request $request, Response $response): Response
36 {
37 return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
38 }
39
40 /**
41 * POST /admin/password - Change admin password - existing and new passwords need to be provided.
42 */
43 public function change(Request $request, Response $response): Response
44 {
45 $this->checkToken($request);
46
47 if ($this->container->conf->get('security.open_shaarli', false)) {
48 throw new OpenShaarliPasswordException();
49 }
50
51 $oldPassword = $request->getParam('oldpassword');
52 $newPassword = $request->getParam('setpassword');
53
54 if (empty($newPassword) || empty($oldPassword)) {
55 $this->saveErrorMessage(t('You must provide the current and new password to change it.'));
56
57 return $response
58 ->withStatus(400)
59 ->write($this->render(TemplatePage::CHANGE_PASSWORD))
60 ;
61 }
62
63 // Make sure old password is correct.
64 $oldHash = sha1(
65 $oldPassword .
66 $this->container->conf->get('credentials.login') .
67 $this->container->conf->get('credentials.salt')
68 );
69
70 if ($oldHash !== $this->container->conf->get('credentials.hash')) {
71 $this->saveErrorMessage(t('The old password is not correct.'));
72
73 return $response
74 ->withStatus(400)
75 ->write($this->render(TemplatePage::CHANGE_PASSWORD))
76 ;
77 }
78
79 // Save new password
80 // Salt renders rainbow-tables attacks useless.
81 $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
82 $this->container->conf->set(
83 'credentials.hash',
84 sha1(
85 $newPassword
86 . $this->container->conf->get('credentials.login')
87 . $this->container->conf->get('credentials.salt')
88 )
89 );
90
91 try {
92 $this->container->conf->write($this->container->loginManager->isLoggedIn());
93 } catch (Throwable $e) {
94 throw new ShaarliFrontException($e->getMessage(), 500, $e);
95 }
96
97 $this->saveSuccessMessage(t('Your password has been changed'));
98
99 return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
100 }
101}
diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php
new file mode 100644
index 00000000..8e059681
--- /dev/null
+++ b/application/front/controller/admin/PluginsController.php
@@ -0,0 +1,85 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Exception;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class PluginsController
14 *
15 * Slim controller used to handle Shaarli plugins configuration page (display + save new config).
16 */
17class PluginsController extends ShaarliAdminController
18{
19 /**
20 * GET /admin/plugins - Displays the configuration page
21 */
22 public function index(Request $request, Response $response): Response
23 {
24 $pluginMeta = $this->container->pluginManager->getPluginsMeta();
25
26 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
27 $enabledPlugins = array_filter($pluginMeta, function ($v) {
28 return ($v['order'] ?? false) !== false;
29 });
30 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $this->container->conf->get('plugins', []));
31 uasort(
32 $enabledPlugins,
33 function ($a, $b) {
34 return $a['order'] - $b['order'];
35 }
36 );
37 $disabledPlugins = array_filter($pluginMeta, function ($v) {
38 return ($v['order'] ?? false) === false;
39 });
40
41 $this->assignView('enabledPlugins', $enabledPlugins);
42 $this->assignView('disabledPlugins', $disabledPlugins);
43 $this->assignView(
44 'pagetitle',
45 t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli')
46 );
47
48 return $response->write($this->render(TemplatePage::PLUGINS_ADMIN));
49 }
50
51 /**
52 * POST /admin/plugins - Update Shaarli's configuration
53 */
54 public function save(Request $request, Response $response): Response
55 {
56 $this->checkToken($request);
57
58 try {
59 $parameters = $request->getParams() ?? [];
60
61 $this->executePageHooks('save_plugin_parameters', $parameters);
62
63 if (isset($parameters['parameters_form'])) {
64 unset($parameters['parameters_form']);
65 unset($parameters['token']);
66 foreach ($parameters as $param => $value) {
67 $this->container->conf->set('plugins.'. $param, escape($value));
68 }
69 } else {
70 $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters));
71 }
72
73 $this->container->conf->write($this->container->loginManager->isLoggedIn());
74 $this->container->history->updateSettings();
75
76 $this->saveSuccessMessage(t('Setting successfully saved.'));
77 } catch (Exception $e) {
78 $this->saveErrorMessage(
79 t('Error while saving plugin configuration: ') . PHP_EOL . $e->getMessage()
80 );
81 }
82
83 return $this->redirect($response, '/admin/plugins');
84 }
85}
diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php
new file mode 100644
index 00000000..bfc99422
--- /dev/null
+++ b/application/front/controller/admin/ServerController.php
@@ -0,0 +1,87 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Helper\ApplicationUtils;
8use Shaarli\Helper\FileUtils;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Slim controller used to handle Server administration page, and actions.
14 */
15class ServerController extends ShaarliAdminController
16{
17 /** @var string Cache type - main - by default pagecache/ and tmp/ */
18 protected const CACHE_MAIN = 'main';
19
20 /** @var string Cache type - thumbnails - by default cache/ */
21 protected const CACHE_THUMB = 'thumbnails';
22
23 /**
24 * GET /admin/server - Display page Server administration
25 */
26 public function index(Request $request, Response $response): Response
27 {
28 $latestVersion = 'v' . ApplicationUtils::getVersion(
29 ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE
30 );
31 $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php');
32 $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion;
33 $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
34
35 $this->assignView('php_version', PHP_VERSION);
36 $this->assignView('php_eol', format_date($phpEol, false));
37 $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
38 $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
39 $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
40 $this->assignView('release_url', ApplicationUtils::$GITHUB_URL . '/releases/tag/' . $latestVersion);
41 $this->assignView('latest_version', $latestVersion);
42 $this->assignView('current_version', $currentVersion);
43 $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode'));
44 $this->assignView('index_url', index_url($this->container->environment));
45 $this->assignView('client_ip', client_ip_id($this->container->environment));
46 $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', []));
47
48 $this->assignView(
49 'pagetitle',
50 t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
51 );
52
53 return $response->write($this->render('server'));
54 }
55
56 /**
57 * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails).
58 */
59 public function clearCache(Request $request, Response $response): Response
60 {
61 $exclude = ['.htaccess'];
62
63 if ($request->getQueryParam('type') === static::CACHE_THUMB) {
64 $folders = [$this->container->conf->get('resource.thumbnails_cache')];
65
66 $this->saveWarningMessage(
67 t('Thumbnails cache has been cleared.') . ' ' .
68 '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>'
69 );
70 } else {
71 $folders = [
72 $this->container->conf->get('resource.page_cache'),
73 $this->container->conf->get('resource.raintpl_tmp'),
74 ];
75
76 $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!'));
77 }
78
79 // Make sure that we don't delete root cache folder
80 $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders))));
81 foreach ($folders as $folder) {
82 FileUtils::clearFolder($folder, false, $exclude);
83 }
84
85 return $this->redirect($response, '/admin/server');
86 }
87}
diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php
new file mode 100644
index 00000000..d9a7a2e0
--- /dev/null
+++ b/application/front/controller/admin/SessionFilterController.php
@@ -0,0 +1,50 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Security\SessionManager;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class SessionFilterController
14 *
15 * Slim controller used to handle filters stored in the user session, such as visibility, etc.
16 */
17class SessionFilterController extends ShaarliAdminController
18{
19 /**
20 * GET /admin/visibility: allows to display only public or only private bookmarks in linklist
21 */
22 public function visibility(Request $request, Response $response, array $args): Response
23 {
24 if (false === $this->container->loginManager->isLoggedIn()) {
25 return $this->redirectFromReferer($request, $response, ['visibility']);
26 }
27
28 $newVisibility = $args['visibility'] ?? null;
29 if (false === in_array($newVisibility, [BookmarkFilter::$PRIVATE, BookmarkFilter::$PUBLIC], true)) {
30 $newVisibility = null;
31 }
32
33 $currentVisibility = $this->container->sessionManager->getSessionParameter(SessionManager::KEY_VISIBILITY);
34
35 // Visibility not set or not already expected value, set expected value, otherwise reset it
36 if ($newVisibility !== null && (null === $currentVisibility || $currentVisibility !== $newVisibility)) {
37 // See only public bookmarks
38 $this->container->sessionManager->setSessionParameter(
39 SessionManager::KEY_VISIBILITY,
40 $newVisibility
41 );
42 } else {
43 $this->container->sessionManager->deleteSessionParameter(SessionManager::KEY_VISIBILITY);
44 }
45
46 return $this->redirectFromReferer($request, $response, ['visibility']);
47 }
48
49
50}
diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php
new file mode 100644
index 00000000..8dc386b2
--- /dev/null
+++ b/application/front/controller/admin/ShaareAddController.php
@@ -0,0 +1,34 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Formatter\BookmarkMarkdownFormatter;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class ShaareAddController extends ShaarliAdminController
13{
14 /**
15 * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL
16 */
17 public function addShaare(Request $request, Response $response): Response
18 {
19 $tags = $this->container->bookmarkService->bookmarksCountPerTag();
20 if ($this->container->conf->get('formatter') === 'markdown') {
21 $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
22 }
23
24 $this->assignView(
25 'pagetitle',
26 t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli')
27 );
28 $this->assignView('tags', $tags);
29 $this->assignView('default_private_links', $this->container->conf->get('privacy.default_private_links', false));
30 $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true));
31
32 return $response->write($this->render(TemplatePage::ADDLINK));
33 }
34}
diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php
new file mode 100644
index 00000000..7ceb8d8a
--- /dev/null
+++ b/application/front/controller/admin/ShaareManageController.php
@@ -0,0 +1,202 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class PostBookmarkController
13 *
14 * Slim controller used to handle Shaarli create or edit bookmarks.
15 */
16class ShaareManageController extends ShaarliAdminController
17{
18 /**
19 * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter).
20 */
21 public function deleteBookmark(Request $request, Response $response): Response
22 {
23 $this->checkToken($request);
24
25 $ids = escape(trim($request->getParam('id') ?? ''));
26 if (empty($ids) || strpos($ids, ' ') !== false) {
27 // multiple, space-separated ids provided
28 $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
29 } else {
30 $ids = [$ids];
31 }
32
33 // assert at least one id is given
34 if (0 === count($ids)) {
35 $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
36
37 return $this->redirectFromReferer($request, $response, [], ['delete-shaare']);
38 }
39
40 $formatter = $this->container->formatterFactory->getFormatter('raw');
41 $count = 0;
42 foreach ($ids as $id) {
43 try {
44 $bookmark = $this->container->bookmarkService->get((int) $id);
45 } catch (BookmarkNotFoundException $e) {
46 $this->saveErrorMessage(sprintf(
47 t('Bookmark with identifier %s could not be found.'),
48 $id
49 ));
50
51 continue;
52 }
53
54 $data = $formatter->format($bookmark);
55 $this->executePageHooks('delete_link', $data);
56 $this->container->bookmarkService->remove($bookmark, false);
57 ++ $count;
58 }
59
60 if ($count > 0) {
61 $this->container->bookmarkService->save();
62 }
63
64 // If we are called from the bookmarklet, we must close the popup:
65 if ($request->getParam('source') === 'bookmarklet') {
66 return $response->write('<script>self.close();</script>');
67 }
68
69 // Don't redirect to where we were previously because the datastore has changed.
70 return $this->redirect($response, '/');
71 }
72
73 /**
74 * GET /admin/shaare/visibility
75 *
76 * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter).
77 */
78 public function changeVisibility(Request $request, Response $response): Response
79 {
80 $this->checkToken($request);
81
82 $ids = trim(escape($request->getParam('id') ?? ''));
83 if (empty($ids) || strpos($ids, ' ') !== false) {
84 // multiple, space-separated ids provided
85 $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
86 } else {
87 // only a single id provided
88 $ids = [$ids];
89 }
90
91 // assert at least one id is given
92 if (0 === count($ids)) {
93 $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
94
95 return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
96 }
97
98 // assert that the visibility is valid
99 $visibility = $request->getParam('newVisibility');
100 if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) {
101 $this->saveErrorMessage(t('Invalid visibility provided.'));
102
103 return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
104 } else {
105 $isPrivate = $visibility === 'private';
106 }
107
108 $formatter = $this->container->formatterFactory->getFormatter('raw');
109 $count = 0;
110
111 foreach ($ids as $id) {
112 try {
113 $bookmark = $this->container->bookmarkService->get((int) $id);
114 } catch (BookmarkNotFoundException $e) {
115 $this->saveErrorMessage(sprintf(
116 t('Bookmark with identifier %s could not be found.'),
117 $id
118 ));
119
120 continue;
121 }
122
123 $bookmark->setPrivate($isPrivate);
124
125 // To preserve backward compatibility with 3rd parties, plugins still use arrays
126 $data = $formatter->format($bookmark);
127 $this->executePageHooks('save_link', $data);
128 $bookmark->fromArray($data);
129
130 $this->container->bookmarkService->set($bookmark, false);
131 ++$count;
132 }
133
134 if ($count > 0) {
135 $this->container->bookmarkService->save();
136 }
137
138 return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']);
139 }
140
141 /**
142 * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark.
143 */
144 public function pinBookmark(Request $request, Response $response, array $args): Response
145 {
146 $this->checkToken($request);
147
148 $id = $args['id'] ?? '';
149 try {
150 if (false === ctype_digit($id)) {
151 throw new BookmarkNotFoundException();
152 }
153 $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
154 } catch (BookmarkNotFoundException $e) {
155 $this->saveErrorMessage(sprintf(
156 t('Bookmark with identifier %s could not be found.'),
157 $id
158 ));
159
160 return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
161 }
162
163 $formatter = $this->container->formatterFactory->getFormatter('raw');
164
165 $bookmark->setSticky(!$bookmark->isSticky());
166
167 // To preserve backward compatibility with 3rd parties, plugins still use arrays
168 $data = $formatter->format($bookmark);
169 $this->executePageHooks('save_link', $data);
170 $bookmark->fromArray($data);
171
172 $this->container->bookmarkService->set($bookmark);
173
174 return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
175 }
176
177 /**
178 * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL.
179 */
180 public function sharePrivate(Request $request, Response $response, array $args): Response
181 {
182 $this->checkToken($request);
183
184 $hash = $args['hash'] ?? '';
185 $bookmark = $this->container->bookmarkService->findByHash($hash);
186
187 if ($bookmark->isPrivate() !== true) {
188 return $this->redirect($response, '/shaare/' . $hash);
189 }
190
191 if (empty($bookmark->getAdditionalContentEntry('private_key'))) {
192 $privateKey = bin2hex(random_bytes(16));
193 $bookmark->addAdditionalContentEntry('private_key', $privateKey);
194 $this->container->bookmarkService->set($bookmark);
195 }
196
197 return $this->redirect(
198 $response,
199 '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key')
200 );
201 }
202}
diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php
new file mode 100644
index 00000000..18afc2d1
--- /dev/null
+++ b/application/front/controller/admin/ShaarePublishController.php
@@ -0,0 +1,263 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\BookmarkMarkdownFormatter;
11use Shaarli\Render\TemplatePage;
12use Shaarli\Thumbnailer;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class ShaarePublishController extends ShaarliAdminController
17{
18 /**
19 * @var BookmarkFormatter[] Statically cached instances of formatters
20 */
21 protected $formatters = [];
22
23 /**
24 * @var array Statically cached bookmark's tags counts
25 */
26 protected $tags;
27
28 /**
29 * GET /admin/shaare - Displays the bookmark form for creation.
30 * Note that if the URL is found in existing bookmarks, then it will be in edit mode.
31 */
32 public function displayCreateForm(Request $request, Response $response): Response
33 {
34 $url = cleanup_url($request->getParam('post'));
35 $link = $this->buildLinkDataFromUrl($request, $url);
36
37 return $this->displayForm($link, $link['linkIsNew'], $request, $response);
38 }
39
40 /**
41 * POST /admin/shaare-batch - Displays multiple creation/edit forms from bulk add in add-link page.
42 */
43 public function displayCreateBatchForms(Request $request, Response $response): Response
44 {
45 $urls = array_map('cleanup_url', explode(PHP_EOL, $request->getParam('urls')));
46
47 $links = [];
48 foreach ($urls as $url) {
49 if (empty($url)) {
50 continue;
51 }
52 $link = $this->buildLinkDataFromUrl($request, $url);
53 $data = $this->buildFormData($link, $link['linkIsNew'], $request);
54 $data['token'] = $this->container->sessionManager->generateToken();
55 $data['source'] = 'batch';
56
57 $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
58
59 $links[] = $data;
60 }
61
62 $this->assignView('links', $links);
63 $this->assignView('batch_mode', true);
64 $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true));
65
66 return $response->write($this->render(TemplatePage::EDIT_LINK_BATCH));
67 }
68
69 /**
70 * GET /admin/shaare/{id} - Displays the bookmark form in edition mode.
71 */
72 public function displayEditForm(Request $request, Response $response, array $args): Response
73 {
74 $id = $args['id'] ?? '';
75 try {
76 if (false === ctype_digit($id)) {
77 throw new BookmarkNotFoundException();
78 }
79 $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
80 } catch (BookmarkNotFoundException $e) {
81 $this->saveErrorMessage(sprintf(
82 t('Bookmark with identifier %s could not be found.'),
83 $id
84 ));
85
86 return $this->redirect($response, '/');
87 }
88
89 $formatter = $this->getFormatter('raw');
90 $link = $formatter->format($bookmark);
91
92 return $this->displayForm($link, false, $request, $response);
93 }
94
95 /**
96 * POST /admin/shaare
97 */
98 public function save(Request $request, Response $response): Response
99 {
100 $this->checkToken($request);
101
102 // lf_id should only be present if the link exists.
103 $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null;
104 if (null !== $id && true === $this->container->bookmarkService->exists($id)) {
105 // Edit
106 $bookmark = $this->container->bookmarkService->get($id);
107 } else {
108 // New link
109 $bookmark = new Bookmark();
110 }
111
112 $bookmark->setTitle($request->getParam('lf_title'));
113 $bookmark->setDescription($request->getParam('lf_description'));
114 $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', []));
115 $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN));
116 $bookmark->setTagsString($request->getParam('lf_tags'));
117
118 if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
119 && true !== $this->container->conf->get('general.enable_async_metadata', true)
120 && $bookmark->shouldUpdateThumbnail()
121 ) {
122 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
123 }
124 $this->container->bookmarkService->addOrSet($bookmark, false);
125
126 // To preserve backward compatibility with 3rd parties, plugins still use arrays
127 $formatter = $this->getFormatter('raw');
128 $data = $formatter->format($bookmark);
129 $this->executePageHooks('save_link', $data);
130
131 $bookmark->fromArray($data);
132 $this->container->bookmarkService->set($bookmark);
133
134 // If we are called from the bookmarklet, we must close the popup:
135 if ($request->getParam('source') === 'bookmarklet') {
136 return $response->write('<script>self.close();</script>');
137 } elseif ($request->getParam('source') === 'batch') {
138 return $response;
139 }
140
141 if (!empty($request->getParam('returnurl'))) {
142 $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
143 }
144
145 return $this->redirectFromReferer(
146 $request,
147 $response,
148 ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'],
149 $bookmark->getShortUrl()
150 );
151 }
152
153 /**
154 * Helper function used to display the shaare form whether it's a new or existing bookmark.
155 *
156 * @param array $link data used in template, either from parameters or from the data store
157 */
158 protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response
159 {
160 $data = $this->buildFormData($link, $isNew, $request);
161
162 $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
163
164 foreach ($data as $key => $value) {
165 $this->assignView($key, $value);
166 }
167
168 $editLabel = false === $isNew ? t('Edit') .' ' : '';
169 $this->assignView(
170 'pagetitle',
171 $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli')
172 );
173
174 return $response->write($this->render(TemplatePage::EDIT_LINK));
175 }
176
177 protected function buildLinkDataFromUrl(Request $request, string $url): array
178 {
179 // Check if URL is not already in database (in this case, we will edit the existing link)
180 $bookmark = $this->container->bookmarkService->findByUrl($url);
181 if (null === $bookmark) {
182 // Get shaare data if it was provided in URL (e.g.: by the bookmarklet).
183 $title = $request->getParam('title');
184 $description = $request->getParam('description');
185 $tags = $request->getParam('tags');
186 if ($request->getParam('private') !== null) {
187 $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN);
188 } else {
189 $private = $this->container->conf->get('privacy.default_private_links', false);
190 }
191
192 // If this is an HTTP(S) link, we try go get the page to extract
193 // the title (otherwise we will to straight to the edit form.)
194 if (true !== $this->container->conf->get('general.enable_async_metadata', true)
195 && empty($title)
196 && strpos(get_url_scheme($url) ?: '', 'http') !== false
197 ) {
198 $metadata = $this->container->metadataRetriever->retrieve($url);
199 }
200
201 if (empty($url)) {
202 $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: '));
203 }
204
205 return [
206 'title' => $title ?? $metadata['title'] ?? '',
207 'url' => $url ?? '',
208 'description' => $description ?? $metadata['description'] ?? '',
209 'tags' => $tags ?? $metadata['tags'] ?? '',
210 'private' => $private,
211 'linkIsNew' => true,
212 ];
213 }
214
215 $formatter = $this->getFormatter('raw');
216 $link = $formatter->format($bookmark);
217 $link['linkIsNew'] = false;
218
219 return $link;
220 }
221
222 protected function buildFormData(array $link, bool $isNew, Request $request): array
223 {
224 return escape([
225 'link' => $link,
226 'link_is_new' => $isNew,
227 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
228 'source' => $request->getParam('source') ?? '',
229 'tags' => $this->getTags(),
230 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
231 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true),
232 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false),
233 ]);
234 }
235
236 /**
237 * Memoize formatterFactory->getFormatter() calls.
238 */
239 protected function getFormatter(string $type): BookmarkFormatter
240 {
241 if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) {
242 $this->formatters[$type] = $this->container->formatterFactory->getFormatter($type);
243 }
244
245 return $this->formatters[$type];
246 }
247
248 /**
249 * Memoize bookmarkService->bookmarksCountPerTag() calls.
250 */
251 protected function getTags(): array
252 {
253 if ($this->tags === null) {
254 $this->tags = $this->container->bookmarkService->bookmarksCountPerTag();
255
256 if ($this->container->conf->get('formatter') === 'markdown') {
257 $this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
258 }
259 }
260
261 return $this->tags;
262 }
263}
diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php
new file mode 100644
index 00000000..c26c9cbe
--- /dev/null
+++ b/application/front/controller/admin/ShaarliAdminController.php
@@ -0,0 +1,71 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Front\Controller\Visitor\ShaarliVisitorController;
8use Shaarli\Front\Exception\WrongTokenException;
9use Shaarli\Security\SessionManager;
10use Slim\Http\Request;
11
12/**
13 * Class ShaarliAdminController
14 *
15 * All admin controllers (for logged in users) MUST extend this abstract class.
16 * It makes sure that the user is properly logged in, and otherwise throw an exception
17 * which will redirect to the login page.
18 *
19 * @package Shaarli\Front\Controller\Admin
20 */
21abstract class ShaarliAdminController extends ShaarliVisitorController
22{
23 /**
24 * Any persistent action to the config or data store must check the XSRF token validity.
25 */
26 protected function checkToken(Request $request): bool
27 {
28 if (!$this->container->sessionManager->checkToken($request->getParam('token'))) {
29 throw new WrongTokenException();
30 }
31
32 return true;
33 }
34
35 /**
36 * Save a SUCCESS message in user session, which will be displayed on any template page.
37 */
38 protected function saveSuccessMessage(string $message): void
39 {
40 $this->saveMessage(SessionManager::KEY_SUCCESS_MESSAGES, $message);
41 }
42
43 /**
44 * Save a WARNING message in user session, which will be displayed on any template page.
45 */
46 protected function saveWarningMessage(string $message): void
47 {
48 $this->saveMessage(SessionManager::KEY_WARNING_MESSAGES, $message);
49 }
50
51 /**
52 * Save an ERROR message in user session, which will be displayed on any template page.
53 */
54 protected function saveErrorMessage(string $message): void
55 {
56 $this->saveMessage(SessionManager::KEY_ERROR_MESSAGES, $message);
57 }
58
59 /**
60 * Use the sessionManager to save the provided message using the proper type.
61 *
62 * @param string $type successed/warnings/errors
63 */
64 protected function saveMessage(string $type, string $message): void
65 {
66 $messages = $this->container->sessionManager->getSessionParameter($type) ?? [];
67 $messages[] = $message;
68
69 $this->container->sessionManager->setSessionParameter($type, $messages);
70 }
71}
diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php
new file mode 100644
index 00000000..4dc09d38
--- /dev/null
+++ b/application/front/controller/admin/ThumbnailsController.php
@@ -0,0 +1,65 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
8use Shaarli\Render\TemplatePage;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ToolsController
14 *
15 * Slim controller used to handle thumbnails update.
16 */
17class ThumbnailsController extends ShaarliAdminController
18{
19 /**
20 * GET /admin/thumbnails - Display thumbnails update page
21 */
22 public function index(Request $request, Response $response): Response
23 {
24 $ids = [];
25 foreach ($this->container->bookmarkService->search() as $bookmark) {
26 // A note or not HTTP(S)
27 if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) {
28 continue;
29 }
30
31 $ids[] = $bookmark->getId();
32 }
33
34 $this->assignView('ids', $ids);
35 $this->assignView(
36 'pagetitle',
37 t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli')
38 );
39
40 return $response->write($this->render(TemplatePage::THUMBNAILS));
41 }
42
43 /**
44 * PATCH /admin/shaare/{id}/thumbnail-update - Route for AJAX calls
45 */
46 public function ajaxUpdate(Request $request, Response $response, array $args): Response
47 {
48 $id = $args['id'] ?? null;
49
50 if (false === ctype_digit($id)) {
51 return $response->withStatus(400);
52 }
53
54 try {
55 $bookmark = $this->container->bookmarkService->get((int) $id);
56 } catch (BookmarkNotFoundException $e) {
57 return $response->withStatus(404);
58 }
59
60 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
61 $this->container->bookmarkService->set($bookmark);
62
63 return $response->withJson($this->container->formatterFactory->getFormatter('raw')->format($bookmark));
64 }
65}
diff --git a/application/front/controller/admin/TokenController.php b/application/front/controller/admin/TokenController.php
new file mode 100644
index 00000000..08d68d0a
--- /dev/null
+++ b/application/front/controller/admin/TokenController.php
@@ -0,0 +1,26 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Class TokenController
12 *
13 * Endpoint used to retrieve a XSRF token. Useful for AJAX requests.
14 */
15class TokenController extends ShaarliAdminController
16{
17 /**
18 * GET /admin/token
19 */
20 public function getToken(Request $request, Response $response): Response
21 {
22 $response = $response->withHeader('Content-Type', 'text/plain');
23
24 return $response->write($this->container->sessionManager->generateToken());
25 }
26}
diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php
new file mode 100644
index 00000000..a87f20d2
--- /dev/null
+++ b/application/front/controller/admin/ToolsController.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Render\TemplatePage;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class ToolsController
13 *
14 * Slim controller used to display the tools page.
15 */
16class ToolsController extends ShaarliAdminController
17{
18 public function index(Request $request, Response $response): Response
19 {
20 $data = [
21 'pageabsaddr' => index_url($this->container->environment),
22 'sslenabled' => is_https($this->container->environment),
23 ];
24
25 $this->executePageHooks('render_tools', $data, TemplatePage::TOOLS);
26
27 foreach ($data as $key => $value) {
28 $this->assignView($key, $value);
29 }
30
31 $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
32
33 return $response->write($this->render(TemplatePage::TOOLS));
34 }
35}
diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php
new file mode 100644
index 00000000..78c474c9
--- /dev/null
+++ b/application/front/controller/visitor/BookmarkListController.php
@@ -0,0 +1,249 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Legacy\LegacyController;
10use Shaarli\Legacy\UnknowLegacyRouteException;
11use Shaarli\Render\TemplatePage;
12use Shaarli\Thumbnailer;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16/**
17 * Class BookmarkListController
18 *
19 * Slim controller used to render the bookmark list, the home page of Shaarli.
20 * It also displays permalinks, and process legacy routes based on GET parameters.
21 */
22class BookmarkListController extends ShaarliVisitorController
23{
24 /**
25 * GET / - Displays the bookmark list, with optional filter parameters.
26 */
27 public function index(Request $request, Response $response): Response
28 {
29 $legacyResponse = $this->processLegacyController($request, $response);
30 if (null !== $legacyResponse) {
31 return $legacyResponse;
32 }
33
34 $formatter = $this->container->formatterFactory->getFormatter();
35 $formatter->addContextData('base_path', $this->container->basePath);
36
37 $searchTags = normalize_spaces($request->getParam('searchtags') ?? '');
38 $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));;
39
40 // Filter bookmarks according search parameters.
41 $visibility = $this->container->sessionManager->getSessionParameter('visibility');
42 $search = [
43 'searchtags' => $searchTags,
44 'searchterm' => $searchTerm,
45 ];
46 $linksToDisplay = $this->container->bookmarkService->search(
47 $search,
48 $visibility,
49 false,
50 !!$this->container->sessionManager->getSessionParameter('untaggedonly')
51 ) ?? [];
52
53 // ---- Handle paging.
54 $keys = [];
55 foreach ($linksToDisplay as $key => $value) {
56 $keys[] = $key;
57 }
58
59 $linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
60
61 // Select articles according to paging.
62 $pageCount = (int) ceil(count($keys) / $linksPerPage) ?: 1;
63 $page = (int) $request->getParam('page') ?? 1;
64 $page = $page < 1 ? 1 : $page;
65 $page = $page > $pageCount ? $pageCount : $page;
66
67 // Start index.
68 $i = ($page - 1) * $linksPerPage;
69 $end = $i + $linksPerPage;
70
71 $linkDisp = [];
72 $save = false;
73 while ($i < $end && $i < count($keys)) {
74 $save = $this->updateThumbnail($linksToDisplay[$keys[$i]], false) || $save;
75 $link = $formatter->format($linksToDisplay[$keys[$i]]);
76
77 $linkDisp[$keys[$i]] = $link;
78 $i++;
79 }
80
81 if ($save) {
82 $this->container->bookmarkService->save();
83 }
84
85 // Compute paging navigation
86 $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags);
87 $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm);
88
89 $previous_page_url = '';
90 if ($i !== count($keys)) {
91 $previous_page_url = '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl;
92 }
93 $next_page_url = '';
94 if ($page > 1) {
95 $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
96 }
97
98 // Fill all template fields.
99 $data = array_merge(
100 $this->initializeTemplateVars(),
101 [
102 'previous_page_url' => $previous_page_url,
103 'next_page_url' => $next_page_url,
104 'page_current' => $page,
105 'page_max' => $pageCount,
106 'result_count' => count($linksToDisplay),
107 'search_term' => escape($searchTerm),
108 'search_tags' => escape($searchTags),
109 'search_tags_url' => array_map('urlencode', explode(' ', $searchTags)),
110 'visibility' => $visibility,
111 'links' => $linkDisp,
112 ]
113 );
114
115 if (!empty($searchTerm) || !empty($searchTags)) {
116 $data['pagetitle'] = t('Search: ');
117 $data['pagetitle'] .= ! empty($searchTerm) ? $searchTerm . ' ' : '';
118 $bracketWrap = function ($tag) {
119 return '[' . $tag . ']';
120 };
121 $data['pagetitle'] .= ! empty($searchTags)
122 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' '
123 : '';
124 $data['pagetitle'] .= '- ';
125 }
126
127 $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli');
128
129 $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
130 $this->assignAllView($data);
131
132 return $response->write($this->render(TemplatePage::LINKLIST));
133 }
134
135 /**
136 * GET /shaare/{hash} - Display a single shaare
137 */
138 public function permalink(Request $request, Response $response, array $args): Response
139 {
140 $privateKey = $request->getParam('key');
141
142 try {
143 $bookmark = $this->container->bookmarkService->findByHash($args['hash'], $privateKey);
144 } catch (BookmarkNotFoundException $e) {
145 $this->assignView('error_message', $e->getMessage());
146
147 return $response->write($this->render(TemplatePage::ERROR_404));
148 }
149
150 $this->updateThumbnail($bookmark);
151
152 $formatter = $this->container->formatterFactory->getFormatter();
153 $formatter->addContextData('base_path', $this->container->basePath);
154
155 $data = array_merge(
156 $this->initializeTemplateVars(),
157 [
158 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'),
159 'links' => [$formatter->format($bookmark)],
160 ]
161 );
162
163 $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
164 $this->assignAllView($data);
165
166 return $response->write($this->render(TemplatePage::LINKLIST));
167 }
168
169 /**
170 * Update the thumbnail of a single bookmark if necessary.
171 */
172 protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
173 {
174 if (false === $this->container->loginManager->isLoggedIn()) {
175 return false;
176 }
177
178 // If thumbnail should be updated, we reset it to null
179 if ($bookmark->shouldUpdateThumbnail()) {
180 $bookmark->setThumbnail(null);
181
182 // Requires an update, not async retrieval, thumbnails enabled
183 if ($bookmark->shouldUpdateThumbnail()
184 && true !== $this->container->conf->get('general.enable_async_metadata', true)
185 && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
186 ) {
187 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
188 $this->container->bookmarkService->set($bookmark, $writeDatastore);
189
190 return true;
191 }
192 }
193
194 return false;
195 }
196
197 /**
198 * @return string[] Default template variables without values.
199 */
200 protected function initializeTemplateVars(): array
201 {
202 return [
203 'previous_page_url' => '',
204 'next_page_url' => '',
205 'page_max' => '',
206 'search_tags' => '',
207 'result_count' => '',
208 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true)
209 ];
210 }
211
212 /**
213 * Process legacy routes if necessary. They used query parameters.
214 * If no legacy routes is passed, return null.
215 */
216 protected function processLegacyController(Request $request, Response $response): ?Response
217 {
218 // Legacy smallhash filter
219 $queryString = $this->container->environment['QUERY_STRING'] ?? null;
220 if (null !== $queryString && 1 === preg_match('/^([a-zA-Z0-9-_@]{6})($|&|#)/', $queryString, $match)) {
221 return $this->redirect($response, '/shaare/' . $match[1]);
222 }
223
224 // Legacy controllers (mostly used for redirections)
225 if (null !== $request->getQueryParam('do')) {
226 $legacyController = new LegacyController($this->container);
227
228 try {
229 return $legacyController->process($request, $response, $request->getQueryParam('do'));
230 } catch (UnknowLegacyRouteException $e) {
231 // We ignore legacy 404
232 return null;
233 }
234 }
235
236 // Legacy GET admin routes
237 $legacyGetRoutes = array_intersect(
238 LegacyController::LEGACY_GET_ROUTES,
239 array_keys($request->getQueryParams() ?? [])
240 );
241 if (1 === count($legacyGetRoutes)) {
242 $legacyController = new LegacyController($this->container);
243
244 return $legacyController->process($request, $response, $legacyGetRoutes[0]);
245 }
246
247 return null;
248 }
249}
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php
new file mode 100644
index 00000000..728bc2d8
--- /dev/null
+++ b/application/front/controller/visitor/DailyController.php
@@ -0,0 +1,205 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use DateTime;
8use Shaarli\Bookmark\Bookmark;
9use Shaarli\Helper\DailyPageHelper;
10use Shaarli\Render\TemplatePage;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14/**
15 * Class DailyController
16 *
17 * Slim controller used to render the daily page.
18 */
19class DailyController extends ShaarliVisitorController
20{
21 public static $DAILY_RSS_NB_DAYS = 8;
22
23 /**
24 * Controller displaying all bookmarks published in a single day.
25 * It take a `day` date query parameter (format YYYYMMDD).
26 */
27 public function index(Request $request, Response $response): Response
28 {
29 $type = DailyPageHelper::extractRequestedType($request);
30 $format = DailyPageHelper::getFormatByType($type);
31 $latestBookmark = $this->container->bookmarkService->getLatest();
32 $dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark);
33 $start = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
34 $end = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
35 $dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime);
36
37 $linksToDisplay = $this->container->bookmarkService->findByDate(
38 $start,
39 $end,
40 $previousDay,
41 $nextDay
42 );
43
44 $formatter = $this->container->formatterFactory->getFormatter();
45 $formatter->addContextData('base_path', $this->container->basePath);
46 // We pre-format some fields for proper output.
47 foreach ($linksToDisplay as $key => $bookmark) {
48 $linksToDisplay[$key] = $formatter->format($bookmark);
49 // This page is a bit specific, we need raw description to calculate the length
50 $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
51 $linksToDisplay[$key]['description'] = $bookmark->getDescription();
52 }
53
54 $data = [
55 'linksToDisplay' => $linksToDisplay,
56 'dayDate' => $start,
57 'day' => $start->getTimestamp(),
58 'previousday' => $previousDay ? $previousDay->format($format) : '',
59 'nextday' => $nextDay ? $nextDay->format($format) : '',
60 'dayDesc' => $dailyDesc,
61 'type' => $type,
62 'localizedType' => $this->translateType($type),
63 ];
64
65 // Hooks are called before column construction so that plugins don't have to deal with columns.
66 $this->executePageHooks('render_daily', $data, TemplatePage::DAILY);
67
68 $data['cols'] = $this->calculateColumns($data['linksToDisplay']);
69
70 $this->assignAllView($data);
71
72 $mainTitle = $this->container->conf->get('general.title', 'Shaarli');
73 $this->assignView(
74 'pagetitle',
75 $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle
76 );
77
78 return $response->write($this->render(TemplatePage::DAILY));
79 }
80
81 /**
82 * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
83 * Gives the last 7 days (which have bookmarks).
84 * This RSS feed cannot be filtered and does not trigger plugins yet.
85 */
86 public function rss(Request $request, Response $response): Response
87 {
88 $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
89
90 $pageUrl = page_url($this->container->environment);
91 $cache = $this->container->pageCacheManager->getCachePage($pageUrl);
92
93 $cached = $cache->cachedVersion();
94 if (!empty($cached)) {
95 return $response->write($cached);
96 }
97
98 $days = [];
99 $type = DailyPageHelper::extractRequestedType($request);
100 $format = DailyPageHelper::getFormatByType($type);
101 $length = DailyPageHelper::getRssLengthByType($type);
102 foreach ($this->container->bookmarkService->search() as $bookmark) {
103 $day = $bookmark->getCreated()->format($format);
104
105 // Stop iterating after DAILY_RSS_NB_DAYS entries
106 if (count($days) === $length && !isset($days[$day])) {
107 break;
108 }
109
110 $days[$day][] = $bookmark;
111 }
112
113 // Build the RSS feed.
114 $indexUrl = escape(index_url($this->container->environment));
115
116 $formatter = $this->container->formatterFactory->getFormatter();
117 $formatter->addContextData('index_url', $indexUrl);
118
119 $dataPerDay = [];
120
121 /** @var Bookmark[] $bookmarks */
122 foreach ($days as $day => $bookmarks) {
123 $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day);
124 $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime);
125
126 // We only want the RSS entry to be published when the period is over.
127 if (new DateTime() < $endDateTime) {
128 continue;
129 }
130
131 $dataPerDay[$day] = [
132 'date' => $endDateTime,
133 'date_rss' => $endDateTime->format(DateTime::RSS),
134 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime),
135 'absolute_url' => $indexUrl . 'daily?'. $type .'=' . $day,
136 'links' => [],
137 ];
138
139 foreach ($bookmarks as $key => $bookmark) {
140 $dataPerDay[$day]['links'][$key] = $formatter->format($bookmark);
141
142 // Make permalink URL absolute
143 if ($bookmark->isNote()) {
144 $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl();
145 }
146 }
147 }
148
149 $this->assignAllView([
150 'title' => $this->container->conf->get('general.title', 'Shaarli'),
151 'index_url' => $indexUrl,
152 'page_url' => $pageUrl,
153 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false),
154 'days' => $dataPerDay,
155 'type' => $type,
156 'localizedType' => $this->translateType($type),
157 ]);
158
159 $rssContent = $this->render(TemplatePage::DAILY_RSS);
160
161 $cache->cache($rssContent);
162
163 return $response->write($rssContent);
164 }
165
166 /**
167 * We need to spread the articles on 3 columns.
168 * did not want to use a JavaScript lib like http://masonry.desandro.com/
169 * so I manually spread entries with a simple method: I roughly evaluate the
170 * height of a div according to title and description length.
171 */
172 protected function calculateColumns(array $links): array
173 {
174 // Entries to display, for each column.
175 $columns = [[], [], []];
176 // Rough estimate of columns fill.
177 $fill = [0, 0, 0];
178 foreach ($links as $link) {
179 // Roughly estimate length of entry (by counting characters)
180 // Title: 30 chars = 1 line. 1 line is 30 pixels height.
181 // Description: 836 characters gives roughly 342 pixel height.
182 // This is not perfect, but it's usually OK.
183 $length = strlen($link['title'] ?? '') + (342 * strlen($link['description'] ?? '')) / 836;
184 if (! empty($link['thumbnail'])) {
185 $length += 100; // 1 thumbnails roughly takes 100 pixels height.
186 }
187 // Then put in column which is the less filled:
188 $smallest = min($fill); // find smallest value in array.
189 $index = array_search($smallest, $fill); // find index of this smallest value.
190 array_push($columns[$index], $link); // Put entry in this column.
191 $fill[$index] += $length;
192 }
193
194 return $columns;
195 }
196
197 protected function translateType($type): string
198 {
199 return [
200 t('day') => t('Daily'),
201 t('week') => t('Weekly'),
202 t('month') => t('Monthly'),
203 ][t($type)] ?? t('Daily');
204 }
205}
diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php
new file mode 100644
index 00000000..8da11172
--- /dev/null
+++ b/application/front/controller/visitor/ErrorController.php
@@ -0,0 +1,42 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Front\Exception\ShaarliFrontException;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Controller used to render the error page, with a provided exception.
13 * It is actually used as a Slim error handler.
14 */
15class ErrorController extends ShaarliVisitorController
16{
17 public function __invoke(Request $request, Response $response, \Throwable $throwable): Response
18 {
19 // Unknown error encountered
20 $this->container->pageBuilder->reset();
21
22 if ($throwable instanceof ShaarliFrontException) {
23 // Functional error
24 $this->assignView('message', nl2br($throwable->getMessage()));
25
26 $response = $response->withStatus($throwable->getCode());
27 } else {
28 // Internal error (any other Throwable)
29 if ($this->container->conf->get('dev.debug', false)) {
30 $this->assignView('message', $throwable->getMessage());
31 $this->assignView('stacktrace', exception2text($throwable));
32 } else {
33 $this->assignView('message', t('An unexpected error occurred.'));
34 }
35
36 $response = $response->withStatus(500);
37 }
38
39
40 return $response->write($this->render('error'));
41 }
42}
diff --git a/application/front/controller/visitor/ErrorNotFoundController.php b/application/front/controller/visitor/ErrorNotFoundController.php
new file mode 100644
index 00000000..758dd83b
--- /dev/null
+++ b/application/front/controller/visitor/ErrorNotFoundController.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Controller used to render the 404 error page.
12 */
13class ErrorNotFoundController extends ShaarliVisitorController
14{
15 public function __invoke(Request $request, Response $response): Response
16 {
17 // Request from the API
18 if (false !== strpos($request->getRequestTarget(), '/api/v1')) {
19 return $response->withStatus(404);
20 }
21
22 // This is required because the middleware is ignored if the route is not found.
23 $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/');
24
25 $this->assignView('error_message', t('Requested page could not be found.'));
26
27 return $response->withStatus(404)->write($this->render('404'));
28 }
29}
diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php
new file mode 100644
index 00000000..8d8b546a
--- /dev/null
+++ b/application/front/controller/visitor/FeedController.php
@@ -0,0 +1,58 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Feed\FeedBuilder;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class FeedController
13 *
14 * Slim controller handling ATOM and RSS feed.
15 */
16class FeedController extends ShaarliVisitorController
17{
18 public function atom(Request $request, Response $response): Response
19 {
20 return $this->processRequest(FeedBuilder::$FEED_ATOM, $request, $response);
21 }
22
23 public function rss(Request $request, Response $response): Response
24 {
25 return $this->processRequest(FeedBuilder::$FEED_RSS, $request, $response);
26 }
27
28 protected function processRequest(string $feedType, Request $request, Response $response): Response
29 {
30 $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8');
31
32 $pageUrl = page_url($this->container->environment);
33 $cache = $this->container->pageCacheManager->getCachePage($pageUrl);
34
35 $cached = $cache->cachedVersion();
36 if (!empty($cached)) {
37 return $response->write($cached);
38 }
39
40 // Generate data.
41 $this->container->feedBuilder->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
42 $this->container->feedBuilder->setHideDates($this->container->conf->get('privacy.hide_timestamps', false));
43 $this->container->feedBuilder->setUsePermalinks(
44 null !== $request->getParam('permalinks') || !$this->container->conf->get('feed.rss_permalinks')
45 );
46
47 $data = $this->container->feedBuilder->buildData($feedType, $request->getParams());
48
49 $this->executePageHooks('render_feed', $data, 'feed.' . $feedType);
50 $this->assignAllView($data);
51
52 $content = $this->render('feed.' . $feedType);
53
54 $cache->cache($content);
55
56 return $response->write($content);
57 }
58}
diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php
new file mode 100644
index 00000000..22329294
--- /dev/null
+++ b/application/front/controller/visitor/InstallController.php
@@ -0,0 +1,175 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Container\ShaarliContainer;
8use Shaarli\Front\Exception\AlreadyInstalledException;
9use Shaarli\Front\Exception\ResourcePermissionException;
10use Shaarli\Helper\ApplicationUtils;
11use Shaarli\Languages;
12use Shaarli\Security\SessionManager;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16/**
17 * Slim controller used to render install page, and create initial configuration file.
18 */
19class InstallController extends ShaarliVisitorController
20{
21 public const SESSION_TEST_KEY = 'session_tested';
22 public const SESSION_TEST_VALUE = 'Working';
23
24 public function __construct(ShaarliContainer $container)
25 {
26 parent::__construct($container);
27
28 if (is_file($this->container->conf->getConfigFileExt())) {
29 throw new AlreadyInstalledException();
30 }
31 }
32
33 /**
34 * Display the install template page.
35 * Also test file permissions and sessions beforehand.
36 */
37 public function index(Request $request, Response $response): Response
38 {
39 // Before installation, we'll make sure that permissions are set properly, and sessions are working.
40 $this->checkPermissions();
41
42 if (static::SESSION_TEST_VALUE
43 !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
44 ) {
45 $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE);
46
47 return $this->redirect($response, '/install/session-test');
48 }
49
50 [$continents, $cities] = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
51
52 $this->assignView('continents', $continents);
53 $this->assignView('cities', $cities);
54 $this->assignView('languages', Languages::getAvailableLanguages());
55
56 $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
57
58 $this->assignView('php_version', PHP_VERSION);
59 $this->assignView('php_eol', format_date($phpEol, false));
60 $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
61 $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
62 $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
63
64 $this->assignView('pagetitle', t('Install Shaarli'));
65
66 return $response->write($this->render('install'));
67 }
68
69 /**
70 * Route checking that the session parameter has been properly saved between two distinct requests.
71 * If the session parameter is preserved, redirect to install template page, otherwise displays error.
72 */
73 public function sessionTest(Request $request, Response $response): Response
74 {
75 // This part makes sure sessions works correctly.
76 // (Because on some hosts, session.save_path may not be set correctly,
77 // or we may not have write access to it.)
78 if (static::SESSION_TEST_VALUE
79 !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
80 ) {
81 // Step 2: Check if data in session is correct.
82 $msg = t(
83 '<pre>Sessions do not seem to work correctly on your server.<br>'.
84 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
85 'and that you have write access to it.<br>'.
86 'It currently points to %s.<br>'.
87 'On some browsers, accessing your server via a hostname like \'localhost\' '.
88 'or any custom hostname without a dot causes cookie storage to fail. '.
89 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
90 );
91 $msg = sprintf($msg, $this->container->sessionManager->getSavePath());
92
93 $this->assignView('message', $msg);
94
95 return $response->write($this->render('error'));
96 }
97
98 return $this->redirect($response, '/install');
99 }
100
101 /**
102 * Save installation form and initialize config file and datastore if necessary.
103 */
104 public function save(Request $request, Response $response): Response
105 {
106 $timezone = 'UTC';
107 if (!empty($request->getParam('continent'))
108 && !empty($request->getParam('city'))
109 && isTimeZoneValid($request->getParam('continent'), $request->getParam('city'))
110 ) {
111 $timezone = $request->getParam('continent') . '/' . $request->getParam('city');
112 }
113 $this->container->conf->set('general.timezone', $timezone);
114
115 $login = $request->getParam('setlogin');
116 $this->container->conf->set('credentials.login', $login);
117 $salt = sha1(uniqid('', true) .'_'. mt_rand());
118 $this->container->conf->set('credentials.salt', $salt);
119 $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt));
120
121 if (!empty($request->getParam('title'))) {
122 $this->container->conf->set('general.title', escape($request->getParam('title')));
123 } else {
124 $this->container->conf->set(
125 'general.title',
126 'Shared bookmarks on '.escape(index_url($this->container->environment))
127 );
128 }
129
130 $this->container->conf->set('translation.language', escape($request->getParam('language')));
131 $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
132 $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
133 $this->container->conf->set(
134 'api.secret',
135 generate_api_secret(
136 $this->container->conf->get('credentials.login'),
137 $this->container->conf->get('credentials.salt')
138 )
139 );
140 $this->container->conf->set('general.header_link', $this->container->basePath . '/');
141
142 try {
143 // Everything is ok, let's create config file.
144 $this->container->conf->write($this->container->loginManager->isLoggedIn());
145 } catch (\Exception $e) {
146 $this->assignView('message', t('Error while writing config file after configuration update.'));
147 $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString());
148
149 return $response->write($this->render('error'));
150 }
151
152 $this->container->sessionManager->setSessionParameter(
153 SessionManager::KEY_SUCCESS_MESSAGES,
154 [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')]
155 );
156
157 return $this->redirect($response, '/login');
158 }
159
160 protected function checkPermissions(): bool
161 {
162 // Ensure Shaarli has proper access to its resources
163 $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true);
164 if (empty($errors)) {
165 return true;
166 }
167
168 $message = t('Insufficient permissions:') . PHP_EOL;
169 foreach ($errors as $error) {
170 $message .= PHP_EOL . $error;
171 }
172
173 throw new ResourcePermissionException($message);
174 }
175}
diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php
new file mode 100644
index 00000000..f5038fe3
--- /dev/null
+++ b/application/front/controller/visitor/LoginController.php
@@ -0,0 +1,153 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Front\Exception\CantLoginException;
8use Shaarli\Front\Exception\LoginBannedException;
9use Shaarli\Front\Exception\WrongTokenException;
10use Shaarli\Render\TemplatePage;
11use Shaarli\Security\CookieManager;
12use Shaarli\Security\SessionManager;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16/**
17 * Class LoginController
18 *
19 * Slim controller used to render the login page.
20 *
21 * The login page is not available if the user is banned
22 * or if open shaarli setting is enabled.
23 */
24class LoginController extends ShaarliVisitorController
25{
26 /**
27 * GET /login - Display the login page.
28 */
29 public function index(Request $request, Response $response): Response
30 {
31 try {
32 $this->checkLoginState();
33 } catch (CantLoginException $e) {
34 return $this->redirect($response, '/');
35 }
36
37 if ($request->getParam('login') !== null) {
38 $this->assignView('username', escape($request->getParam('login')));
39 }
40
41 $returnUrl = $request->getParam('returnurl') ?? $this->container->environment['HTTP_REFERER'] ?? null;
42
43 $this
44 ->assignView('returnurl', escape($returnUrl))
45 ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
46 ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli'))
47 ;
48
49 return $response->write($this->render(TemplatePage::LOGIN));
50 }
51
52 /**
53 * POST /login - Process login
54 */
55 public function login(Request $request, Response $response): Response
56 {
57 if (!$this->container->sessionManager->checkToken($request->getParam('token'))) {
58 throw new WrongTokenException();
59 }
60
61 try {
62 $this->checkLoginState();
63 } catch (CantLoginException $e) {
64 return $this->redirect($response, '/');
65 }
66
67 if (!$this->container->loginManager->checkCredentials(
68 client_ip_id($this->container->environment),
69 $request->getParam('login'),
70 $request->getParam('password')
71 )
72 ) {
73 $this->container->loginManager->handleFailedLogin($this->container->environment);
74
75 $this->container->sessionManager->setSessionParameter(
76 SessionManager::KEY_ERROR_MESSAGES,
77 [t('Wrong login/password.')]
78 );
79
80 // Call controller directly instead of unnecessary redirection
81 return $this->index($request, $response);
82 }
83
84 $this->container->loginManager->handleSuccessfulLogin($this->container->environment);
85
86 $cookiePath = $this->container->basePath . '/';
87 $expirationTime = $this->saveLongLastingSession($request, $cookiePath);
88 $this->renewUserSession($cookiePath, $expirationTime);
89
90 // Force referer from given return URL
91 $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
92
93 return $this->redirectFromReferer($request, $response, ['login', 'install']);
94 }
95
96 /**
97 * Make sure that the user is allowed to login and/or displaying the login page:
98 * - not already logged in
99 * - not open shaarli
100 * - not banned
101 */
102 protected function checkLoginState(): bool
103 {
104 if ($this->container->loginManager->isLoggedIn()
105 || $this->container->conf->get('security.open_shaarli', false)
106 ) {
107 throw new CantLoginException();
108 }
109
110 if (true !== $this->container->loginManager->canLogin($this->container->environment)) {
111 throw new LoginBannedException();
112 }
113
114 return true;
115 }
116
117 /**
118 * @return int Session duration in seconds
119 */
120 protected function saveLongLastingSession(Request $request, string $cookiePath): int
121 {
122 if (empty($request->getParam('longlastingsession'))) {
123 // Standard session expiration (=when browser closes)
124 $expirationTime = 0;
125 } else {
126 // Keep the session cookie even after the browser closes
127 $this->container->sessionManager->setStaySignedIn(true);
128 $expirationTime = $this->container->sessionManager->extendSession();
129 }
130
131 $this->container->cookieManager->setCookieParameter(
132 CookieManager::STAY_SIGNED_IN,
133 $this->container->loginManager->getStaySignedInToken(),
134 $expirationTime,
135 $cookiePath
136 );
137
138 return $expirationTime;
139 }
140
141 protected function renewUserSession(string $cookiePath, int $expirationTime): void
142 {
143 // Send cookie with the new expiration date to the browser
144 $this->container->sessionManager->destroy();
145 $this->container->sessionManager->cookieParameters(
146 $expirationTime,
147 $cookiePath,
148 $this->container->environment['SERVER_NAME']
149 );
150 $this->container->sessionManager->start();
151 $this->container->sessionManager->regenerateId(true);
152 }
153}
diff --git a/application/front/controller/visitor/OpenSearchController.php b/application/front/controller/visitor/OpenSearchController.php
new file mode 100644
index 00000000..36d60acf
--- /dev/null
+++ b/application/front/controller/visitor/OpenSearchController.php
@@ -0,0 +1,27 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Render\TemplatePage;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class OpenSearchController
13 *
14 * Slim controller used to render open search template.
15 * This allows to add Shaarli as a search engine within the browser.
16 */
17class OpenSearchController extends ShaarliVisitorController
18{
19 public function index(Request $request, Response $response): Response
20 {
21 $response = $response->withHeader('Content-Type', 'application/opensearchdescription+xml; charset=utf-8');
22
23 $this->assignView('serverurl', index_url($this->container->environment));
24
25 return $response->write($this->render(TemplatePage::OPEN_SEARCH));
26 }
27}
diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php
new file mode 100644
index 00000000..3c57f8dd
--- /dev/null
+++ b/application/front/controller/visitor/PictureWallController.php
@@ -0,0 +1,54 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Front\Exception\ThumbnailsDisabledException;
8use Shaarli\Render\TemplatePage;
9use Shaarli\Thumbnailer;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13/**
14 * Class PicturesWallController
15 *
16 * Slim controller used to render the pictures wall page.
17 * If thumbnails mode is set to NONE, we just render the template without any image.
18 */
19class PictureWallController extends ShaarliVisitorController
20{
21 public function index(Request $request, Response $response): Response
22 {
23 if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
24 throw new ThumbnailsDisabledException();
25 }
26
27 $this->assignView(
28 'pagetitle',
29 t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli')
30 );
31
32 // Optionally filter the results:
33 $links = $this->container->bookmarkService->search($request->getQueryParams());
34 $linksToDisplay = [];
35
36 // Get only bookmarks which have a thumbnail.
37 // Note: we do not retrieve thumbnails here, the request is too heavy.
38 $formatter = $this->container->formatterFactory->getFormatter('raw');
39 foreach ($links as $key => $link) {
40 if (!empty($link->getThumbnail())) {
41 $linksToDisplay[] = $formatter->format($link);
42 }
43 }
44
45 $data = ['linksToDisplay' => $linksToDisplay];
46 $this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL);
47
48 foreach ($data as $key => $value) {
49 $this->assignView($key, $value);
50 }
51
52 return $response->write($this->render(TemplatePage::PICTURE_WALL));
53 }
54}
diff --git a/application/front/controller/visitor/PublicSessionFilterController.php b/application/front/controller/visitor/PublicSessionFilterController.php
new file mode 100644
index 00000000..1a66362d
--- /dev/null
+++ b/application/front/controller/visitor/PublicSessionFilterController.php
@@ -0,0 +1,46 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Security\SessionManager;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Slim controller used to handle filters stored in the visitor session, links per page, etc.
13 */
14class PublicSessionFilterController extends ShaarliVisitorController
15{
16 /**
17 * GET /links-per-page: set the number of bookmarks to display per page in homepage
18 */
19 public function linksPerPage(Request $request, Response $response): Response
20 {
21 $linksPerPage = $request->getParam('nb') ?? null;
22 if (null === $linksPerPage || false === is_numeric($linksPerPage)) {
23 $linksPerPage = $this->container->conf->get('general.links_per_page', 20);
24 }
25
26 $this->container->sessionManager->setSessionParameter(
27 SessionManager::KEY_LINKS_PER_PAGE,
28 abs(intval($linksPerPage))
29 );
30
31 return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']);
32 }
33
34 /**
35 * GET /untagged-only: allows to display only bookmarks without any tag
36 */
37 public function untaggedOnly(Request $request, Response $response): Response
38 {
39 $this->container->sessionManager->setSessionParameter(
40 SessionManager::KEY_UNTAGGED_ONLY,
41 empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY))
42 );
43
44 return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']);
45 }
46}
diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php
new file mode 100644
index 00000000..54f9fe03
--- /dev/null
+++ b/application/front/controller/visitor/ShaarliVisitorController.php
@@ -0,0 +1,181 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Container\ShaarliContainer;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ShaarliVisitorController
14 *
15 * All controllers accessible by visitors (non logged in users) should extend this abstract class.
16 * Contains a few helper function for template rendering, plugins, etc.
17 *
18 * @package Shaarli\Front\Controller\Visitor
19 */
20abstract class ShaarliVisitorController
21{
22 /** @var ShaarliContainer */
23 protected $container;
24
25 /** @param ShaarliContainer $container Slim container (extended for attribute completion). */
26 public function __construct(ShaarliContainer $container)
27 {
28 $this->container = $container;
29 }
30
31 /**
32 * Assign variables to RainTPL template through the PageBuilder.
33 *
34 * @param mixed $value Value to assign to the template
35 */
36 protected function assignView(string $name, $value): self
37 {
38 $this->container->pageBuilder->assign($name, $value);
39
40 return $this;
41 }
42
43 /**
44 * Assign variables to RainTPL template through the PageBuilder.
45 *
46 * @param mixed $data Values to assign to the template and their keys
47 */
48 protected function assignAllView(array $data): self
49 {
50 foreach ($data as $key => $value) {
51 $this->assignView($key, $value);
52 }
53
54 return $this;
55 }
56
57 protected function render(string $template): string
58 {
59 $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL));
60 $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));
61
62 $this->executeDefaultHooks($template);
63
64 $this->assignView('plugin_errors', $this->container->pluginManager->getErrors());
65
66 return $this->container->pageBuilder->render($template, $this->container->basePath);
67 }
68
69 /**
70 * Call plugin hooks for header, footer and includes, specifying which page will be rendered.
71 * Then assign generated data to RainTPL.
72 */
73 protected function executeDefaultHooks(string $template): void
74 {
75 $common_hooks = [
76 'includes',
77 'header',
78 'footer',
79 ];
80
81 $parameters = $this->buildPluginParameters($template);
82
83 foreach ($common_hooks as $name) {
84 $pluginData = [];
85 $this->container->pluginManager->executeHooks(
86 'render_' . $name,
87 $pluginData,
88 $parameters
89 );
90 $this->assignView('plugins_' . $name, $pluginData);
91 }
92 }
93
94 protected function executePageHooks(string $hook, array &$data, string $template = null): void
95 {
96 $this->container->pluginManager->executeHooks(
97 $hook,
98 $data,
99 $this->buildPluginParameters($template)
100 );
101 }
102
103 protected function buildPluginParameters(?string $template): array
104 {
105 return [
106 'target' => $template,
107 'loggedin' => $this->container->loginManager->isLoggedIn(),
108 'basePath' => $this->container->basePath,
109 'rootPath' => preg_replace('#/index\.php$#', '', $this->container->basePath),
110 'bookmarkService' => $this->container->bookmarkService
111 ];
112 }
113
114 /**
115 * Simple helper which prepend the base path to redirect path.
116 *
117 * @param Response $response
118 * @param string $path Absolute path, e.g.: `/`, or `/admin/shaare/123` regardless of install directory
119 *
120 * @return Response updated
121 */
122 protected function redirect(Response $response, string $path): Response
123 {
124 return $response->withRedirect($this->container->basePath . $path);
125 }
126
127 /**
128 * Generates a redirection to the previous page, based on the HTTP_REFERER.
129 * It fails back to the home page.
130 *
131 * @param array $loopTerms Terms to remove from path and query string to prevent direction loop.
132 * @param array $clearParams List of parameter to remove from the query string of the referrer.
133 */
134 protected function redirectFromReferer(
135 Request $request,
136 Response $response,
137 array $loopTerms = [],
138 array $clearParams = [],
139 string $anchor = null
140 ): Response {
141 $defaultPath = $this->container->basePath . '/';
142 $referer = $this->container->environment['HTTP_REFERER'] ?? null;
143
144 if (null !== $referer) {
145 $currentUrl = parse_url($referer);
146 // If the referer is not related to Shaarli instance, redirect to default
147 if (isset($currentUrl['host'])
148 && strpos(index_url($this->container->environment), $currentUrl['host']) === false
149 ) {
150 return $response->withRedirect($defaultPath);
151 }
152
153 parse_str($currentUrl['query'] ?? '', $params);
154 $path = $currentUrl['path'] ?? $defaultPath;
155 } else {
156 $params = [];
157 $path = $defaultPath;
158 }
159
160 // Prevent redirection loop
161 if (isset($currentUrl)) {
162 foreach ($clearParams as $value) {
163 unset($params[$value]);
164 }
165
166 $checkQuery = implode('', array_keys($params));
167 foreach ($loopTerms as $value) {
168 if (strpos($path . $checkQuery, $value) !== false) {
169 $params = [];
170 $path = $defaultPath;
171 break;
172 }
173 }
174 }
175
176 $queryString = count($params) > 0 ? '?'. http_build_query($params) : '';
177 $anchor = $anchor ? '#' . $anchor : '';
178
179 return $response->withRedirect($path . $queryString . $anchor);
180 }
181}
diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php
new file mode 100644
index 00000000..76ed7690
--- /dev/null
+++ b/application/front/controller/visitor/TagCloudController.php
@@ -0,0 +1,121 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Class TagCloud
12 *
13 * Slim controller used to render the tag cloud and tag list pages.
14 */
15class TagCloudController extends ShaarliVisitorController
16{
17 protected const TYPE_CLOUD = 'cloud';
18 protected const TYPE_LIST = 'list';
19
20 /**
21 * Display the tag cloud through the template engine.
22 * This controller a few filters:
23 * - Visibility stored in the session for logged in users
24 * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
25 */
26 public function cloud(Request $request, Response $response): Response
27 {
28 return $this->processRequest(static::TYPE_CLOUD, $request, $response);
29 }
30
31 /**
32 * Display the tag list through the template engine.
33 * This controller a few filters:
34 * - Visibility stored in the session for logged in users
35 * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
36 * - `sort` query parameters:
37 * + `usage` (default): most used tags first
38 * + `alpha`: alphabetical order
39 */
40 public function list(Request $request, Response $response): Response
41 {
42 return $this->processRequest(static::TYPE_LIST, $request, $response);
43 }
44
45 /**
46 * Process the request for both tag cloud and tag list endpoints.
47 */
48 protected function processRequest(string $type, Request $request, Response $response): Response
49 {
50 if ($this->container->loginManager->isLoggedIn() === true) {
51 $visibility = $this->container->sessionManager->getSessionParameter('visibility');
52 }
53
54 $sort = $request->getQueryParam('sort');
55 $searchTags = $request->getQueryParam('searchtags');
56 $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : [];
57
58 $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
59
60 if (static::TYPE_CLOUD === $type || 'alpha' === $sort) {
61 // TODO: the sorting should be handled by bookmarkService instead of the controller
62 alphabetical_sort($tags, false, true);
63 }
64
65 if (static::TYPE_CLOUD === $type) {
66 $tags = $this->formatTagsForCloud($tags);
67 }
68
69 $tagsUrl = [];
70 foreach ($tags as $tag => $value) {
71 $tagsUrl[escape($tag)] = urlencode((string) $tag);
72 }
73
74 $searchTags = implode(' ', escape($filteringTags));
75 $searchTagsUrl = urlencode(implode(' ', $filteringTags));
76 $data = [
77 'search_tags' => escape($searchTags),
78 'search_tags_url' => $searchTagsUrl,
79 'tags' => escape($tags),
80 'tags_url' => $tagsUrl,
81 ];
82 $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
83 $this->assignAllView($data);
84
85 $searchTags = !empty($searchTags) ? $searchTags .' - ' : '';
86 $this->assignView(
87 'pagetitle',
88 $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli')
89 );
90
91 return $response->write($this->render('tag.' . $type));
92 }
93
94 /**
95 * Format the tags array for the tag cloud template.
96 *
97 * @param array<string, int> $tags List of tags as key with count as value
98 *
99 * @return mixed[] List of tags as key, with count and expected font size in a subarray
100 */
101 protected function formatTagsForCloud(array $tags): array
102 {
103 // We sort tags alphabetically, then choose a font size according to count.
104 // First, find max value.
105 $maxCount = count($tags) > 0 ? max($tags) : 0;
106 $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1;
107 $tagList = [];
108 foreach ($tags as $key => $value) {
109 // Tag font size scaling:
110 // default 15 and 30 logarithm bases affect scaling,
111 // 2.2 and 0.8 are arbitrary font sizes in em.
112 $size = log($value, 15) / $logMaxCount * 2.2 + 0.8;
113 $tagList[$key] = [
114 'count' => $value,
115 'size' => number_format($size, 2, '.', ''),
116 ];
117 }
118
119 return $tagList;
120 }
121}
diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php
new file mode 100644
index 00000000..de4e7ea2
--- /dev/null
+++ b/application/front/controller/visitor/TagController.php
@@ -0,0 +1,118 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Class TagController
12 *
13 * Slim controller handle tags.
14 */
15class TagController extends ShaarliVisitorController
16{
17 /**
18 * Add another tag in the current search through an HTTP redirection.
19 *
20 * @param array $args Should contain `newTag` key as tag to add to current search
21 */
22 public function addTag(Request $request, Response $response, array $args): Response
23 {
24 $newTag = $args['newTag'] ?? null;
25 $referer = $this->container->environment['HTTP_REFERER'] ?? null;
26
27 // In case browser does not send HTTP_REFERER, we search a single tag
28 if (null === $referer) {
29 if (null !== $newTag) {
30 return $this->redirect($response, '/?searchtags='. urlencode($newTag));
31 }
32
33 return $this->redirect($response, '/');
34 }
35
36 $currentUrl = parse_url($referer);
37 parse_str($currentUrl['query'] ?? '', $params);
38
39 if (null === $newTag) {
40 return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
41 }
42
43 // Prevent redirection loop
44 if (isset($params['addtag'])) {
45 unset($params['addtag']);
46 }
47
48 // Check if this tag is already in the search query and ignore it if it is.
49 // Each tag is always separated by a space
50 $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : [];
51
52 $addtag = true;
53 foreach ($currentTags as $value) {
54 if ($value === $newTag) {
55 $addtag = false;
56 break;
57 }
58 }
59
60 // Append the tag if necessary
61 if (true === $addtag) {
62 $currentTags[] = trim($newTag);
63 }
64
65 $params['searchtags'] = trim(implode(' ', $currentTags));
66
67 // We also remove page (keeping the same page has no sense, since the results are different)
68 unset($params['page']);
69
70 return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
71 }
72
73 /**
74 * Remove a tag from the current search through an HTTP redirection.
75 *
76 * @param array $args Should contain `tag` key as tag to remove from current search
77 */
78 public function removeTag(Request $request, Response $response, array $args): Response
79 {
80 $referer = $this->container->environment['HTTP_REFERER'] ?? null;
81
82 // If the referrer is not provided, we can update the search, so we failback on the bookmark list
83 if (empty($referer)) {
84 return $this->redirect($response, '/');
85 }
86
87 $tagToRemove = $args['tag'] ?? null;
88 $currentUrl = parse_url($referer);
89 parse_str($currentUrl['query'] ?? '', $params);
90
91 if (null === $tagToRemove) {
92 return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
93 }
94
95 // Prevent redirection loop
96 if (isset($params['removetag'])) {
97 unset($params['removetag']);
98 }
99
100 if (isset($params['searchtags'])) {
101 $tags = explode(' ', $params['searchtags']);
102 // Remove value from array $tags.
103 $tags = array_diff($tags, [$tagToRemove]);
104 $params['searchtags'] = implode(' ', $tags);
105
106 if (empty($params['searchtags'])) {
107 unset($params['searchtags']);
108 }
109
110 // We also remove page (keeping the same page has no sense, since the results are different)
111 unset($params['page']);
112 }
113
114 $queryParams = count($params) > 0 ? '?' . http_build_query($params) : '';
115
116 return $response->withRedirect(($currentUrl['path'] ?? './') . $queryParams);
117 }
118}
diff --git a/application/front/controllers/LoginController.php b/application/front/controllers/LoginController.php
deleted file mode 100644
index ae3599e0..00000000
--- a/application/front/controllers/LoginController.php
+++ /dev/null
@@ -1,48 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use Shaarli\Front\Exception\LoginBannedException;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class LoginController
13 *
14 * Slim controller used to render the login page.
15 *
16 * The login page is not available if the user is banned
17 * or if open shaarli setting is enabled.
18 *
19 * @package Front\Controller
20 */
21class LoginController extends ShaarliController
22{
23 public function index(Request $request, Response $response): Response
24 {
25 if ($this->container->loginManager->isLoggedIn()
26 || $this->container->conf->get('security.open_shaarli', false)
27 ) {
28 return $response->withRedirect('./');
29 }
30
31 $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams());
32 if ($userCanLogin !== true) {
33 throw new LoginBannedException();
34 }
35
36 if ($request->getParam('username') !== null) {
37 $this->assignView('username', escape($request->getParam('username')));
38 }
39
40 $this
41 ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER')))
42 ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
43 ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli'))
44 ;
45
46 return $response->write($this->render('loginform'));
47 }
48}
diff --git a/application/front/controllers/ShaarliController.php b/application/front/controllers/ShaarliController.php
deleted file mode 100644
index 2b828588..00000000
--- a/application/front/controllers/ShaarliController.php
+++ /dev/null
@@ -1,69 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Container\ShaarliContainer;
9
10abstract class ShaarliController
11{
12 /** @var ShaarliContainer */
13 protected $container;
14
15 /** @param ShaarliContainer $container Slim container (extended for attribute completion). */
16 public function __construct(ShaarliContainer $container)
17 {
18 $this->container = $container;
19 }
20
21 /**
22 * Assign variables to RainTPL template through the PageBuilder.
23 *
24 * @param mixed $value Value to assign to the template
25 */
26 protected function assignView(string $name, $value): self
27 {
28 $this->container->pageBuilder->assign($name, $value);
29
30 return $this;
31 }
32
33 protected function render(string $template): string
34 {
35 $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL));
36 $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));
37 $this->assignView('plugin_errors', $this->container->pluginManager->getErrors());
38
39 $this->executeDefaultHooks($template);
40
41 return $this->container->pageBuilder->render($template);
42 }
43
44 /**
45 * Call plugin hooks for header, footer and includes, specifying which page will be rendered.
46 * Then assign generated data to RainTPL.
47 */
48 protected function executeDefaultHooks(string $template): void
49 {
50 $common_hooks = [
51 'includes',
52 'header',
53 'footer',
54 ];
55
56 foreach ($common_hooks as $name) {
57 $plugin_data = [];
58 $this->container->pluginManager->executeHooks(
59 'render_' . $name,
60 $plugin_data,
61 [
62 'target' => $template,
63 'loggedin' => $this->container->loginManager->isLoggedIn()
64 ]
65 );
66 $this->assignView('plugins_' . $name, $plugin_data);
67 }
68 }
69}
diff --git a/application/front/exceptions/AlreadyInstalledException.php b/application/front/exceptions/AlreadyInstalledException.php
new file mode 100644
index 00000000..4add86cf
--- /dev/null
+++ b/application/front/exceptions/AlreadyInstalledException.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class AlreadyInstalledException extends ShaarliFrontException
8{
9 public function __construct()
10 {
11 $message = t('Shaarli has already been installed. Login to edit the configuration.');
12
13 parent::__construct($message, 401);
14 }
15}
diff --git a/application/front/exceptions/CantLoginException.php b/application/front/exceptions/CantLoginException.php
new file mode 100644
index 00000000..cd16635d
--- /dev/null
+++ b/application/front/exceptions/CantLoginException.php
@@ -0,0 +1,10 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class CantLoginException extends \Exception
8{
9
10}
diff --git a/application/front/exceptions/LoginBannedException.php b/application/front/exceptions/LoginBannedException.php
index b31a4a14..79d0ea15 100644
--- a/application/front/exceptions/LoginBannedException.php
+++ b/application/front/exceptions/LoginBannedException.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Front\Exception; 5namespace Shaarli\Front\Exception;
6 6
7class LoginBannedException extends ShaarliException 7class LoginBannedException extends ShaarliFrontException
8{ 8{
9 public function __construct() 9 public function __construct()
10 { 10 {
diff --git a/application/front/exceptions/OpenShaarliPasswordException.php b/application/front/exceptions/OpenShaarliPasswordException.php
new file mode 100644
index 00000000..a6f0b3ae
--- /dev/null
+++ b/application/front/exceptions/OpenShaarliPasswordException.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7/**
8 * Class OpenShaarliPasswordException
9 *
10 * Raised if the user tries to change the admin password on an open shaarli instance.
11 */
12class OpenShaarliPasswordException extends ShaarliFrontException
13{
14 public function __construct()
15 {
16 parent::__construct(t('You are not supposed to change a password on an Open Shaarli.'), 403);
17 }
18}
diff --git a/application/front/exceptions/ResourcePermissionException.php b/application/front/exceptions/ResourcePermissionException.php
new file mode 100644
index 00000000..8fbf03b9
--- /dev/null
+++ b/application/front/exceptions/ResourcePermissionException.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class ResourcePermissionException extends ShaarliFrontException
8{
9 public function __construct(string $message)
10 {
11 parent::__construct($message, 500);
12 }
13}
diff --git a/application/front/exceptions/ShaarliException.php b/application/front/exceptions/ShaarliFrontException.php
index 800bfbec..73847e6d 100644
--- a/application/front/exceptions/ShaarliException.php
+++ b/application/front/exceptions/ShaarliFrontException.php
@@ -9,11 +9,11 @@ use Throwable;
9/** 9/**
10 * Class ShaarliException 10 * Class ShaarliException
11 * 11 *
12 * Abstract exception class used to defined any custom exception thrown during front rendering. 12 * Exception class used to defined any custom exception thrown during front rendering.
13 * 13 *
14 * @package Front\Exception 14 * @package Front\Exception
15 */ 15 */
16abstract class ShaarliException extends \Exception 16class ShaarliFrontException extends \Exception
17{ 17{
18 /** Override parent constructor to force $message and $httpCode parameters to be set. */ 18 /** Override parent constructor to force $message and $httpCode parameters to be set. */
19 public function __construct(string $message, int $httpCode, Throwable $previous = null) 19 public function __construct(string $message, int $httpCode, Throwable $previous = null)
diff --git a/application/front/exceptions/ThumbnailsDisabledException.php b/application/front/exceptions/ThumbnailsDisabledException.php
new file mode 100644
index 00000000..0ed337f5
--- /dev/null
+++ b/application/front/exceptions/ThumbnailsDisabledException.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class ThumbnailsDisabledException extends ShaarliFrontException
8{
9 public function __construct()
10 {
11 $message = t('Picture wall unavailable (thumbnails are disabled).');
12
13 parent::__construct($message, 400);
14 }
15}
diff --git a/application/front/exceptions/UnauthorizedException.php b/application/front/exceptions/UnauthorizedException.php
new file mode 100644
index 00000000..4231094a
--- /dev/null
+++ b/application/front/exceptions/UnauthorizedException.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7/**
8 * Class UnauthorizedException
9 *
10 * Exception raised if the user tries to access a ShaarliAdminController while logged out.
11 */
12class UnauthorizedException extends \Exception
13{
14
15}
diff --git a/application/front/exceptions/WrongTokenException.php b/application/front/exceptions/WrongTokenException.php
new file mode 100644
index 00000000..42002720
--- /dev/null
+++ b/application/front/exceptions/WrongTokenException.php
@@ -0,0 +1,18 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7/**
8 * Class OpenShaarliPasswordException
9 *
10 * Raised if the user tries to perform an action with an invalid XSRF token.
11 */
12class WrongTokenException extends ShaarliFrontException
13{
14 public function __construct()
15 {
16 parent::__construct(t('Wrong token.'), 403);
17 }
18}
diff --git a/application/ApplicationUtils.php b/application/helper/ApplicationUtils.php
index 3aa21829..4b34e114 100644
--- a/application/ApplicationUtils.php
+++ b/application/helper/ApplicationUtils.php
@@ -1,5 +1,5 @@
1<?php 1<?php
2namespace Shaarli; 2namespace Shaarli\Helper;
3 3
4use Exception; 4use Exception;
5use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
@@ -14,8 +14,9 @@ class ApplicationUtils
14 */ 14 */
15 public static $VERSION_FILE = 'shaarli_version.php'; 15 public static $VERSION_FILE = 'shaarli_version.php';
16 16
17 private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; 17 public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli';
18 private static $GIT_BRANCHES = array('latest', 'stable'); 18 public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
19 public static $GIT_BRANCHES = array('latest', 'stable');
19 private static $VERSION_START_TAG = '<?php /* '; 20 private static $VERSION_START_TAG = '<?php /* ';
20 private static $VERSION_END_TAG = ' */ ?>'; 21 private static $VERSION_END_TAG = ' */ ?>';
21 22
@@ -125,7 +126,7 @@ class ApplicationUtils
125 // Late Static Binding allows overriding within tests 126 // Late Static Binding allows overriding within tests
126 // See http://php.net/manual/en/language.oop5.late-static-bindings.php 127 // See http://php.net/manual/en/language.oop5.late-static-bindings.php
127 $latestVersion = static::getVersion( 128 $latestVersion = static::getVersion(
128 self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE 129 self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
129 ); 130 );
130 131
131 if (!$latestVersion) { 132 if (!$latestVersion) {
@@ -171,35 +172,45 @@ class ApplicationUtils
171 /** 172 /**
172 * Checks Shaarli has the proper access permissions to its resources 173 * Checks Shaarli has the proper access permissions to its resources
173 * 174 *
174 * @param ConfigManager $conf Configuration Manager instance. 175 * @param ConfigManager $conf Configuration Manager instance.
176 * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template.
177 * Currently we only need to be able to read the theme and write in raintpl cache.
175 * 178 *
176 * @return array A list of the detected configuration issues 179 * @return array A list of the detected configuration issues
177 */ 180 */
178 public static function checkResourcePermissions($conf) 181 public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array
179 { 182 {
180 $errors = array(); 183 $errors = [];
181 $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); 184 $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
182 185
183 // Check script and template directories are readable 186 // Check script and template directories are readable
184 foreach (array( 187 foreach ([
185 'application', 188 'application',
186 'inc', 189 'inc',
187 'plugins', 190 'plugins',
188 $rainTplDir, 191 $rainTplDir,
189 $rainTplDir . '/' . $conf->get('resource.theme'), 192 $rainTplDir . '/' . $conf->get('resource.theme'),
190 ) as $path) { 193 ] as $path) {
191 if (!is_readable(realpath($path))) { 194 if (!is_readable(realpath($path))) {
192 $errors[] = '"' . $path . '" ' . t('directory is not readable'); 195 $errors[] = '"' . $path . '" ' . t('directory is not readable');
193 } 196 }
194 } 197 }
195 198
196 // Check cache and data directories are readable and writable 199 // Check cache and data directories are readable and writable
197 foreach (array( 200 if ($minimalMode) {
198 $conf->get('resource.thumbnails_cache'), 201 $folders = [
199 $conf->get('resource.data_dir'), 202 $conf->get('resource.raintpl_tmp'),
200 $conf->get('resource.page_cache'), 203 ];
201 $conf->get('resource.raintpl_tmp'), 204 } else {
202 ) as $path) { 205 $folders = [
206 $conf->get('resource.thumbnails_cache'),
207 $conf->get('resource.data_dir'),
208 $conf->get('resource.page_cache'),
209 $conf->get('resource.raintpl_tmp'),
210 ];
211 }
212
213 foreach ($folders as $path) {
203 if (!is_readable(realpath($path))) { 214 if (!is_readable(realpath($path))) {
204 $errors[] = '"' . $path . '" ' . t('directory is not readable'); 215 $errors[] = '"' . $path . '" ' . t('directory is not readable');
205 } 216 }
@@ -208,6 +219,10 @@ class ApplicationUtils
208 } 219 }
209 } 220 }
210 221
222 if ($minimalMode) {
223 return $errors;
224 }
225
211 // Check configuration files are readable and writable 226 // Check configuration files are readable and writable
212 foreach (array( 227 foreach (array(
213 $conf->getConfigFileExt(), 228 $conf->getConfigFileExt(),
@@ -246,4 +261,54 @@ class ApplicationUtils
246 { 261 {
247 return hash_hmac('sha256', $currentVersion, $salt); 262 return hash_hmac('sha256', $currentVersion, $salt);
248 } 263 }
264
265 /**
266 * Get a list of PHP extensions used by Shaarli.
267 *
268 * @return array[] List of extension with following keys:
269 * - name: extension name
270 * - required: whether the extension is required to use Shaarli
271 * - desc: short description of extension usage in Shaarli
272 * - loaded: whether the extension is properly loaded or not
273 */
274 public static function getPhpExtensionsRequirement(): array
275 {
276 $extensions = [
277 ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')],
278 ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')],
279 ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')],
280 ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')],
281 ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')],
282 ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')],
283 ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')],
284 ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')],
285 ];
286
287 foreach ($extensions as &$extension) {
288 $extension['loaded'] = extension_loaded($extension['name']);
289 }
290
291 return $extensions;
292 }
293
294 /**
295 * Return the EOL date of given PHP version. If the version is unknown,
296 * we return today + 2 years.
297 *
298 * @param string $fullVersion PHP version, e.g. 7.4.7
299 *
300 * @return string Date format: YYYY-MM-DD
301 */
302 public static function getPhpEol(string $fullVersion): string
303 {
304 preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches);
305
306 return [
307 '7.1' => '2019-12-01',
308 '7.2' => '2020-11-30',
309 '7.3' => '2021-12-06',
310 '7.4' => '2022-11-28',
311 '8.0' => '2023-12-01',
312 ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d');
313 }
249} 314}
diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php
new file mode 100644
index 00000000..5fabc907
--- /dev/null
+++ b/application/helper/DailyPageHelper.php
@@ -0,0 +1,208 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Helper;
6
7use Shaarli\Bookmark\Bookmark;
8use Slim\Http\Request;
9
10class DailyPageHelper
11{
12 public const MONTH = 'month';
13 public const WEEK = 'week';
14 public const DAY = 'day';
15
16 /**
17 * Extracts the type of the daily to display from the HTTP request parameters
18 *
19 * @param Request $request HTTP request
20 *
21 * @return string month/week/day
22 */
23 public static function extractRequestedType(Request $request): string
24 {
25 if ($request->getQueryParam(static::MONTH) !== null) {
26 return static::MONTH;
27 } elseif ($request->getQueryParam(static::WEEK) !== null) {
28 return static::WEEK;
29 }
30
31 return static::DAY;
32 }
33
34 /**
35 * Extracts a DateTimeImmutable from provided HTTP request.
36 * If no parameter is provided, we rely on the creation date of the latest provided created bookmark.
37 * If the datastore is empty or no bookmark is provided, we use the current date.
38 *
39 * @param string $type month/week/day
40 * @param string|null $requestedDate Input string extracted from the request
41 * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
42 *
43 * @return \DateTimeImmutable from input or latest bookmark.
44 *
45 * @throws \Exception Type not supported.
46 */
47 public static function extractRequestedDateTime(
48 string $type,
49 ?string $requestedDate,
50 Bookmark $latestBookmark = null
51 ): \DateTimeImmutable {
52 $format = static::getFormatByType($type);
53 if (empty($requestedDate)) {
54 return $latestBookmark instanceof Bookmark
55 ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
56 : new \DateTimeImmutable()
57 ;
58 }
59
60 // W is not supported by createFromFormat...
61 if ($type === static::WEEK) {
62 return (new \DateTimeImmutable())
63 ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
64 ;
65 }
66
67 return \DateTimeImmutable::createFromFormat($format, $requestedDate);
68 }
69
70 /**
71 * Get the DateTime format used by provided type
72 * Examples:
73 * - day: 20201016 (<year><month><day>)
74 * - week: 202041 (<year><week number>)
75 * - month: 202010 (<year><month>)
76 *
77 * @param string $type month/week/day
78 *
79 * @return string DateTime compatible format
80 *
81 * @see https://www.php.net/manual/en/datetime.format.php
82 *
83 * @throws \Exception Type not supported.
84 */
85 public static function getFormatByType(string $type): string
86 {
87 switch ($type) {
88 case static::MONTH:
89 return 'Ym';
90 case static::WEEK:
91 return 'YW';
92 case static::DAY:
93 return 'Ymd';
94 default:
95 throw new \Exception('Unsupported daily format type');
96 }
97 }
98
99 /**
100 * Get the first DateTime of the time period depending on given datetime and type.
101 * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
102 * and we don't want to alter original datetime.
103 *
104 * @param string $type month/week/day
105 * @param \DateTimeImmutable $requested DateTime extracted from request input
106 * (should come from extractRequestedDateTime)
107 *
108 * @return \DateTimeInterface First DateTime of the time period
109 *
110 * @throws \Exception Type not supported.
111 */
112 public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
113 {
114 switch ($type) {
115 case static::MONTH:
116 return $requested->modify('first day of this month midnight');
117 case static::WEEK:
118 return $requested->modify('Monday this week midnight');
119 case static::DAY:
120 return $requested->modify('Today midnight');
121 default:
122 throw new \Exception('Unsupported daily format type');
123 }
124 }
125
126 /**
127 * Get the last DateTime of the time period depending on given datetime and type.
128 * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
129 * and we don't want to alter original datetime.
130 *
131 * @param string $type month/week/day
132 * @param \DateTimeImmutable $requested DateTime extracted from request input
133 * (should come from extractRequestedDateTime)
134 *
135 * @return \DateTimeInterface Last DateTime of the time period
136 *
137 * @throws \Exception Type not supported.
138 */
139 public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
140 {
141 switch ($type) {
142 case static::MONTH:
143 return $requested->modify('last day of this month 23:59:59');
144 case static::WEEK:
145 return $requested->modify('Sunday this week 23:59:59');
146 case static::DAY:
147 return $requested->modify('Today 23:59:59');
148 default:
149 throw new \Exception('Unsupported daily format type');
150 }
151 }
152
153 /**
154 * Get localized description of the time period depending on given datetime and type.
155 * Example: for a month period, it returns `October, 2020`.
156 *
157 * @param string $type month/week/day
158 * @param \DateTimeImmutable $requested DateTime extracted from request input
159 * (should come from extractRequestedDateTime)
160 *
161 * @return string Localized time period description
162 *
163 * @throws \Exception Type not supported.
164 */
165 public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string
166 {
167 switch ($type) {
168 case static::MONTH:
169 return $requested->format('F') . ', ' . $requested->format('Y');
170 case static::WEEK:
171 $requested = $requested->modify('Monday this week');
172 return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
173 case static::DAY:
174 $out = '';
175 if ($requested->format('Ymd') === date('Ymd')) {
176 $out = t('Today') . ' - ';
177 } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
178 $out = t('Yesterday') . ' - ';
179 }
180 return $out . format_date($requested, false);
181 default:
182 throw new \Exception('Unsupported daily format type');
183 }
184 }
185
186 /**
187 * Get the number of items to display in the RSS feed depending on the given type.
188 *
189 * @param string $type month/week/day
190 *
191 * @return int number of elements
192 *
193 * @throws \Exception Type not supported.
194 */
195 public static function getRssLengthByType(string $type): int
196 {
197 switch ($type) {
198 case static::MONTH:
199 return 12; // 1 year
200 case static::WEEK:
201 return 26; // ~6 months
202 case static::DAY:
203 return 30; // ~1 month
204 default:
205 throw new \Exception('Unsupported daily format type');
206 }
207 }
208}
diff --git a/application/FileUtils.php b/application/helper/FileUtils.php
index 30560bfc..2eac0793 100644
--- a/application/FileUtils.php
+++ b/application/helper/FileUtils.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2 2
3namespace Shaarli; 3namespace Shaarli\Helper;
4 4
5use Shaarli\Exceptions\IOException; 5use Shaarli\Exceptions\IOException;
6 6
@@ -81,4 +81,60 @@ class FileUtils
81 ) 81 )
82 ); 82 );
83 } 83 }
84
85 /**
86 * Recursively deletes a folder content, and deletes itself optionally.
87 * If an excluded file is found, folders won't be deleted.
88 *
89 * Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory.
90 *
91 * @param string $path
92 * @param bool $selfDelete Delete the provided folder if true, only its content if false.
93 * @param array $exclude
94 */
95 public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool
96 {
97 $skipped = false;
98
99 if (!is_dir($path)) {
100 throw new IOException(t('Provided path is not a directory.'));
101 }
102
103 if (!static::isPathInShaarliFolder($path)) {
104 throw new IOException(t('Trying to delete a folder outside of Shaarli path.'));
105 }
106
107 foreach (new \DirectoryIterator($path) as $file) {
108 if($file->isDot()) {
109 continue;
110 }
111
112 if (in_array($file->getBasename(), $exclude, true)) {
113 $skipped = true;
114 continue;
115 }
116
117 if ($file->isFile()) {
118 unlink($file->getPathname());
119 } elseif($file->isDir()) {
120 $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped;
121 }
122 }
123
124 if ($selfDelete && !$skipped) {
125 rmdir($path);
126 }
127
128 return $skipped;
129 }
130
131 /**
132 * Checks that the given path is inside Shaarli directory.
133 */
134 public static function isPathInShaarliFolder(string $path): bool
135 {
136 $rootDirectory = dirname(dirname(dirname(__FILE__)));
137
138 return strpos(realpath($path), $rootDirectory) !== false;
139 }
84} 140}
diff --git a/application/http/HttpAccess.php b/application/http/HttpAccess.php
new file mode 100644
index 00000000..646a5264
--- /dev/null
+++ b/application/http/HttpAccess.php
@@ -0,0 +1,47 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Http;
6
7/**
8 * Class HttpAccess
9 *
10 * This is mostly an OOP wrapper for HTTP functions defined in `HttpUtils`.
11 * It is used as dependency injection in Shaarli's container.
12 *
13 * @package Shaarli\Http
14 */
15class HttpAccess
16{
17 public function getHttpResponse(
18 $url,
19 $timeout = 30,
20 $maxBytes = 4194304,
21 $curlHeaderFunction = null,
22 $curlWriteFunction = null
23 ) {
24 return get_http_response($url, $timeout, $maxBytes, $curlHeaderFunction, $curlWriteFunction);
25 }
26
27 public function getCurlDownloadCallback(
28 &$charset,
29 &$title,
30 &$description,
31 &$keywords,
32 $retrieveDescription
33 ) {
34 return get_curl_download_callback(
35 $charset,
36 $title,
37 $description,
38 $keywords,
39 $retrieveDescription
40 );
41 }
42
43 public function getCurlHeaderCallback(&$charset, $curlGetInfo = 'curl_getinfo')
44 {
45 return get_curl_header_callback($charset, $curlGetInfo);
46 }
47}
diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php
index 2ea9195d..28c12969 100644
--- a/application/http/HttpUtils.php
+++ b/application/http/HttpUtils.php
@@ -6,12 +6,14 @@ use Shaarli\Http\Url;
6 * GET an HTTP URL to retrieve its content 6 * GET an HTTP URL to retrieve its content
7 * Uses the cURL library or a fallback method 7 * Uses the cURL library or a fallback method
8 * 8 *
9 * @param string $url URL to get (http://...) 9 * @param string $url URL to get (http://...)
10 * @param int $timeout network timeout (in seconds) 10 * @param int $timeout network timeout (in seconds)
11 * @param int $maxBytes maximum downloaded bytes (default: 4 MiB) 11 * @param int $maxBytes maximum downloaded bytes (default: 4 MiB)
12 * @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION). 12 * @param callable|string $curlHeaderFunction Optional callback called during the download of headers
13 * Can be used to add download conditions on the 13 * (CURLOPT_HEADERFUNCTION)
14 * headers (response code, content type, etc.). 14 * @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION).
15 * Can be used to add download conditions on the
16 * headers (response code, content type, etc.).
15 * 17 *
16 * @return array HTTP response headers, downloaded content 18 * @return array HTTP response headers, downloaded content
17 * 19 *
@@ -35,8 +37,13 @@ use Shaarli\Http\Url;
35 * @see http://stackoverflow.com/q/9183178 37 * @see http://stackoverflow.com/q/9183178
36 * @see http://stackoverflow.com/q/1462720 38 * @see http://stackoverflow.com/q/1462720
37 */ 39 */
38function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null) 40function get_http_response(
39{ 41 $url,
42 $timeout = 30,
43 $maxBytes = 4194304,
44 $curlHeaderFunction = null,
45 $curlWriteFunction = null
46) {
40 $urlObj = new Url($url); 47 $urlObj = new Url($url);
41 $cleanUrl = $urlObj->idnToAscii(); 48 $cleanUrl = $urlObj->idnToAscii();
42 49
@@ -70,7 +77,8 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF
70 // General cURL settings 77 // General cURL settings
71 curl_setopt($ch, CURLOPT_AUTOREFERER, true); 78 curl_setopt($ch, CURLOPT_AUTOREFERER, true);
72 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 79 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
73 curl_setopt($ch, CURLOPT_HEADER, true); 80 // Default header download if the $curlHeaderFunction is not defined
81 curl_setopt($ch, CURLOPT_HEADER, !is_callable($curlHeaderFunction));
74 curl_setopt( 82 curl_setopt(
75 $ch, 83 $ch,
76 CURLOPT_HTTPHEADER, 84 CURLOPT_HTTPHEADER,
@@ -81,25 +89,21 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF
81 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); 89 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
82 curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); 90 curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
83 91
84 if (is_callable($curlWriteFunction)) {
85 curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction);
86 }
87
88 // Max download size management 92 // Max download size management
89 curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16); 93 curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16);
90 curl_setopt($ch, CURLOPT_NOPROGRESS, false); 94 curl_setopt($ch, CURLOPT_NOPROGRESS, false);
95 if (is_callable($curlHeaderFunction)) {
96 curl_setopt($ch, CURLOPT_HEADERFUNCTION, $curlHeaderFunction);
97 }
98 if (is_callable($curlWriteFunction)) {
99 curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction);
100 }
91 curl_setopt( 101 curl_setopt(
92 $ch, 102 $ch,
93 CURLOPT_PROGRESSFUNCTION, 103 CURLOPT_PROGRESSFUNCTION,
94 function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) { 104 function ($arg0, $arg1, $arg2, $arg3, $arg4) use ($maxBytes) {
95 if (version_compare(phpversion(), '5.5', '<')) { 105 $downloaded = $arg2;
96 // PHP version lower than 5.5 106
97 // Callback has 4 arguments
98 $downloaded = $arg1;
99 } else {
100 // Callback has 5 arguments
101 $downloaded = $arg2;
102 }
103 // Non-zero return stops downloading 107 // Non-zero return stops downloading
104 return ($downloaded > $maxBytes) ? 1 : 0; 108 return ($downloaded > $maxBytes) ? 1 : 0;
105 } 109 }
@@ -369,7 +373,11 @@ function server_url($server)
369 */ 373 */
370function index_url($server) 374function index_url($server)
371{ 375{
372 $scriptname = $server['SCRIPT_NAME']; 376 if (defined('SHAARLI_ROOT_URL') && null !== SHAARLI_ROOT_URL) {
377 return rtrim(SHAARLI_ROOT_URL, '/') . '/';
378 }
379
380 $scriptname = !empty($server['SCRIPT_NAME']) ? $server['SCRIPT_NAME'] : '/';
373 if (endsWith($scriptname, 'index.php')) { 381 if (endsWith($scriptname, 'index.php')) {
374 $scriptname = substr($scriptname, 0, -9); 382 $scriptname = substr($scriptname, 0, -9);
375 } 383 }
@@ -377,7 +385,7 @@ function index_url($server)
377} 385}
378 386
379/** 387/**
380 * Returns the absolute URL of the current script, with the query 388 * Returns the absolute URL of the current script, with current route and query
381 * 389 *
382 * If the resource is "index.php", then it is removed (for better-looking URLs) 390 * If the resource is "index.php", then it is removed (for better-looking URLs)
383 * 391 *
@@ -387,10 +395,17 @@ function index_url($server)
387 */ 395 */
388function page_url($server) 396function page_url($server)
389{ 397{
398 $scriptname = $server['SCRIPT_NAME'] ?? '';
399 if (endsWith($scriptname, 'index.php')) {
400 $scriptname = substr($scriptname, 0, -9);
401 }
402
403 $route = preg_replace('@^' . $scriptname . '@', '', $server['REQUEST_URI'] ?? '');
390 if (! empty($server['QUERY_STRING'])) { 404 if (! empty($server['QUERY_STRING'])) {
391 return index_url($server).'?'.$server['QUERY_STRING']; 405 return index_url($server) . $route . '?' . $server['QUERY_STRING'];
392 } 406 }
393 return index_url($server); 407
408 return index_url($server) . $route;
394} 409}
395 410
396/** 411/**
@@ -477,3 +492,132 @@ function is_https($server)
477 492
478 return ! empty($server['HTTPS']); 493 return ! empty($server['HTTPS']);
479} 494}
495
496/**
497 * Get cURL callback function for CURLOPT_WRITEFUNCTION
498 *
499 * @param string $charset to extract from the downloaded page (reference)
500 * @param string $curlGetInfo Optionally overrides curl_getinfo function
501 *
502 * @return Closure
503 */
504function get_curl_header_callback(
505 &$charset,
506 $curlGetInfo = 'curl_getinfo'
507) {
508 $isRedirected = false;
509
510 return function ($ch, $data) use ($curlGetInfo, &$charset, &$isRedirected) {
511 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
512 $chunkLength = strlen($data);
513 if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
514 $isRedirected = true;
515 return $chunkLength;
516 }
517 if (!empty($responseCode) && $responseCode !== 200) {
518 return false;
519 }
520 // After a redirection, the content type will keep the previous request value
521 // until it finds the next content-type header.
522 if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
523 $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
524 }
525 if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
526 return false;
527 }
528 if (!empty($contentType) && empty($charset)) {
529 $charset = header_extract_charset($contentType);
530 }
531
532 return $chunkLength;
533 };
534}
535
536/**
537 * Get cURL callback function for CURLOPT_WRITEFUNCTION
538 *
539 * @param string $charset to extract from the downloaded page (reference)
540 * @param string $title to extract from the downloaded page (reference)
541 * @param string $description to extract from the downloaded page (reference)
542 * @param string $keywords to extract from the downloaded page (reference)
543 * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
544 * @param string $curlGetInfo Optionally overrides curl_getinfo function
545 *
546 * @return Closure
547 */
548function get_curl_download_callback(
549 &$charset,
550 &$title,
551 &$description,
552 &$keywords,
553 $retrieveDescription
554) {
555 $currentChunk = 0;
556 $foundChunk = null;
557
558 /**
559 * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
560 *
561 * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
562 * Then we extract the title and the charset and stop the download when it's done.
563 *
564 * @param resource $ch cURL resource
565 * @param string $data chunk of data being downloaded
566 *
567 * @return int|bool length of $data or false if we need to stop the download
568 */
569 return function ($ch, $data) use (
570 $retrieveDescription,
571 &$charset,
572 &$title,
573 &$description,
574 &$keywords,
575 &$currentChunk,
576 &$foundChunk
577 ) {
578 $chunkLength = strlen($data);
579 $currentChunk++;
580
581 if (empty($charset)) {
582 $charset = html_extract_charset($data);
583 }
584 if (empty($title)) {
585 $title = html_extract_title($data);
586 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
587 }
588 if (empty($title)) {
589 $title = html_extract_tag('title', $data);
590 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
591 }
592 if ($retrieveDescription && empty($description)) {
593 $description = html_extract_tag('description', $data);
594 $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
595 }
596 if ($retrieveDescription && empty($keywords)) {
597 $keywords = html_extract_tag('keywords', $data);
598 if (! empty($keywords)) {
599 $foundChunk = $currentChunk;
600 // Keywords use the format tag1, tag2 multiple words, tag
601 // So we format them to match Shaarli's separator and glue multiple words with '-'
602 $keywords = implode(' ', array_map(function($keyword) {
603 return implode('-', preg_split('/\s+/', trim($keyword)));
604 }, explode(',', $keywords)));
605 }
606 }
607
608 // We got everything we want, stop the download.
609 // If we already found either the title, description or keywords,
610 // it's highly unlikely that we'll found the other metas further than
611 // in the same chunk of data or the next one. So we also stop the download after that.
612 if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
613 && (! $retrieveDescription
614 || $foundChunk < $currentChunk
615 || (!empty($title) && !empty($description) && !empty($keywords))
616 )
617 ) {
618 return false;
619 }
620
621 return $chunkLength;
622 };
623}
diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php
new file mode 100644
index 00000000..ba9bd40c
--- /dev/null
+++ b/application/http/MetadataRetriever.php
@@ -0,0 +1,69 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Http;
6
7use Shaarli\Config\ConfigManager;
8
9/**
10 * HTTP Tool used to extract metadata from external URL (title, description, etc.).
11 */
12class MetadataRetriever
13{
14 /** @var ConfigManager */
15 protected $conf;
16
17 /** @var HttpAccess */
18 protected $httpAccess;
19
20 public function __construct(ConfigManager $conf, HttpAccess $httpAccess)
21 {
22 $this->conf = $conf;
23 $this->httpAccess = $httpAccess;
24 }
25
26 /**
27 * Retrieve metadata for given URL.
28 *
29 * @return array [
30 * 'title' => <remote title>,
31 * 'description' => <remote description>,
32 * 'tags' => <remote keywords>,
33 * ]
34 */
35 public function retrieve(string $url): array
36 {
37 $charset = null;
38 $title = null;
39 $description = null;
40 $tags = null;
41 $retrieveDescription = $this->conf->get('general.retrieve_description');
42
43 // Short timeout to keep the application responsive
44 // The callback will fill $charset and $title with data from the downloaded page.
45 $this->httpAccess->getHttpResponse(
46 $url,
47 $this->conf->get('general.download_timeout', 30),
48 $this->conf->get('general.download_max_size', 4194304),
49 $this->httpAccess->getCurlHeaderCallback($charset),
50 $this->httpAccess->getCurlDownloadCallback(
51 $charset,
52 $title,
53 $description,
54 $tags,
55 $retrieveDescription
56 )
57 );
58
59 if (!empty($title) && strtolower($charset) !== 'utf-8') {
60 $title = mb_convert_encoding($title, 'utf-8', $charset);
61 }
62
63 return [
64 'title' => $title,
65 'description' => $description,
66 'tags' => $tags,
67 ];
68 }
69}
diff --git a/application/legacy/LegacyController.php b/application/legacy/LegacyController.php
new file mode 100644
index 00000000..826604e7
--- /dev/null
+++ b/application/legacy/LegacyController.php
@@ -0,0 +1,162 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Legacy;
6
7use Shaarli\Feed\FeedBuilder;
8use Shaarli\Front\Controller\Visitor\ShaarliVisitorController;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * We use this to maintain legacy routes, and redirect requests to the corresponding Slim route.
14 * Only public routes, and both `?addlink` and `?post` were kept here.
15 * Other routes will just display the linklist.
16 *
17 * @deprecated
18 */
19class LegacyController extends ShaarliVisitorController
20{
21 /** @var string[] Both `?post` and `?addlink` do not use `?do=` format. */
22 public const LEGACY_GET_ROUTES = [
23 'post',
24 'addlink',
25 ];
26
27 /**
28 * This method will call `$action` method, which will redirect to corresponding Slim route.
29 */
30 public function process(Request $request, Response $response, string $action): Response
31 {
32 if (!method_exists($this, $action)) {
33 throw new UnknowLegacyRouteException();
34 }
35
36 return $this->{$action}($request, $response);
37 }
38
39 /** Legacy route: ?post= */
40 public function post(Request $request, Response $response): Response
41 {
42 $route = '/admin/shaare';
43 $buildParameters = function (?array $parameters, bool $encode) {
44 if ($encode) {
45 $parameters = array_map('urlencode', $parameters);
46 }
47
48 return count($parameters) > 0 ? '?' . http_build_query($parameters) : '';
49 };
50
51
52 if (!$this->container->loginManager->isLoggedIn()) {
53 $parameters = $buildParameters($request->getQueryParams(), true);
54 return $this->redirect($response, '/login?returnurl='. $this->getBasePath() . $route . $parameters);
55 }
56
57 $parameters = $buildParameters($request->getQueryParams(), false);
58
59 return $this->redirect($response, $route . $parameters);
60 }
61
62 /** Legacy route: ?addlink= */
63 protected function addlink(Request $request, Response $response): Response
64 {
65 $route = '/admin/add-shaare';
66
67 if (!$this->container->loginManager->isLoggedIn()) {
68 return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route);
69 }
70
71 return $this->redirect($response, $route);
72 }
73
74 /** Legacy route: ?do=login */
75 protected function login(Request $request, Response $response): Response
76 {
77 $returnUrl = $request->getQueryParam('returnurl');
78
79 return $this->redirect($response, '/login' . ($returnUrl ? '?returnurl=' . $returnUrl : ''));
80 }
81
82 /** Legacy route: ?do=logout */
83 protected function logout(Request $request, Response $response): Response
84 {
85 return $this->redirect($response, '/admin/logout');
86 }
87
88 /** Legacy route: ?do=picwall */
89 protected function picwall(Request $request, Response $response): Response
90 {
91 return $this->redirect($response, '/picture-wall');
92 }
93
94 /** Legacy route: ?do=tagcloud */
95 protected function tagcloud(Request $request, Response $response): Response
96 {
97 return $this->redirect($response, '/tags/cloud');
98 }
99
100 /** Legacy route: ?do=taglist */
101 protected function taglist(Request $request, Response $response): Response
102 {
103 return $this->redirect($response, '/tags/list');
104 }
105
106 /** Legacy route: ?do=daily */
107 protected function daily(Request $request, Response $response): Response
108 {
109 $dayParam = !empty($request->getParam('day')) ? '?day=' . escape($request->getParam('day')) : '';
110
111 return $this->redirect($response, '/daily' . $dayParam);
112 }
113
114 /** Legacy route: ?do=rss */
115 protected function rss(Request $request, Response $response): Response
116 {
117 return $this->feed($request, $response, FeedBuilder::$FEED_RSS);
118 }
119
120 /** Legacy route: ?do=atom */
121 protected function atom(Request $request, Response $response): Response
122 {
123 return $this->feed($request, $response, FeedBuilder::$FEED_ATOM);
124 }
125
126 /** Legacy route: ?do=opensearch */
127 protected function opensearch(Request $request, Response $response): Response
128 {
129 return $this->redirect($response, '/open-search');
130 }
131
132 /** Legacy route: ?do=dailyrss */
133 protected function dailyrss(Request $request, Response $response): Response
134 {
135 return $this->redirect($response, '/daily-rss');
136 }
137
138 /** Legacy route: ?do=feed */
139 protected function feed(Request $request, Response $response, string $feedType): Response
140 {
141 $parameters = count($request->getQueryParams()) > 0 ? '?' . http_build_query($request->getQueryParams()) : '';
142
143 return $this->redirect($response, '/feed/' . $feedType . $parameters);
144 }
145
146 /** Legacy route: ?do=configure */
147 protected function configure(Request $request, Response $response): Response
148 {
149 $route = '/admin/configure';
150
151 if (!$this->container->loginManager->isLoggedIn()) {
152 return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route);
153 }
154
155 return $this->redirect($response, $route);
156 }
157
158 protected function getBasePath(): string
159 {
160 return $this->container->basePath ?: '';
161 }
162}
diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php
index 7ccf5e54..5c02a21b 100644
--- a/application/legacy/LegacyLinkDB.php
+++ b/application/legacy/LegacyLinkDB.php
@@ -8,7 +8,8 @@ use DateTime;
8use Iterator; 8use Iterator;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Exceptions\IOException; 10use Shaarli\Exceptions\IOException;
11use Shaarli\FileUtils; 11use Shaarli\Helper\FileUtils;
12use Shaarli\Render\PageCacheManager;
12 13
13/** 14/**
14 * Data storage for bookmarks. 15 * Data storage for bookmarks.
@@ -352,7 +353,8 @@ You use the community supported version of the original Shaarli project, by Seba
352 353
353 $this->write(); 354 $this->write();
354 355
355 invalidateCaches($pageCacheDir); 356 $pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn);
357 $pageCacheManager->invalidateCaches();
356 } 358 }
357 359
358 /** 360 /**
diff --git a/application/legacy/LegacyRouter.php b/application/legacy/LegacyRouter.php
new file mode 100644
index 00000000..0449c7e1
--- /dev/null
+++ b/application/legacy/LegacyRouter.php
@@ -0,0 +1,63 @@
1<?php
2
3namespace Shaarli\Legacy;
4
5/**
6 * Class Router
7 *
8 * (only displayable pages here)
9 *
10 * @deprecated
11 */
12class LegacyRouter
13{
14 public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
15
16 public static $PAGE_LOGIN = 'login';
17
18 public static $PAGE_PICWALL = 'picwall';
19
20 public static $PAGE_TAGCLOUD = 'tag.cloud';
21
22 public static $PAGE_TAGLIST = 'tag.list';
23
24 public static $PAGE_DAILY = 'daily';
25
26 public static $PAGE_FEED_ATOM = 'feed.atom';
27
28 public static $PAGE_FEED_RSS = 'feed.rss';
29
30 public static $PAGE_TOOLS = 'tools';
31
32 public static $PAGE_CHANGEPASSWORD = 'changepasswd';
33
34 public static $PAGE_CONFIGURE = 'configure';
35
36 public static $PAGE_CHANGETAG = 'changetag';
37
38 public static $PAGE_ADDLINK = 'addlink';
39
40 public static $PAGE_EDITLINK = 'editlink';
41
42 public static $PAGE_DELETELINK = 'delete_link';
43
44 public static $PAGE_CHANGE_VISIBILITY = 'change_visibility';
45
46 public static $PAGE_PINLINK = 'pin';
47
48 public static $PAGE_EXPORT = 'export';
49
50 public static $PAGE_IMPORT = 'import';
51
52 public static $PAGE_OPENSEARCH = 'opensearch';
53
54 public static $PAGE_LINKLIST = 'linklist';
55
56 public static $PAGE_PLUGINSADMIN = 'pluginadmin';
57
58 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
59
60 public static $PAGE_THUMBS_UPDATE = 'thumbs_update';
61
62 public static $GET_TOKEN = 'token';
63}
diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php
index 3a5de79f..fe1a286f 100644
--- a/application/legacy/LegacyUpdater.php
+++ b/application/legacy/LegacyUpdater.php
@@ -7,16 +7,16 @@ use RainTPL;
7use ReflectionClass; 7use ReflectionClass;
8use ReflectionException; 8use ReflectionException;
9use ReflectionMethod; 9use ReflectionMethod;
10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\Bookmark; 10use Shaarli\Bookmark\Bookmark;
12use Shaarli\Bookmark\BookmarkArray; 11use Shaarli\Bookmark\BookmarkArray;
13use Shaarli\Bookmark\LinkDB;
14use Shaarli\Bookmark\BookmarkFilter; 12use Shaarli\Bookmark\BookmarkFilter;
15use Shaarli\Bookmark\BookmarkIO; 13use Shaarli\Bookmark\BookmarkIO;
14use Shaarli\Bookmark\LinkDB;
16use Shaarli\Config\ConfigJson; 15use Shaarli\Config\ConfigJson;
17use Shaarli\Config\ConfigManager; 16use Shaarli\Config\ConfigManager;
18use Shaarli\Config\ConfigPhp; 17use Shaarli\Config\ConfigPhp;
19use Shaarli\Exceptions\IOException; 18use Shaarli\Exceptions\IOException;
19use Shaarli\Helper\ApplicationUtils;
20use Shaarli\Thumbnailer; 20use Shaarli\Thumbnailer;
21use Shaarli\Updater\Exception\UpdaterException; 21use Shaarli\Updater\Exception\UpdaterException;
22 22
@@ -534,7 +534,8 @@ class LegacyUpdater
534 534
535 if ($thumbnailsEnabled) { 535 if ($thumbnailsEnabled) {
536 $this->session['warnings'][] = t( 536 $this->session['warnings'][] = t(
537 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.' 537 t('You have enabled or changed thumbnails mode.') .
538 '<a href="./admin/thumbnails">' . t('Please synchronize them.') . '</a>'
538 ); 539 );
539 } 540 }
540 541
diff --git a/application/legacy/UnknowLegacyRouteException.php b/application/legacy/UnknowLegacyRouteException.php
new file mode 100644
index 00000000..ae1518ad
--- /dev/null
+++ b/application/legacy/UnknowLegacyRouteException.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Legacy;
6
7class UnknowLegacyRouteException extends \Exception
8{
9}
diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php
index d64eef7f..b83f16f8 100644
--- a/application/netscape/NetscapeBookmarkUtils.php
+++ b/application/netscape/NetscapeBookmarkUtils.php
@@ -6,6 +6,7 @@ use DateTime;
6use DateTimeZone; 6use DateTimeZone;
7use Exception; 7use Exception;
8use Katzgrau\KLogger\Logger; 8use Katzgrau\KLogger\Logger;
9use Psr\Http\Message\UploadedFileInterface;
9use Psr\Log\LogLevel; 10use Psr\Log\LogLevel;
10use Shaarli\Bookmark\Bookmark; 11use Shaarli\Bookmark\Bookmark;
11use Shaarli\Bookmark\BookmarkServiceInterface; 12use Shaarli\Bookmark\BookmarkServiceInterface;
@@ -16,10 +17,24 @@ use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser;
16 17
17/** 18/**
18 * Utilities to import and export bookmarks using the Netscape format 19 * Utilities to import and export bookmarks using the Netscape format
19 * TODO: Not static, use a container.
20 */ 20 */
21class NetscapeBookmarkUtils 21class NetscapeBookmarkUtils
22{ 22{
23 /** @var BookmarkServiceInterface */
24 protected $bookmarkService;
25
26 /** @var ConfigManager */
27 protected $conf;
28
29 /** @var History */
30 protected $history;
31
32 public function __construct(BookmarkServiceInterface $bookmarkService, ConfigManager $conf, History $history)
33 {
34 $this->bookmarkService = $bookmarkService;
35 $this->conf = $conf;
36 $this->history = $history;
37 }
23 38
24 /** 39 /**
25 * Filters bookmarks and adds Netscape-formatted fields 40 * Filters bookmarks and adds Netscape-formatted fields
@@ -28,18 +43,16 @@ class NetscapeBookmarkUtils
28 * - timestamp link addition date, using the Unix epoch format 43 * - timestamp link addition date, using the Unix epoch format
29 * - taglist comma-separated tag list 44 * - taglist comma-separated tag list
30 * 45 *
31 * @param BookmarkServiceInterface $bookmarkService Link datastore
32 * @param BookmarkFormatter $formatter instance 46 * @param BookmarkFormatter $formatter instance
33 * @param string $selection Which bookmarks to export: (all|private|public) 47 * @param string $selection Which bookmarks to export: (all|private|public)
34 * @param bool $prependNoteUrl Prepend note permalinks with the server's URL 48 * @param bool $prependNoteUrl Prepend note permalinks with the server's URL
35 * @param string $indexUrl Absolute URL of the Shaarli index page 49 * @param string $indexUrl Absolute URL of the Shaarli index page
36 * 50 *
37 * @return array The bookmarks to be exported, with additional fields 51 * @return array The bookmarks to be exported, with additional fields
38 *@throws Exception Invalid export selection
39 * 52 *
53 * @throws Exception Invalid export selection
40 */ 54 */
41 public static function filterAndFormat( 55 public function filterAndFormat(
42 $bookmarkService,
43 $formatter, 56 $formatter,
44 $selection, 57 $selection,
45 $prependNoteUrl, 58 $prependNoteUrl,
@@ -51,11 +64,11 @@ class NetscapeBookmarkUtils
51 } 64 }
52 65
53 $bookmarkLinks = array(); 66 $bookmarkLinks = array();
54 foreach ($bookmarkService->search([], $selection) as $bookmark) { 67 foreach ($this->bookmarkService->search([], $selection) as $bookmark) {
55 $link = $formatter->format($bookmark); 68 $link = $formatter->format($bookmark);
56 $link['taglist'] = implode(',', $bookmark->getTags()); 69 $link['taglist'] = implode(',', $bookmark->getTags());
57 if ($bookmark->isNote() && $prependNoteUrl) { 70 if ($bookmark->isNote() && $prependNoteUrl) {
58 $link['url'] = $indexUrl . $link['url']; 71 $link['url'] = rtrim($indexUrl, '/') . '/' . ltrim($link['url'], '/');
59 } 72 }
60 73
61 $bookmarkLinks[] = $link; 74 $bookmarkLinks[] = $link;
@@ -65,60 +78,22 @@ class NetscapeBookmarkUtils
65 } 78 }
66 79
67 /** 80 /**
68 * Generates an import status summary
69 *
70 * @param string $filename name of the file to import
71 * @param int $filesize size of the file to import
72 * @param int $importCount how many bookmarks were imported
73 * @param int $overwriteCount how many bookmarks were overwritten
74 * @param int $skipCount how many bookmarks were skipped
75 * @param int $duration how many seconds did the import take
76 *
77 * @return string Summary of the bookmark import status
78 */
79 private static function importStatus(
80 $filename,
81 $filesize,
82 $importCount = 0,
83 $overwriteCount = 0,
84 $skipCount = 0,
85 $duration = 0
86 ) {
87 $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
88 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
89 $status .= t('has an unknown file format. Nothing was imported.');
90 } else {
91 $status .= vsprintf(
92 t(
93 'was successfully processed in %d seconds: '
94 . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
95 ),
96 [$duration, $importCount, $overwriteCount, $skipCount]
97 );
98 }
99 return $status;
100 }
101
102 /**
103 * Imports Web bookmarks from an uploaded Netscape bookmark dump 81 * Imports Web bookmarks from an uploaded Netscape bookmark dump
104 * 82 *
105 * @param array $post Server $_POST parameters 83 * @param array $post Server $_POST parameters
106 * @param array $files Server $_FILES parameters 84 * @param UploadedFileInterface $file File in PSR-7 object format
107 * @param BookmarkServiceInterface $bookmarkService Loaded LinkDB instance
108 * @param ConfigManager $conf instance
109 * @param History $history History instance
110 * 85 *
111 * @return string Summary of the bookmark import status 86 * @return string Summary of the bookmark import status
112 */ 87 */
113 public static function import($post, $files, $bookmarkService, $conf, $history) 88 public function import($post, UploadedFileInterface $file)
114 { 89 {
115 $start = time(); 90 $start = time();
116 $filename = $files['filetoupload']['name']; 91 $filename = $file->getClientFilename();
117 $filesize = $files['filetoupload']['size']; 92 $filesize = $file->getSize();
118 $data = file_get_contents($files['filetoupload']['tmp_name']); 93 $data = (string) $file->getStream();
119 94
120 if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) { 95 if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) {
121 return self::importStatus($filename, $filesize); 96 return $this->importStatus($filename, $filesize);
122 } 97 }
123 98
124 // Overwrite existing bookmarks? 99 // Overwrite existing bookmarks?
@@ -141,11 +116,11 @@ class NetscapeBookmarkUtils
141 true, // nested tag support 116 true, // nested tag support
142 $defaultTags, // additional user-specified tags 117 $defaultTags, // additional user-specified tags
143 strval(1 - $defaultPrivacy), // defaultPub = 1 - defaultPrivacy 118 strval(1 - $defaultPrivacy), // defaultPub = 1 - defaultPrivacy
144 $conf->get('resource.data_dir') // log path, will be overridden 119 $this->conf->get('resource.data_dir') // log path, will be overridden
145 ); 120 );
146 $logger = new Logger( 121 $logger = new Logger(
147 $conf->get('resource.data_dir'), 122 $this->conf->get('resource.data_dir'),
148 !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG, 123 !$this->conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
149 [ 124 [
150 'prefix' => 'import.', 125 'prefix' => 'import.',
151 'extension' => 'log', 126 'extension' => 'log',
@@ -171,7 +146,7 @@ class NetscapeBookmarkUtils
171 $private = 0; 146 $private = 0;
172 } 147 }
173 148
174 $link = $bookmarkService->findByUrl($bkm['uri']); 149 $link = $this->bookmarkService->findByUrl($bkm['uri']);
175 $existingLink = $link !== null; 150 $existingLink = $link !== null;
176 if (! $existingLink) { 151 if (! $existingLink) {
177 $link = new Bookmark(); 152 $link = new Bookmark();
@@ -193,20 +168,21 @@ class NetscapeBookmarkUtils
193 } 168 }
194 169
195 $link->setTitle($bkm['title']); 170 $link->setTitle($bkm['title']);
196 $link->setUrl($bkm['uri'], $conf->get('security.allowed_protocols')); 171 $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
197 $link->setDescription($bkm['note']); 172 $link->setDescription($bkm['note']);
198 $link->setPrivate($private); 173 $link->setPrivate($private);
199 $link->setTagsString($bkm['tags']); 174 $link->setTagsString($bkm['tags']);
200 175
201 $bookmarkService->addOrSet($link, false); 176 $this->bookmarkService->addOrSet($link, false);
202 $importCount++; 177 $importCount++;
203 } 178 }
204 179
205 $bookmarkService->save(); 180 $this->bookmarkService->save();
206 $history->importLinks(); 181 $this->history->importLinks();
207 182
208 $duration = time() - $start; 183 $duration = time() - $start;
209 return self::importStatus( 184
185 return $this->importStatus(
210 $filename, 186 $filename,
211 $filesize, 187 $filesize,
212 $importCount, 188 $importCount,
@@ -215,4 +191,39 @@ class NetscapeBookmarkUtils
215 $duration 191 $duration
216 ); 192 );
217 } 193 }
194
195 /**
196 * Generates an import status summary
197 *
198 * @param string $filename name of the file to import
199 * @param int $filesize size of the file to import
200 * @param int $importCount how many bookmarks were imported
201 * @param int $overwriteCount how many bookmarks were overwritten
202 * @param int $skipCount how many bookmarks were skipped
203 * @param int $duration how many seconds did the import take
204 *
205 * @return string Summary of the bookmark import status
206 */
207 protected function importStatus(
208 $filename,
209 $filesize,
210 $importCount = 0,
211 $overwriteCount = 0,
212 $skipCount = 0,
213 $duration = 0
214 ) {
215 $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
216 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
217 $status .= t('has an unknown file format. Nothing was imported.');
218 } else {
219 $status .= vsprintf(
220 t(
221 'was successfully processed in %d seconds: '
222 . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
223 ),
224 [$duration, $importCount, $overwriteCount, $skipCount]
225 );
226 }
227 return $status;
228 }
218} 229}
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php
index f7b24a8e..da66dea3 100644
--- a/application/plugin/PluginManager.php
+++ b/application/plugin/PluginManager.php
@@ -16,7 +16,7 @@ class PluginManager
16 * 16 *
17 * @var array $authorizedPlugins 17 * @var array $authorizedPlugins
18 */ 18 */
19 private $authorizedPlugins; 19 private $authorizedPlugins = [];
20 20
21 /** 21 /**
22 * List of loaded plugins. 22 * List of loaded plugins.
@@ -100,21 +100,36 @@ class PluginManager
100 */ 100 */
101 public function executeHooks($hook, &$data, $params = array()) 101 public function executeHooks($hook, &$data, $params = array())
102 { 102 {
103 if (!empty($params['target'])) { 103 $metadataParameters = [
104 $data['_PAGE_'] = $params['target']; 104 'target' => '_PAGE_',
105 } 105 'loggedin' => '_LOGGEDIN_',
106 106 'basePath' => '_BASE_PATH_',
107 if (isset($params['loggedin'])) { 107 'rootPath' => '_ROOT_PATH_',
108 $data['_LOGGEDIN_'] = $params['loggedin']; 108 'bookmarkService' => '_BOOKMARK_SERVICE_',
109 ];
110
111 foreach ($metadataParameters as $parameter => $metaKey) {
112 if (array_key_exists($parameter, $params)) {
113 $data[$metaKey] = $params[$parameter];
114 }
109 } 115 }
110 116
111 foreach ($this->loadedPlugins as $plugin) { 117 foreach ($this->loadedPlugins as $plugin) {
112 $hookFunction = $this->buildHookName($hook, $plugin); 118 $hookFunction = $this->buildHookName($hook, $plugin);
113 119
114 if (function_exists($hookFunction)) { 120 if (function_exists($hookFunction)) {
115 $data = call_user_func($hookFunction, $data, $this->conf); 121 try {
122 $data = call_user_func($hookFunction, $data, $this->conf);
123 } catch (\Throwable $e) {
124 $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
125 $this->errors = array_unique(array_merge($this->errors, [$error]));
126 }
116 } 127 }
117 } 128 }
129
130 foreach ($metadataParameters as $metaKey) {
131 unset($data[$metaKey]);
132 }
118 } 133 }
119 134
120 /** 135 /**
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index f4fefda8..c2fae705 100644
--- a/application/render/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -3,10 +3,12 @@
3namespace Shaarli\Render; 3namespace Shaarli\Render;
4 4
5use Exception; 5use Exception;
6use Psr\Log\LoggerInterface;
6use RainTPL; 7use RainTPL;
7use Shaarli\ApplicationUtils;
8use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
10use Shaarli\Helper\ApplicationUtils;
11use Shaarli\Security\SessionManager;
10use Shaarli\Thumbnailer; 12use Shaarli\Thumbnailer;
11 13
12/** 14/**
@@ -33,6 +35,9 @@ class PageBuilder
33 */ 35 */
34 protected $session; 36 protected $session;
35 37
38 /** @var LoggerInterface */
39 protected $logger;
40
36 /** 41 /**
37 * @var BookmarkServiceInterface $bookmarkService instance. 42 * @var BookmarkServiceInterface $bookmarkService instance.
38 */ 43 */
@@ -52,23 +57,40 @@ class PageBuilder
52 * PageBuilder constructor. 57 * PageBuilder constructor.
53 * $tpl is initialized at false for lazy loading. 58 * $tpl is initialized at false for lazy loading.
54 * 59 *
55 * @param ConfigManager $conf Configuration Manager instance (reference). 60 * @param ConfigManager $conf Configuration Manager instance (reference).
56 * @param array $session $_SESSION array 61 * @param array $session $_SESSION array
57 * @param BookmarkServiceInterface $linkDB instance. 62 * @param LoggerInterface $logger
58 * @param string $token Session token 63 * @param null $linkDB instance.
59 * @param bool $isLoggedIn 64 * @param null $token Session token
65 * @param bool $isLoggedIn
60 */ 66 */
61 public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false) 67 public function __construct(
62 { 68 ConfigManager &$conf,
69 array $session,
70 LoggerInterface $logger,
71 $linkDB = null,
72 $token = null,
73 $isLoggedIn = false
74 ) {
63 $this->tpl = false; 75 $this->tpl = false;
64 $this->conf = $conf; 76 $this->conf = $conf;
65 $this->session = $session; 77 $this->session = $session;
78 $this->logger = $logger;
66 $this->bookmarkService = $linkDB; 79 $this->bookmarkService = $linkDB;
67 $this->token = $token; 80 $this->token = $token;
68 $this->isLoggedIn = $isLoggedIn; 81 $this->isLoggedIn = $isLoggedIn;
69 } 82 }
70 83
71 /** 84 /**
85 * Reset current state of template rendering.
86 * Mostly useful for error handling. We remove everything, and display the error template.
87 */
88 public function reset(): void
89 {
90 $this->tpl = false;
91 }
92
93 /**
72 * Initialize all default tpl tags. 94 * Initialize all default tpl tags.
73 */ 95 */
74 private function initialize() 96 private function initialize()
@@ -87,7 +109,7 @@ class PageBuilder
87 $this->tpl->assign('newVersion', escape($version)); 109 $this->tpl->assign('newVersion', escape($version));
88 $this->tpl->assign('versionError', ''); 110 $this->tpl->assign('versionError', '');
89 } catch (Exception $exc) { 111 } catch (Exception $exc) {
90 logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); 112 $this->logger->error(format_log('Error: ' . $exc->getMessage(), client_ip_id($_SERVER)));
91 $this->tpl->assign('newVersion', ''); 113 $this->tpl->assign('newVersion', '');
92 $this->tpl->assign('versionError', escape($exc->getMessage())); 114 $this->tpl->assign('versionError', escape($exc->getMessage()));
93 } 115 }
@@ -126,7 +148,7 @@ class PageBuilder
126 $this->tpl->assign('language', $this->conf->get('translation.language')); 148 $this->tpl->assign('language', $this->conf->get('translation.language'));
127 149
128 if ($this->bookmarkService !== null) { 150 if ($this->bookmarkService !== null) {
129 $this->tpl->assign('tags', $this->bookmarkService->bookmarksCountPerTag()); 151 $this->tpl->assign('tags', escape($this->bookmarkService->bookmarksCountPerTag()));
130 } 152 }
131 153
132 $this->tpl->assign( 154 $this->tpl->assign(
@@ -136,18 +158,45 @@ class PageBuilder
136 $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width')); 158 $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
137 $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height')); 159 $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
138 160
139 if (!empty($_SESSION['warnings'])) {
140 $this->tpl->assign('global_warnings', $_SESSION['warnings']);
141 unset($_SESSION['warnings']);
142 }
143
144 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default')); 161 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
145 162
163 $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20);
164
146 // To be removed with a proper theme configuration. 165 // To be removed with a proper theme configuration.
147 $this->tpl->assign('conf', $this->conf); 166 $this->tpl->assign('conf', $this->conf);
148 } 167 }
149 168
150 /** 169 /**
170 * Affect variable after controller processing.
171 * Used for alert messages.
172 */
173 protected function finalize(string $basePath): void
174 {
175 // TODO: use the SessionManager
176 $messageKeys = [
177 SessionManager::KEY_SUCCESS_MESSAGES,
178 SessionManager::KEY_WARNING_MESSAGES,
179 SessionManager::KEY_ERROR_MESSAGES
180 ];
181 foreach ($messageKeys as $messageKey) {
182 if (!empty($_SESSION[$messageKey])) {
183 $this->tpl->assign('global_' . $messageKey, $_SESSION[$messageKey]);
184 unset($_SESSION[$messageKey]);
185 }
186 }
187
188 $rootPath = preg_replace('#/index\.php$#', '', $basePath);
189 $this->assign('base_path', $basePath);
190 $this->assign('root_path', $rootPath);
191 $this->assign(
192 'asset_path',
193 $rootPath . '/' .
194 rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' .
195 $this->conf->get('resource.theme', 'default')
196 );
197 }
198
199 /**
151 * The following assign() method is basically the same as RainTPL (except lazy loading) 200 * The following assign() method is basically the same as RainTPL (except lazy loading)
152 * 201 *
153 * @param string $placeholder Template placeholder. 202 * @param string $placeholder Template placeholder.
@@ -185,21 +234,6 @@ class PageBuilder
185 } 234 }
186 235
187 /** 236 /**
188 * Render a specific page (using a template file).
189 * e.g. $pb->renderPage('picwall');
190 *
191 * @param string $page Template filename (without extension).
192 */
193 public function renderPage($page)
194 {
195 if ($this->tpl === false) {
196 $this->initialize();
197 }
198
199 $this->tpl->draw($page);
200 }
201
202 /**
203 * Render a specific page as string (using a template file). 237 * Render a specific page as string (using a template file).
204 * e.g. $pb->render('picwall'); 238 * e.g. $pb->render('picwall');
205 * 239 *
@@ -207,28 +241,14 @@ class PageBuilder
207 * 241 *
208 * @return string Processed template content 242 * @return string Processed template content
209 */ 243 */
210 public function render(string $page): string 244 public function render(string $page, string $basePath): string
211 { 245 {
212 if ($this->tpl === false) { 246 if ($this->tpl === false) {
213 $this->initialize(); 247 $this->initialize();
214 } 248 }
215 249
216 return $this->tpl->draw($page, true); 250 $this->finalize($basePath);
217 }
218 251
219 /** 252 return $this->tpl->draw($page, true);
220 * Render a 404 page (uses the template : tpl/404.tpl)
221 * usage: $PAGE->render404('The link was deleted')
222 *
223 * @param string $message A message to display what is not found
224 */
225 public function render404($message = '')
226 {
227 if (empty($message)) {
228 $message = t('The page you are trying to reach does not exist or has been deleted.');
229 }
230 header($_SERVER['SERVER_PROTOCOL'] . ' ' . t('404 Not Found'));
231 $this->tpl->assign('error_message', $message);
232 $this->renderPage('404');
233 } 253 }
234} 254}
diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php
new file mode 100644
index 00000000..97805c35
--- /dev/null
+++ b/application/render/PageCacheManager.php
@@ -0,0 +1,60 @@
1<?php
2
3namespace Shaarli\Render;
4
5use Shaarli\Feed\CachedPage;
6
7/**
8 * Cache utilities
9 */
10class PageCacheManager
11{
12 /** @var string Cache directory */
13 protected $pageCacheDir;
14
15 /** @var bool */
16 protected $isLoggedIn;
17
18 public function __construct(string $pageCacheDir, bool $isLoggedIn)
19 {
20 $this->pageCacheDir = $pageCacheDir;
21 $this->isLoggedIn = $isLoggedIn;
22 }
23
24 /**
25 * Purges all cached pages
26 *
27 * @return string|null an error string if the directory is missing
28 */
29 public function purgeCachedPages(): ?string
30 {
31 if (!is_dir($this->pageCacheDir)) {
32 $error = sprintf(t('Cannot purge %s: no directory'), $this->pageCacheDir);
33 error_log($error);
34
35 return $error;
36 }
37
38 array_map('unlink', glob($this->pageCacheDir . '/*.cache'));
39
40 return null;
41 }
42
43 /**
44 * Invalidates caches when the database is changed or the user logs out.
45 */
46 public function invalidateCaches(): void
47 {
48 // Purge page cache shared by sessions.
49 $this->purgeCachedPages();
50 }
51
52 public function getCachePage(string $pageUrl): CachedPage
53 {
54 return new CachedPage(
55 $this->pageCacheDir,
56 $pageUrl,
57 false === $this->isLoggedIn
58 );
59 }
60}
diff --git a/application/render/TemplatePage.php b/application/render/TemplatePage.php
new file mode 100644
index 00000000..03b424f3
--- /dev/null
+++ b/application/render/TemplatePage.php
@@ -0,0 +1,34 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Render;
6
7interface TemplatePage
8{
9 public const ERROR_404 = '404';
10 public const ADDLINK = 'addlink';
11 public const CHANGE_PASSWORD = 'changepassword';
12 public const CHANGE_TAG = 'changetag';
13 public const CONFIGURE = 'configure';
14 public const DAILY = 'daily';
15 public const DAILY_RSS = 'dailyrss';
16 public const EDIT_LINK = 'editlink';
17 public const EDIT_LINK_BATCH = 'editlink.batch';
18 public const ERROR = 'error';
19 public const EXPORT = 'export';
20 public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks';
21 public const FEED_ATOM = 'feed.atom';
22 public const FEED_RSS = 'feed.rss';
23 public const IMPORT = 'import';
24 public const INSTALL = 'install';
25 public const LINKLIST = 'linklist';
26 public const LOGIN = 'loginform';
27 public const OPEN_SEARCH = 'opensearch';
28 public const PICTURE_WALL = 'picwall';
29 public const PLUGINS_ADMIN = 'pluginsadmin';
30 public const TAG_CLOUD = 'tag.cloud';
31 public const TAG_LIST = 'tag.list';
32 public const THUMBNAILS = 'thumbnails';
33 public const TOOLS = 'tools';
34}
diff --git a/application/security/BanManager.php b/application/security/BanManager.php
index 68190c54..288cbde0 100644
--- a/application/security/BanManager.php
+++ b/application/security/BanManager.php
@@ -3,7 +3,8 @@
3 3
4namespace Shaarli\Security; 4namespace Shaarli\Security;
5 5
6use Shaarli\FileUtils; 6use Psr\Log\LoggerInterface;
7use Shaarli\Helper\FileUtils;
7 8
8/** 9/**
9 * Class BanManager 10 * Class BanManager
@@ -28,8 +29,8 @@ class BanManager
28 /** @var string Path to the file containing IP bans and failures */ 29 /** @var string Path to the file containing IP bans and failures */
29 protected $banFile; 30 protected $banFile;
30 31
31 /** @var string Path to the log file, used to log bans */ 32 /** @var LoggerInterface Path to the log file, used to log bans */
32 protected $logFile; 33 protected $logger;
33 34
34 /** @var array List of IP with their associated number of failed attempts */ 35 /** @var array List of IP with their associated number of failed attempts */
35 protected $failures = []; 36 protected $failures = [];
@@ -40,18 +41,19 @@ class BanManager
40 /** 41 /**
41 * BanManager constructor. 42 * BanManager constructor.
42 * 43 *
43 * @param array $trustedProxies List of allowed proxies IP 44 * @param array $trustedProxies List of allowed proxies IP
44 * @param int $nbAttempts Number of allowed failed attempt before the ban 45 * @param int $nbAttempts Number of allowed failed attempt before the ban
45 * @param int $banDuration Ban duration in seconds 46 * @param int $banDuration Ban duration in seconds
46 * @param string $banFile Path to the file containing IP bans and failures 47 * @param string $banFile Path to the file containing IP bans and failures
47 * @param string $logFile Path to the log file, used to log bans 48 * @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory
48 */ 49 */
49 public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) { 50 public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger) {
50 $this->trustedProxies = $trustedProxies; 51 $this->trustedProxies = $trustedProxies;
51 $this->nbAttempts = $nbAttempts; 52 $this->nbAttempts = $nbAttempts;
52 $this->banDuration = $banDuration; 53 $this->banDuration = $banDuration;
53 $this->banFile = $banFile; 54 $this->banFile = $banFile;
54 $this->logFile = $logFile; 55 $this->logger = $logger;
56
55 $this->readBanFile(); 57 $this->readBanFile();
56 } 58 }
57 59
@@ -78,11 +80,7 @@ class BanManager
78 80
79 if ($this->failures[$ip] >= $this->nbAttempts) { 81 if ($this->failures[$ip] >= $this->nbAttempts) {
80 $this->bans[$ip] = time() + $this->banDuration; 82 $this->bans[$ip] = time() + $this->banDuration;
81 logm( 83 $this->logger->info(format_log('IP address banned from login: '. $ip, $ip));
82 $this->logFile,
83 $server['REMOTE_ADDR'],
84 'IP address banned from login: '. $ip
85 );
86 } 84 }
87 $this->writeBanFile(); 85 $this->writeBanFile();
88 } 86 }
@@ -138,7 +136,7 @@ class BanManager
138 unset($this->failures[$ip]); 136 unset($this->failures[$ip]);
139 } 137 }
140 unset($this->bans[$ip]); 138 unset($this->bans[$ip]);
141 logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip); 139 $this->logger->info(format_log('Ban lifted for: '. $ip, $ip));
142 140
143 $this->writeBanFile(); 141 $this->writeBanFile();
144 return false; 142 return false;
diff --git a/application/security/CookieManager.php b/application/security/CookieManager.php
new file mode 100644
index 00000000..cde4746e
--- /dev/null
+++ b/application/security/CookieManager.php
@@ -0,0 +1,33 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Security;
6
7class CookieManager
8{
9 /** @var string Name of the cookie set after logging in **/
10 public const STAY_SIGNED_IN = 'shaarli_staySignedIn';
11
12 /** @var mixed $_COOKIE set by reference */
13 protected $cookies;
14
15 public function __construct(array &$cookies)
16 {
17 $this->cookies = $cookies;
18 }
19
20 public function setCookieParameter(string $key, string $value, int $expires, string $path): self
21 {
22 $this->cookies[$key] = $value;
23
24 setcookie($key, $value, $expires, $path);
25
26 return $this;
27 }
28
29 public function getCookieParameter(string $key, string $default = null): ?string
30 {
31 return $this->cookies[$key] ?? $default;
32 }
33}
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php
index 39ec9b2e..426e785e 100644
--- a/application/security/LoginManager.php
+++ b/application/security/LoginManager.php
@@ -2,6 +2,7 @@
2namespace Shaarli\Security; 2namespace Shaarli\Security;
3 3
4use Exception; 4use Exception;
5use Psr\Log\LoggerInterface;
5use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
6 7
7/** 8/**
@@ -9,9 +10,6 @@ use Shaarli\Config\ConfigManager;
9 */ 10 */
10class LoginManager 11class LoginManager
11{ 12{
12 /** @var string Name of the cookie set after logging in **/
13 public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
14
15 /** @var array A reference to the $_GLOBALS array */ 13 /** @var array A reference to the $_GLOBALS array */
16 protected $globals = []; 14 protected $globals = [];
17 15
@@ -32,24 +30,32 @@ class LoginManager
32 30
33 /** @var string User sign-in token depending on remote IP and credentials */ 31 /** @var string User sign-in token depending on remote IP and credentials */
34 protected $staySignedInToken = ''; 32 protected $staySignedInToken = '';
33 /** @var CookieManager */
34 protected $cookieManager;
35 /** @var LoggerInterface */
36 protected $logger;
35 37
36 /** 38 /**
37 * Constructor 39 * Constructor
38 * 40 *
39 * @param ConfigManager $configManager Configuration Manager instance 41 * @param ConfigManager $configManager Configuration Manager instance
40 * @param SessionManager $sessionManager SessionManager instance 42 * @param SessionManager $sessionManager SessionManager instance
43 * @param CookieManager $cookieManager CookieManager instance
44 * @param BanManager $banManager
45 * @param LoggerInterface $logger Used to log login attempts
41 */ 46 */
42 public function __construct($configManager, $sessionManager) 47 public function __construct(
43 { 48 ConfigManager $configManager,
49 SessionManager $sessionManager,
50 CookieManager $cookieManager,
51 BanManager $banManager,
52 LoggerInterface $logger
53 ) {
44 $this->configManager = $configManager; 54 $this->configManager = $configManager;
45 $this->sessionManager = $sessionManager; 55 $this->sessionManager = $sessionManager;
46 $this->banManager = new BanManager( 56 $this->cookieManager = $cookieManager;
47 $this->configManager->get('security.trusted_proxies', []), 57 $this->banManager = $banManager;
48 $this->configManager->get('security.ban_after'), 58 $this->logger = $logger;
49 $this->configManager->get('security.ban_duration'),
50 $this->configManager->get('resource.ban_file', 'data/ipbans.php'),
51 $this->configManager->get('resource.log')
52 );
53 59
54 if ($this->configManager->get('security.open_shaarli') === true) { 60 if ($this->configManager->get('security.open_shaarli') === true) {
55 $this->openShaarli = true; 61 $this->openShaarli = true;
@@ -86,10 +92,9 @@ class LoginManager
86 /** 92 /**
87 * Check user session state and validity (expiration) 93 * Check user session state and validity (expiration)
88 * 94 *
89 * @param array $cookie The $_COOKIE array
90 * @param string $clientIpId Client IP address identifier 95 * @param string $clientIpId Client IP address identifier
91 */ 96 */
92 public function checkLoginState($cookie, $clientIpId) 97 public function checkLoginState($clientIpId)
93 { 98 {
94 if (! $this->configManager->exists('credentials.login')) { 99 if (! $this->configManager->exists('credentials.login')) {
95 // Shaarli is not configured yet 100 // Shaarli is not configured yet
@@ -97,9 +102,7 @@ class LoginManager
97 return; 102 return;
98 } 103 }
99 104
100 if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) 105 if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) {
101 && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
102 ) {
103 // The user client has a valid stay-signed-in cookie 106 // The user client has a valid stay-signed-in cookie
104 // Session information is updated with the current client information 107 // Session information is updated with the current client information
105 $this->sessionManager->storeLoginInfo($clientIpId); 108 $this->sessionManager->storeLoginInfo($clientIpId);
@@ -120,7 +123,7 @@ class LoginManager
120 * 123 *
121 * @return true when the user is logged in, false otherwise 124 * @return true when the user is logged in, false otherwise
122 */ 125 */
123 public function isLoggedIn() 126 public function isLoggedIn(): bool
124 { 127 {
125 if ($this->openShaarli) { 128 if ($this->openShaarli) {
126 return true; 129 return true;
@@ -131,48 +134,34 @@ class LoginManager
131 /** 134 /**
132 * Check user credentials are valid 135 * Check user credentials are valid
133 * 136 *
134 * @param string $remoteIp Remote client IP address
135 * @param string $clientIpId Client IP address identifier 137 * @param string $clientIpId Client IP address identifier
136 * @param string $login Username 138 * @param string $login Username
137 * @param string $password Password 139 * @param string $password Password
138 * 140 *
139 * @return bool true if the provided credentials are valid, false otherwise 141 * @return bool true if the provided credentials are valid, false otherwise
140 */ 142 */
141 public function checkCredentials($remoteIp, $clientIpId, $login, $password) 143 public function checkCredentials($clientIpId, $login, $password)
142 { 144 {
143 // Check login matches config
144 if ($login !== $this->configManager->get('credentials.login')) {
145 return false;
146 }
147
148 // Check credentials 145 // Check credentials
149 try { 146 try {
150 $useLdapLogin = !empty($this->configManager->get('ldap.host')); 147 $useLdapLogin = !empty($this->configManager->get('ldap.host'));
151 if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) 148 if ($login === $this->configManager->get('credentials.login')
152 || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) 149 && (
150 (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password))
151 || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password))
152 )
153 ) { 153 ) {
154 $this->sessionManager->storeLoginInfo($clientIpId); 154 $this->sessionManager->storeLoginInfo($clientIpId);
155 logm( 155 $this->logger->info(format_log('Login successful', $clientIpId));
156 $this->configManager->get('resource.log'), 156
157 $remoteIp, 157 return true;
158 'Login successful'
159 );
160 return true;
161 } 158 }
162 } 159 } catch(Exception $exception) {
163 catch(Exception $exception) { 160 $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId));
164 logm(
165 $this->configManager->get('resource.log'),
166 $remoteIp,
167 'Exception while checking credentials: ' . $exception
168 );
169 } 161 }
170 162
171 logm( 163 $this->logger->info(format_log('Login failed for user ' . $login, $clientIpId));
172 $this->configManager->get('resource.log'), 164
173 $remoteIp,
174 'Login failed for user ' . $login
175 );
176 return false; 165 return false;
177 } 166 }
178 167
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
index 994fcbe5..96bf193c 100644
--- a/application/security/SessionManager.php
+++ b/application/security/SessionManager.php
@@ -8,6 +8,14 @@ use Shaarli\Config\ConfigManager;
8 */ 8 */
9class SessionManager 9class SessionManager
10{ 10{
11 public const KEY_LINKS_PER_PAGE = 'LINKS_PER_PAGE';
12 public const KEY_VISIBILITY = 'visibility';
13 public const KEY_UNTAGGED_ONLY = 'untaggedonly';
14
15 public const KEY_SUCCESS_MESSAGES = 'successes';
16 public const KEY_WARNING_MESSAGES = 'warnings';
17 public const KEY_ERROR_MESSAGES = 'errors';
18
11 /** @var int Session expiration timeout, in seconds */ 19 /** @var int Session expiration timeout, in seconds */
12 public static $SHORT_TIMEOUT = 3600; // 1 hour 20 public static $SHORT_TIMEOUT = 3600; // 1 hour
13 21
@@ -23,16 +31,35 @@ class SessionManager
23 /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ 31 /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
24 protected $staySignedIn = false; 32 protected $staySignedIn = false;
25 33
34 /** @var string */
35 protected $savePath;
36
26 /** 37 /**
27 * Constructor 38 * Constructor
28 * 39 *
29 * @param array $session The $_SESSION array (reference) 40 * @param array $session The $_SESSION array (reference)
30 * @param ConfigManager $conf ConfigManager instance 41 * @param ConfigManager $conf ConfigManager instance
42 * @param string $savePath Session save path returned by builtin function session_save_path()
31 */ 43 */
32 public function __construct(& $session, $conf) 44 public function __construct(&$session, $conf, string $savePath)
33 { 45 {
34 $this->session = &$session; 46 $this->session = &$session;
35 $this->conf = $conf; 47 $this->conf = $conf;
48 $this->savePath = $savePath;
49 }
50
51 /**
52 * Initialize XSRF token and links per page session variables.
53 */
54 public function initialize(): void
55 {
56 if (!isset($this->session['tokens'])) {
57 $this->session['tokens'] = [];
58 }
59
60 if (!isset($this->session['LINKS_PER_PAGE'])) {
61 $this->session['LINKS_PER_PAGE'] = $this->conf->get('general.links_per_page', 20);
62 }
36 } 63 }
37 64
38 /** 65 /**
@@ -156,7 +183,6 @@ class SessionManager
156 unset($this->session['expires_on']); 183 unset($this->session['expires_on']);
157 unset($this->session['username']); 184 unset($this->session['username']);
158 unset($this->session['visibility']); 185 unset($this->session['visibility']);
159 unset($this->session['untaggedonly']);
160 } 186 }
161 } 187 }
162 188
@@ -202,4 +228,81 @@ class SessionManager
202 { 228 {
203 return $this->session; 229 return $this->session;
204 } 230 }
231
232 /**
233 * @param mixed $default value which will be returned if the $key is undefined
234 *
235 * @return mixed Content stored in session
236 */
237 public function getSessionParameter(string $key, $default = null)
238 {
239 return $this->session[$key] ?? $default;
240 }
241
242 /**
243 * Store a variable in user session.
244 *
245 * @param string $key Session key
246 * @param mixed $value Session value to store
247 *
248 * @return $this
249 */
250 public function setSessionParameter(string $key, $value): self
251 {
252 $this->session[$key] = $value;
253
254 return $this;
255 }
256
257 /**
258 * Store a variable in user session.
259 *
260 * @param string $key Session key
261 *
262 * @return $this
263 */
264 public function deleteSessionParameter(string $key): self
265 {
266 unset($this->session[$key]);
267
268 return $this;
269 }
270
271 public function getSavePath(): string
272 {
273 return $this->savePath;
274 }
275
276 /*
277 * Next public functions wrapping native PHP session API.
278 */
279
280 public function destroy(): bool
281 {
282 $this->session = [];
283
284 return session_destroy();
285 }
286
287 public function start(): bool
288 {
289 if (session_status() === PHP_SESSION_ACTIVE) {
290 $this->destroy();
291 }
292
293 return session_start();
294 }
295
296 /**
297 * Be careful, return type of session_set_cookie_params() changed between PHP 7.1 and 7.2.
298 */
299 public function cookieParameters(int $lifeTime, string $path, string $domain): void
300 {
301 session_set_cookie_params($lifeTime, $path, $domain);
302 }
303
304 public function regenerateId(bool $deleteOldSession = false): bool
305 {
306 return session_regenerate_id($deleteOldSession);
307 }
205} 308}
diff --git a/application/updater/Updater.php b/application/updater/Updater.php
index 95654d81..88a7bc7b 100644
--- a/application/updater/Updater.php
+++ b/application/updater/Updater.php
@@ -2,8 +2,8 @@
2 2
3namespace Shaarli\Updater; 3namespace Shaarli\Updater;
4 4
5use Shaarli\Config\ConfigManager;
6use Shaarli\Bookmark\BookmarkServiceInterface; 5use Shaarli\Bookmark\BookmarkServiceInterface;
6use Shaarli\Config\ConfigManager;
7use Shaarli\Updater\Exception\UpdaterException; 7use Shaarli\Updater\Exception\UpdaterException;
8 8
9/** 9/**
@@ -21,7 +21,7 @@ class Updater
21 /** 21 /**
22 * @var BookmarkServiceInterface instance. 22 * @var BookmarkServiceInterface instance.
23 */ 23 */
24 protected $linkServices; 24 protected $bookmarkService;
25 25
26 /** 26 /**
27 * @var ConfigManager $conf Configuration Manager instance. 27 * @var ConfigManager $conf Configuration Manager instance.
@@ -39,6 +39,11 @@ class Updater
39 protected $methods; 39 protected $methods;
40 40
41 /** 41 /**
42 * @var string $basePath Shaarli root directory (from HTTP Request)
43 */
44 protected $basePath = null;
45
46 /**
42 * Object constructor. 47 * Object constructor.
43 * 48 *
44 * @param array $doneUpdates Updates which are already done. 49 * @param array $doneUpdates Updates which are already done.
@@ -49,7 +54,7 @@ class Updater
49 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) 54 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
50 { 55 {
51 $this->doneUpdates = $doneUpdates; 56 $this->doneUpdates = $doneUpdates;
52 $this->linkServices = $linkDB; 57 $this->bookmarkService = $linkDB;
53 $this->conf = $conf; 58 $this->conf = $conf;
54 $this->isLoggedIn = $isLoggedIn; 59 $this->isLoggedIn = $isLoggedIn;
55 60
@@ -62,13 +67,15 @@ class Updater
62 * Run all new updates. 67 * Run all new updates.
63 * Update methods have to start with 'updateMethod' and return true (on success). 68 * Update methods have to start with 'updateMethod' and return true (on success).
64 * 69 *
70 * @param string $basePath Shaarli root directory (from HTTP Request)
71 *
65 * @return array An array containing ran updates. 72 * @return array An array containing ran updates.
66 * 73 *
67 * @throws UpdaterException If something went wrong. 74 * @throws UpdaterException If something went wrong.
68 */ 75 */
69 public function update() 76 public function update(string $basePath = null)
70 { 77 {
71 $updatesRan = array(); 78 $updatesRan = [];
72 79
73 // If the user isn't logged in, exit without updating. 80 // If the user isn't logged in, exit without updating.
74 if ($this->isLoggedIn !== true) { 81 if ($this->isLoggedIn !== true) {
@@ -111,4 +118,62 @@ class Updater
111 { 118 {
112 return $this->doneUpdates; 119 return $this->doneUpdates;
113 } 120 }
121
122 public function readUpdates(string $updatesFilepath): array
123 {
124 return UpdaterUtils::read_updates_file($updatesFilepath);
125 }
126
127 public function writeUpdates(string $updatesFilepath, array $updates): void
128 {
129 UpdaterUtils::write_updates_file($updatesFilepath, $updates);
130 }
131
132 /**
133 * With the Slim routing system, default header link should be `/subfolder/` instead of `?`.
134 * Otherwise you can not go back to the home page.
135 * Example: `/subfolder/picture-wall` -> `/subfolder/picture-wall?` instead of `/subfolder/`.
136 */
137 public function updateMethodRelativeHomeLink(): bool
138 {
139 if ('?' === trim($this->conf->get('general.header_link'))) {
140 $this->conf->set('general.header_link', $this->basePath . '/', true, true);
141 }
142
143 return true;
144 }
145
146 /**
147 * With the Slim routing system, note bookmarks URL formatted `?abcdef`
148 * should be replaced with `/shaare/abcdef`
149 */
150 public function updateMethodMigrateExistingNotesUrl(): bool
151 {
152 $updated = false;
153
154 foreach ($this->bookmarkService->search() as $bookmark) {
155 if ($bookmark->isNote()
156 && startsWith($bookmark->getUrl(), '?')
157 && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match)
158 ) {
159 $updated = true;
160 $bookmark = $bookmark->setUrl('/shaare/' . $match[1]);
161
162 $this->bookmarkService->set($bookmark, false);
163 }
164 }
165
166 if ($updated) {
167 $this->bookmarkService->save();
168 }
169
170 return true;
171 }
172
173 public function setBasePath(string $basePath): self
174 {
175 $this->basePath = $basePath;
176
177 return $this;
178 }
114} 179}
diff --git a/assets/common/js/metadata.js b/assets/common/js/metadata.js
new file mode 100644
index 00000000..d5a28a35
--- /dev/null
+++ b/assets/common/js/metadata.js
@@ -0,0 +1,107 @@
1import he from 'he';
2
3/**
4 * This script is used to retrieve bookmarks metadata asynchronously:
5 * - title, description and keywords while creating a new bookmark
6 * - thumbnails while visiting the bookmark list
7 *
8 * Note: it should only be included if the user is logged in
9 * and the setting general.enable_async_metadata is enabled.
10 */
11
12/**
13 * Removes given input loaders - used in edit link template.
14 *
15 * @param {object} loaders List of input DOM element that need to be cleared
16 */
17function clearLoaders(loaders) {
18 if (loaders != null && loaders.length > 0) {
19 [...loaders].forEach((loader) => {
20 loader.classList.remove('loading-input');
21 });
22 }
23}
24
25/**
26 * AJAX request to update the thumbnail of a bookmark with the provided ID.
27 * If a thumbnail is retrieved, it updates the divElement with the image src, and displays it.
28 *
29 * @param {string} basePath Shaarli subfolder for XHR requests
30 * @param {object} divElement Main <div> DOM element containing the thumbnail placeholder
31 * @param {int} id Bookmark ID to update
32 */
33function updateThumb(basePath, divElement, id) {
34 const xhr = new XMLHttpRequest();
35 xhr.open('PATCH', `${basePath}/admin/shaare/${id}/update-thumbnail`);
36 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
37 xhr.responseType = 'json';
38 xhr.onload = () => {
39 if (xhr.status !== 200) {
40 alert(`An error occurred. Return code: ${xhr.status}`);
41 } else {
42 const { response } = xhr;
43
44 if (response.thumbnail !== false) {
45 const imgElement = divElement.querySelector('img');
46
47 imgElement.src = response.thumbnail;
48 imgElement.dataset.src = response.thumbnail;
49 imgElement.style.opacity = '1';
50 divElement.classList.remove('hidden');
51 }
52 }
53 };
54 xhr.send();
55}
56
57(() => {
58 const basePath = document.querySelector('input[name="js_base_path"]').value;
59
60 /*
61 * METADATA FOR EDIT BOOKMARK PAGE
62 */
63 const inputTitles = document.querySelectorAll('input[name="lf_title"]');
64 if (inputTitles != null) {
65 [...inputTitles].forEach((inputTitle) => {
66 const form = inputTitle.closest('form[name="linkform"]');
67 const loaders = form.querySelectorAll('.loading-input');
68
69 if (inputTitle.value.length > 0) {
70 clearLoaders(loaders);
71 return;
72 }
73
74 const url = form.querySelector('input[name="lf_url"]').value;
75
76 const xhr = new XMLHttpRequest();
77 xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true);
78 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
79 xhr.onload = () => {
80 const result = JSON.parse(xhr.response);
81 Object.keys(result).forEach((key) => {
82 if (result[key] !== null && result[key].length) {
83 const element = form.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`);
84 if (element != null && element.value.length === 0) {
85 element.value = he.decode(result[key]);
86 }
87 }
88 });
89 clearLoaders(loaders);
90 };
91
92 xhr.send();
93 });
94 }
95
96 /*
97 * METADATA FOR THUMBNAIL RETRIEVAL
98 */
99 const thumbsToLoad = document.querySelectorAll('div[data-async-thumbnail]');
100 if (thumbsToLoad != null) {
101 [...thumbsToLoad].forEach((divElement) => {
102 const { id } = divElement.closest('[data-id]').dataset;
103
104 updateThumb(basePath, divElement, id);
105 });
106 }
107})();
diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js
new file mode 100644
index 00000000..557325ee
--- /dev/null
+++ b/assets/common/js/shaare-batch.js
@@ -0,0 +1,121 @@
1const sendBookmarkForm = (basePath, formElement) => {
2 const inputs = formElement
3 .querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="hidden"]');
4
5 const formData = new FormData();
6 [...inputs].forEach((input) => {
7 formData.append(input.getAttribute('name'), input.value);
8 });
9
10 return new Promise((resolve, reject) => {
11 const xhr = new XMLHttpRequest();
12 xhr.open('POST', `${basePath}/admin/shaare`);
13 xhr.onload = () => {
14 if (xhr.status !== 200) {
15 alert(`An error occurred. Return code: ${xhr.status}`);
16 reject();
17 } else {
18 formElement.closest('.edit-link-container').remove();
19 resolve();
20 }
21 };
22 xhr.send(formData);
23 });
24};
25
26const sendBookmarkDelete = (buttonElement, formElement) => (
27 new Promise((resolve, reject) => {
28 const xhr = new XMLHttpRequest();
29 xhr.open('GET', buttonElement.href);
30 xhr.onload = () => {
31 if (xhr.status !== 200) {
32 alert(`An error occurred. Return code: ${xhr.status}`);
33 reject();
34 } else {
35 formElement.closest('.edit-link-container').remove();
36 resolve();
37 }
38 };
39 xhr.send();
40 })
41);
42
43const redirectIfEmptyBatch = (basePath, formElements, path) => {
44 if (formElements == null || formElements.length === 0) {
45 window.location.href = `${basePath}${path}`;
46 }
47};
48
49(() => {
50 const basePath = document.querySelector('input[name="js_base_path"]').value;
51 const getForms = () => document.querySelectorAll('form[name="linkform"]');
52
53 const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]');
54 if (cancelButtons != null) {
55 [...cancelButtons].forEach((cancelButton) => {
56 cancelButton.addEventListener('click', (e) => {
57 e.preventDefault();
58 e.target.closest('form[name="linkform"]').remove();
59 redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare');
60 });
61 });
62 }
63
64 const saveButtons = document.querySelectorAll('[name="save_edit"]');
65 if (saveButtons != null) {
66 [...saveButtons].forEach((saveButton) => {
67 saveButton.addEventListener('click', (e) => {
68 e.preventDefault();
69
70 const formElement = e.target.closest('form[name="linkform"]');
71 sendBookmarkForm(basePath, formElement)
72 .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
73 });
74 });
75 }
76
77 const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]');
78 if (saveAllButtons != null) {
79 [...saveAllButtons].forEach((saveAllButton) => {
80 saveAllButton.addEventListener('click', (e) => {
81 e.preventDefault();
82
83 const forms = [...getForms()];
84 const nbForm = forms.length;
85 let current = 0;
86 const progressBar = document.querySelector('.progressbar > div');
87 const progressBarCurrent = document.querySelector('.progressbar-current');
88
89 document.querySelector('.dark-layer').style.display = 'block';
90 document.querySelector('.progressbar-max').innerHTML = nbForm;
91 progressBarCurrent.innerHTML = current;
92
93 const promises = [];
94 forms.forEach((formElement) => {
95 promises.push(sendBookmarkForm(basePath, formElement).then(() => {
96 current += 1;
97 progressBar.style.width = `${(current * 100) / nbForm}%`;
98 progressBarCurrent.innerHTML = current;
99 }));
100 });
101
102 Promise.all(promises).then(() => {
103 window.location.href = basePath || '/';
104 });
105 });
106 });
107 }
108
109 const deleteButtons = document.querySelectorAll('[name="delete_link"]');
110 if (deleteButtons != null) {
111 [...deleteButtons].forEach((deleteButton) => {
112 deleteButton.addEventListener('click', (e) => {
113 e.preventDefault();
114
115 const formElement = e.target.closest('form[name="linkform"]');
116 sendBookmarkDelete(e.target, formElement)
117 .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
118 });
119 });
120 }
121})();
diff --git a/assets/common/js/thumbnails-update.js b/assets/common/js/thumbnails-update.js
index b66ca3ae..3cd4c2a7 100644
--- a/assets/common/js/thumbnails-update.js
+++ b/assets/common/js/thumbnails-update.js
@@ -10,13 +10,14 @@
10 * It contains a recursive call to retrieve the thumb of the next link when it succeed. 10 * It contains a recursive call to retrieve the thumb of the next link when it succeed.
11 * It also update the progress bar and other visual feedback elements. 11 * It also update the progress bar and other visual feedback elements.
12 * 12 *
13 * @param {string} basePath Shaarli subfolder for XHR requests
13 * @param {array} ids List of LinkID to update 14 * @param {array} ids List of LinkID to update
14 * @param {int} i Current index in ids 15 * @param {int} i Current index in ids
15 * @param {object} elements List of DOM element to avoid retrieving them at each iteration 16 * @param {object} elements List of DOM element to avoid retrieving them at each iteration
16 */ 17 */
17function updateThumb(ids, i, elements) { 18function updateThumb(basePath, ids, i, elements) {
18 const xhr = new XMLHttpRequest(); 19 const xhr = new XMLHttpRequest();
19 xhr.open('POST', '?do=ajax_thumb_update'); 20 xhr.open('PATCH', `${basePath}/admin/shaare/${ids[i]}/update-thumbnail`);
20 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 21 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
21 xhr.responseType = 'json'; 22 xhr.responseType = 'json';
22 xhr.onload = () => { 23 xhr.onload = () => {
@@ -29,17 +30,18 @@ function updateThumb(ids, i, elements) {
29 elements.current.innerHTML = i; 30 elements.current.innerHTML = i;
30 elements.title.innerHTML = response.title; 31 elements.title.innerHTML = response.title;
31 if (response.thumbnail !== false) { 32 if (response.thumbnail !== false) {
32 elements.thumbnail.innerHTML = `<img src="${response.thumbnail}">`; 33 elements.thumbnail.innerHTML = `<img src="${basePath}/${response.thumbnail}">`;
33 } 34 }
34 if (i < ids.length) { 35 if (i < ids.length) {
35 updateThumb(ids, i, elements); 36 updateThumb(basePath, ids, i, elements);
36 } 37 }
37 } 38 }
38 }; 39 };
39 xhr.send(`id=${ids[i]}`); 40 xhr.send();
40} 41}
41 42
42(() => { 43(() => {
44 const basePath = document.querySelector('input[name="js_base_path"]').value;
43 const ids = document.getElementsByName('ids')[0].value.split(','); 45 const ids = document.getElementsByName('ids')[0].value.split(',');
44 const elements = { 46 const elements = {
45 progressBar: document.querySelector('.progressbar > div'), 47 progressBar: document.querySelector('.progressbar > div'),
@@ -47,5 +49,5 @@ function updateThumb(ids, i, elements) {
47 thumbnail: document.querySelector('.thumbnail-placeholder'), 49 thumbnail: document.querySelector('.thumbnail-placeholder'),
48 title: document.querySelector('.thumbnail-link-title'), 50 title: document.querySelector('.thumbnail-link-title'),
49 }; 51 };
50 updateThumb(ids, 0, elements); 52 updateThumb(basePath, ids, 0, elements);
51})(); 53})();
diff --git a/assets/default/js/base.js b/assets/default/js/base.js
index d5c29c69..4163577d 100644
--- a/assets/default/js/base.js
+++ b/assets/default/js/base.js
@@ -1,4 +1,5 @@
1import Awesomplete from 'awesomplete'; 1import Awesomplete from 'awesomplete';
2import he from 'he';
2 3
3/** 4/**
4 * Find a parent element according to its tag and its attributes 5 * Find a parent element according to its tag and its attributes
@@ -10,7 +11,7 @@ import Awesomplete from 'awesomplete';
10 * @returns Found element or null. 11 * @returns Found element or null.
11 */ 12 */
12function findParent(element, tagName, attributes) { 13function findParent(element, tagName, attributes) {
13 const parentMatch = key => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1; 14 const parentMatch = (key) => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1;
14 while (element) { 15 while (element) {
15 if (element.tagName.toLowerCase() === tagName) { 16 if (element.tagName.toLowerCase() === tagName) {
16 if (Object.keys(attributes).find(parentMatch)) { 17 if (Object.keys(attributes).find(parentMatch)) {
@@ -25,12 +26,18 @@ function findParent(element, tagName, attributes) {
25/** 26/**
26 * Ajax request to refresh the CSRF token. 27 * Ajax request to refresh the CSRF token.
27 */ 28 */
28function refreshToken() { 29function refreshToken(basePath, callback) {
29 const xhr = new XMLHttpRequest(); 30 const xhr = new XMLHttpRequest();
30 xhr.open('GET', '?do=token'); 31 xhr.open('GET', `${basePath}/admin/token`);
31 xhr.onload = () => { 32 xhr.onload = () => {
32 const token = document.getElementById('token'); 33 const elements = document.querySelectorAll('input[name="token"]');
33 token.setAttribute('value', xhr.responseText); 34 [...elements].forEach((element) => {
35 element.setAttribute('value', xhr.responseText);
36 });
37
38 if (callback) {
39 callback(xhr.response);
40 }
34 }; 41 };
35 xhr.send(); 42 xhr.send();
36} 43}
@@ -90,15 +97,6 @@ function updateAwesompleteList(selector, tags, instances) {
90} 97}
91 98
92/** 99/**
93 * html_entities in JS
94 *
95 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
96 */
97function htmlEntities(str) {
98 return str.replace(/[\u00A0-\u9999<>&]/gim, i => `&#${i.charCodeAt(0)};`);
99}
100
101/**
102 * Add the class 'hidden' to city options not attached to the current selected continent. 100 * Add the class 'hidden' to city options not attached to the current selected continent.
103 * 101 *
104 * @param cities List of <option> elements 102 * @param cities List of <option> elements
@@ -188,8 +186,8 @@ function removeClass(element, classname) {
188function init(description) { 186function init(description) {
189 function resize() { 187 function resize() {
190 /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */ 188 /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
191 const scrollTop = window.pageYOffset || 189 const scrollTop = window.pageYOffset
192 (document.documentElement || document.body.parentNode || document.body).scrollTop; 190 || (document.documentElement || document.body.parentNode || document.body).scrollTop;
193 191
194 description.style.height = 'auto'; 192 description.style.height = 'auto';
195 description.style.height = `${description.scrollHeight + 10}px`; 193 description.style.height = `${description.scrollHeight + 10}px`;
@@ -215,6 +213,8 @@ function init(description) {
215} 213}
216 214
217(() => { 215(() => {
216 const basePath = document.querySelector('input[name="js_base_path"]').value;
217
218 /** 218 /**
219 * Handle responsive menu. 219 * Handle responsive menu.
220 * Source: http://purecss.io/layouts/tucked-menu-vertical/ 220 * Source: http://purecss.io/layouts/tucked-menu-vertical/
@@ -294,7 +294,7 @@ function init(description) {
294 const deleteLinks = document.querySelectorAll('.confirm-delete'); 294 const deleteLinks = document.querySelectorAll('.confirm-delete');
295 [...deleteLinks].forEach((deleteLink) => { 295 [...deleteLinks].forEach((deleteLink) => {
296 deleteLink.addEventListener('click', (event) => { 296 deleteLink.addEventListener('click', (event) => {
297 if (!confirm(document.getElementById('translation-delete-link').innerHTML)) { 297 if (!confirm(document.getElementById('translation-delete-tag').innerHTML)) {
298 event.preventDefault(); 298 event.preventDefault();
299 } 299 }
300 }); 300 });
@@ -461,7 +461,7 @@ function init(description) {
461 }); 461 });
462 462
463 if (window.confirm(message)) { 463 if (window.confirm(message)) {
464 window.location = `?delete_link&lf_linkdate=${ids.join('+')}&token=${token.value}`; 464 window.location = `${basePath}/admin/shaare/delete?id=${ids.join('+')}&token=${token.value}`;
465 } 465 }
466 }); 466 });
467 } 467 }
@@ -482,8 +482,10 @@ function init(description) {
482 }); 482 });
483 }); 483 });
484 484
485 const ids = links.map(item => item.id); 485 const ids = links.map((item) => item.id);
486 window.location = `?change_visibility&token=${token.value}&newVisibility=${visibility}&ids=${ids.join('+')}`; 486 window.location = (
487 `${basePath}/admin/shaare/visibility?token=${token.value}&newVisibility=${visibility}&id=${ids.join('+')}`
488 );
487 }); 489 });
488 }); 490 });
489 } 491 }
@@ -545,8 +547,9 @@ function init(description) {
545 } 547 }
546 const refreshedToken = document.getElementById('token').value; 548 const refreshedToken = document.getElementById('token').value;
547 const fromtag = block.getAttribute('data-tag'); 549 const fromtag = block.getAttribute('data-tag');
550 const fromtagUrl = block.getAttribute('data-tag-url');
548 const xhr = new XMLHttpRequest(); 551 const xhr = new XMLHttpRequest();
549 xhr.open('POST', '?do=changetag'); 552 xhr.open('POST', `${basePath}/admin/tags`);
550 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 553 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
551 xhr.onload = () => { 554 xhr.onload = () => {
552 if (xhr.status !== 200) { 555 if (xhr.status !== 200) {
@@ -554,20 +557,28 @@ function init(description) {
554 location.reload(); 557 location.reload();
555 } else { 558 } else {
556 block.setAttribute('data-tag', totag); 559 block.setAttribute('data-tag', totag);
560 block.setAttribute('data-tag-url', encodeURIComponent(totag));
557 input.setAttribute('name', totag); 561 input.setAttribute('name', totag);
558 input.setAttribute('value', totag); 562 input.setAttribute('value', totag);
559 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none'; 563 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
560 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag); 564 block.querySelector('a.tag-link').innerHTML = he.encode(totag);
561 block.querySelector('a.tag-link').setAttribute('href', `?searchtags=${encodeURIComponent(totag)}`); 565 block
562 block.querySelector('a.rename-tag').setAttribute('href', `?do=changetag&fromtag=${encodeURIComponent(totag)}`); 566 .querySelector('a.tag-link')
567 .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
568 block
569 .querySelector('a.count')
570 .setAttribute('href', `${basePath}/add-tag/${encodeURIComponent(totag)}`);
571 block
572 .querySelector('a.rename-tag')
573 .setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
563 574
564 // Refresh awesomplete values 575 // Refresh awesomplete values
565 existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag)); 576 existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag));
566 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); 577 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
567 } 578 }
568 }; 579 };
569 xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`); 580 xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
570 refreshToken(); 581 refreshToken(basePath);
571 }); 582 });
572 }); 583 });
573 584
@@ -589,19 +600,20 @@ function init(description) {
589 event.preventDefault(); 600 event.preventDefault();
590 const block = findParent(event.target, 'div', { class: 'tag-list-item' }); 601 const block = findParent(event.target, 'div', { class: 'tag-list-item' });
591 const tag = block.getAttribute('data-tag'); 602 const tag = block.getAttribute('data-tag');
603 const tagUrl = block.getAttribute('data-tag-url');
592 const refreshedToken = document.getElementById('token').value; 604 const refreshedToken = document.getElementById('token').value;
593 605
594 if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) { 606 if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
595 const xhr = new XMLHttpRequest(); 607 const xhr = new XMLHttpRequest();
596 xhr.open('POST', '?do=changetag'); 608 xhr.open('POST', `${basePath}/admin/tags`);
597 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 609 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
598 xhr.onload = () => { 610 xhr.onload = () => {
599 block.remove(); 611 block.remove();
600 }; 612 };
601 xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`)); 613 xhr.send(`deletetag=1&fromtag=${tagUrl}&token=${refreshedToken}`);
602 refreshToken(); 614 refreshToken(basePath);
603 615
604 existingTags = existingTags.filter(tagItem => tagItem !== tag); 616 existingTags = existingTags.filter((tagItem) => tagItem !== tag);
605 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); 617 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
606 } 618 }
607 }); 619 });
@@ -611,4 +623,44 @@ function init(description) {
611 [...autocompleteFields].forEach((autocompleteField) => { 623 [...autocompleteFields].forEach((autocompleteField) => {
612 awesomepletes.push(createAwesompleteInstance(autocompleteField)); 624 awesomepletes.push(createAwesompleteInstance(autocompleteField));
613 }); 625 });
626
627 const exportForm = document.querySelector('#exportform');
628 if (exportForm != null) {
629 exportForm.addEventListener('submit', (event) => {
630 event.preventDefault();
631
632 refreshToken(basePath, () => {
633 event.target.submit();
634 });
635 });
636 }
637
638 const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block');
639 if (bulkCreationButton != null) {
640 const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => {
641 if (bulkCreationButton.classList.contains('pure-u-0')) {
642 showMoreBlockElement.classList.remove('pure-u-0');
643 formElement.classList.add('pure-u-0');
644 } else {
645 showMoreBlockElement.classList.add('pure-u-0');
646 formElement.classList.remove('pure-u-0');
647 }
648 };
649
650 const bulkCreationForm = document.querySelector('.addlink-batch-form-block');
651
652 toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
653 bulkCreationButton.querySelector('a').addEventListener('click', (e) => {
654 e.preventDefault();
655 toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
656 });
657
658 // Force to send falsy value if the checkbox is not checked.
659 const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]');
660 const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]');
661 privateButton.addEventListener('click', () => {
662 privateHiddenButton.disabled = !privateHiddenButton.disabled;
663 });
664 privateHiddenButton.disabled = privateButton.checked;
665 }
614})(); 666})();
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index 243ab1b2..a7f091e9 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -69,20 +69,22 @@ pre {
69 font-family: 'Roboto'; 69 font-family: 'Roboto';
70 font-weight: 400; 70 font-weight: 400;
71 font-style: normal; 71 font-style: normal;
72 src: local('Roboto'), 72 src:
73 local('Roboto-Regular'), 73 local('Roboto'),
74 url('../fonts/Roboto-Regular.woff2') format('woff2'), 74 local('Roboto-Regular'),
75 url('../fonts/Roboto-Regular.woff') format('woff'); 75 url('../fonts/Roboto-Regular.woff2') format('woff2'),
76 url('../fonts/Roboto-Regular.woff') format('woff');
76} 77}
77 78
78@font-face { 79@font-face {
79 font-family: 'Roboto'; 80 font-family: 'Roboto';
80 font-weight: 700; 81 font-weight: 700;
81 font-style: normal; 82 font-style: normal;
82 src: local('Roboto'), 83 src:
83 local('Roboto-Bold'), 84 local('Roboto'),
84 url('../fonts/Roboto-Bold.woff2') format('woff2'), 85 local('Roboto-Bold'),
85 url('../fonts/Roboto-Bold.woff') format('woff'); 86 url('../fonts/Roboto-Bold.woff2') format('woff2'),
87 url('../fonts/Roboto-Bold.woff') format('woff');
86} 88}
87 89
88body, 90body,
@@ -375,7 +377,7 @@ body,
375} 377}
376 378
377@media screen and (max-width: 64em) { 379@media screen and (max-width: 64em) {
378 .header-search , 380 .header-search,
379 .header-search * { 381 .header-search * {
380 visibility: hidden; 382 visibility: hidden;
381 } 383 }
@@ -490,6 +492,10 @@ body,
490 } 492 }
491} 493}
492 494
495.header-alert-message {
496 text-align: center;
497}
498
493// CONTENT - GENERAL 499// CONTENT - GENERAL
494.container { 500.container {
495 position: relative; 501 position: relative;
@@ -550,7 +556,6 @@ body,
550 color: $dark-grey; 556 color: $dark-grey;
551 font-size: .9em; 557 font-size: .9em;
552 558
553
554 a { 559 a {
555 display: inline-block; 560 display: inline-block;
556 margin: 3px 0; 561 margin: 3px 0;
@@ -612,6 +617,11 @@ body,
612 padding: 5px; 617 padding: 5px;
613 text-decoration: none; 618 text-decoration: none;
614 color: $dark-grey; 619 color: $dark-grey;
620
621 &.selected {
622 background: var(--main-color);
623 color: $white;
624 }
615 } 625 }
616 626
617 input { 627 input {
@@ -661,6 +671,10 @@ body,
661 content: ''; 671 content: '';
662 } 672 }
663 } 673 }
674
675 .search-highlight {
676 background-color: yellow;
677 }
664} 678}
665 679
666.linklist-item-buttons { 680.linklist-item-buttons {
@@ -1009,6 +1023,10 @@ body,
1009 &.button-red { 1023 &.button-red {
1010 background: $red; 1024 background: $red;
1011 } 1025 }
1026
1027 &.button-grey {
1028 background: $light-grey;
1029 }
1012 } 1030 }
1013 1031
1014 .submit-buttons { 1032 .submit-buttons {
@@ -1033,7 +1051,7 @@ body,
1033 } 1051 }
1034 1052
1035 table { 1053 table {
1036 margin: auto; 1054 margin: 10px auto 25px auto;
1037 width: 90%; 1055 width: 90%;
1038 1056
1039 .order { 1057 .order {
@@ -1069,6 +1087,11 @@ body,
1069 position: absolute; 1087 position: absolute;
1070 right: 5%; 1088 right: 5%;
1071 } 1089 }
1090
1091 &.button-grey {
1092 position: absolute;
1093 left: 5%;
1094 }
1072 } 1095 }
1073 } 1096 }
1074 } 1097 }
@@ -1259,6 +1282,57 @@ form {
1259 } 1282 }
1260} 1283}
1261 1284
1285.loading-input {
1286 position: relative;
1287
1288 @keyframes around {
1289 0% {
1290 transform: rotate(0deg);
1291 }
1292
1293 100% {
1294 transform: rotate(360deg);
1295 }
1296 }
1297
1298 .icon-container {
1299 position: absolute;
1300 right: 60px;
1301 top: calc(50% - 10px);
1302 }
1303
1304 .loader {
1305 position: relative;
1306 height: 20px;
1307 width: 20px;
1308 display: inline-block;
1309 animation: around 5.4s infinite;
1310
1311 &::after,
1312 &::before {
1313 content: "";
1314 background: $form-input-background;
1315 position: absolute;
1316 display: inline-block;
1317 width: 100%;
1318 height: 100%;
1319 border-width: 2px;
1320 border-color: #333 #333 transparent transparent;
1321 border-style: solid;
1322 border-radius: 20px;
1323 box-sizing: border-box;
1324 top: 0;
1325 left: 0;
1326 animation: around 0.7s ease-in-out infinite;
1327 }
1328
1329 &::after {
1330 animation: around 0.7s ease-in-out 0.1s infinite;
1331 background: transparent;
1332 }
1333 }
1334}
1335
1262// LOGIN 1336// LOGIN
1263.login-form-container { 1337.login-form-container {
1264 .remember-me { 1338 .remember-me {
@@ -1548,11 +1622,11 @@ form {
1548 text-align: center; 1622 text-align: center;
1549 1623
1550 a { 1624 a {
1625 background: $almost-white;
1551 display: inline-block; 1626 display: inline-block;
1552 margin: 0 15px; 1627 padding: 5px;
1553 text-decoration: none; 1628 text-decoration: none;
1554 color: $white; 1629 color: $dark-grey;
1555 font-weight: bold;
1556 } 1630 }
1557} 1631}
1558 1632
@@ -1600,13 +1674,14 @@ form {
1600 1674
1601 > div { 1675 > div {
1602 border-radius: 10px; 1676 border-radius: 10px;
1603 background: repeating-linear-gradient( 1677 background:
1604 -45deg, 1678 repeating-linear-gradient(
1605 $almost-white, 1679 -45deg,
1606 $almost-white 6px, 1680 $almost-white,
1607 var(--background-color) 6px, 1681 $almost-white 6px,
1608 var(--background-color) 12px 1682 var(--background-color) 6px,
1609 ); 1683 var(--background-color) 12px
1684 );
1610 width: 0%; 1685 width: 0%;
1611 height: 10px; 1686 height: 10px;
1612 } 1687 }
@@ -1630,6 +1705,123 @@ form {
1630 } 1705 }
1631} 1706}
1632 1707
1708// SERVER PAGE
1709
1710.server-tables-page,
1711.server-tables {
1712 .window-subtitle {
1713 &::before {
1714 display: block;
1715 margin: 8px auto;
1716 background: linear-gradient(to right, var(--background-color), $dark-grey, var(--background-color));
1717 width: 50%;
1718 height: 1px;
1719 content: '';
1720 }
1721 }
1722
1723 .server-row {
1724 p {
1725 height: 25px;
1726 padding: 0 10px;
1727 }
1728 }
1729
1730 .server-label {
1731 text-align: right;
1732 font-weight: bold;
1733 }
1734
1735 i {
1736 &.fa-color-green {
1737 color: $main-green;
1738 }
1739
1740 &.fa-color-orange {
1741 color: $orange;
1742 }
1743
1744 &.fa-color-red {
1745 color: $red;
1746 }
1747 }
1748
1749 @media screen and (max-width: 64em) {
1750 .server-label {
1751 text-align: center;
1752 }
1753
1754 .server-row {
1755 p {
1756 text-align: center;
1757 }
1758 }
1759 }
1760}
1761
1762// Batch creation
1763input[name='save_edit_batch'] {
1764 @extend %page-form-button;
1765}
1766
1767.addlink-batch-show-more {
1768 display: flex;
1769 align-items: center;
1770 margin: 20px 0 8px;
1771
1772 a {
1773 color: var(--main-color);
1774 text-decoration: none;
1775 }
1776
1777 &::before,
1778 &::after {
1779 content: "";
1780 flex-grow: 1;
1781 background: rgba(0, 0, 0, 0.35);
1782 height: 1px;
1783 font-size: 0;
1784 line-height: 0;
1785 }
1786
1787 &::before {
1788 margin: 0 16px 0 0;
1789 }
1790
1791 &::after {
1792 margin: 0 0 0 16px;
1793 }
1794}
1795
1796.dark-layer {
1797 display: none;
1798 position: fixed;
1799 height: 100%;
1800 width: 100%;
1801 z-index: 998;
1802 background-color: rgba(0, 0, 0, .75);
1803 color: #fff;
1804
1805 .screen-center {
1806 display: flex;
1807 flex-direction: column;
1808 justify-content: center;
1809 align-items: center;
1810 text-align: center;
1811 min-height: 100vh;
1812 }
1813
1814 .progressbar {
1815 width: 33%;
1816 }
1817}
1818
1819.addlink-batch-form-block {
1820 .pure-alert {
1821 margin: 25px 0 0 0;
1822 }
1823}
1824
1633// Print rules 1825// Print rules
1634@media print { 1826@media print {
1635 .shaarli-menu { 1827 .shaarli-menu {
diff --git a/assets/vintage/css/shaarli.css b/assets/vintage/css/shaarli.css
index 87c440c8..1688dce0 100644
--- a/assets/vintage/css/shaarli.css
+++ b/assets/vintage/css/shaarli.css
@@ -746,8 +746,6 @@ a.bigbutton, #pageheader a.bigbutton {
746 text-align: left; 746 text-align: left;
747 background-color: transparent; 747 background-color: transparent;
748 background-color: rgba(0, 0, 0, 0.4); 748 background-color: rgba(0, 0, 0, 0.4);
749 /* FF3+, Saf3+, Opera 10.10+, Chrome, IE9 */
750 filter: progid: DXImageTransform.Microsoft.gradient(startColorstr=#66000000, endColorstr=#66000000);
751 /* IE6–IE9 */ 749 /* IE6–IE9 */
752 text-shadow: 2px 2px 1px #000000; 750 text-shadow: 2px 2px 1px #000000;
753} 751}
diff --git a/composer.json b/composer.json
index 6b670fa2..94492586 100644
--- a/composer.json
+++ b/composer.json
@@ -10,6 +10,7 @@
10 }, 10 },
11 "keywords": ["bookmark", "link", "share", "web"], 11 "keywords": ["bookmark", "link", "share", "web"],
12 "config": { 12 "config": {
13 "sort-packages": true,
13 "platform": { 14 "platform": {
14 "php": "7.1.29" 15 "php": "7.1.29"
15 } 16 }
@@ -18,18 +19,20 @@
18 "php": ">=7.1", 19 "php": ">=7.1",
19 "ext-json": "*", 20 "ext-json": "*",
20 "ext-zlib": "*", 21 "ext-zlib": "*",
21 "shaarli/netscape-bookmark-parser": "^2.1",
22 "erusev/parsedown": "^1.6",
23 "slim/slim": "^3.0",
24 "arthurhoaro/web-thumbnailer": "^2.0", 22 "arthurhoaro/web-thumbnailer": "^2.0",
23 "erusev/parsedown": "^1.6",
24 "erusev/parsedown-extra": "^0.8.1",
25 "gettext/gettext": "^4.4",
26 "katzgrau/klogger": "^1.2",
27 "malkusch/lock": "^2.1",
25 "pubsubhubbub/publisher": "dev-master", 28 "pubsubhubbub/publisher": "dev-master",
26 "gettext/gettext": "^4.4" 29 "shaarli/netscape-bookmark-parser": "^2.1",
30 "slim/slim": "^3.0"
27 }, 31 },
28 "require-dev": { 32 "require-dev": {
29 "roave/security-advisories": "dev-master", 33 "roave/security-advisories": "dev-master",
30 "phpunit/phpcov": "*", 34 "squizlabs/php_codesniffer": "3.*",
31 "phpunit/phpunit": "^7.5", 35 "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
32 "squizlabs/php_codesniffer": "3.*"
33 }, 36 },
34 "suggest": { 37 "suggest": {
35 "ext-curl": "Allows fetching web pages and thumbnails in a more robust way", 38 "ext-curl": "Allows fetching web pages and thumbnails in a more robust way",
@@ -53,8 +56,10 @@
53 "Shaarli\\Feed\\": "application/feed", 56 "Shaarli\\Feed\\": "application/feed",
54 "Shaarli\\Formatter\\": "application/formatter", 57 "Shaarli\\Formatter\\": "application/formatter",
55 "Shaarli\\Front\\": "application/front", 58 "Shaarli\\Front\\": "application/front",
56 "Shaarli\\Front\\Controller\\": "application/front/controllers", 59 "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin",
60 "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor",
57 "Shaarli\\Front\\Exception\\": "application/front/exceptions", 61 "Shaarli\\Front\\Exception\\": "application/front/exceptions",
62 "Shaarli\\Helper\\": "application/helper",
58 "Shaarli\\Http\\": "application/http", 63 "Shaarli\\Http\\": "application/http",
59 "Shaarli\\Legacy\\": "application/legacy", 64 "Shaarli\\Legacy\\": "application/legacy",
60 "Shaarli\\Netscape\\": "application/netscape", 65 "Shaarli\\Netscape\\": "application/netscape",
diff --git a/composer.lock b/composer.lock
index b3373a32..3c89036f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,29 +4,31 @@
4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 "This file is @generated automatically" 5 "This file is @generated automatically"
6 ], 6 ],
7 "content-hash": "37e420b4b6e9fa74b27e127dd422d9a6", 7 "content-hash": "61360efbb2e1ba4c4fe00ce1f7a78ec5",
8 "packages": [ 8 "packages": [
9 { 9 {
10 "name": "arthurhoaro/web-thumbnailer", 10 "name": "arthurhoaro/web-thumbnailer",
11 "version": "v2.0.1", 11 "version": "v2.0.3",
12 "source": { 12 "source": {
13 "type": "git", 13 "type": "git",
14 "url": "https://github.com/ArthurHoaro/web-thumbnailer.git", 14 "url": "https://github.com/ArthurHoaro/web-thumbnailer.git",
15 "reference": "4aa27a1b54b9823341fedd7ca2dcfb11a6b3186a" 15 "reference": "39bfd4f3136d9e6096496b9720e877326cfe4775"
16 }, 16 },
17 "dist": { 17 "dist": {
18 "type": "zip", 18 "type": "zip",
19 "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/4aa27a1b54b9823341fedd7ca2dcfb11a6b3186a", 19 "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/39bfd4f3136d9e6096496b9720e877326cfe4775",
20 "reference": "4aa27a1b54b9823341fedd7ca2dcfb11a6b3186a", 20 "reference": "39bfd4f3136d9e6096496b9720e877326cfe4775",
21 "shasum": "" 21 "shasum": ""
22 }, 22 },
23 "require": { 23 "require": {
24 "php": ">=7.1", 24 "php": ">=7.1",
25 "phpunit/php-text-template": "^1.2" 25 "phpunit/php-text-template": "^1.2 || ^2.0"
26 }, 26 },
27 "require-dev": { 27 "require-dev": {
28 "gskema/phpcs-type-sniff": "^0.13.1",
28 "php-coveralls/php-coveralls": "^2.0", 29 "php-coveralls/php-coveralls": "^2.0",
29 "phpunit/phpunit": "^7.0 || ^8.0", 30 "phpstan/phpstan": "^0.12.9",
31 "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
30 "squizlabs/php_codesniffer": "^3.0" 32 "squizlabs/php_codesniffer": "^3.0"
31 }, 33 },
32 "type": "library", 34 "type": "library",
@@ -49,7 +51,11 @@
49 } 51 }
50 ], 52 ],
51 "description": "PHP library which will retrieve a thumbnail for any given URL", 53 "description": "PHP library which will retrieve a thumbnail for any given URL",
52 "time": "2020-01-17T19:42:49+00:00" 54 "support": {
55 "issues": "https://github.com/ArthurHoaro/web-thumbnailer/issues",
56 "source": "https://github.com/ArthurHoaro/web-thumbnailer/tree/v2.0.3"
57 },
58 "time": "2020-09-29T15:51:03+00:00"
53 }, 59 },
54 { 60 {
55 "name": "erusev/parsedown", 61 "name": "erusev/parsedown",
@@ -95,9 +101,64 @@
95 "markdown", 101 "markdown",
96 "parser" 102 "parser"
97 ], 103 ],
104 "support": {
105 "issues": "https://github.com/erusev/parsedown/issues",
106 "source": "https://github.com/erusev/parsedown/tree/1.7.x"
107 },
98 "time": "2019-12-30T22:54:17+00:00" 108 "time": "2019-12-30T22:54:17+00:00"
99 }, 109 },
100 { 110 {
111 "name": "erusev/parsedown-extra",
112 "version": "0.8.1",
113 "source": {
114 "type": "git",
115 "url": "https://github.com/erusev/parsedown-extra.git",
116 "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef"
117 },
118 "dist": {
119 "type": "zip",
120 "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/91ac3ff98f0cea243bdccc688df43810f044dcef",
121 "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef",
122 "shasum": ""
123 },
124 "require": {
125 "erusev/parsedown": "^1.7.4"
126 },
127 "require-dev": {
128 "phpunit/phpunit": "^4.8.35"
129 },
130 "type": "library",
131 "autoload": {
132 "psr-0": {
133 "ParsedownExtra": ""
134 }
135 },
136 "notification-url": "https://packagist.org/downloads/",
137 "license": [
138 "MIT"
139 ],
140 "authors": [
141 {
142 "name": "Emanuil Rusev",
143 "email": "hello@erusev.com",
144 "homepage": "http://erusev.com"
145 }
146 ],
147 "description": "An extension of Parsedown that adds support for Markdown Extra.",
148 "homepage": "https://github.com/erusev/parsedown-extra",
149 "keywords": [
150 "markdown",
151 "markdown extra",
152 "parsedown",
153 "parser"
154 ],
155 "support": {
156 "issues": "https://github.com/erusev/parsedown-extra/issues",
157 "source": "https://github.com/erusev/parsedown-extra/tree/0.8.x"
158 },
159 "time": "2019-12-30T23:20:37+00:00"
160 },
161 {
101 "name": "gettext/gettext", 162 "name": "gettext/gettext",
102 "version": "v4.8.2", 163 "version": "v4.8.2",
103 "source": { 164 "source": {
@@ -157,6 +218,11 @@
157 "po", 218 "po",
158 "translation" 219 "translation"
159 ], 220 ],
221 "support": {
222 "email": "oom@oscarotero.com",
223 "issues": "https://github.com/oscarotero/Gettext/issues",
224 "source": "https://github.com/php-gettext/Gettext/tree/v4.8.2"
225 },
160 "time": "2019-12-02T10:21:14+00:00" 226 "time": "2019-12-02T10:21:14+00:00"
161 }, 227 },
162 { 228 {
@@ -218,6 +284,10 @@
218 "translations", 284 "translations",
219 "unicode" 285 "unicode"
220 ], 286 ],
287 "support": {
288 "issues": "https://github.com/php-gettext/Languages/issues",
289 "source": "https://github.com/php-gettext/Languages/tree/2.6.0"
290 },
221 "time": "2019-11-13T10:30:21+00:00" 291 "time": "2019-11-13T10:30:21+00:00"
222 }, 292 },
223 { 293 {
@@ -268,9 +338,98 @@
268 "keywords": [ 338 "keywords": [
269 "logging" 339 "logging"
270 ], 340 ],
341 "support": {
342 "issues": "https://github.com/katzgrau/KLogger/issues",
343 "source": "https://github.com/katzgrau/KLogger/tree/master"
344 },
271 "time": "2016-11-07T19:29:14+00:00" 345 "time": "2016-11-07T19:29:14+00:00"
272 }, 346 },
273 { 347 {
348 "name": "malkusch/lock",
349 "version": "v2.1",
350 "source": {
351 "type": "git",
352 "url": "https://github.com/php-lock/lock.git",
353 "reference": "093f389ec2f38fc8686d2f70e23378182fce7714"
354 },
355 "dist": {
356 "type": "zip",
357 "url": "https://api.github.com/repos/php-lock/lock/zipball/093f389ec2f38fc8686d2f70e23378182fce7714",
358 "reference": "093f389ec2f38fc8686d2f70e23378182fce7714",
359 "shasum": ""
360 },
361 "require": {
362 "php": ">=7.1",
363 "psr/log": "^1"
364 },
365 "require-dev": {
366 "eloquent/liberator": "^2.0",
367 "ext-memcached": "*",
368 "ext-pcntl": "*",
369 "ext-pdo_mysql": "*",
370 "ext-pdo_sqlite": "*",
371 "ext-redis": "*",
372 "ext-sysvsem": "*",
373 "johnkary/phpunit-speedtrap": "^3.0",
374 "kriswallsmith/spork": "^0.3",
375 "mikey179/vfsstream": "^1.6",
376 "php-mock/php-mock-phpunit": "^2.1",
377 "phpunit/phpunit": "^7.4",
378 "predis/predis": "^1.1",
379 "squizlabs/php_codesniffer": "^3.3"
380 },
381 "suggest": {
382 "ext-pnctl": "Enables locking with flock without busy waiting in CLI scripts.",
383 "ext-redis": "To use this library with the PHP Redis extension.",
384 "ext-sysvsem": "Enables locking using semaphores.",
385 "predis/predis": "To use this library with predis."
386 },
387 "type": "library",
388 "autoload": {
389 "psr-4": {
390 "malkusch\\lock\\": "classes/"
391 }
392 },
393 "notification-url": "https://packagist.org/downloads/",
394 "license": [
395 "WTFPL"
396 ],
397 "authors": [
398 {
399 "name": "Markus Malkusch",
400 "email": "markus@malkusch.de",
401 "homepage": "http://markus.malkusch.de",
402 "role": "Developer"
403 },
404 {
405 "name": "Willem Stuursma-Ruwen",
406 "email": "willem@stuursma.name",
407 "role": "Developer"
408 }
409 ],
410 "description": "Mutex library for exclusive code execution.",
411 "homepage": "https://github.com/malkusch/lock",
412 "keywords": [
413 "advisory-locks",
414 "cas",
415 "flock",
416 "lock",
417 "locking",
418 "memcache",
419 "mutex",
420 "mysql",
421 "postgresql",
422 "redis",
423 "redlock",
424 "semaphore"
425 ],
426 "support": {
427 "issues": "https://github.com/php-lock/lock/issues",
428 "source": "https://github.com/php-lock/lock/tree/v2.1"
429 },
430 "time": "2018-12-12T19:53:29+00:00"
431 },
432 {
274 "name": "nikic/fast-route", 433 "name": "nikic/fast-route",
275 "version": "v1.3.0", 434 "version": "v1.3.0",
276 "source": { 435 "source": {
@@ -314,6 +473,10 @@
314 "router", 473 "router",
315 "routing" 474 "routing"
316 ], 475 ],
476 "support": {
477 "issues": "https://github.com/nikic/FastRoute/issues",
478 "source": "https://github.com/nikic/FastRoute/tree/master"
479 },
317 "time": "2018-02-13T20:26:39+00:00" 480 "time": "2018-02-13T20:26:39+00:00"
318 }, 481 },
319 { 482 {
@@ -355,6 +518,10 @@
355 "keywords": [ 518 "keywords": [
356 "template" 519 "template"
357 ], 520 ],
521 "support": {
522 "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
523 "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
524 },
358 "time": "2015-06-21T13:50:34+00:00" 525 "time": "2015-06-21T13:50:34+00:00"
359 }, 526 },
360 { 527 {
@@ -405,6 +572,10 @@
405 "container", 572 "container",
406 "dependency injection" 573 "dependency injection"
407 ], 574 ],
575 "support": {
576 "issues": "https://github.com/silexphp/Pimple/issues",
577 "source": "https://github.com/silexphp/Pimple/tree/master"
578 },
408 "time": "2018-01-21T07:42:36+00:00" 579 "time": "2018-01-21T07:42:36+00:00"
409 }, 580 },
410 { 581 {
@@ -454,6 +625,10 @@
454 "container-interop", 625 "container-interop",
455 "psr" 626 "psr"
456 ], 627 ],
628 "support": {
629 "issues": "https://github.com/php-fig/container/issues",
630 "source": "https://github.com/php-fig/container/tree/master"
631 },
457 "time": "2017-02-14T16:28:37+00:00" 632 "time": "2017-02-14T16:28:37+00:00"
458 }, 633 },
459 { 634 {
@@ -504,20 +679,23 @@
504 "request", 679 "request",
505 "response" 680 "response"
506 ], 681 ],
682 "support": {
683 "source": "https://github.com/php-fig/http-message/tree/master"
684 },
507 "time": "2016-08-06T14:39:51+00:00" 685 "time": "2016-08-06T14:39:51+00:00"
508 }, 686 },
509 { 687 {
510 "name": "psr/log", 688 "name": "psr/log",
511 "version": "1.1.2", 689 "version": "1.1.3",
512 "source": { 690 "source": {
513 "type": "git", 691 "type": "git",
514 "url": "https://github.com/php-fig/log.git", 692 "url": "https://github.com/php-fig/log.git",
515 "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801" 693 "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
516 }, 694 },
517 "dist": { 695 "dist": {
518 "type": "zip", 696 "type": "zip",
519 "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801", 697 "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
520 "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801", 698 "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
521 "shasum": "" 699 "shasum": ""
522 }, 700 },
523 "require": { 701 "require": {
@@ -551,7 +729,10 @@
551 "psr", 729 "psr",
552 "psr-3" 730 "psr-3"
553 ], 731 ],
554 "time": "2019-11-01T11:05:21+00:00" 732 "support": {
733 "source": "https://github.com/php-fig/log/tree/1.1.3"
734 },
735 "time": "2020-03-23T09:12:05+00:00"
555 }, 736 },
556 { 737 {
557 "name": "pubsubhubbub/publisher", 738 "name": "pubsubhubbub/publisher",
@@ -571,6 +752,7 @@
571 "ext-curl": "*", 752 "ext-curl": "*",
572 "php": "~5.4 || ~7.0" 753 "php": "~5.4 || ~7.0"
573 }, 754 },
755 "default-branch": true,
574 "type": "library", 756 "type": "library",
575 "autoload": { 757 "autoload": {
576 "psr-4": { 758 "psr-4": {
@@ -596,20 +778,24 @@
596 "pubsubhubbub", 778 "pubsubhubbub",
597 "websub" 779 "websub"
598 ], 780 ],
781 "support": {
782 "issues": "https://github.com/pubsubhubbub/php-publisher/issues",
783 "source": "https://github.com/pubsubhubbub/php-publisher/tree/master"
784 },
599 "time": "2018-10-09T05:20:28+00:00" 785 "time": "2018-10-09T05:20:28+00:00"
600 }, 786 },
601 { 787 {
602 "name": "shaarli/netscape-bookmark-parser", 788 "name": "shaarli/netscape-bookmark-parser",
603 "version": "v2.1.0", 789 "version": "v2.2.0",
604 "source": { 790 "source": {
605 "type": "git", 791 "type": "git",
606 "url": "https://github.com/shaarli/netscape-bookmark-parser.git", 792 "url": "https://github.com/shaarli/netscape-bookmark-parser.git",
607 "reference": "819008ee42c4dd7e45d988176a4a22d6ed689577" 793 "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df"
608 }, 794 },
609 "dist": { 795 "dist": {
610 "type": "zip", 796 "type": "zip",
611 "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/819008ee42c4dd7e45d988176a4a22d6ed689577", 797 "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/432a010af2bb1832d6fbc4763e6b0100b980a1df",
612 "reference": "819008ee42c4dd7e45d988176a4a22d6ed689577", 798 "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df",
613 "shasum": "" 799 "shasum": ""
614 }, 800 },
615 "require": { 801 "require": {
@@ -649,9 +835,13 @@
649 "bookmark", 835 "bookmark",
650 "link", 836 "link",
651 "netscape", 837 "netscape",
652 "parser" 838 "parse"
653 ], 839 ],
654 "time": "2018-10-06T14:43:38+00:00" 840 "support": {
841 "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues",
842 "source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v2.2.0"
843 },
844 "time": "2020-06-06T15:53:53+00:00"
655 }, 845 },
656 { 846 {
657 "name": "slim/slim", 847 "name": "slim/slim",
@@ -724,26 +914,30 @@
724 "micro", 914 "micro",
725 "router" 915 "router"
726 ], 916 ],
917 "support": {
918 "issues": "https://github.com/slimphp/Slim/issues",
919 "source": "https://github.com/slimphp/Slim/tree/3.x"
920 },
727 "time": "2019-11-28T17:40:33+00:00" 921 "time": "2019-11-28T17:40:33+00:00"
728 } 922 }
729 ], 923 ],
730 "packages-dev": [ 924 "packages-dev": [
731 { 925 {
732 "name": "doctrine/instantiator", 926 "name": "doctrine/instantiator",
733 "version": "1.3.0", 927 "version": "1.3.1",
734 "source": { 928 "source": {
735 "type": "git", 929 "type": "git",
736 "url": "https://github.com/doctrine/instantiator.git", 930 "url": "https://github.com/doctrine/instantiator.git",
737 "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" 931 "reference": "f350df0268e904597e3bd9c4685c53e0e333feea"
738 }, 932 },
739 "dist": { 933 "dist": {
740 "type": "zip", 934 "type": "zip",
741 "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", 935 "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea",
742 "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", 936 "reference": "f350df0268e904597e3bd9c4685c53e0e333feea",
743 "shasum": "" 937 "shasum": ""
744 }, 938 },
745 "require": { 939 "require": {
746 "php": "^7.1" 940 "php": "^7.1 || ^8.0"
747 }, 941 },
748 "require-dev": { 942 "require-dev": {
749 "doctrine/coding-standard": "^6.0", 943 "doctrine/coding-standard": "^6.0",
@@ -782,24 +976,42 @@
782 "constructor", 976 "constructor",
783 "instantiate" 977 "instantiate"
784 ], 978 ],
785 "time": "2019-10-21T16:45:58+00:00" 979 "support": {
980 "issues": "https://github.com/doctrine/instantiator/issues",
981 "source": "https://github.com/doctrine/instantiator/tree/1.3.x"
982 },
983 "funding": [
984 {
985 "url": "https://www.doctrine-project.org/sponsorship.html",
986 "type": "custom"
987 },
988 {
989 "url": "https://www.patreon.com/phpdoctrine",
990 "type": "patreon"
991 },
992 {
993 "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
994 "type": "tidelift"
995 }
996 ],
997 "time": "2020-05-29T17:27:14+00:00"
786 }, 998 },
787 { 999 {
788 "name": "myclabs/deep-copy", 1000 "name": "myclabs/deep-copy",
789 "version": "1.9.5", 1001 "version": "1.10.1",
790 "source": { 1002 "source": {
791 "type": "git", 1003 "type": "git",
792 "url": "https://github.com/myclabs/DeepCopy.git", 1004 "url": "https://github.com/myclabs/DeepCopy.git",
793 "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef" 1005 "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
794 }, 1006 },
795 "dist": { 1007 "dist": {
796 "type": "zip", 1008 "type": "zip",
797 "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef", 1009 "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
798 "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef", 1010 "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
799 "shasum": "" 1011 "shasum": ""
800 }, 1012 },
801 "require": { 1013 "require": {
802 "php": "^7.1" 1014 "php": "^7.1 || ^8.0"
803 }, 1015 },
804 "replace": { 1016 "replace": {
805 "myclabs/deep-copy": "self.version" 1017 "myclabs/deep-copy": "self.version"
@@ -830,7 +1042,17 @@
830 "object", 1042 "object",
831 "object graph" 1043 "object graph"
832 ], 1044 ],
833 "time": "2020-01-17T21:11:47+00:00" 1045 "support": {
1046 "issues": "https://github.com/myclabs/DeepCopy/issues",
1047 "source": "https://github.com/myclabs/DeepCopy/tree/1.x"
1048 },
1049 "funding": [
1050 {
1051 "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
1052 "type": "tidelift"
1053 }
1054 ],
1055 "time": "2020-06-29T13:22:24+00:00"
834 }, 1056 },
835 { 1057 {
836 "name": "phar-io/manifest", 1058 "name": "phar-io/manifest",
@@ -885,6 +1107,10 @@
885 } 1107 }
886 ], 1108 ],
887 "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 1109 "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
1110 "support": {
1111 "issues": "https://github.com/phar-io/manifest/issues",
1112 "source": "https://github.com/phar-io/manifest/tree/master"
1113 },
888 "time": "2018-07-08T19:23:20+00:00" 1114 "time": "2018-07-08T19:23:20+00:00"
889 }, 1115 },
890 { 1116 {
@@ -932,28 +1158,29 @@
932 } 1158 }
933 ], 1159 ],
934 "description": "Library for handling version information and constraints", 1160 "description": "Library for handling version information and constraints",
1161 "support": {
1162 "issues": "https://github.com/phar-io/version/issues",
1163 "source": "https://github.com/phar-io/version/tree/master"
1164 },
935 "time": "2018-07-08T19:19:57+00:00" 1165 "time": "2018-07-08T19:19:57+00:00"
936 }, 1166 },
937 { 1167 {
938 "name": "phpdocumentor/reflection-common", 1168 "name": "phpdocumentor/reflection-common",
939 "version": "2.0.0", 1169 "version": "2.1.0",
940 "source": { 1170 "source": {
941 "type": "git", 1171 "type": "git",
942 "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 1172 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
943 "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" 1173 "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
944 }, 1174 },
945 "dist": { 1175 "dist": {
946 "type": "zip", 1176 "type": "zip",
947 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", 1177 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
948 "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", 1178 "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
949 "shasum": "" 1179 "shasum": ""
950 }, 1180 },
951 "require": { 1181 "require": {
952 "php": ">=7.1" 1182 "php": ">=7.1"
953 }, 1183 },
954 "require-dev": {
955 "phpunit/phpunit": "~6"
956 },
957 "type": "library", 1184 "type": "library",
958 "extra": { 1185 "extra": {
959 "branch-alias": { 1186 "branch-alias": {
@@ -984,7 +1211,11 @@
984 "reflection", 1211 "reflection",
985 "static analysis" 1212 "static analysis"
986 ], 1213 ],
987 "time": "2018-08-07T13:53:10+00:00" 1214 "support": {
1215 "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
1216 "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master"
1217 },
1218 "time": "2020-04-27T09:25:28+00:00"
988 }, 1219 },
989 { 1220 {
990 "name": "phpdocumentor/reflection-docblock", 1221 "name": "phpdocumentor/reflection-docblock",
@@ -1036,6 +1267,10 @@
1036 } 1267 }
1037 ], 1268 ],
1038 "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 1269 "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
1270 "support": {
1271 "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
1272 "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/4.x"
1273 },
1039 "time": "2019-12-28T18:55:12+00:00" 1274 "time": "2019-12-28T18:55:12+00:00"
1040 }, 1275 },
1041 { 1276 {
@@ -1083,28 +1318,32 @@
1083 } 1318 }
1084 ], 1319 ],
1085 "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 1320 "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
1321 "support": {
1322 "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
1323 "source": "https://github.com/phpDocumentor/TypeResolver/tree/0.7.2"
1324 },
1086 "time": "2019-08-22T18:11:29+00:00" 1325 "time": "2019-08-22T18:11:29+00:00"
1087 }, 1326 },
1088 { 1327 {
1089 "name": "phpspec/prophecy", 1328 "name": "phpspec/prophecy",
1090 "version": "1.10.1", 1329 "version": "v1.10.3",
1091 "source": { 1330 "source": {
1092 "type": "git", 1331 "type": "git",
1093 "url": "https://github.com/phpspec/prophecy.git", 1332 "url": "https://github.com/phpspec/prophecy.git",
1094 "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc" 1333 "reference": "451c3cd1418cf640de218914901e51b064abb093"
1095 }, 1334 },
1096 "dist": { 1335 "dist": {
1097 "type": "zip", 1336 "type": "zip",
1098 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/cbe1df668b3fe136bcc909126a0f529a78d4cbbc", 1337 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
1099 "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc", 1338 "reference": "451c3cd1418cf640de218914901e51b064abb093",
1100 "shasum": "" 1339 "shasum": ""
1101 }, 1340 },
1102 "require": { 1341 "require": {
1103 "doctrine/instantiator": "^1.0.2", 1342 "doctrine/instantiator": "^1.0.2",
1104 "php": "^5.3|^7.0", 1343 "php": "^5.3|^7.0",
1105 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", 1344 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
1106 "sebastian/comparator": "^1.2.3|^2.0|^3.0", 1345 "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
1107 "sebastian/recursion-context": "^1.0|^2.0|^3.0" 1346 "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
1108 }, 1347 },
1109 "require-dev": { 1348 "require-dev": {
1110 "phpspec/phpspec": "^2.5 || ^3.2", 1349 "phpspec/phpspec": "^2.5 || ^3.2",
@@ -1146,7 +1385,11 @@
1146 "spy", 1385 "spy",
1147 "stub" 1386 "stub"
1148 ], 1387 ],
1149 "time": "2019-12-22T21:05:45+00:00" 1388 "support": {
1389 "issues": "https://github.com/phpspec/prophecy/issues",
1390 "source": "https://github.com/phpspec/prophecy/tree/v1.10.3"
1391 },
1392 "time": "2020-03-05T15:02:03+00:00"
1150 }, 1393 },
1151 { 1394 {
1152 "name": "phpunit/php-code-coverage", 1395 "name": "phpunit/php-code-coverage",
@@ -1209,6 +1452,10 @@
1209 "testing", 1452 "testing",
1210 "xunit" 1453 "xunit"
1211 ], 1454 ],
1455 "support": {
1456 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
1457 "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/master"
1458 },
1212 "time": "2018-10-31T16:06:48+00:00" 1459 "time": "2018-10-31T16:06:48+00:00"
1213 }, 1460 },
1214 { 1461 {
@@ -1259,6 +1506,10 @@
1259 "filesystem", 1506 "filesystem",
1260 "iterator" 1507 "iterator"
1261 ], 1508 ],
1509 "support": {
1510 "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
1511 "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.2"
1512 },
1262 "time": "2018-09-13T20:33:42+00:00" 1513 "time": "2018-09-13T20:33:42+00:00"
1263 }, 1514 },
1264 { 1515 {
@@ -1308,6 +1559,10 @@
1308 "keywords": [ 1559 "keywords": [
1309 "timer" 1560 "timer"
1310 ], 1561 ],
1562 "support": {
1563 "issues": "https://github.com/sebastianbergmann/php-timer/issues",
1564 "source": "https://github.com/sebastianbergmann/php-timer/tree/master"
1565 },
1311 "time": "2019-06-07T04:22:29+00:00" 1566 "time": "2019-06-07T04:22:29+00:00"
1312 }, 1567 },
1313 { 1568 {
@@ -1357,59 +1612,12 @@
1357 "keywords": [ 1612 "keywords": [
1358 "tokenizer" 1613 "tokenizer"
1359 ], 1614 ],
1360 "time": "2019-09-17T06:23:10+00:00" 1615 "support": {
1361 }, 1616 "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
1362 { 1617 "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.1"
1363 "name": "phpunit/phpcov",
1364 "version": "5.0.0",
1365 "source": {
1366 "type": "git",
1367 "url": "https://github.com/sebastianbergmann/phpcov.git",
1368 "reference": "72fb974e6fe9b39d7e0b0d44061d2ba4c49ee0b8"
1369 },
1370 "dist": {
1371 "type": "zip",
1372 "url": "https://api.github.com/repos/sebastianbergmann/phpcov/zipball/72fb974e6fe9b39d7e0b0d44061d2ba4c49ee0b8",
1373 "reference": "72fb974e6fe9b39d7e0b0d44061d2ba4c49ee0b8",
1374 "shasum": ""
1375 },
1376 "require": {
1377 "php": "^7.1",
1378 "phpunit/php-code-coverage": "^6.0",
1379 "phpunit/phpunit": "^7.0",
1380 "sebastian/diff": "^3.0",
1381 "sebastian/finder-facade": "^1.1",
1382 "sebastian/version": "^2.0",
1383 "symfony/console": "^3.0 || ^4.0"
1384 },
1385 "bin": [
1386 "phpcov"
1387 ],
1388 "type": "library",
1389 "extra": {
1390 "branch-alias": {
1391 "dev-master": "5.0-dev"
1392 }
1393 },
1394 "autoload": {
1395 "classmap": [
1396 "src/"
1397 ]
1398 }, 1618 },
1399 "notification-url": "https://packagist.org/downloads/", 1619 "abandoned": true,
1400 "license": [ 1620 "time": "2019-09-17T06:23:10+00:00"
1401 "BSD-3-Clause"
1402 ],
1403 "authors": [
1404 {
1405 "name": "Sebastian Bergmann",
1406 "email": "sebastian@phpunit.de",
1407 "role": "lead"
1408 }
1409 ],
1410 "description": "CLI frontend for php-code-coverage",
1411 "homepage": "https://github.com/sebastianbergmann/phpcov",
1412 "time": "2018-02-04T10:18:50+00:00"
1413 }, 1621 },
1414 { 1622 {
1415 "name": "phpunit/phpunit", 1623 "name": "phpunit/phpunit",
@@ -1493,6 +1701,10 @@
1493 "testing", 1701 "testing",
1494 "xunit" 1702 "xunit"
1495 ], 1703 ],
1704 "support": {
1705 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
1706 "source": "https://github.com/sebastianbergmann/phpunit/tree/7.5.20"
1707 },
1496 "time": "2020-01-08T08:45:45+00:00" 1708 "time": "2020-01-08T08:45:45+00:00"
1497 }, 1709 },
1498 { 1710 {
@@ -1501,12 +1713,12 @@
1501 "source": { 1713 "source": {
1502 "type": "git", 1714 "type": "git",
1503 "url": "https://github.com/Roave/SecurityAdvisories.git", 1715 "url": "https://github.com/Roave/SecurityAdvisories.git",
1504 "reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389" 1716 "reference": "ba5d234b3a1559321b816b64aafc2ce6728799ff"
1505 }, 1717 },
1506 "dist": { 1718 "dist": {
1507 "type": "zip", 1719 "type": "zip",
1508 "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389", 1720 "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ba5d234b3a1559321b816b64aafc2ce6728799ff",
1509 "reference": "67ac6ea8f4a078c3c9b7aec5d7ae70f098c37389", 1721 "reference": "ba5d234b3a1559321b816b64aafc2ce6728799ff",
1510 "shasum": "" 1722 "shasum": ""
1511 }, 1723 },
1512 "conflict": { 1724 "conflict": {
@@ -1515,22 +1727,32 @@
1515 "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", 1727 "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
1516 "amphp/artax": "<1.0.6|>=2,<2.0.6", 1728 "amphp/artax": "<1.0.6|>=2,<2.0.6",
1517 "amphp/http": "<1.0.1", 1729 "amphp/http": "<1.0.1",
1730 "amphp/http-client": ">=4,<4.4",
1518 "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6", 1731 "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
1519 "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99", 1732 "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
1520 "aws/aws-sdk-php": ">=3,<3.2.1", 1733 "aws/aws-sdk-php": ">=3,<3.2.1",
1734 "bagisto/bagisto": "<0.1.5",
1735 "barrelstrength/sprout-base-email": "<1.2.7",
1736 "barrelstrength/sprout-forms": "<3.9",
1737 "baserproject/basercms": ">=4,<=4.3.6",
1738 "bolt/bolt": "<3.7.1",
1521 "brightlocal/phpwhois": "<=4.2.5", 1739 "brightlocal/phpwhois": "<=4.2.5",
1740 "buddypress/buddypress": "<5.1.2",
1522 "bugsnag/bugsnag-laravel": ">=2,<2.0.2", 1741 "bugsnag/bugsnag-laravel": ">=2,<2.0.2",
1523 "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7", 1742 "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7",
1524 "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", 1743 "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
1525 "cartalyst/sentry": "<=2.1.6", 1744 "cartalyst/sentry": "<=2.1.6",
1745 "centreon/centreon": "<18.10.8|>=19,<19.4.5",
1746 "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
1526 "codeigniter/framework": "<=3.0.6", 1747 "codeigniter/framework": "<=3.0.6",
1527 "composer/composer": "<=1-alpha.11", 1748 "composer/composer": "<=1-alpha.11",
1528 "contao-components/mediaelement": ">=2.14.2,<2.21.1", 1749 "contao-components/mediaelement": ">=2.14.2,<2.21.1",
1529 "contao/core": ">=2,<3.5.39", 1750 "contao/core": ">=2,<3.5.39",
1530 "contao/core-bundle": ">=4,<4.4.46|>=4.5,<4.8.6", 1751 "contao/core-bundle": ">=4,<4.4.52|>=4.5,<4.9.6|= 4.10.0",
1531 "contao/listing-bundle": ">=4,<4.4.8", 1752 "contao/listing-bundle": ">=4,<4.4.8",
1532 "datadog/dd-trace": ">=0.30,<0.30.2", 1753 "datadog/dd-trace": ">=0.30,<0.30.2",
1533 "david-garcia/phpwhois": "<=4.3.1", 1754 "david-garcia/phpwhois": "<=4.3.1",
1755 "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1",
1534 "doctrine/annotations": ">=1,<1.2.7", 1756 "doctrine/annotations": ">=1,<1.2.7",
1535 "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", 1757 "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2",
1536 "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1", 1758 "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1",
@@ -1540,46 +1762,77 @@
1540 "doctrine/mongodb-odm": ">=1,<1.0.2", 1762 "doctrine/mongodb-odm": ">=1,<1.0.2",
1541 "doctrine/mongodb-odm-bundle": ">=2,<3.0.1", 1763 "doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
1542 "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1", 1764 "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1",
1765 "dolibarr/dolibarr": "<11.0.4",
1543 "dompdf/dompdf": ">=0.6,<0.6.2", 1766 "dompdf/dompdf": ">=0.6,<0.6.2",
1544 "drupal/core": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1", 1767 "drupal/core": ">=7,<7.73|>=8,<8.8.10|>=8.9,<8.9.6|>=9,<9.0.6",
1545 "drupal/drupal": ">=7,<7.69|>=8,<8.7.11|>=8.8,<8.8.1", 1768 "drupal/drupal": ">=7,<7.73|>=8,<8.8.10|>=8.9,<8.9.6|>=9,<9.0.6",
1546 "endroid/qr-code-bundle": "<3.4.2", 1769 "endroid/qr-code-bundle": "<3.4.2",
1770 "enshrined/svg-sanitize": "<0.13.1",
1547 "erusev/parsedown": "<1.7.2", 1771 "erusev/parsedown": "<1.7.2",
1548 "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.4", 1772 "ezsystems/demobundle": ">=5.4,<5.4.6.1",
1549 "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.13.1|>=6,<6.7.9.1|>=6.8,<6.13.5.1|>=7,<7.2.4.1|>=7.3,<7.3.2.1", 1773 "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1",
1550 "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.12.3|>=2011,<2017.12.4.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3", 1774 "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1",
1775 "ezsystems/ezplatform": ">=1.7,<1.7.9.1|>=1.13,<1.13.5.1|>=2.5,<2.5.4",
1776 "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6",
1777 "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1",
1778 "ezsystems/ezplatform-kernel": ">=1,<1.0.2.1",
1779 "ezsystems/ezplatform-user": ">=1,<1.0.1",
1780 "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.14.2|>=6,<6.7.9.1|>=6.8,<6.13.6.3|>=7,<7.2.4.1|>=7.3,<7.3.2.1|>=7.5,<7.5.7.1",
1781 "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.2|>=2011,<2017.12.7.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.5.1",
1782 "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
1551 "ezsystems/repository-forms": ">=2.3,<2.3.2.1", 1783 "ezsystems/repository-forms": ">=2.3,<2.3.2.1",
1552 "ezyang/htmlpurifier": "<4.1.1", 1784 "ezyang/htmlpurifier": "<4.1.1",
1553 "firebase/php-jwt": "<2", 1785 "firebase/php-jwt": "<2",
1554 "fooman/tcpdf": "<6.2.22", 1786 "fooman/tcpdf": "<6.2.22",
1555 "fossar/tcpdf-parser": "<6.2.22", 1787 "fossar/tcpdf-parser": "<6.2.22",
1788 "friendsofsymfony/oauth2-php": "<1.3",
1556 "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", 1789 "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
1557 "friendsofsymfony/user-bundle": ">=1.2,<1.3.5", 1790 "friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
1791 "friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
1558 "fuel/core": "<1.8.1", 1792 "fuel/core": "<1.8.1",
1793 "getgrav/grav": "<1.7-beta.8",
1794 "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3",
1559 "gree/jose": "<=2.2", 1795 "gree/jose": "<=2.2",
1560 "gregwar/rst": "<1.0.3", 1796 "gregwar/rst": "<1.0.3",
1561 "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1", 1797 "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
1562 "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10", 1798 "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10",
1563 "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", 1799 "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.31|>=7,<7.22.4",
1564 "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29", 1800 "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29|>=5.5,<=5.5.44|>=6,<6.18.34|>=7,<7.23.2",
1565 "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", 1801 "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15",
1802 "illuminate/view": ">=7,<7.1.2",
1566 "ivankristianto/phpwhois": "<=4.3", 1803 "ivankristianto/phpwhois": "<=4.3",
1567 "james-heinrich/getid3": "<1.9.9", 1804 "james-heinrich/getid3": "<1.9.9",
1568 "joomla/session": "<1.3.1", 1805 "joomla/session": "<1.3.1",
1569 "jsmitty12/phpwhois": "<5.1", 1806 "jsmitty12/phpwhois": "<5.1",
1570 "kazist/phpwhois": "<=4.2.6", 1807 "kazist/phpwhois": "<=4.2.6",
1808 "kitodo/presentation": "<3.1.2",
1571 "kreait/firebase-php": ">=3.2,<3.8.1", 1809 "kreait/firebase-php": ">=3.2,<3.8.1",
1572 "la-haute-societe/tcpdf": "<6.2.22", 1810 "la-haute-societe/tcpdf": "<6.2.22",
1573 "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", 1811 "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.34|>=7,<7.23.2",
1574 "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", 1812 "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
1575 "league/commonmark": "<0.18.3", 1813 "league/commonmark": "<0.18.3",
1814 "librenms/librenms": "<1.53",
1815 "livewire/livewire": ">2.2.4,<2.2.6",
1816 "magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
1576 "magento/magento1ce": "<1.9.4.3", 1817 "magento/magento1ce": "<1.9.4.3",
1577 "magento/magento1ee": ">=1,<1.14.4.3", 1818 "magento/magento1ee": ">=1,<1.14.4.3",
1578 "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2", 1819 "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
1820 "marcwillmann/turn": "<0.3.3",
1821 "mittwald/typo3_forum": "<1.2.1",
1579 "monolog/monolog": ">=1.8,<1.12", 1822 "monolog/monolog": ">=1.8,<1.12",
1580 "namshi/jose": "<2.2", 1823 "namshi/jose": "<2.2",
1824 "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6",
1825 "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13",
1826 "nystudio107/craft-seomatic": "<3.3",
1827 "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
1828 "october/backend": ">=1.0.319,<1.0.467",
1829 "october/cms": ">=1.0.319,<1.0.466",
1830 "october/october": ">=1.0.319,<1.0.466",
1831 "october/rain": ">=1.0.319,<1.0.468",
1581 "onelogin/php-saml": "<2.10.4", 1832 "onelogin/php-saml": "<2.10.4",
1833 "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
1582 "openid/php-openid": "<2.3", 1834 "openid/php-openid": "<2.3",
1835 "openmage/magento-lts": "<19.4.6|>=20,<20.0.2",
1583 "oro/crm": ">=1.7,<1.7.4", 1836 "oro/crm": ">=1.7,<1.7.4",
1584 "oro/platform": ">=1.7,<1.7.4", 1837 "oro/platform": ">=1.7,<1.7.4",
1585 "padraic/humbug_get_contents": "<1.1.2", 1838 "padraic/humbug_get_contents": "<1.1.2",
@@ -1587,50 +1840,77 @@
1587 "paragonie/random_compat": "<2", 1840 "paragonie/random_compat": "<2",
1588 "paypal/merchant-sdk-php": "<3.12", 1841 "paypal/merchant-sdk-php": "<3.12",
1589 "pear/archive_tar": "<1.4.4", 1842 "pear/archive_tar": "<1.4.4",
1590 "phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6", 1843 "personnummer/personnummer": "<3.0.2",
1591 "phpoffice/phpexcel": "<=1.8.1", 1844 "phpfastcache/phpfastcache": ">=5,<5.0.13",
1592 "phpoffice/phpspreadsheet": "<=1.5", 1845 "phpmailer/phpmailer": "<6.1.6",
1846 "phpmussel/phpmussel": ">=1,<1.6",
1847 "phpmyadmin/phpmyadmin": "<4.9.2",
1848 "phpoffice/phpexcel": "<1.8.2",
1849 "phpoffice/phpspreadsheet": "<1.8",
1593 "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", 1850 "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
1594 "phpwhois/phpwhois": "<=4.2.5", 1851 "phpwhois/phpwhois": "<=4.2.5",
1595 "phpxmlrpc/extras": "<0.6.1", 1852 "phpxmlrpc/extras": "<0.6.1",
1853 "pimcore/pimcore": "<6.3",
1854 "prestashop/autoupgrade": ">=4,<4.10.1",
1855 "prestashop/contactform": ">1.0.1,<4.3",
1856 "prestashop/gamification": "<2.3.2",
1857 "prestashop/ps_facetedsearch": "<3.4.1",
1858 "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2",
1596 "propel/propel": ">=2-alpha.1,<=2-alpha.7", 1859 "propel/propel": ">=2-alpha.1,<=2-alpha.7",
1597 "propel/propel1": ">=1,<=1.7.1", 1860 "propel/propel1": ">=1,<=1.7.1",
1861 "pterodactyl/panel": "<0.7.19|>=1-rc.0,<=1-rc.6",
1598 "pusher/pusher-php-server": "<2.2.1", 1862 "pusher/pusher-php-server": "<2.2.1",
1599 "robrichards/xmlseclibs": ">=1,<3.0.4", 1863 "rainlab/debugbar-plugin": "<3.1",
1864 "robrichards/xmlseclibs": "<3.0.4",
1865 "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1",
1600 "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9", 1866 "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9",
1601 "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", 1867 "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
1602 "sensiolabs/connect": "<4.2.3", 1868 "sensiolabs/connect": "<4.2.3",
1603 "serluck/phpwhois": "<=4.2.6", 1869 "serluck/phpwhois": "<=4.2.6",
1870 "shopware/core": "<=6.3.1",
1871 "shopware/platform": "<=6.3.1",
1604 "shopware/shopware": "<5.3.7", 1872 "shopware/shopware": "<5.3.7",
1605 "silverstripe/cms": ">=3,<=3.0.11|>=3.1,<3.1.11", 1873 "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
1874 "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
1875 "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
1876 "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
1606 "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", 1877 "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
1607 "silverstripe/framework": ">=3,<3.6.7|>=3.7,<3.7.3|>=4,<4.4", 1878 "silverstripe/framework": "<4.4.7|>=4.5,<4.5.4",
1608 "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2", 1879 "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2|>=3.2,<3.2.4",
1609 "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", 1880 "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
1610 "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4", 1881 "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
1882 "silverstripe/subsites": ">=2,<2.1.1",
1883 "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1",
1611 "silverstripe/userforms": "<3", 1884 "silverstripe/userforms": "<3",
1612 "simple-updates/phpwhois": "<=1", 1885 "simple-updates/phpwhois": "<=1",
1613 "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4", 1886 "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4",
1614 "simplesamlphp/simplesamlphp": "<1.17.8", 1887 "simplesamlphp/simplesamlphp": "<1.18.6",
1615 "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", 1888 "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
1889 "simplito/elliptic-php": "<1.0.6",
1616 "slim/slim": "<2.6", 1890 "slim/slim": "<2.6",
1617 "smarty/smarty": "<3.1.33", 1891 "smarty/smarty": "<3.1.33",
1618 "socalnick/scn-social-auth": "<1.15.2", 1892 "socalnick/scn-social-auth": "<1.15.2",
1619 "spoonity/tcpdf": "<6.2.22", 1893 "spoonity/tcpdf": "<6.2.22",
1620 "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", 1894 "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
1895 "ssddanbrown/bookstack": "<0.29.2",
1621 "stormpath/sdk": ">=0,<9.9.99", 1896 "stormpath/sdk": ">=0,<9.9.99",
1622 "studio-42/elfinder": "<2.1.48", 1897 "studio-42/elfinder": "<2.1.49",
1898 "sulu/sulu": "<1.6.34|>=2,<2.0.10|>=2.1,<2.1.1",
1623 "swiftmailer/swiftmailer": ">=4,<5.4.5", 1899 "swiftmailer/swiftmailer": ">=4,<5.4.5",
1624 "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", 1900 "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
1625 "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", 1901 "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
1626 "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", 1902 "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
1627 "sylius/sylius": ">=1,<1.1.18|>=1.2,<1.2.17|>=1.3,<1.3.12|>=1.4,<1.4.4", 1903 "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4",
1904 "sylius/sylius": "<1.3.16|>=1.4,<1.4.12|>=1.5,<1.5.9|>=1.6,<1.6.5",
1905 "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
1906 "symbiote/silverstripe-versionedfiles": "<=2.0.3",
1628 "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", 1907 "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
1629 "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1908 "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
1909 "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4",
1630 "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", 1910 "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
1631 "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1911 "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
1632 "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", 1912 "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
1633 "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", 1913 "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.4.13|>=5,<5.1.5",
1634 "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", 1914 "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
1635 "symfony/mime": ">=4.3,<4.3.8", 1915 "symfony/mime": ">=4.3,<4.3.8",
1636 "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1916 "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
@@ -1638,19 +1918,20 @@
1638 "symfony/polyfill-php55": ">=1,<1.10", 1918 "symfony/polyfill-php55": ">=1,<1.10",
1639 "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1919 "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
1640 "symfony/routing": ">=2,<2.0.19", 1920 "symfony/routing": ">=2,<2.0.19",
1641 "symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1921 "symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=4.4,<4.4.7|>=5,<5.0.7",
1642 "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", 1922 "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
1643 "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7", 1923 "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7",
1644 "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", 1924 "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
1645 "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", 1925 "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
1646 "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8", 1926 "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
1647 "symfony/serializer": ">=2,<2.0.11", 1927 "symfony/serializer": ">=2,<2.0.11",
1648 "symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", 1928 "symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.4.13|>=5,<5.1.5",
1649 "symfony/translation": ">=2,<2.0.17", 1929 "symfony/translation": ">=2,<2.0.17",
1650 "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", 1930 "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
1651 "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", 1931 "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
1652 "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", 1932 "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4",
1653 "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7", 1933 "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7",
1934 "t3g/svg-sanitizer": "<1.0.3",
1654 "tecnickcom/tcpdf": "<6.2.22", 1935 "tecnickcom/tcpdf": "<6.2.22",
1655 "thelia/backoffice-default-template": ">=2.1,<2.1.2", 1936 "thelia/backoffice-default-template": ">=2.1,<2.1.2",
1656 "thelia/thelia": ">=2.1-beta.1,<2.1.3", 1937 "thelia/thelia": ">=2.1-beta.1,<2.1.3",
@@ -1658,22 +1939,27 @@
1658 "titon/framework": ">=0,<9.9.99", 1939 "titon/framework": ">=0,<9.9.99",
1659 "truckersmp/phpwhois": "<=4.3.1", 1940 "truckersmp/phpwhois": "<=4.3.1",
1660 "twig/twig": "<1.38|>=2,<2.7", 1941 "twig/twig": "<1.38|>=2,<2.7",
1661 "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1", 1942 "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.20|>=10,<10.4.6",
1662 "typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1", 1943 "typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.20|>=10,<10.4.6",
1663 "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5", 1944 "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5",
1664 "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4", 1945 "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4",
1665 "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", 1946 "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
1947 "typo3fluid/fluid": ">=2,<2.0.5|>=2.1,<2.1.4|>=2.2,<2.2.1|>=2.3,<2.3.5|>=2.4,<2.4.1|>=2.5,<2.5.5|>=2.6,<2.6.1",
1666 "ua-parser/uap-php": "<3.8", 1948 "ua-parser/uap-php": "<3.8",
1949 "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
1950 "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
1667 "wallabag/tcpdf": "<6.2.22", 1951 "wallabag/tcpdf": "<6.2.22",
1668 "willdurand/js-translation-bundle": "<2.1.1", 1952 "willdurand/js-translation-bundle": "<2.1.1",
1953 "yii2mod/yii2-cms": "<1.9.2",
1669 "yiisoft/yii": ">=1.1.14,<1.1.15", 1954 "yiisoft/yii": ">=1.1.14,<1.1.15",
1670 "yiisoft/yii2": "<2.0.15", 1955 "yiisoft/yii2": "<2.0.38",
1671 "yiisoft/yii2-bootstrap": "<2.0.4", 1956 "yiisoft/yii2-bootstrap": "<2.0.4",
1672 "yiisoft/yii2-dev": "<2.0.15", 1957 "yiisoft/yii2-dev": "<2.0.15",
1673 "yiisoft/yii2-elasticsearch": "<2.0.5", 1958 "yiisoft/yii2-elasticsearch": "<2.0.5",
1674 "yiisoft/yii2-gii": "<2.0.4", 1959 "yiisoft/yii2-gii": "<2.0.4",
1675 "yiisoft/yii2-jui": "<2.0.4", 1960 "yiisoft/yii2-jui": "<2.0.4",
1676 "yiisoft/yii2-redis": "<2.0.8", 1961 "yiisoft/yii2-redis": "<2.0.8",
1962 "yourls/yourls": "<1.7.4",
1677 "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", 1963 "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
1678 "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", 1964 "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
1679 "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", 1965 "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
@@ -1710,10 +1996,29 @@
1710 "name": "Marco Pivetta", 1996 "name": "Marco Pivetta",
1711 "email": "ocramius@gmail.com", 1997 "email": "ocramius@gmail.com",
1712 "role": "maintainer" 1998 "role": "maintainer"
1999 },
2000 {
2001 "name": "Ilya Tribusean",
2002 "email": "slash3b@gmail.com",
2003 "role": "maintainer"
1713 } 2004 }
1714 ], 2005 ],
1715 "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", 2006 "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
1716 "time": "2020-01-06T19:16:46+00:00" 2007 "support": {
2008 "issues": "https://github.com/Roave/SecurityAdvisories/issues",
2009 "source": "https://github.com/Roave/SecurityAdvisories/tree/latest"
2010 },
2011 "funding": [
2012 {
2013 "url": "https://github.com/Ocramius",
2014 "type": "github"
2015 },
2016 {
2017 "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories",
2018 "type": "tidelift"
2019 }
2020 ],
2021 "time": "2020-10-08T21:02:27+00:00"
1717 }, 2022 },
1718 { 2023 {
1719 "name": "sebastian/code-unit-reverse-lookup", 2024 "name": "sebastian/code-unit-reverse-lookup",
@@ -1758,6 +2063,10 @@
1758 ], 2063 ],
1759 "description": "Looks up which function or method a line of code belongs to", 2064 "description": "Looks up which function or method a line of code belongs to",
1760 "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 2065 "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
2066 "support": {
2067 "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
2068 "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/master"
2069 },
1761 "time": "2017-03-04T06:30:41+00:00" 2070 "time": "2017-03-04T06:30:41+00:00"
1762 }, 2071 },
1763 { 2072 {
@@ -1822,6 +2131,10 @@
1822 "compare", 2131 "compare",
1823 "equality" 2132 "equality"
1824 ], 2133 ],
2134 "support": {
2135 "issues": "https://github.com/sebastianbergmann/comparator/issues",
2136 "source": "https://github.com/sebastianbergmann/comparator/tree/master"
2137 },
1825 "time": "2018-07-12T15:12:46+00:00" 2138 "time": "2018-07-12T15:12:46+00:00"
1826 }, 2139 },
1827 { 2140 {
@@ -1878,6 +2191,10 @@
1878 "unidiff", 2191 "unidiff",
1879 "unified diff" 2192 "unified diff"
1880 ], 2193 ],
2194 "support": {
2195 "issues": "https://github.com/sebastianbergmann/diff/issues",
2196 "source": "https://github.com/sebastianbergmann/diff/tree/master"
2197 },
1881 "time": "2019-02-04T06:01:07+00:00" 2198 "time": "2019-02-04T06:01:07+00:00"
1882 }, 2199 },
1883 { 2200 {
@@ -1931,6 +2248,10 @@
1931 "environment", 2248 "environment",
1932 "hhvm" 2249 "hhvm"
1933 ], 2250 ],
2251 "support": {
2252 "issues": "https://github.com/sebastianbergmann/environment/issues",
2253 "source": "https://github.com/sebastianbergmann/environment/tree/4.2.3"
2254 },
1934 "time": "2019-11-20T08:46:58+00:00" 2255 "time": "2019-11-20T08:46:58+00:00"
1935 }, 2256 },
1936 { 2257 {
@@ -1998,50 +2319,11 @@
1998 "export", 2319 "export",
1999 "exporter" 2320 "exporter"
2000 ], 2321 ],
2001 "time": "2019-09-14T09:02:43+00:00" 2322 "support": {
2002 }, 2323 "issues": "https://github.com/sebastianbergmann/exporter/issues",
2003 { 2324 "source": "https://github.com/sebastianbergmann/exporter/tree/master"
2004 "name": "sebastian/finder-facade",
2005 "version": "1.2.3",
2006 "source": {
2007 "type": "git",
2008 "url": "https://github.com/sebastianbergmann/finder-facade.git",
2009 "reference": "167c45d131f7fc3d159f56f191a0a22228765e16"
2010 },
2011 "dist": {
2012 "type": "zip",
2013 "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/167c45d131f7fc3d159f56f191a0a22228765e16",
2014 "reference": "167c45d131f7fc3d159f56f191a0a22228765e16",
2015 "shasum": ""
2016 }, 2325 },
2017 "require": { 2326 "time": "2019-09-14T09:02:43+00:00"
2018 "php": "^7.1",
2019 "symfony/finder": "^2.3|^3.0|^4.0|^5.0",
2020 "theseer/fdomdocument": "^1.6"
2021 },
2022 "type": "library",
2023 "extra": {
2024 "branch-alias": []
2025 },
2026 "autoload": {
2027 "classmap": [
2028 "src/"
2029 ]
2030 },
2031 "notification-url": "https://packagist.org/downloads/",
2032 "license": [
2033 "BSD-3-Clause"
2034 ],
2035 "authors": [
2036 {
2037 "name": "Sebastian Bergmann",
2038 "email": "sebastian@phpunit.de",
2039 "role": "lead"
2040 }
2041 ],
2042 "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
2043 "homepage": "https://github.com/sebastianbergmann/finder-facade",
2044 "time": "2020-01-16T08:08:45+00:00"
2045 }, 2327 },
2046 { 2328 {
2047 "name": "sebastian/global-state", 2329 "name": "sebastian/global-state",
@@ -2092,6 +2374,10 @@
2092 "keywords": [ 2374 "keywords": [
2093 "global state" 2375 "global state"
2094 ], 2376 ],
2377 "support": {
2378 "issues": "https://github.com/sebastianbergmann/global-state/issues",
2379 "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0"
2380 },
2095 "time": "2017-04-27T15:39:26+00:00" 2381 "time": "2017-04-27T15:39:26+00:00"
2096 }, 2382 },
2097 { 2383 {
@@ -2139,6 +2425,10 @@
2139 ], 2425 ],
2140 "description": "Traverses array structures and object graphs to enumerate all referenced objects", 2426 "description": "Traverses array structures and object graphs to enumerate all referenced objects",
2141 "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 2427 "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
2428 "support": {
2429 "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
2430 "source": "https://github.com/sebastianbergmann/object-enumerator/tree/master"
2431 },
2142 "time": "2017-08-03T12:35:26+00:00" 2432 "time": "2017-08-03T12:35:26+00:00"
2143 }, 2433 },
2144 { 2434 {
@@ -2184,6 +2474,10 @@
2184 ], 2474 ],
2185 "description": "Allows reflection of object attributes, including inherited and non-public ones", 2475 "description": "Allows reflection of object attributes, including inherited and non-public ones",
2186 "homepage": "https://github.com/sebastianbergmann/object-reflector/", 2476 "homepage": "https://github.com/sebastianbergmann/object-reflector/",
2477 "support": {
2478 "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
2479 "source": "https://github.com/sebastianbergmann/object-reflector/tree/master"
2480 },
2187 "time": "2017-03-29T09:07:27+00:00" 2481 "time": "2017-03-29T09:07:27+00:00"
2188 }, 2482 },
2189 { 2483 {
@@ -2237,6 +2531,10 @@
2237 ], 2531 ],
2238 "description": "Provides functionality to recursively process PHP variables", 2532 "description": "Provides functionality to recursively process PHP variables",
2239 "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 2533 "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
2534 "support": {
2535 "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
2536 "source": "https://github.com/sebastianbergmann/recursion-context/tree/master"
2537 },
2240 "time": "2017-03-03T06:23:57+00:00" 2538 "time": "2017-03-03T06:23:57+00:00"
2241 }, 2539 },
2242 { 2540 {
@@ -2279,6 +2577,10 @@
2279 ], 2577 ],
2280 "description": "Provides a list of PHP built-in functions that operate on resources", 2578 "description": "Provides a list of PHP built-in functions that operate on resources",
2281 "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 2579 "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
2580 "support": {
2581 "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
2582 "source": "https://github.com/sebastianbergmann/resource-operations/tree/master"
2583 },
2282 "time": "2018-10-04T04:07:39+00:00" 2584 "time": "2018-10-04T04:07:39+00:00"
2283 }, 2585 },
2284 { 2586 {
@@ -2322,20 +2624,24 @@
2322 ], 2624 ],
2323 "description": "Library that helps with managing the version number of Git-hosted PHP projects", 2625 "description": "Library that helps with managing the version number of Git-hosted PHP projects",
2324 "homepage": "https://github.com/sebastianbergmann/version", 2626 "homepage": "https://github.com/sebastianbergmann/version",
2627 "support": {
2628 "issues": "https://github.com/sebastianbergmann/version/issues",
2629 "source": "https://github.com/sebastianbergmann/version/tree/master"
2630 },
2325 "time": "2016-10-03T07:35:21+00:00" 2631 "time": "2016-10-03T07:35:21+00:00"
2326 }, 2632 },
2327 { 2633 {
2328 "name": "squizlabs/php_codesniffer", 2634 "name": "squizlabs/php_codesniffer",
2329 "version": "3.5.3", 2635 "version": "3.5.6",
2330 "source": { 2636 "source": {
2331 "type": "git", 2637 "type": "git",
2332 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 2638 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
2333 "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb" 2639 "reference": "e97627871a7eab2f70e59166072a6b767d5834e0"
2334 }, 2640 },
2335 "dist": { 2641 "dist": {
2336 "type": "zip", 2642 "type": "zip",
2337 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", 2643 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0",
2338 "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", 2644 "reference": "e97627871a7eab2f70e59166072a6b767d5834e0",
2339 "shasum": "" 2645 "shasum": ""
2340 }, 2646 },
2341 "require": { 2647 "require": {
@@ -2373,145 +2679,25 @@
2373 "phpcs", 2679 "phpcs",
2374 "standards" 2680 "standards"
2375 ], 2681 ],
2376 "time": "2019-12-04T04:46:47+00:00" 2682 "support": {
2377 }, 2683 "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
2378 { 2684 "source": "https://github.com/squizlabs/PHP_CodeSniffer",
2379 "name": "symfony/console", 2685 "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
2380 "version": "v4.4.2",
2381 "source": {
2382 "type": "git",
2383 "url": "https://github.com/symfony/console.git",
2384 "reference": "82437719dab1e6bdd28726af14cb345c2ec816d0"
2385 }, 2686 },
2386 "dist": { 2687 "time": "2020-08-10T04:50:15+00:00"
2387 "type": "zip",
2388 "url": "https://api.github.com/repos/symfony/console/zipball/82437719dab1e6bdd28726af14cb345c2ec816d0",
2389 "reference": "82437719dab1e6bdd28726af14cb345c2ec816d0",
2390 "shasum": ""
2391 },
2392 "require": {
2393 "php": "^7.1.3",
2394 "symfony/polyfill-mbstring": "~1.0",
2395 "symfony/polyfill-php73": "^1.8",
2396 "symfony/service-contracts": "^1.1|^2"
2397 },
2398 "conflict": {
2399 "symfony/dependency-injection": "<3.4",
2400 "symfony/event-dispatcher": "<4.3|>=5",
2401 "symfony/lock": "<4.4",
2402 "symfony/process": "<3.3"
2403 },
2404 "provide": {
2405 "psr/log-implementation": "1.0"
2406 },
2407 "require-dev": {
2408 "psr/log": "~1.0",
2409 "symfony/config": "^3.4|^4.0|^5.0",
2410 "symfony/dependency-injection": "^3.4|^4.0|^5.0",
2411 "symfony/event-dispatcher": "^4.3",
2412 "symfony/lock": "^4.4|^5.0",
2413 "symfony/process": "^3.4|^4.0|^5.0",
2414 "symfony/var-dumper": "^4.3|^5.0"
2415 },
2416 "suggest": {
2417 "psr/log": "For using the console logger",
2418 "symfony/event-dispatcher": "",
2419 "symfony/lock": "",
2420 "symfony/process": ""
2421 },
2422 "type": "library",
2423 "extra": {
2424 "branch-alias": {
2425 "dev-master": "4.4-dev"
2426 }
2427 },
2428 "autoload": {
2429 "psr-4": {
2430 "Symfony\\Component\\Console\\": ""
2431 },
2432 "exclude-from-classmap": [
2433 "/Tests/"
2434 ]
2435 },
2436 "notification-url": "https://packagist.org/downloads/",
2437 "license": [
2438 "MIT"
2439 ],
2440 "authors": [
2441 {
2442 "name": "Fabien Potencier",
2443 "email": "fabien@symfony.com"
2444 },
2445 {
2446 "name": "Symfony Community",
2447 "homepage": "https://symfony.com/contributors"
2448 }
2449 ],
2450 "description": "Symfony Console Component",
2451 "homepage": "https://symfony.com",
2452 "time": "2019-12-17T10:32:23+00:00"
2453 },
2454 {
2455 "name": "symfony/finder",
2456 "version": "v4.4.2",
2457 "source": {
2458 "type": "git",
2459 "url": "https://github.com/symfony/finder.git",
2460 "reference": "ce8743441da64c41e2a667b8eb66070444ed911e"
2461 },
2462 "dist": {
2463 "type": "zip",
2464 "url": "https://api.github.com/repos/symfony/finder/zipball/ce8743441da64c41e2a667b8eb66070444ed911e",
2465 "reference": "ce8743441da64c41e2a667b8eb66070444ed911e",
2466 "shasum": ""
2467 },
2468 "require": {
2469 "php": "^7.1.3"
2470 },
2471 "type": "library",
2472 "extra": {
2473 "branch-alias": {
2474 "dev-master": "4.4-dev"
2475 }
2476 },
2477 "autoload": {
2478 "psr-4": {
2479 "Symfony\\Component\\Finder\\": ""
2480 },
2481 "exclude-from-classmap": [
2482 "/Tests/"
2483 ]
2484 },
2485 "notification-url": "https://packagist.org/downloads/",
2486 "license": [
2487 "MIT"
2488 ],
2489 "authors": [
2490 {
2491 "name": "Fabien Potencier",
2492 "email": "fabien@symfony.com"
2493 },
2494 {
2495 "name": "Symfony Community",
2496 "homepage": "https://symfony.com/contributors"
2497 }
2498 ],
2499 "description": "Symfony Finder Component",
2500 "homepage": "https://symfony.com",
2501 "time": "2019-11-17T21:56:56+00:00"
2502 }, 2688 },
2503 { 2689 {
2504 "name": "symfony/polyfill-ctype", 2690 "name": "symfony/polyfill-ctype",
2505 "version": "v1.13.1", 2691 "version": "v1.18.1",
2506 "source": { 2692 "source": {
2507 "type": "git", 2693 "type": "git",
2508 "url": "https://github.com/symfony/polyfill-ctype.git", 2694 "url": "https://github.com/symfony/polyfill-ctype.git",
2509 "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" 2695 "reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
2510 }, 2696 },
2511 "dist": { 2697 "dist": {
2512 "type": "zip", 2698 "type": "zip",
2513 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", 2699 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
2514 "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", 2700 "reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
2515 "shasum": "" 2701 "shasum": ""
2516 }, 2702 },
2517 "require": { 2703 "require": {
@@ -2523,7 +2709,11 @@
2523 "type": "library", 2709 "type": "library",
2524 "extra": { 2710 "extra": {
2525 "branch-alias": { 2711 "branch-alias": {
2526 "dev-master": "1.13-dev" 2712 "dev-master": "1.18-dev"
2713 },
2714 "thanks": {
2715 "name": "symfony/polyfill",
2716 "url": "https://github.com/symfony/polyfill"
2527 } 2717 }
2528 }, 2718 },
2529 "autoload": { 2719 "autoload": {
@@ -2556,222 +2746,24 @@
2556 "polyfill", 2746 "polyfill",
2557 "portable" 2747 "portable"
2558 ], 2748 ],
2559 "time": "2019-11-27T13:56:44+00:00" 2749 "support": {
2560 }, 2750 "source": "https://github.com/symfony/polyfill-ctype/tree/v1.18.0"
2561 {
2562 "name": "symfony/polyfill-mbstring",
2563 "version": "v1.13.1",
2564 "source": {
2565 "type": "git",
2566 "url": "https://github.com/symfony/polyfill-mbstring.git",
2567 "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f"
2568 },
2569 "dist": {
2570 "type": "zip",
2571 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f",
2572 "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f",
2573 "shasum": ""
2574 },
2575 "require": {
2576 "php": ">=5.3.3"
2577 },
2578 "suggest": {
2579 "ext-mbstring": "For best performance"
2580 },
2581 "type": "library",
2582 "extra": {
2583 "branch-alias": {
2584 "dev-master": "1.13-dev"
2585 }
2586 },
2587 "autoload": {
2588 "psr-4": {
2589 "Symfony\\Polyfill\\Mbstring\\": ""
2590 },
2591 "files": [
2592 "bootstrap.php"
2593 ]
2594 }, 2751 },
2595 "notification-url": "https://packagist.org/downloads/", 2752 "funding": [
2596 "license": [
2597 "MIT"
2598 ],
2599 "authors": [
2600 { 2753 {
2601 "name": "Nicolas Grekas", 2754 "url": "https://symfony.com/sponsor",
2602 "email": "p@tchwork.com" 2755 "type": "custom"
2603 }, 2756 },
2604 { 2757 {
2605 "name": "Symfony Community", 2758 "url": "https://github.com/fabpot",
2606 "homepage": "https://symfony.com/contributors" 2759 "type": "github"
2607 }
2608 ],
2609 "description": "Symfony polyfill for the Mbstring extension",
2610 "homepage": "https://symfony.com",
2611 "keywords": [
2612 "compatibility",
2613 "mbstring",
2614 "polyfill",
2615 "portable",
2616 "shim"
2617 ],
2618 "time": "2019-11-27T14:18:11+00:00"
2619 },
2620 {
2621 "name": "symfony/polyfill-php73",
2622 "version": "v1.13.1",
2623 "source": {
2624 "type": "git",
2625 "url": "https://github.com/symfony/polyfill-php73.git",
2626 "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f"
2627 },
2628 "dist": {
2629 "type": "zip",
2630 "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/4b0e2222c55a25b4541305a053013d5647d3a25f",
2631 "reference": "4b0e2222c55a25b4541305a053013d5647d3a25f",
2632 "shasum": ""
2633 },
2634 "require": {
2635 "php": ">=5.3.3"
2636 },
2637 "type": "library",
2638 "extra": {
2639 "branch-alias": {
2640 "dev-master": "1.13-dev"
2641 }
2642 },
2643 "autoload": {
2644 "psr-4": {
2645 "Symfony\\Polyfill\\Php73\\": ""
2646 },
2647 "files": [
2648 "bootstrap.php"
2649 ],
2650 "classmap": [
2651 "Resources/stubs"
2652 ]
2653 },
2654 "notification-url": "https://packagist.org/downloads/",
2655 "license": [
2656 "MIT"
2657 ],
2658 "authors": [
2659 {
2660 "name": "Nicolas Grekas",
2661 "email": "p@tchwork.com"
2662 }, 2760 },
2663 { 2761 {
2664 "name": "Symfony Community", 2762 "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
2665 "homepage": "https://symfony.com/contributors" 2763 "type": "tidelift"
2666 } 2764 }
2667 ], 2765 ],
2668 "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", 2766 "time": "2020-07-14T12:35:20+00:00"
2669 "homepage": "https://symfony.com",
2670 "keywords": [
2671 "compatibility",
2672 "polyfill",
2673 "portable",
2674 "shim"
2675 ],
2676 "time": "2019-11-27T16:25:15+00:00"
2677 },
2678 {
2679 "name": "symfony/service-contracts",
2680 "version": "v1.1.8",
2681 "source": {
2682 "type": "git",
2683 "url": "https://github.com/symfony/service-contracts.git",
2684 "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf"
2685 },
2686 "dist": {
2687 "type": "zip",
2688 "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffc7f5692092df31515df2a5ecf3b7302b3ddacf",
2689 "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf",
2690 "shasum": ""
2691 },
2692 "require": {
2693 "php": "^7.1.3",
2694 "psr/container": "^1.0"
2695 },
2696 "suggest": {
2697 "symfony/service-implementation": ""
2698 },
2699 "type": "library",
2700 "extra": {
2701 "branch-alias": {
2702 "dev-master": "1.1-dev"
2703 }
2704 },
2705 "autoload": {
2706 "psr-4": {
2707 "Symfony\\Contracts\\Service\\": ""
2708 }
2709 },
2710 "notification-url": "https://packagist.org/downloads/",
2711 "license": [
2712 "MIT"
2713 ],
2714 "authors": [
2715 {
2716 "name": "Nicolas Grekas",
2717 "email": "p@tchwork.com"
2718 },
2719 {
2720 "name": "Symfony Community",
2721 "homepage": "https://symfony.com/contributors"
2722 }
2723 ],
2724 "description": "Generic abstractions related to writing services",
2725 "homepage": "https://symfony.com",
2726 "keywords": [
2727 "abstractions",
2728 "contracts",
2729 "decoupling",
2730 "interfaces",
2731 "interoperability",
2732 "standards"
2733 ],
2734 "time": "2019-10-14T12:27:06+00:00"
2735 },
2736 {
2737 "name": "theseer/fdomdocument",
2738 "version": "1.6.6",
2739 "source": {
2740 "type": "git",
2741 "url": "https://github.com/theseer/fDOMDocument.git",
2742 "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca"
2743 },
2744 "dist": {
2745 "type": "zip",
2746 "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca",
2747 "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca",
2748 "shasum": ""
2749 },
2750 "require": {
2751 "ext-dom": "*",
2752 "lib-libxml": "*",
2753 "php": ">=5.3.3"
2754 },
2755 "type": "library",
2756 "autoload": {
2757 "classmap": [
2758 "src/"
2759 ]
2760 },
2761 "notification-url": "https://packagist.org/downloads/",
2762 "license": [
2763 "BSD-3-Clause"
2764 ],
2765 "authors": [
2766 {
2767 "name": "Arne Blankerts",
2768 "email": "arne@blankerts.de",
2769 "role": "lead"
2770 }
2771 ],
2772 "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
2773 "homepage": "https://github.com/theseer/fDOMDocument",
2774 "time": "2017-06-30T11:53:12+00:00"
2775 }, 2767 },
2776 { 2768 {
2777 "name": "theseer/tokenizer", 2769 "name": "theseer/tokenizer",
@@ -2811,28 +2803,33 @@
2811 } 2803 }
2812 ], 2804 ],
2813 "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 2805 "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
2806 "support": {
2807 "issues": "https://github.com/theseer/tokenizer/issues",
2808 "source": "https://github.com/theseer/tokenizer/tree/master"
2809 },
2814 "time": "2019-06-13T22:48:21+00:00" 2810 "time": "2019-06-13T22:48:21+00:00"
2815 }, 2811 },
2816 { 2812 {
2817 "name": "webmozart/assert", 2813 "name": "webmozart/assert",
2818 "version": "1.6.0", 2814 "version": "1.9.1",
2819 "source": { 2815 "source": {
2820 "type": "git", 2816 "type": "git",
2821 "url": "https://github.com/webmozart/assert.git", 2817 "url": "https://github.com/webmozart/assert.git",
2822 "reference": "573381c0a64f155a0d9a23f4b0c797194805b925" 2818 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
2823 }, 2819 },
2824 "dist": { 2820 "dist": {
2825 "type": "zip", 2821 "type": "zip",
2826 "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925", 2822 "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
2827 "reference": "573381c0a64f155a0d9a23f4b0c797194805b925", 2823 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
2828 "shasum": "" 2824 "shasum": ""
2829 }, 2825 },
2830 "require": { 2826 "require": {
2831 "php": "^5.3.3 || ^7.0", 2827 "php": "^5.3.3 || ^7.0 || ^8.0",
2832 "symfony/polyfill-ctype": "^1.8" 2828 "symfony/polyfill-ctype": "^1.8"
2833 }, 2829 },
2834 "conflict": { 2830 "conflict": {
2835 "vimeo/psalm": "<3.6.0" 2831 "phpstan/phpstan": "<0.12.20",
2832 "vimeo/psalm": "<3.9.1"
2836 }, 2833 },
2837 "require-dev": { 2834 "require-dev": {
2838 "phpunit/phpunit": "^4.8.36 || ^7.5.13" 2835 "phpunit/phpunit": "^4.8.36 || ^7.5.13"
@@ -2859,7 +2856,11 @@
2859 "check", 2856 "check",
2860 "validate" 2857 "validate"
2861 ], 2858 ],
2862 "time": "2019-11-24T13:36:37+00:00" 2859 "support": {
2860 "issues": "https://github.com/webmozart/assert/issues",
2861 "source": "https://github.com/webmozart/assert/tree/master"
2862 },
2863 "time": "2020-07-08T17:02:28+00:00"
2863 } 2864 }
2864 ], 2865 ],
2865 "aliases": [], 2866 "aliases": [],
@@ -2878,5 +2879,6 @@
2878 "platform-dev": [], 2879 "platform-dev": [],
2879 "platform-overrides": { 2880 "platform-overrides": {
2880 "php": "7.1.29" 2881 "php": "7.1.29"
2881 } 2882 },
2883 "plugin-api-version": "2.0.0"
2882} 2884}
diff --git a/doc/md/3rd-party-libraries.md b/doc/md/3rd-party-libraries.md
deleted file mode 100644
index 7e7dd334..00000000
--- a/doc/md/3rd-party-libraries.md
+++ /dev/null
@@ -1,21 +0,0 @@
1## CSS
2
3- Yahoo UI [CSS Reset](http://yuilibrary.com/yui/docs/cssreset/) - standardize cross-browser rendering
4
5## Javascript
6
7- [Awesomeplete](https://leaverou.github.io/awesomplete/) ([GitHub](https://github.com/LeaVerou/awesomplete)) - autocompletion in input forms
8- [bLazy](http://dinbror.dk/blazy/) ([GitHub](https://github.com/dinbror/blazy)) - lazy loading for thumbnails
9- [qr.js](http://neocotic.com/qr.js/) ([GitHub](https://github.com/neocotic/qr.js)) - QR code generation
10
11## PHP
12
13- [RainTPL](https://github.com/rainphp/raintpl) - HTML templating for PHP
14
15### Composer
16
17Library | Usage
18---|---
19[`shaarli/netscape-bookmark-parser`](https://packagist.org/packages/shaarli/netscape-bookmark-parser) | Import bookmarks from Netscape files
20[`erusev/parsedown`](https://packagist.org/packages/erusev/parsedown) | Parse MarkDown syntax for the MarkDown plugin
21[`slim/slim`](https://packagist.org/packages/slim/slim) | Handle routes and middleware for the REST API
diff --git a/doc/md/Backup-and-restore.md b/doc/md/Backup-and-restore.md
new file mode 100644
index 00000000..e7e2775c
--- /dev/null
+++ b/doc/md/Backup-and-restore.md
@@ -0,0 +1,11 @@
1## Backup and restore
2
3All data and [configuration](Shaarli-configuration.md) is kept in the `data` directory. Backup this directory:
4
5```bash
6rsync -avzP my.server.com:/var/www/shaarli.mydomain.org/data ~/backups/shaarli-data-$(date +%Y-%m-%d_%H%M)
7```
8
9It is strongly recommended to do periodic, automatic backups to a seperate machine. You can automate the command above using a cron job or full-featured backup solutions such as [rsnapshot](https://rsnapshot.org/)
10
11To restore a backup, simply put back the `data/` directory in place, owerwriting any existing files. \ No newline at end of file
diff --git a/doc/md/Browsing-and-searching.md b/doc/md/Browsing-and-searching.md
deleted file mode 100644
index 16c69855..00000000
--- a/doc/md/Browsing-and-searching.md
+++ /dev/null
@@ -1,37 +0,0 @@
1## Plain text search
2
3Use the `Search text` field to search in _any_ of the fields of all links (Title, URL, Description...)
4
5**Exclude text/tags:** Use the `-` operator before a word or tag (example `-uninteresting`) to prevent entries containing (or tagged) `uninteresting` from showing up in the search results.
6
7**Exact text search:** Use double-quotes (example `"exact search"`) to search for the exact expression.
8
9Both exclude patterns and exact searches can be combined with normal searches (example `"exact search" term otherterm -notthis "very exact" stuff -notagain`)
10
11## Tags search
12
13Use the `Filter by tags` field to restrict displayed links to entries tagged with one or multiple tags (use space to separate tags).
14
15**Hidden tags:** Tags starting with a dot `.` (example `.secret`) are private. They can only be seen and searched when logged in.
16
17### Tag cloud
18
19The `Tag cloud` page diplays a "cloud" view of all tags in your Shaarli.
20
21 * The most frequently used tags are displayed with a bigger font size.
22 * When sorting by `Most used` or `Alphabetical`, tags are displayed as a _list_, along with counters and edit/delete buttons for each tag.
23 * Clicking on any tag will display a list of all Shaares matching this tag.
24 * Clicking on the counter next to a tag `example`, will filter the tag cloud to only display tags found in Shaares tagged `example`. Repeat this any number of times to further filter the tag cloud. Click `List all links with those tags` to display Shaares matching your current tag filter.
25
26## Filtering RSS feeds/Picture wall
27
28RSS feeds can also be restricted to only return items matching a text/tag search: see [RSS feeds](RSS-feeds).
29
30## Filter buttons
31
32Filter buttons can be found at the top left of the link list. They allow you to apply different filters to the list:
33
34 * **Private links:** When this toggle button is enabled, only shaares set to `private` will be shown.
35 * **Untagged links:** When the this toggle button is enabled (top left of the link list), only shaares _without any tags_ will be shown in the link list.
36
37Filter buttons are only available when logged in.
diff --git a/doc/md/Community-&-Related-software.md b/doc/md/Community-and-related-software.md
index 54f18c8e..53a7555e 100644
--- a/doc/md/Community-&-Related-software.md
+++ b/doc/md/Community-and-related-software.md
@@ -1,66 +1,87 @@
1# Community & related software
2
1_Unofficial but related work on Shaarli. If you maintain one of these, 3_Unofficial but related work on Shaarli. If you maintain one of these,
2please get in touch with us to help us find a way to adapt your work to our fork._ 4please get in touch with us to help us find a way to adapt your work to our fork._
3 5
4## Related software
5 6
7## Related software
6 8
7### REST API clients 9### REST API clients
8See [REST API](REST-API) for a list of official and community clients. 10See [REST API](REST-API) for a list of official and community clients.
9 11
10 12
11### Third party plugins 13### Third party plugins
12- [autosave](https://github.com/kalvn/shaarli-plugin-autosave) by [@kalvn](https://github.com/kalvn): Automatically saves data when editing a link to avoid any loss in case of crash or unexpected shutdown. 14
13- [Code Coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter. 15- [autosave](https://github.com/kalvn/shaarli-plugin-autosave) by [@kalvn](https://github.com/kalvn): Automatically saves data when editing a Shaare to avoid any loss in case of crash or unexpected shutdown.
14- [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli. 16- [code-coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter.
17- [custom-css](https://github.com/immanuelfodor/shaarli-custom-css) by [@immanuelfodor](https://github.com/immanuelfodor) - Customize the look and feel of the UI with custom CSS rules
18- [disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli.
19- [emojione](https://github.com/immanuelfodor/emojione) by [@immanuelfodor](https://github.com/immanuelfodor) - Resurrected fork of the original emojione project
20- [favicons](https://github.com/trailjeep/shaarli-favicons) by [@trailjeep](https://github.com/trailjeep) - Shaarli plugin to add favicon/filetype icons to Shaares.
15- [google analytics](https://github.com/ericjuden/Shaarli-Google-Analytics-Plugin) by [@ericjuden](http://github.com/ericjuden): Adds Google Analytics tracking support 21- [google analytics](https://github.com/ericjuden/Shaarli-Google-Analytics-Plugin) by [@ericjuden](http://github.com/ericjuden): Adds Google Analytics tracking support
16- [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli. 22- [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.
17- [markdown-toolbar](https://github.com/immanuelfodor/shaarli-markdown-toolbar) by [@immanuelfodor](https://github.com/immanuelfodor) - Easily insert markdown syntax into the Description field when editing a link. 23- [markdown-toolbar](https://github.com/immanuelfodor/shaarli-markdown-toolbar) by [@immanuelfodor](https://github.com/immanuelfodor) - Easily insert markdown syntax into the Description field when editing a Shaare.
18- [related](https://github.com/ilesinge/shaarli-related) by [@ilesinge](https://github.com/ilesinge) - Show related links based on the number of identical tags. 24- [related](https://github.com/ilesinge/shaarli-related) by [@ilesinge](https://github.com/ilesinge) - Show related Shaares based on the number of identical tags.
19- [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks. 25- [shaarli-descriptor](https://github.com/immanuelfodor/shaarli-descriptor) by [@immanuelfodor](https://github.com/immanuelfodor) - Customize the default height/number of rows of the Description field when editing a Shaare.
20- [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli
21- [shaarli2mastodon](https://github.com/kalvn/shaarli2mastodon) by [@kalvn](https://github.com/kalvn) - This Shaarli plugin allows you to automatically publish links you post on your Mastodon timeline. 26- [shaarli2mastodon](https://github.com/kalvn/shaarli2mastodon) by [@kalvn](https://github.com/kalvn) - This Shaarli plugin allows you to automatically publish links you post on your Mastodon timeline.
22- [shaarli-descriptor](https://github.com/immanuelfodor/shaarli-descriptor) by [@immanuelfodor](https://github.com/immanuelfodor) - Customize the default height/number of rows of the Description field when editing a link. 27- [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your Shaares from Shaarli
28- [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks.
23- [urlextern](https://github.com/trailjeep/shaarli-urlextern) by [@trailjeep](https://github.com/trailjeep) - Shaarli plugin to open external links in a new tab/window. 29- [urlextern](https://github.com/trailjeep/shaarli-urlextern) by [@trailjeep](https://github.com/trailjeep) - Shaarli plugin to open external links in a new tab/window.
24- [favicons](https://github.com/trailjeep/shaarli-favicons) by [@trailjeep](https://github.com/trailjeep) - Shaarli plugin to add favicon/filetype icons to links. 30- [webhooks](https://gitlab.com/flow.gunso/shaarli-webhooks) by [@flow.gunso](https://gitlab.com/flow.gunso) - Shaarli plugin that enables user-defined callback URL, i.e. webhooks, for specific Shaarli events (link saving, deletion...)
31
25 32
26### Third-party themes 33### Third-party themes
34
27See [Theming](Theming) for a list of community-contributed themes, and an installation guide. 35See [Theming](Theming) for a list of community-contributed themes, and an installation guide.
28 36
29 37
30### Integration with other platforms 38### Integration with other platforms
39
31- [tt-rss-shaarli](https://github.com/jcsaaddupuy/tt-rss-shaarli) - [Tiny-Tiny RSS](http://tt-rss.org/) plugin that adds support for sharing articles with Shaarli 40- [tt-rss-shaarli](https://github.com/jcsaaddupuy/tt-rss-shaarli) - [Tiny-Tiny RSS](http://tt-rss.org/) plugin that adds support for sharing articles with Shaarli
32- [octopress-shaarli](https://github.com/ahmet2mir/octopress-shaarli) - Octopress plugin to retrieve Shaarli links on the sidebar 41- [octopress-shaarli](https://github.com/ahmet2mir/octopress-shaarli) - Octopress plugin to retrieve Shaarli Shaares on the sidebar
33- [Scuttle to Shaarli](https://github.com/q2apro/scuttle-to-shaarli) - Import bookmarks from Scuttle 42- [Scuttle to Shaarli](https://github.com/q2apro/scuttle-to-shaarli) - Import bookmarks from Scuttle
34- [Shaarli app for Cloudron](https://git.cloudron.io/cloudron/shaarli-app) - Effortlessly run Shaarli with the help of [Cloudron](https://cloudron.io/) [![Install](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=com.github.shaarli) 43- [Shaarli app for Cloudron](https://git.cloudron.io/cloudron/shaarli-app) - Effortlessly run Shaarli with the help of [Cloudron](https://cloudron.io/) [![Install](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=com.github.shaarli)
35- [Shaarli_ynh](https://github.com/YunoHost-Apps/shaarli_ynh) - Shaarli is available as a [Yunohost](https://yunohost.org) app [![Install Shaarli with YunoHost](https://install-app.yunohost.org/install-with-yunohost.png)](https://install-app.yunohost.org/?app=shaarli) 44- [Shaarli_ynh](https://github.com/YunoHost-Apps/shaarli_ynh) - Shaarli is available as a [Yunohost](https://yunohost.org) app [![Install Shaarli with YunoHost](https://install-app.yunohost.org/install-with-yunohost.png)](https://install-app.yunohost.org/?app=shaarli)
36- [pelican](https://blog.getpelican.com) static blog generator plugin to auto-post articles on a Shaarli instance: [shaarli_poster](https://github.com/getpelican/pelican-plugins/tree/master/shaarli_poster) 45- [pelican](https://blog.getpelican.com) static blog generator plugin to auto-post articles on a Shaarli instance: [shaarli_poster](https://github.com/getpelican/pelican-plugins/tree/master/shaarli_poster)
37 46
47
38### Mobile Apps 48### Mobile Apps
49
39- [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension. 50- [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension.
40- [Shaarli for Android](http://sebsauvage.net/links/?ZAyDzg) - Android application that adds Shaarli as a sharing provider 51- [Shaarli for Android](http://sebsauvage.net/links/?ZAyDzg) - Android application that adds Shaarli as a sharing provider
41- [Shaarlier for Android](https://github.com/dimtion/Shaarlier) - Android application to simply add links directly into your Shaarli 52- [Shaarlier for Android](https://github.com/dimtion/Shaarlier) - Android application to simply add Shaares directly into your Shaarli
42- [Stakali for Android](https://stakali.toneiv.eu) - Stakali is a personal bookmark manager which synchronizes with Shaarli 53- [Stakali for Android](https://stakali.toneiv.eu) - Stakali is a personal bookmark manager which synchronizes with Shaarli
43 54
55
44### Desktop Apps 56### Desktop Apps
57
45- [Ulauncher Extension](https://github.com/sebw/ulauncher-shaarli) - Ulauncher is an an application launcher for Linux, this extension allows research in your Shaarli 58- [Ulauncher Extension](https://github.com/sebw/ulauncher-shaarli) - Ulauncher is an an application launcher for Linux, this extension allows research in your Shaarli
46 59
60
47### Browser addons 61### Browser addons
62
48- [Shaarli Firefox Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli. 63- [Shaarli Firefox Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli.
49- [Shaarli Chrome Extension](https://github.com/octplane/Shiny-Shaarli) - toolbar button to share your current tab with Shaarli. 64- [Shaarli Chrome Extension](https://github.com/octplane/Shiny-Shaarli) - toolbar button to share your current tab with Shaarli.
50 65
66
51### Server apps 67### Server apps
68
52- [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content 69- [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content
53- [shaarli-river](https://github.com/mknexen/shaarli-river) - An aggregator for shaarlis with many features 70- [shaarli-river](https://github.com/mknexen/shaarli-river) - An aggregator for shaarlis with many features
54- [Shaarlo](https://github.com/DMeloni/shaarlo) - An aggregator for shaarlis with many features (a very popular running instance among French shaarliers: [shaarli.fr](http://shaarli.fr/)) 71- [Shaarlo](https://github.com/DMeloni/shaarlo) - An aggregator for shaarlis with many features
55- [Shaarlimages](https://github.com/BoboTiG/shaarlimages) - An image-oriented aggregator for Shaarlis 72- [Shaarlimages](https://github.com/BoboTiG/shaarlimages) - An image-oriented aggregator for Shaarlis
56- [mknexen/shaarli-api](https://github.com/mknexen/shaarli-api) - A REST API for Shaarli 73- [mknexen/shaarli-api](https://github.com/mknexen/shaarli-api) - A REST API for Shaarli
57- [Self dead link](https://framagit.org/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php) - Detect dead links on shaarli. This version use the database of shaarli. [Another version](https://framagit.org/qwertygc/shaarli-dev-code/blob/master/dead-link.php), can be used for other shaarli instances (but is more resource consuming). 74- [Self dead link](https://framagit.org/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php) - Detect dead links on shaarli. This version use the database of shaarli. [Another version](https://framagit.org/qwertygc/shaarli-dev-code/blob/master/dead-link.php), can be used for other shaarli instances (but is more resource consuming).
58- [Bookmark Archiver](https://github.com/pirate/bookmark-archiver) - Save an archived copy of all websites starred using browser bookmarks/Shaarli/Delicious/Instapaper/Unmark.it/Pocket/Pinboard. Outputs browseable html. 75- [Bookmark Archiver](https://github.com/pirate/bookmark-archiver) - Save an archived copy of all websites starred using browser bookmarks/Shaarli/Delicious/Instapaper/Unmark.it/Pocket/Pinboard. Outputs browseable html.
59 76
77
60## Alternatives to Shaarli 78## Alternatives to Shaarli
79
61See [awesome-selfhosted: bookmarks & link sharing](https://github.com/Kickball/awesome-selfhosted/#bookmarks--link-sharing). 80See [awesome-selfhosted: bookmarks & link sharing](https://github.com/Kickball/awesome-selfhosted/#bookmarks--link-sharing).
62 81
82
63## Community 83## Community
84
64- [Liens en vrac de sebsauvage](http://sebsauvage.net/links/) - the original Shaarli 85- [Liens en vrac de sebsauvage](http://sebsauvage.net/links/) - the original Shaarli
65- [A large list of Shaarlis](http://porneia.free.fr/pub/links/ou-est-shaarli.html) 86- [A large list of Shaarlis](http://porneia.free.fr/pub/links/ou-est-shaarli.html)
66- [A list of working Shaarli aggregators](https://raw.githubusercontent.com/Oros42/find_shaarlis/master/annuaires.json) 87- [A list of working Shaarli aggregators](https://raw.githubusercontent.com/Oros42/find_shaarlis/master/annuaires.json)
@@ -71,8 +92,17 @@ See [awesome-selfhosted: bookmarks & link sharing](https://github.com/Kickball/a
71- [Original revisions history](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history) 92- [Original revisions history](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
72- [Shaarli.fr/my](https://www.shaarli.fr/my.php) - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of [DMeloni](https://github.com/DMeloni) 93- [Shaarli.fr/my](https://www.shaarli.fr/my.php) - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of [DMeloni](https://github.com/DMeloni)
73 94
95
74### Articles and social media discussions 96### Articles and social media discussions
75- 2016-09-22 - Hacker News - https://news.ycombinator.com/item?id=12552176 97- 2020-04-05 - Hacker News - [Self-hosted instance of Shaarli - it is simple, fast and reliable](https://news.ycombinator.com/item?id=22780219)
98- 2016-10-10 - Framasoft - [MyFrama : vos favoris partout, avec vous, rien qu’à vous !](https://framablog.org/2016/10/10/myframa-vos-favoris-et-framasofteries-partout-avec-vous-rien-qua-vous/)
99- 2016-09-22 - Hacker News - [Shaarli – Personal, minimalist, database-free, bookmarking service (github.com)](https://news.ycombinator.com/item?id=12552176)
76- 2015-08-15 - Reddit - [Question about migrating from WordPress to Shaarli.](https://www.reddit.com/r/selfhosted/comments/3h3zwh/question_about_migrating_from_wordpress_to_shaarli/) 100- 2015-08-15 - Reddit - [Question about migrating from WordPress to Shaarli.](https://www.reddit.com/r/selfhosted/comments/3h3zwh/question_about_migrating_from_wordpress_to_shaarli/)
77- 2015-06-22 - Hacker News - https://news.ycombinator.com/item?id=9755366 101- 2015-06-22 - Hacker News - [Shaarli: Self-hosted del.icio.us alternative (sebsauvage.net)](https://news.ycombinator.com/item?id=9755366)
78- 2015-05-12 - Reddit - [shaarli - Self hosted Bookmarking / Delicious (PHP, MySQL)](https://www.reddit.com/r/selfhosted/comments/35pkkc/shaarli_self_hosted_bookmarking_delicious_php/) 102- 2015-05-12 - Reddit - [shaarli - Self hosted Bookmarking / Delicious (PHP, MySQL)](https://www.reddit.com/r/selfhosted/comments/35pkkc/shaarli_self_hosted_bookmarking_delicious_php/)
103- 2014-10-15 - OpenSource.com - [Five open source alternatives to popular web apps](https://opensource.com/life/14/10/five-open-source-alternatives-popular-web-apps)
104
105It also appears in the following recommendation lists:
106- [AlternativeTo](https://alternativeto.net/software/shaarli/)
107- [FramaLibre](https://framalibre.org/content/shaarli)
108- [Project Awesome: Selfhosted Bookmarks and Link Sharing](https://project-awesome.org/Kickball/awesome-selfhosted)
diff --git a/doc/md/Continuous-integration-tools.md b/doc/md/Continuous-integration-tools.md
deleted file mode 100644
index f7819d5a..00000000
--- a/doc/md/Continuous-integration-tools.md
+++ /dev/null
@@ -1,32 +0,0 @@
1## Local development
2A [`Makefile`](https://github.com/shaarli/Shaarli/blob/master/Makefile) is available to perform project-related operations:
3
4- Documentation - generate a local HTML copy of the GitHub wiki
5- [Static analysis](Static-analysis) - check that the code is compliant to PHP conventions
6- [Unit tests](Unit-tests) - ensure there are no regressions introduced by new commits
7
8## Automatic builds
9[Travis CI](http://docs.travis-ci.com/) is a Continuous Integration build server, that runs a build:
10
11- each time a commit is merged to the mainline (`master` branch)
12- each time a Pull Request is submitted or updated
13
14A build is composed of several jobs: one for each supported PHP version (see [Server requirements](Server requirements)).
15
16Each build job:
17
18- updates Composer
19- installs 3rd-party test dependencies with Composer
20- runs [Unit tests](Unit-tests)
21- runs ESLint check
22
23After all jobs have finished, Travis returns the results to GitHub:
24
25- a status icon represents the result for the `master` branch: [![](https://api.travis-ci.org/shaarli/Shaarli.svg)](https://travis-ci.org/shaarli/Shaarli)
26- Pull Requests are updated with the Travis result
27 - Green: all tests have passed
28 - Red: some tests failed
29 - Orange: tests are pending
30
31## Documentation
32[mkdocs](https://www.mkdocs.org/) is used to convert markdown documentation to HTML pages. The [public documentation](https://shaarli.readthedocs.io/en/master/) website is rendered and hosted by [readthedocs.org](https://readthedocs.org/). A copy of the documentation is also included in prebuilt [release archives](https://github.com/shaarli/Shaarli/releases) (`doc/html/` path in your Shaarli installation). To generate the HTML documentation locally, install a recent version of Python `setuptools` and run `make doc`.
diff --git a/doc/md/Development-guidelines.md b/doc/md/Development-guidelines.md
deleted file mode 100644
index 46b7c6f8..00000000
--- a/doc/md/Development-guidelines.md
+++ /dev/null
@@ -1,13 +0,0 @@
1## Development guidelines
2
3Please have a look at the following pages:
4
5- [Contributing to Shaarli](https://github.com/shaarli/Shaarli/tree/master/CONTRIBUTING.md)
6- [Static analysis](Static-analysis) - patches should try to stick to the
7[PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), especially:
8 - [PSR-1](http://www.php-fig.org/psr/psr-1/) - Basic Coding Standard
9 - [PSR-2](http://www.php-fig.org/psr/psr-2/) - Coding Style Guide
10- [Unit tests](Unit-tests)
11- Javascript linting - Shaarli uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript).
12Run `make eslint` to check JS style.
13- [GnuPG signature](GnuPG-signature) for tags/releases
diff --git a/doc/md/Directory-structure.md b/doc/md/Directory-structure.md
deleted file mode 100644
index c0b49393..00000000
--- a/doc/md/Directory-structure.md
+++ /dev/null
@@ -1,54 +0,0 @@
1## Directory structure
2
3Here is the directory structure of Shaarli and the purpose of the different files:
4
5```bash
6 index.php # Main program
7 application/ # Shaarli classes
8 ├── LinkDB.php
9
10 ...
11
12 └── Utils.php
13 tests/ # Shaarli unitary & functional tests
14 ├── LinkDBTest.php
15
16 ...
17
18 ├── utils # utilities to ease testing
19 │ └── ReferenceLinkDB.php
20 └── UtilsTest.php
21 assets/
22 ├── common/ # Assets shared by multiple themes
23 ├── ...
24 ├── default/ # Assets for the default template, before compilation
25 ├── fonts/ # Font files
26 ├── img/ # Images used by the default theme
27 ├── js/ # JavaScript files in ES6 syntax
28 ├── scss/ # SASS files
29 └── vintage/ # Assets for the vintage template, before compilation
30 └── ...
31 COPYING # Shaarli license
32 inc/ # static assets and 3rd party libraries
33 └── rain.tpl.class.php # RainTPL templating library
34 images/ # Images and icons used in Shaarli
35 data/ # data storage: bookmark database, configuration, logs, banlist...
36 ├── config.json.php # Shaarli configuration (login, password, timezone, title...)
37 ├── datastore.php # Your link database (compressed).
38 ├── ipban.php # IP address ban system data
39 ├── lastupdatecheck.txt # Update check timestamp file
40 └── log.txt # login/IPban log.
41 tpl/ # RainTPL templates for Shaarli. They are used to build the pages.
42 ├── default/ # Default Shaarli theme
43 ├── fonts/ # Font files
44 ├── img/ # Images
45 ├── js/ # JavaScript files compiled by Babel and compatible with all browsers
46 ├── css/ # CSS files compiled with SASS
47 └── vintage/ # Legacy Shaarli theme
48 └── ...
49 cache/ # thumbnails cache
50 # This directory is automatically created. You can erase it anytime you want.
51 tmp/ # Temporary directory for compiled RainTPL templates.
52 # This directory is automatically created. You can erase it anytime you want.
53 vendor/ # Third-party dependencies. This directory is created by Composer
54```
diff --git a/doc/md/Docker.md b/doc/md/Docker.md
new file mode 100644
index 00000000..c152fe92
--- /dev/null
+++ b/doc/md/Docker.md
@@ -0,0 +1,227 @@
1# Docker
2
3[Docker](https://docs.docker.com/get-started/overview/) is an open platform for developing, shipping, and running applications
4
5## Install Docker
6
7Install [Docker](https://docs.docker.com/engine/install/), by following the instructions relevant to your OS / distribution, and start the service. For example on [Debian](https://docs.docker.com/engine/install/debian/):
8
9```bash
10# update your package lists
11sudo apt update
12# remove old versions
13sudo apt-get remove docker docker-engine docker.io containerd runc
14# install requirements
15sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
16# add docker's GPG signing key
17curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
18# add the repository
19sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"
20# install docker engine
21sudo apt-get update
22sudo apt-get install docker-ce docker-ce-cli containerd.io
23# Start and enable Docker service
24sudo systemctl enable docker && sudo systemctl start docker
25# verify that Docker is properly configured
26sudo docker run hello-world
27```
28
29In order to run Docker commands as a non-root user, you must add the `docker` group to this user:
30
31```bash
32# Add docker group as secondary group
33sudo usermod -aG docker your-user
34# Reboot or logout
35# Then verify that Docker is properly configured, as "your-user"
36docker run hello-world
37```
38
39## Get and run a Shaarli image
40
41Shaarli images are available on [DockerHub](https://hub.docker.com/r/shaarli/shaarli/) `shaarli/shaarli`:
42
43- `latest`: latest branch (last release)
44- `stable`: stable branch (last release in previous major version)
45- `master`: master branch (development branch)
46
47These images are built automatically on DockerHub and rely on:
48
49- [Alpine Linux](https://www.alpinelinux.org/)
50- [PHP7-FPM](http://php-fpm.org/)
51- [Nginx](http://nginx.org/)
52
53Additional Dockerfiles are provided for the `arm32v7` platform, relying on [Linuxserver.io Alpine armhf images](https://hub.docker.com/r/lsiobase/alpine.armhf/). These images must be built using [`docker build`](https://docs.docker.com/engine/reference/commandline/build/) on an `arm32v7` machine or using an emulator such as [qemu](https://resin.io/blog/building-arm-containers-on-any-x86-machine-even-dockerhub/).
54
55Here is an example of how to run Shaarli latest image using Docker:
56
57```bash
58# download the 'latest' image from dockerhub
59docker pull shaarli/shaarli
60
61# create persistent data volumes/directories on the host
62docker volume create shaarli-data
63docker volume create shaarli-cache
64
65# create a new container using the Shaarli image
66# --detach: run the container in background
67# --name: name of the created container/instance
68# --publish: map the host's :8000 port to the container's :80 port
69# --rm: automatically remove the container when it exits
70# --volume: mount persistent volumes in the container ($volume_name:$volume_mountpoint)
71docker run --detach \
72 --name myshaarli \
73 --publish 8000:80 \
74 --rm \
75 --volume shaarli-data:/var/www/shaarli/data \
76 --volume shaarli-cache:/var/www/shaarli/cache \
77 shaarli/shaarli:latest
78
79# verify that the container is running
80docker ps | grep myshaarli
81
82# to completely remove the container
83docker stop myshaarli # stop the running container
84docker ps | grep myshaarli # verify the container is no longer running
85docker ps -a | grep myshaarli # verify the container is stopped
86docker rm myshaarli # destroy the container
87docker ps -a | grep myshaarli # verify th container has been destroyed
88
89```
90
91After running `docker run` command, your Shaarli instance should be available on the host machine at [localhost:8000](http://localhost:8000). In order to access your instance through a reverse proxy, we recommend using our [Docker Compose](#docker-compose) build.
92
93## Docker Compose
94
95A [Compose file](https://docs.docker.com/compose/compose-file/) is a common format for defining and running multi-container Docker applications.
96
97A `docker-compose.yml` file can be used to run a persistent/autostarted shaarli service using [Docker Compose](https://docs.docker.com/compose/) or in a [Docker stack](https://docs.docker.com/engine/reference/commandline/stack_deploy/).
98
99Shaarli provides configuration file for Docker Compose, that will setup a Shaarli instance, a [Træfik](https://containo.us/traefik/) instance (reverse proxy) with [Let's Encrypt](https://letsencrypt.org/) certificates, a Docker network, and volumes for Shaarli data and Træfik TLS configuration and certificates.
100
101Download docker-compose from the [release page](https://docs.docker.com/compose/install/):
102
103```bash
104$ sudo curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
105$ sudo chmod +x /usr/local/bin/docker-compose
106```
107
108To run Shaarli container and its reverse proxy, you can execute the following commands:
109
110```bash
111# create a new directory to store the configuration:
112$ mkdir shaarli && cd shaarli
113# Download the latest version of Shaarli's docker-compose.yml
114$ curl -L https://raw.githubusercontent.com/shaarli/Shaarli/latest/docker-compose.yml -o docker-compose.yml
115# Create the .env file and fill in your VPS and domain information
116# (replace <MY_SHAARLI_DOMAIN> and <MY_CONTACT_EMAIL> with your actual information)
117$ echo 'SHAARLI_VIRTUAL_HOST=shaarli.mydomain.org' > .env
118$ echo 'SHAARLI_LETSENCRYPT_EMAIL=admin@mydomain.org' >> .env
119# Pull the Docker images
120$ docker-compose pull
121# Run!
122$ docker-compose up -d
123```
124
125After a few seconds, you should be able to access your Shaarli instance at [https://shaarli.mydomain.org](https://shaarli.mydomain.org) (replace your own domain name).
126
127## Running dockerized Shaarli as a systemd service
128
129It is possible to start a dockerized Shaarli instance as a systemd service (systemd is the service management tool on several distributions). After installing Docker, use the following steps to run your shaarli container Shaarli to run on system start.
130
131As root, create `/etc/systemd/system/docker.shaarli.service`:
132
133```ini
134[Unit]
135Description=Shaarli Bookmark Manager Container
136After=docker.service
137Requires=docker.service
138
139
140[Service]
141Restart=always
142
143# Put any environment you want in an included file, like $host- or $domainname in this example
144EnvironmentFile=/etc/sysconfig/box-environment
145
146# It's just an example..
147ExecStart=/usr/bin/docker run \
148 -p 28010:80 \
149 --name ${hostname}-shaarli \
150 --hostname shaarli.${domainname} \
151 -v /srv/docker-volumes-local/shaarli-data:/var/www/shaarli/data:rw \
152 -v /etc/localtime:/etc/localtime:ro \
153 shaarli/shaarli:latest
154
155ExecStop=/usr/bin/docker rm -f ${hostname}-shaarli
156
157[Install]
158WantedBy=multi-user.target
159```
160
161```bash
162# reload systemd services definitions
163systemctl daemon-reload
164# start the servie and enable it a boot time
165systemctl enable docker.shaarli.service --now
166# verify that the service is running
167systemctl status docker.*
168# inspect system log if needed
169journalctl -f
170```
171
172
173
174## Docker cheatsheet
175
176```bash
177# pull/update an image
178$ docker pull shaarli/shaarli:release
179# run a container from an image
180$ docker run shaarli/shaarli:latest
181# list available images
182$ docker images ls
183# list running containers
184$ docker ps
185# list running AND stopped containers
186$ docker ps -a
187# run a command in a running container
188$ docker exec -ti <container-name-or-first-letters-of-id> bash
189# follow logs of a running container
190$ docker logs -f <container-name-or-first-letters-of-id>
191# delete unused images to free up disk space
192$ docker system prune --images
193# delete unused volumes to free up disk space (CAUTION all data in unused volumes will be lost)
194$ docker system prunt --volumes
195# delete unused containers
196$ docker system prune
197```
198
199
200## References
201
202- [Docker: using volumes](https://docs.docker.com/storage/volumes/)
203- [Dockerfile best practices](https://docs.docker.com/articles/dockerfile_best-practices/)
204- [Dockerfile reference](https://docs.docker.com/reference/builder/)
205- [DockerHub: GitHub automated build](https://docs.docker.com/docker-hub/github/)
206- [DockerHub: Repositories](https://docs.docker.com/userguide/dockerrepos/)
207- [DockerHub: Teams and organizations](https://docs.docker.com/docker-hub/orgs/)
208- [Get Docker CE for Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
209- [Install Docker Compose](https://docs.docker.com/compose/install/)
210- [Interactive Docker training portal](https://www.katacoda.com/courses/docker/) on [Katakoda](https://www.katacoda.com/)
211- [Service management: Nginx in the foreground](http://nginx.org/en/docs/ngx_core_module.html#daemon)
212- [Service management: Using supervisord](https://docs.docker.com/articles/using_supervisord/)
213- [Volumes](https://docs.docker.com/storage/volumes/)
214- [Volumes](https://docs.docker.com/userguide/dockervolumes/)
215- [Where are Docker images stored?](http://blog.thoward37.me/articles/where-are-docker-images-stored/)
216- [docker create](https://docs.docker.com/engine/reference/commandline/create/)
217- [Docker Documentation](https://docs.docker.com/)
218- [docker exec](https://docs.docker.com/engine/reference/commandline/exec/)
219- [docker images](https://docs.docker.com/engine/reference/commandline/images/)
220- [docker logs](https://docs.docker.com/engine/reference/commandline/logs/)
221- [docker logs](https://docs.docker.com/engine/reference/commandline/logs/)
222- [Docker Overview](https://docs.docker.com/engine/docker-overview/)
223- [docker ps](https://docs.docker.com/engine/reference/commandline/ps/)
224- [docker pull](https://docs.docker.com/engine/reference/commandline/pull/)
225- [docker run](https://docs.docker.com/engine/reference/commandline/run/)
226- [docker-compose logs](https://docs.docker.com/compose/reference/logs/)
227- Træfik: [Getting Started](https://docs.traefik.io/), [Docker backend](https://docs.traefik.io/configuration/backends/docker/), [Let's Encrypt](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/), [Docker image](https://hub.docker.com/_/traefik/) \ No newline at end of file
diff --git a/doc/md/Download-and-Installation.md b/doc/md/Download-and-Installation.md
deleted file mode 100644
index ec68762e..00000000
--- a/doc/md/Download-and-Installation.md
+++ /dev/null
@@ -1,124 +0,0 @@
1To install Shaarli, simply place the files in a directory under your webserver's
2Document Root (or directly at the document root).
3
4Also, please make sure your server is properly [configured](Server-configuration.md).
5
6Multiple releases branches are available:
7
8- latest (last release)
9- stable (previous major release)
10- master (development)
11
12Using one of the following methods:
13
14- by downloading full release archives including all dependencies
15- by downloading Github archives
16- by cloning the Git repository
17- using Docker: [see the documentation](docker/shaarli-images.md)
18
19--------------------------------------------------------------------------------
20
21## Latest release (recommended)
22
23### Download as an archive
24
25In most cases, you should download the latest Shaarli release from the [releases](https://github.com/shaarli/Shaarli/releases) page. Download our **shaarli-full** archive to include dependencies.
26
27The current latest released version is `v0.10.4`
28
29```bash
30$ wget https://github.com/shaarli/Shaarli/releases/download/v0.10.4/shaarli-v0.10.4-full.zip
31$ unzip shaarli-v0.10.4-full.zip
32$ mv Shaarli /path/to/shaarli/
33```
34
35### Using git
36
37Cloning using `git` or downloading Github branches as zip files requires additional steps:
38
39 * Install [Composer](Unit-tests.md#install_composer) to manage third-party [PHP dependencies](3rd-party-libraries.md#composer).
40 * Install [yarn](https://yarnpkg.com/lang/en/docs/install/) to build the frontend dependencies.
41 * Install [python3-virtualenv](https://pypi.python.org/pypi/virtualenv) to build the local HTML documentation.
42
43```
44$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/
45$ git clone -b latest https://github.com/shaarli/Shaarli.git .
46$ composer install --no-dev --prefer-dist
47$ make build_frontend
48$ make translate
49$ make htmldoc
50```
51
52--------------------------------------------------------------------------------
53
54## Stable version
55
56The stable version has been experienced by Shaarli users, and will receive security updates.
57
58
59### Download as an archive
60
61As a .zip archive:
62
63```bash
64$ wget https://github.com/shaarli/Shaarli/archive/stable.zip
65$ unzip stable.zip
66$ mv Shaarli-stable /path/to/shaarli/
67```
68
69As a .tar.gz archive :
70
71```bash
72$ wget https://github.com/shaarli/Shaarli/archive/stable.tar.gz
73$ tar xvf stable.tar.gz
74$ mv Shaarli-stable /path/to/shaarli/
75```
76
77### Using git
78
79Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
80
81```bash
82$ git clone https://github.com/shaarli/Shaarli.git -b stable /path/to/shaarli/
83# install/update third-party dependencies
84$ cd /path/to/shaarli/
85$ composer install --no-dev --prefer-dist
86```
87
88
89--------------------------------------------------------------------------------
90
91## Development version (mainline)
92
93_Use at your own risk!_
94
95Install [Composer](Unit-tests.md#install_composer) to manage Shaarli PHP dependencies,
96and [yarn](https://yarnpkg.com/lang/en/docs/install/)
97for front-end dependencies.
98
99To get the latest changes from the `master` branch:
100
101```bash
102# clone the repository
103$ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
104# install/update third-party dependencies
105$ cd /path/to/shaarli
106$ composer install --no-dev --prefer-dist
107$ make build_frontend
108$ make translate
109$ make htmldoc
110```
111
112-------------------------------------------------------------------------------
113
114## Finish Installation
115
116Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.
117
118![install screenshot](images/install-shaarli.png)
119
120Setup your Shaarli installation, and it's ready to use!
121
122## Updating Shaarli
123
124See [Upgrade and Migration](Upgrade-and-migration)
diff --git a/doc/md/FAQ.md b/doc/md/FAQ.md
deleted file mode 100644
index a2ec7d57..00000000
--- a/doc/md/FAQ.md
+++ /dev/null
@@ -1,46 +0,0 @@
1### Why did you create Shaarli ?
2
3I was a StumbleUpon user. Then I got fed up with they big toolbar. I switched to delicious, which was lighter, faster and more beautiful. Until Yahoo bought it. Then the export API broke all the time, delicious became slow and was ditched by Yahoo. I switched to Diigo, which is not bad, but does too much. And Diigo is sslllooooowww and their Firefox extension a bit buggy. And… oh… **their Firefox addon sends to Diigo every single URL you visit** (Don't believe me ? Use [Tamper Data](https://addons.mozilla.org/en-US/firefox/addon/tamper-data/) and open any page).
4
5Enough is enough. Saving simple links should not be a complicated heavy thing. I ditched them all and wrote my own: Shaarli. It's simple, but it does the job and does it well. And my data is not hosted on a foreign server, but on my server.
6
7### Why use Shaarli and not Delicious/Diigo ?
8
9With Shaarli:
10
11- The data is yours: It's hosted on your server.
12- Never fear of having your data locked-in.
13- Never fear to have your data sold to third party.
14- Your private links are not hosted on a third party server.
15- You are not tracked by browser addons (like Diigo does)
16- You can change the look and feel of the pages if you want.
17- You can change the behaviour of the program.
18- It's magnitude faster than most bookmarking services.
19
20### What does Shaarli mean?
21
22Shaarli stands for _shaaring_ your _links_.
23
24### My Shaarli is broken!
25First of all, ensure that both the [web server](Server-configuration) and
26[Shaarli](Shaarli-configuration) are correctly configured, and that your
27installation is [supported](Server-configuration).
28
29If everything looks right but the issue(s) remain(s), please:
30
31- take a look at the [troubleshooting](Troubleshooting) section
32- come [chat with us](https://gitter.im/shaarli/Shaarli) on Gitter, we'll be happy to help ;-)
33- browse active [issues](https://github.com/shaarli/Shaarli/issues) and [Pull Requests](https://github.com/shaarli/Shaarli/pulls)
34 - if you find one that is related to the issue, feel free to comment and provide additional details (host/Shaarli setup)
35 - else, [open a new issue](https://github.com/shaarli/Shaarli/issues/new), and provide information about the problem:
36 - _what happens?_ - display glitches, invalid data, security flaws...
37 - _what is your configuration?_ - OS, server version, activated extensions, web browser...
38 - _is it reproducible?_
39
40### Why not use a real database? Files are slow!
41
42Does browsing [this page](http://sebsauvage.net/links/) feel slow? Try browsing older pages, too.
43
44It's not slow at all, is it? And don't forget the database contains more than 16000 links, and it's on a shared host, with 32000 visitors/day for my website alone. And it's still damn fast. Why?
45
46The data file is only 3.7 Mb. It's read 99% of the time, and is probably already in the operation system disk cache. So generating a page involves no I/O at all most of the time.
diff --git a/doc/md/Installation.md b/doc/md/Installation.md
new file mode 100644
index 00000000..11b5da85
--- /dev/null
+++ b/doc/md/Installation.md
@@ -0,0 +1,78 @@
1# Installation
2
3Once your server is [configured](Server-configuration.md), install Shaarli:
4
5## From release ZIP
6
7To install Shaarli, simply place the files from the latest [release .zip archive](https://github.com/shaarli/Shaarli/releases) under your webserver's document root (directly at the document root, or in a subdirectory). Download the **shaarli-vX.X.X-full** archive to include dependencies.
8
9```bash
10wget https://github.com/shaarli/Shaarli/releases/download/v0.11.1/shaarli-v0.11.1-full.zip
11unzip shaarli-v0.11.1-full.zip
12sudo rsync -avP Shaarli/ /var/www/shaarli.mydomain.org/
13```
14
15## From sources
16
17These components are required to build Shaarli:
18
19- [Composer](dev/Development.md#install-composer) to manage third-party [PHP dependencies](dev/Development#third-party-libraries).
20- [yarn](https://yarnpkg.com/lang/en/docs/install/) to build frontend dependencies.
21- [python3-virtualenv](https://pypi.python.org/pypi/virtualenv) to build local HTML documentation.
22
23Clone the repository, either pointing to:
24
25- any [tagged release](https://github.com/shaarli/Shaarli/releases)
26- `latest`: the latest tagged release
27- `master`: development branch
28
29```bash
30# clone the branch/tag of your choice
31$ git clone -b latest https://github.com/shaarli/Shaarli.git /home/me/Shaarli
32# OR download/extract the tar.gz/zip: wget https://github.com/shaarli/Shaarli/archive/latest.tar.gz...
33
34# enter the directory
35$ cd /home/me/Shaarli
36# install 3rd-party PHP dependencies
37$ composer install --no-dev --prefer-dist
38# build frontend static assets
39$ make build_frontend
40# build translations
41$ make translate
42# build HTML documentation
43$ make htmldoc
44# copy the resulting shaarli directory under your webserver's document root
45$ rsync -avP /home/me/Shaarli/ /var/www/shaarli.mydomain.org/
46```
47
48## Set file permissions
49
50Regardless of the installation method, appropriate [file permissions](dev/Development.md#directory-structure) must be set:
51
52```bash
53sudo chown -R root:www-data /var/www/shaarli.mydomain.org
54sudo chmod -R g+rX /var/www/shaarli.mydomain.org
55sudo chmod -R g+rwX /var/www/shaarli.mydomain.org/{cache/,data/,pagecache/,tmp/}
56```
57
58## Using Docker
59
60[See the documentation](Docker.md)
61
62
63## Finish Installation
64
65Once Shaarli is downloaded and files have been placed at the correct location, open this location your web browser.
66
67Enter basic settings for your Shaarli installation, and it's ready to use!
68
69![](images/07-installation.jpg)
70
71Congratulations! Your Shaarli is now available at `https://shaarli.mydomain.org`.
72
73You can further [configure Shaarli](Shaarli-configuration.md), setup [Plugins](Plugins.md) or [additional software](Community-and-related-software.md).
74
75
76## Upgrading Shaarli
77
78See [Upgrade and Migration](Upgrade-and-migration)
diff --git a/doc/md/Link-structure.md b/doc/md/Link-structure.md
deleted file mode 100644
index 0a2d0f88..00000000
--- a/doc/md/Link-structure.md
+++ /dev/null
@@ -1,18 +0,0 @@
1## Link structure
2
3Every link available through the `LinkDB` object is represented as an array
4containing the following fields:
5
6 * `id` (integer): Unique identifier.
7 * `title` (string): Title of the link.
8 * `url` (string): URL of the link. Used for displayable links (without redirector, url encoding, etc.).
9 Can be absolute or relative for Notes.
10 * `real_url` (string): Real destination URL, can be redirected, encoded, etc.
11 * `shorturl` (string): Permalink small hash.
12 * `description` (string): Link text description.
13 * `private` (boolean): whether the link is private or not.
14 * `tags` (string): all link tags separated by a single space
15 * `thumbnail` (string|boolean): relative path of the thumbnail cache file, or false if there isn't any.
16 * `created` (DateTime): link creation date time.
17 * `updated` (DateTime): last modification date time.
18 \ No newline at end of file
diff --git a/doc/md/Plugins.md b/doc/md/Plugins.md
index 3e261815..a9f5f1a8 100644
--- a/doc/md/Plugins.md
+++ b/doc/md/Plugins.md
@@ -1,14 +1,13 @@
1## Plugin installation 1# Plugins
2 2
3There is a bunch of plugins shipped with Shaarli, where there is nothing to do to install them. 3## Installation
4 4
5If you want to install a third party plugin: 5For plugins shipped with Shaarli, no installation is required.
6 6
7- Download it. 7If you want to install a third party plugin, download it to the `plugins` directory in Shaarli's installation folder:
8- Put it in the `plugins` directory in Shaarli's installation folder.
9- Make sure you put it correctly:
10 8
11``` 9```bash
10# example directory structure
12| index.php 11| index.php
13| plugins/ 12| plugins/
14|---| custom_plugin/ 13|---| custom_plugin/
@@ -17,63 +16,47 @@ If you want to install a third party plugin:
17 16
18``` 17```
19 18
20 * Make sure your webserver can read and write the files in your plugin folder. 19Make sure your webserver can read and write the files in your plugin folder.
21 20
22## Plugin configuration
23 21
24In Shaarli's administration page (`Tools` link), go to `Plugin administration`. 22## Configuration
25 23
26Here you can enable and disable all plugins available, and configure them. 24From Shaarli's administration page (`Tools` link), go to `Plugin administration`. Here you can enable and disable all plugins available, and configure them.
27 25
28![administration screenshot](https://camo.githubusercontent.com/5da68e191969007492ca0fbeb25f3b2357b748cc/687474703a2f2f692e696d6775722e636f6d2f766837544643712e706e67) 26![administration screenshot](https://camo.githubusercontent.com/5da68e191969007492ca0fbeb25f3b2357b748cc/687474703a2f2f692e696d6775722e636f6d2f766837544643712e706e67)
29 27
30## Plugin order 28
29## Order
31 30
32In the plugin administration page, you can move enabled plugins to the top or bottom of the list. The first plugins in the list will be processed first. 31In the plugin administration page, you can move enabled plugins to the top or bottom of the list. The first plugins in the list will be processed first.
33 32
34This is important in case plugins are depending on each other. Read plugins README details for more information. 33This is important in case plugins depend on each other. Read plugins READMEs for more information.
35 34
36**Use case**: The (non existent) plugin `shaares_footer` adds a footer to every shaare in Markdown syntax. It needs to be processed *before* (higher in the list) the Markdown plugin. Otherwise its syntax won't be translated in HTML. 35**Use case**: The (non existent) plugin `shaares_footer` adds a footer to every shaare in Markdown syntax. It needs to be processed *before* (higher in the list) the Markdown plugin. Otherwise its syntax won't be translated in HTML.
37 36
38## File mode
39 37
40Enabled plugin are stored in your `config.json.php` parameters file, under the `array`: 38## Configuration file
41 39
42```php 40Enabled plugins are stored in your [Configuration file](Shaarli-configuration).
43$GLOBALS['config']['ENABLED_PLUGINS']
44```
45 41
46You can edit them manually here. 42## Usage
47Example:
48 43
49```php 44### Official plugins
50$GLOBALS['config']['ENABLED_PLUGINS'] = array(
51 'qrcode',
52 'archiveorg',
53 'wallabag',
54 'markdown',
55);
56```
57
58### Plugin usage
59
60#### Official plugins
61 45
62Usage of each plugin is documented in it's README file: 46Usage of each plugin is documented in it's README file:
63 47
64 * `addlink-toolbar`: Adds the addlink input on the linklist page 48 * `addlink-toolbar`: Adds the addlink input on the Shaares list page
65 * `archiveorg`: For each link, add an Archive.org icon 49 * `archiveorg`: For each Shaare, add a link to the archived page on Archive.org
66 * `default_colors`: Override default theme colors. 50 * `default_colors`: Override default theme colors.
67 * `isso`: Let visitor comment your shaares on permalinks with Isso. 51 * `isso`: Let visitor comment your shaares on permalinks with Isso.
68 * [`markdown`](https://github.com/shaarli/Shaarli/blob/master/plugins/markdown/README.md): Render shaare description with Markdown syntax. 52 * [`markdown`](https://github.com/shaarli/Shaarli/blob/master/plugins/markdown/README.md): Render shaare description with Markdown syntax.
69 * `piwik`: A plugin that adds Piwik tracking code to Shaarli pages. 53 * `piwik`: A plugin that adds Piwik tracking code to Shaarli pages.
70 * [`playvideos`](https://github.com/shaarli/Shaarli/blob/master/plugins/playvideos/README.md): Add a button in the toolbar allowing to watch all videos. 54 * [`playvideos`](https://github.com/shaarli/Shaarli/blob/master/plugins/playvideos/README.md): Add a button in the toolbar allowing to watch all videos.
71 * `pubsubhubbub`: Enable PubSubHubbub feed publishing 55 * `pubsubhubbub`: Enable PubSubHubbub feed publishing
72 * `qrcode`: For each link, add a QRCode icon. 56 * `qrcode`: For each Shaare, add a QRCode icon.
73 * [`wallabag`](https://github.com/shaarli/Shaarli/blob/master/plugins/wallabag/README.md): For each link, add a Wallabag icon to save it in your instance. 57 * [`wallabag`](https://github.com/shaarli/Shaarli/blob/master/plugins/wallabag/README.md): For each Shaare, add a Wallabag icon to save it in your instance.
74
75 58
76 59
77#### Third party plugins 60### Third party plugins
78 61
79See [Community & related software](https://shaarli.readthedocs.io/en/master/Community-&-Related-software/) 62See [Community & related software](https://shaarli.readthedocs.io/en/master/Community-and-Related-software/)
diff --git a/doc/md/REST-API.md b/doc/md/REST-API.md
index 11bd1cd2..01071d8e 100644
--- a/doc/md/REST-API.md
+++ b/doc/md/REST-API.md
@@ -1,101 +1,24 @@
1## Usage and Prerequisites 1# REST API
2 2
3See the [REST API documentation](http://shaarli.github.io/api-documentation/) 3## Server requirements
4for a list of available endpoints and parameters.
5 4
6Please ensure that your server meets the 5See the **[REST API documentation](http://shaarli.github.io/api-documentation/)** for a list of available endpoints and parameters.
7[requirements](Server-configuration#prerequisites) and is properly 6
8[configured](Server-configuration): 7Please ensure that your server meets the requirements and is properly [configured](Server-configuration):
9 8
10- URL rewriting is enabled (see specific Apache and Nginx sections) 9- URL rewriting is enabled (see specific Apache and Nginx sections)
11- the server's timezone is properly defined 10- the server's timezone is properly defined
12- the server's clock is synchronized with 11- the server's clock is synchronized with [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol)
13 [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol)
14
15The host where the API client is invoked should also be synchronized with NTP,
16see [token expiration](#payload).
17
18## Authentication
19
20All requests to Shaarli's API must include a JWT token to verify their authenticity.
21
22This token has to be included as an HTTP header called `Authentication: Bearer <jwt token>`.
23
24JWT resources :
25
26- [jwt.io](https://jwt.io) (including a list of client per language).
27- RFC : https://tools.ietf.org/html/rfc7519
28- https://float-middle.com/json-web-tokens-jwt-vs-sessions/
29- HackerNews thread: https://news.ycombinator.com/item?id=11929267
30
31
32### Shaarli JWT Token
33
34JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64:
35
36```
37[header].[payload].[signature]
38```
39
40#### Header
41
42Shaarli only allow one hash algorithm, so the header will always be the same:
43
44```json
45{
46 "typ": "JWT",
47 "alg": "HS512"
48}
49```
50
51Encoded in base64, it gives:
52
53```
54ewogICAgICAgICJ0eXAiOiAiSldUIiwKICAgICAgICAiYWxnIjogIkhTNTEyIgogICAgfQ==
55```
56
57#### Payload
58
59**Token expiration**
60
61To avoid infinite token validity, JWT tokens must include their creation date
62in UNIX timestamp format (timezone independent - UTC) under the key `iat` (issued at).
63This token will be valid during **9 minutes**.
64
65```json
66{
67 "iat": 1468663519
68}
69```
70
71See [RFC reference](https://tools.ietf.org/html/rfc7519#section-4.1.6).
72
73 12
74#### Signature 13The host where the API client is invoked should also be synchronized with NTP, see _payload/token expiration_
75
76The signature authenticate the token validity. It contains the base64 of the header and the body, separated by a dot `.`, hashed in SHA512 with the API secret available in Shaarli administration page.
77
78Signature example with PHP:
79
80```php
81$content = base64_encode($header) . '.' . base64_encode($payload);
82$signature = hash_hmac('sha512', $content, $secret);
83```
84 14
85 15
86## Clients and examples 16## Clients and examples
87### Android, Java, Kotlin
88
89- [Android client example with Kotlin](https://gitlab.com/snippets/1665808)
90 by [Braincoke](https://github.com/Braincoke)
91
92### Javascript, NodeJS
93 17
94- [shaarli-client](https://www.npmjs.com/package/shaarli-client) 18- **[python-shaarli-client](https://github.com/shaarli/python-shaarli-client)** - the reference API client ([Documentation](http://python-shaarli-client.readthedocs.io/en/latest/))
95 ([source code](https://github.com/laBecasse/shaarli-client)) 19- [shaarli-client](https://www.npmjs.com/package/shaarli-client) - NodeJs client ([source code](https://github.com/laBecasse/shaarli-client)) by [laBecasse](https://github.com/laBecasse)
96 by [laBecasse](https://github.com/laBecasse) 20- [Android client example with Kotlin](https://gitlab.com/snippets/1665808) by [Braincoke](https://github.com/Braincoke)
97 21
98### PHP
99 22
100This example uses the [PHP cURL](http://php.net/manual/en/book.curl.php) library. 23This example uses the [PHP cURL](http://php.net/manual/en/book.curl.php) library.
101 24
@@ -145,13 +68,57 @@ function getInfo($baseUrl, $secret) {
145var_dump(getInfo($baseUrl, $secret)); 68var_dump(getInfo($baseUrl, $secret));
146``` 69```
147 70
71## Implementation
72
73### Authentication
74
75- All requests to Shaarli's API must include a **JWT token** to verify their authenticity.
76- This token must be included as an HTTP header called `Authentication: Bearer <jwt token>`.
77- JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64:
78
79```
80[header].[payload].[signature]
81```
82
83##### Header
84
85Shaarli only allow one hash algorithm, so the header will always be the same:
86
87```json
88{
89 "typ": "JWT",
90 "alg": "HS512"
91}
92```
93
94Encoded in base64, it gives:
148 95
149### Python 96```
97ewogICAgICAgICJ0eXAiOiAiSldUIiwKICAgICAgICAiYWxnIjogIkhTNTEyIgogICAgfQ==
98```
99
100##### Payload
101
102Token expiration: To avoid infinite token validity, JWT tokens must include their creation date in UNIX timestamp format (timezone independent - UTC) under the key `iat` (issued at) field ([1](https://tools.ietf.org/html/rfc7519#section-4.1.6)). This token will be valid during **9 minutes**.
103
104```json
105{
106 "iat": 1468663519
107}
108```
109
110##### Signature
111
112The signature authenticates the token validity. It contains the base64 of the header and the body, separated by a dot `.`, hashed in SHA512 with the API secret available in Shaarli administration page.
113
114Example signature with PHP:
115
116```php
117$content = base64_encode($header) . '.' . base64_encode($payload);
118$signature = hash_hmac('sha512', $content, $secret);
119```
150 120
151See the reference API client:
152 121
153- [Documentation](http://python-shaarli-client.readthedocs.io/en/latest/) on ReadTheDocs
154- [python-shaarli-client](https://github.com/shaarli/python-shaarli-client) on Github
155 122
156## Troubleshooting 123## Troubleshooting
157 124
@@ -171,3 +138,13 @@ to get the actual error message in the HTTP response body with:
171 } 138 }
172} 139}
173``` 140```
141
142## References
143
144- [jwt.io](https://jwt.io) (including a list of client per language).
145- [RFC - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)
146- [JSON Web Tokens (JWT) vs Sessions](https://float-middle.com/json-web-tokens-jwt-vs-sessions/), [HackerNews thread](https://news.ycombinator.com/item?id=11929267)
147
148
149
150
diff --git a/doc/md/RSS-feeds.md b/doc/md/RSS-feeds.md
deleted file mode 100644
index d943218e..00000000
--- a/doc/md/RSS-feeds.md
+++ /dev/null
@@ -1,28 +0,0 @@
1### Feeds options
2
3Feeds are available in ATOM with `?do=atom` and RSS with `do=RSS`.
4
5Options:
6
7- You can use `permalinks` in the feed URL to get permalink to Shaares instead of direct link to shaared URL.
8 - E.G. `https://my.shaarli.domain/?do=atom&permalinks`.
9- You can use `nb` parameter in the feed URL to specify the number of Shaares you want in a feed (default if not specified: `50`). The keyword `all` is available if you want everything.
10 - `https://my.shaarli.domain/?do=atom&permalinks&nb=42`
11 - `https://my.shaarli.domain/?do=atom&permalinks&nb=all`
12
13### RSS Feeds or Picture Wall for a specific search/tag
14
15It is possible to filter RSS/ATOM feeds and Picture Wall on a Shaarli to **only display results of a specific search, or for a specific tag**.
16
17For example, if you want to subscribe only to links tagged `photography`:
18
19- Go to the desired Shaarli instance.
20- Search for the `photography` tag in the _Filter by tag_ box. Links tagged `photography` are displayed.
21- Click on the `RSS Feed` button.
22- You are presented with an RSS feed showing only these links. Subscribe to it to receive only updates with this tag.
23- The same method **also works for a full-text search** (_Search_ box) **and for the Picture Wall** (want to only see pictures about `nature`?)
24- You can also build the URLs manually:
25 - `https://my.shaarli.domain/?do=rss&searchtags=nature`
26 - `https://my.shaarli.domain/links/?do=picwall&searchterm=poney`
27
28![](images/rss-filter-1.png) ![](images/rss-filter-2.png)
diff --git a/doc/md/Release-Shaarli.md b/doc/md/Release-Shaarli.md
deleted file mode 100644
index e22eabc9..00000000
--- a/doc/md/Release-Shaarli.md
+++ /dev/null
@@ -1,161 +0,0 @@
1See [Git - Maintaining a project - Tagging your
2releases](http://git-scm.com/book/en/v2/Distributed-Git-Maintaining-a-Project#Tagging-Your-Releases).
3
4## Prerequisites
5This guide assumes that you have:
6
7- a GPG key matching your GitHub authentication credentials
8 - i.e., the email address identified by the GPG key is the same as the one in your `~/.gitconfig`
9- a GitHub fork of Shaarli
10- a local clone of your Shaarli fork, with the following remotes:
11 - `origin` pointing to your GitHub fork
12 - `upstream` pointing to the main Shaarli repository
13- maintainer permissions on the main Shaarli repository, to:
14 - push the signed tag
15 - create a new release
16- [Composer](https://getcomposer.org/) needs to be installed
17- The [venv](https://docs.python.org/3/library/venv.html) Python 3 module needs to be installed for HTML documentation generation.
18
19## GitHub release draft and `CHANGELOG.md`
20See http://keepachangelog.com/en/0.3.0/ for changelog formatting.
21
22### GitHub release draft
23GitHub allows drafting the release note for the upcoming release, from the [Releases](https://github.com/shaarli/Shaarli/releases) page. This way, the release note can be drafted while contributions are merged to `master`.
24
25### `CHANGELOG.md`
26This file should contain the same information as the release note draft for the upcoming version.
27
28Update it to:
29
30- add new entries (additions, fixes, etc.)
31- mark the current version as released by setting its date and link
32- add a new section for the future unreleased version
33
34```bash
35$ cd /path/to/shaarli
36
37$ nano CHANGELOG.md
38
39[...]
40## vA.B.C - UNRELEASED
41TBA
42
43## [vX.Y.Z](https://github.com/shaarli/Shaarli/releases/tag/vX.Y.Z) - YYYY-MM-DD
44[...]
45```
46
47
48## Increment the version code, update docs, create and push a signed tag
49### Update the list of Git contributors
50```bash
51$ make authors
52$ git commit -s -m "Update AUTHORS"
53```
54
55### Create and merge a Pull Request
56This one is pretty straightforward ;-)
57
58### Bump Shaarli version to v0.x branch
59
60```bash
61$ git checkout master
62$ git fetch upstream
63$ git pull upstream master
64
65# IF the branch doesn't exists
66$ git checkout -b v0.5
67# OR if the branch already exists
68$ git checkout v0.5
69$ git rebase upstream/master
70
71# Bump shaarli version from dev to 0.5.0, **without the `v`**
72$ vim shaarli_version.php
73$ git add shaarli_version
74$ git commit -s -m "Bump Shaarli version to v0.5.0"
75$ git push upstream v0.5
76```
77
78### Create and push a signed tag
79```bash
80# update your local copy
81$ git checkout v0.5
82$ git fetch upstream
83$ git pull upstream v0.5
84
85# create a signed tag
86$ git tag -s -m "Release v0.5.0" v0.5.0
87
88# push it to "upstream"
89$ git push --tags upstream
90```
91
92### Verify a signed tag
93[`v0.5.0`](https://github.com/shaarli/Shaarli/releases/tag/v0.5.0) is the first GPG-signed tag pushed on the Community Shaarli.
94
95Let's have a look at its signature!
96
97```bash
98$ cd /path/to/shaarli
99$ git fetch upstream
100
101# get the SHA1 reference of the tag
102$ git show-ref tags/v0.5.0
103f7762cf803f03f5caf4b8078359a63783d0090c1 refs/tags/v0.5.0
104
105# verify the tag signature information
106$ git verify-tag f7762cf803f03f5caf4b8078359a63783d0090c1
107gpg: Signature made Thu 30 Jul 2015 11:46:34 CEST using RSA key ID 4100DF6F
108gpg: Good signature from "VirtualTam <virtualtam@flibidi.net>" [ultimate]
109```
110
111## Publish the GitHub release
112### Update release badges
113Update `README.md` so version badges display and point to the newly released Shaarli version(s), in the `master` branch.
114
115### Create a GitHub release from a Git tag
116From the previously drafted release:
117
118- edit the release notes (if needed)
119- specify the appropriate Git tag
120- publish the release
121- profit!
122
123### Generate and upload all-in-one release archives
124Users with a shared hosting may have:
125
126- no SSH access
127- no possibility to install PHP packages or server extensions
128- no possibility to run scripts
129
130To ease Shaarli installations, it is possible to generate and upload additional release archives,
131that will contain Shaarli code plus all required third-party libraries.
132
133**From the `v0.5` branch:**
134
135```bash
136$ make release_archive
137```
138
139This will create the following archives:
140
141- `shaarli-vX.Y.Z-full.tar`
142- `shaarli-vX.Y.Z-full.zip`
143
144The archives need to be manually uploaded on the previously created GitHub release.
145
146### Update `stable` and `latest` branches
147
148```
149$ git checkout latest
150# latest release
151$ git merge v0.5.0
152# fix eventual conflicts
153$ make test
154$ git push upstream latest
155$ git checkout stable
156# latest previous major
157$ git merge v0.4.5
158# fix eventual conflicts
159$ make test
160$ git push upstream stable
161```
diff --git a/doc/md/Reverse-proxy.md b/doc/md/Reverse-proxy.md
new file mode 100644
index 00000000..b7e347d5
--- /dev/null
+++ b/doc/md/Reverse-proxy.md
@@ -0,0 +1,141 @@
1# Reverse proxy
2
3If Shaarli is hosted on a server behind a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) (i.e. there is a proxy server between clients and the web server hosting Shaarli), configure it accordingly. See [Reverse proxy](Reverse-proxy.md) configuration. In this example:
4
5- The Shaarli application server exposes port `10080` to the proxy (for example docker container started with `--publish 127.0.0.1:10080:80`).
6- The Shaarli application server runs at `127.0.0.1` (container). Replace with the server's IP address if running on a different machine.
7- Shaarli's Fully Qualified Domain Name (FQDN) is `shaarli.mydomain.org`.
8- No HTTPS is setup on the application server, SSL termination is done at the reverse proxy.
9
10In your [Shaarli configuration](Shaarli-configuration) `data/config.json.php`, add the public IP of your proxy under `security.trusted_proxies`.
11
12See also [proxy-related](https://github.com/shaarli/Shaarli/issues?utf8=%E2%9C%93&q=label%3Aproxy+) issues.
13
14
15## Apache
16
17```apache
18<VirtualHost *:80>
19 ServerName shaarli.mydomain.org
20
21 # For SSL/TLS certificates acquired with certbot or self-signed certificates
22 # Redirect HTTP requests to HTTPS, except Let's Encrypt ACME challenge requests
23 RewriteEngine on
24 RewriteRule ^.well-known/acme-challenge/ - [L]
25 RewriteCond %{HTTP_HOST} =shaarli.mydomain.org
26 RewriteRule ^ https://shaarli.mydomain.org%{REQUEST_URI} [END,NE,R=permanent]
27</VirtualHost>
28
29# SSL/TLS configuration for Let's Encrypt certificates managed with mod_md
30#MDomain shaarli.mydomain.org
31#MDCertificateAgreement accepted
32#MDContactEmail admin@shaarli.mydomain.org
33#MDPrivateKeys RSA 4096
34
35<VirtualHost *:443>
36 ServerName shaarli.mydomain.org
37
38 # SSL/TLS configuration for Let's Encrypt certificates acquired with certbot standalone
39 SSLEngine on
40 SSLCertificateFile /etc/letsencrypt/live/shaarli.mydomain.org/fullchain.pem
41 SSLCertificateKeyFile /etc/letsencrypt/live/shaarli.mydomain.org/privkey.pem
42 # Let's Encrypt settings from https://github.com/certbot/certbot/blob/master/certbot-apache/certbot_apache/_internal/tls_configs/current-options-ssl-apache.conf
43 SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
44 SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
45 SSLHonorCipherOrder off
46 SSLSessionTickets off
47 SSLOptions +StrictRequire
48
49 # SSL/TLS configuration for self-signed certificates
50 #SSLEngine on
51 #SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
52 #SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
53
54 # let the proxied shaarli server/container know HTTPS URLs should be served
55 RequestHeader set X-Forwarded-Proto "https"
56
57 # send the original SERVER_NAME to the proxied host
58 ProxyPreserveHost On
59
60 # pass requests to the proxied host
61 # sets X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Server headers
62 ProxyPass / http://127.0.0.1:10080/
63 ProxyPassReverse / http://127.0.0.1:10080/
64</VirtualHost>
65```
66
67
68## HAProxy
69
70
71```conf
72global
73 [...]
74
75defaults
76 [...]
77
78frontend http-in
79 bind :80
80 redirect scheme https code 301 if !{ ssl_fc }
81 bind :443 ssl crt /path/to/cert.pem
82 default_backend shaarli
83
84backend shaarli
85 mode http
86 option http-server-close
87 option forwardfor
88 reqadd X-Forwarded-Proto: https
89 server shaarli1 127.0.0.1:10080
90```
91
92- [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/)
93
94## Nginx
95
96
97```nginx
98http {
99 [...]
100
101 index index.html index.php;
102
103 root /home/john/web;
104 access_log /var/log/nginx/access.log combined;
105 error_log /var/log/nginx/error.log;
106
107 server {
108 listen 80;
109 server_name shaarli.mydomain.org;
110 # redirect HTTP to HTTPS
111 return 301 https://shaarli.mydomain.org$request_uri;
112 }
113
114 server {
115 listen 443 ssl http2;
116 server_name shaarli.mydomain.org;
117
118 ssl_certificate /path/to/certificate
119 ssl_certificate_key /path/to/private/key
120
121 location / {
122 proxy_set_header X-Real-IP $remote_addr;
123 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
124 proxy_set_header X-Forwarded-Proto $scheme;
125 proxy_set_header X-Forwarded-Host $host;
126
127 # pass requests to the proxied host
128 proxy_pass http://localhost:10080/;
129 proxy_set_header Host $host;
130 proxy_connect_timeout 30s;
131 proxy_read_timeout 120s;
132 }
133 }
134}
135```
136
137## References
138
139- [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto)
140- [`X-Forwarded-Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host)
141- [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
diff --git a/doc/md/Security.md b/doc/md/Security.md
deleted file mode 100644
index 65db4225..00000000
--- a/doc/md/Security.md
+++ /dev/null
@@ -1,25 +0,0 @@
1## Client browser
2- Shaarli relies on `HTTP_REFERER` for some functions (like redirects and clicking on tags). If you have disabled or masqueraded `HTTP_REFERER` in your browser, some features of Shaarli may not work
3
4## Server and sessions
5- Directories are protected using `.htaccess` files
6- Forms are protected against XSRF (Cross-site requests forgery):
7 - Forms which act on data (save,delete…) contain a token generated by the server.
8 - Any posted form which does not contain a valid token is rejected.
9 - Any token can only be used once.
10 - Tokens are attached to the session and cannot be reused in another session.
11- Sessions automatically expire after 60 minutes.
12- Sessions are protected against hijacking: the session ID cannot be used from a different IP address.
13
14## Shaarli datastore and configuration
15- The password is salted, hashed and stored in the data subdirectory, in a PHP file, and protected by htaccess. Even if the webserver does not support htaccess, the hash is not readable by URL. Even if the .php file is stolen, the password cannot deduced from the hash. The salt prevents rainbow-tables attacks.
16- Links are stored as an associative array which is serialized, compressed (with deflate), base64-encoded and saved as a comment in a `.php` file.
17- Even if the server does not support `.htaccess` files, the data file will still not be readable by URL.
18- The database looks like this:
19
20```php
21<?php /* zP1ZjxxJtiYIvvevEPJ2lDOaLrZv7o...
22...ka7gaco/Z+TFXM2i7BlfMf8qxpaSSYfKlvqv/x8= */ ?>
23```
24
25- Small hashes are used to make a link to an entry in Shaarli. They are unique. In fact, the date of the items (eg. `20110923_150523`) is hashed with CRC32, then converted to base64 and some characters are replaced. They are always 6 characters longs and use only `A-Z a-z 0-9 - _` and `@`.
diff --git a/doc/md/Server-configuration.md b/doc/md/Server-configuration.md
index f9ea2ed2..4e74d80b 100644
--- a/doc/md/Server-configuration.md
+++ b/doc/md/Server-configuration.md
@@ -1,20 +1,48 @@
1# Server configuration
1 2
2- [Prerequisites](#prerequisistes) 3## Requirements
3- [Apache](#apache)
4- [Nginx](#nginx)
5- [Proxies](#proxies)
6- [See also](#see-also)
7 4
8## Prerequisites 5### Operating system and web server
9### Shaarli
10 6
11- A web server and PHP interpreter module/service have been installed. 7Shaarli can be hosted on dedicated/virtual servers, or shared hosting.
12- You have write access to the Shaarli installation directory. 8
13- The correct read/write permissions have been granted to the web server user and group. 9You need write access to the Shaarli installation directory - you should have received instructions from your hosting provider on how to connect to the server using SSH (or FTP for shared hosts).
14- Your PHP interpreter is compatible with supported PHP versions: 10
11Examples in this documentation are given for [Debian](https://www.debian.org/), a GNU/Linux distribution widely used in server environments. Please adapt them to your specific Linux distribution.
12
13A $5/month VPS (1 CPU, 1 GiB RAM and 25 GiB SSD) will run any Shaarli installation without problems. Some hosting providers: [DigitalOcean](https://www.digitalocean.com/) ([1](https://www.digitalocean.com/docs/droplets/overview/), [2](https://www.digitalocean.com/pricing/), [3](https://www.digitalocean.com/docs/droplets/how-to/create/), [4](https://www.digitalocean.com/docs/droplets/how-to/add-ssh-keys/), [5](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-debian-8), [6](https://www.digitalocean.com/community/tutorials/an-introduction-to-securing-your-linux-vps)), [Gandi](https://www.gandi.net/en), [OVH](https://www.ovh.co.uk/), [RackSpace](https://www.rackspace.com/), etc.
14
15
16### Network and domain name
17
18Try to host the server in a region that is geographically close to your users.
19
20A **domain name** ([DNS record](https://opensource.com/article/17/4/introduction-domain-name-system-dns)) pointing to the server's public IP address is required to obtain a SSL/TLS certificate and setup HTTPS to secure client traffic to your Shaarli instance.
21
22You can obtain a domain name from a [registrar](https://en.wikipedia.org/wiki/Domain_name_registrar) ([1](https://www.ovh.co.uk/domains), [2](https://www.gandi.net/en/domain)), or from free subdomain providers ([1](https://freedns.afraid.org/)). If you don't have a domain name, please set up a private domain name ([FQDN](ttps://en.wikipedia.org/wiki/Fully_qualified_domain_name)) in your clients' [hosts files](https://en.wikipedia.org/wiki/Hosts_(file)) to access the server (direct access by IP address can result in unexpected behavior).
23
24Setup a **firewall** (using `iptables`, [ufw](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-debian-10), [fireHOL](https://firehol.org/) or any frontend of your choice) to deny all incoming traffic except `tcp/80` and `tcp/443`, which are needed to access the web server (and any other posrts you might need, like SSH). If the server is in a private network behind a NAT, ensure these **ports are forwarded** to the server.
25
26Shaarli makes outbound HTTP/HTTPS connections to websites you bookmark to fetch page information (title, thumbnails), the server must then have access to the Internet as well, and a working DNS resolver.
27
28
29### Screencast
30
31Here is a screencast of the installation procedure
32
33[![asciicast](https://asciinema.org/a/z3RXxcJIRgWk0jM2ws6EnUFgO.svg)](https://asciinema.org/a/z3RXxcJIRgWk0jM2ws6EnUFgO)
34
35--------------------------------------------------------------------------------
36
37### PHP
38
39Supported PHP versions:
15 40
16Version | Status | Shaarli compatibility 41Version | Status | Shaarli compatibility
17:---:|:---:|:---: 42:---:|:---:|:---:
438.0 | Supported | Yes
447.4 | Supported | Yes
457.3 | Supported | Yes
187.2 | Supported | Yes 467.2 | Supported | Yes
197.1 | Supported | Yes 477.1 | Supported | Yes
207.0 | EOL: 2018-12-03 | Yes (up to Shaarli 0.10.x) 487.0 | EOL: 2018-12-03 | Yes (up to Shaarli 0.10.x)
@@ -23,71 +51,132 @@ Version | Status | Shaarli compatibility
235.4 | EOL: 2015-09-14 | Yes (up to Shaarli 0.8.x) 515.4 | EOL: 2015-09-14 | Yes (up to Shaarli 0.8.x)
245.3 | EOL: 2014-08-14 | Yes (up to Shaarli 0.8.x) 525.3 | EOL: 2014-08-14 | Yes (up to Shaarli 0.8.x)
25 53
26- The following PHP extensions are installed on the server: 54Required PHP extensions:
27 55
28Extension | Required? | Usage 56Extension | Required? | Usage
29---|:---:|--- 57---|:---:|---
30[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS 58[`openssl`](http://php.net/manual/en/book.openssl.php) | required | OpenSSL, HTTPS
31[`php-json`](http://php.net/manual/en/book.json.php) | required | configuration parsing 59[`php-json`](http://php.net/manual/en/book.json.php) | required | configuration parsing
60[`php-simplexml`](https://www.php.net/manual/en/book.simplexml.php) | required | REST API (Slim framework)
32[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support 61[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
33[`php-gd`](http://php.net/manual/en/book.image.php) | optional | required to use thumbnails 62[`php-gd`](http://php.net/manual/en/book.image.php) | optional | required to use thumbnails
34[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`) 63[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
35[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way 64[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
36[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster) 65[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)
37--------------------------------------------------------------------------------
38 66
39### SSL/TLS configuration 67Some [plugins](Plugins.md) may require additional configuration.
68
69- [PHP: Supported versions](http://php.net/supported-versions.php)
70- [PHP: Unsupported versions (EOL/End-of-life)](http://php.net/eol.php)
71- [PHP 7 Changelog](http://php.net/ChangeLog-7.php)
72- [PHP 5 Changelog](http://php.net/ChangeLog-5.php)
73- [PHP: Bugs](https://bugs.php.net/)
74
75
76## SSL/TLS (HTTPS)
40 77
41To setup HTTPS / SSL on your webserver (recommended), you must generate a public/private **key pair** and a **certificate**, and install, configure and activate the appropriate **webserver SSL extension**. 78We recommend setting up [HTTPS](https://en.wikipedia.org/wiki/HTTPS) (SSL/[TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security)) on your webserver for secure communication between clients and the server.
42 79
43#### Let's Encrypt 80### Let's Encrypt
44 81
45[Let's Encrypt](https://en.wikipedia.org/wiki/Let%27s_Encrypt) is a certificate authority that provides free TLS/X.509 certificates via an automated process. 82For public-facing web servers this can be done using free SSL/TLS certificates from [Let's Encrypt](https://en.wikipedia.org/wiki/Let's_Encrypt), a non-profit certificate authority provididing free certificates.
46 83
47 * Install `certbot` using the appropriate method described on https://certbot.eff.org/. 84 - [How to secure Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-debian-10)
48 85 - [How to secure Nginx with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-debian-10)
49Location of the `certbot` program and template configuration files may vary depending on which installation method was used. Change the file paths below accordingly. Here is an easy way to create a signed certificate using `certbot`, it assumes `certbot` was installed through APT on a Debian-based distribution: 86 - [How To Use Certbot Standalone Mode to Retrieve Let's Encrypt SSL Certificates](https://www.digitalocean.com/community/tutorials/how-to-use-certbot-standalone-mode-to-retrieve-let-s-encrypt-ssl-certificates-on-debian-10).
50 87
51 * Stop the apache2/nginx service. 88In short:
52 * Run `certbot --agree-tos --standalone --preferred-challenges tls-sni --email "youremail@example.com" --domain yourdomain.example.com`
53 * For the Apache webserver, copy `/usr/lib/python2.7/dist-packages/certbot_apache/options-ssl-apache.conf` to `/etc/letsencrypt/options-ssl-apache.conf` (paths may vary depending on installation method)
54 * For Nginx: TODO
55 * Setup your webserver as described below
56 * Restart the apache2/nginx service.
57 89
58#### Self-signed certificates 90```bash
91# install certbot
92sudo apt install certbot
59 93
60If you don't want to request a certificate from Let's Encrypt, or are unable to (for example, webserver on a LAN, or domain name not registered in the public DNS system), you can generate a self-signed certificate. This certificate will trigger security warnings in web browsers, unless you add it to the browser's SSL store manually. 94# stop your webserver if you already have one running
95# certbot in standalone mode needs to bind to port 80 (only needed on initial generation)
96sudo systemctl stop apache2
97sudo systemctl stop nginx
61 98
62* Apache: run `make-ssl-cert generate-default-snakeoil --force-overwrite` 99# generate initial certificates
63* Nginx: TODO 100# Let's Encrypt ACME servers must be able to access your server! port forwarding and firewall must be properly configured
101sudo certbot certonly --standalone --noninteractive --agree-tos --email "admin@shaarli.mydomain.org" -d shaarli.mydomain.org
102# this will generate a private key and certificate at /etc/letsencrypt/live/shaarli.mydomain.org/{privkey,fullchain}.pem
103
104# restart the web server
105sudo systemctl start apache2
106sudo systemctl start nginx
107```
108
109On apache `2.4.43+`, you can also delegate LE certificate management to [mod_md](https://httpd.apache.org/docs/2.4/mod/mod_md.html) [[1](https://www.cyberciti.biz/faq/how-to-secure-apache-with-mod_md-lets-encrypt-on-ubuntu-20-04-lts/)] in which case you don't need certbot and manual SSL configuration in virtualhosts.
110
111### Self-signed
112
113If you don't want to rely on a certificate authority, or the server can only be accessed from your own network, you can also generate self-signed certificates. Not that this will generate security warnings in web browsers/clients trying to access Shaarli:
114
115- [How To Create a Self-Signed SSL Certificate for Apache](https://www.digitalocean.com/community/tutorials/how-to-create-a-self-signed-ssl-certificate-for-apache-on-debian-10)
116- [How To Create a Self-Signed SSL Certificate for Nginx](https://www.digitalocean.com/community/tutorials/how-to-create-a-self-signed-ssl-certificate-for-nginx-on-debian-10)
117- [How to Create Self-Signed SSL Certificates with OpenSSL](http://www.xenocafe.com/tutorials/linux/centos/openssl/self_signed_certificates/index.php)
118- [How do I create my own Certificate Authority?](https://workaround.org/certificate-authority)
64 119
65-------------------------------------------------------------------------------- 120--------------------------------------------------------------------------------
66 121
67## Apache 122## Examples
68 123
69Here is a basic configuration example for the Apache web server with `mod_php`. 124The following examples assume a Debian-based operating system is installed. On other distributions you may have to adapt details such as package installation procedures, configuration file locations, and webserver username/group (`www-data` or `httpd` are common values). In these examples we assume the document root for your web server/virtualhost is at `/var/www/shaarli.mydomain.org/`:
70 125
71In `/etc/apache2/sites-available/shaarli.conf`: 126```bash
127# create the document root (replace with your own domain name)
128sudo mkdir -p /var/www/shaarli.mydomain.org/
129```
130
131You can install Shaarli at the root of your virtualhost, or in a subdirectory as well. See [Directory structure](Directory-structure)
132
133
134### Apache
135
136```bash
137# Install apache + mod_php and PHP modules
138sudo apt update
139sudo apt install apache2 libapache2-mod-php php-json php-mbstring php-gd php-intl php-curl php-gettext
140
141# Edit the virtualhost configuration file with your favorite editor (replace the example domain name)
142sudo nano /etc/apache2/sites-available/shaarli.mydomain.org.conf
143```
72 144
73```apache 145```apache
74<VirtualHost *:443> 146<VirtualHost *:80>
75 ServerName shaarli.my-domain.org 147 ServerName shaarli.mydomain.org
76 DocumentRoot /absolute/path/to/shaarli/ 148 DocumentRoot /var/www/shaarli.mydomain.org/
149
150 # For SSL/TLS certificates acquired with certbot or self-signed certificates
151 # Redirect HTTP requests to HTTPS, except Let's Encrypt ACME challenge requests
152 RewriteEngine on
153 RewriteRule ^.well-known/acme-challenge/ - [L]
154 RewriteCond %{HTTP_HOST} =shaarli.mydomain.org
155 RewriteRule ^ https://shaarli.mydomain.org%{REQUEST_URI} [END,NE,R=permanent]
156</VirtualHost>
77 157
78 # Logging 158# SSL/TLS configuration for Let's Encrypt certificates managed with mod_md
79 # Possible values include: debug, info, notice, warn, error, crit, alert, emerg. 159#MDomain shaarli.mydomain.org
80 LogLevel warn 160#MDCertificateAgreement accepted
81 ErrorLog /var/log/apache2/shaarli-error.log 161#MDContactEmail admin@shaarli.mydomain.org
82 CustomLog /var/log/apache2/shaarli-access.log combined 162#MDPrivateKeys RSA 4096
83 163
84 # Let's Encrypt SSL configuration (recommended) 164<VirtualHost *:443>
85 SSLEngine on 165 ServerName shaarli.mydomain.org
86 SSLCertificateFile /etc/letsencrypt/live/yourdomain.example.com/fullchain.pem 166 DocumentRoot /var/www/shaarli.mydomain.org/
87 SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.example.com/privkey.pem
88 Include /etc/letsencrypt/options-ssl-apache.conf
89 167
90 # Self-signed SSL cert configuration 168 # SSL/TLS configuration for Let's Encrypt certificates acquired with certbot standalone
169 SSLEngine on
170 SSLCertificateFile /etc/letsencrypt/live/shaarli.mydomain.org/fullchain.pem
171 SSLCertificateKeyFile /etc/letsencrypt/live/shaarli.mydomain.org/privkey.pem
172 # Let's Encrypt settings from https://github.com/certbot/certbot/blob/master/certbot-apache/certbot_apache/_internal/tls_configs/current-options-ssl-apache.conf
173 SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
174 SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
175 SSLHonorCipherOrder off
176 SSLSessionTickets off
177 SSLOptions +StrictRequire
178
179 # SSL/TLS configuration for self-signed certificates
91 #SSLEngine on 180 #SSLEngine on
92 #SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem 181 #SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
93 #SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key 182 #SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
@@ -98,345 +187,283 @@ In `/etc/apache2/sites-available/shaarli.conf`:
98 #php_value error_reporting 2147483647 187 #php_value error_reporting 2147483647
99 #php_value error_log /var/log/apache2/shaarli-php-error.log 188 #php_value error_log /var/log/apache2/shaarli-php-error.log
100 189
101 <Directory /absolute/path/to/shaarli/> 190 <Directory /var/www/shaarli.mydomain.org/>
102 #Required for .htaccess support 191 # Required for .htaccess support
103 AllowOverride All 192 AllowOverride All
104 Order allow,deny 193 Require all granted
105 Allow from all
106
107 Options Indexes FollowSymLinks MultiViews #TODO is Indexes/Multiviews required?
108
109 # Optional - required for playvideos plugin
110 #Header set Content-Security-Policy "script-src 'self' 'unsafe-inline' https://www.youtube.com https://s.ytimg.com 'unsafe-eval'"
111 </Directory> 194 </Directory>
112 195
113</VirtualHost> 196 <LocationMatch "/\.">
114``` 197 # Prevent accessing dotfiles
115 198 RedirectMatch 404 ".*"
116Enable this configuration with `sudo a2ensite shaarli` 199 </LocationMatch>
117 200
118_Note: If you use Apache 2.2 or lower, you need [mod_version](https://httpd.apache.org/docs/current/mod/mod_version.html) to be installed and enabled._ 201 <LocationMatch "\.(?:ico|css|js|gif|jpe?g|png)$">
202 # allow client-side caching of static files
203 Header set Cache-Control "max-age=2628000, public, must-revalidate, proxy-revalidate"
204 </LocationMatch>
119 205
120_Note: Apache module `mod_rewrite` must be enabled to use the REST API._ 206 # serve the Shaarli favicon from its custom location
207 Alias favicon.ico /var/www/shaarli.mydomain.org/images/favicon.ico
121 208
209</VirtualHost>
210```
122 211
123## Nginx 212```bash
124 213# Enable the virtualhost
125Here is a basic configuration example for the Nginx web server, using the [php-fpm](http://php-fpm.org) PHP FastCGI Process Manager, and Nginx's [FastCGI](https://en.wikipedia.org/wiki/FastCGI) module. 214sudo a2ensite shaarli.mydomain.org
126
127<!--- TODO refactor everything below this point --->
128
129### Common setup
130Once Nginx and PHP-FPM are installed, we need to ensure:
131 215
132- Nginx and PHP-FPM are running using the _same user and group_ 216# mod_ssl must be enabled to use TLS/SSL certificates
133- both these user and group have 217# https://httpd.apache.org/docs/current/mod/mod_ssl.html
134 - `read` permissions for Shaarli resources 218sudo a2enmod ssl
135 - `execute` permissions for Shaarli directories _AND_ their parent directories
136 219
137On a production server: 220# mod_rewrite must be enabled to use the REST API
221# https://httpd.apache.org/docs/current/mod/mod_rewrite.html
222sudo a2enmod rewrite
138 223
139- `user:group` will likely be `http:http`, `www:www` or `www-data:www-data` 224# mod_headers must be enabled to set custom headers from the server config
140- files will be located under `/var/www`, `/var/http` or `/usr/share/nginx` 225sudo a2enmod headers
141 226
142On a development server: 227# mod_version must only be enabled if you use Apache 2.2 or lower
228# https://httpd.apache.org/docs/current/mod/mod_version.html
229# sudo a2enmod version
143 230
144- files may be located in a user's home directory 231# restart the apache service
145- in this case, make sure both Nginx and PHP-FPM are running as the local user/group! 232sudo systemctl restart apache2
233```
146 234
147For all following configuration examples, this user/group pair will be used: 235- [How to install the Apache web server](https://www.digitalocean.com/community/tutorials/how-to-install-the-apache-web-server-on-debian-10)
236- [Apache/PHP - error log per VirtualHost - StackOverflow](http://stackoverflow.com/q/176)
237- [Apache - PHP: php_value vs php_admin_value and the use of php_flag explained](https://ma.ttias.be/php-php_value-vs-php_admin_value-and-the-use-of-php_flag-explained/)
238- [Server-side TLS (Apache) - Mozilla](https://wiki.mozilla.org/Security/Server_Side_TLS#Apache)
239- [Apache 2.4 documentation](https://httpd.apache.org/docs/2.4/)
240- [Apache mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
241- [Apache Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)
148 242
149- `user:group = john:users`,
150 243
151which corresponds to the following service configuration: 244### Nginx
152 245
153```ini 246This examples uses nginx and the [PHP-FPM](https://www.digitalocean.com/community/tutorials/how-to-install-linux-nginx-mariadb-php-lemp-stack-on-debian-10#step-3-%E2%80%94-installing-php-for-processing) PHP interpreter. Nginx and PHP-FPM must be running using the same user and group, here we assume the user/group to be `www-data:www-data`.
154; /etc/php/php-fpm.conf
155user = john
156group = users
157 247
158[...]
159listen.owner = john
160listen.group = users
161```
162 248
163```nginx 249```bash
164# /etc/nginx/nginx.conf 250# install nginx and php-fpm
165user john users; 251sudo apt update
252sudo apt install nginx php-fpm
166 253
167http { 254# Edit the virtualhost configuration file with your favorite editor
168 [...] 255sudo nano /etc/nginx/sites-available/shaarli.mydomain.org
169}
170``` 256```
171 257
172### (Optional) Increase the maximum file upload size
173Some bookmark dumps generated by web browsers can be _huge_ due to the presence of Base64-encoded images and favicons, as well as extra verbosity when nesting links in (sub-)folders.
174
175To increase upload size, you will need to modify both nginx and PHP configuration:
176
177```nginx 258```nginx
178# /etc/nginx/nginx.conf 259server {
179 260 listen 80;
180http { 261 server_name shaarli.mydomain.org;
181 [...]
182
183 client_max_body_size 10m;
184 262
185 [...] 263 # redirect all plain HTTP requests to HTTPS
264 return 301 https://shaarli.mydomain.org$request_uri;
186} 265}
187```
188 266
189```ini 267server {
190# /etc/php/<PHP_VERSION>/fpm/php.ini 268 # ipv4 listening port/protocol
269 listen 443 ssl http2;
270 # ipv6 listening port/protocol
271 listen [::]:443 ssl http2;
272 server_name shaarli.mydomain.org;
273 root /var/www/shaarli.mydomain.org;
274
275 # log file locations
276 # combined log format prepends the virtualhost/domain name to log entries
277 access_log /var/log/nginx/access.log combined;
278 error_log /var/log/nginx/error.log;
191 279
192[...] 280 # paths to private key and certificates for SSL/TLS
193post_max_size = 10M 281 ssl_certificate /etc/ssl/shaarli.mydomain.org.crt;
194[...] 282 ssl_certificate_key /etc/ssl/private/shaarli.mydomain.org.key;
195upload_max_filesize = 10M 283
196``` 284 # Let's Encrypt SSL settings from https://github.com/certbot/certbot/blob/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf
285 ssl_session_cache shared:le_nginx_SSL:10m;
286 ssl_session_timeout 1440m;
287 ssl_session_tickets off;
288 ssl_protocols TLSv1.2 TLSv1.3;
289 ssl_prefer_server_ciphers off;
290 ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
291
292 # increase the maximum file upload size if needed: by default nginx limits file upload to 1MB (413 Entity Too Large error)
293 client_max_body_size 100m;
294
295 # relative path to shaarli from the root of the webserver
296 location / {
297 # default index file when no file URI is requested
298 index index.php;
299 try_files $uri /index.php$is_args$args;
300 }
197 301
198### Minimal 302 location ~ (index)\.php$ {
199_WARNING: Use for development only!_ 303 try_files $uri =404;
304 # slim API - split URL path into (script_filename, path_info)
305 fastcgi_split_path_info ^(.+\.php)(/.+)$;
306 # pass PHP requests to PHP-FPM
307 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
308 fastcgi_index index.php;
309 include fastcgi.conf;
310 }
200 311
201```nginx 312 location ~ \.php$ {
202user john users; 313 # deny access to all other PHP scripts
203worker_processes 1; 314 # disable this if you host other PHP applications on the same virtualhost
204events { 315 deny all;
205 worker_connections 1024; 316 }
206}
207 317
208http { 318 location ~ /\. {
209 include mime.types; 319 # deny access to dotfiles
210 default_type application/octet-stream; 320 deny all;
211 keepalive_timeout 20;
212
213 index index.html index.php;
214
215 server {
216 listen 80;
217 server_name localhost;
218 root /home/john/web;
219
220 access_log /var/log/nginx/access.log;
221 error_log /var/log/nginx/error.log;
222
223 location /shaarli/ {
224 try_files $uri /shaarli/index.php$is_args$args;
225 access_log /var/log/nginx/shaarli.access.log;
226 error_log /var/log/nginx/shaarli.error.log;
227 }
228
229 location ~ (index)\.php$ {
230 try_files $uri =404;
231 fastcgi_split_path_info ^(.+\.php)(/.+)$;
232 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
233 fastcgi_index index.php;
234 include fastcgi.conf;
235 }
236 } 321 }
237}
238```
239 322
240### Modular 323 location ~ ~$ {
241The previous setup is sufficient for development purposes, but has several major caveats: 324 # deny access to temp editor files, e.g. "script.php~"
325 deny all;
326 }
242 327
243- every content that does not match the PHP rule will be sent to client browsers: 328 location ~ /doc/ {
244 - dotfiles - in our case, `.htaccess` 329 default_type "text/html";
245 - temporary files, e.g. Vim or Emacs files: `index.php~` 330 try_files $uri $uri/ $uri.html =404;
246- asset / static resource caching is not optimized 331 }
247- if serving several PHP sites, there will be a lot of duplication: `location /shaarli/`, `location /mysite/`, etc.
248 332
249To solve this, we will split Nginx configuration in several parts, that will be included when needed: 333 location = /favicon.ico {
334 # serve the Shaarli favicon from its custom location
335 alias /var/www/shaarli/images/favicon.ico;
336 }
250 337
251```nginx 338 # allow client-side caching of static files
252# /etc/nginx/deny.conf 339 location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
253location ~ /\. { 340 expires max;
254 # deny access to dotfiles 341 add_header Cache-Control "public, must-revalidate, proxy-revalidate";
255 access_log off; 342 # HTTP 1.0 compatibility
256 log_not_found off; 343 add_header Pragma public;
257 deny all; 344 }
258}
259 345
260location ~ ~$ {
261 # deny access to temp editor files, e.g. "script.php~"
262 access_log off;
263 log_not_found off;
264 deny all;
265} 346}
266``` 347```
267 348
268```nginx 349```bash
269# /etc/nginx/php.conf 350# enable the configuration/virtualhost
270location ~ (index)\.php$ { 351sudo ln -s /etc/nginx/sites-available/shaarli.mydomain.org /etc/nginx/sites-enabled/shaarli.mydomain.org
271 # Slim - split URL path into (script_filename, path_info) 352# reload nginx configuration
272 try_files $uri =404; 353sudo systemctl reload nginx
273 fastcgi_split_path_info ^(.+\.php)(/.+)$;
274
275 # filter and proxy PHP requests to PHP-FPM
276 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
277 fastcgi_index index.php;
278 include fastcgi.conf;
279}
280
281location ~ \.php$ {
282 # deny access to all other PHP scripts
283 deny all;
284}
285``` 354```
286 355
287```nginx 356- [How to install the Nginx web server](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-debian-10)
288# /etc/nginx/static_assets.conf 357- [Nginx Beginner's guide](http://nginx.org/en/docs/beginners_guide.html)
289location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { 358- [Nginx documentation](https://nginx.org/en/docs/)
290 expires max; 359- [Nginx ngx_http_fastcgi_module](http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html)
291 add_header Pragma public; 360- [Nginx Pitfalls](http://wiki.nginx.org/Pitfalls)
292 add_header Cache-Control "public, must-revalidate, proxy-revalidate"; 361- [Nginx PHP configuration examples - Karl Blessing](http://kbeezie.com/nginx-configuration-examples/)
293} 362- [Server-side TLS (Nginx) - Mozilla](https://wiki.mozilla.org/Security/Server_Side_TLS#Nginx)
294```
295 363
296```nginx
297# /etc/nginx/nginx.conf
298[...]
299 364
300http {
301 [...]
302 365
303 root /home/john/web; 366## Reverse proxies
304 access_log /var/log/nginx/access.log;
305 error_log /var/log/nginx/error.log;
306 367
307 server { 368If Shaarli is hosted on a server behind a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) (i.e. there is a proxy server between clients and the web server hosting Shaarli), configure it accordingly. See [Reverse proxy](Reverse-proxy.md) configuration.
308 # virtual host for a first domain
309 listen 80;
310 server_name my.first.domain.org;
311 369
312 location /shaarli/ { 370## Using Shaarli without URL rewriting
313 # Slim - rewrite URLs
314 try_files $uri /shaarli/index.php$is_args$args;
315 371
316 access_log /var/log/nginx/shaarli.access.log; 372By default, Shaarli uses Slim framework's URL, which requires
317 error_log /var/log/nginx/shaarli.error.log; 373URL rewriting.
318 }
319 374
320 location = /shaarli/favicon.ico { 375If you can't use URL rewriting for any reason (not supported by
321 # serve the Shaarli favicon from its custom location 376your web server, shared hosting, etc.), you *can* use Shaarli
322 alias /var/www/shaarli/images/favicon.ico; 377without URL rewriting.
323 }
324 378
325 include deny.conf; 379You just need to prefix your URL by `/index.php/`.
326 include static_assets.conf; 380Example: instead of accessing `https://shaarli.mydomain.org/`,
327 include php.conf; 381use `https://shaarli.mydomain.org/index.php/`.
328 }
329 382
330 server { 383**Recommended:**
331 # virtual host for a second domain 384 * after installation, in the configuration page, set your header link to `/index.php/`.
332 listen 80; 385 * in your configuration file `config.json.php` set `general.root_url` to
333 server_name second.domain.com; 386 `https://shaarli.mydomain.org/index.php/`.
334 387
335 location /minigal/ { 388## Allow import of large browser bookmarks export
336 access_log /var/log/nginx/minigal.access.log;
337 error_log /var/log/nginx/minigal.error.log;
338 }
339 389
340 include deny.conf; 390Web browser bookmark exports can be large due to the presence of base64-encoded images and favicons/long subfolder names. Edit the PHP configuration file.
341 include static_assets.conf;
342 include php.conf;
343 }
344}
345```
346 391
347### Redirect HTTP to HTTPS 392- Apache: `/etc/php/<PHP_VERSION>/apache2/php.ini`
348Assuming you have generated a (self-signed) key and certificate, and they are 393- Nginx + PHP-FPM: `/etc/php/<PHP_VERSION>/fpm/php.ini` (in addition to `client_max_body_size` in the [Nginx configuration](#nginx))
349located under `/home/john/ssl/localhost.{key,crt}`, it is pretty straightforward
350to set an HTTP (:80) to HTTPS (:443) redirection to force SSL/TLS usage.
351 394
352```nginx 395```ini
353# /etc/nginx/nginx.conf
354[...] 396[...]
397# (optional) increase the maximum file upload size:
398post_max_size = 100M
399[...]
400# (optional) increase the maximum file upload size:
401upload_max_filesize = 100M
402```
355 403
356http { 404To verify PHP settings currently set on the server, create a `phpinfo.php` in your webserver's document root
357 [...]
358
359 index index.html index.php;
360
361 root /home/john/web;
362 access_log /var/log/nginx/access.log;
363 error_log /var/log/nginx/error.log;
364
365 server {
366 listen 80;
367 server_name localhost;
368 405
369 return 301 https://localhost$request_uri; 406```bash
370 } 407# example
408echo '<?php phpinfo(); ?>' | sudo tee /var/www/shaarli.mydomain.org/phpinfo.php
409#give read-only access to this file to the webserver user
410sudo chown www-data:root /var/www/shaarli.mydomain.org/phpinfo.php
411sudo chmod 0400 /var/www/shaarli.mydomain.org/phpinfo.php
412```
371 413
372 server { 414Access the file from a web browser (eg. <https://shaarli.mydomain.org/phpinfo.php> and look at the _Loaded Configuration File_ and _Scan this dir for additional .ini files_ entries
373 listen 443 ssl;
374 server_name localhost;
375 415
376 ssl_certificate /home/john/ssl/localhost.crt; 416It is recommended to remove the `phpinfo.php` when no longer needed as it publicly discloses details about your webserver configuration.
377 ssl_certificate_key /home/john/ssl/localhost.key;
378 417
379 location /shaarli/ {
380 # Slim - rewrite URLs
381 try_files $uri /index.php$is_args$args;
382 418
383 access_log /var/log/nginx/shaarli.access.log; 419## Robots and crawlers
384 error_log /var/log/nginx/shaarli.error.log;
385 }
386 420
387 location = /shaarli/favicon.ico { 421To opt-out of indexing your Shaarli instance by search engines, create a `robots.txt` file at the root of your virtualhost:
388 # serve the Shaarli favicon from its custom location
389 alias /var/www/shaarli/images/favicon.ico;
390 }
391 422
392 include deny.conf; 423```
393 include static_assets.conf; 424User-agent: *
394 include php.conf; 425Disallow: /
395 }
396}
397``` 426```
398 427
399## Proxies 428By default Shaarli already disallows indexing of your local copy of the documentation by default, using `<meta name="robots">` HTML tags. Your Shaarli instance may still be indexed by various robots on the public Internet, that do not respect this header or the robots standard.
400
401If Shaarli is served behind a proxy (i.e. there is a proxy server between clients and the web server hosting Shaarli), please refer to the proxy server documentation for proper configuration. In particular, you have to ensure that the following server variables are properly set:
402
403- `X-Forwarded-Proto`
404- `X-Forwarded-Host`
405- `X-Forwarded-For`
406 429
407In you [Shaarli configuration](Shaarli-configuration) `data/config.json.php`, add the public IP of your proxy under `security.trusted_proxies`. 430- [Robots exclusion standard](https://en.wikipedia.org/wiki/Robots_exclusion_standard)
431- [Introduction to robots.txt](https://support.google.com/webmasters/answer/6062608?hl=en)
432- [Robots meta tag, data-nosnippet, and X-Robots-Tag specifications](https://developers.google.com/search/reference/robots_meta_tag)
433- [About robots.txt](http://www.robotstxt.org)
434- [About the robots META tag](https://www.robotstxt.org/meta.html)
408 435
409See also [proxy-related](https://github.com/shaarli/Shaarli/issues?utf8=%E2%9C%93&q=label%3Aproxy+) issues.
410 436
411## Robots and crawlers 437## Fail2ban
412 438
413Shaarli disallows indexing and crawling of your local documentation pages by search engines, using `<meta name="robots">` HTML tags. 439[fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page) is an intrusion prevention framework that reads server (Apache, SSH, etc.) and uses `iptables` profiles to block brute-force attempts. You need to create a filter to detect shaarli login failures in logs, and a jail configuation to configure the behavior when failed login attempts are detected:
414Your Shaarli instance and other pages you host may still be indexed by various robots on the public Internet.
415You may want to setup a robots.txt file or other crawler control mechanism on your server.
416See [[1]](https://en.wikipedia.org/wiki/Robots_exclusion_standard), [[2]](https://support.google.com/webmasters/answer/6062608?hl=en) and [[3]](https://developers.google.com/search/reference/robots_meta_tag)
417 440
418## See also 441```ini
442# /etc/fail2ban/filter.d/shaarli-auth.conf
443[INCLUDES]
444before = common.conf
445[Definition]
446failregex = \s-\s<HOST>\s-\sLogin failed for user.*$
447ignoreregex =
448```
419 449
420 * [Server security](Server-security.md) 450```ini
451# /etc/fail2ban/jail.local
452[shaarli-auth]
453enabled = true
454port = https,http
455filter = shaarli-auth
456logpath = /var/www/shaarli.mydomain.org/data/log.txt
457# allow 3 login attempts per IP address
458# (over a period specified by findtime = in /etc/fail2ban/jail.conf)
459maxretry = 3
460# permanently ban the IP address after reaching the limit
461bantime = -1
462```
421 463
422#### Webservers 464Then restart the service: `sudo systemctl restart fail2ban`
423 465
424- [Apache/PHP - error log per VirtualHost](http://stackoverflow.com/q/176) (StackOverflow)
425- [Apache - PHP: php_value vs php_admin_value and the use of php_flag explained](https://ma.ttias.be/php-php_value-vs-php_admin_value-and-the-use-of-php_flag-explained/)
426- [Server-side TLS (Apache)](https://wiki.mozilla.org/Security/Server_Side_TLS#Apache) (Mozilla)
427- [Nginx Beginner's guide](http://nginx.org/en/docs/beginners_guide.html)
428- [Nginx ngx_http_fastcgi_module](http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html)
429- [Nginx Pitfalls](http://wiki.nginx.org/Pitfalls)
430- [Nginx PHP configuration examples](http://kbeezie.com/nginx-configuration-examples/) (Karl Blessing)
431- [Server-side TLS (Nginx)](https://wiki.mozilla.org/Security/Server_Side_TLS#Nginx) (Mozilla)
432- [How to Create Self-Signed SSL Certificates with OpenSSL](http://www.xenocafe.com/tutorials/linux/centos/openssl/self_signed_certificates/index.php)
433- [How do I create my own Certificate Authority?](https://workaround.org/certificate-authority)
434 466
435#### PHP 467## What next?
436 468
437- [Travis configuration](https://github.com/shaarli/Shaarli/blob/master/.travis.yml) 469[Shaarli installation](Installation.md)
438- [PHP: Supported versions](http://php.net/supported-versions.php)
439- [PHP: Unsupported versions](http://php.net/eol.php) _(EOL - End Of Life)_
440- [PHP 7 Changelog](http://php.net/ChangeLog-7.php)
441- [PHP 5 Changelog](http://php.net/ChangeLog-5.php)
442- [PHP: Bugs](https://bugs.php.net/)
diff --git a/doc/md/Server-security.md b/doc/md/Server-security.md
deleted file mode 100644
index ea1b637d..00000000
--- a/doc/md/Server-security.md
+++ /dev/null
@@ -1,76 +0,0 @@
1## php.ini
2PHP settings are defined in:
3
4- a main configuration file, usually found under `/etc/php/$php_version/php.ini`; some distributions provide different configuration environments, e.g.
5 - `/etc/php/$php_version/cli/php.ini` - used when running console scripts
6 - `/etc/php/$php_version/apache2/php.ini` - used when a client requests PHP resources from Apache
7 - `/etc/php/$php_version/php-fpm.conf` - used when PHP requests are proxied to PHP-FPM
8- additional configuration files/entries, depending on the installed/enabled extensions:
9 - `/etc/php/conf.d/xdebug.ini`
10
11### Locate .ini files
12#### Console environment
13```bash
14$ php --ini
15Configuration File (php.ini) Path: /etc/php
16Loaded Configuration File: /etc/php/php.ini
17Scan for additional .ini files in: /etc/php/conf.d
18Additional .ini files parsed: /etc/php/conf.d/xdebug.ini
19```
20
21#### Server environment
22- create a `phpinfo.php` script located in a path supported by the web server, e.g.
23 - Apache (with user dirs enabled): `/home/myself/public_html/phpinfo.php`
24 - `/var/www/test/phpinfo.php`
25- make sure the script is readable by the web server user/group (usually, `www`, `www-data` or `httpd`)
26- access the script from a web browser
27- look at the _Loaded Configuration File_ and _Scan this dir for additional .ini files_ entries
28```php
29<?php phpinfo(); ?>
30```
31
32## fail2ban
33`fail2ban` is an intrusion prevention framework that reads server (Apache, SSH, etc.) and uses `iptables` profiles to block brute-force attempts:
34
35- [Official website](http://www.fail2ban.org/wiki/index.php/Main_Page)
36- [Source code](https://github.com/fail2ban/fail2ban)
37
38### Read Shaarli logs to ban IPs
39Example configuration:
40- allow 3 login attempts per IP address
41- after 3 failures, permanently ban the corresponding IP adddress
42
43`/etc/fail2ban/jail.local`
44```ini
45[shaarli-auth]
46enabled = true
47port = https,http
48filter = shaarli-auth
49logpath = /var/www/path/to/shaarli/data/log.txt
50maxretry = 3
51bantime = -1
52```
53
54`/etc/fail2ban/filter.d/shaarli-auth.conf`
55```ini
56[INCLUDES]
57before = common.conf
58[Definition]
59failregex = \s-\s<HOST>\s-\sLogin failed for user.*$
60ignoreregex =
61```
62
63## Robots - Restricting search engines and web crawler traffic
64
65Creating a `robots.txt` with the following contents at the root of your Shaarli installation will prevent _honest_ web crawlers from indexing each and every link and Daily page from a Shaarli instance, thus getting rid of a certain amount of unsollicited network traffic.
66
67```
68User-agent: *
69Disallow: /
70```
71
72See:
73
74- http://www.robotstxt.org
75- http://www.robotstxt.org/robotstxt.html
76- http://www.robotstxt.org/meta.html
diff --git a/doc/md/Shaarli-configuration.md b/doc/md/Shaarli-configuration.md
index 2462e20e..dbfc3da9 100644
--- a/doc/md/Shaarli-configuration.md
+++ b/doc/md/Shaarli-configuration.md
@@ -1,126 +1,19 @@
1## Foreword 1# Shaarli configuration
2
3**Do not edit configuration options in index.php! Your changes would be lost.**
4 2
5Once your Shaarli instance is installed, the file `data/config.json.php` is generated: 3Once your Shaarli instance is installed, the file `data/config.json.php` is generated:
6* it contains all settings in JSON format, and can be edited to customize values
7* it defines which [plugins](Plugin-System) are enabled
8* its values override those defined in `index.php`
9* it is wrap in a PHP comment to prevent anyone accessing it, regardless of server configuration
10
11## File and directory permissions
12
13The server process running Shaarli must have:
14
15- `read` access to the following resources:
16 - PHP scripts: `index.php`, `application/*.php`, `plugins/*.php`
17 - 3rd party PHP and Javascript libraries: `inc/*.php`, `inc/*.js`
18 - static assets:
19 - CSS stylesheets: `inc/*.css`
20 - `images/*`
21 - RainTPL templates: `tpl/*.html`
22- `read`, `write` and `execution` access to the following directories:
23 - `cache` - thumbnail cache
24 - `data` - link data store, configuration options
25 - `pagecache` - Atom/RSS feed cache
26 - `tmp` - RainTPL page cache
27
28On a Linux distribution:
29
30- the web server user will likely be `www` or `http` (for Apache2)
31- it will be a member of a group of the same name: `www:www`, `http:http`
32- to give it access to Shaarli, either:
33 - unzip Shaarli in the default web server location (usually `/var/www/`) and set the web server user as the owner
34 - put users in the same group as the web server, and set the appropriate access rights
35- if you have a domain / subdomain to serve Shaarli, [configure the server](Server-configuration) accordingly
36
37## Configuration
38
39In `data/config.json.php`.
40
41See also [Plugin System](Plugin-System).
42
43### Credentials
44
45_These settings should not be edited_
46
47- **login**: Login username.
48- **hash**: Generated password hash.
49- **salt**: Password salt.
50
51### General
52
53- **title**: Shaarli's instance title.
54- **header_link**: Link to the homepage.
55- **links_per_page**: Number of shaares displayed per page.
56- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).
57- **enabled_plugins**: List of enabled plugins.
58- **default_note_title**: Default title of a new note.
59- **retrieve_description** (boolean): If set to true, for every new links Shaarli will try
60to retrieve the description and keywords from the HTML meta tags.
61
62### Security
63
64- **session_protection_disabled**: Disable session cookie hijacking protection (not recommended).
65 It might be useful if your IP adress often changes.
66- **ban_after**: Failed login attempts before being IP banned.
67- **ban_duration**: IP ban duration in seconds.
68- **open_shaarli**: Anyone can add a new link while logged out if enabled.
69- **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy.
70- **allowed_protocols**: List of allowed protocols in shaare URLs or markdown-rendered descriptions. Useful if you want to store `javascript:` links (bookmarklets) in Shaarli (default: `["ftp", "ftps", "magnet"]`).
71
72### Resources
73 4
74- **data_dir**: Data directory. 5- it contains all settings in JSON format, and can be edited to customize values
75- **datastore**: Shaarli's links database file path. 6- it defines which [plugins](Plugins.md) are enabled
76- **history**: Shaarli's operation history file path. 7- its values override those defined in `index.php`
77- **updates**: File path for the ran updates file. 8- it is wrapped in a PHP comment so that its contents are never served by the web server, regardless of configuration
78- **log**: Log file path.
79- **update_check**: Last update check file path.
80- **raintpl_tpl**: Templates directory.
81- **raintpl_tmp**: Template engine cache directory.
82- **thumbnails_cache**: Thumbnails cache directory.
83- **page_cache**: Shaarli's internal cache directory.
84- **ban_file**: Banned IP file path.
85 9
86### Translation 10**Do not edit configuration options in index.php! Your changes would be lost.**
87 11
88- **language**: translation language (also see [Translations](Translations)) 12## Tools menu
89 - **auto** (default): The translation language is chosen from the browser locale.
90 It means that the language can be different for 2 different visitors depending on their locale.
91 - **en**: Use the English translation.
92 - **fr**: Use the French translation.
93- **mode**:
94 - **auto** or **php** (default): Use the PHP implementation of gettext (slower)
95 - **gettext**: Use PHP builtin gettext extension
96 (faster, but requires `php-gettext` to be installed and to reload the web server on update)
97- **extension**: Translation extensions for custom themes or plugins.
98Must be an associative array: `translation domain => translation path`.
99
100### Updates
101
102- **check_updates**: Enable or disable update check to the git repository.
103- **check_updates_branch**: Git branch used to check updates (e.g. `stable` or `master`).
104- **check_updates_interval**: Look for new version every N seconds (default: every day).
105 13
106### Privacy 14Some settings can be configured directly from a web browser by accesing the `Tools` menu. Values are read/written to/from the configuration file.
107
108- **default_private_links**: Check the private checkbox by default for every new link.
109- **hide_public_links**: All links are hidden while logged out.
110- **force_login**: if **hide_public_links** and this are set to `true`, all anonymous users are redirected to the login page.
111- **hide_timestamps**: Timestamps are hidden.
112- **remember_user_default**: Default state of the login page's *remember me* checkbox
113 - `true`: checked by default, `false`: unchecked by default
114
115### Feed
116
117- **rss_permalinks**: Enable this to redirect RSS links to Shaarli's permalinks instead of shaared URL.
118- **show_atom**: Display ATOM feed button.
119
120### Thumbnail
121 15
122- **enable_thumbnails**: Enable or disable thumbnail display. 16![](https://i.imgur.com/boaaibC.png)
123- **enable_localcache**: Enable or disable local cache.
124 17
125### LDAP 18### LDAP
126 19
@@ -182,6 +75,9 @@ Must be an associative array: `translation domain => translation path`.
182 "title": "My Shaarli", 75 "title": "My Shaarli",
183 "header_link": "?" 76 "header_link": "?"
184 }, 77 },
78 "dev": {
79 "debug": false,
80 }
185 "extras": { 81 "extras": {
186 "show_atom": false, 82 "show_atom": false,
187 "hide_public_links": false, 83 "hide_public_links": false,
@@ -236,9 +132,91 @@ Must be an associative array: `translation domain => translation path`.
236} ?> 132} ?>
237``` 133```
238 134
239## Additional configuration 135## Settings
136
137### Credentials
138
139_These settings should not be edited_
140
141- **login**: Login username.
142- **hash**: Generated password hash.
143- **salt**: Password salt.
144
145### General
146
147- **title**: Shaarli's instance title.
148- **header_link**: Link to the homepage.
149- **links_per_page**: Number of Shaares displayed per page.
150- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).
151- **enabled_plugins**: List of enabled plugins.
152- **default_note_title**: Default title of a new note.
153- **enable_async_metadata** (boolean): Retrieve external bookmark metadata asynchronously to prevent bookmark creation slowdown.
154- **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags.
155- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`.
156
157### Security
158
159- **session_protection_disabled**: Disable session cookie hijacking protection (not recommended).
160 It might be useful if your IP adress often changes.
161- **ban_after**: Failed login attempts before being IP banned.
162- **ban_duration**: IP ban duration in seconds.
163- **open_shaarli**: Anyone can add a new Shaare while logged out if enabled.
164- **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy.
165- **allowed_protocols**: List of allowed protocols in shaare URLs or markdown-rendered descriptions. Useful if you want to store `javascript:` links (bookmarklets) in Shaarli (default: `["ftp", "ftps", "magnet"]`).
166
167### Resources
168
169- **data_dir**: Data directory.
170- **datastore**: Shaarli's Shaares database file path.
171- **history**: Shaarli's operation history file path.
172- **updates**: File path for the ran updates file.
173- **log**: Log file path.
174- **update_check**: Last update check file path.
175- **raintpl_tpl**: Templates directory.
176- **raintpl_tmp**: Template engine cache directory.
177- **thumbnails_cache**: Thumbnails cache directory.
178- **page_cache**: Shaarli's internal cache directory.
179- **ban_file**: Banned IP file path.
180
181### Translation
182
183- **language**: translation language (also see [Translations](Translations))
184 - **auto** (default): The translation language is chosen from the browser locale.
185 It means that the language can be different for 2 different visitors depending on their locale.
186 - **en**: Use the English translation.
187 - **fr**: Use the French translation.
188- **mode**:
189 - **auto** or **php** (default): Use the PHP implementation of gettext (slower)
190 - **gettext**: Use PHP builtin gettext extension
191 (faster, but requires `php-gettext` to be installed and to reload the web server on update)
192- **extension**: Translation extensions for custom themes or plugins.
193Must be an associative array: `translation domain => translation path`.
194
195### Updates
196
197- **check_updates**: Enable or disable update check to the git repository.
198- **check_updates_branch**: Git branch used to check updates (e.g. `stable` or `master`).
199- **check_updates_interval**: Look for new version every N seconds (default: every day).
200
201### Privacy
202
203- **default_private_links**: Check the private checkbox by default for every new Shaare.
204- **hide_public_links**: All Shaares are hidden while logged out.
205- **force_login**: if **hide_public_links** and this are set to `true`, all anonymous users are redirected to the login page.
206- **hide_timestamps**: Timestamps are hidden.
207- **remember_user_default**: Default state of the login page's *remember me* checkbox
208 - `true`: checked by default, `false`: unchecked by default
209
210### Feed
211
212- **rss_permalinks**: Enable this to redirect RSS links to Shaarli's permalinks instead of shaared URL.
213- **show_atom**: Display ATOM feed button.
214
215### Thumbnail
216
217- **enable_thumbnails**: Enable or disable thumbnail display.
218- **enable_localcache**: Enable or disable local cache.
240 219
241The `playvideos` plugin may require that you adapt your server's 220## Plugins configuration
242[Content Security Policy](https://github.com/shaarli/Shaarli/blob/master/plugins/playvideos/README.md#troubleshooting)
243configuration to work properly.
244 221
222See [Plugins](Plugins.md)
diff --git a/doc/md/Sharing-content.md b/doc/md/Sharing-content.md
deleted file mode 100644
index 9a16fc62..00000000
--- a/doc/md/Sharing-content.md
+++ /dev/null
@@ -1,71 +0,0 @@
1Content posted to Shaarli is separated in items called _Shaares_. For each Shaare,
2you can customize the following aspects:
3
4 * URL to link to
5 * Title
6 * Free-text description
7 * Tags
8 * Public/private status
9
10--------------------------------------------------------------------------------
11
12## Adding new Shaares
13
14While logged in to your Shaarli, you can add new Shaares in several ways:
15
16 * [+Shaare button](#shaare-button)
17 * [Bookmarklet](#bookmarklet)
18 * Third-party [apps and browser addons](Community-&-Related-software.md#mobile-apps)
19 * [REST API](https://shaarli.github.io/api-documentation/)
20
21### +Shaare button
22
23 * While logged in to your Shaarli, click the **`+Shaare`** button located in the toolbar.
24 * Enter the URL of a link you want to share.
25 * Click `Add link`
26 * The `New Shaare` dialog appears, allowing you to fill in the details of your Shaare.
27 * The Description, Title, and Tags will help you find your Shaare later using tags or full-text search.
28 * You can also check the “Private” box so that the link is saved but only visible to you (the logged-in user).
29 * Click `Save`.
30
31<!-- TODO Add screenshot of add/edit link dialog -->
32
33### Bookmarklet
34
35The _Bookmarklet_ \[[1](https://en.wikipedia.org/wiki/Bookmarklet)\] is a special
36browser bookmark you can use to add new content to your Shaarli. This bookmarklet is
37compatible with Firefox, Opera, Chrome and Safari. To set it up:
38
39 * Access the `Tools` page from the button in the toolbar.
40 * Drag the **`✚Shaare link` button** to your browser's bookmarks bar.
41
42Once this is done, you can shaare any URL you are visiting simply by clicking the
43bookmarklet in your browser! The same `New Shaare` dialog as above is displayed.
44
45| Note | Websites which enforce Content Security Policy (CSP), such as github.com, disallow usage of bookmarklets. Unfortunately, there is nothing Shaarli can do about it. \[[1](https://github.com/shaarli/Shaarli/issues/196)]\ \[[2](https://bugzilla.mozilla.org/show_bug.cgi?id=866522)]\ \[[3](https://code.google.com/p/chromium/issues/detail?id=233903)]\ |
46|---------|---------|
47
48| Note | Under Opera, you can't drag'n drop the button: You have to right-click on it and add a bookmark to your personal toolbar. |
49|---------|---------|
50
51![](images/bookmarklet.png)
52
53
54--------------------------------------------------------------------------------
55
56## Editing Shaares
57
58Any Shaare can edited by clicking its ![](images/edit_icon.png) `Edit` button.
59
60Editing a Shaare will not change it's permalink, each permalink always points to the
61latest revision of a Shaare.
62
63--------------------------------------------------------------------------------
64
65## Using shaarli as a blog, notepad, pastebin...
66
67While adding or editing a link, leave the URL field blank to create a text-only
68("note") post. This allows you to post any kind of text content, such as blog
69articles, private or public notes, snippets... There is no character limit! You can
70access your Shaare from its permalink.
71
diff --git a/doc/md/Static-analysis.md b/doc/md/Static-analysis.md
deleted file mode 100644
index 29d98362..00000000
--- a/doc/md/Static-analysis.md
+++ /dev/null
@@ -1,13 +0,0 @@
1## WIP
2This topic is currently being discussed here:
3
4- [Fix coding style (static analysis)](https://github.com/shaarli/Shaarli/issues/95) (#95)
5- [Continuous Integration tools & features](https://github.com/shaarli/Shaarli/issues/130) (#130)
6
7### Usage
8Static analysis tools can be installed with Composer, and used through Shaarli's [Makefile](https://github.com/shaarli/Shaarli/blob/master/Makefile).
9
10For an overview of the available features, see:
11
12- [Code quality: Makefile to run static code checkers](https://github.com/shaarli/Shaarli/pull/124) (#124)
13- [Run PHPCS against different coding standards](https://github.com/shaarli/Shaarli/pull/276) (#276)
diff --git a/doc/md/Troubleshooting.md b/doc/md/Troubleshooting.md
index 01fd9840..e1ed5e00 100644
--- a/doc/md/Troubleshooting.md
+++ b/doc/md/Troubleshooting.md
@@ -1,5 +1,8 @@
1# Troubleshooting 1# Troubleshooting
2 2
3First of all, ensure that both the [web server](Server-configuration.md) and [Shaarli](Shaarli-configuration.md) are correctly configured.
4
5
3## Login 6## Login
4 7
5### I forgot my password! 8### I forgot my password!
@@ -8,38 +11,48 @@ Delete the file `data/config.json.php` and display the page again. You will be a
8 11
9### I'm locked out - Login bruteforce protection 12### I'm locked out - Login bruteforce protection
10 13
11Login form is protected against brute force attacks: 4 failed logins will ban the IP address from login for 30 minutes. Banned IPs can still browse links. 14Login form is protected against brute force attacks: 4 failed logins will ban the IP address from login for 30 minutes. Banned IPs can still browse Shaares.
12 15
13- To remove the current IP bans, delete the file `data/ipbans.php` 16- To remove the current IP bans, delete the file `data/ipbans.php`
14- To list all login attempts, see `data/log.txt` (succesful/failed logins, bans/lifted bans) 17- To list all login attempts, see `data/log.txt` (succesful/failed logins, bans/lifted bans)
15 18
19--------------------------------------
20
16## Browser issues 21## Browser issues
17 22
18### Redirection issues (HTTP Referer) 23### Redirection issues (HTTP Referer)
19 24
20Depending on its configuration and installed plugins, the browser may remove or alter (spoof) [HTTP referers](https://en.wikipedia.org/wiki/HTTP_referer), thus preventing Shaarli from properly redirecting between pages. Referer settings are available by browsing `about:config` and are documented [here](https://wiki.mozilla.org/Security/Referrer). `network.http.referer.spoofSource = true` in particular is known to break some functionality in Shaarli. 25Shaarli relies on `HTTP_REFERER` for some functions (like redirects and clicking on tags). If you have disabled or altered/spoofed [HTTP referers](https://en.wikipedia.org/wiki/HTTP_referer) in your browser, some features of Shaarli may not work as expected (depending on configuration and installed plugins), notably redirections between pages.
26
27Firefox Referer settings are available by browsing `about:config` and are documented [here](https://wiki.mozilla.org/Security/Referrer). `network.http.referer.spoofSource = true` in particular is known to break some functionality in Shaarli.
28
21 29
22### Firefox, localhost and redirections 30### Firefox, localhost and redirections
23 31
24`localhost` is not a proper Fully Qualified Domain Name (FQDN); if Firefox has been set up to spoof referers, or only accept requests from the same base domain/host, 32`localhost` is not a proper Fully Qualified Domain Name (FQDN); if Firefox has been set up to spoof referers, or only accept requests from the same base domain/host,
25Shaarli redirections will not work properly. To solve this, assign a local domain to your host, e.g. `localhost.lan` in your [hosts file](https://en.wikipedia.org/wiki/Hosts_(file)) and browse Shaarli at http://localhost.lan/. 33Shaarli redirections will not work properly. To solve this, assign a local domain to your host, e.g. `localhost.lan` in your [hosts file](https://en.wikipedia.org/wiki/Hosts_(file)) and browse Shaarli at http://localhost.lan/.
26 34
35-----------------------------------------
36
27## Hosting problems 37## Hosting problems
28 38
29### Old PHP versions 39### Old PHP versions
30 40
31On **free.fr**: free.fr now supports php 5.6.x([link](http://les.pages.perso.chez.free.fr/migrations/php5v6.io)) 41- On hosts (such as **free.fr**) which only support PHP 5.6, Shaarli [v0.10.4](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4) is the maximum supported version. At the root of your webspace create a `sessions` directory and a `.htaccess` file containing:
32and so support now the tag autocompletion but you have to do the following.
33
34At the root of your webspace create a `sessions` directory and a `.htaccess` file containing:
35 42
36```xml 43```xml
37<IfDefine Free> 44<IfDefine Free>
38php56 1 45php56 1
39</IfDefine> 46</IfDefine>
47<Files ".ht*">
48Order allow,deny
49Deny from all
50Satisfy all
51</Files>
52Options -Indexes
40``` 53```
41 54
42- If you have an error such as: `Parse error: syntax error, unexpected '=', expecting '(' in /links/index.php on line xxx`, it means that your host is using php4, not php5. Shaarli requires php 5.1. Try changing the file extension to `.php5` 55- If you have an error such as: `Parse error: syntax error, unexpected '=', expecting '(' in /links/index.php on line xxx`, it means that your host is using PHP 4, not PHP 5. Shaarli requires PHP 5.1. Try changing the file extension to `.php5`
43- On **1and1** : If you add the link from the page (and not from the bookmarklet), Shaarli will no be able to get the title of the page. You will have to enter it manually. (Because they have disabled the ability to download a file through HTTP). 56- On **1and1** : If you add the link from the page (and not from the bookmarklet), Shaarli will no be able to get the title of the page. You will have to enter it manually. (Because they have disabled the ability to download a file through HTTP).
44- If you have the error `Warning: file_get_contents() [function.file-get-contents]: URL file-access is disabled in the server configuration in /…/index.php on line xxx`, it means that your host has disabled the ability to fetch a file by HTTP in the php config (Typically in 1and1 hosting). Bad host. Change host. Or comment the following lines: 57- If you have the error `Warning: file_get_contents() [function.file-get-contents]: URL file-access is disabled in the server configuration in /…/index.php on line xxx`, it means that your host has disabled the ability to fetch a file by HTTP in the php config (Typically in 1and1 hosting). Bad host. Change host. Or comment the following lines:
45 58
@@ -49,9 +62,11 @@ php56 1
49//if (strpos($status,'200 OK')) $title=html_extract_title($data); 62//if (strpos($status,'200 OK')) $title=html_extract_title($data);
50``` 63```
51 64
52- On hosts which forbid outgoing HTTP requests (such as free.fr), some thumbnails will not work. 65- On hosts (such as **free.fr**) which forbid outgoing HTTP requests, some thumbnails will not work.
66- On hosts (such as **free.fr**) which limit the number of FTP connections, setup your FTP client accordingly (else some files may be missing after upload).
53- On **lost-oasis**, RSS doesn't work correctly, because of this message at the begining of the RSS/ATOM feed : `<? // tout ce qui est charge ici (generalement des includes et require) est charge en permanence. ?>`. To fix this, remove this message from `php-include/prepend.php` 67- On **lost-oasis**, RSS doesn't work correctly, because of this message at the begining of the RSS/ATOM feed : `<? // tout ce qui est charge ici (generalement des includes et require) est charge en permanence. ?>`. To fix this, remove this message from `php-include/prepend.php`
54 68
69
55### Dates are not properly formatted 70### Dates are not properly formatted
56 71
57Shaarli tries to sniff the language of the browser (using `HTTP_ACCEPT_LANGUAGE` headers) 72Shaarli tries to sniff the language of the browser (using `HTTP_ACCEPT_LANGUAGE` headers)
@@ -71,11 +86,118 @@ This can be caused by several things:
71- You may be using OperaTurbo or OperaMini, which use their own proxies which may change from time to time. 86- You may be using OperaTurbo or OperaMini, which use their own proxies which may change from time to time.
72- If you have another application on the same webserver where Shaarli is installed, these application may forcefully expire php sessions. 87- If you have another application on the same webserver where Shaarli is installed, these application may forcefully expire php sessions.
73 88
89
74### Old apache versions, Internal Server Error 90### Old apache versions, Internal Server Error
75 91
76If you hosting provider only provides apache 2.2 and no support for `mod_version`, `.htaccess` files may cause 500 errors (Internal Server Error). See [this workaround](https://github.com/shaarli/Shaarli/issues/1196#issuecomment-412271085). 92If you hosting provider only provides apache 2.2 and no support for `mod_version`, `.htaccess` files may cause 500 errors (Internal Server Error). See [this workaround](https://github.com/shaarli/Shaarli/issues/1196#issuecomment-412271085).
77 93
78## Sessions do not seem to work correctly on your server 94
95### Sessions do not seem to work correctly on your server
79 96
80Follow the instructions in the error message. Make sure you are accessing shaarli via a direct IP address or a proper hostname. If you have **no dots** in the hostname (e.g. `localhost` or `http://my-webserver/shaarli/`), some browsers will not store cookies at all (this respects the [HTTP cookie specification](http://curl.haxx.se/rfc/cookie_spec.html)). 97Follow the instructions in the error message. Make sure you are accessing shaarli via a direct IP address or a proper hostname. If you have **no dots** in the hostname (e.g. `localhost` or `http://my-webserver/shaarli/`), some browsers will not store cookies at all (this respects the [HTTP cookie specification](http://curl.haxx.se/rfc/cookie_spec.html)).
81 98
99----------------------------------------------------------
100
101## Upgrades
102
103### You must specify an integer as a key
104
105In `v0.8.1` we changed how Shaare keys are handled (from timestamps to incremental integers). Take a look at `data/updates.txt` content.
106
107
108### `updates.txt` contains `updateMethodDatastoreIds`
109
110Try to delete it and refresh your page while being logged in.
111
112### `updates.txt` doesn't exist or doesn't contain `updateMethodDatastoreIds`
113
1141. Create `data/updates.txt` if it doesn't exist
1152. Paste this string in the update file `;updateMethodRenameDashTags;`
1163. Login to Shaarli
1174. Delete the update file
1185. Refresh
119
120
121
122--------------------------------------------------------
123
124## Import/export
125
126### Importing shaarli data to Firefox
127
128- In Firefox, open the bookmark manager (`Bookmarks menu > Show all bookmarks` or `Ctrl+Shift+B`), select `Import and Backup > Import bookmarks in HTML format`
129- Make sure the `Prepend note permalinks with this Shaarli instance's URL` box is checked when exporting, so that text-only/notes Shaares still point to the Shaarli instance you exported them from.
130- Depending on the number of bookmarks, the import can take some time.
131
132You may be interested in these Firefox addons to manage bookmarks imported from Shaarli
133
134- [Bookmark Deduplicator](https://addons.mozilla.org/en-US/firefox/addon/bookmark-deduplicator/) - provides an easy way to deduplicate your bookmarks
135- [TagSieve](https://addons.mozilla.org/en-US/firefox/addon/tagsieve/) - browse your bookmarks by their tags
136
137### Diigo
138
139If you export your bookmark from Diigo, make sure you use the Delicious export, not the Netscape export. (Their Netscape export is broken, and they don't seem to be interested in fixing it.)
140
141### Mister Wong
142
143See [this issue](https://github.com/sebsauvage/Shaarli/issues/146) for import tweaks.
144
145### SemanticScuttle
146
147To correctly import the tags from a [SemanticScuttle](http://semanticscuttle.sourceforge.net/) HTML export, edit the HTML file before importing and replace all occurences of `tags=` (lowercase) to `TAGS=` (uppercase).
148
149### Scuttle
150
151Shaarli cannot import data directly from [Scuttle](https://github.com/scronide/scuttle).
152
153However, you can use the third-party [scuttle-to-shaarli](https://github.com/q2apro/scuttle-to-shaarli)
154tool to export the Scuttle database to the Netscape HTML format compatible with the Shaarli importer.
155
156### Refind.com
157
158You can use the third-party tool [Derefind](https://github.com/ShawnPConroy/Derefind) to convert refind.com bookmark exports to a format that can be imported into Shaarli.
159
160
161-------------------------------------------------------
162
163## Other
164
165### The bookmarklet doesn't work
166
167Websites which enforce Content Security Policy (CSP), such as github.com, disallow usage of bookmarklets. Unfortunately, there is nothing Shaarli can do about it ([1](https://github.com/shaarli/Shaarli/issues/196), [2](https://bugzilla.mozilla.org/show_bug.cgi?id=866522), [3](https://code.google.com/p/chromium/issues/detail?id=233903).
168
169Under Opera, you can't drag'n drop the button: You have to right-click on it and add a bookmark to your personal toolbar.
170
171
172### Changing the timestamp for a shaare
173
174- Look for `<input type="hidden" name="lf_linkdate" value="{$link.linkdate}">` in `tpl/editlink.tpl` (line 14)
175- Replace `type="hidden"` with `type="text"` from this line
176- A new date/time field becomes available in the edit/new Shaare dialog.
177- You can set the timestamp manually by entering it in the format `YYYMMDD_HHMMS`.
178
179### Clearing Shaarli caches
180
181For debugging purposes:
182
183```bash
184# clear raintpl cache and temporary files
185find /var/www/links/cache/ /var/www/links/pagecache/ /var/www/links/tmp/ -type f -exec rm -v '{}' \;
186# if you have a php accelerator such as php-apcu, restart the webserver
187sudo systemctl restart apache2
188```
189
190-------------------------------------------------------
191
192## Support
193
194If the solutions above did not help, please:
195
196- Come and ask question on the [Gitter chat](https://gitter.im/shaarli/Shaarli) (also reachable via [IRC](https://irc.gitter.im/))
197- Search for [issues](https://github.com/shaarli/Shaarli/issues) and [Pull Requests](https://github.com/shaarli/Shaarli/pulls)
198 - if you find one that is related to the issue, feel free to comment and provide additional details (host/Shaarli setup...)
199 - check issues labeled [`feature`](https://github.com/shaarli/Shaarli/labels/feature), [`enhancement`](https://github.com/shaarli/Shaarli/labels/enhancement), and [`plugin`](https://github.com/shaarli/Shaarli/labels/plugin) if you would like a feature added to Shaarli.
200 - else, [open a new issue](https://github.com/shaarli/Shaarli/issues/new), and provide information about the problem:
201 - _what happens?_ - display glitches, invalid data, security flaws...
202 - _what is your configuration?_ - OS, server version, activated extensions, web browser...
203 - _is it reproducible?_ \ No newline at end of file
diff --git a/doc/md/Unit-tests.md b/doc/md/Unit-tests.md
deleted file mode 100644
index a9544656..00000000
--- a/doc/md/Unit-tests.md
+++ /dev/null
@@ -1,119 +0,0 @@
1The testing framework used is [PHPUnit](https://phpunit.de/); it can be installed with [Composer](https://getcomposer.org/), which is a dependency management tool.
2
3## Setup a testing environment
4
5### Install composer
6
7You can either use:
8
9- a system-wide version, e.g. installed through your distro's package manager (eg. `sudo apt install composer`)
10- a local version, downloadable [here](https://getcomposer.org/download/). To update a local composer installation, run `php composer.phar self-update`
11
12
13### Install Shaarli development dependencies
14
15```bash
16$ cd /path/to/shaarli
17$ composer install
18```
19
20### Install Xdebug
21
22Xdebug must be installed and enable for PHPUnit to generate coverage reports. See http://xdebug.org/docs/install.
23
24```bash
25# for Debian-based distributions
26$ aptitude install php-xdebug
27
28# for ArchLinux:
29$ pacman -S xdebug
30```
31
32Then add the following line to `/etc/php/<PHP_VERSION>/cli/php.ini`:
33
34```ini
35zend_extension=xdebug.so
36```
37
38## Run unit tests
39
40Run `make test` and ensure tests return `OK`. If tests return failures, refer to PHPUnit messages and fix your code/tests accordingly.
41
42By default, PHPUnit will run all suitable tests found under the `tests` directory. Each test has 3 possible outcomes:
43
44- `.` - success
45- `F` - failure: the test was run but its results are invalid
46 - the code does not behave as expected
47 - dependencies to external elements: globals, session, cache...
48- `E` - error: something went wrong and the tested code has crashed
49 - typos in the code, or in the test code
50 - dependencies to missing external elements
51
52If Xdebug has been installed and activated, two coverage reports will be generated:
53
54- a summary in the console
55- a detailed HTML report with metrics for tested code
56 - to open it in a web browser: `firefox coverage/index.html &`
57
58### Executing specific tests
59
60Add a [`@group`](https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.group) annotation in a test class or method comment:
61
62```php
63/**
64 * Netscape bookmark import
65 * @group WIP
66 */
67class BookmarkImportTest extends PHPUnit_Framework_TestCase
68{
69 [...]
70}
71```
72
73To run all tests annotated with `@group WIP`:
74```bash
75$ vendor/bin/phpunit --group WIP tests/
76```
77
78### Running tests inside Docker containers
79
80Test Dockerfiles are located under `tests/docker/<distribution>/Dockerfile`,
81and can be used to build Docker images to run Shaarli test suites under common
82Linux environments.
83
84Dockerfiles are provided for the following environments:
85
86- `alpine36` - [Alpine 3.6](https://www.alpinelinux.org/downloads/)
87- `debian8` - [Debian 8 Jessie](https://www.debian.org/DebianJessie) (oldstable)
88- `debian9` - [Debian 9 Stretch](https://wiki.debian.org/DebianStretch) (stable)
89- `ubuntu16` - [Ubuntu 16.04 Xenial Xerus](http://releases.ubuntu.com/16.04/) (LTS)
90
91What's behind the curtains:
92
93- each image provides:
94 - a base Linux OS
95 - Shaarli PHP dependencies (OS packages)
96 - test PHP dependencies (OS packages)
97 - Composer
98- the local workspace is mapped to the container's `/shaarli/` directory,
99- the files are rsync'd so tests are run using a standard Linux user account
100 (running tests as `root` would bypass permission checks and may hide issues)
101- the tests are run inside the container.
102
103To run tests inside a Docker container:
104
105```bash
106# build the Debian 9 Docker image for unit tests
107$ cd /path/to/shaarli
108$ cd tests/docker/debian9
109$ docker build -t shaarli-test:debian9 .
110
111# install/update 3rd-party test dependencies
112$ composer install --prefer-dist
113
114# run tests using the freshly built image
115$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_test
116
117# run the full test campaign
118$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_all_tests
119```
diff --git a/doc/md/Upgrade-and-migration.md b/doc/md/Upgrade-and-migration.md
index d5682a34..bfef3e8c 100644
--- a/doc/md/Upgrade-and-migration.md
+++ b/doc/md/Upgrade-and-migration.md
@@ -1,96 +1,83 @@
1## Preparation 1# Upgrade and migration
2 2
3### Note your current version 3## Note your current version
4 4
5If anything goes wrong, it's important for us to know which version you're upgrading from. 5If anything goes wrong, it's important for us to know which version you're upgrading from.
6The current version is present in the `shaarli_version.php` file. 6The current version is present in the `shaarli_version.php` file.
7 7
8### Backup your data
9 8
10Shaarli stores all user data under the `data` directory: 9## Backup your data
11 10
12- `data/config.json.php` (or `data/config.php` for older Shaarli versions) - main configuration file 11Shaarli stores all user data and [configuration](Shaarli-configuration.md) under the `data` directory. [Backup](Backup-and-restore.md) this repository _before_ upgrading Shaarli. You will need to restore it after the following upgrade steps.
13- `data/datastore.php` - bookmarked links
14- `data/ipbans.php` - banned IP addresses
15- `data/updates.txt` - contains all automatic update to the configuration and datastore files already run
16 12
17See [Shaarli configuration](Shaarli-configuration) for more information about Shaarli resources. 13```bash
18 14sudo cp -r /var/www/shaarli.mydomain.org/data ~/shaarli-data-backup
19It is recommended to backup this repository _before_ starting updating/upgrading Shaarli: 15```
20
21- users with SSH access: copy or archive the directory to a temporary location
22- users with FTP access: download a local copy of your Shaarli installation using your favourite client
23 16
24### Migrating data from a previous installation 17## Upgrading from ZIP archives
25 18
26As all user data is kept under `data`, this is the only directory you need to worry about when migrating to a new installation, which corresponds to the following steps: 19If you installed Shaarli from a [release ZIP archive](Installation.md#from-release-zip):
27 20
28- backup the `data` directory 21```bash
29- install or update Shaarli: 22# Download the archive to the server, and extract it
30 - fresh installation - see [Download and Installation](Download-and-Installation) 23cd ~
31 - update - see the following sections 24wget https://github.com/shaarli/Shaarli/releases/download/v0.X.Y/shaarli-v0.X.Y-full.zip
32- check or restore the `data` directory 25unzip shaarli-v0.X.Y-full.zip
33 26
34## Recommended : Upgrading from release archives 27# overwrite your Shaarli installation with the new release **All data will be lost, see _Backup your data_ above.**
28sudo rsync -avP --delete Shaarli/ /var/www/shaarli.mydomain.org/
35 29
36All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page. 30# restore file permissions as described on the installation page
31sudo chown -R root:www-data /var/www/shaarli.mydomain.org
32sudo chmod -R g+rX /var/www/shaarli.mydomain.org
33sudo chmod -R g+rwX /var/www/shaarli.mydomain.org/{cache/,data/,pagecache/,tmp/}
37 34
38We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and Installation](Download-and-Installation) for `git` complete instructions. 35# restore backups of the data directory
36sudo cp -r ~/shaarli-data-backup/* /var/www/shaarli.mydomain.org/data/
39 37
40Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! 38# If you use gettext mode for translations (not the default), reload your web server.
39sudo systemctl restart apache2
40sudo systemctl restart nginx
41```
41 42
42If you use translations in gettext mode - meaning you manually changed the default mode -, 43If you don't have shell access (eg. on shared hosting), backup the shaarli data directory, download the ZIP archive locally, extract it, upload it to the server using file transfer, and restore the data directory backup.
43reload your web server.
44 44
45After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details). 45Access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli-configuration.md) for more details).
46 46
47## Upgrading with Git
48 47
49### Updating a community Shaarli 48## Upgrading from Git
50 49
51If you have installed Shaarli from the [community Git repository](Download#clone-with-git-recommended), simply [pull new changes](https://www.git-scm.com/docs/git-pull) from your local clone: 50If you have installed Shaarli [from sources](Installation.md#from-sources):
52 51
53```bash 52```bash
54$ cd /path/to/shaarli 53# pull new changes from your local clone
55$ git pull 54cd /var/www/shaarli.mydomain.org/
56 55sudo git pull
57From github.com:shaarli/Shaarli
58 * branch master -> FETCH_HEAD
59Updating ebd67c6..521f0e6
60Fast-forward
61 application/Url.php | 1 +
62 shaarli_version.php | 2 +-
63 tests/Url/UrlTest.php | 1 +
64 3 files changed, 3 insertions(+), 1 deletion(-)
65```
66 56
67Shaarli >= `v0.8.x`: install/update third-party PHP dependencies using [Composer](https://getcomposer.org/): 57# update PHP dependencies (Shaarli >= v0.8)
58sudo composer install --no-dev
68 59
69```bash 60# update translations (Shaarli >= v0.9.2)
70$ composer install --no-dev 61sudo make translate
71 62
72Loading composer repositories with package information 63# If you use translations in gettext mode (not the default), reload your web server.
73Updating dependencies 64sudo systemctl reload apache
74 - Installing shaarli/netscape-bookmark-parser (v1.0.1) 65sudo systemctl reload nginx
75 Downloading: 100%
76```
77 66
78Shaarli >= `v0.9.2` supports translations: 67# update front-end dependencies (Shaarli >= v0.10.0)
68sudo make build_frontend
79 69
80```bash 70# restore file permissions as described on the installation page
81$ make translate 71sudo chown -R root:www-data /var/www/shaarli.mydomain.org
82``` 72sudo chmod -R g+rX /var/www/shaarli.mydomain.org
73sudo chmod -R g+rwX /var/www/shaarli.mydomain.org/{cache/,data/,pagecache/,tmp/}
74```
83 75
84If you use translations in gettext mode, reload your web server. 76Access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli-configuration.md) for more details).
85 77
86Shaarli >= `v0.10.0` manages its front-end dependencies with nodejs. You need to install 78---------------------------------------------------------------
87[yarn](https://yarnpkg.com/lang/en/docs/install/):
88 79
89```bash 80## Migrating and upgrading from Sebsauvage's repository
90$ make build_frontend
91```
92
93### Migrating and upgrading from Sebsauvage's repository
94 81
95If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy. 82If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy.
96 83
@@ -104,7 +91,7 @@ The following guide assumes that:
104 - no versioned file has been locally modified 91 - no versioned file has been locally modified
105 - no untracked files are present 92 - no untracked files are present
106 93
107#### Step 0: show repository information 94### Step 0: show repository information
108 95
109```bash 96```bash
110$ cd /path/to/shaarli 97$ cd /path/to/shaarli
@@ -122,7 +109,7 @@ Your branch is up-to-date with 'origin/master'.
122nothing to commit, working directory clean 109nothing to commit, working directory clean
123``` 110```
124 111
125#### Step 1: update Git remotes 112### Step 1: update Git remotes
126 113
127``` 114```
128$ git remote rename origin sebsauvage 115$ git remote rename origin sebsauvage
@@ -146,7 +133,7 @@ From https://github.com/shaarli/Shaarli
146 * [new tag] v0.7.0 -> v0.7.0 133 * [new tag] v0.7.0 -> v0.7.0
147``` 134```
148 135
149#### Step 2: use the stable community branch 136### Step 2: use the stable community branch
150 137
151```bash 138```bash
152$ git checkout origin/stable -b stable 139$ git checkout origin/stable -b stable
@@ -177,8 +164,7 @@ $ make translate
177 164
178If you use translations in gettext mode, reload your web server. 165If you use translations in gettext mode, reload your web server.
179 166
180Shaarli >= `v0.10.0` manages its front-end dependencies with nodejs. You need to install 167Shaarli >= `v0.10.0` manages its front-end dependencies with nodejs. You need to install [yarn](https://yarnpkg.com/lang/en/docs/install/):
181[yarn](https://yarnpkg.com/lang/en/docs/install/):
182 168
183```bash 169```bash
184$ make build_frontend 170$ make build_frontend
@@ -204,30 +190,14 @@ Writing objects: 100% (3317/3317), done.
204Total 3317 (delta 2050), reused 3301 (delta 2034)to 190Total 3317 (delta 2050), reused 3301 (delta 2034)to
205``` 191```
206 192
207#### Step 3: configuration 193### Step 3: configuration
208 194
209After migrating, access your fresh Shaarli installation from a web browser; the 195After migrating, access your fresh Shaarli installation from a web browser; the
210configuration will then be automatically updated, and new settings added to 196configuration will then be automatically updated, and new settings added to
211`data/config.json.php` (see [Shaarli configuration](Shaarli-configuration) for more 197`data/config.json.php` (see [Shaarli configuration](Shaarli-configuration.md) for more
212details). 198details).
213 199
214## Troubleshooting 200## Troubleshooting
215 201
216If the solutions provided here don't work, please open an issue specifying which version you're upgrading from and to. 202If the solutions provided here don't work, see [Troubleshooting](Troubleshooting.md) and/or open an issue specifying which version you're upgrading from and to.
217
218### You must specify an integer as a key
219
220In `v0.8.1` we changed how link keys are handled (from timestamps to incremental integers).
221Take a look at `data/updates.txt` content.
222
223#### `updates.txt` contains `updateMethodDatastoreIds`
224
225Try to delete it and refresh your page while being logged in.
226
227#### `updates.txt` doesn't exist or doesn't contain `updateMethodDatastoreIds`
228 203
2291. Create `data/updates.txt` if it doesn't exist
2302. Paste this string in the update file `;updateMethodRenameDashTags;`
2313. Login to Shaarli
2324. Delete the update file
2335. Refresh
diff --git a/doc/md/Usage.md b/doc/md/Usage.md
new file mode 100644
index 00000000..6dadde0a
--- /dev/null
+++ b/doc/md/Usage.md
@@ -0,0 +1,111 @@
1## Features
2
3For any item posted to Shaarli (called a _Shaare_), you can customize the following aspects:
4
5- URL to link to
6- Title
7- Free-text description
8- Tags
9- Public/private status
10
11
12### Adding/editing Shaares
13
14While logged in to your Shaarli, you can add, edit or delete Shaares:
15
16- Using the **+Shaare** button: enter the URL you want to share, click `Add link`, fill in the details of your Shaare, and `Save`
17- Using the [Bookmarklet](https://en.wikipedia.org/wiki/Bookmarklet): drag the `✚Shaare link` button from the `Tools` page to your browser's bookmarks bar, click it to share the current page.
18- Using [apps and browser addons](Community-and-related-software.md#mobile-apps)
19- Using the [REST API](https://shaarli.github.io/api-documentation/)
20- Any Shaare can edited by clicking its ![](images/edit_icon.png) `Edit` button.
21
22
23### Tags
24
25Tags can be be used to organize and categorize your Shaares:
26
27- You can rename, merge and delete tags from the _Tools_ menu or the [tag cloud/list](#tag-cloud)
28- Tags are auto-completed (from the list of existing tags) in all dialogs
29- Tags can be combined with text in [search](#search) queries
30
31
32### Public/private Shaares
33
34Additional filter buttons can be found at the top left of the Shaare list **only when logged in**:
35
36- **Only show private Shaares:** Private shares can be searched by clicking the `only show private links` toggle button top left of the Shaares list (only when logged in)
37
38
39### Permalinks
40
41Permalinks are fixed, short links attached to each Shaare. Editing a Shaare will not change it's permalink, each permalink always points to the latest revision of a Shaare.
42
43
44### Text-only (note) Shaares
45
46Shaarli can be used as a minimal blog, notepad, pastebin...: While adding or editing a Shaare, leave the URL field blank to create a text-only ("note") post. This allows you to post any kind of text content, such as blog articles, private or public notes, snippets... There is no character limit! You can access your post from its permalink.
47
48
49### Search
50
51- **Plain text search:** Use `Search text` to search in all fields of all Shaares (Title, URL, Description...). Use double-quotes (example `"exact search"`) to search for the exact expression.
52- **Tags search:** `Filter by tags` allow only displaying Shaares tagged with one or multiple tags (use space to separate tags).
53- **Hidden tags:** tags starting with a dot `.` (example `.secret`) are private. They can only be seen and searched when logged in.
54- **Exclude text/tags:** Use the `-` operator before a word or tag to exclude Shaares matching this word from search results (`NOT` operator).
55- **Untagged links:** Shaares without tags can be searched by clicking the `untagged` toggle button top left of the Shaares list (only when logged in).
56
57Both exclude patterns and exact searches can be combined with normal searches (example `"exact search" term otherterm -notthis "very exact" stuff -notagain`). Only AND (and NOT) search is currrently supported.
58
59Active search terms are displayed on top of the link list. To remove terms/tags from the curent search, click the `x` next to any of them, or simply clear text/tag search fields.
60
61
62### Tag cloud
63
64The `Tag cloud` page diplays a "cloud" or list view of all tags in your Shaarli (most frequently used tags are displayed with a bigger font size)
65
66
67- **Tags list:** click on `Most used` or `Alphabetical` to display tags as a list. You can also edit/delete tags for this page.
68- Click on any tag to search all Shaares matching this tag.
69- **Filtering the tag cloud/list:** Click on the counter next to a tag to show other tags of Shaares with this tag. Repeat this any number of times to further filter the tag cloud. Click `List all links with those tags` to display Shaares matching your current tag filter set.
70
71
72
73### RSS feeds
74
75RSS/ATOM feeds feeds are available (in ATOM with `/feed/atom` and RSS with `/feed/rss`)
76
77- **Filtering RSS feeds:** RSS feeds and picture wall can also be restricted to only return items matching a text/tag search. For example, search for `photography` (text or tags) in Shaarli, then click the `RSS Feed` button. A feed with only matching results is displayed.
78- Add the `&nb` parameter in feed URLs to specify the number of Shaares you want in a feed (default if not specified: `50`). The keyword `all` is available if you want everything.
79- Add the `&permalinks` parameter in feed URLs to point permalinks to the corresponding shaarly entry/link instead of the direct, Shaare URL attribute
80
81![](images/rss-filter-1.png) ![](images/rss-filter-2.png)
82
83```bash
84# examples
85https://shaarli.mydomain.org/feed/atom?permalinks
86https://shaarli.mydomain.org/feed/atom?permalinks&nb=42
87https://shaarli.mydomain.org/feed/atom?permalinks&nb=all
88https://shaarli.mydomain.org/feed/rss?searchtags=nature
89https://shaarli.mydomain.org/links/picture-wall?searchterm=poney
90```
91
92
93### Picture wall
94
95- The picture wall can be filtered by text or tags search in the same way as [RSS feeds](#rss-feeds)
96
97
98### Import/export
99
100To **export Shaares as a HTML file**, under _Tools > Export_, choose:
101
102- `Export all` to export both public and private Shaares
103- `Export public` to export public Shaares only
104- `Export private` to export private Shaares only
105
106Restore by using the `Import` feature.
107
108- These exports contain the full data (URL, title, tags, date, description, public/private status of your Shaares)
109- They can also be imported to your web browser bookmarks.
110
111To **import a HTML bookmarks file** exported from your browser, just use the `Import` feature. For each "folder" in the bookmarks you imported, a new tag will be created (for example a bookmark in `Movies > Sci-fi` folder will be tagged `Movies` `Sci-fi`).
diff --git a/doc/md/dev/Development.md b/doc/md/dev/Development.md
new file mode 100644
index 00000000..5c085e03
--- /dev/null
+++ b/doc/md/dev/Development.md
@@ -0,0 +1,179 @@
1# Development
2
3Please read [Contributing to Shaarli](https://github.com/shaarli/Shaarli/tree/master/CONTRIBUTING.md)
4
5## Guidelines
6
7
8- [Unit tests](Unit-tests)
9- Javascript linting - Shaarli uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript).
10Run `make eslint` to check JS style.
11- [GnuPG signature](GnuPG-signature) for tags/releases
12
13
14## Third-party libraries
15
16CSS:
17
18- Yahoo UI [CSS Reset](http://yuilibrary.com/yui/docs/cssreset/) - standardize cross-browser rendering
19
20Javascript:
21
22- [Awesomeplete](https://leaverou.github.io/awesomplete/) ([GitHub](https://github.com/LeaVerou/awesomplete)) - autocompletion in input forms
23- [bLazy](http://dinbror.dk/blazy/) ([GitHub](https://github.com/dinbror/blazy)) - lazy loading for thumbnails
24- [qr.js](http://neocotic.com/qr.js/) ([GitHub](https://github.com/neocotic/qr.js)) - QR code generation
25
26PHP (managed through [`composer.json`](https://github.com/shaarli/Shaarli/blob/master/composer.json)):
27
28- [RainTPL](https://github.com/rainphp/raintpl) - HTML templating for PHP
29- [`shaarli/netscape-bookmark-parser`](https://packagist.org/packages/shaarli/netscape-bookmark-parser) - Import bookmarks from Netscape files
30- [`erusev/parsedown`](https://packagist.org/packages/erusev/parsedown) - Parse MarkDown syntax for the MarkDown plugin
31- [`slim/slim`](https://packagist.org/packages/slim/slim) - Handle routes and middleware for the REST API
32- [`ArthurHoaro/web-thumbnailer`](https://github.com/ArthurHoaro/web-thumbnailer) - PHP library which will retrieve a thumbnail for any given URL
33- [`pubsubhubbub/publisher`](https://github.com/pubsubhubbub/php-publisher) - A PubSubHubbub publisher module for PHP.
34- [`gettext/gettext`](https://github.com/php-gettext/Gettext) - PHP library to collect and manipulate gettext (.po, .mo, .php, .json, etc)
35
36
37## Security
38
39- The password is salted, hashed and stored in the data subdirectory, in a PHP file, and protected by htaccess. Even if the webserver does not support htaccess, the hash is not readable by URL. Even if the .php file is stolen, the password cannot deduced from the hash. The salt prevents rainbow-tables attacks.
40- Directories are protected using `.htaccess` files
41- Forms are protected against [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery):
42 - Forms which act on data (save,delete…) contain a token generated by the server.
43 - Any posted form which does not contain a valid token is rejected.
44 - Any token can only be used once.
45 - Tokens are attached to the session and cannot be reused in another session.
46- Sessions automatically expire after 60 minutes.
47- Sessions are protected against hijacking: the session ID cannot be used from a different IP address.
48- Links are stored as an associative array which is serialized, compressed (with deflate), base64-encoded and saved as a comment in a `.php` file - even if the server does not support `.htaccess` files, the data file will still not be readable by URL.
49- Bruteforce protection: Successful and failed login attempts are logged - IP bans are enforced after a configurable amount of failures. Logs can also be used consumed by [fail2ban](../Server-configuration.md#fail2ban)
50- A pop-up notification is shown when a new release is available.
51
52## Link structure
53
54Every link available through the `LinkDB` object is represented as an array
55containing the following fields:
56
57 * `id` (integer): Unique identifier.
58 * `title` (string): Title of the link.
59 * `url` (string): URL of the link. Used for displayable links (without redirector, url encoding, etc.).
60 Can be absolute or relative for Notes.
61 * `real_url` (string): Real destination URL, can be redirected, encoded, etc.
62 * `shorturl` (string): Permalink small hash.
63 * `description` (string): Link text description.
64 * `private` (boolean): whether the link is private or not.
65 * `tags` (string): all link tags separated by a single space
66 * `thumbnail` (string|boolean): relative path of the thumbnail cache file, or false if there isn't any.
67 * `created` (DateTime): link creation date time.
68 * `updated` (DateTime): last modification date time.
69
70Small hashes are used to make a link to an entry in Shaarli. They are unique: the date of the item (eg. `20110923_150523`) is hashed with CRC32, then converted to base64 and some characters are replaced. They are always 6 characters longs and use only `A-Z a-z 0-9 - _` and `@`.
71
72
73## Directory structure
74
75Here is the directory structure of Shaarli and the purpose of the different files:
76
77```bash
78 index.php # Main program
79 application/ # Shaarli classes
80 ├── LinkDB.php
81
82 ...
83
84 └── Utils.php
85 tests/ # Shaarli unitary & functional tests
86 ├── LinkDBTest.php
87
88 ...
89
90 ├── utils # utilities to ease testing
91 │ └── ReferenceLinkDB.php
92 └── UtilsTest.php
93 assets/
94 ├── common/ # Assets shared by multiple themes
95 ├── ...
96 ├── default/ # Assets for the default template, before compilation
97 ├── fonts/ # Font files
98 ├── img/ # Images used by the default theme
99 ├── js/ # JavaScript files in ES6 syntax
100 ├── scss/ # SASS files
101 └── vintage/ # Assets for the vintage template, before compilation
102 └── ...
103 COPYING # Shaarli license
104 inc/ # static assets and 3rd party libraries
105 └── rain.tpl.class.php # RainTPL templating library
106 images/ # Images and icons used in Shaarli
107 data/ # data storage: bookmark database, configuration, logs, banlist...
108 ├── config.json.php # Shaarli configuration (login, password, timezone, title...)
109 ├── datastore.php # Your link database (compressed).
110 ├── ipban.php # IP address ban system data
111 ├── lastupdatecheck.txt # Update check timestamp file
112 └── log.txt # login/IPban log.
113 tpl/ # RainTPL templates for Shaarli. They are used to build the pages.
114 ├── default/ # Default Shaarli theme
115 ├── fonts/ # Font files
116 ├── img/ # Images
117 ├── js/ # JavaScript files compiled by Babel and compatible with all browsers
118 ├── css/ # CSS files compiled with SASS
119 └── vintage/ # Legacy Shaarli theme
120 └── ...
121 cache/ # thumbnails cache
122 # This directory is automatically created. You can erase it anytime you want.
123 tmp/ # Temporary directory for compiled RainTPL templates.
124 # This directory is automatically created. You can erase it anytime you want.
125 vendor/ # Third-party dependencies. This directory is created by Composer
126```
127
128Shaarli needs read access to:
129
130- the root index.php file
131- the `application/`, `plugins/` and `inc/` directories (recursively)
132
133Shaarli needs read/write access to the `cache/`, `data/`, `pagecache/`, and `tmp/` directories
134
135
136## Automation
137
138A [`Makefile`](https://github.com/shaarli/Shaarli/blob/master/Makefile) is available to perform project-related operations:
139
140- [Static analysis](#Static-analysis) - check that the code is compliant to PHP conventions
141- [Unit tests](#Unit-tests) - ensure there are no regressions introduced by new commits
142- Documentation - generate a local HTML copy of the markdown documentation
143
144### Continuous Integration
145
146[Travis CI](http://docs.travis-ci.com/) is a Continuous Integration build server, that runs a build:
147
148- each time a commit is merged to the mainline (`master` branch)
149- each time a Pull Request is submitted or updated
150
151After all jobs have finished, Travis returns the results to GitHub:
152
153- a status icon represents the result for the `master` branch: [![](https://api.travis-ci.org/shaarli/Shaarli.svg)](https://travis-ci.org/shaarli/Shaarli)
154- Pull Requests are updated with the Travis build result.
155
156See [`.travis.yml`](https://github.com/shaarli/Shaarli/blob/master/.travis.yml).
157
158
159### Documentation
160
161[mkdocs](https://www.mkdocs.org/) is used to convert markdown documentation to HTML pages. The [public documentation](https://shaarli.readthedocs.io/en/master/) website is rendered and hosted by [readthedocs.org](https://readthedocs.org/). A copy of the documentation is also included in prebuilt [release archives](https://github.com/shaarli/Shaarli/releases) (`doc/html/` path in your Shaarli installation). To generate the HTML documentation locally, install a recent version of Python `setuptools` and run `make doc`.
162
163
164## Static analysis
165
166Patches should try to stick to the [PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), especially:
167
168- [PSR-1](http://www.php-fig.org/psr/psr-1/) - Basic Coding Standard
169- [PSR-2](http://www.php-fig.org/psr/psr-2/) - Coding Style Guide
170
171
172**Work in progress:** Static analysis is currently being discussed here: in [#95 - Fix coding style (static analysis)](https://github.com/shaarli/Shaarli/issues/95), [#130 - Continuous Integration tools & features](https://github.com/shaarli/Shaarli/issues/130)
173
174Static analysis tools can be installed with Composer, and used through Shaarli's [Makefile](https://github.com/shaarli/Shaarli/blob/master/Makefile).
175
176For an overview of the available features, see:
177
178- [Code quality: Makefile to run static code checkers](https://github.com/shaarli/Shaarli/pull/124) (#124)
179- [Run PHPCS against different coding standards](https://github.com/shaarli/Shaarli/pull/276) (#276)
diff --git a/doc/md/GnuPG-signature.md b/doc/md/dev/GnuPG-signature.md
index d1fc10a5..25578001 100644
--- a/doc/md/GnuPG-signature.md
+++ b/doc/md/dev/GnuPG-signature.md
@@ -1,24 +1,16 @@
1## Introduction 1## Introduction
2### PGP and GPG 2### PGP and GPG
3[Gnu Privacy Guard](https://gnupg.org/) (GnuPG) is an Open Source implementation of the 3[Gnu Privacy Guard](https://gnupg.org/) (GnuPG) is an Open Source implementation of the [Pretty Good Privacy](https://en.wikipedia.org/wiki/Pretty_Good_Privacy#OpenPGP) (OpenPGP) specification. Its main purposes are digital authentication, signature and encryption. It is often used by the [FLOSS](https://en.wikipedia.org/wiki/Free_and_open-source_software) community to verify:
4[Pretty Good Privacy](https://en.wikipedia.org/wiki/Pretty_Good_Privacy#OpenPGP)
5(OpenPGP) specification. Its main purposes are digital authentication, signature and encryption.
6 4
7It is often used by the [FLOSS](https://en.wikipedia.org/wiki/Free_and_open-source_software) community to verify: 5- Linux package signatures: Debian [SecureApt](https://wiki.debian.org/SecureApt), ArchLinux [Master Keys](https://www.archlinux.org/master-keys/)
6- [Version control](https://en.wikipedia.org/wiki/Revision_control) releases & maintainer identity
8 7
9- Linux package signatures: Debian [SecureApt](https://wiki.debian.org/SecureApt), ArchLinux [Master 8> You MUST understand that presence of data in the keyserver (pools) in no way connotes trust. Anyone can generate a key, with any name or email address, and upload it. All security and trust comes from evaluating security at the “object level”, via PGP [Web of trust](https://en.wikipedia.org/wiki/Web_of_trust) signatures. This keyserver makes it possible to retrieve keys, looking them up via various indices, but the collection of keys in this public pool is KNOWN to contain malicious and fraudulent keys. It is the common expectation of server operators that users understand this and use software which, like all known common OpenPGP implementations, evaluates trust accordingly. This expectation is so common that it is not normally explicitly stated.
10Keys](https://www.archlinux.org/master-keys/)
11- [SCM](https://en.wikipedia.org/wiki/Revision_control) releases & maintainer identity
12 9
13### Trust 10-- Phil Pennock (author of the [SKS](https://bitbucket.org/skskeyserver/sks-keyserver/wiki/Home) key server - http://sks.spodhuis.org/)
14To quote Phil Pennock (the author of the [SKS](https://bitbucket.org/skskeyserver/sks-keyserver/wiki/Home) key server - http://sks.spodhuis.org/):
15 11
16> You MUST understand that presence of data in the keyserver (pools) in no way connotes trust. Anyone can generate a key, with any name or email address, and upload it. All security and trust comes from evaluating security at the “object level”, via PGP Web-Of-Trust signatures. This keyserver makes it possible to retrieve keys, looking them up via various indices, but the collection of keys in this public pool is KNOWN to contain malicious and fraudulent keys. It is the common expectation of server operators that users understand this and use software which, like all known common OpenPGP implementations, evaluates trust accordingly. This expectation is so common that it is not normally explicitly stated. 12Trust can be gained by having your key signed by other people (and signing their key back, too :) ), for instance during [key signing parties](https://en.wikipedia.org/wiki/Key_signing_party): [Keysigning party HOWTO](http://www.cryptnet.net/fdp/crypto/keysigning_party/en/keysigning_party.html),
17 13
18Trust can be gained by having your key signed by other people (and signing their key back, too :) ), for instance during [key signing parties](https://en.wikipedia.org/wiki/Key_signing_party), see:
19
20- [The Keysigning party HOWTO](http://www.cryptnet.net/fdp/crypto/keysigning_party/en/keysigning_party.html)
21- [Web of trust](https://en.wikipedia.org/wiki/Web_of_trust)
22 14
23## Generate a GPG key 15## Generate a GPG key
24- [Generating a GPG key for Git tagging](http://stackoverflow.com/a/16725717) (StackOverflow) 16- [Generating a GPG key for Git tagging](http://stackoverflow.com/a/16725717) (StackOverflow)
diff --git a/doc/md/Plugin-System.md b/doc/md/dev/Plugin-system.md
index d5b16e2d..f09fadc2 100644
--- a/doc/md/Plugin-System.md
+++ b/doc/md/dev/Plugin-system.md
@@ -1,19 +1,16 @@
1[**I am a developer: ** Developer API](#developer-api) 1# Plugin system
2
3[**I am a template designer: ** Guide for template designers](#guide-for-template-designer)
4
5---
6 2
7## Developer API 3## Developer API
8 4
9### What can I do with plugins? 5### What can I do with plugins?
10 6
11The plugin system let you: 7The plugin system lets you:
12 8
13- insert content into specific places across templates. 9- insert content into specific places across templates.
14- alter data before templates rendering. 10- alter data before templates rendering.
15- alter data before saving new links. 11- alter data before saving new links.
16 12
13
17### How can I create a plugin for Shaarli? 14### How can I create a plugin for Shaarli?
18 15
19First, chose a plugin name, such as `demo_plugin`. 16First, chose a plugin name, such as `demo_plugin`.
@@ -30,6 +27,7 @@ You should have the following tree view:
30| |---| demo_plugin.php 27| |---| demo_plugin.php
31``` 28```
32 29
30
33### Plugin initialization 31### Plugin initialization
34 32
35At the beginning of Shaarli execution, all enabled plugins are loaded. At this point, the plugin system looks for an `init()` function in the <plugin_name>.php to execute and run it if it exists. This function must be named this way, and takes the `ConfigManager` as parameter. 33At the beginning of Shaarli execution, all enabled plugins are loaded. At this point, the plugin system looks for an `init()` function in the <plugin_name>.php to execute and run it if it exists. This function must be named this way, and takes the `ConfigManager` as parameter.
@@ -63,6 +61,7 @@ For example, if my plugin want to add data to the header, this function is neede
63 61
64If this function is declared, and the plugin enabled, it will be called every time Shaarli is rendering the header. 62If this function is declared, and the plugin enabled, it will be called every time Shaarli is rendering the header.
65 63
64
66### Plugin's data 65### Plugin's data
67 66
68#### Parameters 67#### Parameters
@@ -73,6 +72,26 @@ Every hook function has a `$data` parameter. Its content differs for each hooks.
73 72
74 return $data; 73 return $data;
75 74
75#### Special data
76
77Special additional data are passed to every hook through the
78`$data` parameter to give you access to additional context, and services.
79
80Complete list:
81
82 * `_PAGE_` (string): if the current hook is used to render a template, its name is passed through this additional parameter.
83 * `_LOGGEDIN_` (bool): whether the user is logged in or not.
84 * `_BASE_PATH_` (string): if Shaarli instance is hosted under a subfolder, contains the subfolder path to `index.php` (e.g. `https://domain.tld/shaarli/` -> `/shaarli/`).
85 * `_BOOKMARK_SERVICE_` (`BookmarkServiceInterface`): bookmark service instance, for advanced usage.
86
87Example:
88
89```php
90if ($data['_PAGE_'] === TemplatePage::LINKLIST && $data['LOGGEDIN'] === true) {
91 // Do something for logged in users when the link list is rendered
92}
93```
94
76#### Filling templates placeholder 95#### Filling templates placeholder
77 96
78Template placeholders are displayed in template in specific places. 97Template placeholders are displayed in template in specific places.
@@ -89,13 +108,14 @@ array_push($data['top_placeholder'], 'My', 'content');
89return $data; 108return $data;
90``` 109```
91 110
111
92#### Data manipulation 112#### Data manipulation
93 113
94When a page is displayed, every variable send to the template engine is passed to plugins before that in `$data`. 114When a page is displayed, every variable send to the template engine is passed to plugins before that in `$data`.
95 115
96The data contained by this array can be altered before template rendering. 116The data contained by this array can be altered before template rendering.
97 117
98For exemple, in linklist, it is possible to alter every title: 118For example, in linklist, it is possible to alter every title:
99 119
100```php 120```php
101// mind the reference if you want $data to be altered 121// mind the reference if you want $data to be altered
@@ -119,19 +139,40 @@ Each file contain two keys:
119 139
120> Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file. 140> Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file.
121 141
142### Understanding relative paths
143
144Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder.
145This means that you can *never* use absolute paths (eg `/plugins/mything/file.png`).
146
147If a file needs to be included in server end, use simple relative path:
148`PluginManager::$PLUGINS_PATH . '/mything/template.html'`.
149
150If it needs to be included in front end side (e.g. an image),
151the relative path must be prefixed with special data:
152
153 * if it's a link that will need to be processed by Shaarli, use `_BASE_PATH_`:
154 for e.g. `$data['_BASE_PATH_'] . '/admin/tools`.
155 * if you want to include an asset, you need to add the root URL (base path without `/index.php`, for people using Shaarli without URL rewriting), then use `_ROOT_PATH_`:
156 for e.g
157`$['_ROOT_PATH_'] . '/' . PluginManager::$PLUGINS_PATH . '/mything/picture.png`.
158
159Note that special placeholders for CSS and JS files (respectively `css_files` and `js_files`) are already prefixed
160with the root path in template files.
161
122### It's not working! 162### It's not working!
123 163
124Use `demo_plugin` as a functional example. It covers most of the plugin system features. 164Use `demo_plugin` as a functional example. It covers most of the plugin system features.
125 165
126If it's still not working, please [open an issue](https://github.com/shaarli/Shaarli/issues/new). 166If it's still not working, please [open an issue](https://github.com/shaarli/Shaarli/issues/new).
127 167
168
128### Hooks 169### Hooks
129 170
130| Hooks | Description | 171| Hooks | Description |
131| ------------- |:-------------:| 172| ------------- |:-------------:|
132| [render_header](#render_header) | Allow plugin to add content in page headers. | 173| [render_header](#render_header) | Allow plugin to add content in page headers. |
133| [render_includes](#render_includes) | Allow plugin to include their own CSS files. | 174| [render_includes](#render_includes) | Allow plugin to include their own CSS files. |
134| [render_footer](#render_footer) | Allow plugin to add content in page footer and include their own JS files. | 175| [render_footer](#render_footer) | Allow plugin to add content in page footer and include their own JS files. |
135| [render_linklist](#render_linklist) | It allows to add content at the begining and end of the page, after every link displayed and to alter link data. | 176| [render_linklist](#render_linklist) | It allows to add content at the begining and end of the page, after every link displayed and to alter link data. |
136| [render_editlink](#render_editlink) | Allow to add fields in the form, or display elements. | 177| [render_editlink](#render_editlink) | Allow to add fields in the form, or display elements. |
137| [render_tools](#render_tools) | Allow to add content at the end of the page. | 178| [render_tools](#render_tools) | Allow to add content at the end of the page. |
@@ -145,19 +186,16 @@ If it's still not working, please [open an issue](https://github.com/shaarli/Sha
145| [save_plugin_parameters](#save_plugin_parameters) | Allow to manipulate plugin parameters before they're saved. | 186| [save_plugin_parameters](#save_plugin_parameters) | Allow to manipulate plugin parameters before they're saved. |
146 187
147 188
148
149#### render_header 189#### render_header
150 190
151Triggered on every page. 191Triggered on every page - allows plugins to add content in page headers.
152 192
153Allow plugin to add content in page headers.
154 193
155##### Data 194##### Data
156 195
157`$data` is an array containing: 196`$data` is an array containing:
158 197
159- `_PAGE_`: current target page (eg: `linklist`, `picwall`, etc.). 198 - [Special data](#special-data)
160- `_LOGGEDIN_`: true if user is logged in, false otherwise.
161 199
162##### Template placeholders 200##### Template placeholders
163 201
@@ -175,18 +213,16 @@ List of placeholders:
175 213
176![fields_toolbar_example](http://i.imgur.com/3GMifI2.png) 214![fields_toolbar_example](http://i.imgur.com/3GMifI2.png)
177 215
178#### render_includes
179 216
180Triggered on every page. 217#### render_includes
181 218
182Allow plugin to include their own CSS files. 219Triggered on every page - allows plugins to include their own CSS files.
183 220
184##### Data 221##### data
185 222
186`$data` is an array containing: 223`$data` is an array containing:
187 224
188- `_PAGE_`: current target page (eg: `linklist`, `picwall`, etc.). 225 - [Special data](#special-data)
189- `_LOGGEDIN_`: true if user is logged in, false otherwise.
190 226
191##### Template placeholders 227##### Template placeholders
192 228
@@ -198,18 +234,18 @@ List of placeholders:
198 234
199> Note: only add the path of the CSS file. E.g: `plugins/demo_plugin/custom_demo.css`. 235> Note: only add the path of the CSS file. E.g: `plugins/demo_plugin/custom_demo.css`.
200 236
237
201#### render_footer 238#### render_footer
202 239
203Triggered on every page. 240Triggered on every page.
204 241
205Allow plugin to add content in page footer and include their own JS files. 242Allow plugin to add content in page footer and include their own JS files.
206 243
207##### Data 244##### data
208 245
209`$data` is an array containing: 246`$data` is an array containing:
210 247
211- `_PAGE_`: current target page (eg: `linklist`, `picwall`, etc.). 248 - [Special data](#special-data)
212- `_LOGGEDIN_`: true if user is logged in, false otherwise.
213 249
214##### Template placeholders 250##### Template placeholders
215 251
@@ -226,20 +262,21 @@ List of placeholders:
226 262
227> Note: only add the path of the JS file. E.g: `plugins/demo_plugin/custom_demo.js`. 263> Note: only add the path of the JS file. E.g: `plugins/demo_plugin/custom_demo.js`.
228 264
265
229#### render_linklist 266#### render_linklist
230 267
231Triggered when `linklist` is displayed (list of links, permalink, search, tag filtered, etc.). 268Triggered when `linklist` is displayed (list of links, permalink, search, tag filtered, etc.).
232 269
233It allows to add content at the begining and end of the page, after every link displayed and to alter link data. 270It allows to add content at the begining and end of the page, after every link displayed and to alter link data.
234 271
235##### Data 272##### data
236 273
237`$data` is an array containing: 274`$data` is an array containing:
238 275
239- `_LOGGEDIN_`: true if user is logged in, false otherwise. 276 - All templates data, including links.
240- All templates data, including links. 277 - [Special data](#special-data)
241 278
242##### Template placeholders 279##### template placeholders
243 280
244Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array. 281Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array.
245 282
@@ -261,19 +298,21 @@ List of placeholders:
261 298
262![plugin_end_zone_example](http://i.imgur.com/6IoRuop.png) 299![plugin_end_zone_example](http://i.imgur.com/6IoRuop.png)
263 300
301
264#### render_editlink 302#### render_editlink
265 303
266Triggered when the link edition form is displayed. 304Triggered when the link edition form is displayed.
267 305
268Allow to add fields in the form, or display elements. 306Allow to add fields in the form, or display elements.
269 307
270##### Data 308##### data
271 309
272`$data` is an array containing: 310`$data` is an array containing:
273 311
274- All templates data. 312 - All templates data.
313 - [Special data](#special-data)
275 314
276##### Template placeholders 315##### template placeholders
277 316
278Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array. 317Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array.
279 318
@@ -283,19 +322,21 @@ List of placeholders:
283 322
284![edit_link_plugin_example](http://i.imgur.com/5u17Ens.png) 323![edit_link_plugin_example](http://i.imgur.com/5u17Ens.png)
285 324
325
286#### render_tools 326#### render_tools
287 327
288Triggered when the "tools" page is displayed. 328Triggered when the "tools" page is displayed.
289 329
290Allow to add content at the end of the page. 330Allow to add content at the end of the page.
291 331
292##### Data 332##### data
293 333
294`$data` is an array containing: 334`$data` is an array containing:
295 335
296- All templates data. 336 - All templates data.
337 - [Special data](#special-data)
297 338
298##### Template placeholders 339##### template placeholders
299 340
300Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array. 341Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array.
301 342
@@ -305,20 +346,21 @@ List of placeholders:
305 346
306![tools_plugin_example](http://i.imgur.com/Bqhu9oQ.png) 347![tools_plugin_example](http://i.imgur.com/Bqhu9oQ.png)
307 348
349
308#### render_picwall 350#### render_picwall
309 351
310Triggered when picwall is displayed. 352Triggered when picwall is displayed.
311 353
312Allow to add content at the top and bottom of the page. 354Allow to add content at the top and bottom of the page.
313 355
314##### Data 356##### data
315 357
316`$data` is an array containing: 358`$data` is an array containing:
317 359
318- `_LOGGEDIN_`: true if user is logged in, false otherwise. 360 - All templates data.
319- All templates data. 361 - [Special data](#special-data)
320 362
321##### Template placeholders 363##### template placeholders
322 364
323Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array. 365Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array.
324 366
@@ -329,18 +371,19 @@ List of placeholders:
329 371
330![plugin_start_end_zone_example](http://i.imgur.com/tVTQFER.png) 372![plugin_start_end_zone_example](http://i.imgur.com/tVTQFER.png)
331 373
374
332#### render_tagcloud 375#### render_tagcloud
333 376
334Triggered when tagcloud is displayed. 377Triggered when tagcloud is displayed.
335 378
336Allow to add content at the top and bottom of the page. 379Allow to add content at the top and bottom of the page.
337 380
338##### Data 381##### data
339 382
340`$data` is an array containing: 383`$data` is an array containing:
341 384
342- `_LOGGEDIN_`: true if user is logged in, false otherwise. 385 - All templates data.
343- All templates data. 386 - [Special data](#special-data)
344 387
345##### Template placeholders 388##### Template placeholders
346 389
@@ -360,16 +403,14 @@ For each tag, the following placeholder can be used:
360 403
361#### render_taglist 404#### render_taglist
362 405
363Triggered when taglist is displayed. 406Triggered when taglist is displayed - allows to add content at the top and bottom of the page.
364
365Allow to add content at the top and bottom of the page.
366 407
367##### Data 408##### data
368 409
369`$data` is an array containing: 410`$data` is an array containing:
370 411
371- `_LOGGEDIN_`: true if user is logged in, false otherwise. 412 - All templates data.
372- All templates data. 413 - [Special data](#special-data)
373 414
374##### Template placeholders 415##### Template placeholders
375 416
@@ -390,12 +431,13 @@ Triggered when tagcloud is displayed.
390 431
391Allow to add content at the top and bottom of the page, the bottom of each link and to alter data. 432Allow to add content at the top and bottom of the page, the bottom of each link and to alter data.
392 433
393##### Data 434
435##### data
394 436
395`$data` is an array containing: 437`$data` is an array containing:
396 438
397- `_LOGGEDIN_`: true if user is logged in, false otherwise. 439 - All templates data, including links.
398- All templates data, including links. 440 - [Special data](#special-data)
399 441
400##### Template placeholders 442##### Template placeholders
401 443
@@ -410,19 +452,19 @@ List of placeholders:
410- `plugin_start_zone`: before displaying the template content. 452- `plugin_start_zone`: before displaying the template content.
411- `plugin_end_zone`: after displaying the template content. 453- `plugin_end_zone`: after displaying the template content.
412 454
455
413#### render_feed 456#### render_feed
414 457
415Triggered when the ATOM or RSS feed is displayed. 458Triggered when the ATOM or RSS feed is displayed.
416 459
417Allow to add tags in the feed, either in the header or for each items. Items (links) can also be altered before being rendered. 460Allow to add tags in the feed, either in the header or for each items. Items (links) can also be altered before being rendered.
418 461
419##### Data 462##### data
420 463
421`$data` is an array containing: 464`$data` is an array containing:
422 465
423- `_LOGGEDIN_`: true if user is logged in, false otherwise. 466 - All templates data, including links.
424- `_PAGE_`: containing either `rss` or `atom`. 467 - [Special data](#special-data)
425- All templates data, including links.
426 468
427##### Template placeholders 469##### Template placeholders
428 470
@@ -436,13 +478,14 @@ For each links:
436 478
437- `feed_plugins`: additional tag for every link entry. 479- `feed_plugins`: additional tag for every link entry.
438 480
481
439#### save_link 482#### save_link
440 483
441Triggered when a link is save (new link or edit). 484Triggered when a link is save (new link or edit).
442 485
443Allow to alter the link being saved in the datastore. 486Allow to alter the link being saved in the datastore.
444 487
445##### Data 488##### data
446 489
447`$data` is an array containing the link being saved: 490`$data` is an array containing the link being saved:
448 491
@@ -456,6 +499,8 @@ Allow to alter the link being saved in the datastore.
456- created 499- created
457- updated 500- updated
458 501
502Also [special data](#special-data).
503
459 504
460#### delete_link 505#### delete_link
461 506
@@ -463,9 +508,9 @@ Triggered when a link is deleted.
463 508
464Allow to execute any action before the link is actually removed from the datastore 509Allow to execute any action before the link is actually removed from the datastore
465 510
466##### Data 511##### data
467 512
468`$data` is an array containing the link being saved: 513`$data` is an array containing the link being deleted:
469 514
470- id 515- id
471- title 516- title
@@ -477,6 +522,7 @@ Allow to execute any action before the link is actually removed from the datasto
477- created 522- created
478- updated 523- updated
479 524
525Also [special data](#special-data).
480 526
481#### save_plugin_parameters 527#### save_plugin_parameters
482 528
@@ -485,15 +531,16 @@ Triggered when the plugin parameters are saved from the plugin administration pa
485Plugins can perform an action every times their settings are updated. 531Plugins can perform an action every times their settings are updated.
486For example it is used to update the CSS file of the `default_colors` plugins. 532For example it is used to update the CSS file of the `default_colors` plugins.
487 533
488##### Data 534##### data
489 535
490`$data` input contains the `$_POST` array. 536`$data` input contains the `$_POST` array.
491 537
492So if the plugin has a parameter called `MYPLUGIN_PARAMETER`, 538So if the plugin has a parameter called `MYPLUGIN_PARAMETER`,
493the array will contain an entry with `MYPLUGIN_PARAMETER` as a key. 539the array will contain an entry with `MYPLUGIN_PARAMETER` as a key.
494 540
541Also [special data](#special-data).
495 542
496## Guide for template designer 543## Guide for template designers
497 544
498### Plugin administration 545### Plugin administration
499 546
@@ -515,7 +562,7 @@ Otherwise, you can use your own JS as long as this field is send by the form:
515 562
516### Placeholder system 563### Placeholder system
517 564
518In order to make plugins work with every custom themes, you need to add variable placeholder in your templates. 565In order to make plugins work with every custom themes, you need to add variable placeholder in your templates.
519 566
520It's a RainTPL loop like this: 567It's a RainTPL loop like this:
521 568
@@ -537,7 +584,7 @@ At the end of the menu:
537 584
538At the end of file, before clearing floating blocks: 585At the end of file, before clearing floating blocks:
539 586
540 {if="!empty($plugin_errors) && isLoggedIn()"} 587 {if="!empty($plugin_errors) && $is_logged_in"}
541 <ul class="errors"> 588 <ul class="errors">
542 {loop="plugin_errors"} 589 {loop="plugin_errors"}
543 <li>{$value}</li> 590 <li>{$value}</li>
diff --git a/doc/md/dev/Release-Shaarli.md b/doc/md/dev/Release-Shaarli.md
new file mode 100644
index 00000000..2c772406
--- /dev/null
+++ b/doc/md/dev/Release-Shaarli.md
@@ -0,0 +1,145 @@
1# Release Shaarli
2
3## Requirements
4
5This guide assumes that you have:
6
7- a GPG key matching your GitHub authentication credentials/email (the email address identified by the GPG key is the same as the one in your `~/.gitconfig`)
8- a GitHub fork of Shaarli
9- a local clone of your Shaarli fork, with the following remotes:
10 - `origin` pointing to your GitHub fork
11 - `upstream` pointing to the main Shaarli repository
12- maintainer permissions on the main Shaarli repository, to:
13 - push the signed tag
14 - create a new release
15- [Composer](https://getcomposer.org/) needs to be installed
16- The [venv](https://docs.python.org/3/library/venv.html) Python 3 module needs to be installed for HTML documentation generation.
17
18## Release notes and `CHANGELOG.md`
19
20GitHub allows drafting the release notes for the upcoming release, from the [Releases](https://github.com/shaarli/Shaarli/releases) page. This way, the release note can be drafted while contributions are merged to `master`. See http://keepachangelog.com/en/0.3.0/ for changelog formatting.
21
22`CHANGELOG.md` should contain the same information as the release note draft for the upcoming version. Update it to:
23
24- add new entries (additions, fixes, etc.)
25- mark the current version as released by setting its date and link
26- add a new section for the future unreleased version
27
28```bash
29## [v0.x.y](https://github.com/shaarli/Shaarli/releases/tag/v0.x.y) - UNRELEASES
30
31### Added
32
33### Changed
34
35### Fixed
36
37### Removed
38
39### Deprecated
40
41### Security
42
43```
44
45
46## Update the list of Git contributors
47
48```bash
49$ make authors
50$ git commit -s -m "Update AUTHORS"
51```
52
53## Create and merge a Pull Request
54
55Create a Pull Request to marge changes from your remote, into `master` in the community Shaarli repository, and have it merged.
56
57
58## Create the release branch and update shaarli_version.php
59
60```bash
61# fetch latest changes from master to your local copy
62git checkout master
63git pull upstream master
64
65# If releasing a new minor version, create a release branch
66$ git checkout -b v0.x
67
68# Bump shaarli_version.php from dev to 0.x.0, **without the v**
69$ vim shaarli_version.php
70$ git add shaarli_version
71$ git commit -s -m "Bump Shaarli version to v0.x.0"
72$ git push upstream v0.x
73```
74
75## Create and push a signed tag
76
77Git [tags](http://git-scm.com/book/en/v2/Distributed-Git-Maintaining-a-Project#Tagging-Your-Releases) are used to identify specific revisions with a unique version number that follows [semantic versioning](https://semver.org/)
78
79```bash
80# update your local copy
81git checkout v0.5
82git pull upstream v0.5
83
84# create a signed tag
85git tag -s -m "Release v0.5.0" v0.5.0
86
87# push the tag to upstream
88git push --tags upstream
89```
90
91Here is how to verify a signed tag. [`v0.5.0`](https://github.com/shaarli/Shaarli/releases/tag/v0.5.0) is the first GPG-signed tag pushed on the Community Shaarli. Let's have a look at its signature!
92
93```bash
94# update the list of available tags
95git fetch upstream
96
97# get the SHA1 reference of the tag
98git show-ref tags/v0.5.0
99# gives: f7762cf803f03f5caf4b8078359a63783d0090c1 refs/tags/v0.5.0
100
101# verify the tag signature information
102git verify-tag f7762cf803f03f5caf4b8078359a63783d0090c1
103# gpg: Signature made Thu 30 Jul 2015 11:46:34 CEST using RSA key ID 4100DF6F
104# gpg: Good signature from "VirtualTam <virtualtam@flibidi.net>" [ultimate]
105```
106
107## Publish the GitHub release
108
109- In the `master` banch, update version badges in `README.md` to point to the newly released Shaarli version
110- Update the previously drafted [release](https://github.com/shaarli/Shaarli/releases) (notes, tag) and publish it
111- Profit!
112
113
114## Generate full release zip archives
115
116Release archives will contain Shaarli code plus all required third-party libraries. They are useful for users who:
117
118- have no SSH access, no possibility to install PHP packages/server extensions, no possibility to run scripts (shared hosting)
119- do not want to install build/dev dependencies on their server
120
121 `git checkout` the appropriate branch, then:
122
123```bash
124# checkout the appropriate branch
125git checkout 0.x.y
126# generate zip archives
127make release_archive
128```
129
130This will create `shaarli-v0.x.y-full.tar`, `shaarli-v0.x.y-full.zip`. These archives need to be manually uploaded on the previously created GitHub [release](https://github.com/shaarli/Shaarli/releases).
131
132
133### Update the `latest` branch
134
135```bash
136# checkout the 'latest' branch
137git checkout latest
138# merge changes from your newly published release branch
139git merge v0.x.y
140# fix eventual conflicts with git mergetool...
141# run tests
142make test
143# push the latest branch
144git push upstream latest
145```
diff --git a/doc/md/Theming.md b/doc/md/dev/Theming.md
index eb84e11c..1ad30465 100644
--- a/doc/md/Theming.md
+++ b/doc/md/dev/Theming.md
@@ -1,3 +1,5 @@
1# Theming
2
1## Foreword 3## Foreword
2 4
3There are two ways of customizing how Shaarli looks: 5There are two ways of customizing how Shaarli looks:
@@ -43,6 +45,7 @@ Installation:
43- [kalvn/shaarli-blocks](https://github.com/kalvn/shaarli-blocks) - A template/theme for Shaarli 45- [kalvn/shaarli-blocks](https://github.com/kalvn/shaarli-blocks) - A template/theme for Shaarli
44- [kalvn/Shaarli-Material](https://github.com/kalvn/Shaarli-Material) - A theme (template) based on Google's Material Design for Shaarli, the superfast delicious clone 46- [kalvn/Shaarli-Material](https://github.com/kalvn/Shaarli-Material) - A theme (template) based on Google's Material Design for Shaarli, the superfast delicious clone
45- [ManufacturaInd/shaarli-2004licious-theme](https://github.com/ManufacturaInd/shaarli-2004licious-theme) - A template/theme as a humble homage to the early looks of the del.icio.us site 47- [ManufacturaInd/shaarli-2004licious-theme](https://github.com/ManufacturaInd/shaarli-2004licious-theme) - A template/theme as a humble homage to the early looks of the del.icio.us site
48- [xfnw/shaarli-default-dark](https://github.com/xfnw/shaarli-default-dark) - The default theme but nice and dark for your eyeballs
46 49
47### Shaarli forks 50### Shaarli forks
48 51
diff --git a/doc/md/Translations.md b/doc/md/dev/Translations.md
index 58b92da3..8f3b8f10 100644
--- a/doc/md/Translations.md
+++ b/doc/md/dev/Translations.md
@@ -7,87 +7,80 @@ Note that only the `default` theme supports translations.
7 7
8### Contributing 8### Contributing
9 9
10We encourage the community to contribute to Shaarli's translation either by improving existing 10We encourage the community to contribute to Shaarli translations, either by improving existing translations or submitting a new language.
11translations or submitting a new language.
12 11
13Contributing to the translation does not require development skill. 12Contributing to the translation does not require software development knowledge.
14 13
15Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`) 14Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`) is not stored on the repository, and is generated during the release process.
16is not stored on the repository, and is generated during the release process.
17 15
18### How to
19
20First, install [Poedit](https://poedit.net/) tool.
21 16
22Poedit will extract strings to translate from the PHP source code. 17### How to
23
24**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract
25every translatable string.
26 18
27You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended) 19Install [Poedit](https://poedit.net/) (used to extract strings to translate from the PHP source code, and generate `.po` files).
28or visit every template page in your browser to generate cache files, while logged in.
29 20
30Here is a list : 21Due to the usage of a template engine, it's important to generate PHP cache files to extract every translatable string. You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended) or visit every template page in your browser to generate cache files, while logged in. Here is a list :
31 22
32``` 23```
33http://<replace_domain>/ 24http://<replace_domain>/
34http://<replace_domain>/?nonope
35http://<replace_domain>/?do=addlink
36http://<replace_domain>/?do=changepasswd
37http://<replace_domain>/?do=changetag
38http://<replace_domain>/?do=configure
39http://<replace_domain>/?do=tools
40http://<replace_domain>/?do=daily
41http://<replace_domain>/?post
42http://<replace_domain>/?do=export
43http://<replace_domain>/?do=import
44http://<replace_domain>/login 25http://<replace_domain>/login
45http://<replace_domain>/?do=picwall 26http://<replace_domain>/daily
46http://<replace_domain>/?do=pluginadmin 27http://<replace_domain>/tags/cloud
47http://<replace_domain>/?do=tagcloud 28http://<replace_domain>/tags/list
48http://<replace_domain>/?do=taglist 29http://<replace_domain>/picture-wall
30http://<replace_domain>/?nonope
31http://<replace_domain>/admin/add-shaare
32http://<replace_domain>/admin/password
33http://<replace_domain>/admin/tags
34http://<replace_domain>/admin/configure
35http://<replace_domain>/admin/tools
36http://<replace_domain>/admin/shaare
37http://<replace_domain>/admin/export
38http://<replace_domain>/admin/import
39http://<replace_domain>/admin/plugins
49``` 40```
50 41
51#### Improve existing translation
52 42
53In Poedit, click on "Edit a Translation", and from Shaarli's directory open 43#### Improve existing translations
54`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
55 44
56The existing list of translatable strings should have been loaded, then click on the "Update" button. 45- In Poedit, click on "Edit a Translation
57 46- Open `inc/languages/<lang>/LC_MESSAGES/shaarli.po` under Shaarli's directory
58You can start editing the translation. 47- The existing list of translatable strings should load
48- Click on the "Update" button.
49- Start editing translations.
59 50
60![poedit-screenshot](images/poedit-1.jpg) 51![poedit-screenshot](images/poedit-1.jpg)
61 52
62Save when you're done, then you can submit a pull request containing the updated `shaarli.po`. 53Save when you're done, then you can submit a pull request containing the updated `shaarli.po`.
63 54
64#### Add a new language
65
66Open Poedit and select "Create New Translation", then from Shaarli's directory open
67`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
68
69Then select the language you want to create.
70 55
71Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`. 56#### Add a new language
72`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2)
73format in lowercase (e.g. `de` for German).
74 57
75Then click on the "Update" button, and you can start to translate every available string. 58- In Poedit select "Create New Translation"
59- Open `inc/languages/<lang>/LC_MESSAGES/shaarli.po` under Shaarli's directory
60- Select the language you want to create.
61- Click on `File > Save as...`, save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po` (`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2) format in lowercase - e.g. `de` for German)
62- Click on the "Update" button
63- Start editing translations.
76 64
77Save when you're done, then you can submit a pull request containing the new `shaarli.po`. 65Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
78 66
67
79### Theme translations 68### Theme translations
80 69
81Theme translation extensions are loaded automatically if they're present. 70[Theme](Theming) translation extensions are loaded automatically if they're present.
82 71
83As a theme developer, all you have to do is to add the `.po` and `.mo` compiled file like this: 72As a theme developer, all you have to do is to add the `.po` and `.mo` compiled file like this:
84 73
85 tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.po 74```
86 tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.mo 75tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.po
76tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.mo
77```
87 78
88Where `<lang>` is the ISO 3166-1 alpha-2 language code. 79Where `<lang>` is the ISO 3166-1 alpha-2 language code.
80
89Read the following section "Extend Shaarli's translation" to learn how to generate those files. 81Read the following section "Extend Shaarli's translation" to learn how to generate those files.
90 82
83
91### Extend Shaarli's translation 84### Extend Shaarli's translation
92 85
93If you're writing a custom theme, or a non official plugin, you might want to use the translation system, 86If you're writing a custom theme, or a non official plugin, you might want to use the translation system,
diff --git a/doc/md/dev/Unit-tests.md b/doc/md/dev/Unit-tests.md
new file mode 100644
index 00000000..fd286bf0
--- /dev/null
+++ b/doc/md/dev/Unit-tests.md
@@ -0,0 +1,133 @@
1# Unit tests
2
3Shaarli uses the [PHPUnit](https://phpunit.de/) test framework; it can be installed with [Composer](https://getcomposer.org/), which is a dependency management tool.
4
5## Install composer
6
7You can either use:
8
9- a system-wide version, e.g. installed through your distro's package manager
10- a local version, downloadable [here](https://getcomposer.org/download/).
11
12```bash
13# for Debian-based distros
14sudo apt install composer
15```
16
17
18## Install Shaarli dev dependencies
19
20```bash
21$ cd /path/to/shaarli
22$ make composer_dependencies_dev
23```
24
25## Install and enable Xdebug to generate PHPUnit coverage reports
26
27
28[Xdebug](http://xdebug.org/docs/install) is a PHP extension which provides debugging and profiling capabilities. Install Xdebug:
29
30```bash
31# for Debian-based distros:
32sudo apt install php-xdebug
33
34# for ArchLinux:
35pacman -S xdebug
36
37# then add the following line to /etc/php/php.ini
38zend_extension=xdebug.so
39```
40
41## Run unit tests
42
43Ensure tests pass successuflly:
44
45```bash
46make test
47# ...
48# OK (36 tests, 65 assertions)
49```
50
51In case of failure the test suite will point you to actual errors and output a summary:
52
53```bash
54make test
55# ...
56# FAILURES!
57# Tests: 36, Assertions: 63, Errors: 1, Failures: 2.
58```
59
60By default, PHPUnit will run all suitable tests found under the `tests` directory. Each test has 3 possible outcomes:
61
62- `.` - success
63- `F` - failure: the test was run but its results are invalid
64 - the code does not behave as expected
65 - dependencies to external elements: globals, session, cache...
66- `E` - error: something went wrong and the tested code has crashed
67 - typos in the code, or in the test code
68 - dependencies to missing external elements
69
70If Xdebug has been installed and activated, two coverage reports will be generated:
71
72- a summary in the console
73- a detailed HTML report with metrics for tested code
74 - to open it in a web browser: `firefox coverage/index.html &`
75
76
77### Executing specific tests
78
79Add a [`@group`](https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.group) annotation in a test class or method comment:
80
81```php
82/**
83 * Netscape bookmark import
84 * @group WIP
85 */
86class BookmarkImportTest extends PHPUnit_Framework_TestCase
87{
88 [...]
89}
90```
91
92To run all tests annotated with `@group WIP`:
93```bash
94$ vendor/bin/phpunit --group WIP tests/
95```
96
97## Running tests inside Docker containers
98
99Unit tests can be run inside [Docker](../Docker.md) containers.
100
101Test Dockerfiles are located under `tests/docker/<distribution>/Dockerfile`, and can be used to build Docker images to run Shaarli test suites under commonLinux environments. Dockerfiles are provided for the following environments:
102
103- [`alpine36`](https://github.com/shaarli/Shaarli/blob/master/tests/docker/alpine36/Dockerfile) - [Alpine Linux 3.6](https://www.alpinelinux.org/downloads/)
104- [`debian8`](https://github.com/shaarli/Shaarli/blob/master/tests/docker/debian8/Dockerfile) - [Debian 8 Jessie](https://www.debian.org/DebianJessie) (oldoldstable)
105- [`debian9`](https://github.com/shaarli/Shaarli/blob/master/tests/docker/debian9/Dockerfile) - [Debian 9 Stretch](https://wiki.debian.org/DebianStretch) (oldstable)
106- [`ubuntu16`](https://github.com/shaarli/Shaarli/blob/master/tests/docker/ubuntu16/Dockerfile) - [Ubuntu 16.04 Xenial Xerus](http://releases.ubuntu.com/16.04/) (old LTS)
107
108Each image provides:
109- a base Linux OS
110- Shaarli PHP dependencies (OS packages)
111- test PHP dependencies (OS packages)
112- Composer
113- Tests that run inside the conatiner using a standard Linux user account (running tests as `root` would bypass permission checks and may hide issues)
114
115Build a test image:
116
117```bash
118# build the Debian 9 Docker image
119cd /path/to/shaarli/tests/docker/debian9
120docker build -t shaarli-test:debian9 .
121```
122
123Run unit tests in a container:
124
125```bash
126cd /path/to/shaarli
127# install/update 3rd-party test dependencies
128composer install --prefer-dist
129# run tests using the freshly built image
130docker run -v $PWD:/shaarli shaarli-test:debian9 docker_test
131# run the full test campaign
132docker run -v $PWD:/shaarli shaarli-test:debian9 docker_all_tests
133```
diff --git a/doc/md/Versioning-and-Branches.md b/doc/md/dev/Versioning.md
index 7097ca0a..32c80a5c 100644
--- a/doc/md/Versioning-and-Branches.md
+++ b/doc/md/dev/Versioning.md
@@ -1,6 +1,7 @@
1**WORK IN PROGRESS** 1# Versioning
2
3If you're maintaining a 3rd party tool for Shaarli (theme, plugin, etc.), It's important to understand how Shaarli branches work ensure your tool stays compatible.
2 4
3It's important to understand how Shaarli branches work, especially if you're maintaining a 3rd party tools for Shaarli (theme, plugin, etc.), to be sure stay compatible.
4 5
5## `master` branch 6## `master` branch
6 7
@@ -11,39 +12,26 @@ Remarks:
11- This branch shouldn't be used for production as it isn't necessary stable. 12- This branch shouldn't be used for production as it isn't necessary stable.
12- 3rd party aren't required to be compatible with the latest changes. 13- 3rd party aren't required to be compatible with the latest changes.
13- Official plugins, themes and libraries (contained within Shaarli organization repos) must be compatible with the master branch. 14- Official plugins, themes and libraries (contained within Shaarli organization repos) must be compatible with the master branch.
14- The version in this branch is always `dev`.
15 15
16## `v0.x` branch
17 16
18This `v0.x` branch, points to the latest `v0.x.y` release. 17## `v0.x` branch
19 18
20Explanation: 19The `v0.x` branch points to the latest `v0.x.y` release.
21 20
22When a new version is released, it might contains a major bug which isn't detected right away. For example, a new PHP version is released, containing backward compatibility issue which doesn't work with Shaarli. 21If a major bug affects the original `v0.x.0` release, we may [backport](https://en.wikipedia.org/wiki/Backporting) a fix for this bug from master, to the `v0.x` branch, and create a new bugfix release (eg. `v0.x.1`) from this branch.
23 22
24In this case, the issue is fixed in the `master` branch, and the fix is backported the to the `v0.x` branch. Then a new release is made from the `v0.x` branch. 23This allows users of the original release to upgrade to the fixed version, without having to upgrade to a completely new minor/major release.
25 24
26This workflow allow us to fix any major bug detected, without having to release bleeding edge feature too soon.
27 25
28## `latest` branch 26## `latest` branch
29 27
30This branch point the latest release. It recommended to use it to get the latest tested changes. 28This branch point the latest release. It recommended to use it to get the latest tested changes.
31 29
32## `stable` branch
33
34The `stable` branch doesn't contain any major bug, and is one major digit version behind the latest release.
35
36For example, the current latest release is `v0.8.3`, the stable branch is an alias to the latest `v0.7.x` release. When the `v0.9.0` version will be released, the stable will move to the latest `v0.8.x` release.
37
38Remarks:
39
40- Shaarli release pace isn't fast, and the stable branch might be a few months behind the latest release.
41 30
42## Releases 31## Releases
43 32
44Releases are always made from the latest `v0.x` branch. 33For every release, we manually generate a .zip file which contains all Shaarli dependencies, making Shaarli's installation only one step.
45 34
46Note that for every release, we manually generate a tarball which contains all Shaarli dependencies, making Shaarli's installation only one step.
47 35
48## Advices on 3rd party git repos workflow 36## Advices on 3rd party git repos workflow
49 37
diff --git a/doc/md/images/poedit-1.jpg b/doc/md/dev/images/poedit-1.jpg
index 673ae6d6..673ae6d6 100644
--- a/doc/md/images/poedit-1.jpg
+++ b/doc/md/dev/images/poedit-1.jpg
Binary files differ
diff --git a/doc/md/docker/docker-101.md b/doc/md/docker/docker-101.md
deleted file mode 100644
index a9c00b85..00000000
--- a/doc/md/docker/docker-101.md
+++ /dev/null
@@ -1,140 +0,0 @@
1## Basics
2Install [Docker](https://www.docker.com/), by following the instructions relevant
3to your OS / distribution, and start the service.
4
5### Search an image on [DockerHub](https://hub.docker.com/)
6
7```bash
8$ docker search debian
9
10NAME DESCRIPTION STARS OFFICIAL AUTOMATED
11ubuntu Ubuntu is a Debian-based Linux operating s... 2065 [OK]
12debian Debian is a Linux distribution that's comp... 603 [OK]
13google/debian 47 [OK]
14```
15
16### Show available tags for a repository
17```bash
18$ curl https://index.docker.io/v1/repositories/debian/tags | python -m json.tool
19
20% Total % Received % Xferd Average Speed Time Time Time Current
21Dload Upload Total Spent Left Speed
22100 1283 0 1283 0 0 433 0 --:--:-- 0:00:02 --:--:-- 433
23```
24
25Sample output:
26```json
27[
28 {
29 "layer": "85a02782",
30 "name": "stretch"
31 },
32 {
33 "layer": "59abecbc",
34 "name": "testing"
35 },
36 {
37 "layer": "bf0fd686",
38 "name": "unstable"
39 },
40 {
41 "layer": "60c52dbe",
42 "name": "wheezy"
43 },
44 {
45 "layer": "c5b806fe",
46 "name": "wheezy-backports"
47 }
48]
49
50```
51
52### Pull an image from DockerHub
53```bash
54$ docker pull repository[:tag]
55
56$ docker pull debian:wheezy
57wheezy: Pulling from debian
584c8cbfd2973e: Pull complete
5960c52dbe9d91: Pull complete
60Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe
61Status: Downloaded newer image for debian:wheezy
62```
63
64Docker re-uses layers already downloaded. In other words if you have images based on Alpine or some Ubuntu version for example, those can share disk space.
65
66### Start a container
67A container is an instance created from an image, that can be run and that keeps running until its main process exits. Or until the user stops the container.
68
69The simplest way to start a container from image is ``docker run``. It also pulls the image for you if it is not locally available. For more advanced use, refer to ``docker create``.
70
71Stopped containers are not destroyed, unless you specify ``--rm``. To view all created, running and stopped containers, enter:
72```bash
73$ docker ps -a
74```
75
76Some containers may be designed or configured to be restarted, others are not. Also remember both network ports and volumes of a container are created on start, and not editable later.
77
78### Access a running container
79A running container is accessible using ``docker exec``, or ``docker copy``. You can use ``exec`` to start a root shell in the Shaarli container:
80```bash
81$ docker exec -ti <container-name-or-id> bash
82```
83Note the names and ID's of containers are listed in ``docker ps``. You can even type only one or two letters of the ID, given they are unique.
84
85Access can also be through one or more network ports, or disk volumes. Both are specified on and fixed on ``docker create`` or ``run``.
86
87You can view the console output of the main container process too:
88```bash
89$ docker logs -f <container-name-or-id>
90```
91
92### Docker disk use
93Trying out different images can fill some gigabytes of disk quickly. Besides images, the docker volumes usually take up most disk space.
94
95If you care only about trying out docker and not about what is running or saved, the following commands should help you out quickly if you run low on disk space:
96
97```bash
98$ docker rmi -f $(docker images -aq) # remove or mark all images for disposal
99$ docker volume rm $(docker volume ls -q) # remove all volumes
100```
101
102### Systemd config
103Systemd is the process manager of choice on Debian-based distributions. Once you have a ``docker`` service installed, you can use the following steps to set up Shaarli to run on system start.
104
105```bash
106systemctl enable /etc/systemd/system/docker.shaarli.service
107systemctl start docker.shaarli
108systemctl status docker.*
109journalctl -f # inspect system log if needed
110```
111
112You will need sudo or a root terminal to perform some or all of the steps above. Here are the contents for the service file:
113```
114[Unit]
115Description=Shaarli Bookmark Manager Container
116After=docker.service
117Requires=docker.service
118
119
120[Service]
121Restart=always
122
123# Put any environment you want in an included file, like $host- or $domainname in this example
124EnvironmentFile=/etc/sysconfig/box-environment
125
126# It's just an example..
127ExecStart=/usr/bin/docker run \
128 -p 28010:80 \
129 --name ${hostname}-shaarli \
130 --hostname shaarli.${domainname} \
131 -v /srv/docker-volumes-local/shaarli-data:/var/www/shaarli/data:rw \
132 -v /etc/localtime:/etc/localtime:ro \
133 shaarli/shaarli:latest
134
135ExecStop=/usr/bin/docker rm -f ${hostname}-shaarli
136
137
138[Install]
139WantedBy=multi-user.target
140```
diff --git a/doc/md/docker/resources.md b/doc/md/docker/resources.md
deleted file mode 100644
index 082d4a46..00000000
--- a/doc/md/docker/resources.md
+++ /dev/null
@@ -1,19 +0,0 @@
1### Docker
2
3- [Interactive Docker training portal](https://www.katacoda.com/courses/docker/) on [Katakoda](https://www.katacoda.com/)
4- [Where are Docker images stored?](http://blog.thoward37.me/articles/where-are-docker-images-stored/)
5- [Dockerfile reference](https://docs.docker.com/reference/builder/)
6- [Dockerfile best practices](https://docs.docker.com/articles/dockerfile_best-practices/)
7- [Volumes](https://docs.docker.com/userguide/dockervolumes/)
8
9### DockerHub
10
11- [Repositories](https://docs.docker.com/userguide/dockerrepos/)
12- [Teams and organizations](https://docs.docker.com/docker-hub/orgs/)
13- [GitHub automated build](https://docs.docker.com/docker-hub/github/)
14
15### Service management
16
17- [Using supervisord](https://docs.docker.com/articles/using_supervisord/)
18- [Nginx in the foreground](http://nginx.org/en/docs/ngx_core_module.html#daemon)
19- [supervisord](http://supervisord.org/)
diff --git a/doc/md/docker/reverse-proxy-configuration.md b/doc/md/docker/reverse-proxy-configuration.md
deleted file mode 100644
index e53c9422..00000000
--- a/doc/md/docker/reverse-proxy-configuration.md
+++ /dev/null
@@ -1,123 +0,0 @@
1## Foreword
2
3This guide assumes that:
4
5- Shaarli runs in a Docker container
6- The host's `10080` port is mapped to the container's `80` port
7- Shaarli's Fully Qualified Domain Name (FQDN) is `shaarli.domain.tld`
8- HTTP traffic is redirected to HTTPS
9
10## Apache
11
12- [Apache 2.4 documentation](https://httpd.apache.org/docs/2.4/)
13 - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
14 - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)
15
16The following HTTP headers are set when the `ProxyPass` directive is set:
17
18- `X-Forwarded-For`
19- `X-Forwarded-Host`
20- `X-Forwarded-Server`
21
22The original `SERVER_NAME` can be sent to the proxied host by setting the [`ProxyPreserveHost`](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#ProxyPreserveHost) directive to `On`.
23
24```apache
25<VirtualHost *:80>
26 ServerName shaarli.domain.tld
27 Redirect permanent / https://shaarli.domain.tld
28</VirtualHost>
29
30<VirtualHost *:443>
31 ServerName shaarli.domain.tld
32
33 SSLEngine on
34 SSLCertificateFile /path/to/cert
35 SSLCertificateKeyFile /path/to/certkey
36
37 LogLevel warn
38 ErrorLog /var/log/apache2/shaarli-error.log
39 CustomLog /var/log/apache2/shaarli-access.log combined
40
41 RequestHeader set X-Forwarded-Proto "https"
42 ProxyPreserveHost On
43
44 ProxyPass / http://127.0.0.1:10080/
45 ProxyPassReverse / http://127.0.0.1:10080/
46</VirtualHost>
47```
48
49
50## HAProxy
51
52- [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/)
53
54```conf
55global
56 [...]
57
58defaults
59 [...]
60
61frontend http-in
62 bind :80
63 redirect scheme https code 301 if !{ ssl_fc }
64
65 bind :443 ssl crt /path/to/cert.pem
66
67 default_backend shaarli
68
69
70backend shaarli
71 mode http
72 option http-server-close
73 option forwardfor
74 reqadd X-Forwarded-Proto: https
75
76 server shaarli1 127.0.0.1:10080
77```
78
79
80## Nginx
81
82- [Nginx documentation](https://nginx.org/en/docs/)
83
84```nginx
85http {
86 [...]
87
88 index index.html index.php;
89
90 root /home/john/web;
91 access_log /var/log/nginx/access.log;
92 error_log /var/log/nginx/error.log;
93
94 server {
95 listen 80;
96 server_name shaarli.domain.tld;
97 return 301 https://shaarli.domain.tld$request_uri;
98 }
99
100 server {
101 listen 443 ssl http2;
102 server_name shaarli.domain.tld;
103
104 ssl_certificate /path/to/cert
105 ssl_certificate_key /path/to/certkey
106
107 location / {
108 proxy_set_header X-Real-IP $remote_addr;
109 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
110 proxy_set_header X-Forwarded-Proto $scheme;
111 proxy_set_header X-Forwarded-Host $host;
112
113 proxy_pass http://localhost:10080/;
114 proxy_set_header Host $host;
115 proxy_connect_timeout 30s;
116 proxy_read_timeout 120s;
117
118 access_log /var/log/nginx/shaarli.access.log;
119 error_log /var/log/nginx/shaarli.error.log;
120 }
121 }
122}
123```
diff --git a/doc/md/docker/shaarli-images.md b/doc/md/docker/shaarli-images.md
deleted file mode 100644
index 14971d54..00000000
--- a/doc/md/docker/shaarli-images.md
+++ /dev/null
@@ -1,118 +0,0 @@
1A brief guide on getting starting using docker is given in [Docker 101](docker-101.md).
2To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](../Upgrade-and-migration.md).
3
4## Get and run a Shaarli image
5
6### DockerHub repository
7The images can be found in the [`shaarli/shaarli`](https://hub.docker.com/r/shaarli/shaarli/)
8repository.
9
10### Available image tags
11- `latest`: latest branch
12- `master`: master branch
13- `stable`: stable branch
14
15The `latest`, `master` and `stable` images rely on:
16
17- [Alpine Linux](https://www.alpinelinux.org/)
18- [PHP7-FPM](http://php-fpm.org/)
19- [Nginx](http://nginx.org/)
20
21Additional Dockerfiles are provided for the `arm32v7` platform, relying on
22[Linuxserver.io Alpine armhf
23images](https://hub.docker.com/r/lsiobase/alpine.armhf/). These images must be
24built using [`docker
25build`](https://docs.docker.com/engine/reference/commandline/build/) on an
26`arm32v7` machine or using an emulator such as
27[qemu](https://resin.io/blog/building-arm-containers-on-any-x86-machine-even-dockerhub/).
28
29### Download from Docker Hub
30```shell
31$ docker pull shaarli/shaarli
32
33latest: Pulling from shaarli/shaarli
3432716d9fcddb: Pull complete
3584899d045435: Pull complete
364b6ad7444763: Pull complete
37e0345ef7a3e0: Pull complete
385c1dd344094f: Pull complete
396422305a200b: Pull complete
407d63f861dbef: Pull complete
413eb97210645c: Pull complete
42869319d746ff: Already exists
43869319d746ff: Pulling fs layer
44902b87aaaec9: Already exists
45Digest: sha256:f836b4627b958b3f83f59c332f22f02fcd495ace3056f2be2c4912bd8704cc98
46Status: Downloaded newer image for shaarli/shaarli:latest
47```
48
49### Create and start a new container from the image
50```shell
51# map the host's :8000 port to the container's :80 port
52$ docker create -p 8000:80 shaarli/shaarli
53d40b7af693d678958adedfb88f87d6ea0237186c23de5c4102a55a8fcb499101
54
55# launch the container in the background
56$ docker start d40b7af693d678958adedfb88f87d6ea0237186c23de5c4102a55a8fcb499101
57d40b7af693d678958adedfb88f87d6ea0237186c23de5c4102a55a8fcb499101
58
59# list active containers
60$ docker ps
61CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
62d40b7af693d6 shaarli/shaarli /usr/bin/supervisor 15 seconds ago Up 4 seconds 0.0.0.0:8000->80/tcp backstabbing_galileo
63```
64
65### Stop and destroy a container
66```shell
67$ docker stop backstabbing_galileo # those docker guys are really rude to physicists!
68backstabbing_galileo
69
70# check the container is stopped
71$ docker ps
72CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
73
74# list ALL containers
75$ docker ps -a
76CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
77d40b7af693d6 shaarli/shaarli /usr/bin/supervisor 5 minutes ago Exited (0) 48 seconds ago backstabbing_galileo
78
79# destroy the container
80$ docker rm backstabbing_galileo # let's put an end to these barbarian practices
81backstabbing_galileo
82
83$ docker ps -a
84CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
85```
86
87### Automatic builds
88Docker users can start a personal instance from an
89[autobuild image](https://hub.docker.com/r/shaarli/shaarli/).
90For example to start a temporary Shaarli at ``localhost:8000``, and keep session
91data (config, storage):
92
93```shell
94MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
95docker run -ti --rm \
96 -p 8000:80 \
97 -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
98 shaarli/shaarli
99```
100
101### Volumes and data persistence
102Data can be persisted by [using volumes](https://docs.docker.com/storage/volumes/).
103Volumes allow to keep your data when renewing and/or updating container images:
104
105```shell
106# Create data volumes
107$ docker volume create shaarli-data
108$ docker volume create shaarli-cache
109
110# Create and start a Shaarli container using these volumes to persist data
111$ docker create \
112 --name shaarli \
113 -v shaarli-cache:/var/www/shaarli/cache \
114 -v shaarli-data:/var/www/shaarli/data \
115 -p 8000:80 \
116 shaarli/shaarli:master
117$ docker start shaarli
118```
diff --git a/doc/md/guides/backup-restore-import-export.md b/doc/md/guides/backup-restore-import-export.md
deleted file mode 100644
index bb790074..00000000
--- a/doc/md/guides/backup-restore-import-export.md
+++ /dev/null
@@ -1,64 +0,0 @@
1## Backup and restore the datastore file
2
3Backup the file `data/datastore.php` (by FTP or SSH). Restore by putting the file back in place.
4
5Example command:
6```bash
7rsync -avzP my.server.com:/var/www/shaarli/data/datastore.php datastore-$(date +%Y-%m-%d_%H%M).php
8```
9
10## Export links as...
11
12To export links as an HTML file, under _Tools > Export_, choose:
13
14- _Export all_ to export both public and private links
15- _Export public_ to export public links only
16- _Export private_ to export private links only
17
18Restore by using the `Import` feature.
19
20- This can be done using the [shaarchiver](https://github.com/nodiscc/shaarchiver) tool.
21
22Example command:
23```bash
24./export-bookmarks.py --url=https://my.server.com/shaarli --username=myusername --password=mysupersecretpassword --download-dir=./ --type=all
25```
26
27## Import links from...
28
29### Diigo
30
31If you export your bookmark from Diigo, make sure you use the Delicious export, not the Netscape export. (Their Netscape export is broken, and they don't seem to be interested in fixing it.)
32
33### Mister Wong
34
35See [this issue](https://github.com/sebsauvage/Shaarli/issues/146) for import tweaks.
36
37### SemanticScuttle
38
39To correctly import the tags from a [SemanticScuttle](http://semanticscuttle.sourceforge.net/) HTML export, edit the HTML file before importing and replace all occurences of `tags=` (lowercase) to `TAGS=` (uppercase).
40
41### Scuttle
42
43Shaarli cannot import data directly from [Scuttle](https://github.com/scronide/scuttle).
44
45However, you can use the third-party [scuttle-to-shaarli](https://github.com/q2apro/scuttle-to-shaarli)
46tool to export the Scuttle database to the Netscape HTML format compatible with the Shaarli importer.
47
48### Refind
49
50You can use the third-party tool [Derefind](https://github.com/ShawnPConroy/Derefind) to convert refind.com bookmark exports to a format that can be imported into Shaarli.
51
52## Import Shaarli links to Firefox
53
54- Export your Shaarli links as described above.
55 - For compatibility reasons, check `Prepend note permalinks with this Shaarli instance's URL (useful to import bookmarks in a web browser)`
56- In Firefox, open the bookmark manager (not the sidebar! `Bookmarks menu > Show all bookmarks` or `Ctrl+Shift+B`)
57- Select `Import and Backup > Import bookmarks in HTML format`
58
59Your bookmarks will be imported in Firefox, ready to use, with tags and descriptions retained. "Self" (notes) shaares will still point to the Shaarli instance you exported them from, but the note text can be viewed directly in the bookmark properties inside your browser. Depending on the number of bookmarks, the import can take some time.
60
61You may be interested in these Firefox addons to manage links imported from Shaarli
62
63- [Bookmark Deduplicator](https://addons.mozilla.org/en-US/firefox/addon/bookmark-deduplicator/) - provides an easy way to deduplicate your bookmarks
64- [TagSieve](https://addons.mozilla.org/en-US/firefox/addon/tagsieve/) - browse your bookmarks by their tags
diff --git a/doc/md/guides/images/01-create-droplet-distro.jpg b/doc/md/guides/images/01-create-droplet-distro.jpg
deleted file mode 100644
index 63682ba8..00000000
--- a/doc/md/guides/images/01-create-droplet-distro.jpg
+++ /dev/null
Binary files differ
diff --git a/doc/md/guides/images/02-create-droplet-region.jpg b/doc/md/guides/images/02-create-droplet-region.jpg
deleted file mode 100644
index 135a78be..00000000
--- a/doc/md/guides/images/02-create-droplet-region.jpg
+++ /dev/null
Binary files differ
diff --git a/doc/md/guides/images/03-create-droplet-size.jpg b/doc/md/guides/images/03-create-droplet-size.jpg
deleted file mode 100644
index aa5b2fd2..00000000
--- a/doc/md/guides/images/03-create-droplet-size.jpg
+++ /dev/null
Binary files differ
diff --git a/doc/md/guides/images/04-finalize.jpg b/doc/md/guides/images/04-finalize.jpg
deleted file mode 100644
index 68ec0dc5..00000000
--- a/doc/md/guides/images/04-finalize.jpg
+++ /dev/null
Binary files differ
diff --git a/doc/md/guides/images/05-droplet.jpg b/doc/md/guides/images/05-droplet.jpg
deleted file mode 100644
index 44e93a1e..00000000
--- a/doc/md/guides/images/05-droplet.jpg
+++ /dev/null
Binary files differ
diff --git a/doc/md/guides/images/06-domain.jpg b/doc/md/guides/images/06-domain.jpg
deleted file mode 100644
index 5827dd93..00000000
--- a/doc/md/guides/images/06-domain.jpg
+++ /dev/null
Binary files differ
diff --git a/doc/md/guides/install-shaarli-with-debian9-and-docker.md b/doc/md/guides/install-shaarli-with-debian9-and-docker.md
deleted file mode 100644
index f1b26d47..00000000
--- a/doc/md/guides/install-shaarli-with-debian9-and-docker.md
+++ /dev/null
@@ -1,257 +0,0 @@
1_Last updated on 2018-07-01._
2
3## Goals
4- Getting a Virtual Private Server (VPS)
5- Running Shaarli:
6 - as a Docker container,
7 - using the Træfik reverse proxy,
8 - securized with TLS certificates from Let's Encrypt.
9
10
11The following components and tools will be used:
12
13- [Debian](https://www.debian.org/), a GNU/Linux distribution widely used in
14 server environments;
15- [Docker](https://docs.docker.com/engine/docker-overview/), an open platform
16 for developing, shipping, and running applications;
17- [Docker Compose](https://docs.docker.com/compose/), a tool for defining and
18 running multi-container Docker applications.
19
20
21More information can be found in the [Resources](#resources) section at the
22bottom of the guide.
23
24## Getting a Virtual Private Server
25For this guide, I went for the smallest VPS available from DigitalOcean,
26a Droplet with 1 CPU, 1 GiB RAM and 25 GiB SSD storage, which costs
27$5/month ($0.007/hour):
28
29- [Droplets Overview](https://www.digitalocean.com/docs/droplets/overview/)
30- [Pricing](https://www.digitalocean.com/pricing/)
31- [How to Create a Droplet from the DigitalOcean Control Panel](https://www.digitalocean.com/docs/droplets/how-to/create/)
32- [How to Add SSH Keys to Droplets](https://www.digitalocean.com/docs/droplets/how-to/add-ssh-keys/)
33- [Initial Server Setup with Debian 8](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-debian-8) (also applies to Debian 9)
34- [An Introduction to Securing your Linux VPS](https://www.digitalocean.com/community/tutorials/an-introduction-to-securing-your-linux-vps)
35
36### Creating a Droplet
37Select `Debian 9` as the Droplet distribution:
38
39<img src="../images/01-create-droplet-distro.jpg"
40 width="500px"
41 alt="Droplet distribution" />
42
43Choose a region that is geographically close to you:
44
45<img src="../images/02-create-droplet-region.jpg"
46 width="500px"
47 alt="Droplet region" />
48
49Choose a Droplet size that corresponds to your usage and budget:
50
51<img src="../images/03-create-droplet-size.jpg"
52 width="500px"
53 alt="Droplet size" />
54
55Finalize the Droplet creation:
56
57<img src="../images/04-finalize.jpg"
58 width="500px"
59 alt="Droplet finalization" />
60
61Droplet information is displayed on the Control Panel:
62
63<img src="../images/05-droplet.jpg"
64 width="500px"
65 alt="Droplet summary" />
66
67Once your VPS has been created, you will receive an e-mail with connection
68instructions.
69
70## Obtaining a domain name
71After creating your VPS, it will be reachable using its IP address; some hosting
72providers also create a DNS record, e.g. `ns4853142.ip-01-47-127.eu`.
73
74A domain name (DNS record) is required to obtain a certificate and setup HTTPS
75(HTTP with TLS encryption).
76
77Domain names can be obtained from registrars through hosting providers such as
78[Gandi](https://www.gandi.net/en/domain).
79
80Once you have your own domain, you need to create a new DNS record that points
81to your VPS' IP address:
82
83<img src="../images/06-domain.jpg"
84 width="650px"
85 alt="Domain configuration" />
86
87## Host setup
88Now's the time to connect to your freshly created VPS!
89
90```shell
91$ ssh root@188.166.85.8
92
93Linux stretch-shaarli-02 4.9.0-6-amd64 #1 SMP Debian 4.9.88-1+deb9u1 (2018-05-07) x86_64
94
95The programs included with the Debian GNU/Linux system are free software;
96the exact distribution terms for each program are described in the
97individual files in /usr/share/doc/*/copyright.
98
99Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
100permitted by applicable law.
101Last login: Sun Jul 1 11:20:18 2018 from <REDACTED>
102
103root@stretch-shaarli-02:~$
104```
105
106### Updating the system
107```shell
108root@stretch-shaarli-02:~$ apt update && apt upgrade -y
109```
110
111### Setting up Docker
112_The following instructions are from the
113[Get Docker CE for Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
114guide._
115
116Install package dependencies:
117
118```shell
119root@stretch-shaarli-02:~$ apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common
120```
121
122Add Docker's package repository GPG key:
123
124```shell
125root@stretch-shaarli-02:~$ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
126```
127
128Add Docker's package repository:
129
130```shell
131root@stretch-shaarli-02:~$ add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian stretch stable"
132```
133
134Update package lists and install Docker:
135
136```shell
137root@stretch-shaarli-02:~$ apt update && apt install -y docker-ce
138```
139
140Verify Docker is properly configured by running the `hello-world` image:
141
142```shell
143root@stretch-shaarli-02:~$ docker run hello-world
144```
145
146### Setting up Docker Compose
147_The following instructions are from the
148[Install Docker Compose](https://docs.docker.com/compose/install/)
149guide._
150
151Download the current version from the release page:
152
153```shell
154root@stretch-shaarli-02:~$ curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
155root@stretch-shaarli-02:~$ chmod +x /usr/local/bin/docker-compose
156```
157
158## Running Shaarli
159Shaarli comes with a configuration file for Docker Compose, that will setup:
160
161- a local Docker network
162- a Docker [volume](https://docs.docker.com/storage/volumes/) to store Shaarli data
163- a Docker [volume](https://docs.docker.com/storage/volumes/) to store Træfik TLS configuration and certificates
164- a [Shaarli](https://hub.docker.com/r/shaarli/shaarli/) instance
165- a [Træfik](https://hub.docker.com/_/traefik/) instance
166
167[Træfik](https://docs.traefik.io/) is a modern HTTP reverse proxy, with native
168support for Docker and [Let's Encrypt](https://letsencrypt.org/).
169
170### Compose configuration
171Create a new directory to store the configuration:
172
173```shell
174root@stretch-shaarli-02:~$ mkdir shaarli && cd shaarli
175root@stretch-shaarli-02:~/shaarli$
176```
177
178Download the current version of Shaarli's `docker-compose.yml`:
179
180```shell
181root@stretch-shaarli-02:~/shaarli$ curl -L https://raw.githubusercontent.com/shaarli/Shaarli/master/docker-compose.yml -o docker-compose.yml
182```
183
184Create the `.env` file and fill in your VPS and domain information (replace
185`<MY_SHAARLI_DOMAIN>` and `<MY_CONTACT_EMAIL>` with your actual information):
186
187```shell
188root@stretch-shaarli-02:~/shaarli$ vim .env
189```
190
191```shell
192SHAARLI_VIRTUAL_HOST=<MY_SHAARLI_DOMAIN>
193SHAARLI_LETSENCRYPT_EMAIL=<MY_CONTACT_EMAIL>
194```
195
196### Pull the Docker images
197```shell
198root@stretch-shaarli-02:~/shaarli$ docker-compose pull
199Pulling shaarli ... done
200Pulling traefik ... done
201```
202
203### Run!
204```shell
205root@stretch-shaarli-02:~/shaarli$ docker-compose up -d
206Creating network "shaarli_http-proxy" with the default driver
207Creating volume "shaarli_traefik-acme" with default driver
208Creating volume "shaarli_shaarli-data" with default driver
209Creating shaarli_shaarli_1 ... done
210Creating shaarli_traefik_1 ... done
211```
212
213## Conclusion
214Congratulations! Your Shaarli instance should be up and running, and available
215at `https://<MY_SHAARLI_DOMAIN>`.
216
217<img src="../images/07-installation.jpg"
218 width="500px"
219 alt="Shaarli installation page" />
220
221## Resources
222### Related Shaarli documentation
223- [Docker 101](../docker/docker-101.md)
224- [Shaarli images](../docker/shaarli-images.md)
225
226### Hosting providers
227- [DigitalOcean](https://www.digitalocean.com/)
228- [Gandi](https://www.gandi.net/en)
229- [OVH](https://www.ovh.co.uk/)
230- [RackSpace](https://www.rackspace.com/)
231- etc.
232
233### Domain Names and Registrars
234- [Introduction to the Domain Name System (DNS)](https://opensource.com/article/17/4/introduction-domain-name-system-dns)
235- [ICANN](https://www.icann.org/)
236- [Domain name registrar](https://en.wikipedia.org/wiki/Domain_name_registrar)
237- [OVH Domain Registration](https://www.ovh.co.uk/domains/)
238- [Gandi Domain Registration](https://www.gandi.net/en/domain)
239
240### HTTPS and Security
241- [Transport Layer Security](https://en.wikipedia.org/wiki/Transport_Layer_Security)
242- [Let's Encrypt](https://letsencrypt.org/)
243
244### Docker
245- [Docker Overview](https://docs.docker.com/engine/docker-overview/)
246- [Docker Documentation](https://docs.docker.com/)
247- [Get Docker CE for Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
248- [docker logs](https://docs.docker.com/engine/reference/commandline/logs/)
249- [Volumes](https://docs.docker.com/storage/volumes/)
250- [Install Docker Compose](https://docs.docker.com/compose/install/)
251- [docker-compose logs](https://docs.docker.com/compose/reference/logs/)
252
253### Træfik
254- [Getting Started](https://docs.traefik.io/)
255- [Docker backend](https://docs.traefik.io/configuration/backends/docker/)
256- [Let's Encrypt and Docker](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/)
257- [traefik](https://hub.docker.com/_/traefik/) Docker image
diff --git a/doc/md/guides/various-hacks.md b/doc/md/guides/various-hacks.md
deleted file mode 100644
index 0cef99df..00000000
--- a/doc/md/guides/various-hacks.md
+++ /dev/null
@@ -1,24 +0,0 @@
1### Decode datastore content
2
3To display the array representing the data saved in `data/datastore.php`, use the following snippet:
4
5```php
6$data = "tZNdb9MwFIb... <Commented content inside datastore.php>";
7$out = unserialize(gzinflate(base64_decode($data)));
8echo "<pre>"; // Pretty printing is love, pretty printing is life
9print_r($out);
10echo "</pre>";
11exit;
12```
13This will output the internal representation of the datastore, "unobfuscated" (if this can really be considered obfuscation).
14
15Alternatively, you can transform to JSON format (and pretty-print if you have `jq` installed):
16```
17php -r 'print(json_encode(unserialize(gzinflate(base64_decode(preg_replace("!.*/\* (.+) \*/.*!", "$1", file_get_contents("data/datastore.php")))))));' | jq .
18```
19
20### See also
21
22- [Add a new custom field to shaares (example patch)](https://gist.github.com/nodiscc/8b0194921f059d7b9ad89a581ecd482c)
23- [Copy an existing Shaarli installation over SSH, and serve it locally](https://gist.github.com/nodiscc/ed161c66e5b028b5299b0a3733d01c77)
24- [Create multiple Shaarli instances, generate an HTML index of them](https://gist.github.com/nodiscc/52e711cda3bc47717c16065231cf6b20)
diff --git a/doc/md/guides/images/07-installation.jpg b/doc/md/images/07-installation.jpg
index 42cc9f10..42cc9f10 100644
--- a/doc/md/guides/images/07-installation.jpg
+++ b/doc/md/images/07-installation.jpg
Binary files differ
diff --git a/doc/md/images/bookmarklet.png b/doc/md/images/bookmarklet.png
deleted file mode 100644
index 0262578e..00000000
--- a/doc/md/images/bookmarklet.png
+++ /dev/null
Binary files differ
diff --git a/doc/md/images/firefoxshare.png b/doc/md/images/firefoxshare.png
deleted file mode 100644
index 8f8fdba4..00000000
--- a/doc/md/images/firefoxshare.png
+++ /dev/null
Binary files differ
diff --git a/doc/md/images/install-shaarli.png b/doc/md/images/install-shaarli.png
deleted file mode 100644
index d5d5baa7..00000000
--- a/doc/md/images/install-shaarli.png
+++ /dev/null
Binary files differ
diff --git a/doc/md/index.md b/doc/md/index.md
index 1431f9e1..2c4995f8 100644
--- a/doc/md/index.md
+++ b/doc/md/index.md
@@ -2,21 +2,19 @@
2 2
3The personal, minimalist, super-fast, database free, bookmarking service. 3The personal, minimalist, super-fast, database free, bookmarking service.
4 4
5Do you want to share the links you discover? 5Do you want to share the links you discover? Shaarli is a minimalist bookmark manager and link sharing service that you can install on your own server. It is designed to be personal (single-user), fast and handy.
6Shaarli is a minimalist bookmark manager and link sharing service that you can install on your own server.
7It is designed to be personal (single-user), fast and handy.
8
9<!-- TODO screenshots -->
10 6
11Visit the pages in the sidebar to find information on how to setup, use, configure, tweak and troubleshoot Shaarli. 7Visit the pages in the sidebar to find information on how to setup, use, configure, tweak and troubleshoot Shaarli.
12 8
13
14* [GitHub project page](https://github.com/shaarli/Shaarli) 9* [GitHub project page](https://github.com/shaarli/Shaarli)
15* [Online documentation](https://shaarli.readthedocs.io/) 10* [Documentation](https://shaarli.readthedocs.io/)
16* [Latest releases](https://github.com/shaarli/Shaarli/releases)
17* [Changelog](https://github.com/shaarli/Shaarli/blob/master/CHANGELOG.md) 11* [Changelog](https://github.com/shaarli/Shaarli/blob/master/CHANGELOG.md)
18 12
19 13
14[![](https://i.imgur.com/8wEBRSG.png)](https://i.imgur.com/WWPfSj0.png) [![](https://i.imgur.com/93PpLLs.png)](https://i.imgur.com/V09kAQt.png) [![](https://i.imgur.com/rrsjWYy.png)](https://i.imgur.com/TZzGHMs.png) [![](https://i.imgur.com/8iRzHfe.png)](https://i.imgur.com/sfJJ6NT.png) [![](https://i.imgur.com/GjZGvIh.png)](https://i.imgur.com/QsedIuJ.png) [![](https://i.imgur.com/TFZ9PEq.png)](https://i.imgur.com/KdtF8Ll.png) [![](https://i.imgur.com/uICDOle.png)](https://i.imgur.com/27wYsbC.png) [![](https://i.imgur.com/tVvD3gH.png)](https://i.imgur.com/zGF4d6L.jpg)
15
16
17
20## Demo 18## Demo
21 19
22You can use this [public demo instance of Shaarli](https://demo.shaarli.org). 20You can use this [public demo instance of Shaarli](https://demo.shaarli.org).
@@ -25,101 +23,80 @@ It runs the latest development version of Shaarli and is updated/reset daily.
25Login: `demo`; Password: `demo` 23Login: `demo`; Password: `demo`
26 24
27 25
26## Getting started
27
28- [Configure your server](Server-configuration.md)
29- [Install Shaarli](Installation.md)
30- Or install Shaarli using [Docker](Docker.md)
31
32
28## Features 33## Features
29 34
30Shaarli can be used: 35Shaarli can be used:
31 36
32- to share, comment and save interesting links and news 37- to share, comment and save interesting links
33- to bookmark useful/frequent links and share them between computers 38- to bookmark useful/frequent links and share them between computers
34- as a minimal blog/microblog/writing platform 39- as a minimal blog/microblog/writing platform
35- as a read-it-later list 40- as a read-it-later/todo list
36- to draft and save articles/posts/ideas 41- as a notepad to draft and save articles/posts/ideas
37- to keep notes, documentation and code snippets 42- as a knowledge base to keep notes, documentation and code snippets
38- as a shared clipboard/notepad/pastebin between machines 43- as a shared clipboard/notepad/pastebin between computers
39- as a todo list 44- as playlist manager for online media
40- to store media playlists 45- to feed other blogs, aggregators, social networks...
41- to keep extracts/comments from webpages that may disappear.
42- to keep track of ongoing discussions
43- to feed other blogs, aggregators, social networks... using RSS feeds
44 46
45### Edit, view and search your links 47### Edit, view and search your links
46 48
47- Minimalist design 49- Editable URL, title, description, tags, private/public status for all your [Shaares](Usage.md)
48- FAST 50- [Tags](Usage.md#tags) to organize your Shaares
49- Customizable link titles and descriptions 51- [Search](Usage.md#search) in all fields
50- Tags to organize your links (features tag autocompletion, renaming, merging and deletion) 52- Unique [permalinks](Usage.md#permalinks) for easy reference
51- Search by tag or using the full-text search 53- Paginated Shaares list view (with image and video thumbnails)
52- Public and private links (visible only to logged-in users) 54- [Tag cloud/list](Usage#tag-cloud) views
53- Unique permalinks for easy reference 55- [Picture wall](Usage#picture-wall)/thumbnails view (with lazy loading)
54- Paginated link list (with image and video thumbnails) 56- [ATOM and RSS feeds](Usage.md#rss-feeds) (can also be filtered using tags or text search)
55- Tag cloud and list views 57- [Daily](Usage.md#daily): newspaper-like daily digest (and daily RSS feed)
56- Picture wall: image and video thumbnails view (with lazy loading) 58- URL cleanup: automatic removal of `?utm_source=...`, `fb=...` tracking parameters
57- ATOM and RSS feeds (can also be filtered using tags or text search) 59- Extensible through [plugins](Plugins.md)
58- Daily: newspaper-like daily digest (and daily RSS feed) 60- Easily extensible by any client using the [REST API](REST-API.md) exposed by Shaarli
59- URL cleanup: automatic removal of `?utm_source=...`, `fb=...` 61- Bookmarklet and [other tools](Community-and-related-software.md) to share links in one click
60- Extensible through [plugins](https://shaarli.readthedocs.io/en/master/Plugins/#plugin-usage) 62- Responsive/support for mobile browsers, degrades gracefully with Javascript disabled
61
62### Easy setup
63
64- Dead-simple installation: drop the files, open the page
65- Links are stored in a file (no database required, easy backup: simply copy the datastore file)
66- Import and export links as Netscape bookmarks compatible with most Web browsers
67
68### Accessibility
69
70- Bookmarklet and other tools to share links in one click
71- Support for mobile browsers
72- Degrades gracefully with Javascript disabled
73- Easy page customization through HTML/CSS/RainTPL
74
75### Security
76
77- Discreet pop-up notification when a new release is available
78- Bruteforce protection on the login form
79- Protected against [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) and session cookie hijacking
80 63
81<!-- TODO Limitations -->
82 64
83### REST API 65### Easy setup
84
85- Easily extensible by any client using the REST API exposed by Shaarli ([API documentation](http://shaarli.github.io/api-documentation/)).
86 66
67- Dead-simple [installation](Installation.md): drop the files on your server, open the page
68- Shaares are stored in a file (no database required, easy [backup](Backup-and-restore.md))
69- [Configurable](Shaarli-configuration.md) from dialog and configuration file
70- Extensible through third-party [plugins and themes](Community-and-related-software.md)
87 71
88 72
89## Screenshots 73### Fast
90 74
91[![](https://i.imgur.com/8wEBRSG.png)](https://i.imgur.com/WWPfSj0.png) [![](https://i.imgur.com/rrsjWYy.png)](https://i.imgur.com/TZzGHMs.png) [![](https://i.imgur.com/uICDOle.png)](https://i.imgur.com/27wYsbC.png) [![](https://i.imgur.com/KNvFGVB.png)](https://i.imgur.com/0f5faqw.png) [![](https://i.imgur.com/tVvD3gH.png)](https://i.imgur.com/zGF4d6L.jpg) [![](https://i.imgur.com/8iRzHfe.png)](https://i.imgur.com/sfJJ6NT.png) [![](https://i.imgur.com/GjZGvIh.png)](https://i.imgur.com/QsedIuJ.png) [![](https://i.imgur.com/TFZ9PEq.png)](https://i.imgur.com/KdtF8Ll.png) [![](https://i.imgur.com/IvlqXXK.png)](https://i.imgur.com/boaaibC.png) [![](https://i.imgur.com/nlETouG.png)](https://i.imgur.com/Ib9O7n3.png) 75- Fast! Small datastore file, write-once/read-many, served most of the time from OS disk caches (no disk I/O)
76- Stays fast with even tens of thousands shaares!
92 77
93 78
79### Self-hosted
94 80
81- Shaarli is an alternative to commercial services such as StumbleUpon, Delicio.us, Diigo...
82- The data is yours, [import and export](Usage#import-export) it to HTML bookmarksformat compatible with most web browser, and from a variety of formats
83- Shaarli does not send any telemetry/metrics/private information to developers
84- Shaarli is Free and Open-Source software, inspect and change how the program works in the [source code](https://github.com/shaarli/Shaarli)
85- Built-in [Security](dev/Development.md#security) features to help you protect your Shaarli instance
95 86
96 87
97## About 88## About
98 89
99### Shaarli community fork 90This [community fork](https://github.com/shaarli/Shaarli) of the original [Shaarli](https://github.com/sebsauvage/Shaarli/) project by [Sébastien Sauvage](http://sebsauvage.net/) (now [unmaintained](https://github.com/sebsauvage/Shaarli/issues/191)) has carried on the work to provide [many patches](https://github.com/shaarli/Shaarli/compare/sebsauvage:master...master) for [bug fixes and enhancements](https://github.com/shaarli/Shaarli/issues?q=is%3Aclosed+) in this repository, and will keep maintaining the project for the foreseeable future, while keeping Shaarli simple and efficient.
100
101This friendly fork is maintained by the Shaarli community at <https://github.com/shaarli/Shaarli>
102
103This is a community fork of the original [Shaarli](https://github.com/sebsauvage/Shaarli/) project by [Sébastien Sauvage](http://sebsauvage.net/).
104
105The original project is currently unmaintained, and the developer [has informed us](https://github.com/sebsauvage/Shaarli/issues/191) that he would have no time to work on Shaarli in the near future.
106 91
107The Shaarli community has carried on the work to provide [many 92The original Shaarli instance is still available [here](https://sebsauvage.net/links/) (+25000 shaares!)
108patches](https://github.com/shaarli/Shaarli/compare/sebsauvage:master...master) for
109[bug fixes and enhancements](https://github.com/shaarli/Shaarli/issues?q=is%3Aclosed+)
110in this repository, and will keep maintaining the project for the foreseeable
111future, while keeping Shaarli simple and efficient.
112 93
113 94
114### Contributing and getting help 95### Contributing and getting help
115 96
116Feedback is very appreciated! 97Feedback is very appreciated! Feel free to propose solutions to existing problems, help us improve the documentation and translations, and submit pull requests :-)
117 98
118- If you have any questions or ideas, please join the [chat](https://gitter.im/shaarli/Shaarli) (also reachable via [IRC](https://irc.gitter.im/)), post them in our [general discussion](https://github.com/shaarli/Shaarli/issues/308) or read the current [issues](https://github.com/shaarli/Shaarli/issues). 99See [Support](Troubleshooting.md#support) to get in touch with the Shaarli community.
119- Have a look at the open [issues](https://github.com/shaarli/Shaarli/issues) and [pull requests](https://github.com/shaarli/Shaarli/pulls)
120- If you would like a feature added to Shaarli, check the issues labeled [`feature`](https://github.com/shaarli/Shaarli/labels/feature), [`enhancement`](https://github.com/shaarli/Shaarli/labels/enhancement), and [`plugin`](https://github.com/shaarli/Shaarli/labels/plugin).
121- If you've found a bug, please create a [new issue](https://github.com/shaarli/Shaarli/issues/new).
122- Feel free to propose solutions to existing problems, help us improve the documentation and translations, and submit pull requests :-)
123 100
124 101
125### License 102### License
diff --git a/docker-compose.yml b/docker-compose.yml
index e8ea4271..a3de4b1c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -33,7 +33,7 @@ services:
33 traefik.frontend.rule: "Host:${SHAARLI_VIRTUAL_HOST}" 33 traefik.frontend.rule: "Host:${SHAARLI_VIRTUAL_HOST}"
34 34
35 traefik: 35 traefik:
36 image: traefik 36 image: traefik:1.7-alpine
37 command: 37 command:
38 - "--defaultentrypoints=http,https" 38 - "--defaultentrypoints=http,https"
39 - "--entrypoints=Name:http Address::80 Redirect.EntryPoint:https" 39 - "--entrypoints=Name:http Address::80 Redirect.EntryPoint:https"
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
index 026d0101..60ea7a97 100644
--- a/inc/languages/fr/LC_MESSAGES/shaarli.po
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -1,55 +1,30 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: Shaarli\n" 3"Project-Id-Version: Shaarli\n"
4"POT-Creation-Date: 2019-07-13 10:45+0200\n" 4"POT-Creation-Date: 2020-10-27 19:44+0100\n"
5"PO-Revision-Date: 2019-07-13 10:49+0200\n" 5"PO-Revision-Date: 2020-10-27 19:44+0100\n"
6"Last-Translator: \n" 6"Last-Translator: \n"
7"Language-Team: Shaarli\n" 7"Language-Team: Shaarli\n"
8"Language: fr_FR\n" 8"Language: fr_FR\n"
9"MIME-Version: 1.0\n" 9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n" 10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n" 11"Content-Transfer-Encoding: 8bit\n"
12"X-Generator: Poedit 2.2.1\n" 12"X-Generator: Poedit 2.3\n"
13"X-Poedit-Basepath: ../../../..\n" 13"X-Poedit-Basepath: ../../../..\n"
14"Plural-Forms: nplurals=2; plural=(n > 1);\n" 14"Plural-Forms: nplurals=2; plural=(n > 1);\n"
15"X-Poedit-SourceCharset: UTF-8\n" 15"X-Poedit-SourceCharset: UTF-8\n"
16"X-Poedit-KeywordsList: t:1,2;t\n" 16"X-Poedit-KeywordsList: t:1,2;t\n"
17"X-Poedit-SearchPath-0: .\n" 17"X-Poedit-SearchPath-0: application\n"
18"X-Poedit-SearchPathExcluded-0: node_modules\n" 18"X-Poedit-SearchPath-1: tmp\n"
19"X-Poedit-SearchPathExcluded-1: vendor\n" 19"X-Poedit-SearchPath-2: index.php\n"
20"X-Poedit-SearchPath-3: init.php\n"
21"X-Poedit-SearchPath-4: plugins\n"
20 22
21#: application/ApplicationUtils.php:159 23#: application/History.php:180
22#, php-format
23msgid ""
24"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
25"cannot run. Your PHP version has known security vulnerabilities and should "
26"be updated as soon as possible."
27msgstr ""
28"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
29"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
30"connues et devrait être mise à jour au plus tôt."
31
32#: application/ApplicationUtils.php:189 application/ApplicationUtils.php:201
33msgid "directory is not readable"
34msgstr "le répertoire n'est pas accessible en lecture"
35
36#: application/ApplicationUtils.php:204
37msgid "directory is not writable"
38msgstr "le répertoire n'est pas accessible en écriture"
39
40#: application/ApplicationUtils.php:222
41msgid "file is not readable"
42msgstr "le fichier n'est pas accessible en lecture"
43
44#: application/ApplicationUtils.php:225
45msgid "file is not writable"
46msgstr "le fichier n'est pas accessible en écriture"
47
48#: application/History.php:178
49msgid "History file isn't readable or writable" 24msgid "History file isn't readable or writable"
50msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" 25msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
51 26
52#: application/History.php:189 27#: application/History.php:191
53msgid "Could not parse history file" 28msgid "Could not parse history file"
54msgstr "Format incorrect pour le fichier d'historique" 29msgstr "Format incorrect pour le fichier d'historique"
55 30
@@ -58,16 +33,20 @@ msgid "Automatic"
58msgstr "Automatique" 33msgstr "Automatique"
59 34
60#: application/Languages.php:182 35#: application/Languages.php:182
36msgid "German"
37msgstr "Allemand"
38
39#: application/Languages.php:183
61msgid "English" 40msgid "English"
62msgstr "Anglais" 41msgstr "Anglais"
63 42
64#: application/Languages.php:183 43#: application/Languages.php:184
65msgid "French" 44msgid "French"
66msgstr "Français" 45msgstr "Français"
67 46
68#: application/Languages.php:184 47#: application/Languages.php:185
69msgid "German" 48msgid "Japanese"
70msgstr "Allemand" 49msgstr "Japonais"
71 50
72#: application/Thumbnailer.php:62 51#: application/Thumbnailer.php:62
73msgid "" 52msgid ""
@@ -77,50 +56,138 @@ msgstr ""
77"l'extension php-gd doit être chargée pour utiliser les miniatures. Les " 56"l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
78"miniatures sont désormais désactivées. Rechargez la page." 57"miniatures sont désormais désactivées. Rechargez la page."
79 58
80#: application/Utils.php:379 tests/UtilsTest.php:343 59#: application/Utils.php:402
81msgid "Setting not set" 60msgid "Setting not set"
82msgstr "Paramètre non défini" 61msgstr "Paramètre non défini"
83 62
84#: application/Utils.php:386 tests/UtilsTest.php:341 tests/UtilsTest.php:342 63#: application/Utils.php:409
85msgid "Unlimited" 64msgid "Unlimited"
86msgstr "Illimité" 65msgstr "Illimité"
87 66
88#: application/Utils.php:389 tests/UtilsTest.php:338 tests/UtilsTest.php:339 67#: application/Utils.php:412
89#: tests/UtilsTest.php:353
90msgid "B" 68msgid "B"
91msgstr "o" 69msgstr "o"
92 70
93#: application/Utils.php:389 tests/UtilsTest.php:332 tests/UtilsTest.php:333 71#: application/Utils.php:412
94#: tests/UtilsTest.php:340
95msgid "kiB" 72msgid "kiB"
96msgstr "ko" 73msgstr "ko"
97 74
98#: application/Utils.php:389 tests/UtilsTest.php:334 tests/UtilsTest.php:335 75#: application/Utils.php:412
99#: tests/UtilsTest.php:351 tests/UtilsTest.php:352
100msgid "MiB" 76msgid "MiB"
101msgstr "Mo" 77msgstr "Mo"
102 78
103#: application/Utils.php:389 tests/UtilsTest.php:336 tests/UtilsTest.php:337 79#: application/Utils.php:412
104msgid "GiB" 80msgid "GiB"
105msgstr "Go" 81msgstr "Go"
106 82
107#: application/bookmark/LinkDB.php:128 83#: application/bookmark/BookmarkFileService.php:183
108msgid "You are not authorized to add a link." 84#: application/bookmark/BookmarkFileService.php:205
109msgstr "Vous n'êtes pas autorisé à ajouter un lien." 85#: application/bookmark/BookmarkFileService.php:227
86#: application/bookmark/BookmarkFileService.php:241
87msgid "You're not authorized to alter the datastore"
88msgstr "Vous n'êtes pas autorisé à modifier les données"
110 89
111#: application/bookmark/LinkDB.php:131 90#: application/bookmark/BookmarkFileService.php:208
112msgid "Internal Error: A link should always have an id and URL." 91msgid "This bookmarks already exists"
113msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL." 92msgstr "Ce marque-page existe déjà"
114 93
115#: application/bookmark/LinkDB.php:134 94#: application/bookmark/BookmarkInitializer.php:39
116msgid "You must specify an integer as a key." 95msgid "(private bookmark with thumbnail demo)"
117msgstr "Vous devez utiliser un entier comme clé." 96msgstr "(marque page privé avec une miniature)"
118 97
119#: application/bookmark/LinkDB.php:137 98#: application/bookmark/BookmarkInitializer.php:42
120msgid "Array offset and link ID must be equal." 99msgid ""
121msgstr "La clé du tableau et l'ID du lien doivent être identiques." 100"Shaarli will automatically pick up the thumbnail for links to a variety of "
101"websites.\n"
102"\n"
103"Explore your new Shaarli instance by trying out controls and menus.\n"
104"Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the "
105"documentation](https://shaarli.readthedocs.io/en/master/) to learn more "
106"about Shaarli.\n"
107"\n"
108"Now you can edit or delete the default shaares.\n"
109msgstr ""
110"Shaarli récupérera automatiquement la miniature associée au liens pour de "
111"nombreux sites web.\n"
112"\n"
113"Explorez votre nouvelle instance de Shaarli en essayant les différents "
114"contrôles et menus.\n"
115"Visitez le projet sur [Github](https://github.com/shaarli/Shaarli) ou [la "
116"documentation](https://shaarli.readthedocs.io/en/master/) pour en apprendre "
117"plus sur Shaarli.\n"
118"\n"
119"Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n"
122 120
123#: application/bookmark/LinkDB.php:243 121#: application/bookmark/BookmarkInitializer.php:55
122msgid "Note: Shaare descriptions"
123msgstr "Note : Description des Shaares"
124
125#: application/bookmark/BookmarkInitializer.php:57
126msgid ""
127"Adding a shaare without entering a URL creates a text-only \"note\" post "
128"such as this one.\n"
129"This note is private, so you are the only one able to see it while logged "
130"in.\n"
131"\n"
132"You can use this to keep notes, post articles, code snippets, and much "
133"more.\n"
134"\n"
135"The Markdown formatting setting allows you to format your notes and bookmark "
136"description:\n"
137"\n"
138"### Title headings\n"
139"\n"
140"#### Multiple headings levels\n"
141" * bullet lists\n"
142" * _italic_ text\n"
143" * **bold** text\n"
144" * ~~strike through~~ text\n"
145" * `code` blocks\n"
146" * images\n"
147" * [links](https://en.wikipedia.org/wiki/Markdown)\n"
148"\n"
149"Markdown also supports tables:\n"
150"\n"
151"| Name | Type | Color | Qty |\n"
152"| ------- | --------- | ------ | ----- |\n"
153"| Orange | Fruit | Orange | 126 |\n"
154"| Apple | Fruit | Any | 62 |\n"
155"| Lemon | Fruit | Yellow | 30 |\n"
156"| Carrot | Vegetable | Red | 14 |\n"
157msgstr ""
158"Ajouter un shaare sans préciser d'URL créé une « note » textuelle, telle que "
159"celle-ci.\n"
160"Cette note est privée, donc vous êtes seul à pouvoir la voir lorsque vous "
161"êtes connecté.\n"
162"\n"
163"Vous pouvez utiliser cette fonctionnalité pour prendre des notes, publier "
164"des articles, des extraits de code, et bien plus.\n"
165"\n"
166"L'option du formatage par Markdown vous permet de formater vos description "
167"de notes et marque-pages :\n"
168"\n"
169"### Titre d'en-tête\n"
170"\n"
171"#### Sur plusieurs niveaux\n"
172" * liste à puce\n"
173" * texte en _italique_\n"
174" * texte en **gras**\n"
175" * texte ~~barré~~\n"
176" * blocs de `code`\n"
177" * images\n"
178" * [liens](https://en.wikipedia.org/wiki/Markdown)\n"
179"\n"
180"Markdown supporte aussi les tableaux :\n"
181"\n"
182"| Nom | Type | Couleur | Qte |\n"
183"| ------- | --------- | ------ | ----- |\n"
184"| Orange | Fruit | Orange | 126 |\n"
185"| Pomme | Fruit | Multiple | 62 |\n"
186"| Citron | Fruit | Jaune | 30 |\n"
187"| Carotte | Légume | Orange | 14 |\n"
188
189#: application/bookmark/BookmarkInitializer.php:91
190#: application/legacy/LegacyLinkDB.php:246
124#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 191#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
125#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 192#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
126#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 193#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
@@ -131,37 +198,56 @@ msgstr ""
131"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " 198"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
132"données" 199"données"
133 200
134#: application/bookmark/LinkDB.php:246 201#: application/bookmark/BookmarkInitializer.php:94
135msgid "" 202msgid ""
136"Welcome to Shaarli! This is your first public bookmark. To edit or delete " 203"Welcome to Shaarli!\n"
137"me, you must first login.\n"
138"\n" 204"\n"
139"To learn how to use Shaarli, consult the link \"Documentation\" at the " 205"Shaarli allows you to bookmark your favorite pages, and share them with "
140"bottom of this page.\n" 206"others or store them privately.\n"
207"You can add a description to your bookmarks, such as this one, and tag "
208"them.\n"
141"\n" 209"\n"
142"You use the community supported version of the original Shaarli project, by " 210"Create a new shaare by clicking the `+Shaare` button, or using any of the "
143"Sebastien Sauvage." 211"recommended tools (browser extension, mobile app, bookmarklet, REST API, "
144msgstr "" 212"etc.).\n"
145"Bienvenue sur Shaarli ! Ceci est votre premier marque-page public. Pour me "
146"modifier ou me supprimer, vous devez d'abord vous connecter.\n"
147"\n" 213"\n"
148"Pour apprendre à utiliser Shaarli, consultez le lien « Documentation » en " 214"You can easily retrieve your links, even with thousands of them, using the "
149"bas de page.\n" 215"internal search engine, or search through tags (e.g. this Shaare is tagged "
216"with `shaarli` and `help`).\n"
217"Hashtags such as #shaarli #help are also supported.\n"
218"You can also filter the available [RSS feed](/feed/atom) and picture wall by "
219"tag or plaintext search.\n"
150"\n" 220"\n"
151"Vous utilisez la version supportée par la communauté du projet original " 221"We hope that you will enjoy using Shaarli, maintained with ❤️ by the "
152"Shaarli de Sébastien Sauvage." 222"community!\n"
153 223"Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if "
154#: application/bookmark/LinkDB.php:263 224"you have a suggestion or encounter an issue.\n"
155msgid "My secret stuff... - Pastebin.com"
156msgstr "Mes trucs secrets... - Pastebin.com"
157
158#: application/bookmark/LinkDB.php:265
159msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
160msgstr "" 225msgstr ""
161"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me " 226"Bienvenue sur Shaarli !\n"
162"supprimer aussi." 227"\n"
228"Shaarli vous permet de sauvegarder des marque-pages de vos pages favorites, "
229"et de les partager avec d'autres, ou de les enregistrer en privé.\n"
230"Vous pouvez ajouter une description à vos marque-pages, comme celle-ci, et y "
231"ajouter des tags.\n"
232"\n"
233"Créez un nouveau shaare en cliquant sur le bouton `+Shaare`, ou en utilisant "
234"l'un des outils recommandés (extension de navigateur, application mobile, "
235"bookmarklet, REST API, etc.).\n"
236"\n"
237"Vous pouvez facilement retrouver vos liens, même parmi des milliers, en "
238"utilisant le moteur de recherche interne, ou en filtrant par tags (par "
239"exemple ce Shaare est taggé avec `shaarli` et `help`).\n"
240"Les hashtags comme #shaarli #help sont aussi supportés.\n"
241"Vous pouvez aussi filtrer les [flux RSS](/feed/atom) et [mur d'images]() par "
242"tag ou par texte brut.\n"
243"\n"
244"Nous espérons que vous apprécierez utiliser Shaarli, maintenu avec ❤️ par la "
245"communauté !\n"
246"N'hésitez pas à ouvrir [un ticket (en)](https://github.com/shaarli/Shaarli/"
247"issues) si vous avez une suggestion ou si vous rencontrez un problème.\n"
248" \n"
163 249
164#: application/bookmark/exception/LinkNotFoundException.php:13 250#: application/bookmark/exception/BookmarkNotFoundException.php:13
165msgid "The link you are trying to reach does not exist or has been deleted." 251msgid "The link you are trying to reach does not exist or has been deleted."
166msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." 252msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé."
167 253
@@ -173,8 +259,8 @@ msgstr ""
173"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que " 259"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
174"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé." 260"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
175 261
176#: application/config/ConfigManager.php:135 262#: application/config/ConfigManager.php:136
177#: application/config/ConfigManager.php:162 263#: application/config/ConfigManager.php:163
178msgid "Invalid setting key parameter. String expected, got: " 264msgid "Invalid setting key parameter. String expected, got: "
179msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " 265msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
180 266
@@ -196,247 +282,235 @@ msgstr "Vous n'êtes pas autorisé à modifier la configuration."
196msgid "Error accessing" 282msgid "Error accessing"
197msgstr "Une erreur s'est produite en accédant à" 283msgstr "Une erreur s'est produite en accédant à"
198 284
199#: application/feed/Cache.php:16 285#: application/feed/FeedBuilder.php:179
200#, php-format
201msgid "Cannot purge %s: no directory"
202msgstr "Impossible de purger %s : le répertoire n'existe pas"
203
204#: application/feed/FeedBuilder.php:155
205msgid "Direct link" 286msgid "Direct link"
206msgstr "Liens directs" 287msgstr "Liens directs"
207 288
208#: application/feed/FeedBuilder.php:157 289#: application/feed/FeedBuilder.php:181
209#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 290#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
210#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177 291#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
292#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
211msgid "Permalink" 293msgid "Permalink"
212msgstr "Permalien" 294msgstr "Permalien"
213 295
214#: application/netscape/NetscapeBookmarkUtils.php:42 296#: application/front/controller/admin/ConfigureController.php:54
215msgid "Invalid export selection:" 297#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
216msgstr "Sélection d'export invalide :" 298msgid "Configure"
299msgstr "Configurer"
217 300
218#: application/netscape/NetscapeBookmarkUtils.php:87 301#: application/front/controller/admin/ConfigureController.php:102
219#, php-format 302#: application/legacy/LegacyUpdater.php:537
220msgid "File %s (%d bytes) " 303msgid "You have enabled or changed thumbnails mode."
221msgstr "Le fichier %s (%d octets) " 304msgstr "Vous avez activé ou changé le mode de miniatures."
222 305
223#: application/netscape/NetscapeBookmarkUtils.php:89 306#: application/front/controller/admin/ConfigureController.php:103
224msgid "has an unknown file format. Nothing was imported." 307#: application/front/controller/admin/ServerController.php:68
225msgstr "a un format inconnu. Rien n'a été importé." 308#: application/legacy/LegacyUpdater.php:538
309msgid "Please synchronize them."
310msgstr "Merci de les synchroniser."
226 311
227#: application/netscape/NetscapeBookmarkUtils.php:93 312#: application/front/controller/admin/ConfigureController.php:113
228#, php-format 313#: application/front/controller/visitor/InstallController.php:146
229msgid "" 314msgid "Error while writing config file after configuration update."
230"was successfully processed in %d seconds: %d links imported, %d links "
231"overwritten, %d links skipped."
232msgstr "" 315msgstr ""
233"a été importé avec succès en %d secondes : %d liens importés, %d liens " 316"Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
234"écrasés, %d liens ignorés."
235 317
236#: application/plugin/exception/PluginFileNotFoundException.php:21 318#: application/front/controller/admin/ConfigureController.php:122
237#, php-format 319msgid "Configuration was saved."
238msgid "Plugin \"%s\" files not found." 320msgstr "La configuration a été sauvegardée."
239msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
240 321
241#: application/render/PageBuilder.php:209 322#: application/front/controller/admin/ExportController.php:26
242msgid "The page you are trying to reach does not exist or has been deleted." 323#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
243msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée." 324msgid "Export"
325msgstr "Exporter"
244 326
245#: application/render/PageBuilder.php:211 327#: application/front/controller/admin/ExportController.php:42
246msgid "404 Not Found" 328msgid "Please select an export mode."
247msgstr "404 Introuvable" 329msgstr "Merci de choisir un mode d'export."
248 330
249#: application/updater/Updater.php:99 331#: application/front/controller/admin/ImportController.php:41
250#, fuzzy 332#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
251#| msgid "Couldn't retrieve Updater class methods." 333msgid "Import"
252msgid "Couldn't retrieve updater class methods." 334msgstr "Importer"
253msgstr "Impossible de récupérer les méthodes de la classe Updater."
254 335
255#: application/updater/Updater.php:526 index.php:1034 336#: application/front/controller/admin/ImportController.php:55
256msgid "" 337msgid "No import file provided."
257"You have enabled or changed thumbnails mode. <a href=\"?do=thumbs_update" 338msgstr "Aucun fichier à importer n'a été fourni."
258"\">Please synchronize them</a>."
259msgstr ""
260"Vous avez activé ou changé le mode de miniatures. <a href=\"?do=thumbs_update"
261"\">Merci de les synchroniser</a>."
262 339
263#: application/updater/UpdaterUtils.php:32 340#: application/front/controller/admin/ImportController.php:66
264msgid "Updates file path is not set, can't write updates." 341#, php-format
342msgid ""
343"The file you are trying to upload is probably bigger than what this "
344"webserver can accept (%s). Please upload in smaller chunks."
265msgstr "" 345msgstr ""
266"Le chemin vers le fichier de mise à jour n'est pas défini, impossible " 346"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
267"d'écrire les mises à jour." 347"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
268 348"légères."
269#: application/updater/UpdaterUtils.php:37
270msgid "Unable to write updates in "
271msgstr "Impossible d'écrire les mises à jour dans "
272
273#: application/updater/exception/UpdaterException.php:51
274msgid "An error occurred while running the update "
275msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
276
277#: index.php:145
278msgid "Shared links on "
279msgstr "Liens partagés sur "
280
281#: index.php:167
282msgid "Insufficient permissions:"
283msgstr "Permissions insuffisantes :"
284
285#: index.php:203
286msgid "I said: NO. You are banned for the moment. Go away."
287msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
288
289#: index.php:275
290msgid "Wrong login/password."
291msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
292
293#: index.php:398 index.php:404
294msgid "Today"
295msgstr "Aujourd'hui"
296
297#: index.php:400
298msgid "Yesterday"
299msgstr "Hier"
300
301#: index.php:484 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
302#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:46
303msgid "Daily"
304msgstr "Quotidien"
305 349
306#: index.php:593 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 350#: application/front/controller/admin/ManageShaareController.php:64
307#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 351#: application/front/controller/admin/ManageShaareController.php:95
308#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 352#: application/front/controller/admin/ManageShaareController.php:193
309#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 353#: application/front/controller/admin/ManageShaareController.php:262
310#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:75 354#: application/front/controller/admin/ManageShaareController.php:302
311#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:99 355#: application/front/controller/admin/ManageShaareController.php:181
312msgid "Login" 356#: application/front/controller/admin/ManageShaareController.php:239
313msgstr "Connexion" 357#: application/front/controller/admin/ManageShaareController.php:247
358#: application/front/controller/admin/ManageShaareController.php:378
359#: application/front/controller/admin/ManageShaareController.php:381
360#: application/front/controller/admin/ManageTagController.php:29
361#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
362#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
363msgid "Manage tags"
364msgstr "Gérer les tags"
314 365
315#: index.php:608 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 366#: application/front/controller/admin/ManageTagController.php:48
316#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:41 367msgid "Invalid tags provided."
317msgid "Picture wall" 368msgstr "Les tags fournis ne sont pas valides."
318msgstr "Mur d'images"
319 369
320#: index.php:683 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 370#: application/front/controller/admin/ManageTagController.php:72
321#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36 371#, php-format
322#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 372msgid "The tag was removed from %d bookmark."
323msgid "Tag cloud" 373msgid_plural "The tag was removed from %d bookmarks."
324msgstr "Nuage de tags" 374msgstr[0] "Le tag a été supprimé du %d lien."
375msgstr[1] "Le tag a été supprimé de %d liens."
325 376
326#: index.php:715 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 377#: application/front/controller/admin/ManageTagController.php:77
327msgid "Tag list" 378#, php-format
328msgstr "Liste des tags" 379msgid "The tag was renamed in %d bookmark."
380msgid_plural "The tag was renamed in %d bookmarks."
381msgstr[0] "Le tag a été renommé dans %d lien."
382msgstr[1] "Le tag a été renommé dans %d liens."
329 383
330#: index.php:944 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 384#: application/front/controller/admin/PasswordController.php:28
331#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31 385#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
332msgid "Tools" 386#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
333msgstr "Outils" 387msgid "Change password"
388msgstr "Modifier le mot de passe"
334 389
335#: index.php:952 390#: application/front/controller/admin/PasswordController.php:55
336msgid "You are not supposed to change a password on an Open Shaarli." 391msgid "You must provide the current and new password to change it."
337msgstr "" 392msgstr ""
338"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert." 393"Vous devez fournir les mots de passe actuel et nouveau pour pouvoir le "
339 394"modifier."
340#: index.php:957 index.php:1007 index.php:1094 index.php:1124 index.php:1234
341#: index.php:1281
342msgid "Wrong token."
343msgstr "Jeton invalide."
344 395
345#: index.php:966 396#: application/front/controller/admin/PasswordController.php:71
346msgid "The old password is not correct." 397msgid "The old password is not correct."
347msgstr "L'ancien mot de passe est incorrect." 398msgstr "L'ancien mot de passe est incorrect."
348 399
349#: index.php:993 400#: application/front/controller/admin/PasswordController.php:97
350msgid "Your password has been changed" 401msgid "Your password has been changed"
351msgstr "Votre mot de passe a été modifié" 402msgstr "Votre mot de passe a été modifié"
352 403
353#: index.php:997 404#: application/front/controller/admin/PluginsController.php:45
354#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 405msgid "Plugin Administration"
355#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 406msgstr "Administration des plugins"
356msgid "Change password"
357msgstr "Modifier le mot de passe"
358 407
359#: index.php:1054 408#: application/front/controller/admin/PluginsController.php:76
360msgid "Configuration was saved." 409msgid "Setting successfully saved."
361msgstr "La configuration a été sauvegardée." 410msgstr "Les paratres ont été sauvegardés avec succès."
362 411
363#: index.php:1078 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 412#: application/front/controller/admin/PluginsController.php:79
364msgid "Configure" 413msgid "Error while saving plugin configuration: "
365msgstr "Configurer" 414msgstr ""
415"Une erreur s'est produite lors de la sauvegarde de la configuration des "
416"plugins : "
366 417
367#: index.php:1088 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 418#: application/front/controller/admin/ServerController.php:50
368#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 419#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
369msgid "Manage tags" 420#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
370msgstr "Gérer les tags" 421msgid "Server administration"
422msgstr "Administration serveur"
371 423
372#: index.php:1107 424#: application/front/controller/admin/ServerController.php:67
373#, php-format 425msgid "Thumbnails cache has been cleared."
374msgid "The tag was removed from %d link." 426msgstr "Le cache des miniatures a été vidé."
375msgid_plural "The tag was removed from %d links." 427
376msgstr[0] "Le tag a été supprimé de %d lien." 428#: application/front/controller/admin/ServerController.php:76
377msgstr[1] "Le tag a été supprimé de %d liens." 429msgid "Shaarli's cache folder has been cleared!"
430msgstr "Le dossier de cache de Shaarli a été vidé !"
378 431
379#: index.php:1108
380#, php-format 432#, php-format
381msgid "The tag was renamed in %d link." 433msgid "Bookmark with identifier %s could not be found."
382msgid_plural "The tag was renamed in %d links." 434msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
383msgstr[0] "Le tag a été renommé dans %d lien."
384msgstr[1] "Le tag a été renommé dans %d liens."
385 435
386#: index.php:1115 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 436#: application/front/controller/admin/ShaareManageController.php:101
387msgid "Shaare a new link" 437msgid "Invalid visibility provided."
388msgstr "Partager un nouveau lien" 438msgstr "Visibilité du lien non valide."
389 439
390#: index.php:1344 tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 440#: application/front/controller/admin/ShaarePublishController.php:154
441#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
391msgid "Edit" 442msgid "Edit"
392msgstr "Modifier" 443msgstr "Modifier"
393 444
394#: index.php:1344 index.php:1416 445#: application/front/controller/admin/ShaarePublishController.php:157
395#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 446#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
396#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26 447#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
397msgid "Shaare" 448msgid "Shaare"
398msgstr "Shaare" 449msgstr "Shaare"
399 450
400#: index.php:1385 451#: application/front/controller/admin/ShaarePublishController.php:184
401msgid "Note: " 452msgid "Note: "
402msgstr "Note : " 453msgstr "Note : "
403 454
404#: index.php:1424 455#: application/front/controller/admin/ThumbnailsController.php:37
405msgid "Invalid link ID provided" 456#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
406msgstr "ID du lien non valide" 457msgid "Thumbnails update"
458msgstr "Mise à jour des miniatures"
407 459
408#: index.php:1444 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 460#: application/front/controller/admin/ToolsController.php:31
409msgid "Export" 461#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
410msgstr "Exporter" 462#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33
463msgid "Tools"
464msgstr "Outils"
411 465
412#: index.php:1506 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 466#: application/front/controller/visitor/BookmarkListController.php:116
413msgid "Import" 467msgid "Search: "
414msgstr "Importer" 468msgstr "Recherche : "
415 469
416#: index.php:1516 470#: application/front/controller/visitor/DailyController.php:200
417#, php-format 471msgid "day"
418msgid "" 472msgstr "jour"
419"The file you are trying to upload is probably bigger than what this "
420"webserver can accept (%s). Please upload in smaller chunks."
421msgstr ""
422"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
423"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
424"légères."
425 473
426#: index.php:1561 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 474#: application/front/controller/visitor/DailyController.php:200
427#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 475#: application/front/controller/visitor/DailyController.php:203
428msgid "Plugin administration" 476#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
429msgstr "Administration des plugins" 477#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
478#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
479msgid "Daily"
480msgstr "Quotidien"
430 481
431#: index.php:1616 tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 482#: application/front/controller/visitor/DailyController.php:201
432msgid "Thumbnails update" 483msgid "week"
433msgstr "Mise à jour des miniatures" 484msgstr "semaine"
434 485
435#: index.php:1782 486#: application/front/controller/visitor/DailyController.php:201
436msgid "Search: " 487#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
437msgstr "Recherche : " 488msgid "Weekly"
489msgstr "Hebdomadaire"
490
491#: application/front/controller/visitor/DailyController.php:202
492msgid "month"
493msgstr "mois"
494
495#: application/front/controller/visitor/DailyController.php:202
496#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
497msgid "Monthly"
498msgstr "Mensuel"
499
500#: application/front/controller/visitor/ErrorController.php:33
501msgid "An unexpected error occurred."
502msgstr "Une erreur inattendue s'est produite."
438 503
439#: index.php:1825 504#: application/front/controller/visitor/ErrorNotFoundController.php:25
505msgid "Requested page could not be found."
506msgstr "La page demandée n'a pas pu être trouvée."
507
508#: application/front/controller/visitor/InstallController.php:64
509#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
510msgid "Install Shaarli"
511msgstr "Installation de Shaarli"
512
513#: application/front/controller/visitor/InstallController.php:83
440#, php-format 514#, php-format
441msgid "" 515msgid ""
442"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " 516"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@@ -455,9 +529,244 @@ msgstr ""
455"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " 529"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
456"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" 530"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
457 531
458#: index.php:1835 532#: application/front/controller/visitor/InstallController.php:154
459msgid "Click to try again." 533msgid ""
460msgstr "Cliquer ici pour réessayer." 534"Shaarli is now configured. Please login and start shaaring your bookmarks!"
535msgstr ""
536"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
537"shaare vos liens !"
538
539#: application/front/controller/visitor/InstallController.php:168
540msgid "Insufficient permissions:"
541msgstr "Permissions insuffisantes :"
542
543#: application/front/controller/visitor/LoginController.php:46
544#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
545#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
546#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
547#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
548#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:77
549#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:101
550msgid "Login"
551msgstr "Connexion"
552
553#: application/front/controller/visitor/LoginController.php:77
554msgid "Wrong login/password."
555msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
556
557#: application/front/controller/visitor/PictureWallController.php:29
558#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
559#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:43
560msgid "Picture wall"
561msgstr "Mur d'images"
562
563#: application/front/controller/visitor/TagCloudController.php:88
564msgid "Tag "
565msgstr "Tag "
566
567#: application/front/exceptions/AlreadyInstalledException.php:11
568msgid "Shaarli has already been installed. Login to edit the configuration."
569msgstr ""
570"Shaarli est déjà installé. Connectez-vous pour modifier la configuration."
571
572#: application/front/exceptions/LoginBannedException.php:11
573msgid ""
574"You have been banned after too many failed login attempts. Try again later."
575msgstr ""
576"Vous avez été banni après trop d'échecs d'authentification. Merci de "
577"réessayer plus tard."
578
579#: application/front/exceptions/OpenShaarliPasswordException.php:16
580msgid "You are not supposed to change a password on an Open Shaarli."
581msgstr ""
582"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
583
584#: application/front/exceptions/ThumbnailsDisabledException.php:11
585msgid "Picture wall unavailable (thumbnails are disabled)."
586msgstr ""
587"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
588
589#: application/front/exceptions/WrongTokenException.php:16
590msgid "Wrong token."
591msgstr "Jeton invalide."
592
593#: application/helper/ApplicationUtils.php:162
594#, php-format
595msgid ""
596"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
597"cannot run. Your PHP version has known security vulnerabilities and should "
598"be updated as soon as possible."
599msgstr ""
600"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
601"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
602"connues et devrait être mise à jour au plus tôt."
603
604#: application/helper/ApplicationUtils.php:195
605#: application/helper/ApplicationUtils.php:215
606msgid "directory is not readable"
607msgstr "le répertoire n'est pas accessible en lecture"
608
609#: application/helper/ApplicationUtils.php:218
610msgid "directory is not writable"
611msgstr "le répertoire n'est pas accessible en écriture"
612
613#: application/helper/ApplicationUtils.php:240
614msgid "file is not readable"
615msgstr "le fichier n'est pas accessible en lecture"
616
617#: application/helper/ApplicationUtils.php:243
618msgid "file is not writable"
619msgstr "le fichier n'est pas accessible en écriture"
620
621#: application/helper/ApplicationUtils.php:277
622msgid "Configuration parsing"
623msgstr "Chargement de la configuration"
624
625#: application/helper/ApplicationUtils.php:278
626msgid "Slim Framework (routing, etc.)"
627msgstr "Slim Framwork (routage, etc.)"
628
629#: application/helper/ApplicationUtils.php:279
630msgid "Multibyte (Unicode) string support"
631msgstr "Support des chaînes de caractère multibytes (Unicode)"
632
633#: application/helper/ApplicationUtils.php:280
634msgid "Required to use thumbnails"
635msgstr "Obligatoire pour utiliser les miniatures"
636
637#: application/helper/ApplicationUtils.php:281
638msgid "Localized text sorting (e.g. e->è->f)"
639msgstr "Tri des textes traduits (ex : e->è->f)"
640
641#: application/helper/ApplicationUtils.php:282
642msgid "Better retrieval of bookmark metadata and thumbnail"
643msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
644
645#: application/helper/ApplicationUtils.php:283
646msgid "Use the translation system in gettext mode"
647msgstr "Utiliser le système de traduction en mode gettext"
648
649#: application/helper/ApplicationUtils.php:284
650msgid "Login using LDAP server"
651msgstr "Authentification via un serveur LDAP"
652
653#: application/helper/DailyPageHelper.php:172
654msgid "Week"
655msgstr "Semaine"
656
657#: application/helper/DailyPageHelper.php:176
658msgid "Today"
659msgstr "Aujourd'hui"
660
661#: application/helper/DailyPageHelper.php:178
662msgid "Yesterday"
663msgstr "Hier"
664
665#: application/helper/FileUtils.php:100
666msgid "Provided path is not a directory."
667msgstr "Le chemin fourni n'est pas un dossier."
668
669#: application/helper/FileUtils.php:104
670msgid "Trying to delete a folder outside of Shaarli path."
671msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli."
672
673#: application/legacy/LegacyLinkDB.php:131
674msgid "You are not authorized to add a link."
675msgstr "Vous n'êtes pas autorisé à ajouter un lien."
676
677#: application/legacy/LegacyLinkDB.php:134
678msgid "Internal Error: A link should always have an id and URL."
679msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
680
681#: application/legacy/LegacyLinkDB.php:137
682msgid "You must specify an integer as a key."
683msgstr "Vous devez utiliser un entier comme clé."
684
685#: application/legacy/LegacyLinkDB.php:140
686msgid "Array offset and link ID must be equal."
687msgstr "La clé du tableau et l'ID du lien doivent être identiques."
688
689#: application/legacy/LegacyLinkDB.php:249
690msgid ""
691"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
692"me, you must first login.\n"
693"\n"
694"To learn how to use Shaarli, consult the link \"Documentation\" at the "
695"bottom of this page.\n"
696"\n"
697"You use the community supported version of the original Shaarli project, by "
698"Sebastien Sauvage."
699msgstr ""
700"Bienvenue sur Shaarli ! Ceci est votre premier marque-page public. Pour me "
701"modifier ou me supprimer, vous devez d'abord vous connecter.\n"
702"\n"
703"Pour apprendre à utiliser Shaarli, consultez le lien « Documentation » en "
704"bas de page.\n"
705"\n"
706"Vous utilisez la version supportée par la communauté du projet original "
707"Shaarli de Sébastien Sauvage."
708
709#: application/legacy/LegacyLinkDB.php:266
710msgid "My secret stuff... - Pastebin.com"
711msgstr "Mes trucs secrets... - Pastebin.com"
712
713#: application/legacy/LegacyLinkDB.php:268
714msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
715msgstr ""
716"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
717"supprimer aussi."
718
719#: application/legacy/LegacyUpdater.php:104
720msgid "Couldn't retrieve updater class methods."
721msgstr "Impossible de récupérer les méthodes de la classe Updater."
722
723#: application/legacy/LegacyUpdater.php:538
724msgid "<a href=\"./admin/thumbnails\">"
725msgstr "<a href=\"./admin/thumbnails\">"
726
727#: application/netscape/NetscapeBookmarkUtils.php:63
728msgid "Invalid export selection:"
729msgstr "Sélection d'export invalide :"
730
731#: application/netscape/NetscapeBookmarkUtils.php:215
732#, php-format
733msgid "File %s (%d bytes) "
734msgstr "Le fichier %s (%d octets) "
735
736#: application/netscape/NetscapeBookmarkUtils.php:217
737msgid "has an unknown file format. Nothing was imported."
738msgstr "a un format inconnu. Rien n'a été importé."
739
740#: application/netscape/NetscapeBookmarkUtils.php:221
741#, php-format
742msgid ""
743"was successfully processed in %d seconds: %d bookmarks imported, %d "
744"bookmarks overwritten, %d bookmarks skipped."
745msgstr ""
746"a été importé avec succès en %d secondes : %d liens importés, %d liens "
747"écrasés, %d liens ignorés."
748
749#: application/plugin/PluginManager.php:124
750msgid " [plugin incompatibility]: "
751msgstr " [incompatibilité de l'extension] : "
752
753#: application/plugin/exception/PluginFileNotFoundException.php:21
754#, php-format
755msgid "Plugin \"%s\" files not found."
756msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
757
758#: application/render/PageCacheManager.php:32
759#, php-format
760msgid "Cannot purge %s: no directory"
761msgstr "Impossible de purger %s : le répertoire n'existe pas"
762
763#: application/updater/exception/UpdaterException.php:51
764msgid "An error occurred while running the update "
765msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
766
767#: index.php:80
768msgid "Shared bookmarks on "
769msgstr "Liens partagés sur "
461 770
462#: plugins/addlink_toolbar/addlink_toolbar.php:31 771#: plugins/addlink_toolbar/addlink_toolbar.php:31
463msgid "URI" 772msgid "URI"
@@ -472,15 +781,15 @@ msgstr "Shaare"
472msgid "Adds the addlink input on the linklist page." 781msgid "Adds the addlink input on the linklist page."
473msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." 782msgstr "Ajoute le formulaire d'ajout de liens sur la page principale."
474 783
475#: plugins/archiveorg/archiveorg.php:25 784#: plugins/archiveorg/archiveorg.php:28
476msgid "View on archive.org" 785msgid "View on archive.org"
477msgstr "Voir sur archive.org" 786msgstr "Voir sur archive.org"
478 787
479#: plugins/archiveorg/archiveorg.php:38 788#: plugins/archiveorg/archiveorg.php:41
480msgid "For each link, add an Archive.org icon." 789msgid "For each link, add an Archive.org icon."
481msgstr "Pour chaque lien, ajoute une icône pour Archive.org." 790msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
482 791
483#: plugins/default_colors/default_colors.php:33 792#: plugins/default_colors/default_colors.php:38
484msgid "" 793msgid ""
485"Default colors plugin error: This plugin is active and no custom color is " 794"Default colors plugin error: This plugin is active and no custom color is "
486"configured." 795"configured."
@@ -488,25 +797,25 @@ msgstr ""
488"Erreur du plugin default colors : ce plugin est actif et aucune couleur " 797"Erreur du plugin default colors : ce plugin est actif et aucune couleur "
489"n'est configurée." 798"n'est configurée."
490 799
491#: plugins/default_colors/default_colors.php:107 800#: plugins/default_colors/default_colors.php:113
492msgid "Override default theme colors. Use any CSS valid color." 801msgid "Override default theme colors. Use any CSS valid color."
493msgstr "" 802msgstr ""
494"Remplacer les couleurs du thème par défaut. Utiliser n'importe quelle " 803"Remplacer les couleurs du thème par défaut. Utiliser n'importe quelle "
495"couleur CSS valide." 804"couleur CSS valide."
496 805
497#: plugins/default_colors/default_colors.php:108 806#: plugins/default_colors/default_colors.php:114
498msgid "Main color (navbar green)" 807msgid "Main color (navbar green)"
499msgstr "Couleur principale (vert de la barre de navigation)" 808msgstr "Couleur principale (vert de la barre de navigation)"
500 809
501#: plugins/default_colors/default_colors.php:109 810#: plugins/default_colors/default_colors.php:115
502msgid "Background color (light grey)" 811msgid "Background color (light grey)"
503msgstr "Couleur de fond (gris léger)" 812msgstr "Couleur de fond (gris léger)"
504 813
505#: plugins/default_colors/default_colors.php:110 814#: plugins/default_colors/default_colors.php:116
506msgid "Dark main color (e.g. visited links)" 815msgid "Dark main color (e.g. visited links)"
507msgstr "Couleur principale sombre (ex : les liens visités)" 816msgstr "Couleur principale sombre (ex : les liens visités)"
508 817
509#: plugins/demo_plugin/demo_plugin.php:482 818#: plugins/demo_plugin/demo_plugin.php:477
510msgid "" 819msgid ""
511"A demo plugin covering all use cases for template designers and plugin " 820"A demo plugin covering all use cases for template designers and plugin "
512"developers." 821"developers."
@@ -514,11 +823,11 @@ msgstr ""
514"Une extension de démonstration couvrant tous les cas d'utilisation pour les " 823"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
515"designers de thèmes et les développeurs d'extensions." 824"designers de thèmes et les développeurs d'extensions."
516 825
517#: plugins/demo_plugin/demo_plugin.php:483 826#: plugins/demo_plugin/demo_plugin.php:478
518msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." 827msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
519msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé." 828msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé."
520 829
521#: plugins/demo_plugin/demo_plugin.php:484 830#: plugins/demo_plugin/demo_plugin.php:479
522msgid "Other demo parameter" 831msgid "Other demo parameter"
523msgstr "Un autre paramètre de démo" 832msgstr "Un autre paramètre de démo"
524 833
@@ -540,36 +849,6 @@ msgstr ""
540msgid "Isso server URL (without 'http://')" 849msgid "Isso server URL (without 'http://')"
541msgstr "URL du serveur Isso (sans 'http://')" 850msgstr "URL du serveur Isso (sans 'http://')"
542 851
543#: plugins/markdown/markdown.php:163
544msgid "Description will be rendered with"
545msgstr "La description sera générée avec"
546
547#: plugins/markdown/markdown.php:164
548msgid "Markdown syntax documentation"
549msgstr "Documentation sur la syntaxe Markdown"
550
551#: plugins/markdown/markdown.php:165
552msgid "Markdown syntax"
553msgstr "la syntaxe Markdown"
554
555#: plugins/markdown/markdown.php:361
556msgid ""
557"Render shaare description with Markdown syntax.<br><strong>Warning</"
558"strong>:\n"
559"If your shaared descriptions contained HTML tags before enabling the "
560"markdown plugin,\n"
561"enabling it might break your page.\n"
562"See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
563"markdown#html-rendering\">README</a>."
564msgstr ""
565"Utilise la syntaxe Markdown pour la description des liens."
566"<br><strong>Attention</strong> :\n"
567"Si vous aviez des descriptions contenant du HTML avant d'activer cette "
568"extension,\n"
569"l'activer pourrait déformer vos pages.\n"
570"Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
571"markdown#html-rendering\">README</a>."
572
573#: plugins/piwik/piwik.php:23 852#: plugins/piwik/piwik.php:23
574msgid "" 853msgid ""
575"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " 854"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
@@ -626,7 +905,7 @@ msgstr "Mauvaise réponse du hub %s"
626msgid "Enable PubSubHubbub feed publishing." 905msgid "Enable PubSubHubbub feed publishing."
627msgstr "Active la publication de flux vers PubSubHubbub." 906msgstr "Active la publication de flux vers PubSubHubbub."
628 907
629#: plugins/qrcode/qrcode.php:72 plugins/wallabag/wallabag.php:68 908#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:70
630msgid "For each link, add a QRCode icon." 909msgid "For each link, add a QRCode icon."
631msgstr "Pour chaque lien, ajouter une icône de QRCode." 910msgstr "Pour chaque lien, ajouter une icône de QRCode."
632 911
@@ -642,24 +921,14 @@ msgstr ""
642msgid "Save to wallabag" 921msgid "Save to wallabag"
643msgstr "Sauvegarder dans Wallabag" 922msgstr "Sauvegarder dans Wallabag"
644 923
645#: plugins/wallabag/wallabag.php:69 924#: plugins/wallabag/wallabag.php:71
646msgid "Wallabag API URL" 925msgid "Wallabag API URL"
647msgstr "URL de l'API Wallabag" 926msgstr "URL de l'API Wallabag"
648 927
649#: plugins/wallabag/wallabag.php:70 928#: plugins/wallabag/wallabag.php:72
650msgid "Wallabag API version (1 or 2)" 929msgid "Wallabag API version (1 or 2)"
651msgstr "Version de l'API Wallabag (1 ou 2)" 930msgstr "Version de l'API Wallabag (1 ou 2)"
652 931
653#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
654#: tests/languages/fr/LanguagesFrTest.php:159
655#: tests/languages/fr/LanguagesFrTest.php:172
656#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
657#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:85
658msgid "Search"
659msgid_plural "Search"
660msgstr[0] "Rechercher"
661msgstr[1] "Rechercher"
662
663#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 932#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
664msgid "Sorry, nothing to see here." 933msgid "Sorry, nothing to see here."
665msgstr "Désolé, il y a rien à voir ici." 934msgstr "Désolé, il y a rien à voir ici."
@@ -668,6 +937,48 @@ msgstr "Désolé, il y a rien à voir ici."
668msgid "URL or leave empty to post a note" 937msgid "URL or leave empty to post a note"
669msgstr "URL ou laisser vide pour créer une note" 938msgstr "URL ou laisser vide pour créer une note"
670 939
940#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
941msgid "BULK CREATION"
942msgstr "CRÉATION DE MASSE"
943
944#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
945msgid "Metadata asynchronous retrieval is disabled."
946msgstr "La récupération asynchrone des meta-données est désactivée."
947
948#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
949msgid ""
950"We recommend that you enable the setting <em>general > "
951"enable_async_metadata</em> in your configuration file to use bulk link "
952"creation."
953msgstr ""
954"Nous recommandons d'activer le paramètre <em>general > "
955"enable_async_metadata</em> dans votre fichier de configuration pour utiliser "
956"la création de masse."
957
958#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
959msgid "Shaare multiple new links"
960msgstr "Partagez plusieurs nouveaux liens"
961
962#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
963msgid "Add one URL per line to create multiple bookmarks."
964msgstr "Ajouter une URL par ligne pour créer plusieurs marque-pages."
965
966#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
967#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
968msgid "Tags"
969msgstr "Tags"
970
971#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
972#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
973#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
974#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
975msgid "Private"
976msgstr "Privé"
977
978#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
979msgid "Add links"
980msgstr "Ajouter des liens"
981
671#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 982#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
672msgid "Current password" 983msgid "Current password"
673msgstr "Mot de passe actuel" 984msgstr "Mot de passe actuel"
@@ -694,16 +1005,13 @@ msgid "Case sensitive"
694msgstr "Sensible à la casse" 1005msgstr "Sensible à la casse"
695 1006
696#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 1007#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
697msgid "Rename" 1008#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
698msgstr "Renommer" 1009msgid "Rename tag"
1010msgstr "Renommer le tag"
699 1011
700#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 1012#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
701#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 1013msgid "Delete tag"
702#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 1014msgstr "Supprimer le tag"
703#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
704#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:145
705msgid "Delete"
706msgstr "Supprimer"
707 1015
708#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 1016#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
709msgid "You can also edit tags in the" 1017msgid "You can also edit tags in the"
@@ -713,33 +1021,6 @@ msgstr "Vous pouvez aussi modifier les tags dans la"
713msgid "tag list" 1021msgid "tag list"
714msgstr "liste des tags" 1022msgstr "liste des tags"
715 1023
716#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:143
717#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:312
718#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
719msgid "All"
720msgstr "Tous"
721
722#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:147
723#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:316
724msgid "Only common media hosts"
725msgstr "Seulement les hébergeurs de média connus"
726
727#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:151
728#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
729msgid "None"
730msgstr "Aucune"
731
732#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:158
733#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:297
734msgid "You need to enable the extension <code>php-gd</code> to use thumbnails."
735msgstr ""
736"Vous devez activer l'extension <code>php-gd</code> pour utiliser les "
737"miniatures."
738
739#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:162
740msgid "Synchonize thumbnails"
741msgstr "Synchroniser les miniatures"
742
743#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 1024#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
744msgid "title" 1025msgid "title"
745msgstr "titre" 1026msgstr "titre"
@@ -756,155 +1037,186 @@ msgstr "Valeur par défaut"
756msgid "Theme" 1037msgid "Theme"
757msgstr "Thème" 1038msgstr "Thème"
758 1039
759#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 1040#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
760#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 1041msgid "Description formatter"
1042msgstr "Format des descriptions"
1043
1044#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
1045#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
761msgid "Language" 1046msgid "Language"
762msgstr "Langue" 1047msgstr "Langue"
763 1048
764#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116 1049#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
765#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 1050#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
766msgid "Timezone" 1051msgid "Timezone"
767msgstr "Fuseau horaire" 1052msgstr "Fuseau horaire"
768 1053
769#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 1054#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
770#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 1055#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
771msgid "Continent" 1056msgid "Continent"
772msgstr "Continent" 1057msgstr "Continent"
773 1058
774#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 1059#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
775#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 1060#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
776msgid "City" 1061msgid "City"
777msgstr "Ville" 1062msgstr "Ville"
778 1063
779#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164 1064#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191
780msgid "Disable session cookie hijacking protection" 1065msgid "Disable session cookie hijacking protection"
781msgstr "Désactiver la protection contre le détournement de cookies" 1066msgstr "Désactiver la protection contre le détournement de cookies"
782 1067
783#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166 1068#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193
784msgid "Check this if you get disconnected or if your IP address changes often" 1069msgid "Check this if you get disconnected or if your IP address changes often"
785msgstr "" 1070msgstr ""
786"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP " 1071"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP "
787"change souvent" 1072"change souvent"
788 1073
789#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 1074#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210
790msgid "Private links by default" 1075msgid "Private links by default"
791msgstr "Liens privés par défaut" 1076msgstr "Liens privés par défaut"
792 1077
793#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184 1078#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211
794msgid "All new links are private by default" 1079msgid "All new links are private by default"
795msgstr "Tous les nouveaux liens sont privés par défaut" 1080msgstr "Tous les nouveaux liens sont privés par défaut"
796 1081
797#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 1082#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226
798msgid "RSS direct links" 1083msgid "RSS direct links"
799msgstr "Liens directs dans le flux RSS" 1084msgstr "Liens directs dans le flux RSS"
800 1085
801#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200 1086#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227
802msgid "Check this to use direct URL instead of permalink in feeds" 1087msgid "Check this to use direct URL instead of permalink in feeds"
803msgstr "" 1088msgstr ""
804"Cocher cette case pour utiliser des liens directs au lieu des permaliens " 1089"Cocher cette case pour utiliser des liens directs au lieu des permaliens "
805"dans le flux RSS" 1090"dans le flux RSS"
806 1091
807#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215 1092#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242
808msgid "Hide public links" 1093msgid "Hide public links"
809msgstr "Cacher les liens publics" 1094msgstr "Cacher les liens publics"
810 1095
811#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216 1096#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243
812msgid "Do not show any links if the user is not logged in" 1097msgid "Do not show any links if the user is not logged in"
813msgstr "N'afficher aucun lien sans être connecté" 1098msgstr "N'afficher aucun lien sans être connecté"
814 1099
815#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231 1100#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258
816#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 1101#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:149
817msgid "Check updates" 1102msgid "Check updates"
818msgstr "Vérifier les mises à jour" 1103msgstr "Vérifier les mises à jour"
819 1104
820#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232 1105#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259
821#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 1106#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
822msgid "Notify me when a new release is ready" 1107msgid "Notify me when a new release is ready"
823msgstr "Me notifier lorsqu'une nouvelle version est disponible" 1108msgstr "Me notifier lorsqu'une nouvelle version est disponible"
824 1109
825#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247 1110#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
826msgid "Automatically retrieve description for new bookmarks" 1111msgid "Automatically retrieve description for new bookmarks"
827msgstr "Récupérer automatiquement la description" 1112msgstr "Récupérer automatiquement la description"
828 1113
829#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248 1114#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275
830msgid "Shaarli will try to retrieve the description from meta HTML headers" 1115msgid "Shaarli will try to retrieve the description from meta HTML headers"
831msgstr "" 1116msgstr ""
832"Shaarli essaiera de récupérer la description depuis les balises HTML meta " 1117"Shaarli essaiera de récupérer la description depuis les balises HTML meta "
833"dans les entêtes" 1118"dans les entêtes"
834 1119
835#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263 1120#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290
836#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 1121#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
837msgid "Enable REST API" 1122msgid "Enable REST API"
838msgstr "Activer l'API REST" 1123msgstr "Activer l'API REST"
839 1124
840#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:264 1125#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:291
841#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 1126#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
842msgid "Allow third party software to use Shaarli such as mobile application" 1127msgid "Allow third party software to use Shaarli such as mobile application"
843msgstr "" 1128msgstr ""
844"Permet aux applications tierces d'utiliser Shaarli, par exemple les " 1129"Permet aux applications tierces d'utiliser Shaarli, par exemple les "
845"applications mobiles" 1130"applications mobiles"
846 1131
847#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:279 1132#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306
848msgid "API secret" 1133msgid "API secret"
849msgstr "Clé d'API secrète" 1134msgstr "Clé d'API secrète"
850 1135
851#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:293 1136#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
852msgid "Enable thumbnails" 1137msgid "Enable thumbnails"
853msgstr "Activer les miniatures" 1138msgstr "Activer les miniatures"
854 1139
855#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:301 1140#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324
856#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 1141msgid "You need to enable the extension <code>php-gd</code> to use thumbnails."
1142msgstr ""
1143"Vous devez activer l'extension <code>php-gd</code> pour utiliser les "
1144"miniatures."
1145
1146#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
1147#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
857msgid "Synchronize thumbnails" 1148msgid "Synchronize thumbnails"
858msgstr "Synchroniser les miniatures" 1149msgstr "Synchroniser les miniatures"
859 1150
860#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 1151#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
861#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 1152#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
1153#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1154msgid "All"
1155msgstr "Tous"
1156
1157#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
1158#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
1159msgid "Only common media hosts"
1160msgstr "Seulement les hébergeurs de média connus"
1161
1162#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
1163#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
1164msgid "None"
1165msgstr "Aucune"
1166
1167#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355
1168#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
862#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 1169#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
863#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 1170#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
864msgid "Save" 1171msgid "Save"
865msgstr "Enregistrer" 1172msgstr "Enregistrer"
866 1173
867#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 1174#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
868msgid "The Daily Shaarli" 1175msgid "1 RSS entry per :type"
869msgstr "Le Quotidien Shaarli" 1176msgid_plural ""
870 1177msgstr[0] "1 entrée RSS par :type"
871#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 1178msgstr[1] ""
872msgid "1 RSS entry per day" 1179
873msgstr "1 entrée RSS par jour" 1180#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
874 1181msgid "Previous :type"
875#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 1182msgid_plural ""
876msgid "Previous day" 1183msgstr[0] ":type précédent"
877msgstr "Jour précédent" 1184msgstr[1] "Jour précédent"
878 1185
879#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 1186#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
880msgid "All links of one day in a single page." 1187#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
881msgstr "Tous les liens d'un jour sur une page." 1188msgid "All links of one :type in a single page."
882 1189msgid_plural ""
883#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 1190msgstr[0] "Tous les liens d'un :type sur une page."
884msgid "Next day" 1191msgstr[1] "Tous les liens d'un jour sur une page."
885msgstr "Jour suivant" 1192
886 1193#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
887#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1194msgid "Next :type"
1195msgid_plural ""
1196msgstr[0] ":type suivant"
1197msgstr[1] ""
1198
1199#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
888msgid "Edit Shaare" 1200msgid "Edit Shaare"
889msgstr "Modifier le Shaare" 1201msgstr "Modifier le Shaare"
890 1202
891#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1203#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
892msgid "New Shaare" 1204msgid "New Shaare"
893msgstr "Nouveau Shaare" 1205msgstr "Nouveau Shaare"
894 1206
895#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 1207#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
896msgid "Created:" 1208msgid "Created:"
897msgstr "Création :" 1209msgstr "Création :"
898 1210
899#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 1211#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
900msgid "URL" 1212msgid "URL"
901msgstr "URL" 1213msgstr "URL"
902 1214
903#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 1215#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
904msgid "Title" 1216msgid "Title"
905msgstr "Titre" 1217msgstr "Titre"
906 1218
907#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 1219#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
908#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 1220#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
909#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 1221#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
910#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 1222#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@@ -912,37 +1224,58 @@ msgstr "Titre"
912msgid "Description" 1224msgid "Description"
913msgstr "Description" 1225msgstr "Description"
914 1226
915#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 1227#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
916msgid "Tags" 1228#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
917msgstr "Tags" 1229#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
1230msgid "Description will be rendered with"
1231msgstr "La description sera générée avec"
918 1232
919#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 1233#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
920#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1234msgid "Markdown syntax documentation"
921#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167 1235msgstr "Documentation sur la syntaxe Markdown"
922msgid "Private" 1236
923msgstr "Privé" 1237#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
1238msgid "Markdown syntax"
1239msgstr "la syntaxe Markdown"
1240
1241#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1242msgid "Cancel"
1243msgstr "Annuler"
924 1244
925#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 1245#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
926msgid "Apply Changes" 1246msgid "Apply Changes"
927msgstr "Appliquer les changements" 1247msgstr "Appliquer les changements"
928 1248
1249#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
1250#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1251msgid "Save all"
1252msgstr "Tout enregistrer"
1253
1254#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
1255#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
1256#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
1257#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
1258#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
1259msgid "Delete"
1260msgstr "Supprimer"
1261
929#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 1262#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
930msgid "Export Database" 1263msgid "Export Database"
931msgstr "Exporter les données" 1264msgstr "Exporter les données"
932 1265
933#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 1266#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
934msgid "Selection" 1267msgid "Selection"
935msgstr "Choisir" 1268msgstr "Choisir"
936 1269
937#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 1270#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
938msgid "Public" 1271msgid "Public"
939msgstr "Publics" 1272msgstr "Publics"
940 1273
941#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 1274#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
942msgid "Prepend note permalinks with this Shaarli instance's URL" 1275msgid "Prepend note permalinks with this Shaarli instance's URL"
943msgstr "Préfixer les liens de note avec l'URL de l'instance de Shaarli" 1276msgstr "Préfixer les liens de note avec l'URL de l'instance de Shaarli"
944 1277
945#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 1278#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
946msgid "Useful to import bookmarks in a web browser" 1279msgid "Useful to import bookmarks in a web browser"
947msgstr "Utile pour importer les marques-pages dans un navigateur" 1280msgstr "Utile pour importer les marques-pages dans un navigateur"
948 1281
@@ -983,42 +1316,42 @@ msgstr "Les doublons s'appuient sur les URL"
983msgid "Add default tags" 1316msgid "Add default tags"
984msgstr "Ajouter des tags par défaut" 1317msgstr "Ajouter des tags par défaut"
985 1318
986#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
987msgid "Install Shaarli"
988msgstr "Installation de Shaarli"
989
990#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 1319#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
991msgid "It looks like it's the first time you run Shaarli. Please configure it." 1320msgid "It looks like it's the first time you run Shaarli. Please configure it."
992msgstr "" 1321msgstr ""
993"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de " 1322"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de "
994"le configurer." 1323"le configurer."
995 1324
996#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 1325#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
997#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 1326#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
998#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 1327#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167
999#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:165 1328#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:167
1000msgid "Username" 1329msgid "Username"
1001msgstr "Nom d'utilisateur" 1330msgstr "Nom d'utilisateur"
1002 1331
1003#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 1332#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1004#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 1333#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
1005#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166 1334#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
1006#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:166 1335#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:168
1007msgid "Password" 1336msgid "Password"
1008msgstr "Mot de passe" 1337msgstr "Mot de passe"
1009 1338
1010#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 1339#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62
1011msgid "Shaarli title" 1340msgid "Shaarli title"
1012msgstr "Titre du Shaarli" 1341msgstr "Titre du Shaarli"
1013 1342
1014#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 1343#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
1015msgid "My links" 1344msgid "My links"
1016msgstr "Mes liens" 1345msgstr "Mes liens"
1017 1346
1018#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 1347#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
1019msgid "Install" 1348msgid "Install"
1020msgstr "Installer" 1349msgstr "Installer"
1021 1350
1351#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190
1352msgid "Server requirements"
1353msgstr "Pré-requis serveur"
1354
1022#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1355#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1023#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 1356#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
1024msgid "shaare" 1357msgid "shaare"
@@ -1034,21 +1367,31 @@ msgstr[0] "lien privé"
1034msgstr[1] "liens privés" 1367msgstr[1] "liens privés"
1035 1368
1036#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 1369#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
1037#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 1370#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1038#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:121 1371#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123
1039msgid "Search text" 1372msgid "Search text"
1040msgstr "Recherche texte" 1373msgstr "Recherche texte"
1041 1374
1042#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 1375#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
1043#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:128 1376#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
1044#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:128 1377#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:130
1045#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1378#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1046#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 1379#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
1047#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1380#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1048#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 1381#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1049msgid "Filter by tag" 1382msgid "Filter by tag"
1050msgstr "Filtrer par tag" 1383msgstr "Filtrer par tag"
1051 1384
1385#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1386#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
1387#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
1388#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:87
1389#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:139
1390#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1391#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
1392msgid "Search"
1393msgstr "Rechercher"
1394
1052#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 1395#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
1053msgid "Nothing found." 1396msgid "Nothing found."
1054msgstr "Aucun résultat." 1397msgstr "Aucun résultat."
@@ -1069,60 +1412,65 @@ msgid "tagged"
1069msgstr "taggé" 1412msgstr "taggé"
1070 1413
1071#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133 1414#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133
1415#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1072msgid "Remove tag" 1416msgid "Remove tag"
1073msgstr "Retirer le tag" 1417msgstr "Retirer le tag"
1074 1418
1075#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:142 1419#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
1076msgid "with status" 1420msgid "with status"
1077msgstr "avec le statut" 1421msgstr "avec le statut"
1078 1422
1079#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153 1423#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1080msgid "without any tag" 1424msgid "without any tag"
1081msgstr "sans tag" 1425msgstr "sans tag"
1082 1426
1083#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 1427#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
1084#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 1428#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
1085#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 1429#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
1086msgid "Fold" 1430msgid "Fold"
1087msgstr "Replier" 1431msgstr "Replier"
1088 1432
1089#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 1433#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
1090msgid "Edited: " 1434msgid "Edited: "
1091msgstr "Modifié : " 1435msgstr "Modifié : "
1092 1436
1093#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 1437#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
1094msgid "permalink" 1438msgid "permalink"
1095msgstr "permalien" 1439msgstr "permalien"
1096 1440
1097#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 1441#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
1098msgid "Add tag" 1442msgid "Add tag"
1099msgstr "Ajouter un tag" 1443msgstr "Ajouter un tag"
1100 1444
1101#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 1445#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185
1102msgid "Toggle sticky" 1446msgid "Toggle sticky"
1103msgstr "Changer statut épinglé" 1447msgstr "Changer statut épinglé"
1104 1448
1105#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185 1449#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187
1106msgid "Sticky" 1450msgid "Sticky"
1107msgstr "Épinglé" 1451msgstr "Épinglé"
1108 1452
1109#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 1453#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
1110#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7 1454msgid "Share a private link"
1455msgstr "Partager un lien privé"
1456
1457#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
1458#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5
1111msgid "Filters" 1459msgid "Filters"
1112msgstr "Filtres" 1460msgstr "Filtres"
1113 1461
1114#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 1462#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:10
1115#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12 1463#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:10
1116msgid "Only display private links" 1464msgid "Only display private links"
1117msgstr "Afficher uniquement les liens privés" 1465msgstr "Afficher uniquement les liens privés"
1118 1466
1119#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 1467#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
1120#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:15 1468#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:13
1121msgid "Only display public links" 1469msgid "Only display public links"
1122msgstr "Afficher uniquement les liens publics" 1470msgstr "Afficher uniquement les liens publics"
1123 1471
1124#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 1472#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
1125#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:20 1473#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
1126msgid "Filter untagged links" 1474msgid "Filter untagged links"
1127msgstr "Filtrer par liens privés" 1475msgstr "Filtrer par liens privés"
1128 1476
@@ -1131,30 +1479,23 @@ msgstr "Filtrer par liens privés"
1131msgid "Select all" 1479msgid "Select all"
1132msgstr "Tout sélectionner" 1480msgstr "Tout sélectionner"
1133 1481
1134#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 1482#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1135#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 1483#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89
1136#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:27 1484#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29
1137#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:79 1485#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89
1138#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 1486#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1139#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 1487#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
1140msgid "Fold all" 1488msgid "Fold all"
1141msgstr "Replier tout" 1489msgstr "Replier tout"
1142 1490
1143#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 1491#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1144#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:72 1492#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76
1145msgid "Links per page" 1493msgid "Links per page"
1146msgstr "Liens par page" 1494msgstr "Liens par page"
1147 1495
1148#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 1496#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
1149msgid "" 1497#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
1150"You have been banned after too many failed login attempts. Try again later." 1498#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171
1151msgstr ""
1152"Vous avez été banni après trop d'échecs d'authentification. Merci de "
1153"réessayer plus tard."
1154
1155#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1156#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1157#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
1158msgid "Remember me" 1499msgid "Remember me"
1159msgstr "Rester connecté" 1500msgstr "Rester connecté"
1160 1501
@@ -1182,65 +1523,67 @@ msgstr "Déplier tout"
1182 1523
1183#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 1524#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1184#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:47 1525#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:47
1185msgid "Are you sure you want to delete this link?" 1526msgid "Are you sure you want to delete this tag?"
1186msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" 1527msgstr "Êtes-vous sûr de vouloir supprimer ce tag ?"
1528
1529#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11
1530#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11
1531msgid "Menu"
1532msgstr "Menu"
1187 1533
1188#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 1534#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
1189#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:90 1535#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:38
1190#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:65 1536#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1191#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:90 1537msgid "Tag cloud"
1538msgstr "Nuage de tags"
1539
1540#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
1541#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
1542#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:67
1543#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:92
1192msgid "RSS Feed" 1544msgid "RSS Feed"
1193msgstr "Flux RSS" 1545msgstr "Flux RSS"
1194 1546
1195#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 1547#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
1196#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 1548#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
1197#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:70 1549#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:72
1198#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:106 1550#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:108
1199msgid "Logout" 1551msgid "Logout"
1200msgstr "Déconnexion" 1552msgstr "Déconnexion"
1201 1553
1202#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 1554#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
1203#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:150 1555#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152
1204msgid "Set public" 1556msgid "Set public"
1205msgstr "Rendre public" 1557msgstr "Rendre public"
1206 1558
1207#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 1559#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157
1208#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:155 1560#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157
1209msgid "Set private" 1561msgid "Set private"
1210msgstr "Rendre privé" 1562msgstr "Rendre privé"
1211 1563
1212#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187 1564#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
1213#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:187 1565#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189
1214msgid "is available" 1566msgid "is available"
1215msgstr "est disponible" 1567msgstr "est disponible"
1216 1568
1217#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:194 1569#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196
1218#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:194 1570#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196
1219msgid "Error" 1571msgid "Error"
1220msgstr "Erreur" 1572msgstr "Erreur"
1221 1573
1222#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1574#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1223msgid "Picture wall unavailable (thumbnails are disabled)." 1575msgid "There is no cached thumbnail."
1224msgstr "" 1576msgstr "Il n'y a aucune miniature dans le cache."
1225"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
1226 1577
1227#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 1578#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
1228#, fuzzy 1579msgid "Try to synchronize them."
1229#| msgid "" 1580msgstr "Essayer de les synchroniser."
1230#| "You don't have any cached thumbnail. Try to <a href=\"?do=thumbs_update"
1231#| "\">synchronize them</a>."
1232msgid ""
1233"There is no cached thumbnail. Try to <a href=\"?do=thumbs_update"
1234"\">synchronize them</a>."
1235msgstr ""
1236"Il n'y a aucune miniature en cache. Essayer de <a href=\"?do=thumbs_update"
1237"\">les synchroniser</a>."
1238 1581
1239#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1582#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1240msgid "Picture Wall" 1583msgid "Picture Wall"
1241msgstr "Mur d'images" 1584msgstr "Mur d'images"
1242 1585
1243#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1586#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1244msgid "pics" 1587msgid "pics"
1245msgstr "images" 1588msgstr "images"
1246 1589
@@ -1249,6 +1592,11 @@ msgid "You need to enable Javascript to change plugin loading order."
1249msgstr "" 1592msgstr ""
1250"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions." 1593"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions."
1251 1594
1595#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
1596#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
1597msgid "Plugin administration"
1598msgstr "Administration des plugins"
1599
1252#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 1600#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1253msgid "Enabled Plugins" 1601msgid "Enabled Plugins"
1254msgstr "Extensions activées" 1602msgstr "Extensions activées"
@@ -1304,6 +1652,100 @@ msgstr "Configuration des extensions"
1304msgid "No parameter available." 1652msgid "No parameter available."
1305msgstr "Aucun paramètre disponible." 1653msgstr "Aucun paramètre disponible."
1306 1654
1655#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1656msgid "General"
1657msgstr "Général"
1658
1659#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
1660msgid "Index URL"
1661msgstr "URL de l'index"
1662
1663#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1664msgid "Base path"
1665msgstr "Chemin de base"
1666
1667#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1668msgid "Client IP"
1669msgstr "IP du client"
1670
1671#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1672msgid "Trusted reverse proxies"
1673msgstr "Reverse proxies de confiance"
1674
1675#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
1676msgid "N/A"
1677msgstr "N/A"
1678
1679#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
1680msgid "Visit releases page on Github"
1681msgstr "Visiter la page des releases sur Github"
1682
1683#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
1684msgid "Synchronize all link thumbnails"
1685msgstr "Synchroniser toutes les miniatures"
1686
1687#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2
1688#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2
1689msgid "Permissions"
1690msgstr "Permissions"
1691
1692#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8
1693#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8
1694msgid "There are permissions that need to be fixed."
1695msgstr "Il y a des permissions qui doivent être corrigées."
1696
1697#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1698#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23
1699msgid "All read/write permissions are properly set."
1700msgstr "Toutes les permissions de lecture/écriture sont définies correctement."
1701
1702#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
1703#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32
1704msgid "Running PHP"
1705msgstr "Fonctionnant avec PHP"
1706
1707#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1708#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36
1709msgid "End of life: "
1710msgstr "Fin de vie : "
1711
1712#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1713#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48
1714msgid "Extension"
1715msgstr "Extension"
1716
1717#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
1718#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49
1719msgid "Usage"
1720msgstr "Utilisation"
1721
1722#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
1723#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50
1724msgid "Status"
1725msgstr "Statut"
1726
1727#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
1728#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1729#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51
1730#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66
1731msgid "Loaded"
1732msgstr "Chargé"
1733
1734#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
1735#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
1736msgid "Required"
1737msgstr "Obligatoire"
1738
1739#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
1740#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
1741msgid "Optional"
1742msgstr "Optionnel"
1743
1744#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
1745#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70
1746msgid "Not loaded"
1747msgstr "Non chargé"
1748
1307#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 1749#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1308#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 1750#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1309msgid "tags" 1751msgid "tags"
@@ -1314,6 +1756,10 @@ msgstr "tags"
1314msgid "List all links with those tags" 1756msgid "List all links with those tags"
1315msgstr "Lister tous les liens avec ces tags" 1757msgstr "Lister tous les liens avec ces tags"
1316 1758
1759#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1760msgid "Tag list"
1761msgstr "Liste des tags"
1762
1317#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 1763#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
1318#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 1764#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
1319msgid "Sort by:" 1765msgid "Sort by:"
@@ -1350,51 +1796,43 @@ msgstr "Configurer Shaarli"
1350msgid "Enable, disable and configure plugins" 1796msgid "Enable, disable and configure plugins"
1351msgstr "Activer, désactiver et configurer les extensions" 1797msgstr "Activer, désactiver et configurer les extensions"
1352 1798
1353#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 1799#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27
1800msgid "Check instance's server configuration"
1801msgstr "Vérifier la configuration serveur de l'instance"
1802
1803#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
1354msgid "Change your password" 1804msgid "Change your password"
1355msgstr "Modifier le mot de passe" 1805msgstr "Modifier le mot de passe"
1356 1806
1357#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 1807#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1358msgid "Rename or delete a tag in all links" 1808msgid "Rename or delete a tag in all links"
1359msgstr "Renommer ou supprimer un tag dans tous les liens" 1809msgstr "Renommer ou supprimer un tag dans tous les liens"
1360 1810
1361#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 1811#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1362#, fuzzy
1363#| msgid ""
1364#| "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1365#| "delicious…)"
1366msgid "" 1812msgid ""
1367"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " 1813"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1368"delicious...)" 1814"delicious...)"
1369msgstr "" 1815msgstr ""
1370"Importer des marques pages au format Netscape HTML (comme exportés depuis " 1816"Importer des marques pages au format Netscape HTML (comme exportés depuis "
1371"Firefox, Chrome, Opera, delicious)" 1817"Firefox, Chrome, Opera, delicious...)"
1372 1818
1373#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 1819#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1374msgid "Import links" 1820msgid "Import links"
1375msgstr "Importer des liens" 1821msgstr "Importer des liens"
1376 1822
1377#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 1823#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
1378#, fuzzy
1379#| msgid ""
1380#| "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1381#| "Opera, delicious…)"
1382msgid "" 1824msgid ""
1383"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " 1825"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1384"Opera, delicious...)" 1826"Opera, delicious...)"
1385msgstr "" 1827msgstr ""
1386"Exporter les marques pages au format Netscape HTML (comme exportés depuis " 1828"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
1387"Firefox, Chrome, Opera, delicious)" 1829"Firefox, Chrome, Opera, delicious...)"
1388 1830
1389#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 1831#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54
1390msgid "Export database" 1832msgid "Export database"
1391msgstr "Exporter les données" 1833msgstr "Exporter les données"
1392 1834
1393#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55 1835#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
1394msgid "Synchronize all link thumbnails"
1395msgstr "Synchroniser toutes les miniatures"
1396
1397#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
1398msgid "" 1836msgid ""
1399"Drag one of these button to your bookmarks toolbar or right-click it and " 1837"Drag one of these button to your bookmarks toolbar or right-click it and "
1400"\"Bookmark This Link\"" 1838"\"Bookmark This Link\""
@@ -1402,13 +1840,13 @@ msgstr ""
1402"Glisser un de ces boutons dans votre barre de favoris ou cliquer droit " 1840"Glisser un de ces boutons dans votre barre de favoris ou cliquer droit "
1403"dessus et « Ajouter aux favoris »" 1841"dessus et « Ajouter aux favoris »"
1404 1842
1405#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 1843#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
1406msgid "then click on the bookmarklet in any page you want to share." 1844msgid "then click on the bookmarklet in any page you want to share."
1407msgstr "" 1845msgstr ""
1408"puis cliquer sur le marque-page depuis un site que vous souhaitez partager." 1846"puis cliquer sur le marque-page depuis un site que vous souhaitez partager."
1409 1847
1410#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 1848#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
1411#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 1849#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
1412msgid "" 1850msgid ""
1413"Drag this link to your bookmarks toolbar or right-click it and Bookmark This " 1851"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
1414"Link" 1852"Link"
@@ -1416,40 +1854,40 @@ msgstr ""
1416"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " 1854"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1417"Ajouter aux favoris »" 1855"Ajouter aux favoris »"
1418 1856
1419#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 1857#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
1420msgid "then click ✚Shaare link button in any page you want to share" 1858msgid "then click ✚Shaare link button in any page you want to share"
1421msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" 1859msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
1422 1860
1423#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 1861#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
1424#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 1862#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
1425msgid "The selected text is too long, it will be truncated." 1863msgid "The selected text is too long, it will be truncated."
1426msgstr "Le texte sélectionné est trop long, il sera tronqué." 1864msgstr "Le texte sélectionné est trop long, il sera tronqué."
1427 1865
1428#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 1866#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1429msgid "Shaare link" 1867msgid "Shaare link"
1430msgstr "Shaare" 1868msgstr "Shaare"
1431 1869
1432#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 1870#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
1433msgid "" 1871msgid ""
1434"Then click ✚Add Note button anytime to start composing a private Note (text " 1872"Then click ✚Add Note button anytime to start composing a private Note (text "
1435"post) to your Shaarli" 1873"post) to your Shaarli"
1436msgstr "" 1874msgstr ""
1437"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" 1875"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
1438 1876
1439#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127 1877#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1440msgid "Add Note" 1878msgid "Add Note"
1441msgstr "Ajouter une Note" 1879msgstr "Ajouter une Note"
1442 1880
1443#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 1881#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
1444msgid "3rd party" 1882msgid "3rd party"
1445msgstr "Applications tierces" 1883msgstr "Applications tierces"
1446 1884
1447#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 1885#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135
1448#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 1886#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140
1449msgid "plugin" 1887msgid "plugin"
1450msgstr "extension" 1888msgstr "extension"
1451 1889
1452#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 1890#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
1453msgid "" 1891msgid ""
1454"Drag this link to your bookmarks toolbar, or right-click it and choose " 1892"Drag this link to your bookmarks toolbar, or right-click it and choose "
1455"Bookmark This Link" 1893"Bookmark This Link"
@@ -1457,6 +1895,74 @@ msgstr ""
1457"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " 1895"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1458"Ajouter aux favoris »" 1896"Ajouter aux favoris »"
1459 1897
1898#~ msgid "Display:"
1899#~ msgstr "Afficher :"
1900
1901#~ msgid "The Daily Shaarli"
1902#~ msgstr "Le Quotidien Shaarli"
1903
1904#, fuzzy
1905#~| msgid "Selection"
1906#~ msgid ".ui-selecting"
1907#~ msgstr "Choisir"
1908
1909#, fuzzy
1910#~| msgid "Documentation"
1911#~ msgid "document"
1912#~ msgstr "Documentation"
1913
1914#~ msgid "The page you are trying to reach does not exist or has been deleted."
1915#~ msgstr ""
1916#~ "La page que vous essayez de consulter n'existe pas ou a été supprimée."
1917
1918#~ msgid "404 Not Found"
1919#~ msgstr "404 Introuvable"
1920
1921#~ msgid "Updates file path is not set, can't write updates."
1922#~ msgstr ""
1923#~ "Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
1924#~ "d'écrire les mises à jour."
1925
1926#~ msgid "Unable to write updates in "
1927#~ msgstr "Impossible d'écrire les mises à jour dans "
1928
1929#~ msgid "I said: NO. You are banned for the moment. Go away."
1930#~ msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
1931
1932#~ msgid "Click to try again."
1933#~ msgstr "Cliquer ici pour réessayer."
1934
1935#~ msgid ""
1936#~ "Render shaare description with Markdown syntax.<br><strong>Warning</"
1937#~ "strong>:\n"
1938#~ "If your shaared descriptions contained HTML tags before enabling the "
1939#~ "markdown plugin,\n"
1940#~ "enabling it might break your page.\n"
1941#~ "See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
1942#~ "markdown#html-rendering\">README</a>."
1943#~ msgstr ""
1944#~ "Utilise la syntaxe Markdown pour la description des liens."
1945#~ "<br><strong>Attention</strong> :\n"
1946#~ "Si vous aviez des descriptions contenant du HTML avant d'activer cette "
1947#~ "extension,\n"
1948#~ "l'activer pourrait déformer vos pages.\n"
1949#~ "Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
1950#~ "markdown#html-rendering\">README</a>."
1951
1952#~ msgid "Synchonize thumbnails"
1953#~ msgstr "Synchroniser les miniatures"
1954
1955#, fuzzy
1956#~| msgid ""
1957#~| "You don't have any cached thumbnail. Try to <a href=\"?do=thumbs_update"
1958#~| "\">synchronize them</a>."
1959#~ msgid ""
1960#~ "There is no cached thumbnail. Try to <a href=\"?do=thumbs_update"
1961#~ "\">synchronize them</a>."
1962#~ msgstr ""
1963#~ "Il n'y a aucune miniature en cache. Essayer de <a href=\"?do=thumbs_update"
1964#~ "\">les synchroniser</a>."
1965
1460#~ msgid "" 1966#~ msgid ""
1461#~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this " 1967#~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1462#~ "functionality." 1968#~ "functionality."
diff --git a/inc/languages/ja/LC_MESSAGES/shaarli.po b/inc/languages/ja/LC_MESSAGES/shaarli.po
deleted file mode 100644
index b420bb51..00000000
--- a/inc/languages/ja/LC_MESSAGES/shaarli.po
+++ /dev/null
@@ -1,1293 +0,0 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Shaarli\n"
4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2020-02-11 09:31+0900\n"
6"PO-Revision-Date: 2020-02-11 10:54+0900\n"
7"Last-Translator: yude <yudesleepy@gmail.com>\n"
8"Language-Team: Shaarli\n"
9"Language: ja\n"
10"MIME-Version: 1.0\n"
11"Content-Type: text/plain; charset=UTF-8\n"
12"Content-Transfer-Encoding: 8bit\n"
13"X-Generator: Poedit 2.3\n"
14"X-Poedit-Basepath: ../../../..\n"
15"Plural-Forms: nplurals=2; plural=(n != 1);\n"
16"X-Poedit-SourceCharset: UTF-8\n"
17"X-Poedit-KeywordsList: t:1,2;t\n"
18"X-Poedit-SearchPath-0: .\n"
19"X-Poedit-SearchPathExcluded-0: node_modules\n"
20"X-Poedit-SearchPathExcluded-1: vendor\n"
21
22#: application/ApplicationUtils.php:153
23#, php-format
24msgid ""
25"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
26"cannot run. Your PHP version has known security vulnerabilities and should "
27"be updated as soon as possible."
28msgstr ""
29"使用している PHP のバージョンが古すぎます! Shaarli の実行には最低でも PHP %s "
30"が必要です。 現在使用している PHP のバージョンには脆弱性があり、できるだけ速"
31"やかにアップデートするべきです。"
32
33#: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195
34msgid "directory is not readable"
35msgstr "ディレクトリを読み込めません"
36
37#: application/ApplicationUtils.php:198
38msgid "directory is not writable"
39msgstr "ディレクトリに書き込めません"
40
41#: application/ApplicationUtils.php:216
42msgid "file is not readable"
43msgstr "ファイルを読み取る権限がありません"
44
45#: application/ApplicationUtils.php:219
46msgid "file is not writable"
47msgstr "ファイルを書き込む権限がありません"
48
49#: application/Cache.php:16
50#, php-format
51msgid "Cannot purge %s: no directory"
52msgstr "%s を削除できません: ディレクトリが存在しません"
53
54#: application/FeedBuilder.php:151
55msgid "Direct link"
56msgstr "ダイレクトリンク"
57
58#: application/FeedBuilder.php:153
59#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
60#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178
61msgid "Permalink"
62msgstr "パーマリンク"
63
64#: application/History.php:174
65msgid "History file isn't readable or writable"
66msgstr "履歴ファイルを読み込む、または書き込むための権限がありません"
67
68#: application/History.php:185
69msgid "Could not parse history file"
70msgstr "履歴ファイルを正常に復元できませんでした"
71
72#: application/Languages.php:177
73msgid "Automatic"
74msgstr "自動"
75
76#: application/Languages.php:178
77msgid "English"
78msgstr "英語"
79
80#: application/Languages.php:179
81msgid "French"
82msgstr "フランス語"
83
84#: application/Languages.php:180
85msgid "German"
86msgstr "ドイツ語"
87
88#: application/LinkDB.php:136
89msgid "You are not authorized to add a link."
90msgstr "リンクを追加するには、ログインする必要があります。"
91
92#: application/LinkDB.php:139
93msgid "Internal Error: A link should always have an id and URL."
94msgstr "エラー: リンクにはIDとURLを登録しなければなりません。"
95
96#: application/LinkDB.php:142
97msgid "You must specify an integer as a key."
98msgstr "正常なキーの値ではありません。"
99
100#: application/LinkDB.php:145
101msgid "Array offset and link ID must be equal."
102msgstr "Array オフセットとリンクのIDは同じでなければなりません。"
103
104#: application/LinkDB.php:251
105#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
106#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
107#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
108#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
109msgid ""
110"The personal, minimalist, super-fast, database free, bookmarking service"
111msgstr ""
112"個人向けの、ミニマムで高速でかつデータベースのいらないブックマークサービス"
113
114#: application/LinkDB.php:253
115msgid ""
116"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
117"me, you must first login.\n"
118"\n"
119"To learn how to use Shaarli, consult the link \"Documentation\" at the "
120"bottom of this page.\n"
121"\n"
122"You use the community supported version of the original Shaarli project, by "
123"Sebastien Sauvage."
124msgstr ""
125"Shaarli へようこそ! これはあなたの最初の公開ブックマークです。これを編集した"
126"り削除したりするには、ログインする必要があります。\n"
127"\n"
128"Shaarli の使い方を知るには、このページの下にある「ドキュメント」のリンクを開"
129"いてください。\n"
130"\n"
131"あなたは Sebastien Sauvage による、コミュニティーサポートのあるバージョンのオ"
132"リジナルのShaarli プロジェクトを使用しています。"
133
134#: application/LinkDB.php:267
135msgid "My secret stuff... - Pastebin.com"
136msgstr "わたしのひ💗み💗つ💗 - Pastebin.com"
137
138#: application/LinkDB.php:269
139msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
140msgstr ""
141"シーッ! これはあなたしか見られないプライベートリンクです。消すこともできま"
142"す。"
143
144#: application/LinkFilter.php:452
145msgid "The link you are trying to reach does not exist or has been deleted."
146msgstr "開こうとしたリンクは存在しないか、削除されています。"
147
148#: application/NetscapeBookmarkUtils.php:35
149msgid "Invalid export selection:"
150msgstr "不正なエクスポートの選択:"
151
152#: application/NetscapeBookmarkUtils.php:81
153#, php-format
154msgid "File %s (%d bytes) "
155msgstr "ファイル %s (%d バイト) "
156
157#: application/NetscapeBookmarkUtils.php:83
158msgid "has an unknown file format. Nothing was imported."
159msgstr "は不明なファイル形式です。インポートは中止されました。"
160
161#: application/NetscapeBookmarkUtils.php:86
162#, php-format
163msgid ""
164"was successfully processed in %d seconds: %d links imported, %d links "
165"overwritten, %d links skipped."
166msgstr ""
167"が %d 秒で処理され、%d 件のリンクがインポートされ、%d 件のリンクが上書きさ"
168"れ、%d 件のリンクがスキップされました。"
169
170#: application/PageBuilder.php:168
171msgid "The page you are trying to reach does not exist or has been deleted."
172msgstr "あなたが開こうとしたページは存在しないか、削除されています。"
173
174#: application/PageBuilder.php:170
175msgid "404 Not Found"
176msgstr "404 ページが存在しません"
177
178#: application/PluginManager.php:243
179#, php-format
180msgid "Plugin \"%s\" files not found."
181msgstr "プラグイン「%s」のファイルが存在しません。"
182
183#: application/Updater.php:76
184msgid "Couldn't retrieve Updater class methods."
185msgstr "アップデーターのクラスメゾットを受信できませんでした。"
186
187#: application/Updater.php:532
188msgid "An error occurred while running the update "
189msgstr "更新中に問題が発生しました "
190
191#: application/Updater.php:572
192msgid "Updates file path is not set, can't write updates."
193msgstr "更新するファイルのパスが指定されていないため、更新を書き込めません。"
194
195#: application/Updater.php:577
196msgid "Unable to write updates in "
197msgstr "更新を次の項目に書き込めませんでした: "
198
199#: application/Utils.php:376 tests/UtilsTest.php:340
200msgid "Setting not set"
201msgstr "未設定"
202
203#: application/Utils.php:383 tests/UtilsTest.php:338 tests/UtilsTest.php:339
204msgid "Unlimited"
205msgstr "無制限"
206
207#: application/Utils.php:386 tests/UtilsTest.php:335 tests/UtilsTest.php:336
208#: tests/UtilsTest.php:350
209msgid "B"
210msgstr "B"
211
212#: application/Utils.php:386 tests/UtilsTest.php:329 tests/UtilsTest.php:330
213#: tests/UtilsTest.php:337
214msgid "kiB"
215msgstr "kiB"
216
217#: application/Utils.php:386 tests/UtilsTest.php:331 tests/UtilsTest.php:332
218#: tests/UtilsTest.php:348 tests/UtilsTest.php:349
219msgid "MiB"
220msgstr "MiB"
221
222#: application/Utils.php:386 tests/UtilsTest.php:333 tests/UtilsTest.php:334
223msgid "GiB"
224msgstr "GiB"
225
226#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121
227msgid ""
228"Shaarli could not create the config file. Please make sure Shaarli has the "
229"right to write in the folder is it installed in."
230msgstr ""
231"Shaarli は設定ファイルを作成できませんでした。Shaarli が正しい権限下に置かれ"
232"ていて、インストールされているディレクトリに書き込みできることを確認してくだ"
233"さい。"
234
235#: application/config/ConfigManager.php:135
236msgid "Invalid setting key parameter. String expected, got: "
237msgstr ""
238"不正なキーの値です。文字列が想定されていますが、次のように入力されました: "
239
240#: application/config/exception/MissingFieldConfigException.php:21
241#, php-format
242msgid "Configuration value is required for %s"
243msgstr "%s には設定が必要です"
244
245#: application/config/exception/PluginConfigOrderException.php:15
246msgid "An error occurred while trying to save plugins loading order."
247msgstr "プラグインの読込順を変更する際にエラーが発生しました。"
248
249#: application/config/exception/UnauthorizedConfigException.php:16
250msgid "You are not authorized to alter config."
251msgstr "設定を変更する権限がありません。"
252
253#: application/exceptions/IOException.php:19
254msgid "Error accessing"
255msgstr "読込中にエラーが発生しました"
256
257#: index.php:142
258msgid "Shared links on "
259msgstr "次において共有されたリンク:"
260
261#: index.php:164
262msgid "Insufficient permissions:"
263msgstr "権限がありません:"
264
265#: index.php:303
266msgid "I said: NO. You are banned for the moment. Go away."
267msgstr "あなたはこのサーバーからBANされています。"
268
269#: index.php:368
270msgid "Wrong login/password."
271msgstr "不正なユーザー名、またはパスワードです。"
272
273#: index.php:576 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
274#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42
275msgid "Daily"
276msgstr "デイリー"
277
278#: index.php:681 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
279#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
280#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
281#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95
282#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71
283#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95
284msgid "Login"
285msgstr "ログイン"
286
287#: index.php:722 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
288#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39
289msgid "Picture wall"
290msgstr "ピクチャウォール"
291
292#: index.php:770 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
293#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36
294#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
295msgid "Tag cloud"
296msgstr "タグクラウド"
297
298#: index.php:803 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
299msgid "Tag list"
300msgstr "タグ一覧"
301
302#: index.php:1028 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
303#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
304msgid "Tools"
305msgstr "ツール"
306
307#: index.php:1037
308msgid "You are not supposed to change a password on an Open Shaarli."
309msgstr ""
310"公開されている Shaarli において、パスワードを変更することは想定されていませ"
311"ん。"
312
313#: index.php:1042 index.php:1084 index.php:1160 index.php:1191 index.php:1291
314msgid "Wrong token."
315msgstr "不正なトークンです。"
316
317#: index.php:1047
318msgid "The old password is not correct."
319msgstr "元のパスワードが正しくありません。"
320
321#: index.php:1067
322msgid "Your password has been changed"
323msgstr "あなたのパスワードは変更されました"
324
325#: index.php:1072
326#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
327#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
328msgid "Change password"
329msgstr "パスワードを変更"
330
331#: index.php:1120
332msgid "Configuration was saved."
333msgstr "設定は保存されました。"
334
335#: index.php:1143 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
336msgid "Configure"
337msgstr "設定"
338
339#: index.php:1154 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
340#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
341msgid "Manage tags"
342msgstr "タグを設定"
343
344#: index.php:1172
345#, php-format
346msgid "The tag was removed from %d link."
347msgid_plural "The tag was removed from %d links."
348msgstr[0] "%d 件のリンクからタグが削除されました。"
349msgstr[1] "The tag was removed from %d links."
350
351#: index.php:1173
352#, php-format
353msgid "The tag was renamed in %d link."
354msgid_plural "The tag was renamed in %d links."
355msgstr[0] "タグが %d 件のリンクにおいて、名前が変更されました。"
356msgstr[1] "タグが %d 件のリンクにおいて、名前が変更されました。"
357
358#: index.php:1181 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
359msgid "Shaare a new link"
360msgstr "新しいリンクを追加"
361
362#: index.php:1351 tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
363#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
364msgid "Edit"
365msgstr "共有"
366
367#: index.php:1351 index.php:1421
368#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
369#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
370#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
371msgid "Shaare"
372msgstr "Shaare"
373
374#: index.php:1390
375msgid "Note: "
376msgstr "注: "
377
378#: index.php:1430 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
379msgid "Export"
380msgstr "エクスポート"
381
382#: index.php:1492 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
383msgid "Import"
384msgstr "インポート"
385
386#: index.php:1502
387#, php-format
388msgid ""
389"The file you are trying to upload is probably bigger than what this "
390"webserver can accept (%s). Please upload in smaller chunks."
391msgstr ""
392"あなたがアップロードしようとしているファイルは、サーバーが許可しているファイ"
393"ルサイズ (%s) よりも大きいです。もう少し小さいものをアップロードしてくださ"
394"い。"
395
396#: index.php:1541 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
397#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
398msgid "Plugin administration"
399msgstr "プラグイン管理"
400
401#: index.php:1706
402msgid "Search: "
403msgstr "検索: "
404
405#: index.php:1933
406#, php-format
407msgid ""
408"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
409"variable \"session.save_path\" is set correctly in your PHP config, and that "
410"you have write access to it.<br>It currently points to %s.<br>On some "
411"browsers, accessing your server via a hostname like 'localhost' or any "
412"custom hostname without a dot causes cookie storage to fail. We recommend "
413"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
414msgstr ""
415"<pre>セッションが正常にあなたのサーバー上で稼働していないようです。<br>PHP の"
416"設定ファイル内にて、正しく \"session.save_path\" の値が設定されていることと、"
417"権限が間違っていないことを確認してください。<br>現在 %s からPHPの設定ファイル"
418"を読み込んでいます。<br>一部のブラウザーにおいて、localhost や他のドットを含"
419"まないホスト名にてサーバーにアクセスする際に、クッキーを保存できないことがあ"
420"ります。IP アドレスや完全なドメイン名でサーバーにアクセスすることをおすすめし"
421"ます。<br>"
422
423#: index.php:1943
424msgid "Click to try again."
425msgstr "クリックして再度試します。"
426
427#: plugins/addlink_toolbar/addlink_toolbar.php:29
428msgid "URI"
429msgstr "URI"
430
431#: plugins/addlink_toolbar/addlink_toolbar.php:33
432#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
433msgid "Add link"
434msgstr "リンクを追加"
435
436#: plugins/addlink_toolbar/addlink_toolbar.php:50
437msgid "Adds the addlink input on the linklist page."
438msgstr "リンク一覧のページに、リンクを追加するためのフォームを表示する。"
439
440#: plugins/archiveorg/archiveorg.php:23
441msgid "View on archive.org"
442msgstr "archive.org 上で表示する"
443
444#: plugins/archiveorg/archiveorg.php:36
445msgid "For each link, add an Archive.org icon."
446msgstr "それぞれのリンクに、Archive.org のアイコンを追加する。"
447
448#: plugins/demo_plugin/demo_plugin.php:465
449msgid ""
450"A demo plugin covering all use cases for template designers and plugin "
451"developers."
452msgstr ""
453"テンプレートのデザイナーや、プラグインの開発者のためのすべての状況に対応でき"
454"るデモプラグインです。"
455
456#: plugins/isso/isso.php:20
457msgid ""
458"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
459"administration page."
460msgstr ""
461"Isso プラグインエラー: \"ISSO_SERVER\" の値をプラグイン管理ページにて指定して"
462"ください。"
463
464#: plugins/isso/isso.php:63
465msgid "Let visitor comment your shaares on permalinks with Isso."
466msgstr ""
467"Isso を使って、あなたのパーマリンク上のリンクに第三者がコメントを残すことがで"
468"きます。"
469
470#: plugins/isso/isso.php:64
471msgid "Isso server URL (without 'http://')"
472msgstr "Isso server URL ('http://' 抜き)"
473
474#: plugins/markdown/markdown.php:158
475msgid "Description will be rendered with"
476msgstr "説明は次の方法で描画されます:"
477
478#: plugins/markdown/markdown.php:159
479msgid "Markdown syntax documentation"
480msgstr "マークダウン形式のドキュメント"
481
482#: plugins/markdown/markdown.php:160
483msgid "Markdown syntax"
484msgstr "マークダウン形式"
485
486#: plugins/markdown/markdown.php:339
487msgid ""
488"Render shaare description with Markdown syntax.<br><strong>Warning</"
489"strong>:\n"
490"If your shaared descriptions contained HTML tags before enabling the "
491"markdown plugin,\n"
492"enabling it might break your page.\n"
493"See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
494"markdown#html-rendering\">README</a>."
495msgstr ""
496"リンクの説明をマークダウン形式で表示します。<br><strong>警告</strong>:\n"
497"リンクの説明にHTMLタグがこのプラグインを有効にする前に含まれていた場合、\n"
498"正常にページを表示できなくなるかもしれません。\n"
499"詳しくは <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
500"markdown#html-rendering\">README</a> をご覧ください。"
501
502#: plugins/piwik/piwik.php:21
503msgid ""
504"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
505"administration page."
506msgstr ""
507"Piwik プラグインエラー: PIWIK_URL と PIWIK_SITEID の値をプラグイン管理ページ"
508"で指定してください。"
509
510#: plugins/piwik/piwik.php:70
511msgid "A plugin that adds Piwik tracking code to Shaarli pages."
512msgstr "Piwik のトラッキングコードをShaarliに追加するプラグインです。"
513
514#: plugins/piwik/piwik.php:71
515msgid "Piwik URL"
516msgstr "Piwik URL"
517
518#: plugins/piwik/piwik.php:72
519msgid "Piwik site ID"
520msgstr "Piwik サイトID"
521
522#: plugins/playvideos/playvideos.php:22
523msgid "Video player"
524msgstr "動画プレイヤー"
525
526#: plugins/playvideos/playvideos.php:25
527msgid "Play Videos"
528msgstr "動画を再生"
529
530#: plugins/playvideos/playvideos.php:56
531msgid "Add a button in the toolbar allowing to watch all videos."
532msgstr "すべての動画を閲覧するボタンをツールバーに追加します。"
533
534#: plugins/playvideos/youtube_playlist.js:214
535msgid "plugins/playvideos/jquery-1.11.2.min.js"
536msgstr "plugins/playvideos/jquery-1.11.2.min.js"
537
538#: plugins/pubsubhubbub/pubsubhubbub.php:69
539#, php-format
540msgid "Could not publish to PubSubHubbub: %s"
541msgstr "PubSubHubbub に登録できませんでした: %s"
542
543#: plugins/pubsubhubbub/pubsubhubbub.php:95
544#, php-format
545msgid "Could not post to %s"
546msgstr "%s に登録できませんでした"
547
548#: plugins/pubsubhubbub/pubsubhubbub.php:99
549#, php-format
550msgid "Bad response from the hub %s"
551msgstr "ハブ %s からの不正なレスポンス"
552
553#: plugins/pubsubhubbub/pubsubhubbub.php:110
554msgid "Enable PubSubHubbub feed publishing."
555msgstr "PubSubHubbub へのフィードを公開する。"
556
557#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68
558msgid "For each link, add a QRCode icon."
559msgstr "それぞれのリンクについて、QRコードのアイコンを追加する。"
560
561#: plugins/wallabag/wallabag.php:21
562msgid ""
563"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
564"plugin administration page."
565msgstr ""
566"Wallabag プラグインエラー: \"WALLABAG_URL\" の値をプラグイン管理ページにおい"
567"て指定してください。"
568
569#: plugins/wallabag/wallabag.php:47
570msgid "Save to wallabag"
571msgstr "Wallabag に保存"
572
573#: plugins/wallabag/wallabag.php:69
574msgid "Wallabag API URL"
575msgstr "Wallabag のAPIのURL"
576
577#: plugins/wallabag/wallabag.php:70
578msgid "Wallabag API version (1 or 2)"
579msgstr "Wallabag のAPIのバージョン (1 または 2)"
580
581#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
582#: tests/languages/fr/LanguagesFrTest.php:160
583#: tests/languages/fr/LanguagesFrTest.php:173
584#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
585#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81
586msgid "Search"
587msgid_plural "Search"
588msgstr[0] "検索"
589msgstr[1] "検索"
590
591#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
592msgid "Sorry, nothing to see here."
593msgstr "すみませんが、ここには何もありません。"
594
595#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
596msgid "URL or leave empty to post a note"
597msgstr "URL を入力するか、空欄にするとノートを投稿します"
598
599#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
600msgid "Current password"
601msgstr "現在のパスワード"
602
603#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
604msgid "New password"
605msgstr "新しいパスワード"
606
607#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
608msgid "Change"
609msgstr "変更"
610
611#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
612#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
613msgid "Tag"
614msgstr "タグ"
615
616#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
617msgid "New name"
618msgstr "変更先の名前"
619
620#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
621msgid "Case sensitive"
622msgstr "大文字と小文字を区別"
623
624#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
625msgid "Rename"
626msgstr "名前を変更"
627
628#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
629#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
630#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172
631msgid "Delete"
632msgstr "削除"
633
634#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
635msgid "You can also edit tags in the"
636msgstr "次に含まれるタグを編集することもできます:"
637
638#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
639msgid "tag list"
640msgstr "タグ一覧"
641
642#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
643msgid "title"
644msgstr "タイトル"
645
646#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
647msgid "Home link"
648msgstr "ホームのリンク先"
649
650#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
651msgid "Default value"
652msgstr "既定の値"
653
654#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
655msgid "Theme"
656msgstr "テーマ"
657
658#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
659#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
660msgid "Language"
661msgstr "言語"
662
663#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116
664#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
665msgid "Timezone"
666msgstr "タイムゾーン"
667
668#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
669#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
670msgid "Continent"
671msgstr "大陸"
672
673#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
674#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
675msgid "City"
676msgstr "町"
677
678#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
679msgid "Disable session cookie hijacking protection"
680msgstr "不正ログイン防止のためのセッションクッキーを無効化"
681
682#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166
683msgid "Check this if you get disconnected or if your IP address changes often"
684msgstr ""
685"あなたが切断されたり、IPアドレスが頻繁に変わる環境下であるならチェックを入れ"
686"てください"
687
688#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
689msgid "Private links by default"
690msgstr "既定でプライベートリンク"
691
692#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184
693msgid "All new links are private by default"
694msgstr "すべての新規リンクをプライベートで作成"
695
696#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
697msgid "RSS direct links"
698msgstr "RSS 直リンク"
699
700#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200
701msgid "Check this to use direct URL instead of permalink in feeds"
702msgstr "フィードでパーマリンクの代わりに直リンクを使う"
703
704#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215
705msgid "Hide public links"
706msgstr "公開リンクを隠す"
707
708#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216
709msgid "Do not show any links if the user is not logged in"
710msgstr "ログインしていないユーザーには何のリンクも表示しない"
711
712#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231
713#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
714msgid "Check updates"
715msgstr "更新を確認"
716
717#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232
718#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
719msgid "Notify me when a new release is ready"
720msgstr "新しいバージョンがリリースされたときに通知"
721
722#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247
723#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
724msgid "Enable REST API"
725msgstr "REST API を有効化"
726
727#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248
728#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
729msgid "Allow third party software to use Shaarli such as mobile application"
730msgstr ""
731"モバイルアプリといったサードパーティーのソフトウェアにShaarliを使用することを"
732"許可"
733
734#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263
735msgid "API secret"
736msgstr "API シークレット"
737
738#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
739#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
740#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
741#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
742msgid "Save"
743msgstr "保存"
744
745#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
746msgid "The Daily Shaarli"
747msgstr "デイリーSharli"
748
749#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
750msgid "1 RSS entry per day"
751msgstr "各日1つずつのRSS項目"
752
753#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
754msgid "Previous day"
755msgstr "前日"
756
757#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
758msgid "All links of one day in a single page."
759msgstr "1日に作成されたすべてのリンクです。"
760
761#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
762msgid "Next day"
763msgstr "翌日"
764
765#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
766msgid "Created:"
767msgstr "作成:"
768
769#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
770msgid "URL"
771msgstr "URL"
772
773#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
774msgid "Title"
775msgstr "タイトル"
776
777#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
778#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
779#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
780#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
781#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
782msgid "Description"
783msgstr "説明"
784
785#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
786msgid "Tags"
787msgstr "タグ"
788
789#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
790#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
791#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
792msgid "Private"
793msgstr "プライベート"
794
795#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
796msgid "Apply Changes"
797msgstr "変更を適用"
798
799#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
800msgid "Export Database"
801msgstr "データベースをエクスポート"
802
803#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
804msgid "Selection"
805msgstr "選択済み"
806
807#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
808msgid "All"
809msgstr "すべて"
810
811#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
812msgid "Public"
813msgstr "公開"
814
815#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
816msgid "Prepend note permalinks with this Shaarli instance's URL"
817msgstr "この Shaarli のインスタンスのURL にノートへのパーマリンクを付け加える"
818
819#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
820msgid "Useful to import bookmarks in a web browser"
821msgstr "ウェブブラウザーのリンクをインポートするのに有効です"
822
823#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
824msgid "Import Database"
825msgstr "データベースをインポート"
826
827#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
828msgid "Maximum size allowed:"
829msgstr "最大サイズ:"
830
831#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
832msgid "Visibility"
833msgstr "可視性"
834
835#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
836msgid "Use values from the imported file, default to public"
837msgstr "インポート元のファイルの値を使用 (既定は公開リンクとなります)"
838
839#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
840msgid "Import all bookmarks as private"
841msgstr "すべてのブックマーク項目をプライベートリンクとしてインポート"
842
843#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
844msgid "Import all bookmarks as public"
845msgstr "すべてのブックマーク項目を公開リンクとしてインポート"
846
847#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
848msgid "Overwrite existing bookmarks"
849msgstr "既に存在しているブックマークを上書き"
850
851#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
852msgid "Duplicates based on URL"
853msgstr "URL による重複"
854
855#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
856msgid "Add default tags"
857msgstr "既定のタグを追加"
858
859#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
860msgid "Install Shaarli"
861msgstr "Shaarli をインストール"
862
863#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
864msgid "It looks like it's the first time you run Shaarli. Please configure it."
865msgstr "どうやら Shaarli を初めて起動しているようです。設定してください。"
866
867#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
868#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
869#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
870#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
871msgid "Username"
872msgstr "ユーザー名"
873
874#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
875#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
876#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
877#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148
878msgid "Password"
879msgstr "パスワード"
880
881#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
882msgid "Shaarli title"
883msgstr "Shaarli のタイトル"
884
885#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
886msgid "My links"
887msgstr "自分のリンク"
888
889#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
890msgid "Install"
891msgstr "インストール"
892
893#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
894#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
895msgid "shaare"
896msgid_plural "shaares"
897msgstr[0] "共有"
898msgstr[1] "共有"
899
900#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
901#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
902msgid "private link"
903msgid_plural "private links"
904msgstr[0] "プライベートリンク"
905msgstr[1] "プライベートリンク"
906
907#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
908#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
909#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117
910msgid "Search text"
911msgstr "文字列で検索"
912
913#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
914#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
915#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124
916#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
917#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
918#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
919#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
920msgid "Filter by tag"
921msgstr "タグによって分類"
922
923#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
924msgid "Nothing found."
925msgstr "何も見つかりませんでした。"
926
927#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119
928#, php-format
929msgid "%s result"
930msgid_plural "%s results"
931msgstr[0] "%s 件の結果"
932msgstr[1] "%s 件の結果"
933
934#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
935msgid "for"
936msgstr "for"
937
938#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
939msgid "tagged"
940msgstr "タグ付けされた"
941
942#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
943msgid "Remove tag"
944msgstr "タグを削除"
945
946#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
947msgid "with status"
948msgstr "with status"
949
950#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
951msgid "without any tag"
952msgstr "タグなし"
953
954#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174
955#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
956#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
957msgid "Fold"
958msgstr "畳む"
959
960#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
961msgid "Edited: "
962msgstr "編集済み: "
963
964#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
965msgid "permalink"
966msgstr "パーマリンク"
967
968#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
969msgid "Add tag"
970msgstr "タグを追加"
971
972#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
973#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7
974msgid "Filters"
975msgstr "分類"
976
977#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
978#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12
979msgid "Only display private links"
980msgstr "プライベートリンクのみを表示"
981
982#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
983#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:15
984msgid "Only display public links"
985msgstr "公開リンクのみを表示"
986
987#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
988#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:20
989msgid "Filter untagged links"
990msgstr "タグ付けされていないリンクで分類"
991
992#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
993#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
994#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24
995#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76
996#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
997#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
998msgid "Fold all"
999msgstr "すべて畳む"
1000
1001#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
1002#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:69
1003msgid "Links per page"
1004msgstr "各ページをリンク"
1005
1006#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1007msgid ""
1008"You have been banned after too many failed login attempts. Try again later."
1009msgstr "複数回に渡るログインへの失敗を検出しました。後でまた試してください。"
1010
1011#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1012#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
1013#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151
1014msgid "Remember me"
1015msgstr "パスワードを保存"
1016
1017#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1018#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1019#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
1020#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
1021msgid "by the Shaarli community"
1022msgstr "by Shaarli コミュニティ"
1023
1024#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1025#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
1026msgid "Documentation"
1027msgstr "ドキュメント"
1028
1029#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1030#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
1031msgid "Expand"
1032msgstr "展開する"
1033
1034#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
1035#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
1036msgid "Expand all"
1037msgstr "すべて展開する"
1038
1039#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1040#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
1041msgid "Are you sure you want to delete this link?"
1042msgstr "本当にこのリンクを削除しますか?"
1043
1044#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
1045#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1046#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61
1047#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86
1048msgid "RSS Feed"
1049msgstr "RSS フィード"
1050
1051#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1052#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1053#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66
1054#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102
1055msgid "Logout"
1056msgstr "ログアウト"
1057
1058#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1059#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
1060msgid "is available"
1061msgstr "が利用可能"
1062
1063#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
1064#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176
1065msgid "Error"
1066msgstr "エラー"
1067
1068#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1069msgid "Picture Wall"
1070msgstr "ピクチャーウォール"
1071
1072#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1073msgid "pics"
1074msgstr "画像"
1075
1076#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1077msgid "You need to enable Javascript to change plugin loading order."
1078msgstr ""
1079"プラグインを読み込む順番を変更するには、Javascriptを有効にする必要がありま"
1080"す。"
1081
1082#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1083msgid "Enabled Plugins"
1084msgstr "有効なプラグイン"
1085
1086#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
1087#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1088msgid "No plugin enabled."
1089msgstr "有効なプラグインはありません。"
1090
1091#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
1092#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
1093msgid "Disable"
1094msgstr "無効化"
1095
1096#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1097#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1098#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98
1099#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1100msgid "Name"
1101msgstr "名前"
1102
1103#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
1104#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1105msgid "Order"
1106msgstr "順序"
1107
1108#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1109msgid "Disabled Plugins"
1110msgstr "無効なプラグイン"
1111
1112#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
1113msgid "No plugin disabled."
1114msgstr "無効なプラグインはありません。"
1115
1116#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97
1117#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
1118msgid "Enable"
1119msgstr "有効化"
1120
1121#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1122msgid "More plugins available"
1123msgstr "さらに利用できるプラグインがあります"
1124
1125#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
1126msgid "in the documentation"
1127msgstr "ドキュメント内"
1128
1129#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
1130msgid "Plugin configuration"
1131msgstr "プラグイン設定"
1132
1133#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195
1134msgid "No parameter available."
1135msgstr "利用可能な設定項目はありません。"
1136
1137#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1138#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1139msgid "tags"
1140msgstr "タグ"
1141
1142#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
1143#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
1144msgid "List all links with those tags"
1145msgstr "このタグが付いているリンクをリスト化する"
1146
1147#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
1148#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
1149msgid "Sort by:"
1150msgstr "分類:"
1151
1152#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
1153#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5
1154msgid "Cloud"
1155msgstr "クラウド"
1156
1157#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6
1158#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6
1159msgid "Most used"
1160msgstr "もっとも使われた"
1161
1162#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
1163#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7
1164msgid "Alphabetical"
1165msgstr "アルファベット順"
1166
1167#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1168msgid "Settings"
1169msgstr "設定"
1170
1171#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1172msgid "Change Shaarli settings: title, timezone, etc."
1173msgstr "Shaarli の設定を変更: タイトル、タイムゾーンなど。"
1174
1175#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
1176msgid "Configure your Shaarli"
1177msgstr "あなたの Shaarli を設定"
1178
1179#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
1180msgid "Enable, disable and configure plugins"
1181msgstr "プラグインを有効化、無効化、設定する"
1182
1183#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1184msgid "Change your password"
1185msgstr "パスワードを変更"
1186
1187#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
1188msgid "Rename or delete a tag in all links"
1189msgstr "すべてのリンクのタグの名前を変更する、または削除する"
1190
1191#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1192msgid ""
1193"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1194"delicious...)"
1195msgstr ""
1196"Netscape HTML 形式のブックマークをインポートする (Firefox、Chrome、Operaと"
1197"いったブラウザーが含まれます)"
1198
1199#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1200msgid "Import links"
1201msgstr "リンクをインポート"
1202
1203#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1204msgid ""
1205"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1206"Opera, delicious...)"
1207msgstr ""
1208"Netscape HTML 形式のブックマークをエクスポートする (Firefox、Chrome、Operaと"
1209"いったブラウザーが含まれます)"
1210
1211#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1212msgid "Export database"
1213msgstr "リンクをエクスポート"
1214
1215#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
1216msgid ""
1217"Drag one of these button to your bookmarks toolbar or right-click it and "
1218"\"Bookmark This Link\""
1219msgstr ""
1220"これらのボタンのうち1つををブックマークバーにドラッグするか、右クリックして"
1221"「このリンクをブックマークに追加」してください"
1222
1223#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
1224msgid "then click on the bookmarklet in any page you want to share."
1225msgstr "共有したいページでブックマークレットをクリックしてください。"
1226
1227#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1228#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100
1229msgid ""
1230"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
1231"Link"
1232msgstr ""
1233"このリンクをブックマークバーにドラッグするか、右クリックして「このリンクを"
1234"ブックマークに追加」してください"
1235
1236#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
1237msgid "then click ✚Shaare link button in any page you want to share"
1238msgstr "✚リンクを共有 ボタンをクリックすることで、どこでもリンクを共有できます"
1239
1240#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1241#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
1242msgid "The selected text is too long, it will be truncated."
1243msgstr "選択された文字列は長すぎるので、一部が切り捨てられます。"
1244
1245#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
1246msgid "Shaare link"
1247msgstr "共有リンク"
1248
1249#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
1250msgid ""
1251"Then click ✚Add Note button anytime to start composing a private Note (text "
1252"post) to your Shaarli"
1253msgstr ""
1254"✚ノートを追加 ボタンをクリックすることで、いつでもプライベートノート(テキスト"
1255"形式)をShaarli上に作成できます"
1256
1257#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
1258msgid "Add Note"
1259msgstr "ノートを追加"
1260
1261#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
1262msgid ""
1263"You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1264"functionality."
1265msgstr ""
1266"この機能を使用するには、<strong>HTTPS</strong> 経由でShaarliに接続してくださ"
1267"い。"
1268
1269#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1270msgid "Add to"
1271msgstr "次に追加:"
1272
1273#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
1274msgid "3rd party"
1275msgstr "サードパーティー"
1276
1277#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
1278#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
1279msgid "Plugin"
1280msgstr "プラグイン"
1281
1282#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
1283#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
1284msgid "plugin"
1285msgstr "プラグイン"
1286
1287#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
1288msgid ""
1289"Drag this link to your bookmarks toolbar, or right-click it and choose "
1290"Bookmark This Link"
1291msgstr ""
1292"このリンクをブックマークバーにドラッグするか、右クリックして「このリンクを"
1293"ブックマークに追加」してください"
diff --git a/inc/languages/jp/LC_MESSAGES/shaarli.po b/inc/languages/jp/LC_MESSAGES/shaarli.po
new file mode 100644
index 00000000..57f42fc2
--- /dev/null
+++ b/inc/languages/jp/LC_MESSAGES/shaarli.po
@@ -0,0 +1,1333 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Shaarli\n"
4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2020-10-19 10:19+0900\n"
6"PO-Revision-Date: 2020-10-19 10:25+0900\n"
7"Last-Translator: yude <yudesleepy@gmail.com>\n"
8"Language-Team: Shaarli\n"
9"Language: ja\n"
10"MIME-Version: 1.0\n"
11"Content-Type: text/plain; charset=UTF-8\n"
12"Content-Transfer-Encoding: 8bit\n"
13"X-Generator: Poedit 2.2.3\n"
14"X-Poedit-Basepath: ../../../..\n"
15"Plural-Forms: nplurals=2; plural=(n != 1);\n"
16"X-Poedit-SourceCharset: UTF-8\n"
17"X-Poedit-KeywordsList: t:1,2;t\n"
18"X-Poedit-SearchPath-0: .\n"
19"X-Poedit-SearchPathExcluded-0: node_modules\n"
20"X-Poedit-SearchPathExcluded-1: vendor\n"
21
22#: application/ApplicationUtils.php:161
23#, php-format
24msgid ""
25"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
26"cannot run. Your PHP version has known security vulnerabilities and should "
27"be updated as soon as possible."
28msgstr ""
29"使用している PHP のバージョンが古すぎます! Shaarli の実行には最低でも PHP %s "
30"が必要です。 現在使用している PHP のバージョンには脆弱性があり、できるだけ速"
31"やかにアップデートするべきです。"
32
33#: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204
34msgid "directory is not readable"
35msgstr "ディレクトリを読み込めません"
36
37#: application/ApplicationUtils.php:207
38msgid "directory is not writable"
39msgstr "ディレクトリに書き込めません"
40
41#: application/ApplicationUtils.php:225
42msgid "file is not readable"
43msgstr "ファイルを読み取る権限がありません"
44
45#: application/ApplicationUtils.php:228
46msgid "file is not writable"
47msgstr "ファイルを書き込む権限がありません"
48
49#: application/History.php:179
50msgid "History file isn't readable or writable"
51msgstr "履歴ファイルを読み込む、または書き込むための権限がありません"
52
53#: application/History.php:190
54msgid "Could not parse history file"
55msgstr "履歴ファイルを正常に復元できませんでした"
56
57#: application/Languages.php:181
58msgid "Automatic"
59msgstr "自動"
60
61#: application/Languages.php:182
62msgid "German"
63msgstr "ドイツ語"
64
65#: application/Languages.php:183
66msgid "English"
67msgstr "英語"
68
69#: application/Languages.php:184
70msgid "French"
71msgstr "フランス語"
72
73#: application/Languages.php:185
74msgid "Japanese"
75msgstr "日本語"
76
77#: application/Thumbnailer.php:62
78msgid ""
79"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
80"disabled. Please reload the page."
81msgstr ""
82"サムネイルを使用するには、php-gd エクステンションが読み込まれている必要があり"
83"ます。サムネイルは無効化されました。ページを再読込してください。"
84
85#: application/Utils.php:383 tests/UtilsTest.php:343
86msgid "Setting not set"
87msgstr "未設定"
88
89#: application/Utils.php:390 tests/UtilsTest.php:341 tests/UtilsTest.php:342
90msgid "Unlimited"
91msgstr "無制限"
92
93#: application/Utils.php:393 tests/UtilsTest.php:338 tests/UtilsTest.php:339
94#: tests/UtilsTest.php:353
95msgid "B"
96msgstr "B"
97
98#: application/Utils.php:393 tests/UtilsTest.php:332 tests/UtilsTest.php:333
99#: tests/UtilsTest.php:340
100msgid "kiB"
101msgstr "kiB"
102
103#: application/Utils.php:393 tests/UtilsTest.php:334 tests/UtilsTest.php:335
104#: tests/UtilsTest.php:351 tests/UtilsTest.php:352
105msgid "MiB"
106msgstr "MiB"
107
108#: application/Utils.php:393 tests/UtilsTest.php:336 tests/UtilsTest.php:337
109msgid "GiB"
110msgstr "GiB"
111
112#: application/bookmark/BookmarkFileService.php:180
113#: application/bookmark/BookmarkFileService.php:202
114#: application/bookmark/BookmarkFileService.php:224
115#: application/bookmark/BookmarkFileService.php:238
116msgid "You're not authorized to alter the datastore"
117msgstr "設定を変更する権限がありません"
118
119#: application/bookmark/BookmarkFileService.php:205
120msgid "This bookmarks already exists"
121msgstr "このブックマークは既に存在します。"
122
123#: application/bookmark/BookmarkInitializer.php:39
124msgid "(private bookmark with thumbnail demo)"
125msgstr "(サムネイルデモが付属しているプライベートブックマーク)"
126
127#: application/bookmark/BookmarkInitializer.php:42
128msgid ""
129"Shaarli will automatically pick up the thumbnail for links to a variety of "
130"websites.\n"
131"\n"
132"Explore your new Shaarli instance by trying out controls and menus.\n"
133"Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the "
134"documentation](https://shaarli.readthedocs.io/en/master/) to learn more "
135"about Shaarli.\n"
136"\n"
137"Now you can edit or delete the default shaares.\n"
138msgstr ""
139"Shaarli は自動的に多様なウェブサイトのサムネイルを取得します。\n"
140"\n"
141"あなたの新しい Shaarli インスタンスをコントロールやメニューを試したりして、探"
142"検してください。\n"
143" [Github](https://github.com/shaarli/Shaarli) または [the documentation]"
144"(https://shaarli.readthedocs.io/en/master/) でプロジェクトを訪問して、"
145"Shaarli をもっとよく知ることができます。\n"
146"\n"
147"今から、既定の shaares を編集したり、削除したりすることができます。\n"
148
149#: application/bookmark/BookmarkInitializer.php:55
150msgid "Note: Shaare descriptions"
151msgstr "説明: Shaare の概要"
152
153#: application/bookmark/BookmarkInitializer.php:57
154msgid ""
155"Adding a shaare without entering a URL creates a text-only \"note\" post "
156"such as this one.\n"
157"This note is private, so you are the only one able to see it while logged "
158"in.\n"
159"\n"
160"You can use this to keep notes, post articles, code snippets, and much "
161"more.\n"
162"\n"
163"The Markdown formatting setting allows you to format your notes and bookmark "
164"description:\n"
165"\n"
166"### Title headings\n"
167"\n"
168"#### Multiple headings levels\n"
169" * bullet lists\n"
170" * _italic_ text\n"
171" * **bold** text\n"
172" * ~~strike through~~ text\n"
173" * `code` blocks\n"
174" * images\n"
175" * [links](https://en.wikipedia.org/wiki/Markdown)\n"
176"\n"
177"Markdown also supports tables:\n"
178"\n"
179"| Name | Type | Color | Qty |\n"
180"| ------- | --------- | ------ | ----- |\n"
181"| Orange | Fruit | Orange | 126 |\n"
182"| Apple | Fruit | Any | 62 |\n"
183"| Lemon | Fruit | Yellow | 30 |\n"
184"| Carrot | Vegetable | Red | 14 |\n"
185msgstr ""
186"URL を追加せずに shaare を作成すると、テキストのみのこのような \"ノート\" が"
187"作成されます。\n"
188"このノートはプライベートなので、ログイン中のあなたしか見ることはできませ"
189"ん。\n"
190"\n"
191"あなたはこれをメモ帳として使ったり、記事を投稿したり、コード スニペットとした"
192"りするなどといったことに使えます。\n"
193"\n"
194"Markdown フォーマットの設定により、ノートやブックマークの概要を以下のように"
195"フォーマットできます:\n"
196"\n"
197"### タイトル ヘッダー\n"
198"\n"
199"#### 複数の見出し\n"
200" * 箇条書きリスト\n"
201" * _イタリック_ 文字\n"
202" * **ボールド** 文字\n"
203" * ~~打ち消し~~ 文字\n"
204" * `コード` ブロック\n"
205" * 画像\n"
206" * [リンク](https://en.wikipedia.org/wiki/Markdown)\n"
207"\n"
208"Markdown は表もサポートします:\n"
209"\n"
210"| 名前 | 種類 | 色 | 数量 |\n"
211"| ------- | --------- | ------ | ----- |\n"
212"| オレンジ | 果物 | 橙 | 126 |\n"
213"| リンゴ | 果物 | 任意 | 62 |\n"
214"| レモン | 果物 | 黄 | 30 |\n"
215"| 人参 | 野菜 | 赤 | 14 |\n"
216
217#: application/bookmark/BookmarkInitializer.php:91
218#: application/legacy/LegacyLinkDB.php:246
219msgid ""
220"The personal, minimalist, super-fast, database free, bookmarking service"
221msgstr ""
222"個人向けの、ミニマムで高速でかつデータベースのいらないブックマークサービス"
223
224#: application/bookmark/BookmarkInitializer.php:94
225msgid ""
226"Welcome to Shaarli!\n"
227"\n"
228"Shaarli allows you to bookmark your favorite pages, and share them with "
229"others or store them privately.\n"
230"You can add a description to your bookmarks, such as this one, and tag "
231"them.\n"
232"\n"
233"Create a new shaare by clicking the `+Shaare` button, or using any of the "
234"recommended tools (browser extension, mobile app, bookmarklet, REST API, "
235"etc.).\n"
236"\n"
237"You can easily retrieve your links, even with thousands of them, using the "
238"internal search engine, or search through tags (e.g. this Shaare is tagged "
239"with `shaarli` and `help`).\n"
240"Hashtags such as #shaarli #help are also supported.\n"
241"You can also filter the available [RSS feed](/feed/atom) and picture wall by "
242"tag or plaintext search.\n"
243"\n"
244"We hope that you will enjoy using Shaarli, maintained with ❤️ by the "
245"community!\n"
246"Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if "
247"you have a suggestion or encounter an issue.\n"
248msgstr ""
249"Shaarli へようこそ!\n"
250"\n"
251"Shaarli では、あなたのお気に入りのページをブックマークしたり、それを他の人と"
252"共有するか、またはプライベートなものとして保管することができます。\n"
253"加えて、あなたのブックマークにこの項目のように概要を追加したり、タグ付けした"
254"りすることができます。\n"
255"\n"
256"`+Shaare` ボタンをクリックすることで新しい shaare を作成できます。また、推奨"
257"されたツールを使うこともできます (ブラウザー 拡張機能、モバイル アプリ、ブッ"
258"クマークレット、REST API など...)。\n"
259"\n"
260"また、簡単にあなたのリンクを取得できます。それが何千と登る数であっても、内部"
261"の検索エンジンや、タグを使って検索できます (例えば、この Shaare は `shaarli` "
262"と `help` というタグが付いています)。\n"
263"#shaarli や #help といったハッシュタグもサポートされています。\n"
264"タグやテキスト検索による [RSS フィード](/feed/atom) や ピクチャー ウォール で"
265"項目を絞ることもできます。\n"
266"\n"
267"私たちはあなたが Shaarli を楽しんでくれることを願っています。Shaarli はコミュ"
268"ニティーによって ♡ と共にメンテナンスされています!\n"
269"何か問題に遭遇したり、提案があれば、気軽に [Issue](https://github.com/"
270"shaarli/Shaarli/issues) を開いてください。\n"
271
272#: application/bookmark/exception/BookmarkNotFoundException.php:13
273msgid "The link you are trying to reach does not exist or has been deleted."
274msgstr "開こうとしたリンクは存在しないか、削除されています。"
275
276#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:129
277msgid ""
278"Shaarli could not create the config file. Please make sure Shaarli has the "
279"right to write in the folder is it installed in."
280msgstr ""
281"Shaarli は設定ファイルを作成できませんでした。Shaarli が正しい権限下に置かれ"
282"ていて、インストールされているディレクトリに書き込みできることを確認してくだ"
283"さい。"
284
285#: application/config/ConfigManager.php:136
286#: application/config/ConfigManager.php:163
287msgid "Invalid setting key parameter. String expected, got: "
288msgstr ""
289"不正なキーの値です。文字列が想定されていますが、次のように入力されました: "
290
291#: application/config/exception/MissingFieldConfigException.php:21
292#, php-format
293msgid "Configuration value is required for %s"
294msgstr "%s には設定が必要です"
295
296#: application/config/exception/PluginConfigOrderException.php:15
297msgid "An error occurred while trying to save plugins loading order."
298msgstr "プラグインの読込順を変更する際にエラーが発生しました。"
299
300#: application/config/exception/UnauthorizedConfigException.php:16
301msgid "You are not authorized to alter config."
302msgstr "設定を変更する権限がありません。"
303
304#: application/exceptions/IOException.php:22
305msgid "Error accessing"
306msgstr "読込中にエラーが発生しました"
307
308#: application/feed/FeedBuilder.php:179
309msgid "Direct link"
310msgstr "ダイレクトリンク"
311
312#: application/feed/FeedBuilder.php:181
313msgid "Permalink"
314msgstr "パーマリンク"
315
316#: application/front/controller/admin/ConfigureController.php:54
317msgid "Configure"
318msgstr "設定"
319
320#: application/front/controller/admin/ConfigureController.php:102
321#: application/legacy/LegacyUpdater.php:537
322msgid "You have enabled or changed thumbnails mode."
323msgstr "サムネイルのモードを有効化、または変更しました。"
324
325#: application/front/controller/admin/ConfigureController.php:103
326#: application/legacy/LegacyUpdater.php:538
327msgid "Please synchronize them."
328msgstr "それらを同期してください。"
329
330#: application/front/controller/admin/ConfigureController.php:113
331#: application/front/controller/visitor/InstallController.php:136
332msgid "Error while writing config file after configuration update."
333msgstr "設定ファイルを更新した後の書き込みに失敗しました。"
334
335#: application/front/controller/admin/ConfigureController.php:122
336msgid "Configuration was saved."
337msgstr "設定は保存されました。"
338
339#: application/front/controller/admin/ExportController.php:26
340msgid "Export"
341msgstr "エクスポート"
342
343#: application/front/controller/admin/ExportController.php:42
344msgid "Please select an export mode."
345msgstr "エクスポート モードを指定してください。"
346
347#: application/front/controller/admin/ImportController.php:41
348msgid "Import"
349msgstr "インポート"
350
351#: application/front/controller/admin/ImportController.php:55
352msgid "No import file provided."
353msgstr "何のインポート元ファイルも指定されませんでした。"
354
355#: application/front/controller/admin/ImportController.php:66
356#, php-format
357msgid ""
358"The file you are trying to upload is probably bigger than what this "
359"webserver can accept (%s). Please upload in smaller chunks."
360msgstr ""
361"あなたがアップロードしようとしているファイルは、サーバーが許可しているファイ"
362"ルサイズ (%s) よりも大きいです。もう少し小さいものをアップロードしてくださ"
363"い。"
364
365#: application/front/controller/admin/ManageShaareController.php:29
366msgid "Shaare a new link"
367msgstr "新しいリンクを追加"
368
369#: application/front/controller/admin/ManageShaareController.php:78
370msgid "Note: "
371msgstr "注: "
372
373#: application/front/controller/admin/ManageShaareController.php:109
374#: application/front/controller/admin/ManageShaareController.php:206
375#: application/front/controller/admin/ManageShaareController.php:275
376#: application/front/controller/admin/ManageShaareController.php:315
377#, php-format
378msgid "Bookmark with identifier %s could not be found."
379msgstr "%s という識別子を持ったブックマークは見つかりませんでした。"
380
381#: application/front/controller/admin/ManageShaareController.php:194
382#: application/front/controller/admin/ManageShaareController.php:252
383msgid "Invalid bookmark ID provided."
384msgstr "不正なブックマーク ID が入力されました。"
385
386#: application/front/controller/admin/ManageShaareController.php:260
387msgid "Invalid visibility provided."
388msgstr "不正な公開設定が入力されました。"
389
390#: application/front/controller/admin/ManageShaareController.php:363
391msgid "Edit"
392msgstr "共有"
393
394#: application/front/controller/admin/ManageShaareController.php:366
395msgid "Shaare"
396msgstr "Shaare"
397
398#: application/front/controller/admin/ManageTagController.php:29
399msgid "Manage tags"
400msgstr "タグを設定"
401
402#: application/front/controller/admin/ManageTagController.php:48
403msgid "Invalid tags provided."
404msgstr "不正なタグが入力されました。"
405
406#: application/front/controller/admin/ManageTagController.php:72
407#, php-format
408msgid "The tag was removed from %d bookmark."
409msgid_plural "The tag was removed from %d bookmarks."
410msgstr[0] "%d 件のリンクからタグが削除されました。"
411msgstr[1] "%d 件のリンクからタグが削除されました。"
412
413#: application/front/controller/admin/ManageTagController.php:77
414#, php-format
415msgid "The tag was renamed in %d bookmark."
416msgid_plural "The tag was renamed in %d bookmarks."
417msgstr[0] "このタグを持つ %d 件のリンクにおいて、名前が変更されました。"
418msgstr[1] "このタグを持つ %d 件のリンクにおいて、名前が変更されました。"
419
420#: application/front/controller/admin/PasswordController.php:28
421msgid "Change password"
422msgstr "パスワードを変更"
423
424#: application/front/controller/admin/PasswordController.php:55
425msgid "You must provide the current and new password to change it."
426msgstr ""
427"パスワードを変更するには、現在のパスワードと、新しいパスワードを入力する必要"
428"があります。"
429
430#: application/front/controller/admin/PasswordController.php:71
431msgid "The old password is not correct."
432msgstr "元のパスワードが正しくありません。"
433
434#: application/front/controller/admin/PasswordController.php:97
435msgid "Your password has been changed"
436msgstr "あなたのパスワードは変更されました"
437
438#: application/front/controller/admin/PluginsController.php:45
439msgid "Plugin Administration"
440msgstr "プラグイン管理"
441
442#: application/front/controller/admin/PluginsController.php:76
443msgid "Setting successfully saved."
444msgstr "設定が正常に保存されました。"
445
446#: application/front/controller/admin/PluginsController.php:79
447msgid "Error while saving plugin configuration: "
448msgstr "プラグインの設定ファイルを保存するときにエラーが発生しました: "
449
450#: application/front/controller/admin/ThumbnailsController.php:37
451msgid "Thumbnails update"
452msgstr "サムネイルの更新"
453
454#: application/front/controller/admin/ToolsController.php:31
455msgid "Tools"
456msgstr "ツール"
457
458#: application/front/controller/visitor/BookmarkListController.php:116
459msgid "Search: "
460msgstr "検索: "
461
462#: application/front/controller/visitor/DailyController.php:45
463msgid "Today"
464msgstr "今日"
465
466#: application/front/controller/visitor/DailyController.php:47
467msgid "Yesterday"
468msgstr "昨日"
469
470#: application/front/controller/visitor/DailyController.php:85
471msgid "Daily"
472msgstr "デイリー"
473
474#: application/front/controller/visitor/ErrorController.php:36
475msgid "An unexpected error occurred."
476msgstr "予期しないエラーが発生しました。"
477
478#: application/front/controller/visitor/ErrorNotFoundController.php:25
479msgid "Requested page could not be found."
480msgstr "リクエストされたページは存在しません。"
481
482#: application/front/controller/visitor/InstallController.php:73
483#, php-format
484msgid ""
485"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
486"variable \"session.save_path\" is set correctly in your PHP config, and that "
487"you have write access to it.<br>It currently points to %s.<br>On some "
488"browsers, accessing your server via a hostname like 'localhost' or any "
489"custom hostname without a dot causes cookie storage to fail. We recommend "
490"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
491msgstr ""
492"<pre>セッションが正常にあなたのサーバー上で稼働していないようです。<br>PHP の"
493"設定ファイル内にて、正しく \"session.save_path\" の値が設定されていることと、"
494"権限が間違っていないことを確認してください。<br>現在 %s からPHPの設定ファイル"
495"を読み込んでいます。<br>一部のブラウザーにおいて、localhost や他のドットを含"
496"まないホスト名にてサーバーにアクセスする際に、クッキーを保存できないことがあ"
497"ります。IP アドレスや完全なドメイン名でサーバーにアクセスすることをおすすめし"
498"ます。<br>"
499
500#: application/front/controller/visitor/InstallController.php:144
501msgid ""
502"Shaarli is now configured. Please login and start shaaring your bookmarks!"
503msgstr ""
504"Shaarli の設定が完了しました。ログインして、あなたのブックマークを登録しま"
505"しょう!"
506
507#: application/front/controller/visitor/InstallController.php:158
508msgid "Insufficient permissions:"
509msgstr "権限がありません:"
510
511#: application/front/controller/visitor/LoginController.php:46
512msgid "Login"
513msgstr "ログイン"
514
515#: application/front/controller/visitor/LoginController.php:78
516msgid "Wrong login/password."
517msgstr "不正なユーザー名、またはパスワードです。"
518
519#: application/front/controller/visitor/PictureWallController.php:29
520msgid "Picture wall"
521msgstr "ピクチャウォール"
522
523#: application/front/controller/visitor/TagCloudController.php:88
524msgid "Tag "
525msgstr "タグ "
526
527#: application/front/exceptions/AlreadyInstalledException.php:11
528msgid "Shaarli has already been installed. Login to edit the configuration."
529msgstr "Shaarli がインストールされました。ログインして設定を変更できます。"
530
531#: application/front/exceptions/LoginBannedException.php:11
532msgid ""
533"You have been banned after too many failed login attempts. Try again later."
534msgstr "複数回に渡るログインへの失敗を検出しました。後でまた試してください。"
535
536#: application/front/exceptions/OpenShaarliPasswordException.php:16
537msgid "You are not supposed to change a password on an Open Shaarli."
538msgstr ""
539"公開されている Shaarli において、パスワードを変更することは想定されていませ"
540"ん。"
541
542#: application/front/exceptions/ThumbnailsDisabledException.php:11
543msgid "Picture wall unavailable (thumbnails are disabled)."
544msgstr "ピクチャ ウォールは利用できません (サムネイルが無効化されています)。"
545
546#: application/front/exceptions/WrongTokenException.php:16
547msgid "Wrong token."
548msgstr "不正なトークンです。"
549
550#: application/legacy/LegacyLinkDB.php:131
551msgid "You are not authorized to add a link."
552msgstr "リンクを追加するには、ログインする必要があります。"
553
554#: application/legacy/LegacyLinkDB.php:134
555msgid "Internal Error: A link should always have an id and URL."
556msgstr "エラー: リンクにはIDとURLを登録しなければなりません。"
557
558#: application/legacy/LegacyLinkDB.php:137
559msgid "You must specify an integer as a key."
560msgstr "正常なキーの値ではありません。"
561
562#: application/legacy/LegacyLinkDB.php:140
563msgid "Array offset and link ID must be equal."
564msgstr "Array オフセットとリンクのIDは同じでなければなりません。"
565
566#: application/legacy/LegacyLinkDB.php:249
567msgid ""
568"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
569"me, you must first login.\n"
570"\n"
571"To learn how to use Shaarli, consult the link \"Documentation\" at the "
572"bottom of this page.\n"
573"\n"
574"You use the community supported version of the original Shaarli project, by "
575"Sebastien Sauvage."
576msgstr ""
577"Shaarli へようこそ! これはあなたの最初の公開ブックマークです。これを編集した"
578"り削除したりするには、ログインする必要があります。\n"
579"\n"
580"Shaarli の使い方を知るには、このページの下にある「ドキュメント」のリンクを開"
581"いてください。\n"
582"\n"
583"あなたは Sebastien Sauvage による、コミュニティーサポートのあるバージョンのオ"
584"リジナルのShaarli プロジェクトを使用しています。"
585
586#: application/legacy/LegacyLinkDB.php:266
587msgid "My secret stuff... - Pastebin.com"
588msgstr "わたしのひ💗み💗つ💗 - Pastebin.com"
589
590#: application/legacy/LegacyLinkDB.php:268
591msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
592msgstr ""
593"シーッ! これはあなたしか見られないプライベートリンクです。消すこともできま"
594"す。"
595
596#: application/legacy/LegacyUpdater.php:104
597#, fuzzy
598#| msgid "Couldn't retrieve Updater class methods."
599msgid "Couldn't retrieve updater class methods."
600msgstr "アップデーターのクラスメゾットを受信できませんでした。"
601
602#: application/legacy/LegacyUpdater.php:538
603msgid "<a href=\"./admin/thumbnails\">"
604msgstr "<a href=\"./admin/thumbnails\">"
605
606#: application/netscape/NetscapeBookmarkUtils.php:63
607msgid "Invalid export selection:"
608msgstr "不正なエクスポートの選択:"
609
610#: application/netscape/NetscapeBookmarkUtils.php:215
611#, php-format
612msgid "File %s (%d bytes) "
613msgstr "ファイル %s (%d バイト) "
614
615#: application/netscape/NetscapeBookmarkUtils.php:217
616msgid "has an unknown file format. Nothing was imported."
617msgstr "は不明なファイル形式です。インポートは中止されました。"
618
619#: application/netscape/NetscapeBookmarkUtils.php:221
620#, fuzzy, php-format
621#| msgid ""
622#| "was successfully processed in %d seconds: %d links imported, %d links "
623#| "overwritten, %d links skipped."
624msgid ""
625"was successfully processed in %d seconds: %d bookmarks imported, %d "
626"bookmarks overwritten, %d bookmarks skipped."
627msgstr ""
628"が %d 秒で処理され、%d 件のリンクがインポートされ、%d 件のリンクが上書きさ"
629"れ、%d 件のリンクがスキップされました。"
630
631#: application/plugin/PluginManager.php:124
632msgid " [plugin incompatibility]: "
633msgstr "[非対応のプラグイン]: "
634
635#: application/plugin/exception/PluginFileNotFoundException.php:21
636#, php-format
637msgid "Plugin \"%s\" files not found."
638msgstr "プラグイン「%s」のファイルが存在しません。"
639
640#: application/render/PageCacheManager.php:32
641#, php-format
642msgid "Cannot purge %s: no directory"
643msgstr "%s を削除できません: ディレクトリが存在しません"
644
645#: application/updater/exception/UpdaterException.php:51
646msgid "An error occurred while running the update "
647msgstr "更新中に問題が発生しました "
648
649#: index.php:65
650msgid "Shared bookmarks on "
651msgstr "次において共有されたリンク "
652
653#: plugins/addlink_toolbar/addlink_toolbar.php:31
654msgid "URI"
655msgstr "URI"
656
657#: plugins/addlink_toolbar/addlink_toolbar.php:35
658msgid "Add link"
659msgstr "リンクを追加"
660
661#: plugins/addlink_toolbar/addlink_toolbar.php:52
662msgid "Adds the addlink input on the linklist page."
663msgstr "リンク一覧のページに、リンクを追加するためのフォームを表示する。"
664
665#: plugins/archiveorg/archiveorg.php:28
666msgid "View on archive.org"
667msgstr "archive.org 上で表示する"
668
669#: plugins/archiveorg/archiveorg.php:41
670msgid "For each link, add an Archive.org icon."
671msgstr "それぞれのリンクに、Archive.org のアイコンを追加する。"
672
673#: plugins/default_colors/default_colors.php:38
674msgid ""
675"Default colors plugin error: This plugin is active and no custom color is "
676"configured."
677msgstr ""
678"既定の色のプラグインにおけるエラー: このプラグインは有効なので、カスタム カ"
679"ラーは適用されません。"
680
681#: plugins/default_colors/default_colors.php:113
682msgid "Override default theme colors. Use any CSS valid color."
683msgstr ""
684"既定のテーマの色を上書きします。どのような CSS カラーコードでも使えます。"
685
686#: plugins/default_colors/default_colors.php:114
687msgid "Main color (navbar green)"
688msgstr "メイン カラー (ナビバーの緑)"
689
690#: plugins/default_colors/default_colors.php:115
691msgid "Background color (light grey)"
692msgstr "背景色 (灰色)"
693
694#: plugins/default_colors/default_colors.php:116
695msgid "Dark main color (e.g. visited links)"
696msgstr "暗い方の メイン カラー (例: 閲覧済みリンク)"
697
698#: plugins/demo_plugin/demo_plugin.php:477
699msgid ""
700"A demo plugin covering all use cases for template designers and plugin "
701"developers."
702msgstr ""
703"テンプレートのデザイナーや、プラグインの開発者のためのすべての状況に対応でき"
704"るデモプラグインです。"
705
706#: plugins/demo_plugin/demo_plugin.php:478
707msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
708msgstr "これはデモプラグイン専用のパラメーターです。末尾に追加されます。"
709
710#: plugins/demo_plugin/demo_plugin.php:479
711msgid "Other demo parameter"
712msgstr "他のデモ パラメーター"
713
714#: plugins/isso/isso.php:22
715msgid ""
716"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
717"administration page."
718msgstr ""
719"Isso プラグインエラー: \"ISSO_SERVER\" の値をプラグイン管理ページにて指定して"
720"ください。"
721
722#: plugins/isso/isso.php:92
723msgid "Let visitor comment your shaares on permalinks with Isso."
724msgstr ""
725"Isso を使って、あなたのパーマリンク上のリンクに第三者がコメントを残すことがで"
726"きます。"
727
728#: plugins/isso/isso.php:93
729msgid "Isso server URL (without 'http://')"
730msgstr "Isso server URL ('http://' 抜き)"
731
732#: plugins/piwik/piwik.php:23
733msgid ""
734"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
735"administration page."
736msgstr ""
737"Piwik プラグインエラー: PIWIK_URL と PIWIK_SITEID の値をプラグイン管理ページ"
738"で指定してください。"
739
740#: plugins/piwik/piwik.php:72
741msgid "A plugin that adds Piwik tracking code to Shaarli pages."
742msgstr "Piwik のトラッキングコードをShaarliに追加するプラグインです。"
743
744#: plugins/piwik/piwik.php:73
745msgid "Piwik URL"
746msgstr "Piwik URL"
747
748#: plugins/piwik/piwik.php:74
749msgid "Piwik site ID"
750msgstr "Piwik サイトID"
751
752#: plugins/playvideos/playvideos.php:25
753msgid "Video player"
754msgstr "動画プレイヤー"
755
756#: plugins/playvideos/playvideos.php:28
757msgid "Play Videos"
758msgstr "動画を再生"
759
760#: plugins/playvideos/playvideos.php:59
761msgid "Add a button in the toolbar allowing to watch all videos."
762msgstr "すべての動画を閲覧するボタンをツールバーに追加します。"
763
764#: plugins/playvideos/youtube_playlist.js:214
765msgid "plugins/playvideos/jquery-1.11.2.min.js"
766msgstr "plugins/playvideos/jquery-1.11.2.min.js"
767
768#: plugins/pubsubhubbub/pubsubhubbub.php:72
769#, php-format
770msgid "Could not publish to PubSubHubbub: %s"
771msgstr "PubSubHubbub に登録できませんでした: %s"
772
773#: plugins/pubsubhubbub/pubsubhubbub.php:99
774#, php-format
775msgid "Could not post to %s"
776msgstr "%s に登録できませんでした"
777
778#: plugins/pubsubhubbub/pubsubhubbub.php:103
779#, php-format
780msgid "Bad response from the hub %s"
781msgstr "ハブ %s からの不正なレスポンス"
782
783#: plugins/pubsubhubbub/pubsubhubbub.php:114
784msgid "Enable PubSubHubbub feed publishing."
785msgstr "PubSubHubbub へのフィードを公開する。"
786
787#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:70
788msgid "For each link, add a QRCode icon."
789msgstr "それぞれのリンクについて、QRコードのアイコンを追加する。"
790
791#: plugins/wallabag/wallabag.php:21
792msgid ""
793"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
794"plugin administration page."
795msgstr ""
796"Wallabag プラグインエラー: \"WALLABAG_URL\" の値をプラグイン管理ページにおい"
797"て指定してください。"
798
799#: plugins/wallabag/wallabag.php:47
800msgid "Save to wallabag"
801msgstr "Wallabag に保存"
802
803#: plugins/wallabag/wallabag.php:71
804msgid "Wallabag API URL"
805msgstr "Wallabag のAPIのURL"
806
807#: plugins/wallabag/wallabag.php:72
808msgid "Wallabag API version (1 or 2)"
809msgstr "Wallabag のAPIのバージョン (1 または 2)"
810
811#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
812#: tests/languages/fr/LanguagesFrTest.php:159
813#: tests/languages/fr/LanguagesFrTest.php:172
814msgid "Search"
815msgid_plural "Search"
816msgstr[0] "検索"
817msgstr[1] "検索"
818
819#~ msgid "The page you are trying to reach does not exist or has been deleted."
820#~ msgstr "あなたが開こうとしたページは存在しないか、削除されています。"
821
822#~ msgid "404 Not Found"
823#~ msgstr "404 ページが存在しません"
824
825#~ msgid "Updates file path is not set, can't write updates."
826#~ msgstr ""
827#~ "更新するファイルのパスが指定されていないため、更新を書き込めません。"
828
829#~ msgid "Unable to write updates in "
830#~ msgstr "更新を次の項目に書き込めませんでした: "
831
832#~ msgid "I said: NO. You are banned for the moment. Go away."
833#~ msgstr "あなたはこのサーバーからBANされています。"
834
835#~ msgid "Tag cloud"
836#~ msgstr "タグクラウド"
837
838#~ msgid "Click to try again."
839#~ msgstr "クリックして再度試します。"
840
841#~ msgid "Description will be rendered with"
842#~ msgstr "説明は次の方法で描画されます:"
843
844#~ msgid "Markdown syntax documentation"
845#~ msgstr "マークダウン形式のドキュメント"
846
847#~ msgid "Markdown syntax"
848#~ msgstr "マークダウン形式"
849
850#~ msgid ""
851#~ "Render shaare description with Markdown syntax.<br><strong>Warning</"
852#~ "strong>:\n"
853#~ "If your shaared descriptions contained HTML tags before enabling the "
854#~ "markdown plugin,\n"
855#~ "enabling it might break your page.\n"
856#~ "See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
857#~ "markdown#html-rendering\">README</a>."
858#~ msgstr ""
859#~ "リンクの説明をマークダウン形式で表示します。<br><strong>警告</strong>:\n"
860#~ "リンクの説明にHTMLタグがこのプラグインを有効にする前に含まれていた場合、\n"
861#~ "正常にページを表示できなくなるかもしれません。\n"
862#~ "詳しくは <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
863#~ "markdown#html-rendering\">README</a> をご覧ください。"
864
865#~ msgid "Sorry, nothing to see here."
866#~ msgstr "すみませんが、ここには何もありません。"
867
868#~ msgid "URL or leave empty to post a note"
869#~ msgstr "URL を入力するか、空欄にするとノートを投稿します"
870
871#~ msgid "Current password"
872#~ msgstr "現在のパスワード"
873
874#~ msgid "New password"
875#~ msgstr "新しいパスワード"
876
877#~ msgid "Change"
878#~ msgstr "変更"
879
880#~ msgid "Tag"
881#~ msgstr "タグ"
882
883#~ msgid "New name"
884#~ msgstr "変更先の名前"
885
886#~ msgid "Case sensitive"
887#~ msgstr "大文字と小文字を区別"
888
889#~ msgid "Rename"
890#~ msgstr "名前を変更"
891
892#~ msgid "Delete"
893#~ msgstr "削除"
894
895#~ msgid "You can also edit tags in the"
896#~ msgstr "次に含まれるタグを編集することもできます:"
897
898#~ msgid "tag list"
899#~ msgstr "タグ一覧"
900
901#~ msgid "title"
902#~ msgstr "タイトル"
903
904#~ msgid "Home link"
905#~ msgstr "ホームのリンク先"
906
907#~ msgid "Default value"
908#~ msgstr "既定の値"
909
910#~ msgid "Theme"
911#~ msgstr "テーマ"
912
913#~ msgid "Language"
914#~ msgstr "言語"
915
916#~ msgid "Timezone"
917#~ msgstr "タイムゾーン"
918
919#~ msgid "Continent"
920#~ msgstr "大陸"
921
922#~ msgid "City"
923#~ msgstr "町"
924
925#~ msgid "Disable session cookie hijacking protection"
926#~ msgstr "不正ログイン防止のためのセッションクッキーを無効化"
927
928#~ msgid ""
929#~ "Check this if you get disconnected or if your IP address changes often"
930#~ msgstr ""
931#~ "あなたが切断されたり、IPアドレスが頻繁に変わる環境下であるならチェックを入"
932#~ "れてください"
933
934#~ msgid "Private links by default"
935#~ msgstr "既定でプライベートリンク"
936
937#~ msgid "All new links are private by default"
938#~ msgstr "すべての新規リンクをプライベートで作成"
939
940#~ msgid "RSS direct links"
941#~ msgstr "RSS 直リンク"
942
943#~ msgid "Check this to use direct URL instead of permalink in feeds"
944#~ msgstr "フィードでパーマリンクの代わりに直リンクを使う"
945
946#~ msgid "Hide public links"
947#~ msgstr "公開リンクを隠す"
948
949#~ msgid "Do not show any links if the user is not logged in"
950#~ msgstr "ログインしていないユーザーには何のリンクも表示しない"
951
952#~ msgid "Check updates"
953#~ msgstr "更新を確認"
954
955#~ msgid "Notify me when a new release is ready"
956#~ msgstr "新しいバージョンがリリースされたときに通知"
957
958#~ msgid "Enable REST API"
959#~ msgstr "REST API を有効化"
960
961#~ msgid "Allow third party software to use Shaarli such as mobile application"
962#~ msgstr ""
963#~ "モバイルアプリといったサードパーティーのソフトウェアにShaarliを使用するこ"
964#~ "とを許可"
965
966#~ msgid "API secret"
967#~ msgstr "API シークレット"
968
969#~ msgid "Save"
970#~ msgstr "保存"
971
972#~ msgid "The Daily Shaarli"
973#~ msgstr "デイリーSharli"
974
975#~ msgid "1 RSS entry per day"
976#~ msgstr "各日1つずつのRSS項目"
977
978#~ msgid "Previous day"
979#~ msgstr "前日"
980
981#~ msgid "All links of one day in a single page."
982#~ msgstr "1日に作成されたすべてのリンクです。"
983
984#~ msgid "Next day"
985#~ msgstr "翌日"
986
987#~ msgid "Created:"
988#~ msgstr "作成:"
989
990#~ msgid "URL"
991#~ msgstr "URL"
992
993#~ msgid "Title"
994#~ msgstr "タイトル"
995
996#~ msgid "Description"
997#~ msgstr "説明"
998
999#~ msgid "Tags"
1000#~ msgstr "タグ"
1001
1002#~ msgid "Private"
1003#~ msgstr "プライベート"
1004
1005#~ msgid "Apply Changes"
1006#~ msgstr "変更を適用"
1007
1008#~ msgid "Export Database"
1009#~ msgstr "データベースをエクスポート"
1010
1011#~ msgid "Selection"
1012#~ msgstr "選択済み"
1013
1014#~ msgid "All"
1015#~ msgstr "すべて"
1016
1017#~ msgid "Public"
1018#~ msgstr "公開"
1019
1020#~ msgid "Prepend note permalinks with this Shaarli instance's URL"
1021#~ msgstr ""
1022#~ "この Shaarli のインスタンスのURL にノートへのパーマリンクを付け加える"
1023
1024#~ msgid "Useful to import bookmarks in a web browser"
1025#~ msgstr "ウェブブラウザーのリンクをインポートするのに有効です"
1026
1027#~ msgid "Import Database"
1028#~ msgstr "データベースをインポート"
1029
1030#~ msgid "Maximum size allowed:"
1031#~ msgstr "最大サイズ:"
1032
1033#~ msgid "Visibility"
1034#~ msgstr "可視性"
1035
1036#~ msgid "Use values from the imported file, default to public"
1037#~ msgstr "インポート元のファイルの値を使用 (既定は公開リンクとなります)"
1038
1039#~ msgid "Import all bookmarks as public"
1040#~ msgstr "すべてのブックマーク項目を公開リンクとしてインポート"
1041
1042#~ msgid "Overwrite existing bookmarks"
1043#~ msgstr "既に存在しているブックマークを上書き"
1044
1045#~ msgid "Duplicates based on URL"
1046#~ msgstr "URL による重複"
1047
1048#~ msgid "Add default tags"
1049#~ msgstr "既定のタグを追加"
1050
1051#~ msgid "Install Shaarli"
1052#~ msgstr "Shaarli をインストール"
1053
1054#~ msgid ""
1055#~ "It looks like it's the first time you run Shaarli. Please configure it."
1056#~ msgstr "どうやら Shaarli を初めて起動しているようです。設定してください。"
1057
1058#~ msgid "Username"
1059#~ msgstr "ユーザー名"
1060
1061#~ msgid "Password"
1062#~ msgstr "パスワード"
1063
1064#~ msgid "Shaarli title"
1065#~ msgstr "Shaarli のタイトル"
1066
1067#~ msgid "My links"
1068#~ msgstr "自分のリンク"
1069
1070#~ msgid "Install"
1071#~ msgstr "インストール"
1072
1073#~ msgid "shaare"
1074#~ msgid_plural "shaares"
1075#~ msgstr[0] "共有"
1076#~ msgstr[1] "共有"
1077
1078#~ msgid "private link"
1079#~ msgid_plural "private links"
1080#~ msgstr[0] "プライベートリンク"
1081#~ msgstr[1] "プライベートリンク"
1082
1083#~ msgid "Search text"
1084#~ msgstr "文字列で検索"
1085
1086#~ msgid "Filter by tag"
1087#~ msgstr "タグによって分類"
1088
1089#~ msgid "Nothing found."
1090#~ msgstr "何も見つかりませんでした。"
1091
1092#~ msgid "%s result"
1093#~ msgid_plural "%s results"
1094#~ msgstr[0] "%s 件の結果"
1095#~ msgstr[1] "%s 件の結果"
1096
1097#~ msgid "for"
1098#~ msgstr "for"
1099
1100#~ msgid "tagged"
1101#~ msgstr "タグ付けされた"
1102
1103#~ msgid "Remove tag"
1104#~ msgstr "タグを削除"
1105
1106#~ msgid "with status"
1107#~ msgstr "with status"
1108
1109#~ msgid "without any tag"
1110#~ msgstr "タグなし"
1111
1112#~ msgid "Fold"
1113#~ msgstr "畳む"
1114
1115#~ msgid "Edited: "
1116#~ msgstr "編集済み: "
1117
1118#~ msgid "permalink"
1119#~ msgstr "パーマリンク"
1120
1121#~ msgid "Add tag"
1122#~ msgstr "タグを追加"
1123
1124#~ msgid "Filters"
1125#~ msgstr "分類"
1126
1127#~ msgid "Only display private links"
1128#~ msgstr "プライベートリンクのみを表示"
1129
1130#~ msgid "Only display public links"
1131#~ msgstr "公開リンクのみを表示"
1132
1133#~ msgid "Filter untagged links"
1134#~ msgstr "タグ付けされていないリンクで分類"
1135
1136#~ msgid "Fold all"
1137#~ msgstr "すべて畳む"
1138
1139#~ msgid "Links per page"
1140#~ msgstr "各ページをリンク"
1141
1142#~ msgid "Remember me"
1143#~ msgstr "パスワードを保存"
1144
1145#~ msgid "by the Shaarli community"
1146#~ msgstr "by Shaarli コミュニティ"
1147
1148#~ msgid "Documentation"
1149#~ msgstr "ドキュメント"
1150
1151#~ msgid "Expand"
1152#~ msgstr "展開する"
1153
1154#~ msgid "Expand all"
1155#~ msgstr "すべて展開する"
1156
1157#~ msgid "Are you sure you want to delete this link?"
1158#~ msgstr "本当にこのリンクを削除しますか?"
1159
1160#~ msgid "RSS Feed"
1161#~ msgstr "RSS フィード"
1162
1163#~ msgid "Logout"
1164#~ msgstr "ログアウト"
1165
1166#~ msgid "is available"
1167#~ msgstr "が利用可能"
1168
1169#~ msgid "Error"
1170#~ msgstr "エラー"
1171
1172#~ msgid "Picture Wall"
1173#~ msgstr "ピクチャーウォール"
1174
1175#~ msgid "pics"
1176#~ msgstr "画像"
1177
1178#~ msgid "You need to enable Javascript to change plugin loading order."
1179#~ msgstr ""
1180#~ "プラグインを読み込む順番を変更するには、Javascriptを有効にする必要がありま"
1181#~ "す。"
1182
1183#~ msgid "Enabled Plugins"
1184#~ msgstr "有効なプラグイン"
1185
1186#~ msgid "No plugin enabled."
1187#~ msgstr "有効なプラグインはありません。"
1188
1189#~ msgid "Disable"
1190#~ msgstr "無効化"
1191
1192#~ msgid "Name"
1193#~ msgstr "名前"
1194
1195#~ msgid "Order"
1196#~ msgstr "順序"
1197
1198#~ msgid "Disabled Plugins"
1199#~ msgstr "無効なプラグイン"
1200
1201#~ msgid "No plugin disabled."
1202#~ msgstr "無効なプラグインはありません。"
1203
1204#~ msgid "Enable"
1205#~ msgstr "有効化"
1206
1207#~ msgid "More plugins available"
1208#~ msgstr "さらに利用できるプラグインがあります"
1209
1210#~ msgid "in the documentation"
1211#~ msgstr "ドキュメント内"
1212
1213#~ msgid "No parameter available."
1214#~ msgstr "利用可能な設定項目はありません。"
1215
1216#~ msgid "tags"
1217#~ msgstr "タグ"
1218
1219#~ msgid "List all links with those tags"
1220#~ msgstr "このタグが付いているリンクをリスト化する"
1221
1222#~ msgid "Sort by:"
1223#~ msgstr "分類:"
1224
1225#~ msgid "Cloud"
1226#~ msgstr "クラウド"
1227
1228#~ msgid "Most used"
1229#~ msgstr "もっとも使われた"
1230
1231#~ msgid "Alphabetical"
1232#~ msgstr "アルファベット順"
1233
1234#~ msgid "Settings"
1235#~ msgstr "設定"
1236
1237#~ msgid "Change Shaarli settings: title, timezone, etc."
1238#~ msgstr "Shaarli の設定を変更: タイトル、タイムゾーンなど。"
1239
1240#~ msgid "Configure your Shaarli"
1241#~ msgstr "あなたの Shaarli を設定"
1242
1243#~ msgid "Enable, disable and configure plugins"
1244#~ msgstr "プラグインを有効化、無効化、設定する"
1245
1246#~ msgid "Change your password"
1247#~ msgstr "パスワードを変更"
1248
1249#~ msgid "Rename or delete a tag in all links"
1250#~ msgstr "すべてのリンクのタグの名前を変更する、または削除する"
1251
1252#~ msgid ""
1253#~ "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1254#~ "delicious...)"
1255#~ msgstr ""
1256#~ "Netscape HTML 形式のブックマークをインポートする (Firefox、Chrome、Operaと"
1257#~ "いったブラウザーが含まれます)"
1258
1259#~ msgid "Import links"
1260#~ msgstr "リンクをインポート"
1261
1262#~ msgid ""
1263#~ "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1264#~ "Opera, delicious...)"
1265#~ msgstr ""
1266#~ "Netscape HTML 形式のブックマークをエクスポートする (Firefox、Chrome、Opera"
1267#~ "といったブラウザーが含まれます)"
1268
1269#~ msgid "Export database"
1270#~ msgstr "リンクをエクスポート"
1271
1272#~ msgid ""
1273#~ "Drag one of these button to your bookmarks toolbar or right-click it and "
1274#~ "\"Bookmark This Link\""
1275#~ msgstr ""
1276#~ "これらのボタンのうち1つををブックマークバーにドラッグするか、右クリックし"
1277#~ "て「このリンクをブックマークに追加」してください"
1278
1279#~ msgid "then click on the bookmarklet in any page you want to share."
1280#~ msgstr "共有したいページでブックマークレットをクリックしてください。"
1281
1282#~ msgid ""
1283#~ "Drag this link to your bookmarks toolbar or right-click it and Bookmark "
1284#~ "This Link"
1285#~ msgstr ""
1286#~ "このリンクをブックマークバーにドラッグするか、右クリックして「このリンクを"
1287#~ "ブックマークに追加」してください"
1288
1289#~ msgid "then click ✚Shaare link button in any page you want to share"
1290#~ msgstr ""
1291#~ "✚リンクを共有 ボタンをクリックすることで、どこでもリンクを共有できます"
1292
1293#~ msgid "The selected text is too long, it will be truncated."
1294#~ msgstr "選択された文字列は長すぎるので、一部が切り捨てられます。"
1295
1296#~ msgid "Shaare link"
1297#~ msgstr "共有リンク"
1298
1299#~ msgid ""
1300#~ "Then click ✚Add Note button anytime to start composing a private Note "
1301#~ "(text post) to your Shaarli"
1302#~ msgstr ""
1303#~ "✚ノートを追加 ボタンをクリックすることで、いつでもプライベートノート(テキ"
1304#~ "スト形式)をShaarli上に作成できます"
1305
1306#~ msgid "Add Note"
1307#~ msgstr "ノートを追加"
1308
1309#~ msgid ""
1310#~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1311#~ "functionality."
1312#~ msgstr ""
1313#~ "この機能を使用するには、<strong>HTTPS</strong> 経由でShaarliに接続してくだ"
1314#~ "さい。"
1315
1316#~ msgid "Add to"
1317#~ msgstr "次に追加:"
1318
1319#~ msgid "3rd party"
1320#~ msgstr "サードパーティー"
1321
1322#~ msgid "Plugin"
1323#~ msgstr "プラグイン"
1324
1325#~ msgid "plugin"
1326#~ msgstr "プラグイン"
1327
1328#~ msgid ""
1329#~ "Drag this link to your bookmarks toolbar, or right-click it and choose "
1330#~ "Bookmark This Link"
1331#~ msgstr ""
1332#~ "このリンクをブックマークバーにドラッグするか、右クリックして「このリンクを"
1333#~ "ブックマークに追加」してください"
diff --git a/index.php b/index.php
index b53b16fe..4b5602ac 100644
--- a/index.php
+++ b/index.php
@@ -12,141 +12,62 @@
12 * Licence: http://www.opensource.org/licenses/zlib-license.php 12 * Licence: http://www.opensource.org/licenses/zlib-license.php
13 */ 13 */
14 14
15// Set 'UTC' as the default timezone if it is not defined in php.ini
16// See http://php.net/manual/en/datetime.configuration.php#ini.date.timezone
17if (date_default_timezone_get() == '') {
18 date_default_timezone_set('UTC');
19}
20
21/*
22 * PHP configuration
23 */
24
25// http://server.com/x/shaarli --> /shaarli/
26define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0)));
27
28// High execution time in case of problematic imports/exports.
29ini_set('max_input_time', '60');
30
31// Try to set max upload file size and read
32ini_set('memory_limit', '128M');
33ini_set('post_max_size', '16M');
34ini_set('upload_max_filesize', '16M');
35
36// See all error except warnings
37error_reporting(E_ALL^E_WARNING);
38
39// 3rd-party libraries
40if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
41 header('Content-Type: text/plain; charset=utf-8');
42 echo "Error: missing Composer configuration\n\n"
43 ."If you installed Shaarli through Git or using the development branch,\n"
44 ."please refer to the installation documentation to install PHP"
45 ." dependencies using Composer:\n"
46 ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
47 ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
48 exit;
49}
50require_once 'inc/rain.tpl.class.php'; 15require_once 'inc/rain.tpl.class.php';
51require_once __DIR__ . '/vendor/autoload.php'; 16require_once __DIR__ . '/vendor/autoload.php';
52 17
53// Shaarli library 18// Shaarli library
54require_once 'application/bookmark/LinkUtils.php'; 19require_once 'application/bookmark/LinkUtils.php';
55require_once 'application/config/ConfigPlugin.php'; 20require_once 'application/config/ConfigPlugin.php';
56require_once 'application/feed/Cache.php';
57require_once 'application/http/HttpUtils.php'; 21require_once 'application/http/HttpUtils.php';
58require_once 'application/http/UrlUtils.php'; 22require_once 'application/http/UrlUtils.php';
59require_once 'application/updater/UpdaterUtils.php';
60require_once 'application/FileUtils.php';
61require_once 'application/TimeZone.php'; 23require_once 'application/TimeZone.php';
62require_once 'application/Utils.php'; 24require_once 'application/Utils.php';
63 25
64use Shaarli\ApplicationUtils; 26require_once __DIR__ . '/init.php';
65use Shaarli\Bookmark\Bookmark; 27
66use Shaarli\Bookmark\BookmarkFileService; 28use Katzgrau\KLogger\Logger;
67use Shaarli\Bookmark\BookmarkFilter; 29use Psr\Log\LogLevel;
68use Shaarli\Bookmark\BookmarkServiceInterface;
69use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
70use Shaarli\Config\ConfigManager; 30use Shaarli\Config\ConfigManager;
71use Shaarli\Container\ContainerBuilder; 31use Shaarli\Container\ContainerBuilder;
72use Shaarli\Feed\CachedPage;
73use Shaarli\Feed\FeedBuilder;
74use Shaarli\Formatter\BookmarkMarkdownFormatter;
75use Shaarli\Formatter\FormatterFactory;
76use Shaarli\History;
77use Shaarli\Languages; 32use Shaarli\Languages;
78use Shaarli\Netscape\NetscapeBookmarkUtils; 33use Shaarli\Security\BanManager;
79use Shaarli\Plugin\PluginManager; 34use Shaarli\Security\CookieManager;
80use Shaarli\Render\PageBuilder;
81use Shaarli\Render\ThemeUtils;
82use Shaarli\Router;
83use Shaarli\Security\LoginManager; 35use Shaarli\Security\LoginManager;
84use Shaarli\Security\SessionManager; 36use Shaarli\Security\SessionManager;
85use Shaarli\Thumbnailer;
86use Shaarli\Updater\Updater;
87use Shaarli\Updater\UpdaterUtils;
88use Slim\App; 37use Slim\App;
89 38
90// Ensure the PHP version is supported
91try {
92 ApplicationUtils::checkPHPVersion('7.1', PHP_VERSION);
93} catch (Exception $exc) {
94 header('Content-Type: text/plain; charset=utf-8');
95 echo $exc->getMessage();
96 exit;
97}
98
99define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
100
101// Force cookie path (but do not change lifetime)
102$cookie = session_get_cookie_params();
103$cookiedir = '';
104if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
105 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
106}
107// Set default cookie expiration and path.
108session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
109// Set session parameters on server side.
110// Use cookies to store session.
111ini_set('session.use_cookies', 1);
112// Force cookies for session (phpsessionID forbidden in URL).
113ini_set('session.use_only_cookies', 1);
114// Prevent PHP form using sessionID in URL if cookies are disabled.
115ini_set('session.use_trans_sid', false);
116
117session_name('shaarli');
118// Start session if needed (Some server auto-start sessions).
119if (session_status() == PHP_SESSION_NONE) {
120 session_start();
121}
122
123// Regenerate session ID if invalid or not defined in cookie.
124if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
125 session_regenerate_id(true);
126 $_COOKIE['shaarli'] = session_id();
127}
128
129$conf = new ConfigManager(); 39$conf = new ConfigManager();
130 40
41// Manually override root URL for complex server configurations
42define('SHAARLI_ROOT_URL', $conf->get('general.root_url', null));
43
131// In dev mode, throw exception on any warning 44// In dev mode, throw exception on any warning
132if ($conf->get('dev.debug', false)) { 45if ($conf->get('dev.debug', false)) {
133 // See all errors (for debugging only) 46 // See all errors (for debugging only)
134 error_reporting(-1); 47 error_reporting(-1);
135 48
136 set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) { 49 set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) {
137 throw new ErrorException($errstr, 0, $errno, $errfile, $errline); 50 throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
138 }); 51 });
139} 52}
140 53
141$sessionManager = new SessionManager($_SESSION, $conf); 54$logger = new Logger(
142$loginManager = new LoginManager($conf, $sessionManager); 55 dirname($conf->get('resource.log')),
56 !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
57 ['filename' => basename($conf->get('resource.log'))]
58);
59$sessionManager = new SessionManager($_SESSION, $conf, session_save_path());
60$sessionManager->initialize();
61$cookieManager = new CookieManager($_COOKIE);
62$banManager = new BanManager(
63 $conf->get('security.trusted_proxies', []),
64 $conf->get('security.ban_after'),
65 $conf->get('security.ban_duration'),
66 $conf->get('resource.ban_file', 'data/ipbans.php'),
67 $logger
68);
69$loginManager = new LoginManager($conf, $sessionManager, $cookieManager, $banManager, $logger);
143$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); 70$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
144$clientIpId = client_ip_id($_SERVER);
145
146// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
147if (! defined('LC_MESSAGES')) {
148 define('LC_MESSAGES', LC_COLLATE);
149}
150 71
151// Sniff browser language and set date format accordingly. 72// Sniff browser language and set date format accordingly.
152if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { 73if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
@@ -157,1773 +78,80 @@ new Languages(setlocale(LC_MESSAGES, 0), $conf);
157 78
158$conf->setEmpty('general.timezone', date_default_timezone_get()); 79$conf->setEmpty('general.timezone', date_default_timezone_get());
159$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER))); 80$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER)));
81
160RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory 82RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
161RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory 83RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
162 84
163$pluginManager = new PluginManager($conf);
164$pluginManager->load($conf->get('general.enabled_plugins'));
165
166date_default_timezone_set($conf->get('general.timezone', 'UTC')); 85date_default_timezone_set($conf->get('general.timezone', 'UTC'));
167 86
168ob_start(); // Output buffering for the page cache. 87$loginManager->checkLoginState(client_ip_id($_SERVER));
169
170// Prevent caching on client side or proxy: (yes, it's ugly)
171header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
172header("Cache-Control: no-store, no-cache, must-revalidate");
173header("Cache-Control: post-check=0, pre-check=0", false);
174header("Pragma: no-cache");
175
176if (! is_file($conf->getConfigFileExt())) {
177 // Ensure Shaarli has proper access to its resources
178 $errors = ApplicationUtils::checkResourcePermissions($conf);
179
180 if ($errors != array()) {
181 $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
182
183 foreach ($errors as $error) {
184 $message .= '<li>'.$error.'</li>';
185 }
186 $message .= '</ul>';
187
188 header('Content-Type: text/html; charset=utf-8');
189 echo $message;
190 exit;
191 }
192
193 // Display the installation form if no existing config is found
194 install($conf, $sessionManager, $loginManager);
195}
196
197$loginManager->checkLoginState($_COOKIE, $clientIpId);
198
199/**
200 * Adapter function to ensure compatibility with third-party templates
201 *
202 * @see https://github.com/shaarli/Shaarli/pull/1086
203 *
204 * @return bool true when the user is logged in, false otherwise
205 */
206function isLoggedIn()
207{
208 global $loginManager;
209 return $loginManager->isLoggedIn();
210}
211
212
213// ------------------------------------------------------------------------------------------
214// Process login form: Check if login/password is correct.
215if (isset($_POST['login'])) {
216 if (! $loginManager->canLogin($_SERVER)) {
217 die(t('I said: NO. You are banned for the moment. Go away.'));
218 }
219 if (isset($_POST['password'])
220 && $sessionManager->checkToken($_POST['token'])
221 && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
222 ) {
223 $loginManager->handleSuccessfulLogin($_SERVER);
224
225 $cookiedir = '';
226 if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
227 // Note: Never forget the trailing slash on the cookie path!
228 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
229 }
230
231 if (!empty($_POST['longlastingsession'])) {
232 // Keep the session cookie even after the browser closes
233 $sessionManager->setStaySignedIn(true);
234 $expirationTime = $sessionManager->extendSession();
235
236 setcookie(
237 $loginManager::$STAY_SIGNED_IN_COOKIE,
238 $loginManager->getStaySignedInToken(),
239 $expirationTime,
240 WEB_PATH
241 );
242 } else {
243 // Standard session expiration (=when browser closes)
244 $expirationTime = 0;
245 }
246
247 // Send cookie with the new expiration date to the browser
248 session_destroy();
249 session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
250 session_start();
251 session_regenerate_id(true);
252
253 // Optional redirect after login:
254 if (isset($_GET['post'])) {
255 $uri = './?post='. urlencode($_GET['post']);
256 foreach (array('description', 'source', 'title', 'tags') as $param) {
257 if (!empty($_GET[$param])) {
258 $uri .= '&'.$param.'='.urlencode($_GET[$param]);
259 }
260 }
261 header('Location: '. $uri);
262 exit;
263 }
264
265 if (isset($_GET['edit_link'])) {
266 header('Location: ./?edit_link='. escape($_GET['edit_link']));
267 exit;
268 }
269
270 if (isset($_POST['returnurl'])) {
271 // Prevent loops over login screen.
272 if (strpos($_POST['returnurl'], '/login') === false) {
273 header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
274 exit;
275 }
276 }
277 header('Location: ./?');
278 exit;
279 } else {
280 $loginManager->handleFailedLogin($_SERVER);
281 $redir = '?username='. urlencode($_POST['login']);
282 if (isset($_GET['post'])) {
283 $redir .= '&post=' . urlencode($_GET['post']);
284 foreach (array('description', 'source', 'title', 'tags') as $param) {
285 if (!empty($_GET[$param])) {
286 $redir .= '&' . $param . '=' . urlencode($_GET[$param]);
287 }
288 }
289 }
290 // Redirect to login screen.
291 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'./login'.$redir.'\';</script>';
292 exit;
293 }
294}
295
296// ------------------------------------------------------------------------------------------
297// Token management for XSRF protection
298// Token should be used in any form which acts on data (create,update,delete,import...).
299if (!isset($_SESSION['tokens'])) {
300 $_SESSION['tokens']=array(); // Token are attached to the session.
301}
302
303/**
304 * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
305 * Gives the last 7 days (which have bookmarks).
306 * This RSS feed cannot be filtered.
307 *
308 * @param BookmarkServiceInterface $bookmarkService
309 * @param ConfigManager $conf Configuration Manager instance
310 * @param LoginManager $loginManager LoginManager instance
311 */
312function showDailyRSS($bookmarkService, $conf, $loginManager)
313{
314 // Cache system
315 $query = $_SERVER['QUERY_STRING'];
316 $cache = new CachedPage(
317 $conf->get('config.PAGE_CACHE'),
318 page_url($_SERVER),
319 startsWith($query, 'do=dailyrss') && !$loginManager->isLoggedIn()
320 );
321 $cached = $cache->cachedVersion();
322 if (!empty($cached)) {
323 echo $cached;
324 exit;
325 }
326
327 /* Some Shaarlies may have very few bookmarks, so we need to look
328 back in time until we have enough days ($nb_of_days).
329 */
330 $nb_of_days = 7; // We take 7 days.
331 $today = date('Ymd');
332 $days = array();
333
334 foreach ($bookmarkService->search() as $bookmark) {
335 $day = $bookmark->getCreated()->format('Ymd'); // Extract day (without time)
336 if (strcmp($day, $today) < 0) {
337 if (empty($days[$day])) {
338 $days[$day] = array();
339 }
340 $days[$day][] = $bookmark;
341 }
342
343 if (count($days) > $nb_of_days) {
344 break; // Have we collected enough days?
345 }
346 }
347
348 // Build the RSS feed.
349 header('Content-Type: application/rss+xml; charset=utf-8');
350 $pageaddr = escape(index_url($_SERVER));
351 echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">';
352 echo '<channel>';
353 echo '<title>Daily - '. $conf->get('general.title') . '</title>';
354 echo '<link>'. $pageaddr .'</link>';
355 echo '<description>Daily shared bookmarks</description>';
356 echo '<language>en-en</language>';
357 echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
358
359 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
360 $formatter = $factory->getFormatter();
361 $formatter->addContextData('index_url', index_url($_SERVER));
362 // For each day.
363 /** @var Bookmark[] $bookmarks */
364 foreach ($days as $day => $bookmarks) {
365 $formattedBookmarks = [];
366 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
367 $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page.
368
369 // We pre-format some fields for proper output.
370 foreach ($bookmarks as $key => $bookmark) {
371 $formattedBookmarks[$key] = $formatter->format($bookmark);
372 // This page is a bit specific, we need raw description to calculate the length
373 $formattedBookmarks[$key]['formatedDescription'] = $formattedBookmarks[$key]['description'];
374 $formattedBookmarks[$key]['description'] = $bookmark->getDescription();
375
376 if ($bookmark->isNote()) {
377 $link['url'] = index_url($_SERVER) . $bookmark->getUrl(); // make permalink URL absolute
378 }
379 }
380
381 // Then build the HTML for this day:
382 $tpl = new RainTPL();
383 $tpl->assign('title', $conf->get('general.title'));
384 $tpl->assign('daydate', $dayDate->getTimestamp());
385 $tpl->assign('absurl', $absurl);
386 $tpl->assign('links', $formattedBookmarks);
387 $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
388 $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
389 $tpl->assign('index_url', $pageaddr);
390 $html = $tpl->draw('dailyrss', true);
391
392 echo $html . PHP_EOL;
393 }
394 echo '</channel></rss><!-- Cached version of '. escape(page_url($_SERVER)) .' -->';
395
396 $cache->cache(ob_get_contents());
397 ob_end_flush();
398 exit;
399}
400
401/**
402 * Show the 'Daily' page.
403 *
404 * @param PageBuilder $pageBuilder Template engine wrapper.
405 * @param BookmarkServiceInterface $bookmarkService instance.
406 * @param ConfigManager $conf Configuration Manager instance.
407 * @param PluginManager $pluginManager Plugin Manager instance.
408 * @param LoginManager $loginManager Login Manager instance
409 */
410function showDaily($pageBuilder, $bookmarkService, $conf, $pluginManager, $loginManager)
411{
412 if (isset($_GET['day'])) {
413 $day = $_GET['day'];
414 if ($day === date('Ymd', strtotime('now'))) {
415 $pageBuilder->assign('dayDesc', t('Today'));
416 } elseif ($day === date('Ymd', strtotime('-1 days'))) {
417 $pageBuilder->assign('dayDesc', t('Yesterday'));
418 }
419 } else {
420 $day = date('Ymd', strtotime('now')); // Today, in format YYYYMMDD.
421 $pageBuilder->assign('dayDesc', t('Today'));
422 }
423
424 $days = $bookmarkService->days();
425 $i = array_search($day, $days);
426 if ($i === false && count($days)) {
427 // no bookmarks for day, but at least one day with bookmarks
428 $i = count($days) - 1;
429 $day = $days[$i];
430 }
431 $previousday = '';
432 $nextday = '';
433
434 if ($i !== false) {
435 if ($i >= 1) {
436 $previousday = $days[$i - 1];
437 }
438 if ($i < count($days) - 1) {
439 $nextday = $days[$i + 1];
440 }
441 }
442 try {
443 $linksToDisplay = $bookmarkService->filterDay($day);
444 } catch (Exception $exc) {
445 error_log($exc);
446 $linksToDisplay = [];
447 }
448
449 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
450 $formatter = $factory->getFormatter();
451 // We pre-format some fields for proper output.
452 foreach ($linksToDisplay as $key => $bookmark) {
453 $linksToDisplay[$key] = $formatter->format($bookmark);
454 // This page is a bit specific, we need raw description to calculate the length
455 $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
456 $linksToDisplay[$key]['description'] = $bookmark->getDescription();
457 }
458
459 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
460 $data = array(
461 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
462 'linksToDisplay' => $linksToDisplay,
463 'day' => $dayDate->getTimestamp(),
464 'dayDate' => $dayDate,
465 'previousday' => $previousday,
466 'nextday' => $nextday,
467 );
468
469 /* Hook is called before column construction so that plugins don't have
470 to deal with columns. */
471 $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
472
473 /* We need to spread the articles on 3 columns.
474 I did not want to use a JavaScript lib like http://masonry.desandro.com/
475 so I manually spread entries with a simple method: I roughly evaluate the
476 height of a div according to title and description length.
477 */
478 $columns = array(array(), array(), array()); // Entries to display, for each column.
479 $fill = array(0, 0, 0); // Rough estimate of columns fill.
480 foreach ($data['linksToDisplay'] as $key => $bookmark) {
481 // Roughly estimate length of entry (by counting characters)
482 // Title: 30 chars = 1 line. 1 line is 30 pixels height.
483 // Description: 836 characters gives roughly 342 pixel height.
484 // This is not perfect, but it's usually OK.
485 $length = strlen($bookmark['title']) + (342 * strlen($bookmark['description'])) / 836;
486 if (! empty($bookmark['thumbnail'])) {
487 $length += 100; // 1 thumbnails roughly takes 100 pixels height.
488 }
489 // Then put in column which is the less filled:
490 $smallest = min($fill); // find smallest value in array.
491 $index = array_search($smallest, $fill); // find index of this smallest value.
492 array_push($columns[$index], $bookmark); // Put entry in this column.
493 $fill[$index] += $length;
494 }
495
496 $data['cols'] = $columns;
497
498 foreach ($data as $key => $value) {
499 $pageBuilder->assign($key, $value);
500 }
501
502 $pageBuilder->assign('pagetitle', t('Daily') .' - '. $conf->get('general.title', 'Shaarli'));
503 $pageBuilder->renderPage('daily');
504 exit;
505}
506
507/**
508 * Renders the linklist
509 *
510 * @param pageBuilder $PAGE pageBuilder instance.
511 * @param BookmarkServiceInterface $linkDb instance.
512 * @param ConfigManager $conf Configuration Manager instance.
513 * @param PluginManager $pluginManager Plugin Manager instance.
514 */
515function showLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
516{
517 buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager);
518 $PAGE->renderPage('linklist');
519}
520
521/**
522 * Render HTML page (according to URL parameters and user rights)
523 *
524 * @param ConfigManager $conf Configuration Manager instance.
525 * @param PluginManager $pluginManager Plugin Manager instance,
526 * @param BookmarkServiceInterface $bookmarkService
527 * @param History $history instance
528 * @param SessionManager $sessionManager SessionManager instance
529 * @param LoginManager $loginManager LoginManager instance
530 */
531function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionManager, $loginManager)
532{
533 $updater = new Updater(
534 UpdaterUtils::read_updates_file($conf->get('resource.updates')),
535 $bookmarkService,
536 $conf,
537 $loginManager->isLoggedIn()
538 );
539 try {
540 $newUpdates = $updater->update();
541 if (! empty($newUpdates)) {
542 UpdaterUtils::write_updates_file(
543 $conf->get('resource.updates'),
544 $updater->getDoneUpdates()
545 );
546 }
547 } catch (Exception $e) {
548 die($e->getMessage());
549 }
550
551 $PAGE = new PageBuilder($conf, $_SESSION, $bookmarkService, $sessionManager->generateToken(), $loginManager->isLoggedIn());
552 $PAGE->assign('linkcount', $bookmarkService->count(BookmarkFilter::$ALL));
553 $PAGE->assign('privateLinkcount', $bookmarkService->count(BookmarkFilter::$PRIVATE));
554 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
555
556 // Determine which page will be rendered.
557 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
558 $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
559
560 if (// if the user isn't logged in
561 !$loginManager->isLoggedIn() &&
562 // and Shaarli doesn't have public content...
563 $conf->get('privacy.hide_public_links') &&
564 // and is configured to enforce the login
565 $conf->get('privacy.force_login') &&
566 // and the current page isn't already the login page
567 $targetPage !== Router::$PAGE_LOGIN &&
568 // and the user is not requesting a feed (which would lead to a different content-type as expected)
569 $targetPage !== Router::$PAGE_FEED_ATOM &&
570 $targetPage !== Router::$PAGE_FEED_RSS
571 ) {
572 // force current page to be the login page
573 $targetPage = Router::$PAGE_LOGIN;
574 }
575
576 // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
577 // Then assign generated data to RainTPL.
578 $common_hooks = array(
579 'includes',
580 'header',
581 'footer',
582 );
583
584 foreach ($common_hooks as $name) {
585 $plugin_data = array();
586 $pluginManager->executeHooks(
587 'render_' . $name,
588 $plugin_data,
589 array(
590 'target' => $targetPage,
591 'loggedin' => $loginManager->isLoggedIn()
592 )
593 );
594 $PAGE->assign('plugins_' . $name, $plugin_data);
595 }
596
597 // -------- Display login form.
598 if ($targetPage == Router::$PAGE_LOGIN) {
599 header('Location: ./login');
600 exit;
601 }
602 // -------- User wants to logout.
603 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) {
604 invalidateCaches($conf->get('resource.page_cache'));
605 $sessionManager->logout();
606 setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
607 header('Location: ?');
608 exit;
609 }
610
611 // -------- Picture wall
612 if ($targetPage == Router::$PAGE_PICWALL) {
613 $PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
614 if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
615 $PAGE->assign('linksToDisplay', []);
616 $PAGE->renderPage('picwall');
617 exit;
618 }
619
620 // Optionally filter the results:
621 $links = $bookmarkService->search($_GET);
622 $linksToDisplay = [];
623
624 // Get only bookmarks which have a thumbnail.
625 // Note: we do not retrieve thumbnails here, the request is too heavy.
626 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
627 $formatter = $factory->getFormatter();
628 foreach ($links as $key => $link) {
629 if ($link->getThumbnail() !== false) {
630 $linksToDisplay[] = $formatter->format($link);
631 }
632 }
633
634 $data = [
635 'linksToDisplay' => $linksToDisplay,
636 ];
637 $pluginManager->executeHooks('render_picwall', $data, ['loggedin' => $loginManager->isLoggedIn()]);
638
639 foreach ($data as $key => $value) {
640 $PAGE->assign($key, $value);
641 }
642
643 $PAGE->renderPage('picwall');
644 exit;
645 }
646
647 // -------- Tag cloud
648 if ($targetPage == Router::$PAGE_TAGCLOUD) {
649 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
650 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
651 $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
652
653 // We sort tags alphabetically, then choose a font size according to count.
654 // First, find max value.
655 $maxcount = 0;
656 foreach ($tags as $value) {
657 $maxcount = max($maxcount, $value);
658 }
659
660 alphabetical_sort($tags, false, true);
661
662 $logMaxCount = $maxcount > 1 ? log($maxcount, 30) : 1;
663 $tagList = array();
664 foreach ($tags as $key => $value) {
665 if (in_array($key, $filteringTags)) {
666 continue;
667 }
668 // Tag font size scaling:
669 // default 15 and 30 logarithm bases affect scaling,
670 // 2.2 and 0.8 are arbitrary font sizes in em.
671 $size = log($value, 15) / $logMaxCount * 2.2 + 0.8;
672 $tagList[$key] = array(
673 'count' => $value,
674 'size' => number_format($size, 2, '.', ''),
675 );
676 }
677
678 $searchTags = implode(' ', escape($filteringTags));
679 $data = array(
680 'search_tags' => $searchTags,
681 'tags' => $tagList,
682 );
683 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
684
685 foreach ($data as $key => $value) {
686 $PAGE->assign($key, $value);
687 }
688
689 $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
690 $PAGE->assign('pagetitle', $searchTags. t('Tag cloud') .' - '. $conf->get('general.title', 'Shaarli'));
691 $PAGE->renderPage('tag.cloud');
692 exit;
693 }
694
695 // -------- Tag list
696 if ($targetPage == Router::$PAGE_TAGLIST) {
697 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
698 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
699 $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
700 foreach ($filteringTags as $tag) {
701 if (array_key_exists($tag, $tags)) {
702 unset($tags[$tag]);
703 }
704 }
705
706 if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
707 alphabetical_sort($tags, false, true);
708 }
709
710 $searchTags = implode(' ', escape($filteringTags));
711 $data = [
712 'search_tags' => $searchTags,
713 'tags' => $tags,
714 ];
715 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
716
717 foreach ($data as $key => $value) {
718 $PAGE->assign($key, $value);
719 }
720
721 $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
722 $PAGE->assign('pagetitle', $searchTags . t('Tag list') .' - '. $conf->get('general.title', 'Shaarli'));
723 $PAGE->renderPage('tag.list');
724 exit;
725 }
726
727 // Daily page.
728 if ($targetPage == Router::$PAGE_DAILY) {
729 showDaily($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
730 }
731
732 // ATOM and RSS feed.
733 if ($targetPage == Router::$PAGE_FEED_ATOM || $targetPage == Router::$PAGE_FEED_RSS) {
734 $feedType = $targetPage == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
735 header('Content-Type: application/'. $feedType .'+xml; charset=utf-8');
736
737 // Cache system
738 $query = $_SERVER['QUERY_STRING'];
739 $cache = new CachedPage(
740 $conf->get('resource.page_cache'),
741 page_url($_SERVER),
742 startsWith($query, 'do='. $targetPage) && !$loginManager->isLoggedIn()
743 );
744 $cached = $cache->cachedVersion();
745 if (!empty($cached)) {
746 echo $cached;
747 exit;
748 }
749
750 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
751 // Generate data.
752 $feedGenerator = new FeedBuilder(
753 $bookmarkService,
754 $factory->getFormatter(),
755 $feedType,
756 $_SERVER,
757 $_GET,
758 $loginManager->isLoggedIn()
759 );
760 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
761 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
762 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
763 $data = $feedGenerator->buildData();
764
765 // Process plugin hook.
766 $pluginManager->executeHooks('render_feed', $data, array(
767 'loggedin' => $loginManager->isLoggedIn(),
768 'target' => $targetPage,
769 ));
770
771 // Render the template.
772 $PAGE->assignAll($data);
773 $PAGE->renderPage('feed.'. $feedType);
774 $cache->cache(ob_get_contents());
775 ob_end_flush();
776 exit;
777 }
778
779 // Display opensearch plugin (XML)
780 if ($targetPage == Router::$PAGE_OPENSEARCH) {
781 header('Content-Type: application/xml; charset=utf-8');
782 $PAGE->assign('serverurl', index_url($_SERVER));
783 $PAGE->renderPage('opensearch');
784 exit;
785 }
786
787 // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...)
788 if (isset($_GET['addtag'])) {
789 // Get previous URL (http_referer) and add the tag to the searchtags parameters in query.
790 if (empty($_SERVER['HTTP_REFERER'])) {
791 // In case browser does not send HTTP_REFERER
792 header('Location: ?searchtags='.urlencode($_GET['addtag']));
793 exit;
794 }
795 parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
796
797 // Prevent redirection loop
798 if (isset($params['addtag'])) {
799 unset($params['addtag']);
800 }
801
802 // Check if this tag is already in the search query and ignore it if it is.
803 // Each tag is always separated by a space
804 if (isset($params['searchtags'])) {
805 $current_tags = explode(' ', $params['searchtags']);
806 } else {
807 $current_tags = array();
808 }
809 $addtag = true;
810 foreach ($current_tags as $value) {
811 if ($value === $_GET['addtag']) {
812 $addtag = false;
813 break;
814 }
815 }
816 // Append the tag if necessary
817 if (empty($params['searchtags'])) {
818 $params['searchtags'] = trim($_GET['addtag']);
819 } elseif ($addtag) {
820 $params['searchtags'] = trim($params['searchtags']).' '.trim($_GET['addtag']);
821 }
822
823 // We also remove page (keeping the same page has no sense, since the
824 // results are different)
825 unset($params['page']);
826
827 header('Location: ?'.http_build_query($params));
828 exit;
829 }
830
831 // -------- User clicks on a tag in result count: Remove the tag from the list of searched tags (searchtags=...)
832 if (isset($_GET['removetag'])) {
833 // Get previous URL (http_referer) and remove the tag from the searchtags parameters in query.
834 if (empty($_SERVER['HTTP_REFERER'])) {
835 header('Location: ?');
836 exit;
837 }
838
839 // In case browser does not send HTTP_REFERER
840 parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
841 88
842 // Prevent redirection loop 89$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager, $logger);
843 if (isset($params['removetag'])) { 90$container = $containerBuilder->build();
844 unset($params['removetag']); 91$app = new App($container);
845 }
846
847 if (isset($params['searchtags'])) {
848 $tags = explode(' ', $params['searchtags']);
849 // Remove value from array $tags.
850 $tags = array_diff($tags, array($_GET['removetag']));
851 $params['searchtags'] = implode(' ', $tags);
852
853 if (empty($params['searchtags'])) {
854 unset($params['searchtags']);
855 }
856
857 // We also remove page (keeping the same page has no sense, since
858 // the results are different)
859 unset($params['page']);
860 }
861 header('Location: ?'.http_build_query($params));
862 exit;
863 }
864
865 // -------- User wants to change the number of bookmarks per page (linksperpage=...)
866 if (isset($_GET['linksperpage'])) {
867 if (is_numeric($_GET['linksperpage'])) {
868 $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
869 }
870
871 if (! empty($_SERVER['HTTP_REFERER'])) {
872 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'));
873 } else {
874 $location = '?';
875 }
876 header('Location: '. $location);
877 exit;
878 }
879
880 // -------- User wants to see only private bookmarks (toggle)
881 if (isset($_GET['visibility'])) {
882 if ($_GET['visibility'] === 'private') {
883 // Visibility not set or not already private, set private, otherwise reset it
884 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
885 // See only private bookmarks
886 $_SESSION['visibility'] = 'private';
887 } else {
888 unset($_SESSION['visibility']);
889 }
890 } elseif ($_GET['visibility'] === 'public') {
891 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
892 // See only public bookmarks
893 $_SESSION['visibility'] = 'public';
894 } else {
895 unset($_SESSION['visibility']);
896 }
897 }
898
899 if (! empty($_SERVER['HTTP_REFERER'])) {
900 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('visibility'));
901 } else {
902 $location = '?';
903 }
904 header('Location: '. $location);
905 exit;
906 }
907
908 // -------- User wants to see only untagged bookmarks (toggle)
909 if (isset($_GET['untaggedonly'])) {
910 $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
911
912 if (! empty($_SERVER['HTTP_REFERER'])) {
913 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
914 } else {
915 $location = '?';
916 }
917 header('Location: '. $location);
918 exit;
919 }
920
921 // -------- Handle other actions allowed for non-logged in users:
922 if (!$loginManager->isLoggedIn()) {
923 // User tries to post new link but is not logged in:
924 // Show login screen, then redirect to ?post=...
925 if (isset($_GET['post'])) {
926 header( // Redirect to login page, then back to post link.
927 'Location: /login?post='.urlencode($_GET['post']).
928 (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
929 (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
930 (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
931 (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
932 );
933 exit;
934 }
935
936 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
937 if (isset($_GET['edit_link'])) {
938 header('Location: /login?edit_link='. escape($_GET['edit_link']));
939 exit;
940 }
941
942 exit; // Never remove this one! All operations below are reserved for logged in user.
943 }
944
945 // -------- All other functions are reserved for the registered user:
946
947 // -------- Display the Tools menu if requested (import/export/bookmarklet...)
948 if ($targetPage == Router::$PAGE_TOOLS) {
949 $data = [
950 'pageabsaddr' => index_url($_SERVER),
951 'sslenabled' => is_https($_SERVER),
952 ];
953 $pluginManager->executeHooks('render_tools', $data);
954
955 foreach ($data as $key => $value) {
956 $PAGE->assign($key, $value);
957 }
958
959 $PAGE->assign('pagetitle', t('Tools') .' - '. $conf->get('general.title', 'Shaarli'));
960 $PAGE->renderPage('tools');
961 exit;
962 }
963
964 // -------- User wants to change his/her password.
965 if ($targetPage == Router::$PAGE_CHANGEPASSWORD) {
966 if ($conf->get('security.open_shaarli')) {
967 die(t('You are not supposed to change a password on an Open Shaarli.'));
968 }
969
970 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) {
971 if (!$sessionManager->checkToken($_POST['token'])) {
972 die(t('Wrong token.')); // Go away!
973 }
974
975 // Make sure old password is correct.
976 $oldhash = sha1(
977 $_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')
978 );
979 if ($oldhash != $conf->get('credentials.hash')) {
980 echo '<script>alert("'
981 . t('The old password is not correct.')
982 .'");document.location=\'?do=changepasswd\';</script>';
983 exit;
984 }
985 // Save new password
986 // Salt renders rainbow-tables attacks useless.
987 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
988 $conf->set(
989 'credentials.hash',
990 sha1(
991 $_POST['setpassword']
992 . $conf->get('credentials.login')
993 . $conf->get('credentials.salt')
994 )
995 );
996 try {
997 $conf->write($loginManager->isLoggedIn());
998 } catch (Exception $e) {
999 error_log(
1000 'ERROR while writing config file after changing password.' . PHP_EOL .
1001 $e->getMessage()
1002 );
1003
1004 // TODO: do not handle exceptions/errors in JS.
1005 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
1006 exit;
1007 }
1008 echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
1009 exit;
1010 } else {
1011 // show the change password form.
1012 $PAGE->assign('pagetitle', t('Change password') .' - '. $conf->get('general.title', 'Shaarli'));
1013 $PAGE->renderPage('changepassword');
1014 exit;
1015 }
1016 }
1017
1018 // -------- User wants to change configuration
1019 if ($targetPage == Router::$PAGE_CONFIGURE) {
1020 if (!empty($_POST['title'])) {
1021 if (!$sessionManager->checkToken($_POST['token'])) {
1022 die(t('Wrong token.')); // Go away!
1023 }
1024 $tz = 'UTC';
1025 if (!empty($_POST['continent']) && !empty($_POST['city'])
1026 && isTimeZoneValid($_POST['continent'], $_POST['city'])
1027 ) {
1028 $tz = $_POST['continent'] . '/' . $_POST['city'];
1029 }
1030 $conf->set('general.timezone', $tz);
1031 $conf->set('general.title', escape($_POST['title']));
1032 $conf->set('general.header_link', escape($_POST['titleLink']));
1033 $conf->set('general.retrieve_description', !empty($_POST['retrieveDescription']));
1034 $conf->set('resource.theme', escape($_POST['theme']));
1035 $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
1036 $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
1037 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
1038 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1039 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
1040 $conf->set('api.enabled', !empty($_POST['enableApi']));
1041 $conf->set('api.secret', escape($_POST['apiSecret']));
1042 $conf->set('formatter', escape($_POST['formatter']));
1043
1044 if (! empty($_POST['language'])) {
1045 $conf->set('translation.language', escape($_POST['language']));
1046 }
1047
1048 $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
1049 if ($thumbnailsMode !== Thumbnailer::MODE_NONE
1050 && $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
1051 ) {
1052 $_SESSION['warnings'][] = t(
1053 'You have enabled or changed thumbnails mode. '
1054 .'<a href="?do=thumbs_update">Please synchronize them</a>.'
1055 );
1056 }
1057 $conf->set('thumbnails.mode', $thumbnailsMode);
1058
1059 try {
1060 $conf->write($loginManager->isLoggedIn());
1061 $history->updateSettings();
1062 invalidateCaches($conf->get('resource.page_cache'));
1063 } catch (Exception $e) {
1064 error_log(
1065 'ERROR while writing config file after configuration update.' . PHP_EOL .
1066 $e->getMessage()
1067 );
1068
1069 // TODO: do not handle exceptions/errors in JS.
1070 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
1071 exit;
1072 }
1073 echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
1074 exit;
1075 } else {
1076 // Show the configuration form.
1077 $PAGE->assign('title', $conf->get('general.title'));
1078 $PAGE->assign('theme', $conf->get('resource.theme'));
1079 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
1080 $PAGE->assign('formatter_available', ['default', 'markdown']);
1081 list($continents, $cities) = generateTimeZoneData(
1082 timezone_identifiers_list(),
1083 $conf->get('general.timezone')
1084 );
1085 $PAGE->assign('continents', $continents);
1086 $PAGE->assign('cities', $cities);
1087 $PAGE->assign('retrieve_description', $conf->get('general.retrieve_description'));
1088 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
1089 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
1090 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
1091 $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true));
1092 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
1093 $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
1094 $PAGE->assign('api_secret', $conf->get('api.secret'));
1095 $PAGE->assign('languages', Languages::getAvailableLanguages());
1096 $PAGE->assign('gd_enabled', extension_loaded('gd'));
1097 $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
1098 $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
1099 $PAGE->renderPage('configure');
1100 exit;
1101 }
1102 }
1103
1104 // -------- User wants to rename a tag or delete it
1105 if ($targetPage == Router::$PAGE_CHANGETAG) {
1106 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
1107 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
1108 $PAGE->assign('pagetitle', t('Manage tags') .' - '. $conf->get('general.title', 'Shaarli'));
1109 $PAGE->renderPage('changetag');
1110 exit;
1111 }
1112
1113 if (!$sessionManager->checkToken($_POST['token'])) {
1114 die(t('Wrong token.'));
1115 }
1116
1117 $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
1118 $fromTag = escape($_POST['fromtag']);
1119 $count = 0;
1120 $bookmarks = $bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
1121 foreach ($bookmarks as $bookmark) {
1122 if ($toTag) {
1123 $bookmark->renameTag($fromTag, $toTag);
1124 } else {
1125 $bookmark->deleteTag($fromTag);
1126 }
1127 $bookmarkService->set($bookmark, false);
1128 $history->updateLink($bookmark);
1129 $count++;
1130 }
1131 $bookmarkService->save();
1132 $delete = empty($_POST['totag']);
1133 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
1134 $alert = $delete
1135 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d bookmarks.', $count), $count)
1136 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d bookmarks.', $count), $count);
1137 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
1138 exit;
1139 }
1140
1141 // -------- User wants to add a link without using the bookmarklet: Show form.
1142 if ($targetPage == Router::$PAGE_ADDLINK) {
1143 $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli'));
1144 $PAGE->renderPage('addlink');
1145 exit;
1146 }
1147
1148 // -------- User clicked the "Save" button when editing a link: Save link to database.
1149 if (isset($_POST['save_edit'])) {
1150 // Go away!
1151 if (! $sessionManager->checkToken($_POST['token'])) {
1152 die(t('Wrong token.'));
1153 }
1154
1155 // lf_id should only be present if the link exists.
1156 $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : null;
1157 if ($id && $bookmarkService->exists($id)) {
1158 // Edit
1159 $bookmark = $bookmarkService->get($id);
1160 } else {
1161 // New link
1162 $bookmark = new Bookmark();
1163 }
1164
1165 $bookmark->setTitle($_POST['lf_title']);
1166 $bookmark->setDescription($_POST['lf_description']);
1167 $bookmark->setUrl($_POST['lf_url'], $conf->get('security.allowed_protocols'));
1168 $bookmark->setPrivate(isset($_POST['lf_private']));
1169 $bookmark->setTagsString($_POST['lf_tags']);
1170
1171 if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
1172 && ! $bookmark->isNote()
1173 ) {
1174 $thumbnailer = new Thumbnailer($conf);
1175 $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
1176 }
1177 $bookmarkService->addOrSet($bookmark, false);
1178
1179 // To preserve backward compatibility with 3rd parties, plugins still use arrays
1180 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1181 $formatter = $factory->getFormatter('raw');
1182 $data = $formatter->format($bookmark);
1183 $pluginManager->executeHooks('save_link', $data);
1184
1185 $bookmark->fromArray($data);
1186 $bookmarkService->set($bookmark);
1187
1188 // If we are called from the bookmarklet, we must close the popup:
1189 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
1190 echo '<script>self.close();</script>';
1191 exit;
1192 }
1193
1194 $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
1195 $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
1196 // Scroll to the link which has been edited.
1197 $location .= '#' . $bookmark->getShortUrl();
1198 // After saving the link, redirect to the page the user was on.
1199 header('Location: '. $location);
1200 exit;
1201 }
1202
1203 // -------- User clicked the "Delete" button when editing a link: Delete link from database.
1204 if ($targetPage == Router::$PAGE_DELETELINK) {
1205 if (! $sessionManager->checkToken($_GET['token'])) {
1206 die(t('Wrong token.'));
1207 }
1208
1209 $ids = trim($_GET['lf_linkdate']);
1210 if (strpos($ids, ' ') !== false) {
1211 // multiple, space-separated ids provided
1212 $ids = array_values(array_filter(
1213 preg_split('/\s+/', escape($ids)),
1214 function ($item) {
1215 return $item !== '';
1216 }
1217 ));
1218 } else {
1219 // only a single id provided
1220 $shortUrl = $bookmarkService->get($ids)->getShortUrl();
1221 $ids = [$ids];
1222 }
1223 // assert at least one id is given
1224 if (!count($ids)) {
1225 die('no id provided');
1226 }
1227 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1228 $formatter = $factory->getFormatter('raw');
1229 foreach ($ids as $id) {
1230 $id = (int) escape($id);
1231 $bookmark = $bookmarkService->get($id);
1232 $data = $formatter->format($bookmark);
1233 $pluginManager->executeHooks('delete_link', $data);
1234 $bookmarkService->remove($bookmark, false);
1235 }
1236 $bookmarkService->save();
1237
1238 // If we are called from the bookmarklet, we must close the popup:
1239 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
1240 echo '<script>self.close();</script>';
1241 exit;
1242 }
1243
1244 $location = '?';
1245 if (isset($_SERVER['HTTP_REFERER'])) {
1246 // Don't redirect to where we were previously if it was a permalink or an edit_link, because it would 404.
1247 $location = generateLocation(
1248 $_SERVER['HTTP_REFERER'],
1249 $_SERVER['HTTP_HOST'],
1250 ['delete_link', 'edit_link', ! empty($shortUrl) ? $shortUrl : null]
1251 );
1252 }
1253
1254 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
1255 exit;
1256 }
1257
1258 // -------- User clicked either "Set public" or "Set private" bulk operation
1259 if ($targetPage == Router::$PAGE_CHANGE_VISIBILITY) {
1260 if (! $sessionManager->checkToken($_GET['token'])) {
1261 die(t('Wrong token.'));
1262 }
1263
1264 $ids = trim($_GET['ids']);
1265 if (strpos($ids, ' ') !== false) {
1266 // multiple, space-separated ids provided
1267 $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
1268 } else {
1269 // only a single id provided
1270 $ids = [$ids];
1271 }
1272
1273 // assert at least one id is given
1274 if (!count($ids)) {
1275 die('no id provided');
1276 }
1277 // assert that the visibility is valid
1278 if (!isset($_GET['newVisibility']) || !in_array($_GET['newVisibility'], ['public', 'private'])) {
1279 die('invalid visibility');
1280 } else {
1281 $private = $_GET['newVisibility'] === 'private';
1282 }
1283 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1284 $formatter = $factory->getFormatter('raw');
1285 foreach ($ids as $id) {
1286 $id = (int) escape($id);
1287 $bookmark = $bookmarkService->get($id);
1288 $bookmark->setPrivate($private);
1289
1290 // To preserve backward compatibility with 3rd parties, plugins still use arrays
1291 $data = $formatter->format($bookmark);
1292 $pluginManager->executeHooks('save_link', $data);
1293 $bookmark->fromArray($data);
1294
1295 $bookmarkService->set($bookmark);
1296 }
1297 $bookmarkService->save();
1298
1299 $location = '?';
1300 if (isset($_SERVER['HTTP_REFERER'])) {
1301 $location = generateLocation(
1302 $_SERVER['HTTP_REFERER'],
1303 $_SERVER['HTTP_HOST']
1304 );
1305 }
1306 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
1307 exit;
1308 }
1309
1310 // -------- User clicked the "EDIT" button on a link: Display link edit form.
1311 if (isset($_GET['edit_link'])) {
1312 $id = (int) escape($_GET['edit_link']);
1313 try {
1314 $link = $bookmarkService->get($id); // Read database
1315 } catch (BookmarkNotFoundException $e) {
1316 // Link not found in database.
1317 header('Location: ?');
1318 exit;
1319 }
1320
1321 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1322 $formatter = $factory->getFormatter('raw');
1323 $formattedLink = $formatter->format($link);
1324 $tags = $bookmarkService->bookmarksCountPerTag();
1325 if ($conf->get('formatter') === 'markdown') {
1326 $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
1327 }
1328 $data = array(
1329 'link' => $formattedLink,
1330 'link_is_new' => false,
1331 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1332 'tags' => $tags,
1333 );
1334 $pluginManager->executeHooks('render_editlink', $data);
1335
1336 foreach ($data as $key => $value) {
1337 $PAGE->assign($key, $value);
1338 }
1339
1340 $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1341 $PAGE->renderPage('editlink');
1342 exit;
1343 }
1344
1345 // -------- User want to post a new link: Display link edit form.
1346 if (isset($_GET['post'])) {
1347 $url = cleanup_url($_GET['post']);
1348
1349 $link_is_new = false;
1350 // Check if URL is not already in database (in this case, we will edit the existing link)
1351 $bookmark = $bookmarkService->findByUrl($url);
1352 if (! $bookmark) {
1353 $link_is_new = true;
1354 // Get title if it was provided in URL (by the bookmarklet).
1355 $title = empty($_GET['title']) ? '' : escape($_GET['title']);
1356 // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
1357 $description = empty($_GET['description']) ? '' : escape($_GET['description']);
1358 $tags = empty($_GET['tags']) ? '' : escape($_GET['tags']);
1359 $private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0;
1360
1361 // If this is an HTTP(S) link, we try go get the page to extract
1362 // the title (otherwise we will to straight to the edit form.)
1363 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
1364 $retrieveDescription = $conf->get('general.retrieve_description');
1365 // Short timeout to keep the application responsive
1366 // The callback will fill $charset and $title with data from the downloaded page.
1367 get_http_response(
1368 $url,
1369 $conf->get('general.download_timeout', 30),
1370 $conf->get('general.download_max_size', 4194304),
1371 get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription)
1372 );
1373 if (! empty($title) && strtolower($charset) != 'utf-8') {
1374 $title = mb_convert_encoding($title, 'utf-8', $charset);
1375 }
1376 }
1377
1378 if ($url == '') {
1379 $title = $conf->get('general.default_note_title', t('Note: '));
1380 }
1381 $url = escape($url);
1382 $title = escape($title);
1383
1384 $link = [
1385 'title' => $title,
1386 'url' => $url,
1387 'description' => $description,
1388 'tags' => $tags,
1389 'private' => $private,
1390 ];
1391 } else {
1392 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1393 $formatter = $factory->getFormatter('raw');
1394 $link = $formatter->format($bookmark);
1395 }
1396
1397 $tags = $bookmarkService->bookmarksCountPerTag();
1398 if ($conf->get('formatter') === 'markdown') {
1399 $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
1400 }
1401 $data = [
1402 'link' => $link,
1403 'link_is_new' => $link_is_new,
1404 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1405 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
1406 'tags' => $tags,
1407 'default_private_links' => $conf->get('privacy.default_private_links', false),
1408 ];
1409 $pluginManager->executeHooks('render_editlink', $data);
1410
1411 foreach ($data as $key => $value) {
1412 $PAGE->assign($key, $value);
1413 }
1414
1415 $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1416 $PAGE->renderPage('editlink');
1417 exit;
1418 }
1419
1420 if ($targetPage == Router::$PAGE_PINLINK) {
1421 if (! isset($_GET['id']) || !$bookmarkService->exists($_GET['id'])) {
1422 // FIXME! Use a proper error system.
1423 $msg = t('Invalid link ID provided');
1424 echo '<script>alert("'. $msg .'");document.location=\''. index_url($_SERVER) .'\';</script>';
1425 exit;
1426 }
1427 if (! $sessionManager->checkToken($_GET['token'])) {
1428 die('Wrong token.');
1429 }
1430
1431 $link = $bookmarkService->get($_GET['id']);
1432 $link->setSticky(! $link->isSticky());
1433 $bookmarkService->set($link);
1434 header('Location: '.index_url($_SERVER));
1435 exit;
1436 }
1437
1438 if ($targetPage == Router::$PAGE_EXPORT) {
1439 // Export bookmarks as a Netscape Bookmarks file
1440
1441 if (empty($_GET['selection'])) {
1442 $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli'));
1443 $PAGE->renderPage('export');
1444 exit;
1445 }
1446
1447 // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html
1448 $selection = $_GET['selection'];
1449 if (isset($_GET['prepend_note_url'])) {
1450 $prependNoteUrl = $_GET['prepend_note_url'];
1451 } else {
1452 $prependNoteUrl = false;
1453 }
1454
1455 try {
1456 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1457 $formatter = $factory->getFormatter('raw');
1458 $PAGE->assign(
1459 'links',
1460 NetscapeBookmarkUtils::filterAndFormat(
1461 $bookmarkService,
1462 $formatter,
1463 $selection,
1464 $prependNoteUrl,
1465 index_url($_SERVER)
1466 )
1467 );
1468 } catch (Exception $exc) {
1469 header('Content-Type: text/plain; charset=utf-8');
1470 echo $exc->getMessage();
1471 exit;
1472 }
1473 $now = new DateTime();
1474 header('Content-Type: text/html; charset=utf-8');
1475 header(
1476 'Content-disposition: attachment; filename=bookmarks_'
1477 .$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html'
1478 );
1479 $PAGE->assign('date', $now->format(DateTime::RFC822));
1480 $PAGE->assign('eol', PHP_EOL);
1481 $PAGE->assign('selection', $selection);
1482 $PAGE->renderPage('export.bookmarks');
1483 exit;
1484 }
1485
1486 if ($targetPage == Router::$PAGE_IMPORT) {
1487 // Upload a Netscape bookmark dump to import its contents
1488
1489 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1490 // Show import dialog
1491 $PAGE->assign(
1492 'maxfilesize',
1493 get_max_upload_size(
1494 ini_get('post_max_size'),
1495 ini_get('upload_max_filesize'),
1496 false
1497 )
1498 );
1499 $PAGE->assign(
1500 'maxfilesizeHuman',
1501 get_max_upload_size(
1502 ini_get('post_max_size'),
1503 ini_get('upload_max_filesize'),
1504 true
1505 )
1506 );
1507 $PAGE->assign('pagetitle', t('Import') .' - '. $conf->get('general.title', 'Shaarli'));
1508 $PAGE->renderPage('import');
1509 exit;
1510 }
1511
1512 // Import bookmarks from an uploaded file
1513 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1514 // The file is too big or some form field may be missing.
1515 $msg = sprintf(
1516 t(
1517 'The file you are trying to upload is probably bigger than what this webserver can accept'
1518 .' (%s). Please upload in smaller chunks.'
1519 ),
1520 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
1521 );
1522 echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
1523 exit;
1524 }
1525 if (! $sessionManager->checkToken($_POST['token'])) {
1526 die('Wrong token.');
1527 }
1528 $status = NetscapeBookmarkUtils::import(
1529 $_POST,
1530 $_FILES,
1531 $bookmarkService,
1532 $conf,
1533 $history
1534 );
1535 echo '<script>alert("'.$status.'");document.location=\'?do='
1536 .Router::$PAGE_IMPORT .'\';</script>';
1537 exit;
1538 }
1539
1540 // Plugin administration page
1541 if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
1542 $pluginMeta = $pluginManager->getPluginsMeta();
1543
1544 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
1545 $enabledPlugins = array_filter($pluginMeta, function ($v) {
1546 return $v['order'] !== false;
1547 });
1548 // Load parameters.
1549 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $conf->get('plugins', array()));
1550 uasort(
1551 $enabledPlugins,
1552 function ($a, $b) {
1553 return $a['order'] - $b['order'];
1554 }
1555 );
1556 $disabledPlugins = array_filter($pluginMeta, function ($v) {
1557 return $v['order'] === false;
1558 });
1559
1560 $PAGE->assign('enabledPlugins', $enabledPlugins);
1561 $PAGE->assign('disabledPlugins', $disabledPlugins);
1562 $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli'));
1563 $PAGE->renderPage('pluginsadmin');
1564 exit;
1565 }
1566
1567 // Plugin administration form action
1568 if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
1569 try {
1570 if (isset($_POST['parameters_form'])) {
1571 $pluginManager->executeHooks('save_plugin_parameters', $_POST);
1572 unset($_POST['parameters_form']);
1573 foreach ($_POST as $param => $value) {
1574 $conf->set('plugins.'. $param, escape($value));
1575 }
1576 } else {
1577 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
1578 }
1579 $conf->write($loginManager->isLoggedIn());
1580 $history->updateSettings();
1581 } catch (Exception $e) {
1582 error_log(
1583 'ERROR while saving plugin configuration:.' . PHP_EOL .
1584 $e->getMessage()
1585 );
1586
1587 // TODO: do not handle exceptions/errors in JS.
1588 echo '<script>alert("'
1589 . $e->getMessage()
1590 .'");document.location=\'?do='
1591 . Router::$PAGE_PLUGINSADMIN
1592 .'\';</script>';
1593 exit;
1594 }
1595 header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
1596 exit;
1597 }
1598
1599 // Get a fresh token
1600 if ($targetPage == Router::$GET_TOKEN) {
1601 header('Content-Type:text/plain');
1602 echo $sessionManager->generateToken();
1603 exit;
1604 }
1605
1606 // -------- Thumbnails Update
1607 if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
1608 $ids = [];
1609 foreach ($bookmarkService->search() as $bookmark) {
1610 // A note or not HTTP(S)
1611 if ($bookmark->isNote() || ! startsWith(strtolower($bookmark->getUrl()), 'http')) {
1612 continue;
1613 }
1614 $ids[] = $bookmark->getId();
1615 }
1616 $PAGE->assign('ids', $ids);
1617 $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
1618 $PAGE->renderPage('thumbnails');
1619 exit;
1620 }
1621
1622 // -------- Single Thumbnail Update
1623 if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
1624 if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
1625 http_response_code(400);
1626 exit;
1627 }
1628 $id = (int) $_POST['id'];
1629 if (! $bookmarkService->exists($id)) {
1630 http_response_code(404);
1631 exit;
1632 }
1633 $thumbnailer = new Thumbnailer($conf);
1634 $bookmark = $bookmarkService->get($id);
1635 $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
1636 $bookmarkService->set($bookmark);
1637
1638 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1639 echo json_encode($factory->getFormatter('raw')->format($bookmark));
1640 exit;
1641 }
1642
1643 // -------- Otherwise, simply display search form and bookmarks:
1644 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
1645 exit;
1646}
1647
1648/**
1649 * Template for the list of bookmarks (<div id="linklist">)
1650 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1651 *
1652 * @param pageBuilder $PAGE pageBuilder instance.
1653 * @param BookmarkServiceInterface $linkDb LinkDB instance.
1654 * @param ConfigManager $conf Configuration Manager instance.
1655 * @param PluginManager $pluginManager Plugin Manager instance.
1656 * @param LoginManager $loginManager LoginManager instance
1657 */
1658function buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
1659{
1660 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1661 $formatter = $factory->getFormatter();
1662
1663 // Used in templates
1664 if (isset($_GET['searchtags'])) {
1665 if (! empty($_GET['searchtags'])) {
1666 $searchtags = escape(normalize_spaces($_GET['searchtags']));
1667 } else {
1668 $searchtags = false;
1669 }
1670 } else {
1671 $searchtags = '';
1672 }
1673 $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : '';
1674
1675 // Smallhash filter
1676 if (! empty($_SERVER['QUERY_STRING'])
1677 && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
1678 try {
1679 $linksToDisplay = $linkDb->findByHash($_SERVER['QUERY_STRING']);
1680 } catch (BookmarkNotFoundException $e) {
1681 $PAGE->render404($e->getMessage());
1682 exit;
1683 }
1684 } else {
1685 // Filter bookmarks according search parameters.
1686 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : null;
1687 $request = [
1688 'searchtags' => $searchtags,
1689 'searchterm' => $searchterm,
1690 ];
1691 $linksToDisplay = $linkDb->search($request, $visibility, false, !empty($_SESSION['untaggedonly']));
1692 }
1693
1694 // ---- Handle paging.
1695 $keys = array();
1696 foreach ($linksToDisplay as $key => $value) {
1697 $keys[] = $key;
1698 }
1699
1700 // Select articles according to paging.
1701 $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
1702 $pagecount = $pagecount == 0 ? 1 : $pagecount;
1703 $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
1704 $page = $page < 1 ? 1 : $page;
1705 $page = $page > $pagecount ? $pagecount : $page;
1706 // Start index.
1707 $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
1708 $end = $i + $_SESSION['LINKS_PER_PAGE'];
1709
1710 $thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE;
1711 if ($thumbnailsEnabled) {
1712 $thumbnailer = new Thumbnailer($conf);
1713 }
1714
1715 $linkDisp = array();
1716 while ($i<$end && $i<count($keys)) {
1717 $link = $formatter->format($linksToDisplay[$keys[$i]]);
1718
1719 // Logged in, thumbnails enabled, not a note,
1720 // and (never retrieved yet or no valid cache file)
1721 if ($loginManager->isLoggedIn()
1722 && $thumbnailsEnabled
1723 && !$linksToDisplay[$keys[$i]]->isNote()
1724 && $linksToDisplay[$keys[$i]]->getThumbnail() !== false
1725 && ! is_file($linksToDisplay[$keys[$i]]->getThumbnail())
1726 ) {
1727 $linksToDisplay[$keys[$i]]->setThumbnail($thumbnailer->get($link['url']));
1728 $linkDb->set($linksToDisplay[$keys[$i]], false);
1729 $updateDB = true;
1730 $link['thumbnail'] = $linksToDisplay[$keys[$i]]->getThumbnail();
1731 }
1732
1733 // Check for both signs of a note: starting with ? and 7 chars long.
1734// if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
1735// $link['url'] = index_url($_SERVER) . $link['url'];
1736// }
1737
1738 $linkDisp[$keys[$i]] = $link;
1739 $i++;
1740 }
1741
1742 // If we retrieved new thumbnails, we update the database.
1743 if (!empty($updateDB)) {
1744 $linkDb->save();
1745 }
1746 92
1747 // Compute paging navigation 93// Main Shaarli routes
1748 $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags); 94$app->group('', function () {
1749 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); 95 $this->get('/install', '\Shaarli\Front\Controller\Visitor\InstallController:index')->setName('displayInstall');
1750 $previous_page_url = ''; 96 $this->get('/install/session-test', '\Shaarli\Front\Controller\Visitor\InstallController:sessionTest');
1751 if ($i != count($keys)) { 97 $this->post('/install', '\Shaarli\Front\Controller\Visitor\InstallController:save')->setName('saveInstall');
1752 $previous_page_url = '?page=' . ($page+1) . $searchtermUrl . $searchtagsUrl; 98
1753 } 99 /* -- PUBLIC --*/
1754 $next_page_url=''; 100 $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index');
1755 if ($page>1) { 101 $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink');
1756 $next_page_url = '?page=' . ($page-1) . $searchtermUrl . $searchtagsUrl; 102 $this->get('/login', '\Shaarli\Front\Controller\Visitor\LoginController:index')->setName('login');
1757 } 103 $this->post('/login', '\Shaarli\Front\Controller\Visitor\LoginController:login')->setName('processLogin');
104 $this->get('/picture-wall', '\Shaarli\Front\Controller\Visitor\PictureWallController:index');
105 $this->get('/tags/cloud', '\Shaarli\Front\Controller\Visitor\TagCloudController:cloud');
106 $this->get('/tags/list', '\Shaarli\Front\Controller\Visitor\TagCloudController:list');
107 $this->get('/daily', '\Shaarli\Front\Controller\Visitor\DailyController:index');
108 $this->get('/daily-rss', '\Shaarli\Front\Controller\Visitor\DailyController:rss')->setName('rss');
109 $this->get('/feed/atom', '\Shaarli\Front\Controller\Visitor\FeedController:atom')->setName('atom');
110 $this->get('/feed/rss', '\Shaarli\Front\Controller\Visitor\FeedController:rss');
111 $this->get('/open-search', '\Shaarli\Front\Controller\Visitor\OpenSearchController:index');
112
113 $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\Visitor\TagController:addTag');
114 $this->get('/remove-tag/{tag}', '\Shaarli\Front\Controller\Visitor\TagController:removeTag');
115 $this->get('/links-per-page', '\Shaarli\Front\Controller\Visitor\PublicSessionFilterController:linksPerPage');
116 $this->get('/untagged-only', '\Shaarli\Front\Controller\Visitor\PublicSessionFilterController:untaggedOnly');
117})->add('\Shaarli\Front\ShaarliMiddleware');
1758 118
1759 // Fill all template fields. 119$app->group('/admin', function () {
1760 $data = array( 120 $this->get('/logout', '\Shaarli\Front\Controller\Admin\LogoutController:index');
1761 'previous_page_url' => $previous_page_url, 121 $this->get('/tools', '\Shaarli\Front\Controller\Admin\ToolsController:index');
1762 'next_page_url' => $next_page_url, 122 $this->get('/password', '\Shaarli\Front\Controller\Admin\PasswordController:index');
1763 'page_current' => $page, 123 $this->post('/password', '\Shaarli\Front\Controller\Admin\PasswordController:change');
1764 'page_max' => $pagecount, 124 $this->get('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:index');
1765 'result_count' => count($linksToDisplay), 125 $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
1766 'search_term' => $searchterm, 126 $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
1767 'search_tags' => $searchtags, 127 $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
1768 'visibility' => ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '', 128 $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare');
1769 'links' => $linkDisp, 129 $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm');
130 $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm');
131 $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate');
132 $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms');
133 $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save');
134 $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark');
135 $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility');
136 $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark');
137 $this->patch(
138 '/shaare/{id:[0-9]+}/update-thumbnail',
139 '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate'
1770 ); 140 );
141 $this->get('/export', '\Shaarli\Front\Controller\Admin\ExportController:index');
142 $this->post('/export', '\Shaarli\Front\Controller\Admin\ExportController:export');
143 $this->get('/import', '\Shaarli\Front\Controller\Admin\ImportController:index');
144 $this->post('/import', '\Shaarli\Front\Controller\Admin\ImportController:import');
145 $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
146 $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
147 $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
148 $this->get('/server', '\Shaarli\Front\Controller\Admin\ServerController:index');
149 $this->get('/clear-cache', '\Shaarli\Front\Controller\Admin\ServerController:clearCache');
150 $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
151 $this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle');
152 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
153})->add('\Shaarli\Front\ShaarliAdminMiddleware');
1771 154
1772 // If there is only a single link, we change on-the-fly the title of the page.
1773 if (count($linksToDisplay) == 1) {
1774 $data['pagetitle'] = $linksToDisplay[$keys[0]]->getTitle() .' - '. $conf->get('general.title');
1775 } elseif (! empty($searchterm) || ! empty($searchtags)) {
1776 $data['pagetitle'] = t('Search: ');
1777 $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : '';
1778 $bracketWrap = function ($tag) {
1779 return '['. $tag .']';
1780 };
1781 $data['pagetitle'] .= ! empty($searchtags)
1782 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchtags))).' '
1783 : '';
1784 $data['pagetitle'] .= '- '. $conf->get('general.title');
1785 }
1786
1787 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
1788
1789 foreach ($data as $key => $value) {
1790 $PAGE->assign($key, $value);
1791 }
1792
1793 return;
1794}
1795
1796/**
1797 * Installation
1798 * This function should NEVER be called if the file data/config.php exists.
1799 *
1800 * @param ConfigManager $conf Configuration Manager instance.
1801 * @param SessionManager $sessionManager SessionManager instance
1802 * @param LoginManager $loginManager LoginManager instance
1803 */
1804function install($conf, $sessionManager, $loginManager)
1805{
1806 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
1807 if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) {
1808 mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705);
1809 }
1810
1811
1812 // This part makes sure sessions works correctly.
1813 // (Because on some hosts, session.save_path may not be set correctly,
1814 // or we may not have write access to it.)
1815 if (isset($_GET['test_session'])
1816 && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) {
1817 // Step 2: Check if data in session is correct.
1818 $msg = t(
1819 '<pre>Sessions do not seem to work correctly on your server.<br>'.
1820 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
1821 'and that you have write access to it.<br>'.
1822 'It currently points to %s.<br>'.
1823 'On some browsers, accessing your server via a hostname like \'localhost\' '.
1824 'or any custom hostname without a dot causes cookie storage to fail. '.
1825 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
1826 );
1827 $msg = sprintf($msg, session_save_path());
1828 echo $msg;
1829 echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
1830 die;
1831 }
1832 if (!isset($_SESSION['session_tested'])) {
1833 // Step 1 : Try to store data in session and reload page.
1834 $_SESSION['session_tested'] = 'Working'; // Try to set a variable in session.
1835 header('Location: '.index_url($_SERVER).'?test_session'); // Redirect to check stored data.
1836 }
1837 if (isset($_GET['test_session'])) {
1838 // Step 3: Sessions are OK. Remove test parameter from URL.
1839 header('Location: '.index_url($_SERVER));
1840 }
1841
1842
1843 if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) {
1844 $tz = 'UTC';
1845 if (!empty($_POST['continent']) && !empty($_POST['city'])
1846 && isTimeZoneValid($_POST['continent'], $_POST['city'])
1847 ) {
1848 $tz = $_POST['continent'].'/'.$_POST['city'];
1849 }
1850 $conf->set('general.timezone', $tz);
1851 $login = $_POST['setlogin'];
1852 $conf->set('credentials.login', $login);
1853 $salt = sha1(uniqid('', true) .'_'. mt_rand());
1854 $conf->set('credentials.salt', $salt);
1855 $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
1856 if (!empty($_POST['title'])) {
1857 $conf->set('general.title', escape($_POST['title']));
1858 } else {
1859 $conf->set('general.title', 'Shared bookmarks on '.escape(index_url($_SERVER)));
1860 }
1861 $conf->set('translation.language', escape($_POST['language']));
1862 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1863 $conf->set('api.enabled', !empty($_POST['enableApi']));
1864 $conf->set(
1865 'api.secret',
1866 generate_api_secret(
1867 $conf->get('credentials.login'),
1868 $conf->get('credentials.salt')
1869 )
1870 );
1871 try {
1872 // Everything is ok, let's create config file.
1873 $conf->write($loginManager->isLoggedIn());
1874 } catch (Exception $e) {
1875 error_log(
1876 'ERROR while writing config file after installation.' . PHP_EOL .
1877 $e->getMessage()
1878 );
1879
1880 // TODO: do not handle exceptions/errors in JS.
1881 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
1882 exit;
1883 }
1884
1885 $history = new History($conf->get('resource.history'));
1886 $bookmarkService = new BookmarkFileService($conf, $history, true);
1887 if ($bookmarkService->count() === 0) {
1888 $bookmarkService->initialize();
1889 }
1890
1891 echo '<script>alert('
1892 .'"Shaarli is now configured. '
1893 .'Please enter your login/password and start shaaring your bookmarks!"'
1894 .');document.location=\'./login\';</script>';
1895 exit;
1896 }
1897
1898 $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
1899 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
1900 $PAGE->assign('continents', $continents);
1901 $PAGE->assign('cities', $cities);
1902 $PAGE->assign('languages', Languages::getAvailableLanguages());
1903 $PAGE->renderPage('install');
1904 exit;
1905}
1906
1907if (!isset($_SESSION['LINKS_PER_PAGE'])) {
1908 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
1909}
1910
1911try {
1912 $history = new History($conf->get('resource.history'));
1913} catch (Exception $e) {
1914 die($e->getMessage());
1915}
1916
1917$linkDb = new BookmarkFileService($conf, $history, $loginManager->isLoggedIn());
1918
1919if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
1920 showDailyRSS($linkDb, $conf, $loginManager);
1921 exit;
1922}
1923
1924$containerBuilder = new ContainerBuilder($conf, $sessionManager, $loginManager);
1925$container = $containerBuilder->build();
1926$app = new App($container);
1927 155
1928// REST API routes 156// REST API routes
1929$app->group('/api/v1', function () { 157$app->group('/api/v1', function () {
@@ -1942,25 +170,12 @@ $app->group('/api/v1', function () {
1942 $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory'); 170 $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory');
1943})->add('\Shaarli\Api\ApiMiddleware'); 171})->add('\Shaarli\Api\ApiMiddleware');
1944 172
1945$app->group('', function () { 173try {
1946 $this->get('/login', '\Shaarli\Front\Controller\LoginController:index')->setName('login'); 174 $response = $app->run(true);
1947})->add('\Shaarli\Front\ShaarliMiddleware');
1948
1949$response = $app->run(true);
1950
1951// Hack to make Slim and Shaarli router work together:
1952// If a Slim route isn't found and NOT API call, we call renderPage().
1953if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
1954 // We use UTF-8 for proper international characters handling.
1955 header('Content-Type: text/html; charset=utf-8');
1956 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);
1957} else {
1958 $response = $response
1959 ->withHeader('Access-Control-Allow-Origin', '*')
1960 ->withHeader(
1961 'Access-Control-Allow-Headers',
1962 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
1963 )
1964 ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
1965 $app->respond($response); 175 $app->respond($response);
176} catch (Throwable $e) {
177 die(nl2br(
178 'An unexpected error happened, and the error template could not be displayed.' . PHP_EOL . PHP_EOL .
179 exception2text($e)
180 ));
1966} 181}
diff --git a/init.php b/init.php
new file mode 100644
index 00000000..d8462712
--- /dev/null
+++ b/init.php
@@ -0,0 +1,86 @@
1<?php
2
3require_once __DIR__ . '/vendor/autoload.php';
4
5use Shaarli\Helper\ApplicationUtils;
6use Shaarli\Security\SessionManager;
7
8// Set 'UTC' as the default timezone if it is not defined in php.ini
9// See http://php.net/manual/en/datetime.configuration.php#ini.date.timezone
10if (date_default_timezone_get() == '') {
11 date_default_timezone_set('UTC');
12}
13
14// High execution time in case of problematic imports/exports.
15ini_set('max_input_time', '60');
16
17// Try to set max upload file size and read
18ini_set('memory_limit', '128M');
19ini_set('post_max_size', '16M');
20ini_set('upload_max_filesize', '16M');
21
22// See all error except warnings
23error_reporting(E_ALL^E_WARNING);
24
25// 3rd-party libraries
26if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
27 header('Content-Type: text/plain; charset=utf-8');
28 echo "Error: missing Composer configuration\n\n"
29 ."If you installed Shaarli through Git or using the development branch,\n"
30 ."please refer to the installation documentation to install PHP"
31 ." dependencies using Composer:\n"
32 ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
33 ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
34 exit;
35}
36
37// Ensure the PHP version is supported
38try {
39 ApplicationUtils::checkPHPVersion('7.1', PHP_VERSION);
40} catch (Exception $exc) {
41 header('Content-Type: text/plain; charset=utf-8');
42 echo $exc->getMessage();
43 exit;
44}
45
46// Force cookie path (but do not change lifetime)
47$cookie = session_get_cookie_params();
48$cookiedir = '';
49if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
50 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
51}
52// Set default cookie expiration and path.
53session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
54// Set session parameters on server side.
55// Use cookies to store session.
56ini_set('session.use_cookies', 1);
57// Force cookies for session (phpsessionID forbidden in URL).
58ini_set('session.use_only_cookies', 1);
59// Prevent PHP form using sessionID in URL if cookies are disabled.
60ini_set('session.use_trans_sid', false);
61
62define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
63define('SHAARLI_MUTEX_FILE', __FILE__);
64
65session_name('shaarli');
66// Start session if needed (Some server auto-start sessions).
67if (session_status() == PHP_SESSION_NONE) {
68 session_start();
69}
70
71// Regenerate session ID if invalid or not defined in cookie.
72if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
73 session_regenerate_id(true);
74 $_COOKIE['shaarli'] = session_id();
75}
76
77// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
78if (! defined('LC_MESSAGES')) {
79 define('LC_MESSAGES', LC_COLLATE);
80}
81
82// Prevent caching on client side or proxy: (yes, it's ugly)
83header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
84header("Cache-Control: no-store, no-cache, must-revalidate");
85header("Cache-Control: post-check=0, pre-check=0", false);
86header("Pragma: no-cache");
diff --git a/mkdocs.yml b/mkdocs.yml
index cee2c5fb..2e201d03 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -15,41 +15,25 @@ site_dir: doc/html
15pages: 15pages:
16- Home: index.md 16- Home: index.md
17- Setup: 17- Setup:
18 - Download and Installation: Download-and-Installation.md
19 - Upgrade and migration: Upgrade-and-migration.md
20 - Server configuration: Server-configuration.md 18 - Server configuration: Server-configuration.md
21 - Server security: Server-security.md 19 - Installation: Installation.md
20 - Docker: Docker.md
21 - Reverse Proxy: Reverse-proxy.md
22 - Backup and restore: Backup-and-restore.md
22 - Shaarli configuration: Shaarli-configuration.md 23 - Shaarli configuration: Shaarli-configuration.md
23 - Plugins: Plugins.md 24 - Plugins: Plugins.md
24- Docker: 25 - Upgrade and migration: Upgrade-and-migration.md
25 - Docker 101: docker/docker-101.md
26 - Shaarli images: docker/shaarli-images.md
27 - Reverse proxy configuration: docker/reverse-proxy-configuration.md
28 - Docker resources: docker/resources.md
29- Usage: 26- Usage:
30 - Browsing and searching: Browsing-and-searching.md 27 - Usage: Usage.md
31 - Sharing content: Sharing-content.md
32 - RSS feeds: RSS-feeds.md
33 - REST API: REST-API.md 28 - REST API: REST-API.md
34 - Community & Related software: Community-&-Related-software.md 29 - Community and Related software: Community-and-related-software.md
35- Guides:
36 - Install Shaarli on Debian 9 with Docker: guides/install-shaarli-with-debian9-and-docker.md
37 - Backup, restore, import and export: guides/backup-restore-import-export.md
38 - Various hacks: guides/various-hacks.md
39- Development: 30- Development:
40 - Development guidelines: Development-guidelines.md 31 - Development: dev/Development.md
41 - Continuous integration tools: Continuous-integration-tools.md 32 - Versioning: dev/Versioning.md
42 - GnuPG signature: GnuPG-signature.md 33 - GnuPG signature: dev/GnuPG-signature.md
43 - Directory structure: Directory-structure.md 34 - Plugin System: dev/Plugin-system.md
44 - Link Structure: Link-structure.md 35 - Translations: dev/Translations.md
45 - 3rd party libraries: 3rd-party-libraries.md 36 - Release Shaarli: dev/Release-Shaarli.md
46 - Plugin System: Plugin-System.md 37 - Theming: dev/Theming.md
47 - Release Shaarli: Release-Shaarli.md 38 - Unit tests: dev/Unit-tests.md
48 - Versioning and Branches: Versioning-and-Branches.md
49 - Security: Security.md
50 - Static analysis: Static-analysis.md
51 - Translations: Translations.md
52 - Theming: Theming.md
53 - Unit tests: Unit-tests.md
54- FAQ: FAQ.md
55- Troubleshooting: Troubleshooting.md 39- Troubleshooting: Troubleshooting.md
diff --git a/package.json b/package.json
index f3d9b51e..b879b223 100644
--- a/package.json
+++ b/package.json
@@ -7,26 +7,28 @@
7 "awesomplete": "^1.1.2", 7 "awesomplete": "^1.1.2",
8 "blazy": "^1.8.2", 8 "blazy": "^1.8.2",
9 "fork-awesome": "^1.1.7", 9 "fork-awesome": "^1.1.7",
10 "he": "^1.2.0",
10 "pure-extras": "^1.0.0", 11 "pure-extras": "^1.0.0",
11 "purecss": "^1.0.0" 12 "purecss": "^1.0.0"
12 }, 13 },
13 "devDependencies": { 14 "devDependencies": {
14 "babel-core": "^6.26.0", 15 "@babel/core": "^7.11.6",
15 "babel-loader": "^7.1.2", 16 "@babel/preset-env": "^7.11.5",
16 "babel-minify-webpack-plugin": "^0.2.0", 17 "babel-loader": "^8.1.0",
17 "babel-preset-env": "^1.6.1", 18 "css-loader": "^4.3.0",
18 "css-loader": "^0.28.9", 19 "eslint": "^7.9.0",
19 "eslint": "^4.16.0", 20 "eslint-config-airbnb-base": "^14.2.0",
20 "eslint-config-airbnb-base": "^12.1.0", 21 "eslint-plugin-import": "^2.22.0",
21 "eslint-plugin-import": "^2.8.0",
22 "extract-text-webpack-plugin": "^3.0.2",
23 "file-loader": "^1.1.6", 22 "file-loader": "^1.1.6",
24 "node-sass": "^4.12.0", 23 "mini-css-extract-plugin": "^0.11.2",
25 "sass-lint": "^1.12.1", 24 "sass": "^1.26.11",
26 "sass-loader": "^6.0.6", 25 "sass-loader": "^10.0.2",
27 "style-loader": "^0.19.1", 26 "stylelint": "^13.7.1",
28 "url-loader": "^0.6.2", 27 "stylelint-config-standard": "^20.0.0",
29 "webpack": "^3.10.0" 28 "stylelint-scss": "^3.18.0",
29 "terser-webpack-plugin": "^4.2.2",
30 "webpack": "^4.44.2",
31 "webpack-cli": "^3.3.12"
30 }, 32 },
31 "scripts": { 33 "scripts": {
32 "build": "webpack", 34 "build": "webpack",
diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php
index 8bf4ed46..ab6ed6de 100644
--- a/plugins/addlink_toolbar/addlink_toolbar.php
+++ b/plugins/addlink_toolbar/addlink_toolbar.php
@@ -5,7 +5,7 @@
5 * Adds the addlink input on the linklist page. 5 * Adds the addlink input on the linklist page.
6 */ 6 */
7 7
8use Shaarli\Router; 8use Shaarli\Render\TemplatePage;
9 9
10/** 10/**
11 * When linklist is displayed, add play videos to header's toolbar. 11 * When linklist is displayed, add play videos to header's toolbar.
@@ -16,11 +16,11 @@ use Shaarli\Router;
16 */ 16 */
17function hook_addlink_toolbar_render_header($data) 17function hook_addlink_toolbar_render_header($data)
18{ 18{
19 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST && $data['_LOGGEDIN_'] === true) { 19 if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) {
20 $form = array( 20 $form = array(
21 'attr' => array( 21 'attr' => array(
22 'method' => 'GET', 22 'method' => 'GET',
23 'action' => '', 23 'action' => $data['_BASE_PATH_'] . '/admin/shaare',
24 'name' => 'addform', 24 'name' => 'addform',
25 'class' => 'addform', 25 'class' => 'addform',
26 ), 26 ),
diff --git a/plugins/archiveorg/archiveorg.html b/plugins/archiveorg/archiveorg.html
index ad501f47..e37d887e 100644
--- a/plugins/archiveorg/archiveorg.html
+++ b/plugins/archiveorg/archiveorg.html
@@ -1,5 +1,5 @@
1<span> 1<span>
2 <a href="https://web.archive.org/web/%s"> 2 <a href="https://web.archive.org/web/%s">
3 <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" /> 3 <img class="linklist-plugin-icon" src="%s/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
4 </a> 4 </a>
5</span> 5</span>
diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php
index 0ee1c73c..ed271532 100644
--- a/plugins/archiveorg/archiveorg.php
+++ b/plugins/archiveorg/archiveorg.php
@@ -17,12 +17,15 @@ use Shaarli\Plugin\PluginManager;
17function hook_archiveorg_render_linklist($data) 17function hook_archiveorg_render_linklist($data)
18{ 18{
19 $archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html'); 19 $archive_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/archiveorg/archiveorg.html');
20 $path = ($data['_ROOT_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
20 21
21 foreach ($data['links'] as &$value) { 22 foreach ($data['links'] as &$value) {
22 if ($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) { 23 $isNote = startsWith($value['real_url'], '/shaare/');
24 if ($value['private'] && $isNote) {
23 continue; 25 continue;
24 } 26 }
25 $archive = sprintf($archive_html, $value['url'], t('View on archive.org')); 27 $url = $isNote ? rtrim(index_url($_SERVER), '/') . $value['real_url'] : $value['real_url'];
28 $archive = sprintf($archive_html, $url, $path, t('View on archive.org'));
26 $value['link_plugin'][] = $archive; 29 $value['link_plugin'][] = $archive;
27 } 30 }
28 31
diff --git a/plugins/default_colors/default_colors.php b/plugins/default_colors/default_colors.php
index 1928cc9f..e1fd5cfb 100644
--- a/plugins/default_colors/default_colors.php
+++ b/plugins/default_colors/default_colors.php
@@ -15,6 +15,8 @@ const DEFAULT_COLORS_PLACEHOLDERS = [
15 'DEFAULT_COLORS_DARK_MAIN', 15 'DEFAULT_COLORS_DARK_MAIN',
16]; 16];
17 17
18const DEFAULT_COLORS_CSS_FILE = '/default_colors/default_colors.css';
19
18/** 20/**
19 * Display an error if the plugin is active a no color is configured. 21 * Display an error if the plugin is active a no color is configured.
20 * 22 *
@@ -24,58 +26,62 @@ const DEFAULT_COLORS_PLACEHOLDERS = [
24 */ 26 */
25function default_colors_init($conf) 27function default_colors_init($conf)
26{ 28{
27 $params = ''; 29 $params = [];
28 foreach (DEFAULT_COLORS_PLACEHOLDERS as $placeholder) { 30 foreach (DEFAULT_COLORS_PLACEHOLDERS as $placeholder) {
29 $params .= trim($conf->get('plugins.'. $placeholder, '')); 31 $value = trim($conf->get('plugins.'. $placeholder, ''));
32 if (strlen($value) > 0) {
33 $params[$placeholder] = $value;
34 }
30 } 35 }
31 36
32 if (empty($params)) { 37 if (empty($params)) {
33 $error = t('Default colors plugin error: '. 38 $error = t('Default colors plugin error: '.
34 'This plugin is active and no custom color is configured.'); 39 'This plugin is active and no custom color is configured.');
35 return array($error); 40 return [$error];
41 }
42
43 // Colors are defined but the custom CSS file does not exist -> generate it
44 if (!file_exists(PluginManager::$PLUGINS_PATH . DEFAULT_COLORS_CSS_FILE)) {
45 default_colors_generate_css_file($params);
36 } 46 }
37} 47}
38 48
39/** 49/**
40 * When plugin parameters are saved, we regenerate the custom CSS file with provided settings. 50 * When linklist is displayed, include default_colors CSS file.
41 * 51 *
42 * @param array $data $_POST array 52 * @param array $data - header data.
43 * 53 *
44 * @return array Updated $_POST array 54 * @return mixed - header data with default_colors CSS file added.
45 */ 55 */
46function hook_default_colors_save_plugin_parameters($data) 56function hook_default_colors_render_includes($data)
47{ 57{
48 $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css'; 58 $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css';
49 $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css.template'); 59 if (file_exists($file )) {
50 $content = ''; 60 $data['css_files'][] = $file ;
51 foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) {
52 $content .= ! empty($data[$rule])
53 ? default_colors_format_css_rule($data, $rule) .';'. PHP_EOL
54 : '';
55 }
56
57 if (! empty($content)) {
58 file_put_contents($file, sprintf($template, $content));
59 } 61 }
60 62
61 return $data; 63 return $data;
62} 64}
63 65
64/** 66/**
65 * When linklist is displayed, include default_colors CSS file. 67 * Regenerate the custom CSS file with provided settings.
66 *
67 * @param array $data - header data.
68 * 68 *
69 * @return mixed - header data with default_colors CSS file added. 69 * @param array $params Plugin configuration (CSS rules)
70 */ 70 */
71function hook_default_colors_render_includes($data) 71function default_colors_generate_css_file($params): void
72{ 72{
73 $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css'; 73 $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css';
74 if (file_exists($file )) { 74 $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css.template');
75 $data['css_files'][] = $file ; 75 $content = '';
76 foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) {
77 $content .= !empty($params[$rule])
78 ? default_colors_format_css_rule($params, $rule) .';'. PHP_EOL
79 : '';
76 } 80 }
77 81
78 return $data; 82 if (! empty($content)) {
83 file_put_contents($file, sprintf($template, $content));
84 }
79} 85}
80 86
81/** 87/**
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php
index 8ae1b479..defb01f7 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -16,7 +16,7 @@
16 16
17use Shaarli\Config\ConfigManager; 17use Shaarli\Config\ConfigManager;
18use Shaarli\Plugin\PluginManager; 18use Shaarli\Plugin\PluginManager;
19use Shaarli\Router; 19use Shaarli\Render\TemplatePage;
20 20
21/** 21/**
22 * In the footer hook, there is a working example of a translation extension for Shaarli. 22 * In the footer hook, there is a working example of a translation extension for Shaarli.
@@ -74,7 +74,7 @@ function demo_plugin_init($conf)
74function hook_demo_plugin_render_header($data) 74function hook_demo_plugin_render_header($data)
75{ 75{
76 // Only execute when linklist is rendered. 76 // Only execute when linklist is rendered.
77 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 77 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
78 // If loggedin 78 // If loggedin
79 if ($data['_LOGGEDIN_'] === true) { 79 if ($data['_LOGGEDIN_'] === true) {
80 /* 80 /*
@@ -118,7 +118,7 @@ function hook_demo_plugin_render_header($data)
118 $form = array( 118 $form = array(
119 'attr' => array( 119 'attr' => array(
120 'method' => 'GET', 120 'method' => 'GET',
121 'action' => '?', 121 'action' => $data['_BASE_PATH_'] . '/',
122 'class' => 'addform', 122 'class' => 'addform',
123 ), 123 ),
124 'inputs' => array( 124 'inputs' => array(
@@ -441,9 +441,9 @@ function hook_demo_plugin_delete_link($data)
441function hook_demo_plugin_render_feed($data) 441function hook_demo_plugin_render_feed($data)
442{ 442{
443 foreach ($data['links'] as &$link) { 443 foreach ($data['links'] as &$link) {
444 if ($data['_PAGE_'] == Router::$PAGE_FEED_ATOM) { 444 if ($data['_PAGE_'] == TemplatePage::FEED_ATOM) {
445 $link['description'] .= ' - ATOM Feed' ; 445 $link['description'] .= ' - ATOM Feed' ;
446 } elseif ($data['_PAGE_'] == Router::$PAGE_FEED_RSS) { 446 } elseif ($data['_PAGE_'] == TemplatePage::FEED_RSS) {
447 $link['description'] .= ' - RSS Feed'; 447 $link['description'] .= ' - RSS Feed';
448 } 448 }
449 } 449 }
diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php
index dab75dd5..d4632163 100644
--- a/plugins/isso/isso.php
+++ b/plugins/isso/isso.php
@@ -6,7 +6,7 @@
6 6
7use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\Plugin\PluginManager; 8use Shaarli\Plugin\PluginManager;
9use Shaarli\Router; 9use Shaarli\Render\TemplatePage;
10 10
11/** 11/**
12 * Display an error everywhere if the plugin is enabled without configuration. 12 * Display an error everywhere if the plugin is enabled without configuration.
@@ -49,12 +49,12 @@ function hook_isso_render_linklist($data, $conf)
49 $isso = sprintf($issoHtml, $issoUrl, $issoUrl, $link['id'], $link['id']); 49 $isso = sprintf($issoHtml, $issoUrl, $issoUrl, $link['id'], $link['id']);
50 $data['plugin_end_zone'][] = $isso; 50 $data['plugin_end_zone'][] = $isso;
51 } else { 51 } else {
52 $button = '<span><a href="?%s#isso-thread">'; 52 $button = '<span><a href="'. ($data['_BASE_PATH_'] ?? '') . '/shaare/%s#isso-thread">';
53 // For the default theme we use a FontAwesome icon which is better than an image 53 // For the default theme we use a FontAwesome icon which is better than an image
54 if ($conf->get('resource.theme') === 'default') { 54 if ($conf->get('resource.theme') === 'default') {
55 $button .= '<i class="linklist-plugin-icon fa fa-comment"></i>'; 55 $button .= '<i class="linklist-plugin-icon fa fa-comment"></i>';
56 } else { 56 } else {
57 $button .= '<img class="linklist-plugin-icon" src="plugins/isso/comment.png" '; 57 $button .= '<img class="linklist-plugin-icon" src="'. $data['_ROOT_PATH_'].'/plugins/isso/comment.png" ';
58 $button .= 'title="Comment on this shaare" alt="Comments" />'; 58 $button .= 'title="Comment on this shaare" alt="Comments" />';
59 } 59 }
60 $button .= '</a></span>'; 60 $button .= '</a></span>';
@@ -76,7 +76,7 @@ function hook_isso_render_linklist($data, $conf)
76 */ 76 */
77function hook_isso_render_includes($data) 77function hook_isso_render_includes($data)
78{ 78{
79 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 79 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
80 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/isso/isso.css'; 80 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/isso/isso.css';
81 } 81 }
82 82
diff --git a/plugins/isso/isso_button.html b/plugins/isso/isso_button.html
deleted file mode 100644
index 3f828480..00000000
--- a/plugins/isso/isso_button.html
+++ /dev/null
@@ -1,5 +0,0 @@
1<span>
2 <a href="?%s#isso-thread">
3 <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
4 </a>
5</span>
diff --git a/plugins/playvideos/README.md b/plugins/playvideos/README.md
index ab4be22a..32a94e88 100644
--- a/plugins/playvideos/README.md
+++ b/plugins/playvideos/README.md
@@ -8,22 +8,21 @@ This uses code from https://zaius.github.io/youtube_playlist/ and is currently o
8 8
9#### Installation and setup 9#### Installation and setup
10 10
11This is a default Shaarli plugin, you just have to enable it. See https://shaarli.readthedocs.io/en/master/Shaarli-configuration/ 11This is a default Shaarli plugin, you just have to enable it. See [Shaarli configuration](../../doc/md/Shaarli-configuration.md).
12 12
13 13
14#### Troubleshooting 14#### Troubleshooting
15 15
16If your server has [Content Security Policy](http://content-security-policy.com/) headers enabled, this may prevent the script from loading fully. You should relax the CSP in your server settings. Example CSP rule for apache2: 16If your server has [Content Security Policy](http://content-security-policy.com/) headers enabled, this may prevent the script from loading fully. You should relax the CSP in your server settings. Example CSP rule for apache2:
17
18In `/etc/apache2/conf-available/shaarli-csp.conf`:
19 17
20```apache 18```apache
21<Directory /path/to/shaarli> 19<Directory /path/to/shaarli>
20 # Required for playvideos plugin
22 Header set Content-Security-Policy "script-src 'self' 'unsafe-inline' https://www.youtube.com https://s.ytimg.com 'unsafe-eval'" 21 Header set Content-Security-Policy "script-src 'self' 'unsafe-inline' https://www.youtube.com https://s.ytimg.com 'unsafe-eval'"
23</Directory> 22</Directory>
24``` 23```
25 24
26Then run `a2enconf shaarli-csp; service apache2 reload` 25You may place the `Header` directive in the `<Directory...` section of your [webserver configuration](../../doc/md/Server-configuration.md)/virtualhost file, or write the above snippet to `/etc/apache2/conf-available/shaarli-csp.conf`; then run `a2enconf shaarli-csp; service apache2 reload`.
27 26
28### License 27### License
29``` 28```
diff --git a/plugins/playvideos/playvideos.php b/plugins/playvideos/playvideos.php
index 0341ed59..91a9c1e5 100644
--- a/plugins/playvideos/playvideos.php
+++ b/plugins/playvideos/playvideos.php
@@ -7,7 +7,7 @@
7 */ 7 */
8 8
9use Shaarli\Plugin\PluginManager; 9use Shaarli\Plugin\PluginManager;
10use Shaarli\Router; 10use Shaarli\Render\TemplatePage;
11 11
12/** 12/**
13 * When linklist is displayed, add play videos to header's toolbar. 13 * When linklist is displayed, add play videos to header's toolbar.
@@ -18,7 +18,7 @@ use Shaarli\Router;
18 */ 18 */
19function hook_playvideos_render_header($data) 19function hook_playvideos_render_header($data)
20{ 20{
21 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 21 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
22 $playvideo = array( 22 $playvideo = array(
23 'attr' => array( 23 'attr' => array(
24 'href' => '#', 24 'href' => '#',
@@ -42,7 +42,7 @@ function hook_playvideos_render_header($data)
42 */ 42 */
43function hook_playvideos_render_footer($data) 43function hook_playvideos_render_footer($data)
44{ 44{
45 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 45 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
46 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/jquery-1.11.2.min.js'; 46 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/jquery-1.11.2.min.js';
47 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/youtube_playlist.js'; 47 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/playvideos/youtube_playlist.js';
48 } 48 }
diff --git a/plugins/pubsubhubbub/pubsubhubbub.php b/plugins/pubsubhubbub/pubsubhubbub.php
index 2878c050..8fe6799c 100644
--- a/plugins/pubsubhubbub/pubsubhubbub.php
+++ b/plugins/pubsubhubbub/pubsubhubbub.php
@@ -13,7 +13,7 @@ use pubsubhubbub\publisher\Publisher;
13use Shaarli\Config\ConfigManager; 13use Shaarli\Config\ConfigManager;
14use Shaarli\Feed\FeedBuilder; 14use Shaarli\Feed\FeedBuilder;
15use Shaarli\Plugin\PluginManager; 15use Shaarli\Plugin\PluginManager;
16use Shaarli\Router; 16use Shaarli\Render\TemplatePage;
17 17
18/** 18/**
19 * Plugin init function - set the hub to the default appspot one. 19 * Plugin init function - set the hub to the default appspot one.
@@ -41,7 +41,7 @@ function pubsubhubbub_init($conf)
41 */ 41 */
42function hook_pubsubhubbub_render_feed($data, $conf) 42function hook_pubsubhubbub_render_feed($data, $conf)
43{ 43{
44 $feedType = $data['_PAGE_'] == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM; 44 $feedType = $data['_PAGE_'] == TemplatePage::FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
45 $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/pubsubhubbub/hub.'. $feedType .'.xml'); 45 $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/pubsubhubbub/hub.'. $feedType .'.xml');
46 $data['feed_plugins_header'][] = sprintf($template, $conf->get('plugins.PUBSUBHUB_URL')); 46 $data['feed_plugins_header'][] = sprintf($template, $conf->get('plugins.PUBSUBHUB_URL'));
47 47
@@ -60,8 +60,8 @@ function hook_pubsubhubbub_render_feed($data, $conf)
60function hook_pubsubhubbub_save_link($data, $conf) 60function hook_pubsubhubbub_save_link($data, $conf)
61{ 61{
62 $feeds = array( 62 $feeds = array(
63 index_url($_SERVER) .'?do=atom', 63 index_url($_SERVER) .'feed/atom',
64 index_url($_SERVER) .'?do=rss', 64 index_url($_SERVER) .'feed/rss',
65 ); 65 );
66 66
67 $httpPost = function_exists('curl_version') ? false : 'nocurl_http_post'; 67 $httpPost = function_exists('curl_version') ? false : 'nocurl_http_post';
diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php
index c1d237d5..24fd18ba 100644
--- a/plugins/qrcode/qrcode.php
+++ b/plugins/qrcode/qrcode.php
@@ -6,7 +6,7 @@
6 */ 6 */
7 7
8use Shaarli\Plugin\PluginManager; 8use Shaarli\Plugin\PluginManager;
9use Shaarli\Router; 9use Shaarli\Render\TemplatePage;
10 10
11/** 11/**
12 * Add qrcode icon to link_plugin when rendering linklist. 12 * Add qrcode icon to link_plugin when rendering linklist.
@@ -19,11 +19,12 @@ function hook_qrcode_render_linklist($data)
19{ 19{
20 $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html'); 20 $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html');
21 21
22 $path = ($data['_ROOT_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
22 foreach ($data['links'] as &$value) { 23 foreach ($data['links'] as &$value) {
23 $qrcode = sprintf( 24 $qrcode = sprintf(
24 $qrcode_html, 25 $qrcode_html,
25 $value['url'], 26 $value['url'],
26 PluginManager::$PLUGINS_PATH 27 $path
27 ); 28 );
28 $value['link_plugin'][] = $qrcode; 29 $value['link_plugin'][] = $qrcode;
29 } 30 }
@@ -40,7 +41,7 @@ function hook_qrcode_render_linklist($data)
40 */ 41 */
41function hook_qrcode_render_footer($data) 42function hook_qrcode_render_footer($data)
42{ 43{
43 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 44 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
44 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js'; 45 $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/shaarli-qrcode.js';
45 } 46 }
46 47
@@ -56,7 +57,7 @@ function hook_qrcode_render_footer($data)
56 */ 57 */
57function hook_qrcode_render_includes($data) 58function hook_qrcode_render_includes($data)
58{ 59{
59 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) { 60 if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
60 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.css'; 61 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.css';
61 } 62 }
62 63
diff --git a/plugins/qrcode/shaarli-qrcode.js b/plugins/qrcode/shaarli-qrcode.js
index fe77c4cd..3316d6f6 100644
--- a/plugins/qrcode/shaarli-qrcode.js
+++ b/plugins/qrcode/shaarli-qrcode.js
@@ -28,14 +28,15 @@
28 28
29// Show the QR-Code of a permalink (when the QR-Code icon is clicked). 29// Show the QR-Code of a permalink (when the QR-Code icon is clicked).
30function showQrCode(caller,loading) 30function showQrCode(caller,loading)
31{ 31{
32 // Dynamic javascript lib loading: We only load qr.js if the QR code icon is clicked: 32 // Dynamic javascript lib loading: We only load qr.js if the QR code icon is clicked:
33 if (typeof(qr) == 'undefined') // Load qr.js only if not present. 33 if (typeof(qr) == 'undefined') // Load qr.js only if not present.
34 { 34 {
35 if (!loading) // If javascript lib is still loading, do not append script to body. 35 if (!loading) // If javascript lib is still loading, do not append script to body.
36 { 36 {
37 var element = document.createElement("script"); 37 var basePath = document.querySelector('input[name="js_base_path"]').value;
38 element.src = "plugins/qrcode/qr-1.1.3.min.js"; 38 var element = document.createElement("script");
39 element.src = basePath + "/plugins/qrcode/qr-1.1.3.min.js";
39 document.body.appendChild(element); 40 document.body.appendChild(element);
40 } 41 }
41 setTimeout(function() { showQrCode(caller,true);}, 200); // Retry in 200 milliseconds. 42 setTimeout(function() { showQrCode(caller,true);}, 200); // Retry in 200 milliseconds.
@@ -44,7 +45,7 @@ function showQrCode(caller,loading)
44 45
45 // Remove previous qrcode if present. 46 // Remove previous qrcode if present.
46 removeQrcode(); 47 removeQrcode();
47 48
48 // Build the div which contains the QR-Code: 49 // Build the div which contains the QR-Code:
49 var element = document.createElement('div'); 50 var element = document.createElement('div');
50 element.id = 'permalinkQrcode'; 51 element.id = 'permalinkQrcode';
@@ -57,11 +58,11 @@ function showQrCode(caller,loading)
57 // Damn IE 58 // Damn IE
58 element.setAttribute('onclick', 'this.parentNode.removeChild(this);' ); 59 element.setAttribute('onclick', 'this.parentNode.removeChild(this);' );
59 } 60 }
60 61
61 // Build the QR-Code: 62 // Build the QR-Code:
62 var image = qr.image({size: 8,value: caller.dataset.permalink}); 63 var image = qr.image({size: 8,value: caller.dataset.permalink});
63 if (image) 64 if (image)
64 { 65 {
65 element.appendChild(image); 66 element.appendChild(image);
66 element.innerHTML += "<br>Click to close"; 67 element.innerHTML += "<br>Click to close";
67 caller.parentNode.appendChild(element); 68 caller.parentNode.appendChild(element);
@@ -87,4 +88,4 @@ function removeQrcode()
87 elem.parentNode.removeChild(elem); 88 elem.parentNode.removeChild(elem);
88 } 89 }
89 return false; 90 return false;
90} \ No newline at end of file 91}
diff --git a/plugins/wallabag/README.md b/plugins/wallabag/README.md
index ea21a519..c53a04d9 100644
--- a/plugins/wallabag/README.md
+++ b/plugins/wallabag/README.md
@@ -21,7 +21,7 @@ The directory structure should look like:
21 21
22To enable the plugin, you can either: 22To enable the plugin, you can either:
23 23
24 * enable it in the plugins administration page (`?do=pluginadmin`). 24 * enable it in the plugins administration page (`/admin/plugins`).
25 * add `wallabag` to your list of enabled plugins in `data/config.json.php` (`general.enabled_plugins` section). 25 * add `wallabag` to your list of enabled plugins in `data/config.json.php` (`general.enabled_plugins` section).
26 26
27### Configuration 27### Configuration
diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php
index bc35df08..d0df3501 100644
--- a/plugins/wallabag/wallabag.php
+++ b/plugins/wallabag/wallabag.php
@@ -45,12 +45,14 @@ function hook_wallabag_render_linklist($data, $conf)
45 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); 45 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
46 46
47 $linkTitle = t('Save to wallabag'); 47 $linkTitle = t('Save to wallabag');
48 $path = ($data['_ROOT_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH;
49
48 foreach ($data['links'] as &$value) { 50 foreach ($data['links'] as &$value) {
49 $wallabag = sprintf( 51 $wallabag = sprintf(
50 $wallabagHtml, 52 $wallabagHtml,
51 $wallabagInstance->getWallabagUrl(), 53 $wallabagInstance->getWallabagUrl(),
52 urlencode($value['url']), 54 urlencode($value['url']),
53 PluginManager::$PLUGINS_PATH, 55 $path,
54 $linkTitle 56 $linkTitle
55 ); 57 );
56 $value['link_plugin'][] = $wallabag; 58 $value['link_plugin'][] = $wallabag;
diff --git a/tests/FileUtilsTest.php b/tests/FileUtilsTest.php
deleted file mode 100644
index 57719175..00000000
--- a/tests/FileUtilsTest.php
+++ /dev/null
@@ -1,110 +0,0 @@
1<?php
2
3namespace Shaarli;
4
5use Exception;
6
7/**
8 * Class FileUtilsTest
9 *
10 * Test file utility class.
11 */
12class FileUtilsTest extends \PHPUnit\Framework\TestCase
13{
14 /**
15 * @var string Test file path.
16 */
17 protected static $file = 'sandbox/flat.db';
18
19 /**
20 * Delete test file after every test.
21 */
22 public function tearDown()
23 {
24 @unlink(self::$file);
25 }
26
27 /**
28 * Test writeDB, then readDB with different data.
29 */
30 public function testSimpleWriteRead()
31 {
32 $data = ['blue', 'red'];
33 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
34 $this->assertTrue(startsWith(file_get_contents(self::$file), '<?php /*'));
35 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
36
37 $data = 0;
38 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
39 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
40
41 $data = null;
42 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
43 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
44
45 $data = false;
46 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
47 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
48 }
49
50 /**
51 * File not writable: raise an exception.
52 *
53 * @expectedException Shaarli\Exceptions\IOException
54 * @expectedExceptionMessage Error accessing "sandbox/flat.db"
55 */
56 public function testWriteWithoutPermission()
57 {
58 touch(self::$file);
59 chmod(self::$file, 0440);
60 FileUtils::writeFlatDB(self::$file, null);
61 }
62
63 /**
64 * Folder non existent: raise an exception.
65 *
66 * @expectedException Shaarli\Exceptions\IOException
67 * @expectedExceptionMessage Error accessing "nopefolder"
68 */
69 public function testWriteFolderDoesNotExist()
70 {
71 FileUtils::writeFlatDB('nopefolder/file', null);
72 }
73
74 /**
75 * Folder non writable: raise an exception.
76 *
77 * @expectedException Shaarli\Exceptions\IOException
78 * @expectedExceptionMessage Error accessing "sandbox"
79 */
80 public function testWriteFolderPermission()
81 {
82 chmod(dirname(self::$file), 0555);
83 try {
84 FileUtils::writeFlatDB(self::$file, null);
85 } catch (Exception $e) {
86 chmod(dirname(self::$file), 0755);
87 throw $e;
88 }
89 }
90
91 /**
92 * Read non existent file, use default parameter.
93 */
94 public function testReadNotExistentFile()
95 {
96 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
97 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
98 }
99
100 /**
101 * Read non readable file, use default parameter.
102 */
103 public function testReadNotReadable()
104 {
105 touch(self::$file);
106 chmod(self::$file, 0220);
107 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
108 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
109 }
110}
diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php
index 7189c3a9..e810104e 100644
--- a/tests/HistoryTest.php
+++ b/tests/HistoryTest.php
@@ -3,10 +3,9 @@
3namespace Shaarli; 3namespace Shaarli;
4 4
5use DateTime; 5use DateTime;
6use Exception;
7use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
8 7
9class HistoryTest extends \PHPUnit\Framework\TestCase 8class HistoryTest extends \Shaarli\TestCase
10{ 9{
11 /** 10 /**
12 * @var string History file path 11 * @var string History file path
@@ -16,7 +15,7 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
16 /** 15 /**
17 * Delete history file. 16 * Delete history file.
18 */ 17 */
19 public function setUp() 18 protected function setUp(): void
20 { 19 {
21 if (file_exists(self::$historyFilePath)) { 20 if (file_exists(self::$historyFilePath)) {
22 unlink(self::$historyFilePath); 21 unlink(self::$historyFilePath);
@@ -44,12 +43,12 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
44 43
45 /** 44 /**
46 * Not writable history file: raise an exception. 45 * Not writable history file: raise an exception.
47 *
48 * @expectedException Exception
49 * @expectedExceptionMessage History file isn't readable or writable
50 */ 46 */
51 public function testConstructNotWritable() 47 public function testConstructNotWritable()
52 { 48 {
49 $this->expectException(\Exception::class);
50 $this->expectExceptionMessage('History file isn\'t readable or writable');
51
53 touch(self::$historyFilePath); 52 touch(self::$historyFilePath);
54 chmod(self::$historyFilePath, 0440); 53 chmod(self::$historyFilePath, 0440);
55 $history = new History(self::$historyFilePath); 54 $history = new History(self::$historyFilePath);
@@ -58,12 +57,12 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
58 57
59 /** 58 /**
60 * Not parsable history file: raise an exception. 59 * Not parsable history file: raise an exception.
61 *
62 * @expectedException Exception
63 * @expectedExceptionMessageRegExp /Could not parse history file/
64 */ 60 */
65 public function testConstructNotParsable() 61 public function testConstructNotParsable()
66 { 62 {
63 $this->expectException(\Exception::class);
64 $this->expectExceptionMessageRegExp('/Could not parse history file/');
65
67 file_put_contents(self::$historyFilePath, 'not parsable'); 66 file_put_contents(self::$historyFilePath, 'not parsable');
68 $history = new History(self::$historyFilePath); 67 $history = new History(self::$historyFilePath);
69 // gzinflate generates a warning 68 // gzinflate generates a warning
@@ -90,14 +89,6 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
90 $this->assertEquals(History::CREATED, $actual['event']); 89 $this->assertEquals(History::CREATED, $actual['event']);
91 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 90 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
92 $this->assertEquals(1, $actual['id']); 91 $this->assertEquals(1, $actual['id']);
93
94 $history = new History(self::$historyFilePath);
95 $bookmark = (new Bookmark())->setId('str');
96 $history->addLink($bookmark);
97 $actual = $history->getHistory()[0];
98 $this->assertEquals(History::CREATED, $actual['event']);
99 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
100 $this->assertEquals('str', $actual['id']);
101 } 92 }
102 93
103// /** 94// /**
diff --git a/tests/LanguagesTest.php b/tests/LanguagesTest.php
index de83f291..ce24c160 100644
--- a/tests/LanguagesTest.php
+++ b/tests/LanguagesTest.php
@@ -7,7 +7,7 @@ use Shaarli\Config\ConfigManager;
7/** 7/**
8 * Class LanguagesTest. 8 * Class LanguagesTest.
9 */ 9 */
10class LanguagesTest extends \PHPUnit\Framework\TestCase 10class LanguagesTest extends \Shaarli\TestCase
11{ 11{
12 /** 12 /**
13 * @var string Config file path (without extension). 13 * @var string Config file path (without extension).
@@ -22,7 +22,7 @@ class LanguagesTest extends \PHPUnit\Framework\TestCase
22 /** 22 /**
23 * 23 *
24 */ 24 */
25 public function setUp() 25 protected function setUp(): void
26 { 26 {
27 $this->conf = new ConfigManager(self::$configFile); 27 $this->conf = new ConfigManager(self::$configFile);
28 } 28 }
diff --git a/tests/PluginManagerTest.php b/tests/PluginManagerTest.php
index 195d959c..efef5e87 100644
--- a/tests/PluginManagerTest.php
+++ b/tests/PluginManagerTest.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2namespace Shaarli\Plugin; 3namespace Shaarli\Plugin;
3 4
4use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
@@ -6,7 +7,7 @@ use Shaarli\Config\ConfigManager;
6/** 7/**
7 * Unit tests for Plugins 8 * Unit tests for Plugins
8 */ 9 */
9class PluginManagerTest extends \PHPUnit\Framework\TestCase 10class PluginManagerTest extends \Shaarli\TestCase
10{ 11{
11 /** 12 /**
12 * Path to tests plugin. 13 * Path to tests plugin.
@@ -25,7 +26,7 @@ class PluginManagerTest extends \PHPUnit\Framework\TestCase
25 */ 26 */
26 protected $pluginManager; 27 protected $pluginManager;
27 28
28 public function setUp() 29 public function setUp(): void
29 { 30 {
30 $conf = new ConfigManager(''); 31 $conf = new ConfigManager('');
31 $this->pluginManager = new PluginManager($conf); 32 $this->pluginManager = new PluginManager($conf);
@@ -33,57 +34,88 @@ class PluginManagerTest extends \PHPUnit\Framework\TestCase
33 34
34 /** 35 /**
35 * Test plugin loading and hook execution. 36 * Test plugin loading and hook execution.
36 *
37 * @return void
38 */ 37 */
39 public function testPlugin() 38 public function testPlugin(): void
40 { 39 {
41 PluginManager::$PLUGINS_PATH = self::$pluginPath; 40 PluginManager::$PLUGINS_PATH = self::$pluginPath;
42 $this->pluginManager->load(array(self::$pluginName)); 41 $this->pluginManager->load(array(self::$pluginName));
43 42
44 $this->assertTrue(function_exists('hook_test_random')); 43 $this->assertTrue(function_exists('hook_test_random'));
45 44
46 $data = array(0 => 'woot'); 45 $data = [0 => 'woot'];
47 $this->pluginManager->executeHooks('random', $data); 46 $this->pluginManager->executeHooks('random', $data);
48 $this->assertEquals('woot', $data[1]);
49 47
50 $data = array(0 => 'woot'); 48 static::assertCount(2, $data);
49 static::assertSame('woot', $data[1]);
50
51 $data = [0 => 'woot'];
51 $this->pluginManager->executeHooks('random', $data, array('target' => 'test')); 52 $this->pluginManager->executeHooks('random', $data, array('target' => 'test'));
52 $this->assertEquals('page test', $data[1]);
53 53
54 $data = array(0 => 'woot'); 54 static::assertCount(2, $data);
55 static::assertSame('page test', $data[1]);
56
57 $data = [0 => 'woot'];
55 $this->pluginManager->executeHooks('random', $data, array('loggedin' => true)); 58 $this->pluginManager->executeHooks('random', $data, array('loggedin' => true));
56 $this->assertEquals('loggedin', $data[1]); 59
60 static::assertCount(2, $data);
61 static::assertEquals('loggedin', $data[1]);
62
63 $data = [0 => 'woot'];
64 $this->pluginManager->executeHooks('random', $data, array('loggedin' => null));
65
66 static::assertCount(3, $data);
67 static::assertEquals('loggedin', $data[1]);
68 static::assertArrayHasKey(2, $data);
69 static::assertNull($data[2]);
70 }
71
72 /**
73 * Test plugin loading and hook execution with an error: raise an incompatibility error.
74 */
75 public function testPluginWithPhpError(): void
76 {
77 PluginManager::$PLUGINS_PATH = self::$pluginPath;
78 $this->pluginManager->load(array(self::$pluginName));
79
80 $this->assertTrue(function_exists('hook_test_error'));
81
82 $data = [];
83 $this->pluginManager->executeHooks('error', $data);
84
85 $this->assertRegExp(
86 '/test \[plugin incompatibility\]: Class [\'"]Unknown[\'"] not found/',
87 $this->pluginManager->getErrors()[0]
88 );
57 } 89 }
58 90
59 /** 91 /**
60 * Test missing plugin loading. 92 * Test missing plugin loading.
61 */ 93 */
62 public function testPluginNotFound() 94 public function testPluginNotFound(): void
63 { 95 {
64 $this->pluginManager->load(array()); 96 $this->pluginManager->load([]);
65 $this->pluginManager->load(array('nope', 'renope')); 97 $this->pluginManager->load(['nope', 'renope']);
66 $this->addToAssertionCount(1); 98 $this->addToAssertionCount(1);
67 } 99 }
68 100
69 /** 101 /**
70 * Test plugin metadata loading. 102 * Test plugin metadata loading.
71 */ 103 */
72 public function testGetPluginsMeta() 104 public function testGetPluginsMeta(): void
73 { 105 {
74 PluginManager::$PLUGINS_PATH = self::$pluginPath; 106 PluginManager::$PLUGINS_PATH = self::$pluginPath;
75 $this->pluginManager->load(array(self::$pluginName)); 107 $this->pluginManager->load([self::$pluginName]);
76 108
77 $expectedParameters = array( 109 $expectedParameters = [
78 'pop' => array( 110 'pop' => [
79 'value' => '', 111 'value' => '',
80 'desc' => 'pop description', 112 'desc' => 'pop description',
81 ), 113 ],
82 'hip' => array( 114 'hip' => [
83 'value' => '', 115 'value' => '',
84 'desc' => '', 116 'desc' => '',
85 ), 117 ],
86 ); 118 ];
87 $meta = $this->pluginManager->getPluginsMeta(); 119 $meta = $this->pluginManager->getPluginsMeta();
88 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']); 120 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
89 $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']); 121 $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']);
diff --git a/tests/RouterTest.php b/tests/RouterTest.php
deleted file mode 100644
index 0cd49bb8..00000000
--- a/tests/RouterTest.php
+++ /dev/null
@@ -1,509 +0,0 @@
1<?php
2namespace Shaarli;
3
4/**
5 * Unit tests for Router
6 */
7class RouterTest extends \PHPUnit\Framework\TestCase
8{
9 /**
10 * Test findPage: login page output.
11 * Valid: page should be return.
12 *
13 * @return void
14 */
15 public function testFindPageLoginValid()
16 {
17 $this->assertEquals(
18 Router::$PAGE_LOGIN,
19 Router::findPage('do=login', array(), false)
20 );
21
22 $this->assertEquals(
23 Router::$PAGE_LOGIN,
24 Router::findPage('do=login', array(), 1)
25 );
26
27 $this->assertEquals(
28 Router::$PAGE_LOGIN,
29 Router::findPage('do=login&stuff', array(), false)
30 );
31 }
32
33 /**
34 * Test findPage: login page output.
35 * Invalid: page shouldn't be return.
36 *
37 * @return void
38 */
39 public function testFindPageLoginInvalid()
40 {
41 $this->assertNotEquals(
42 Router::$PAGE_LOGIN,
43 Router::findPage('do=login', array(), true)
44 );
45
46 $this->assertNotEquals(
47 Router::$PAGE_LOGIN,
48 Router::findPage('do=other', array(), false)
49 );
50 }
51
52 /**
53 * Test findPage: picwall page output.
54 * Valid: page should be return.
55 *
56 * @return void
57 */
58 public function testFindPagePicwallValid()
59 {
60 $this->assertEquals(
61 Router::$PAGE_PICWALL,
62 Router::findPage('do=picwall', array(), false)
63 );
64
65 $this->assertEquals(
66 Router::$PAGE_PICWALL,
67 Router::findPage('do=picwall', array(), true)
68 );
69 }
70
71 /**
72 * Test findPage: picwall page output.
73 * Invalid: page shouldn't be return.
74 *
75 * @return void
76 */
77 public function testFindPagePicwallInvalid()
78 {
79 $this->assertEquals(
80 Router::$PAGE_PICWALL,
81 Router::findPage('do=picwall&stuff', array(), false)
82 );
83
84 $this->assertNotEquals(
85 Router::$PAGE_PICWALL,
86 Router::findPage('do=other', array(), false)
87 );
88 }
89
90 /**
91 * Test findPage: tagcloud page output.
92 * Valid: page should be return.
93 *
94 * @return void
95 */
96 public function testFindPageTagcloudValid()
97 {
98 $this->assertEquals(
99 Router::$PAGE_TAGCLOUD,
100 Router::findPage('do=tagcloud', array(), false)
101 );
102
103 $this->assertEquals(
104 Router::$PAGE_TAGCLOUD,
105 Router::findPage('do=tagcloud', array(), true)
106 );
107
108 $this->assertEquals(
109 Router::$PAGE_TAGCLOUD,
110 Router::findPage('do=tagcloud&stuff', array(), false)
111 );
112 }
113
114 /**
115 * Test findPage: tagcloud page output.
116 * Invalid: page shouldn't be return.
117 *
118 * @return void
119 */
120 public function testFindPageTagcloudInvalid()
121 {
122 $this->assertNotEquals(
123 Router::$PAGE_TAGCLOUD,
124 Router::findPage('do=other', array(), false)
125 );
126 }
127
128 /**
129 * Test findPage: linklist page output.
130 * Valid: page should be return.
131 *
132 * @return void
133 */
134 public function testFindPageLinklistValid()
135 {
136 $this->assertEquals(
137 Router::$PAGE_LINKLIST,
138 Router::findPage('', array(), true)
139 );
140
141 $this->assertEquals(
142 Router::$PAGE_LINKLIST,
143 Router::findPage('whatever', array(), true)
144 );
145
146 $this->assertEquals(
147 Router::$PAGE_LINKLIST,
148 Router::findPage('whatever', array(), false)
149 );
150
151 $this->assertEquals(
152 Router::$PAGE_LINKLIST,
153 Router::findPage('do=tools', array(), false)
154 );
155 }
156
157 /**
158 * Test findPage: tools page output.
159 * Valid: page should be return.
160 *
161 * @return void
162 */
163 public function testFindPageToolsValid()
164 {
165 $this->assertEquals(
166 Router::$PAGE_TOOLS,
167 Router::findPage('do=tools', array(), true)
168 );
169
170 $this->assertEquals(
171 Router::$PAGE_TOOLS,
172 Router::findPage('do=tools&stuff', array(), true)
173 );
174 }
175
176 /**
177 * Test findPage: tools page output.
178 * Invalid: page shouldn't be return.
179 *
180 * @return void
181 */
182 public function testFindPageToolsInvalid()
183 {
184 $this->assertNotEquals(
185 Router::$PAGE_TOOLS,
186 Router::findPage('do=tools', array(), 1)
187 );
188
189 $this->assertNotEquals(
190 Router::$PAGE_TOOLS,
191 Router::findPage('do=tools', array(), false)
192 );
193
194 $this->assertNotEquals(
195 Router::$PAGE_TOOLS,
196 Router::findPage('do=other', array(), true)
197 );
198 }
199
200 /**
201 * Test findPage: changepasswd page output.
202 * Valid: page should be return.
203 *
204 * @return void
205 */
206 public function testFindPageChangepasswdValid()
207 {
208 $this->assertEquals(
209 Router::$PAGE_CHANGEPASSWORD,
210 Router::findPage('do=changepasswd', array(), true)
211 );
212 $this->assertEquals(
213 Router::$PAGE_CHANGEPASSWORD,
214 Router::findPage('do=changepasswd&stuff', array(), true)
215 );
216 }
217
218 /**
219 * Test findPage: changepasswd page output.
220 * Invalid: page shouldn't be return.
221 *
222 * @return void
223 */
224 public function testFindPageChangepasswdInvalid()
225 {
226 $this->assertNotEquals(
227 Router::$PAGE_CHANGEPASSWORD,
228 Router::findPage('do=changepasswd', array(), 1)
229 );
230
231 $this->assertNotEquals(
232 Router::$PAGE_CHANGEPASSWORD,
233 Router::findPage('do=changepasswd', array(), false)
234 );
235
236 $this->assertNotEquals(
237 Router::$PAGE_CHANGEPASSWORD,
238 Router::findPage('do=other', array(), true)
239 );
240 }
241 /**
242 * Test findPage: configure page output.
243 * Valid: page should be return.
244 *
245 * @return void
246 */
247 public function testFindPageConfigureValid()
248 {
249 $this->assertEquals(
250 Router::$PAGE_CONFIGURE,
251 Router::findPage('do=configure', array(), true)
252 );
253
254 $this->assertEquals(
255 Router::$PAGE_CONFIGURE,
256 Router::findPage('do=configure&stuff', array(), true)
257 );
258 }
259
260 /**
261 * Test findPage: configure page output.
262 * Invalid: page shouldn't be return.
263 *
264 * @return void
265 */
266 public function testFindPageConfigureInvalid()
267 {
268 $this->assertNotEquals(
269 Router::$PAGE_CONFIGURE,
270 Router::findPage('do=configure', array(), 1)
271 );
272
273 $this->assertNotEquals(
274 Router::$PAGE_CONFIGURE,
275 Router::findPage('do=configure', array(), false)
276 );
277
278 $this->assertNotEquals(
279 Router::$PAGE_CONFIGURE,
280 Router::findPage('do=other', array(), true)
281 );
282 }
283
284 /**
285 * Test findPage: changetag page output.
286 * Valid: page should be return.
287 *
288 * @return void
289 */
290 public function testFindPageChangetagValid()
291 {
292 $this->assertEquals(
293 Router::$PAGE_CHANGETAG,
294 Router::findPage('do=changetag', array(), true)
295 );
296
297 $this->assertEquals(
298 Router::$PAGE_CHANGETAG,
299 Router::findPage('do=changetag&stuff', array(), true)
300 );
301 }
302
303 /**
304 * Test findPage: changetag page output.
305 * Invalid: page shouldn't be return.
306 *
307 * @return void
308 */
309 public function testFindPageChangetagInvalid()
310 {
311 $this->assertNotEquals(
312 Router::$PAGE_CHANGETAG,
313 Router::findPage('do=changetag', array(), 1)
314 );
315
316 $this->assertNotEquals(
317 Router::$PAGE_CHANGETAG,
318 Router::findPage('do=changetag', array(), false)
319 );
320
321 $this->assertNotEquals(
322 Router::$PAGE_CHANGETAG,
323 Router::findPage('do=other', array(), true)
324 );
325 }
326
327 /**
328 * Test findPage: addlink page output.
329 * Valid: page should be return.
330 *
331 * @return void
332 */
333 public function testFindPageAddlinkValid()
334 {
335 $this->assertEquals(
336 Router::$PAGE_ADDLINK,
337 Router::findPage('do=addlink', array(), true)
338 );
339
340 $this->assertEquals(
341 Router::$PAGE_ADDLINK,
342 Router::findPage('do=addlink&stuff', array(), true)
343 );
344 }
345
346 /**
347 * Test findPage: addlink page output.
348 * Invalid: page shouldn't be return.
349 *
350 * @return void
351 */
352 public function testFindPageAddlinkInvalid()
353 {
354 $this->assertNotEquals(
355 Router::$PAGE_ADDLINK,
356 Router::findPage('do=addlink', array(), 1)
357 );
358
359 $this->assertNotEquals(
360 Router::$PAGE_ADDLINK,
361 Router::findPage('do=addlink', array(), false)
362 );
363
364 $this->assertNotEquals(
365 Router::$PAGE_ADDLINK,
366 Router::findPage('do=other', array(), true)
367 );
368 }
369
370 /**
371 * Test findPage: export page output.
372 * Valid: page should be return.
373 *
374 * @return void
375 */
376 public function testFindPageExportValid()
377 {
378 $this->assertEquals(
379 Router::$PAGE_EXPORT,
380 Router::findPage('do=export', array(), true)
381 );
382
383 $this->assertEquals(
384 Router::$PAGE_EXPORT,
385 Router::findPage('do=export&stuff', array(), true)
386 );
387 }
388
389 /**
390 * Test findPage: export page output.
391 * Invalid: page shouldn't be return.
392 *
393 * @return void
394 */
395 public function testFindPageExportInvalid()
396 {
397 $this->assertNotEquals(
398 Router::$PAGE_EXPORT,
399 Router::findPage('do=export', array(), 1)
400 );
401
402 $this->assertNotEquals(
403 Router::$PAGE_EXPORT,
404 Router::findPage('do=export', array(), false)
405 );
406
407 $this->assertNotEquals(
408 Router::$PAGE_EXPORT,
409 Router::findPage('do=other', array(), true)
410 );
411 }
412
413 /**
414 * Test findPage: import page output.
415 * Valid: page should be return.
416 *
417 * @return void
418 */
419 public function testFindPageImportValid()
420 {
421 $this->assertEquals(
422 Router::$PAGE_IMPORT,
423 Router::findPage('do=import', array(), true)
424 );
425
426 $this->assertEquals(
427 Router::$PAGE_IMPORT,
428 Router::findPage('do=import&stuff', array(), true)
429 );
430 }
431
432 /**
433 * Test findPage: import page output.
434 * Invalid: page shouldn't be return.
435 *
436 * @return void
437 */
438 public function testFindPageImportInvalid()
439 {
440 $this->assertNotEquals(
441 Router::$PAGE_IMPORT,
442 Router::findPage('do=import', array(), 1)
443 );
444
445 $this->assertNotEquals(
446 Router::$PAGE_IMPORT,
447 Router::findPage('do=import', array(), false)
448 );
449
450 $this->assertNotEquals(
451 Router::$PAGE_IMPORT,
452 Router::findPage('do=other', array(), true)
453 );
454 }
455
456 /**
457 * Test findPage: editlink page output.
458 * Valid: page should be return.
459 *
460 * @return void
461 */
462 public function testFindPageEditlinkValid()
463 {
464 $this->assertEquals(
465 Router::$PAGE_EDITLINK,
466 Router::findPage('whatever', array('edit_link' => 1), true)
467 );
468
469 $this->assertEquals(
470 Router::$PAGE_EDITLINK,
471 Router::findPage('', array('edit_link' => 1), true)
472 );
473
474
475 $this->assertEquals(
476 Router::$PAGE_EDITLINK,
477 Router::findPage('whatever', array('post' => 1), true)
478 );
479
480 $this->assertEquals(
481 Router::$PAGE_EDITLINK,
482 Router::findPage('whatever', array('post' => 1, 'edit_link' => 1), true)
483 );
484 }
485
486 /**
487 * Test findPage: editlink page output.
488 * Invalid: page shouldn't be return.
489 *
490 * @return void
491 */
492 public function testFindPageEditlinkInvalid()
493 {
494 $this->assertNotEquals(
495 Router::$PAGE_EDITLINK,
496 Router::findPage('whatever', array('edit_link' => 1), false)
497 );
498
499 $this->assertNotEquals(
500 Router::$PAGE_EDITLINK,
501 Router::findPage('whatever', array('edit_link' => 1), 1)
502 );
503
504 $this->assertNotEquals(
505 Router::$PAGE_EDITLINK,
506 Router::findPage('whatever', array(), true)
507 );
508 }
509}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 00000000..781e7aa3
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,77 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli;
6
7/**
8 * Helper class extending \PHPUnit\Framework\TestCase.
9 * Used to make Shaarli UT run on multiple versions of PHPUnit.
10 */
11class TestCase extends \PHPUnit\Framework\TestCase
12{
13 /**
14 * expectExceptionMessageRegExp has been removed and replaced by expectExceptionMessageMatches in PHPUnit 9.
15 */
16 public function expectExceptionMessageRegExp(string $regularExpression): void
17 {
18 if (method_exists($this, 'expectExceptionMessageMatches')) {
19 $this->expectExceptionMessageMatches($regularExpression);
20 } else {
21 parent::expectExceptionMessageRegExp($regularExpression);
22 }
23 }
24
25 /**
26 * assertContains is now used for iterable, strings should use assertStringContainsString
27 */
28 public function assertContainsPolyfill($expected, $actual, string $message = ''): void
29 {
30 if (is_string($actual) && method_exists($this, 'assertStringContainsString')) {
31 static::assertStringContainsString($expected, $actual, $message);
32 } else {
33 static::assertContains($expected, $actual, $message);
34 }
35 }
36
37 /**
38 * assertNotContains is now used for iterable, strings should use assertStringNotContainsString
39 */
40 public function assertNotContainsPolyfill($expected, $actual, string $message = ''): void
41 {
42 if (is_string($actual) && method_exists($this, 'assertStringNotContainsString')) {
43 static::assertStringNotContainsString($expected, $actual, $message);
44 } else {
45 static::assertNotContains($expected, $actual, $message);
46 }
47 }
48
49 /**
50 * assertFileNotExists has been renamed in assertFileDoesNotExist
51 */
52 public static function assertFileNotExists(string $filename, string $message = ''): void
53 {
54 if (method_exists(TestCase::class, 'assertFileDoesNotExist')) {
55 static::assertFileDoesNotExist($filename, $message);
56 } else {
57 parent::assertFileNotExists($filename, $message);
58 }
59 }
60
61 /**
62 * assertRegExp has been renamed in assertMatchesRegularExpression
63 */
64 public static function assertRegExp(string $pattern, string $string, string $message = ''): void
65 {
66 if (method_exists(TestCase::class, 'assertMatchesRegularExpression')) {
67 static::assertMatchesRegularExpression($pattern, $string, $message);
68 } else {
69 parent::assertRegExp($pattern, $string, $message);
70 }
71 }
72
73 public function isInTestsContext(): bool
74 {
75 return true;
76 }
77}
diff --git a/tests/ThumbnailerTest.php b/tests/ThumbnailerTest.php
index c01849f7..70519aca 100644
--- a/tests/ThumbnailerTest.php
+++ b/tests/ThumbnailerTest.php
@@ -2,7 +2,6 @@
2 2
3namespace Shaarli; 3namespace Shaarli;
4 4
5use PHPUnit\Framework\TestCase;
6use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
7use WebThumbnailer\Application\ConfigManager as WTConfigManager; 6use WebThumbnailer\Application\ConfigManager as WTConfigManager;
8 7
@@ -30,7 +29,7 @@ class ThumbnailerTest extends TestCase
30 */ 29 */
31 protected $conf; 30 protected $conf;
32 31
33 public function setUp() 32 protected function setUp(): void
34 { 33 {
35 $this->conf = new ConfigManager('tests/utils/config/configJson'); 34 $this->conf = new ConfigManager('tests/utils/config/configJson');
36 $this->conf->set('thumbnails.mode', Thumbnailer::MODE_ALL); 35 $this->conf->set('thumbnails.mode', Thumbnailer::MODE_ALL);
@@ -43,7 +42,7 @@ class ThumbnailerTest extends TestCase
43 WTConfigManager::addFile('tests/utils/config/wt.json'); 42 WTConfigManager::addFile('tests/utils/config/wt.json');
44 } 43 }
45 44
46 public function tearDown() 45 protected function tearDown(): void
47 { 46 {
48 $this->rrmdirContent('sandbox/'); 47 $this->rrmdirContent('sandbox/');
49 } 48 }
diff --git a/tests/TimeZoneTest.php b/tests/TimeZoneTest.php
index 02bf060f..77862855 100644
--- a/tests/TimeZoneTest.php
+++ b/tests/TimeZoneTest.php
@@ -8,14 +8,14 @@ require_once 'application/TimeZone.php';
8/** 8/**
9 * Unitary tests for timezone utilities 9 * Unitary tests for timezone utilities
10 */ 10 */
11class TimeZoneTest extends PHPUnit\Framework\TestCase 11class TimeZoneTest extends \Shaarli\TestCase
12{ 12{
13 /** 13 /**
14 * @var array of timezones 14 * @var array of timezones
15 */ 15 */
16 protected $installedTimezones; 16 protected $installedTimezones;
17 17
18 public function setUp() 18 protected function setUp(): void
19 { 19 {
20 $this->installedTimezones = [ 20 $this->installedTimezones = [
21 'Antarctica/Syowa', 21 'Antarctica/Syowa',
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index 26d2a6b8..59dca75f 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -10,7 +10,7 @@ require_once 'application/Languages.php';
10/** 10/**
11 * Unitary tests for Shaarli utilities 11 * Unitary tests for Shaarli utilities
12 */ 12 */
13class UtilsTest extends PHPUnit\Framework\TestCase 13class UtilsTest extends \Shaarli\TestCase
14{ 14{
15 // Log file 15 // Log file
16 protected static $testLogFile = 'tests.log'; 16 protected static $testLogFile = 'tests.log';
@@ -26,7 +26,7 @@ class UtilsTest extends PHPUnit\Framework\TestCase
26 /** 26 /**
27 * Assign reference data 27 * Assign reference data
28 */ 28 */
29 public static function setUpBeforeClass() 29 public static function setUpBeforeClass(): void
30 { 30 {
31 self::$defaultTimeZone = date_default_timezone_get(); 31 self::$defaultTimeZone = date_default_timezone_get();
32 // Timezone without DST for test consistency 32 // Timezone without DST for test consistency
@@ -36,7 +36,7 @@ class UtilsTest extends PHPUnit\Framework\TestCase
36 /** 36 /**
37 * Reset the timezone 37 * Reset the timezone
38 */ 38 */
39 public static function tearDownAfterClass() 39 public static function tearDownAfterClass(): void
40 { 40 {
41 date_default_timezone_set(self::$defaultTimeZone); 41 date_default_timezone_set(self::$defaultTimeZone);
42 } 42 }
@@ -44,7 +44,7 @@ class UtilsTest extends PHPUnit\Framework\TestCase
44 /** 44 /**
45 * Resets test data before each test 45 * Resets test data before each test
46 */ 46 */
47 protected function setUp() 47 protected function setUp(): void
48 { 48 {
49 if (file_exists(self::$testLogFile)) { 49 if (file_exists(self::$testLogFile)) {
50 unlink(self::$testLogFile); 50 unlink(self::$testLogFile);
@@ -63,41 +63,25 @@ class UtilsTest extends PHPUnit\Framework\TestCase
63 } 63 }
64 64
65 /** 65 /**
66 * Log a message to a file - IPv4 client address 66 * Format a log a message - IPv4 client address
67 */ 67 */
68 public function testLogmIp4() 68 public function testFormatLogIp4()
69 { 69 {
70 $logMessage = 'IPv4 client connected'; 70 $message = 'IPv4 client connected';
71 logm(self::$testLogFile, '127.0.0.1', $logMessage); 71 $log = format_log($message, '127.0.0.1');
72 list($date, $ip, $message) = $this->getLastLogEntry();
73 72
74 $this->assertInstanceOf( 73 static::assertSame('- 127.0.0.1 - IPv4 client connected', $log);
75 'DateTime',
76 DateTime::createFromFormat(self::$dateFormat, $date)
77 );
78 $this->assertTrue(
79 filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false
80 );
81 $this->assertEquals($logMessage, $message);
82 } 74 }
83 75
84 /** 76 /**
85 * Log a message to a file - IPv6 client address 77 * Format a log a message - IPv6 client address
86 */ 78 */
87 public function testLogmIp6() 79 public function testFormatLogIp6()
88 { 80 {
89 $logMessage = 'IPv6 client connected'; 81 $message = 'IPv6 client connected';
90 logm(self::$testLogFile, '2001:db8::ff00:42:8329', $logMessage); 82 $log = format_log($message, '2001:db8::ff00:42:8329');
91 list($date, $ip, $message) = $this->getLastLogEntry();
92 83
93 $this->assertInstanceOf( 84 static::assertSame('- 2001:db8::ff00:42:8329 - IPv6 client connected', $log);
94 'DateTime',
95 DateTime::createFromFormat(self::$dateFormat, $date)
96 );
97 $this->assertTrue(
98 filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false
99 );
100 $this->assertEquals($logMessage, $message);
101 } 85 }
102 86
103 /** 87 /**
diff --git a/tests/api/ApiMiddlewareTest.php b/tests/api/ApiMiddlewareTest.php
index df2fb33a..86700840 100644
--- a/tests/api/ApiMiddlewareTest.php
+++ b/tests/api/ApiMiddlewareTest.php
@@ -18,7 +18,7 @@ use Slim\Http\Response;
18 * 18 *
19 * @package Api 19 * @package Api
20 */ 20 */
21class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase 21class ApiMiddlewareTest extends \Shaarli\TestCase
22{ 22{
23 /** 23 /**
24 * @var string datastore to test write operations 24 * @var string datastore to test write operations
@@ -26,7 +26,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
26 protected static $testDatastore = 'sandbox/datastore.php'; 26 protected static $testDatastore = 'sandbox/datastore.php';
27 27
28 /** 28 /**
29 * @var \ConfigManager instance 29 * @var ConfigManager instance
30 */ 30 */
31 protected $conf; 31 protected $conf;
32 32
@@ -43,7 +43,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
43 /** 43 /**
44 * Before every test, instantiate a new Api with its config, plugins and bookmarks. 44 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
45 */ 45 */
46 public function setUp() 46 protected function setUp(): void
47 { 47 {
48 $this->conf = new ConfigManager('tests/utils/config/configJson'); 48 $this->conf = new ConfigManager('tests/utils/config/configJson');
49 $this->conf->set('api.secret', 'NapoleonWasALizard'); 49 $this->conf->set('api.secret', 'NapoleonWasALizard');
@@ -61,12 +61,59 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
61 /** 61 /**
62 * After every test, remove the test datastore. 62 * After every test, remove the test datastore.
63 */ 63 */
64 public function tearDown() 64 protected function tearDown(): void
65 { 65 {
66 @unlink(self::$testDatastore); 66 @unlink(self::$testDatastore);
67 } 67 }
68 68
69 /** 69 /**
70 * Invoke the middleware with a valid token
71 */
72 public function testInvokeMiddlewareWithValidToken(): void
73 {
74 $next = function (Request $request, Response $response): Response {
75 return $response;
76 };
77 $mw = new ApiMiddleware($this->container);
78 $env = Environment::mock([
79 'REQUEST_METHOD' => 'GET',
80 'REQUEST_URI' => '/echo',
81 'HTTP_AUTHORIZATION'=> 'Bearer ' . ApiUtilsTest::generateValidJwtToken('NapoleonWasALizard'),
82 ]);
83 $request = Request::createFromEnvironment($env);
84 $response = new Response();
85 /** @var Response $response */
86 $response = $mw($request, $response, $next);
87
88 $this->assertEquals(200, $response->getStatusCode());
89 }
90
91 /**
92 * Invoke the middleware with a valid token
93 * Using specific Apache CGI redirected authorization.
94 */
95 public function testInvokeMiddlewareWithValidTokenFromRedirectedHeader(): void
96 {
97 $next = function (Request $request, Response $response): Response {
98 return $response;
99 };
100
101 $token = 'Bearer ' . ApiUtilsTest::generateValidJwtToken('NapoleonWasALizard');
102 $this->container->environment['REDIRECT_HTTP_AUTHORIZATION'] = $token;
103 $mw = new ApiMiddleware($this->container);
104 $env = Environment::mock([
105 'REQUEST_METHOD' => 'GET',
106 'REQUEST_URI' => '/echo',
107 ]);
108 $request = Request::createFromEnvironment($env);
109 $response = new Response();
110 /** @var Response $response */
111 $response = $mw($request, $response, $next);
112
113 $this->assertEquals(200, $response->getStatusCode());
114 }
115
116 /**
70 * Invoke the middleware with the API disabled: 117 * Invoke the middleware with the API disabled:
71 * should return a 401 error Unauthorized. 118 * should return a 401 error Unauthorized.
72 */ 119 */
@@ -109,7 +156,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
109 $this->assertEquals(401, $response->getStatusCode()); 156 $this->assertEquals(401, $response->getStatusCode());
110 $body = json_decode((string) $response->getBody()); 157 $body = json_decode((string) $response->getBody());
111 $this->assertEquals('Not authorized: API is disabled', $body->message); 158 $this->assertEquals('Not authorized: API is disabled', $body->message);
112 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 159 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
113 } 160 }
114 161
115 /** 162 /**
@@ -132,7 +179,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
132 $this->assertEquals(401, $response->getStatusCode()); 179 $this->assertEquals(401, $response->getStatusCode());
133 $body = json_decode((string) $response->getBody()); 180 $body = json_decode((string) $response->getBody());
134 $this->assertEquals('Not authorized: JWT token not provided', $body->message); 181 $this->assertEquals('Not authorized: JWT token not provided', $body->message);
135 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 182 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
136 } 183 }
137 184
138 /** 185 /**
@@ -157,7 +204,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
157 $this->assertEquals(401, $response->getStatusCode()); 204 $this->assertEquals(401, $response->getStatusCode());
158 $body = json_decode((string) $response->getBody()); 205 $body = json_decode((string) $response->getBody());
159 $this->assertEquals('Not authorized: Token secret must be set in Shaarli\'s administration', $body->message); 206 $this->assertEquals('Not authorized: Token secret must be set in Shaarli\'s administration', $body->message);
160 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 207 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
161 } 208 }
162 209
163 /** 210 /**
@@ -180,7 +227,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
180 $this->assertEquals(401, $response->getStatusCode()); 227 $this->assertEquals(401, $response->getStatusCode());
181 $body = json_decode((string) $response->getBody()); 228 $body = json_decode((string) $response->getBody());
182 $this->assertEquals('Not authorized: Invalid JWT header', $body->message); 229 $this->assertEquals('Not authorized: Invalid JWT header', $body->message);
183 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 230 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
184 } 231 }
185 232
186 /** 233 /**
@@ -206,6 +253,6 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
206 $this->assertEquals(401, $response->getStatusCode()); 253 $this->assertEquals(401, $response->getStatusCode());
207 $body = json_decode((string) $response->getBody()); 254 $body = json_decode((string) $response->getBody());
208 $this->assertEquals('Not authorized: Malformed JWT token', $body->message); 255 $this->assertEquals('Not authorized: Malformed JWT token', $body->message);
209 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 256 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
210 } 257 }
211} 258}
diff --git a/tests/api/ApiUtilsTest.php b/tests/api/ApiUtilsTest.php
index 7efec9bb..7a143859 100644
--- a/tests/api/ApiUtilsTest.php
+++ b/tests/api/ApiUtilsTest.php
@@ -8,12 +8,12 @@ use Shaarli\Http\Base64Url;
8/** 8/**
9 * Class ApiUtilsTest 9 * Class ApiUtilsTest
10 */ 10 */
11class ApiUtilsTest extends \PHPUnit\Framework\TestCase 11class ApiUtilsTest extends \Shaarli\TestCase
12{ 12{
13 /** 13 /**
14 * Force the timezone for ISO datetimes. 14 * Force the timezone for ISO datetimes.
15 */ 15 */
16 public static function setUpBeforeClass() 16 public static function setUpBeforeClass(): void
17 { 17 {
18 date_default_timezone_set('UTC'); 18 date_default_timezone_set('UTC');
19 } 19 }
@@ -66,143 +66,143 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
66 66
67 /** 67 /**
68 * Test validateJwtToken() with a malformed JWT token. 68 * Test validateJwtToken() with a malformed JWT token.
69 *
70 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
71 * @expectedExceptionMessage Malformed JWT token
72 */ 69 */
73 public function testValidateJwtTokenMalformed() 70 public function testValidateJwtTokenMalformed()
74 { 71 {
72 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
73 $this->expectExceptionMessage('Malformed JWT token');
74
75 $token = 'ABC.DEF'; 75 $token = 'ABC.DEF';
76 ApiUtils::validateJwtToken($token, 'foo'); 76 ApiUtils::validateJwtToken($token, 'foo');
77 } 77 }
78 78
79 /** 79 /**
80 * Test validateJwtToken() with an empty JWT token. 80 * Test validateJwtToken() with an empty JWT token.
81 *
82 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
83 * @expectedExceptionMessage Malformed JWT token
84 */ 81 */
85 public function testValidateJwtTokenMalformedEmpty() 82 public function testValidateJwtTokenMalformedEmpty()
86 { 83 {
84 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
85 $this->expectExceptionMessage('Malformed JWT token');
86
87 $token = false; 87 $token = false;
88 ApiUtils::validateJwtToken($token, 'foo'); 88 ApiUtils::validateJwtToken($token, 'foo');
89 } 89 }
90 90
91 /** 91 /**
92 * Test validateJwtToken() with a JWT token without header. 92 * Test validateJwtToken() with a JWT token without header.
93 *
94 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
95 * @expectedExceptionMessage Malformed JWT token
96 */ 93 */
97 public function testValidateJwtTokenMalformedEmptyHeader() 94 public function testValidateJwtTokenMalformedEmptyHeader()
98 { 95 {
96 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
97 $this->expectExceptionMessage('Malformed JWT token');
98
99 $token = '.payload.signature'; 99 $token = '.payload.signature';
100 ApiUtils::validateJwtToken($token, 'foo'); 100 ApiUtils::validateJwtToken($token, 'foo');
101 } 101 }
102 102
103 /** 103 /**
104 * Test validateJwtToken() with a JWT token without payload 104 * Test validateJwtToken() with a JWT token without payload
105 *
106 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
107 * @expectedExceptionMessage Malformed JWT token
108 */ 105 */
109 public function testValidateJwtTokenMalformedEmptyPayload() 106 public function testValidateJwtTokenMalformedEmptyPayload()
110 { 107 {
108 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
109 $this->expectExceptionMessage('Malformed JWT token');
110
111 $token = 'header..signature'; 111 $token = 'header..signature';
112 ApiUtils::validateJwtToken($token, 'foo'); 112 ApiUtils::validateJwtToken($token, 'foo');
113 } 113 }
114 114
115 /** 115 /**
116 * Test validateJwtToken() with a JWT token with an empty signature. 116 * Test validateJwtToken() with a JWT token with an empty signature.
117 *
118 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
119 * @expectedExceptionMessage Invalid JWT signature
120 */ 117 */
121 public function testValidateJwtTokenInvalidSignatureEmpty() 118 public function testValidateJwtTokenInvalidSignatureEmpty()
122 { 119 {
120 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
121 $this->expectExceptionMessage('Invalid JWT signature');
122
123 $token = 'header.payload.'; 123 $token = 'header.payload.';
124 ApiUtils::validateJwtToken($token, 'foo'); 124 ApiUtils::validateJwtToken($token, 'foo');
125 } 125 }
126 126
127 /** 127 /**
128 * Test validateJwtToken() with a JWT token with an invalid signature. 128 * Test validateJwtToken() with a JWT token with an invalid signature.
129 *
130 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
131 * @expectedExceptionMessage Invalid JWT signature
132 */ 129 */
133 public function testValidateJwtTokenInvalidSignature() 130 public function testValidateJwtTokenInvalidSignature()
134 { 131 {
132 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
133 $this->expectExceptionMessage('Invalid JWT signature');
134
135 $token = 'header.payload.nope'; 135 $token = 'header.payload.nope';
136 ApiUtils::validateJwtToken($token, 'foo'); 136 ApiUtils::validateJwtToken($token, 'foo');
137 } 137 }
138 138
139 /** 139 /**
140 * Test validateJwtToken() with a JWT token with a signature generated with the wrong API secret. 140 * Test validateJwtToken() with a JWT token with a signature generated with the wrong API secret.
141 *
142 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
143 * @expectedExceptionMessage Invalid JWT signature
144 */ 141 */
145 public function testValidateJwtTokenInvalidSignatureSecret() 142 public function testValidateJwtTokenInvalidSignatureSecret()
146 { 143 {
144 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
145 $this->expectExceptionMessage('Invalid JWT signature');
146
147 ApiUtils::validateJwtToken(self::generateValidJwtToken('foo'), 'bar'); 147 ApiUtils::validateJwtToken(self::generateValidJwtToken('foo'), 'bar');
148 } 148 }
149 149
150 /** 150 /**
151 * Test validateJwtToken() with a JWT token with a an invalid header (not JSON). 151 * Test validateJwtToken() with a JWT token with a an invalid header (not JSON).
152 *
153 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
154 * @expectedExceptionMessage Invalid JWT header
155 */ 152 */
156 public function testValidateJwtTokenInvalidHeader() 153 public function testValidateJwtTokenInvalidHeader()
157 { 154 {
155 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
156 $this->expectExceptionMessage('Invalid JWT header');
157
158 $token = $this->generateCustomJwtToken('notJSON', '{"JSON":1}', 'secret'); 158 $token = $this->generateCustomJwtToken('notJSON', '{"JSON":1}', 'secret');
159 ApiUtils::validateJwtToken($token, 'secret'); 159 ApiUtils::validateJwtToken($token, 'secret');
160 } 160 }
161 161
162 /** 162 /**
163 * Test validateJwtToken() with a JWT token with a an invalid payload (not JSON). 163 * Test validateJwtToken() with a JWT token with a an invalid payload (not JSON).
164 *
165 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
166 * @expectedExceptionMessage Invalid JWT payload
167 */ 164 */
168 public function testValidateJwtTokenInvalidPayload() 165 public function testValidateJwtTokenInvalidPayload()
169 { 166 {
167 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
168 $this->expectExceptionMessage('Invalid JWT payload');
169
170 $token = $this->generateCustomJwtToken('{"JSON":1}', 'notJSON', 'secret'); 170 $token = $this->generateCustomJwtToken('{"JSON":1}', 'notJSON', 'secret');
171 ApiUtils::validateJwtToken($token, 'secret'); 171 ApiUtils::validateJwtToken($token, 'secret');
172 } 172 }
173 173
174 /** 174 /**
175 * Test validateJwtToken() with a JWT token without issued time. 175 * Test validateJwtToken() with a JWT token without issued time.
176 *
177 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
178 * @expectedExceptionMessage Invalid JWT issued time
179 */ 176 */
180 public function testValidateJwtTokenInvalidTimeEmpty() 177 public function testValidateJwtTokenInvalidTimeEmpty()
181 { 178 {
179 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
180 $this->expectExceptionMessage('Invalid JWT issued time');
181
182 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"JSON":1}', 'secret'); 182 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"JSON":1}', 'secret');
183 ApiUtils::validateJwtToken($token, 'secret'); 183 ApiUtils::validateJwtToken($token, 'secret');
184 } 184 }
185 185
186 /** 186 /**
187 * Test validateJwtToken() with an expired JWT token. 187 * Test validateJwtToken() with an expired JWT token.
188 *
189 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
190 * @expectedExceptionMessage Invalid JWT issued time
191 */ 188 */
192 public function testValidateJwtTokenInvalidTimeExpired() 189 public function testValidateJwtTokenInvalidTimeExpired()
193 { 190 {
191 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
192 $this->expectExceptionMessage('Invalid JWT issued time');
193
194 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() - 600) . '}', 'secret'); 194 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() - 600) . '}', 'secret');
195 ApiUtils::validateJwtToken($token, 'secret'); 195 ApiUtils::validateJwtToken($token, 'secret');
196 } 196 }
197 197
198 /** 198 /**
199 * Test validateJwtToken() with a JWT token issued in the future. 199 * Test validateJwtToken() with a JWT token issued in the future.
200 *
201 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
202 * @expectedExceptionMessage Invalid JWT issued time
203 */ 200 */
204 public function testValidateJwtTokenInvalidTimeFuture() 201 public function testValidateJwtTokenInvalidTimeFuture()
205 { 202 {
203 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
204 $this->expectExceptionMessage('Invalid JWT issued time');
205
206 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() + 60) . '}', 'secret'); 206 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() + 60) . '}', 'secret');
207 ApiUtils::validateJwtToken($token, 'secret'); 207 ApiUtils::validateJwtToken($token, 'secret');
208 } 208 }
diff --git a/tests/api/controllers/history/HistoryTest.php b/tests/api/controllers/history/HistoryTest.php
index f4d3b646..84f8716e 100644
--- a/tests/api/controllers/history/HistoryTest.php
+++ b/tests/api/controllers/history/HistoryTest.php
@@ -11,7 +11,7 @@ use Slim\Http\Response;
11 11
12require_once 'tests/utils/ReferenceHistory.php'; 12require_once 'tests/utils/ReferenceHistory.php';
13 13
14class HistoryTest extends \PHPUnit\Framework\TestCase 14class HistoryTest extends \Shaarli\TestCase
15{ 15{
16 /** 16 /**
17 * @var string datastore to test write operations 17 * @var string datastore to test write operations
@@ -41,7 +41,7 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
41 /** 41 /**
42 * Before every test, instantiate a new Api with its config, plugins and bookmarks. 42 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
43 */ 43 */
44 public function setUp() 44 protected function setUp(): void
45 { 45 {
46 $this->conf = new ConfigManager('tests/utils/config/configJson'); 46 $this->conf = new ConfigManager('tests/utils/config/configJson');
47 $this->refHistory = new \ReferenceHistory(); 47 $this->refHistory = new \ReferenceHistory();
@@ -57,7 +57,7 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
57 /** 57 /**
58 * After every test, remove the test datastore. 58 * After every test, remove the test datastore.
59 */ 59 */
60 public function tearDown() 60 protected function tearDown(): void
61 { 61 {
62 @unlink(self::$testHistory); 62 @unlink(self::$testHistory);
63 } 63 }
diff --git a/tests/api/controllers/info/InfoTest.php b/tests/api/controllers/info/InfoTest.php
index b5c938e1..10b29ab2 100644
--- a/tests/api/controllers/info/InfoTest.php
+++ b/tests/api/controllers/info/InfoTest.php
@@ -1,10 +1,11 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use PHPUnit\Framework\TestCase; 4use malkusch\lock\mutex\NoMutex;
5use Shaarli\Bookmark\BookmarkFileService; 5use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Shaarli\History; 7use Shaarli\History;
8use Shaarli\TestCase;
8use Slim\Container; 9use Slim\Container;
9use Slim\Http\Environment; 10use Slim\Http\Environment;
10use Slim\Http\Request; 11use Slim\Http\Request;
@@ -47,8 +48,9 @@ class InfoTest extends TestCase
47 /** 48 /**
48 * Before every test, instantiate a new Api with its config, plugins and bookmarks. 49 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
49 */ 50 */
50 public function setUp() 51 protected function setUp(): void
51 { 52 {
53 $mutex = new NoMutex();
52 $this->conf = new ConfigManager('tests/utils/config/configJson'); 54 $this->conf = new ConfigManager('tests/utils/config/configJson');
53 $this->conf->set('resource.datastore', self::$testDatastore); 55 $this->conf->set('resource.datastore', self::$testDatastore);
54 $this->refDB = new \ReferenceLinkDB(); 56 $this->refDB = new \ReferenceLinkDB();
@@ -58,7 +60,7 @@ class InfoTest extends TestCase
58 60
59 $this->container = new Container(); 61 $this->container = new Container();
60 $this->container['conf'] = $this->conf; 62 $this->container['conf'] = $this->conf;
61 $this->container['db'] = new BookmarkFileService($this->conf, $history, true); 63 $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
62 $this->container['history'] = null; 64 $this->container['history'] = null;
63 65
64 $this->controller = new Info($this->container); 66 $this->controller = new Info($this->container);
@@ -67,7 +69,7 @@ class InfoTest extends TestCase
67 /** 69 /**
68 * After every test, remove the test datastore. 70 * After every test, remove the test datastore.
69 */ 71 */
70 public function tearDown() 72 protected function tearDown(): void
71 { 73 {
72 @unlink(self::$testDatastore); 74 @unlink(self::$testDatastore);
73 } 75 }
diff --git a/tests/api/controllers/links/DeleteLinkTest.php b/tests/api/controllers/links/DeleteLinkTest.php
index 6c2b3698..805c9be3 100644
--- a/tests/api/controllers/links/DeleteLinkTest.php
+++ b/tests/api/controllers/links/DeleteLinkTest.php
@@ -3,6 +3,7 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use malkusch\lock\mutex\NoMutex;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
8use Shaarli\History; 9use Shaarli\History;
@@ -11,7 +12,7 @@ use Slim\Http\Environment;
11use Slim\Http\Request; 12use Slim\Http\Request;
12use Slim\Http\Response; 13use Slim\Http\Response;
13 14
14class DeleteLinkTest extends \PHPUnit\Framework\TestCase 15class DeleteLinkTest extends \Shaarli\TestCase
15{ 16{
16 /** 17 /**
17 * @var string datastore to test write operations 18 * @var string datastore to test write operations
@@ -53,11 +54,15 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
53 */ 54 */
54 protected $controller; 55 protected $controller;
55 56
57 /** @var NoMutex */
58 protected $mutex;
59
56 /** 60 /**
57 * Before each test, instantiate a new Api with its config, plugins and bookmarks. 61 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
58 */ 62 */
59 public function setUp() 63 protected function setUp(): void
60 { 64 {
65 $this->mutex = new NoMutex();
61 $this->conf = new ConfigManager('tests/utils/config/configJson'); 66 $this->conf = new ConfigManager('tests/utils/config/configJson');
62 $this->conf->set('resource.datastore', self::$testDatastore); 67 $this->conf->set('resource.datastore', self::$testDatastore);
63 $this->refDB = new \ReferenceLinkDB(); 68 $this->refDB = new \ReferenceLinkDB();
@@ -65,7 +70,7 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
65 $refHistory = new \ReferenceHistory(); 70 $refHistory = new \ReferenceHistory();
66 $refHistory->write(self::$testHistory); 71 $refHistory->write(self::$testHistory);
67 $this->history = new History(self::$testHistory); 72 $this->history = new History(self::$testHistory);
68 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 73 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
69 74
70 $this->container = new Container(); 75 $this->container = new Container();
71 $this->container['conf'] = $this->conf; 76 $this->container['conf'] = $this->conf;
@@ -78,7 +83,7 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
78 /** 83 /**
79 * After each test, remove the test datastore. 84 * After each test, remove the test datastore.
80 */ 85 */
81 public function tearDown() 86 protected function tearDown(): void
82 { 87 {
83 @unlink(self::$testDatastore); 88 @unlink(self::$testDatastore);
84 @unlink(self::$testHistory); 89 @unlink(self::$testHistory);
@@ -100,7 +105,7 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
100 $this->assertEquals(204, $response->getStatusCode()); 105 $this->assertEquals(204, $response->getStatusCode());
101 $this->assertEmpty((string) $response->getBody()); 106 $this->assertEmpty((string) $response->getBody());
102 107
103 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 108 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
104 $this->assertFalse($this->bookmarkService->exists($id)); 109 $this->assertFalse($this->bookmarkService->exists($id));
105 110
106 $historyEntry = $this->history->getHistory()[0]; 111 $historyEntry = $this->history->getHistory()[0];
@@ -113,11 +118,11 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
113 118
114 /** 119 /**
115 * Test DELETE link endpoint: reach not existing ID. 120 * Test DELETE link endpoint: reach not existing ID.
116 *
117 * @expectedException \Shaarli\Api\Exceptions\ApiLinkNotFoundException
118 */ 121 */
119 public function testDeleteLink404() 122 public function testDeleteLink404()
120 { 123 {
124 $this->expectException(\Shaarli\Api\Exceptions\ApiLinkNotFoundException::class);
125
121 $id = -1; 126 $id = -1;
122 $this->assertFalse($this->bookmarkService->exists($id)); 127 $this->assertFalse($this->bookmarkService->exists($id));
123 $env = Environment::mock([ 128 $env = Environment::mock([
diff --git a/tests/api/controllers/links/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php
index c26411ac..1ec56ef3 100644
--- a/tests/api/controllers/links/GetLinkIdTest.php
+++ b/tests/api/controllers/links/GetLinkIdTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
@@ -20,7 +21,7 @@ use Slim\Http\Response;
20 * 21 *
21 * @package Shaarli\Api\Controllers 22 * @package Shaarli\Api\Controllers
22 */ 23 */
23class GetLinkIdTest extends \PHPUnit\Framework\TestCase 24class GetLinkIdTest extends \Shaarli\TestCase
24{ 25{
25 /** 26 /**
26 * @var string datastore to test write operations 27 * @var string datastore to test write operations
@@ -55,8 +56,9 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
55 /** 56 /**
56 * Before each test, instantiate a new Api with its config, plugins and bookmarks. 57 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
57 */ 58 */
58 public function setUp() 59 protected function setUp(): void
59 { 60 {
61 $mutex = new NoMutex();
60 $this->conf = new ConfigManager('tests/utils/config/configJson'); 62 $this->conf = new ConfigManager('tests/utils/config/configJson');
61 $this->conf->set('resource.datastore', self::$testDatastore); 63 $this->conf->set('resource.datastore', self::$testDatastore);
62 $this->refDB = new \ReferenceLinkDB(); 64 $this->refDB = new \ReferenceLinkDB();
@@ -65,7 +67,7 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
65 67
66 $this->container = new Container(); 68 $this->container = new Container();
67 $this->container['conf'] = $this->conf; 69 $this->container['conf'] = $this->conf;
68 $this->container['db'] = new BookmarkFileService($this->conf, $history, true); 70 $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
69 $this->container['history'] = null; 71 $this->container['history'] = null;
70 72
71 $this->controller = new Links($this->container); 73 $this->controller = new Links($this->container);
@@ -74,7 +76,7 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
74 /** 76 /**
75 * After each test, remove the test datastore. 77 * After each test, remove the test datastore.
76 */ 78 */
77 public function tearDown() 79 protected function tearDown(): void
78 { 80 {
79 @unlink(self::$testDatastore); 81 @unlink(self::$testDatastore);
80 } 82 }
@@ -102,7 +104,7 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
102 $this->assertEquals($id, $data['id']); 104 $this->assertEquals($id, $data['id']);
103 105
104 // Check link elements 106 // Check link elements
105 $this->assertEquals('http://domain.tld/?WDWyig', $data['url']); 107 $this->assertEquals('http://domain.tld/shaare/WDWyig', $data['url']);
106 $this->assertEquals('WDWyig', $data['shorturl']); 108 $this->assertEquals('WDWyig', $data['shorturl']);
107 $this->assertEquals('Link title: @website', $data['title']); 109 $this->assertEquals('Link title: @website', $data['title']);
108 $this->assertEquals( 110 $this->assertEquals(
@@ -120,12 +122,12 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
120 122
121 /** 123 /**
122 * Test basic getLink service: get non existent link => ApiLinkNotFoundException. 124 * Test basic getLink service: get non existent link => ApiLinkNotFoundException.
123 *
124 * @expectedException Shaarli\Api\Exceptions\ApiLinkNotFoundException
125 * @expectedExceptionMessage Link not found
126 */ 125 */
127 public function testGetLink404() 126 public function testGetLink404()
128 { 127 {
128 $this->expectException(\Shaarli\Api\Exceptions\ApiLinkNotFoundException::class);
129 $this->expectExceptionMessage('Link not found');
130
129 $env = Environment::mock([ 131 $env = Environment::mock([
130 'REQUEST_METHOD' => 'GET', 132 'REQUEST_METHOD' => 'GET',
131 ]); 133 ]);
diff --git a/tests/api/controllers/links/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php
index 4e2d55ac..b1c46ee2 100644
--- a/tests/api/controllers/links/GetLinksTest.php
+++ b/tests/api/controllers/links/GetLinksTest.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use malkusch\lock\mutex\NoMutex;
4use Shaarli\Bookmark\Bookmark; 5use Shaarli\Bookmark\Bookmark;
5use Shaarli\Bookmark\BookmarkFileService; 6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Bookmark\LinkDB; 7use Shaarli\Bookmark\LinkDB;
@@ -20,7 +21,7 @@ use Slim\Http\Response;
20 * 21 *
21 * @package Shaarli\Api\Controllers 22 * @package Shaarli\Api\Controllers
22 */ 23 */
23class GetLinksTest extends \PHPUnit\Framework\TestCase 24class GetLinksTest extends \Shaarli\TestCase
24{ 25{
25 /** 26 /**
26 * @var string datastore to test write operations 27 * @var string datastore to test write operations
@@ -55,8 +56,9 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
55 /** 56 /**
56 * Before every test, instantiate a new Api with its config, plugins and bookmarks. 57 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
57 */ 58 */
58 public function setUp() 59 protected function setUp(): void
59 { 60 {
61 $mutex = new NoMutex();
60 $this->conf = new ConfigManager('tests/utils/config/configJson'); 62 $this->conf = new ConfigManager('tests/utils/config/configJson');
61 $this->conf->set('resource.datastore', self::$testDatastore); 63 $this->conf->set('resource.datastore', self::$testDatastore);
62 $this->refDB = new \ReferenceLinkDB(); 64 $this->refDB = new \ReferenceLinkDB();
@@ -65,7 +67,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
65 67
66 $this->container = new Container(); 68 $this->container = new Container();
67 $this->container['conf'] = $this->conf; 69 $this->container['conf'] = $this->conf;
68 $this->container['db'] = new BookmarkFileService($this->conf, $history, true); 70 $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
69 $this->container['history'] = null; 71 $this->container['history'] = null;
70 72
71 $this->controller = new Links($this->container); 73 $this->controller = new Links($this->container);
@@ -74,7 +76,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
74 /** 76 /**
75 * After every test, remove the test datastore. 77 * After every test, remove the test datastore.
76 */ 78 */
77 public function tearDown() 79 protected function tearDown(): void
78 { 80 {
79 @unlink(self::$testDatastore); 81 @unlink(self::$testDatastore);
80 } 82 }
@@ -109,7 +111,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
109 111
110 // Check first element fields 112 // Check first element fields
111 $first = $data[2]; 113 $first = $data[2];
112 $this->assertEquals('http://domain.tld/?WDWyig', $first['url']); 114 $this->assertEquals('http://domain.tld/shaare/WDWyig', $first['url']);
113 $this->assertEquals('WDWyig', $first['shorturl']); 115 $this->assertEquals('WDWyig', $first['shorturl']);
114 $this->assertEquals('Link title: @website', $first['title']); 116 $this->assertEquals('Link title: @website', $first['title']);
115 $this->assertEquals( 117 $this->assertEquals(
@@ -396,7 +398,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
396 $response = $this->controller->getLinks($request, new Response()); 398 $response = $this->controller->getLinks($request, new Response());
397 $this->assertEquals(200, $response->getStatusCode()); 399 $this->assertEquals(200, $response->getStatusCode());
398 $data = json_decode((string) $response->getBody(), true); 400 $data = json_decode((string) $response->getBody(), true);
399 $this->assertEquals(4, count($data)); 401 $this->assertEquals(5, count($data));
400 $this->assertEquals(6, $data[0]['id']); 402 $this->assertEquals(6, $data[0]['id']);
401 403
402 // wildcard: placeholder at the middle 404 // wildcard: placeholder at the middle
diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php
index 969b9fd9..e12f803b 100644
--- a/tests/api/controllers/links/PostLinkTest.php
+++ b/tests/api/controllers/links/PostLinkTest.php
@@ -2,11 +2,12 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use PHPUnit\Framework\TestCase; 5use malkusch\lock\mutex\NoMutex;
6use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\History; 9use Shaarli\History;
10use Shaarli\TestCase;
10use Slim\Container; 11use Slim\Container;
11use Slim\Http\Environment; 12use Slim\Http\Environment;
12use Slim\Http\Request; 13use Slim\Http\Request;
@@ -70,8 +71,9 @@ class PostLinkTest extends TestCase
70 /** 71 /**
71 * Before every test, instantiate a new Api with its config, plugins and bookmarks. 72 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
72 */ 73 */
73 public function setUp() 74 protected function setUp(): void
74 { 75 {
76 $mutex = new NoMutex();
75 $this->conf = new ConfigManager('tests/utils/config/configJson'); 77 $this->conf = new ConfigManager('tests/utils/config/configJson');
76 $this->conf->set('resource.datastore', self::$testDatastore); 78 $this->conf->set('resource.datastore', self::$testDatastore);
77 $this->refDB = new \ReferenceLinkDB(); 79 $this->refDB = new \ReferenceLinkDB();
@@ -79,7 +81,7 @@ class PostLinkTest extends TestCase
79 $refHistory = new \ReferenceHistory(); 81 $refHistory = new \ReferenceHistory();
80 $refHistory->write(self::$testHistory); 82 $refHistory->write(self::$testHistory);
81 $this->history = new History(self::$testHistory); 83 $this->history = new History(self::$testHistory);
82 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 84 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true);
83 85
84 $this->container = new Container(); 86 $this->container = new Container();
85 $this->container['conf'] = $this->conf; 87 $this->container['conf'] = $this->conf;
@@ -107,7 +109,7 @@ class PostLinkTest extends TestCase
107 /** 109 /**
108 * After every test, remove the test datastore. 110 * After every test, remove the test datastore.
109 */ 111 */
110 public function tearDown() 112 protected function tearDown(): void
111 { 113 {
112 @unlink(self::$testDatastore); 114 @unlink(self::$testDatastore);
113 @unlink(self::$testHistory); 115 @unlink(self::$testHistory);
@@ -131,8 +133,8 @@ class PostLinkTest extends TestCase
131 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 133 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
132 $this->assertEquals(43, $data['id']); 134 $this->assertEquals(43, $data['id']);
133 $this->assertRegExp('/[\w_-]{6}/', $data['shorturl']); 135 $this->assertRegExp('/[\w_-]{6}/', $data['shorturl']);
134 $this->assertEquals('http://domain.tld/?' . $data['shorturl'], $data['url']); 136 $this->assertEquals('http://domain.tld/shaare/' . $data['shorturl'], $data['url']);
135 $this->assertEquals('?' . $data['shorturl'], $data['title']); 137 $this->assertEquals('/shaare/' . $data['shorturl'], $data['title']);
136 $this->assertEquals('', $data['description']); 138 $this->assertEquals('', $data['description']);
137 $this->assertEquals([], $data['tags']); 139 $this->assertEquals([], $data['tags']);
138 $this->assertEquals(true, $data['private']); 140 $this->assertEquals(true, $data['private']);
@@ -160,6 +162,8 @@ class PostLinkTest extends TestCase
160 'description' => 'shaare description', 162 'description' => 'shaare description',
161 'tags' => ['one', 'two'], 163 'tags' => ['one', 'two'],
162 'private' => true, 164 'private' => true,
165 'created' => '2015-05-05T12:30:00+03:00',
166 'updated' => '2016-06-05T14:32:10+03:00',
163 ]; 167 ];
164 $env = Environment::mock([ 168 $env = Environment::mock([
165 'REQUEST_METHOD' => 'POST', 169 'REQUEST_METHOD' => 'POST',
@@ -181,10 +185,8 @@ class PostLinkTest extends TestCase
181 $this->assertEquals($link['description'], $data['description']); 185 $this->assertEquals($link['description'], $data['description']);
182 $this->assertEquals($link['tags'], $data['tags']); 186 $this->assertEquals($link['tags'], $data['tags']);
183 $this->assertEquals(true, $data['private']); 187 $this->assertEquals(true, $data['private']);
184 $this->assertTrue( 188 $this->assertSame($link['created'], $data['created']);
185 new \DateTime('2 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 189 $this->assertSame($link['updated'], $data['updated']);
186 );
187 $this->assertEquals('', $data['updated']);
188 } 190 }
189 191
190 /** 192 /**
diff --git a/tests/api/controllers/links/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php
index cb63742e..240ee323 100644
--- a/tests/api/controllers/links/PutLinkTest.php
+++ b/tests/api/controllers/links/PutLinkTest.php
@@ -3,6 +3,7 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use malkusch\lock\mutex\NoMutex;
6use Shaarli\Bookmark\Bookmark; 7use Shaarli\Bookmark\Bookmark;
7use Shaarli\Bookmark\BookmarkFileService; 8use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
@@ -12,7 +13,7 @@ use Slim\Http\Environment;
12use Slim\Http\Request; 13use Slim\Http\Request;
13use Slim\Http\Response; 14use Slim\Http\Response;
14 15
15class PutLinkTest extends \PHPUnit\Framework\TestCase 16class PutLinkTest extends \Shaarli\TestCase
16{ 17{
17 /** 18 /**
18 * @var string datastore to test write operations 19 * @var string datastore to test write operations
@@ -62,8 +63,9 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
62 /** 63 /**
63 * Before every test, instantiate a new Api with its config, plugins and bookmarks. 64 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
64 */ 65 */
65 public function setUp() 66 protected function setUp(): void
66 { 67 {
68 $mutex = new NoMutex();
67 $this->conf = new ConfigManager('tests/utils/config/configJson'); 69 $this->conf = new ConfigManager('tests/utils/config/configJson');
68 $this->conf->set('resource.datastore', self::$testDatastore); 70 $this->conf->set('resource.datastore', self::$testDatastore);
69 $this->refDB = new \ReferenceLinkDB(); 71 $this->refDB = new \ReferenceLinkDB();
@@ -71,7 +73,7 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
71 $refHistory = new \ReferenceHistory(); 73 $refHistory = new \ReferenceHistory();
72 $refHistory->write(self::$testHistory); 74 $refHistory->write(self::$testHistory);
73 $this->history = new History(self::$testHistory); 75 $this->history = new History(self::$testHistory);
74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 76 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true);
75 77
76 $this->container = new Container(); 78 $this->container = new Container();
77 $this->container['conf'] = $this->conf; 79 $this->container['conf'] = $this->conf;
@@ -91,7 +93,7 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
91 /** 93 /**
92 * After every test, remove the test datastore. 94 * After every test, remove the test datastore.
93 */ 95 */
94 public function tearDown() 96 protected function tearDown(): void
95 { 97 {
96 @unlink(self::$testDatastore); 98 @unlink(self::$testDatastore);
97 @unlink(self::$testHistory); 99 @unlink(self::$testHistory);
@@ -114,8 +116,8 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
114 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 116 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
115 $this->assertEquals($id, $data['id']); 117 $this->assertEquals($id, $data['id']);
116 $this->assertEquals('WDWyig', $data['shorturl']); 118 $this->assertEquals('WDWyig', $data['shorturl']);
117 $this->assertEquals('http://domain.tld/?WDWyig', $data['url']); 119 $this->assertEquals('http://domain.tld/shaare/WDWyig', $data['url']);
118 $this->assertEquals('?WDWyig', $data['title']); 120 $this->assertEquals('/shaare/WDWyig', $data['title']);
119 $this->assertEquals('', $data['description']); 121 $this->assertEquals('', $data['description']);
120 $this->assertEquals([], $data['tags']); 122 $this->assertEquals([], $data['tags']);
121 $this->assertEquals(true, $data['private']); 123 $this->assertEquals(true, $data['private']);
@@ -218,12 +220,12 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
218 220
219 /** 221 /**
220 * Test link update on non existent link => ApiLinkNotFoundException. 222 * Test link update on non existent link => ApiLinkNotFoundException.
221 *
222 * @expectedException Shaarli\Api\Exceptions\ApiLinkNotFoundException
223 * @expectedExceptionMessage Link not found
224 */ 223 */
225 public function testGetLink404() 224 public function testGetLink404()
226 { 225 {
226 $this->expectException(\Shaarli\Api\Exceptions\ApiLinkNotFoundException::class);
227 $this->expectExceptionMessage('Link not found');
228
227 $env = Environment::mock([ 229 $env = Environment::mock([
228 'REQUEST_METHOD' => 'PUT', 230 'REQUEST_METHOD' => 'PUT',
229 ]); 231 ]);
diff --git a/tests/api/controllers/tags/DeleteTagTest.php b/tests/api/controllers/tags/DeleteTagTest.php
index c6748872..37f07229 100644
--- a/tests/api/controllers/tags/DeleteTagTest.php
+++ b/tests/api/controllers/tags/DeleteTagTest.php
@@ -3,6 +3,7 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use malkusch\lock\mutex\NoMutex;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\LinkDB; 8use Shaarli\Bookmark\LinkDB;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
@@ -12,7 +13,7 @@ use Slim\Http\Environment;
12use Slim\Http\Request; 13use Slim\Http\Request;
13use Slim\Http\Response; 14use Slim\Http\Response;
14 15
15class DeleteTagTest extends \PHPUnit\Framework\TestCase 16class DeleteTagTest extends \Shaarli\TestCase
16{ 17{
17 /** 18 /**
18 * @var string datastore to test write operations 19 * @var string datastore to test write operations
@@ -54,11 +55,15 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
54 */ 55 */
55 protected $controller; 56 protected $controller;
56 57
58 /** @var NoMutex */
59 protected $mutex;
60
57 /** 61 /**
58 * Before each test, instantiate a new Api with its config, plugins and bookmarks. 62 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
59 */ 63 */
60 public function setUp() 64 protected function setUp(): void
61 { 65 {
66 $this->mutex = new NoMutex();
62 $this->conf = new ConfigManager('tests/utils/config/configJson'); 67 $this->conf = new ConfigManager('tests/utils/config/configJson');
63 $this->conf->set('resource.datastore', self::$testDatastore); 68 $this->conf->set('resource.datastore', self::$testDatastore);
64 $this->refDB = new \ReferenceLinkDB(); 69 $this->refDB = new \ReferenceLinkDB();
@@ -66,7 +71,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
66 $refHistory = new \ReferenceHistory(); 71 $refHistory = new \ReferenceHistory();
67 $refHistory->write(self::$testHistory); 72 $refHistory->write(self::$testHistory);
68 $this->history = new History(self::$testHistory); 73 $this->history = new History(self::$testHistory);
69 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
70 75
71 $this->container = new Container(); 76 $this->container = new Container();
72 $this->container['conf'] = $this->conf; 77 $this->container['conf'] = $this->conf;
@@ -79,7 +84,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
79 /** 84 /**
80 * After each test, remove the test datastore. 85 * After each test, remove the test datastore.
81 */ 86 */
82 public function tearDown() 87 protected function tearDown(): void
83 { 88 {
84 @unlink(self::$testDatastore); 89 @unlink(self::$testDatastore);
85 @unlink(self::$testHistory); 90 @unlink(self::$testHistory);
@@ -102,7 +107,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
102 $this->assertEquals(204, $response->getStatusCode()); 107 $this->assertEquals(204, $response->getStatusCode());
103 $this->assertEmpty((string) $response->getBody()); 108 $this->assertEmpty((string) $response->getBody());
104 109
105 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 110 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
106 $tags = $this->bookmarkService->bookmarksCountPerTag(); 111 $tags = $this->bookmarkService->bookmarksCountPerTag();
107 $this->assertFalse(isset($tags[$tagName])); 112 $this->assertFalse(isset($tags[$tagName]));
108 113
@@ -136,7 +141,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
136 $this->assertEquals(204, $response->getStatusCode()); 141 $this->assertEquals(204, $response->getStatusCode());
137 $this->assertEmpty((string) $response->getBody()); 142 $this->assertEmpty((string) $response->getBody());
138 143
139 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 144 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
140 $tags = $this->bookmarkService->bookmarksCountPerTag(); 145 $tags = $this->bookmarkService->bookmarksCountPerTag();
141 $this->assertFalse(isset($tags[$tagName])); 146 $this->assertFalse(isset($tags[$tagName]));
142 $this->assertTrue($tags[strtolower($tagName)] > 0); 147 $this->assertTrue($tags[strtolower($tagName)] > 0);
@@ -150,12 +155,12 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
150 155
151 /** 156 /**
152 * Test DELETE tag endpoint: reach not existing tag. 157 * Test DELETE tag endpoint: reach not existing tag.
153 *
154 * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException
155 * @expectedExceptionMessage Tag not found
156 */ 158 */
157 public function testDeleteLink404() 159 public function testDeleteLink404()
158 { 160 {
161 $this->expectException(\Shaarli\Api\Exceptions\ApiTagNotFoundException::class);
162 $this->expectExceptionMessage('Tag not found');
163
159 $tagName = 'nopenope'; 164 $tagName = 'nopenope';
160 $tags = $this->bookmarkService->bookmarksCountPerTag(); 165 $tags = $this->bookmarkService->bookmarksCountPerTag();
161 $this->assertFalse(isset($tags[$tagName])); 166 $this->assertFalse(isset($tags[$tagName]));
diff --git a/tests/api/controllers/tags/GetTagNameTest.php b/tests/api/controllers/tags/GetTagNameTest.php
index b9a81f9b..878de5a4 100644
--- a/tests/api/controllers/tags/GetTagNameTest.php
+++ b/tests/api/controllers/tags/GetTagNameTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Bookmark\BookmarkFileService; 6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Bookmark\LinkDB; 7use Shaarli\Bookmark\LinkDB;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
@@ -18,7 +19,7 @@ use Slim\Http\Response;
18 * 19 *
19 * @package Shaarli\Api\Controllers 20 * @package Shaarli\Api\Controllers
20 */ 21 */
21class GetTagNameTest extends \PHPUnit\Framework\TestCase 22class GetTagNameTest extends \Shaarli\TestCase
22{ 23{
23 /** 24 /**
24 * @var string datastore to test write operations 25 * @var string datastore to test write operations
@@ -53,8 +54,9 @@ class GetTagNameTest extends \PHPUnit\Framework\TestCase
53 /** 54 /**
54 * Before each test, instantiate a new Api with its config, plugins and bookmarks. 55 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
55 */ 56 */
56 public function setUp() 57 protected function setUp(): void
57 { 58 {
59 $mutex = new NoMutex();
58 $this->conf = new ConfigManager('tests/utils/config/configJson'); 60 $this->conf = new ConfigManager('tests/utils/config/configJson');
59 $this->conf->set('resource.datastore', self::$testDatastore); 61 $this->conf->set('resource.datastore', self::$testDatastore);
60 $this->refDB = new \ReferenceLinkDB(); 62 $this->refDB = new \ReferenceLinkDB();
@@ -63,7 +65,7 @@ class GetTagNameTest extends \PHPUnit\Framework\TestCase
63 65
64 $this->container = new Container(); 66 $this->container = new Container();
65 $this->container['conf'] = $this->conf; 67 $this->container['conf'] = $this->conf;
66 $this->container['db'] = new BookmarkFileService($this->conf, $history, true); 68 $this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true);
67 $this->container['history'] = null; 69 $this->container['history'] = null;
68 70
69 $this->controller = new Tags($this->container); 71 $this->controller = new Tags($this->container);
@@ -72,7 +74,7 @@ class GetTagNameTest extends \PHPUnit\Framework\TestCase
72 /** 74 /**
73 * After each test, remove the test datastore. 75 * After each test, remove the test datastore.
74 */ 76 */
75 public function tearDown() 77 protected function tearDown(): void
76 { 78 {
77 @unlink(self::$testDatastore); 79 @unlink(self::$testDatastore);
78 } 80 }
@@ -117,12 +119,12 @@ class GetTagNameTest extends \PHPUnit\Framework\TestCase
117 119
118 /** 120 /**
119 * Test basic getTag service: get non existent tag => ApiTagNotFoundException. 121 * Test basic getTag service: get non existent tag => ApiTagNotFoundException.
120 *
121 * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException
122 * @expectedExceptionMessage Tag not found
123 */ 122 */
124 public function testGetTag404() 123 public function testGetTag404()
125 { 124 {
125 $this->expectException(\Shaarli\Api\Exceptions\ApiTagNotFoundException::class);
126 $this->expectExceptionMessage('Tag not found');
127
126 $env = Environment::mock([ 128 $env = Environment::mock([
127 'REQUEST_METHOD' => 'GET', 129 'REQUEST_METHOD' => 'GET',
128 ]); 130 ]);
diff --git a/tests/api/controllers/tags/GetTagsTest.php b/tests/api/controllers/tags/GetTagsTest.php
index 53a3326d..b565a8c4 100644
--- a/tests/api/controllers/tags/GetTagsTest.php
+++ b/tests/api/controllers/tags/GetTagsTest.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use malkusch\lock\mutex\NoMutex;
4use Shaarli\Bookmark\BookmarkFileService; 5use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Bookmark\LinkDB; 6use Shaarli\Bookmark\LinkDB;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
@@ -17,7 +18,7 @@ use Slim\Http\Response;
17 * 18 *
18 * @package Shaarli\Api\Controllers 19 * @package Shaarli\Api\Controllers
19 */ 20 */
20class GetTagsTest extends \PHPUnit\Framework\TestCase 21class GetTagsTest extends \Shaarli\TestCase
21{ 22{
22 /** 23 /**
23 * @var string datastore to test write operations 24 * @var string datastore to test write operations
@@ -57,15 +58,16 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
57 /** 58 /**
58 * Before every test, instantiate a new Api with its config, plugins and bookmarks. 59 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
59 */ 60 */
60 public function setUp() 61 protected function setUp(): void
61 { 62 {
63 $mutex = new NoMutex();
62 $this->conf = new ConfigManager('tests/utils/config/configJson'); 64 $this->conf = new ConfigManager('tests/utils/config/configJson');
63 $this->conf->set('resource.datastore', self::$testDatastore); 65 $this->conf->set('resource.datastore', self::$testDatastore);
64 $this->refDB = new \ReferenceLinkDB(); 66 $this->refDB = new \ReferenceLinkDB();
65 $this->refDB->write(self::$testDatastore); 67 $this->refDB->write(self::$testDatastore);
66 $history = new History('sandbox/history.php'); 68 $history = new History('sandbox/history.php');
67 69
68 $this->bookmarkService = new BookmarkFileService($this->conf, $history, true); 70 $this->bookmarkService = new BookmarkFileService($this->conf, $history, $mutex, true);
69 71
70 $this->container = new Container(); 72 $this->container = new Container();
71 $this->container['conf'] = $this->conf; 73 $this->container['conf'] = $this->conf;
@@ -78,7 +80,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
78 /** 80 /**
79 * After every test, remove the test datastore. 81 * After every test, remove the test datastore.
80 */ 82 */
81 public function tearDown() 83 protected function tearDown(): void
82 { 84 {
83 @unlink(self::$testDatastore); 85 @unlink(self::$testDatastore);
84 } 86 }
diff --git a/tests/api/controllers/tags/PutTagTest.php b/tests/api/controllers/tags/PutTagTest.php
index 2a3cc15a..c73f6d3b 100644
--- a/tests/api/controllers/tags/PutTagTest.php
+++ b/tests/api/controllers/tags/PutTagTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use malkusch\lock\mutex\NoMutex;
5use Shaarli\Api\Exceptions\ApiBadParametersException; 6use Shaarli\Api\Exceptions\ApiBadParametersException;
6use Shaarli\Bookmark\BookmarkFileService; 7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\LinkDB; 8use Shaarli\Bookmark\LinkDB;
@@ -12,7 +13,7 @@ use Slim\Http\Environment;
12use Slim\Http\Request; 13use Slim\Http\Request;
13use Slim\Http\Response; 14use Slim\Http\Response;
14 15
15class PutTagTest extends \PHPUnit\Framework\TestCase 16class PutTagTest extends \Shaarli\TestCase
16{ 17{
17 /** 18 /**
18 * @var string datastore to test write operations 19 * @var string datastore to test write operations
@@ -62,8 +63,9 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
62 /** 63 /**
63 * Before every test, instantiate a new Api with its config, plugins and bookmarks. 64 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
64 */ 65 */
65 public function setUp() 66 protected function setUp(): void
66 { 67 {
68 $mutex = new NoMutex();
67 $this->conf = new ConfigManager('tests/utils/config/configJson'); 69 $this->conf = new ConfigManager('tests/utils/config/configJson');
68 $this->conf->set('resource.datastore', self::$testDatastore); 70 $this->conf->set('resource.datastore', self::$testDatastore);
69 $this->refDB = new \ReferenceLinkDB(); 71 $this->refDB = new \ReferenceLinkDB();
@@ -71,7 +73,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
71 $refHistory = new \ReferenceHistory(); 73 $refHistory = new \ReferenceHistory();
72 $refHistory->write(self::$testHistory); 74 $refHistory->write(self::$testHistory);
73 $this->history = new History(self::$testHistory); 75 $this->history = new History(self::$testHistory);
74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 76 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true);
75 77
76 $this->container = new Container(); 78 $this->container = new Container();
77 $this->container['conf'] = $this->conf; 79 $this->container['conf'] = $this->conf;
@@ -84,7 +86,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
84 /** 86 /**
85 * After every test, remove the test datastore. 87 * After every test, remove the test datastore.
86 */ 88 */
87 public function tearDown() 89 protected function tearDown(): void
88 { 90 {
89 @unlink(self::$testDatastore); 91 @unlink(self::$testDatastore);
90 @unlink(self::$testHistory); 92 @unlink(self::$testHistory);
@@ -159,12 +161,12 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
159 161
160 /** 162 /**
161 * Test tag update with an empty new tag name => ApiBadParametersException 163 * Test tag update with an empty new tag name => ApiBadParametersException
162 *
163 * @expectedException Shaarli\Api\Exceptions\ApiBadParametersException
164 * @expectedExceptionMessage New tag name is required in the request body
165 */ 164 */
166 public function testPutTagEmpty() 165 public function testPutTagEmpty()
167 { 166 {
167 $this->expectException(\Shaarli\Api\Exceptions\ApiBadParametersException::class);
168 $this->expectExceptionMessage('New tag name is required in the request body');
169
168 $tagName = 'gnu'; 170 $tagName = 'gnu';
169 $newName = ''; 171 $newName = '';
170 172
@@ -194,12 +196,12 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
194 196
195 /** 197 /**
196 * Test tag update on non existent tag => ApiTagNotFoundException. 198 * Test tag update on non existent tag => ApiTagNotFoundException.
197 *
198 * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException
199 * @expectedExceptionMessage Tag not found
200 */ 199 */
201 public function testPutTag404() 200 public function testPutTag404()
202 { 201 {
202 $this->expectException(\Shaarli\Api\Exceptions\ApiTagNotFoundException::class);
203 $this->expectExceptionMessage('Tag not found');
204
203 $env = Environment::mock([ 205 $env = Environment::mock([
204 'REQUEST_METHOD' => 'PUT', 206 'REQUEST_METHOD' => 'PUT',
205 ]); 207 ]);
diff --git a/tests/bookmark/BookmarkArrayTest.php b/tests/bookmark/BookmarkArrayTest.php
index 0f8f04c5..1953078c 100644
--- a/tests/bookmark/BookmarkArrayTest.php
+++ b/tests/bookmark/BookmarkArrayTest.php
@@ -2,10 +2,7 @@
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use PHPUnit\Framework\TestCase; 5use Shaarli\TestCase;
6use Shaarli\Bookmark\Exception\InvalidBookmarkException;
7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
9 6
10/** 7/**
11 * Class BookmarkArrayTest 8 * Class BookmarkArrayTest
@@ -47,22 +44,22 @@ class BookmarkArrayTest extends TestCase
47 44
48 /** 45 /**
49 * Test adding a bad entry: wrong type 46 * Test adding a bad entry: wrong type
50 *
51 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
52 */ 47 */
53 public function testArrayAccessAddBadEntryInstance() 48 public function testArrayAccessAddBadEntryInstance()
54 { 49 {
50 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
51
55 $array = new BookmarkArray(); 52 $array = new BookmarkArray();
56 $array[] = 'nope'; 53 $array[] = 'nope';
57 } 54 }
58 55
59 /** 56 /**
60 * Test adding a bad entry: no id 57 * Test adding a bad entry: no id
61 *
62 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
63 */ 58 */
64 public function testArrayAccessAddBadEntryNoId() 59 public function testArrayAccessAddBadEntryNoId()
65 { 60 {
61 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
62
66 $array = new BookmarkArray(); 63 $array = new BookmarkArray();
67 $bookmark = new Bookmark(); 64 $bookmark = new Bookmark();
68 $array[] = $bookmark; 65 $array[] = $bookmark;
@@ -70,11 +67,11 @@ class BookmarkArrayTest extends TestCase
70 67
71 /** 68 /**
72 * Test adding a bad entry: no url 69 * Test adding a bad entry: no url
73 *
74 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
75 */ 70 */
76 public function testArrayAccessAddBadEntryNoUrl() 71 public function testArrayAccessAddBadEntryNoUrl()
77 { 72 {
73 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
74
78 $array = new BookmarkArray(); 75 $array = new BookmarkArray();
79 $bookmark = (new Bookmark())->setId(11); 76 $bookmark = (new Bookmark())->setId(11);
80 $array[] = $bookmark; 77 $array[] = $bookmark;
@@ -82,11 +79,11 @@ class BookmarkArrayTest extends TestCase
82 79
83 /** 80 /**
84 * Test adding a bad entry: invalid offset 81 * Test adding a bad entry: invalid offset
85 *
86 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
87 */ 82 */
88 public function testArrayAccessAddBadEntryOffset() 83 public function testArrayAccessAddBadEntryOffset()
89 { 84 {
85 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
86
90 $array = new BookmarkArray(); 87 $array = new BookmarkArray();
91 $bookmark = (new Bookmark())->setId(11); 88 $bookmark = (new Bookmark())->setId(11);
92 $bookmark->validate(); 89 $bookmark->validate();
@@ -94,25 +91,12 @@ class BookmarkArrayTest extends TestCase
94 } 91 }
95 92
96 /** 93 /**
97 * Test adding a bad entry: invalid ID type
98 *
99 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
100 */
101 public function testArrayAccessAddBadEntryIdType()
102 {
103 $array = new BookmarkArray();
104 $bookmark = (new Bookmark())->setId('nope');
105 $bookmark->validate();
106 $array[] = $bookmark;
107 }
108
109 /**
110 * Test adding a bad entry: ID/offset not consistent 94 * Test adding a bad entry: ID/offset not consistent
111 *
112 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
113 */ 95 */
114 public function testArrayAccessAddBadEntryIdOffset() 96 public function testArrayAccessAddBadEntryIdOffset()
115 { 97 {
98 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
99
116 $array = new BookmarkArray(); 100 $array = new BookmarkArray();
117 $bookmark = (new Bookmark())->setId(11); 101 $bookmark = (new Bookmark())->setId(11);
118 $bookmark->validate(); 102 $bookmark->validate();
diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php
index 4900d41d..f619aff3 100644
--- a/tests/bookmark/BookmarkFileServiceTest.php
+++ b/tests/bookmark/BookmarkFileServiceTest.php
@@ -6,7 +6,7 @@
6namespace Shaarli\Bookmark; 6namespace Shaarli\Bookmark;
7 7
8use DateTime; 8use DateTime;
9use PHPUnit\Framework\TestCase; 9use malkusch\lock\mutex\NoMutex;
10use ReferenceLinkDB; 10use ReferenceLinkDB;
11use ReflectionClass; 11use ReflectionClass;
12use Shaarli; 12use Shaarli;
@@ -14,6 +14,7 @@ use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
14use Shaarli\Config\ConfigManager; 14use Shaarli\Config\ConfigManager;
15use Shaarli\Formatter\BookmarkMarkdownFormatter; 15use Shaarli\Formatter\BookmarkMarkdownFormatter;
16use Shaarli\History; 16use Shaarli\History;
17use Shaarli\TestCase;
17 18
18/** 19/**
19 * Unitary tests for LegacyLinkDBTest 20 * Unitary tests for LegacyLinkDBTest
@@ -52,6 +53,9 @@ class BookmarkFileServiceTest extends TestCase
52 */ 53 */
53 protected $privateLinkDB = null; 54 protected $privateLinkDB = null;
54 55
56 /** @var NoMutex */
57 protected $mutex;
58
55 /** 59 /**
56 * Instantiates public and private LinkDBs with test data 60 * Instantiates public and private LinkDBs with test data
57 * 61 *
@@ -66,8 +70,10 @@ class BookmarkFileServiceTest extends TestCase
66 * 70 *
67 * Resets test data for each test 71 * Resets test data for each test
68 */ 72 */
69 protected function setUp() 73 protected function setUp(): void
70 { 74 {
75 $this->mutex = new NoMutex();
76
71 if (file_exists(self::$testDatastore)) { 77 if (file_exists(self::$testDatastore)) {
72 unlink(self::$testDatastore); 78 unlink(self::$testDatastore);
73 } 79 }
@@ -87,8 +93,8 @@ class BookmarkFileServiceTest extends TestCase
87 $this->refDB = new \ReferenceLinkDB(); 93 $this->refDB = new \ReferenceLinkDB();
88 $this->refDB->write(self::$testDatastore); 94 $this->refDB->write(self::$testDatastore);
89 $this->history = new History('sandbox/history.php'); 95 $this->history = new History('sandbox/history.php');
90 $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, false); 96 $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
91 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 97 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
92 } 98 }
93 99
94 /** 100 /**
@@ -105,7 +111,7 @@ class BookmarkFileServiceTest extends TestCase
105 $db = self::getMethod('migrate'); 111 $db = self::getMethod('migrate');
106 $db->invokeArgs($this->privateLinkDB, []); 112 $db->invokeArgs($this->privateLinkDB, []);
107 113
108 $db = new \FakeBookmarkService($this->conf, $this->history, true); 114 $db = new \FakeBookmarkService($this->conf, $this->history, $this->mutex, true);
109 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); 115 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
110 $this->assertEquals($this->refDB->countLinks(), $db->count()); 116 $this->assertEquals($this->refDB->countLinks(), $db->count());
111 } 117 }
@@ -134,11 +140,11 @@ class BookmarkFileServiceTest extends TestCase
134 140
135 /** 141 /**
136 * Test get() method for an undefined bookmark 142 * Test get() method for an undefined bookmark
137 *
138 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
139 */ 143 */
140 public function testGetUndefined() 144 public function testGetUndefined()
141 { 145 {
146 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
147
142 $this->privateLinkDB->get(666); 148 $this->privateLinkDB->get(666);
143 } 149 }
144 150
@@ -174,7 +180,7 @@ class BookmarkFileServiceTest extends TestCase
174 $this->assertEquals($updated, $bookmark->getUpdated()); 180 $this->assertEquals($updated, $bookmark->getUpdated());
175 181
176 // reload from file 182 // reload from file
177 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 183 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
178 184
179 $bookmark = $this->privateLinkDB->get(43); 185 $bookmark = $this->privateLinkDB->get(43);
180 $this->assertEquals(43, $bookmark->getId()); 186 $this->assertEquals(43, $bookmark->getId());
@@ -200,7 +206,7 @@ class BookmarkFileServiceTest extends TestCase
200 206
201 $bookmark = $this->privateLinkDB->get(43); 207 $bookmark = $this->privateLinkDB->get(43);
202 $this->assertEquals(43, $bookmark->getId()); 208 $this->assertEquals(43, $bookmark->getId());
203 $this->assertRegExp('/\?[\w\-]{6}/', $bookmark->getUrl()); 209 $this->assertRegExp('#/shaare/[\w\-]{6}#', $bookmark->getUrl());
204 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl()); 210 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
205 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle()); 211 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
206 $this->assertEmpty($bookmark->getDescription()); 212 $this->assertEmpty($bookmark->getDescription());
@@ -212,11 +218,11 @@ class BookmarkFileServiceTest extends TestCase
212 $this->assertNull($bookmark->getUpdated()); 218 $this->assertNull($bookmark->getUpdated());
213 219
214 // reload from file 220 // reload from file
215 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 221 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
216 222
217 $bookmark = $this->privateLinkDB->get(43); 223 $bookmark = $this->privateLinkDB->get(43);
218 $this->assertEquals(43, $bookmark->getId()); 224 $this->assertEquals(43, $bookmark->getId());
219 $this->assertRegExp('/\?[\w\-]{6}/', $bookmark->getUrl()); 225 $this->assertRegExp('#/shaare/[\w\-]{6}#', $bookmark->getUrl());
220 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl()); 226 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
221 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle()); 227 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
222 $this->assertEmpty($bookmark->getDescription()); 228 $this->assertEmpty($bookmark->getDescription());
@@ -230,53 +236,42 @@ class BookmarkFileServiceTest extends TestCase
230 236
231 /** 237 /**
232 * Test add() method for a bookmark without any field set and without writing the data store 238 * Test add() method for a bookmark without any field set and without writing the data store
233 *
234 * @expectedExceptionMessage Shaarli\Bookmark\Exception\BookmarkNotFoundException
235 */ 239 */
236 public function testAddMinimalNoWrite() 240 public function testAddMinimalNoWrite()
237 { 241 {
242 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
243
238 $bookmark = new Bookmark(); 244 $bookmark = new Bookmark();
239 $this->privateLinkDB->add($bookmark); 245 $this->privateLinkDB->add($bookmark, false);
240 246
241 $bookmark = $this->privateLinkDB->get(43); 247 $bookmark = $this->privateLinkDB->get(43);
242 $this->assertEquals(43, $bookmark->getId()); 248 $this->assertEquals(43, $bookmark->getId());
243 249
244 // reload from file 250 // reload from file
245 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 251 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
246 252
247 $this->privateLinkDB->get(43); 253 $this->privateLinkDB->get(43);
248 } 254 }
249 255
250 /** 256 /**
251 * Test add() method while logged out 257 * Test add() method while logged out
252 *
253 * @expectedException \Exception
254 * @expectedExceptionMessage You're not authorized to alter the datastore
255 */ 258 */
256 public function testAddLoggedOut() 259 public function testAddLoggedOut()
257 { 260 {
258 $this->publicLinkDB->add(new Bookmark()); 261 $this->expectException(\Exception::class);
259 } 262 $this->expectExceptionMessage('You\'re not authorized to alter the datastore');
260 263
261 /** 264 $this->publicLinkDB->add(new Bookmark());
262 * Test add() method with an entry which is not a bookmark instance
263 *
264 * @expectedException \Exception
265 * @expectedExceptionMessage Provided data is invalid
266 */
267 public function testAddNotABookmark()
268 {
269 $this->privateLinkDB->add(['title' => 'hi!']);
270 } 265 }
271 266
272 /** 267 /**
273 * Test add() method with a Bookmark already containing an ID 268 * Test add() method with a Bookmark already containing an ID
274 *
275 * @expectedException \Exception
276 * @expectedExceptionMessage This bookmarks already exists
277 */ 269 */
278 public function testAddWithId() 270 public function testAddWithId()
279 { 271 {
272 $this->expectException(\Exception::class);
273 $this->expectExceptionMessage('This bookmarks already exists');
274
280 $bookmark = new Bookmark(); 275 $bookmark = new Bookmark();
281 $bookmark->setId(43); 276 $bookmark->setId(43);
282 $this->privateLinkDB->add($bookmark); 277 $this->privateLinkDB->add($bookmark);
@@ -314,7 +309,7 @@ class BookmarkFileServiceTest extends TestCase
314 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); 309 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
315 310
316 // reload from file 311 // reload from file
317 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 312 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
318 313
319 $bookmark = $this->privateLinkDB->get(42); 314 $bookmark = $this->privateLinkDB->get(42);
320 $this->assertEquals(42, $bookmark->getId()); 315 $this->assertEquals(42, $bookmark->getId());
@@ -340,7 +335,7 @@ class BookmarkFileServiceTest extends TestCase
340 335
341 $bookmark = $this->privateLinkDB->get(42); 336 $bookmark = $this->privateLinkDB->get(42);
342 $this->assertEquals(42, $bookmark->getId()); 337 $this->assertEquals(42, $bookmark->getId());
343 $this->assertEquals('?WDWyig', $bookmark->getUrl()); 338 $this->assertEquals('/shaare/WDWyig', $bookmark->getUrl());
344 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl()); 339 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
345 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle()); 340 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
346 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription()); 341 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
@@ -355,11 +350,11 @@ class BookmarkFileServiceTest extends TestCase
355 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); 350 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
356 351
357 // reload from file 352 // reload from file
358 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 353 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
359 354
360 $bookmark = $this->privateLinkDB->get(42); 355 $bookmark = $this->privateLinkDB->get(42);
361 $this->assertEquals(42, $bookmark->getId()); 356 $this->assertEquals(42, $bookmark->getId());
362 $this->assertEquals('?WDWyig', $bookmark->getUrl()); 357 $this->assertEquals('/shaare/WDWyig', $bookmark->getUrl());
363 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl()); 358 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
364 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle()); 359 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
365 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription()); 360 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
@@ -388,7 +383,7 @@ class BookmarkFileServiceTest extends TestCase
388 $this->assertEquals($title, $bookmark->getTitle()); 383 $this->assertEquals($title, $bookmark->getTitle());
389 384
390 // reload from file 385 // reload from file
391 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 386 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
392 387
393 $bookmark = $this->privateLinkDB->get(42); 388 $bookmark = $this->privateLinkDB->get(42);
394 $this->assertEquals(42, $bookmark->getId()); 389 $this->assertEquals(42, $bookmark->getId());
@@ -397,44 +392,33 @@ class BookmarkFileServiceTest extends TestCase
397 392
398 /** 393 /**
399 * Test set() method while logged out 394 * Test set() method while logged out
400 *
401 * @expectedException \Exception
402 * @expectedExceptionMessage You're not authorized to alter the datastore
403 */ 395 */
404 public function testSetLoggedOut() 396 public function testSetLoggedOut()
405 { 397 {
406 $this->publicLinkDB->set(new Bookmark()); 398 $this->expectException(\Exception::class);
407 } 399 $this->expectExceptionMessage('You\'re not authorized to alter the datastore');
408 400
409 /** 401 $this->publicLinkDB->set(new Bookmark());
410 * Test set() method with an entry which is not a bookmark instance
411 *
412 * @expectedException \Exception
413 * @expectedExceptionMessage Provided data is invalid
414 */
415 public function testSetNotABookmark()
416 {
417 $this->privateLinkDB->set(['title' => 'hi!']);
418 } 402 }
419 403
420 /** 404 /**
421 * Test set() method with a Bookmark without an ID defined. 405 * Test set() method with a Bookmark without an ID defined.
422 *
423 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
424 */ 406 */
425 public function testSetWithoutId() 407 public function testSetWithoutId()
426 { 408 {
409 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
410
427 $bookmark = new Bookmark(); 411 $bookmark = new Bookmark();
428 $this->privateLinkDB->set($bookmark); 412 $this->privateLinkDB->set($bookmark);
429 } 413 }
430 414
431 /** 415 /**
432 * Test set() method with a Bookmark with an unknow ID 416 * Test set() method with a Bookmark with an unknow ID
433 *
434 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
435 */ 417 */
436 public function testSetWithUnknownId() 418 public function testSetWithUnknownId()
437 { 419 {
420 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
421
438 $bookmark = new Bookmark(); 422 $bookmark = new Bookmark();
439 $bookmark->setId(666); 423 $bookmark->setId(666);
440 $this->privateLinkDB->set($bookmark); 424 $this->privateLinkDB->set($bookmark);
@@ -452,7 +436,7 @@ class BookmarkFileServiceTest extends TestCase
452 $this->assertEquals(43, $bookmark->getId()); 436 $this->assertEquals(43, $bookmark->getId());
453 437
454 // reload from file 438 // reload from file
455 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 439 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
456 440
457 $bookmark = $this->privateLinkDB->get(43); 441 $bookmark = $this->privateLinkDB->get(43);
458 $this->assertEquals(43, $bookmark->getId()); 442 $this->assertEquals(43, $bookmark->getId());
@@ -472,7 +456,7 @@ class BookmarkFileServiceTest extends TestCase
472 $this->assertEquals($title, $bookmark->getTitle()); 456 $this->assertEquals($title, $bookmark->getTitle());
473 457
474 // reload from file 458 // reload from file
475 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 459 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
476 460
477 $bookmark = $this->privateLinkDB->get(42); 461 $bookmark = $this->privateLinkDB->get(42);
478 $this->assertEquals(42, $bookmark->getId()); 462 $this->assertEquals(42, $bookmark->getId());
@@ -481,24 +465,13 @@ class BookmarkFileServiceTest extends TestCase
481 465
482 /** 466 /**
483 * Test addOrSet() method while logged out 467 * Test addOrSet() method while logged out
484 *
485 * @expectedException \Exception
486 * @expectedExceptionMessage You're not authorized to alter the datastore
487 */ 468 */
488 public function testAddOrSetLoggedOut() 469 public function testAddOrSetLoggedOut()
489 { 470 {
490 $this->publicLinkDB->addOrSet(new Bookmark()); 471 $this->expectException(\Exception::class);
491 } 472 $this->expectExceptionMessage('You\'re not authorized to alter the datastore');
492 473
493 /** 474 $this->publicLinkDB->addOrSet(new Bookmark());
494 * Test addOrSet() method with an entry which is not a bookmark instance
495 *
496 * @expectedException \Exception
497 * @expectedExceptionMessage Provided data is invalid
498 */
499 public function testAddOrSetNotABookmark()
500 {
501 $this->privateLinkDB->addOrSet(['title' => 'hi!']);
502 } 475 }
503 476
504 /** 477 /**
@@ -515,7 +488,7 @@ class BookmarkFileServiceTest extends TestCase
515 $this->assertEquals($title, $bookmark->getTitle()); 488 $this->assertEquals($title, $bookmark->getTitle());
516 489
517 // reload from file 490 // reload from file
518 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 491 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
519 492
520 $bookmark = $this->privateLinkDB->get(42); 493 $bookmark = $this->privateLinkDB->get(42);
521 $this->assertEquals(42, $bookmark->getId()); 494 $this->assertEquals(42, $bookmark->getId());
@@ -524,11 +497,11 @@ class BookmarkFileServiceTest extends TestCase
524 497
525 /** 498 /**
526 * Test remove() method with an existing Bookmark 499 * Test remove() method with an existing Bookmark
527 *
528 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
529 */ 500 */
530 public function testRemoveExisting() 501 public function testRemoveExisting()
531 { 502 {
503 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
504
532 $bookmark = $this->privateLinkDB->get(42); 505 $bookmark = $this->privateLinkDB->get(42);
533 $this->privateLinkDB->remove($bookmark); 506 $this->privateLinkDB->remove($bookmark);
534 507
@@ -541,41 +514,30 @@ class BookmarkFileServiceTest extends TestCase
541 $this->assertInstanceOf(BookmarkNotFoundException::class, $exception); 514 $this->assertInstanceOf(BookmarkNotFoundException::class, $exception);
542 515
543 // reload from file 516 // reload from file
544 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true); 517 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
545 518
546 $this->privateLinkDB->get(42); 519 $this->privateLinkDB->get(42);
547 } 520 }
548 521
549 /** 522 /**
550 * Test remove() method while logged out 523 * Test remove() method while logged out
551 *
552 * @expectedException \Exception
553 * @expectedExceptionMessage You're not authorized to alter the datastore
554 */ 524 */
555 public function testRemoveLoggedOut() 525 public function testRemoveLoggedOut()
556 { 526 {
527 $this->expectException(\Exception::class);
528 $this->expectExceptionMessage('You\'re not authorized to alter the datastore');
529
557 $bookmark = $this->privateLinkDB->get(42); 530 $bookmark = $this->privateLinkDB->get(42);
558 $this->publicLinkDB->remove($bookmark); 531 $this->publicLinkDB->remove($bookmark);
559 } 532 }
560 533
561 /** 534 /**
562 * Test remove() method with an entry which is not a bookmark instance
563 *
564 * @expectedException \Exception
565 * @expectedExceptionMessage Provided data is invalid
566 */
567 public function testRemoveNotABookmark()
568 {
569 $this->privateLinkDB->remove(['title' => 'hi!']);
570 }
571
572 /**
573 * Test remove() method with a Bookmark with an unknown ID 535 * Test remove() method with a Bookmark with an unknown ID
574 *
575 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
576 */ 536 */
577 public function testRemoveWithUnknownId() 537 public function testRemoveWithUnknownId()
578 { 538 {
539 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
540
579 $bookmark = new Bookmark(); 541 $bookmark = new Bookmark();
580 $bookmark->setId(666); 542 $bookmark->setId(666);
581 $this->privateLinkDB->remove($bookmark); 543 $this->privateLinkDB->remove($bookmark);
@@ -615,14 +577,18 @@ class BookmarkFileServiceTest extends TestCase
615 { 577 {
616 $dbSize = $this->privateLinkDB->count(); 578 $dbSize = $this->privateLinkDB->count();
617 $this->privateLinkDB->initialize(); 579 $this->privateLinkDB->initialize();
618 $this->assertEquals($dbSize + 2, $this->privateLinkDB->count()); 580 $this->assertEquals($dbSize + 3, $this->privateLinkDB->count());
619 $this->assertEquals( 581 $this->assertStringStartsWith(
620 'My secret stuff... - Pastebin.com', 582 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.',
621 $this->privateLinkDB->get(43)->getTitle() 583 $this->privateLinkDB->get(43)->getDescription()
622 ); 584 );
623 $this->assertEquals( 585 $this->assertStringStartsWith(
624 'The personal, minimalist, super-fast, database free, bookmarking service', 586 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.',
625 $this->privateLinkDB->get(44)->getTitle() 587 $this->privateLinkDB->get(44)->getDescription()
588 );
589 $this->assertStringStartsWith(
590 'Welcome to Shaarli!',
591 $this->privateLinkDB->get(45)->getDescription()
626 ); 592 );
627 } 593 }
628 594
@@ -631,18 +597,17 @@ class BookmarkFileServiceTest extends TestCase
631 * to make sure that nothing have been broken in the migration process. 597 * to make sure that nothing have been broken in the migration process.
632 * They mostly cover search/filters. Some of them might be redundant with the previous ones. 598 * They mostly cover search/filters. Some of them might be redundant with the previous ones.
633 */ 599 */
634
635 /** 600 /**
636 * Attempt to instantiate a LinkDB whereas the datastore is not writable 601 * Attempt to instantiate a LinkDB whereas the datastore is not writable
637 *
638 * @expectedException Shaarli\Bookmark\Exception\NotWritableDataStoreException
639 * @expectedExceptionMessageRegExp #Couldn't load data from the data store file "null".*#
640 */ 602 */
641 public function testConstructDatastoreNotWriteable() 603 public function testConstructDatastoreNotWriteable()
642 { 604 {
605 $this->expectException(\Shaarli\Bookmark\Exception\NotWritableDataStoreException::class);
606 $this->expectExceptionMessageRegExp('#Couldn\'t load data from the data store file "null".*#');
607
643 $conf = new ConfigManager('tests/utils/config/configJson'); 608 $conf = new ConfigManager('tests/utils/config/configJson');
644 $conf->set('resource.datastore', 'null/store.db'); 609 $conf->set('resource.datastore', 'null/store.db');
645 new BookmarkFileService($conf, $this->history, true); 610 new BookmarkFileService($conf, $this->history, $this->mutex, true);
646 } 611 }
647 612
648 /** 613 /**
@@ -652,7 +617,7 @@ class BookmarkFileServiceTest extends TestCase
652 { 617 {
653 unlink(self::$testDatastore); 618 unlink(self::$testDatastore);
654 $this->assertFileNotExists(self::$testDatastore); 619 $this->assertFileNotExists(self::$testDatastore);
655 new BookmarkFileService($this->conf, $this->history, true); 620 new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
656 $this->assertFileExists(self::$testDatastore); 621 $this->assertFileExists(self::$testDatastore);
657 622
658 // ensure the correct data has been written 623 // ensure the correct data has been written
@@ -666,7 +631,7 @@ class BookmarkFileServiceTest extends TestCase
666 { 631 {
667 unlink(self::$testDatastore); 632 unlink(self::$testDatastore);
668 $this->assertFileNotExists(self::$testDatastore); 633 $this->assertFileNotExists(self::$testDatastore);
669 $db = new \FakeBookmarkService($this->conf, $this->history, false); 634 $db = new \FakeBookmarkService($this->conf, $this->history, $this->mutex, false);
670 $this->assertFileNotExists(self::$testDatastore); 635 $this->assertFileNotExists(self::$testDatastore);
671 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); 636 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
672 $this->assertCount(0, $db->getBookmarks()); 637 $this->assertCount(0, $db->getBookmarks());
@@ -699,13 +664,13 @@ class BookmarkFileServiceTest extends TestCase
699 */ 664 */
700 public function testSave() 665 public function testSave()
701 { 666 {
702 $testDB = new BookmarkFileService($this->conf, $this->history, true); 667 $testDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
703 $dbSize = $testDB->count(); 668 $dbSize = $testDB->count();
704 669
705 $bookmark = new Bookmark(); 670 $bookmark = new Bookmark();
706 $testDB->add($bookmark); 671 $testDB->add($bookmark);
707 672
708 $testDB = new BookmarkFileService($this->conf, $this->history, true); 673 $testDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
709 $this->assertEquals($dbSize + 1, $testDB->count()); 674 $this->assertEquals($dbSize + 1, $testDB->count());
710 } 675 }
711 676
@@ -715,28 +680,12 @@ class BookmarkFileServiceTest extends TestCase
715 public function testCountHiddenPublic() 680 public function testCountHiddenPublic()
716 { 681 {
717 $this->conf->set('privacy.hide_public_links', true); 682 $this->conf->set('privacy.hide_public_links', true);
718 $linkDB = new BookmarkFileService($this->conf, $this->history, false); 683 $linkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
719 684
720 $this->assertEquals(0, $linkDB->count()); 685 $this->assertEquals(0, $linkDB->count());
721 } 686 }
722 687
723 /** 688 /**
724 * List the days for which bookmarks have been posted
725 */
726 public function testDays()
727 {
728 $this->assertEquals(
729 ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'],
730 $this->publicLinkDB->days()
731 );
732
733 $this->assertEquals(
734 ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'],
735 $this->privateLinkDB->days()
736 );
737 }
738
739 /**
740 * The URL corresponds to an existing entry in the DB 689 * The URL corresponds to an existing entry in the DB
741 */ 690 */
742 public function testGetKnownLinkFromURL() 691 public function testGetKnownLinkFromURL()
@@ -744,7 +693,7 @@ class BookmarkFileServiceTest extends TestCase
744 $link = $this->publicLinkDB->findByUrl('http://mediagoblin.org/'); 693 $link = $this->publicLinkDB->findByUrl('http://mediagoblin.org/');
745 694
746 $this->assertNotEquals(false, $link); 695 $this->assertNotEquals(false, $link);
747 $this->assertContains( 696 $this->assertContainsPolyfill(
748 'A free software media publishing platform', 697 'A free software media publishing platform',
749 $link->getDescription() 698 $link->getDescription()
750 ); 699 );
@@ -783,6 +732,10 @@ class BookmarkFileServiceTest extends TestCase
783 // They need to be grouped with the first case found - order by date DESC: `sTuff`. 732 // They need to be grouped with the first case found - order by date DESC: `sTuff`.
784 'sTuff' => 2, 733 'sTuff' => 2,
785 'ut' => 1, 734 'ut' => 1,
735 'assurance' => 1,
736 'coding-style' => 1,
737 'quality' => 1,
738 'standards' => 1,
786 ], 739 ],
787 $this->publicLinkDB->bookmarksCountPerTag() 740 $this->publicLinkDB->bookmarksCountPerTag()
788 ); 741 );
@@ -811,12 +764,15 @@ class BookmarkFileServiceTest extends TestCase
811 'tag3' => 1, 764 'tag3' => 1,
812 'tag4' => 1, 765 'tag4' => 1,
813 'ut' => 1, 766 'ut' => 1,
767 'assurance' => 1,
768 'coding-style' => 1,
769 'quality' => 1,
770 'standards' => 1,
814 ], 771 ],
815 $this->privateLinkDB->bookmarksCountPerTag() 772 $this->privateLinkDB->bookmarksCountPerTag()
816 ); 773 );
817 $this->assertEquals( 774 $this->assertEquals(
818 [ 775 [
819 'web' => 4,
820 'cartoon' => 2, 776 'cartoon' => 2,
821 'gnu' => 1, 777 'gnu' => 1,
822 'dev' => 1, 778 'dev' => 1,
@@ -833,7 +789,6 @@ class BookmarkFileServiceTest extends TestCase
833 ); 789 );
834 $this->assertEquals( 790 $this->assertEquals(
835 [ 791 [
836 'web' => 1,
837 'html' => 1, 792 'html' => 1,
838 'w3c' => 1, 793 'w3c' => 1,
839 'css' => 1, 794 'css' => 1,
@@ -894,39 +849,70 @@ class BookmarkFileServiceTest extends TestCase
894 public function testFilterHashValid() 849 public function testFilterHashValid()
895 { 850 {
896 $request = smallHash('20150310_114651'); 851 $request = smallHash('20150310_114651');
897 $this->assertEquals( 852 $this->assertSame(
898 1, 853 $request,
899 count($this->publicLinkDB->findByHash($request)) 854 $this->publicLinkDB->findByHash($request)->getShortUrl()
900 ); 855 );
901 $request = smallHash('20150310_114633' . 8); 856 $request = smallHash('20150310_114633' . 8);
902 $this->assertEquals( 857 $this->assertSame(
903 1, 858 $request,
904 count($this->publicLinkDB->findByHash($request)) 859 $this->publicLinkDB->findByHash($request)->getShortUrl()
905 ); 860 );
906 } 861 }
907 862
908 /** 863 /**
909 * Test filterHash() with an invalid smallhash. 864 * Test filterHash() with an invalid smallhash.
910 *
911 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
912 */ 865 */
913 public function testFilterHashInValid1() 866 public function testFilterHashInValid1()
914 { 867 {
868 $this->expectException(BookmarkNotFoundException::class);
869
915 $request = 'blabla'; 870 $request = 'blabla';
916 $this->publicLinkDB->findByHash($request); 871 $this->publicLinkDB->findByHash($request);
917 } 872 }
918 873
919 /** 874 /**
920 * Test filterHash() with an empty smallhash. 875 * Test filterHash() with an empty smallhash.
921 *
922 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
923 */ 876 */
924 public function testFilterHashInValid() 877 public function testFilterHashInValid()
925 { 878 {
879 $this->expectException(BookmarkNotFoundException::class);
880
926 $this->publicLinkDB->findByHash(''); 881 $this->publicLinkDB->findByHash('');
927 } 882 }
928 883
929 /** 884 /**
885 * Test filterHash() on a private bookmark while logged out.
886 */
887 public function testFilterHashPrivateWhileLoggedOut()
888 {
889 $this->expectException(BookmarkNotFoundException::class);
890 $this->expectExceptionMessage('The link you are trying to reach does not exist or has been deleted');
891
892 $hash = smallHash('20141125_084734' . 6);
893
894 $this->publicLinkDB->findByHash($hash);
895 }
896
897 /**
898 * Test filterHash() with private key.
899 */
900 public function testFilterHashWithPrivateKey()
901 {
902 $hash = smallHash('20141125_084734' . 6);
903 $privateKey = 'this is usually auto generated';
904
905 $bookmark = $this->privateLinkDB->findByHash($hash);
906 $bookmark->addAdditionalContentEntry('private_key', $privateKey);
907 $this->privateLinkDB->save();
908
909 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
910 $bookmark = $this->privateLinkDB->findByHash($hash, $privateKey);
911
912 static::assertSame(6, $bookmark->getId());
913 }
914
915 /**
930 * Test linksCountPerTag all tags without filter. 916 * Test linksCountPerTag all tags without filter.
931 * Equal occurrences should be sorted alphabetically. 917 * Equal occurrences should be sorted alphabetically.
932 */ 918 */
@@ -955,6 +941,10 @@ class BookmarkFileServiceTest extends TestCase
955 'tag4' => 1, 941 'tag4' => 1,
956 'ut' => 1, 942 'ut' => 1,
957 'w3c' => 1, 943 'w3c' => 1,
944 'assurance' => 1,
945 'coding-style' => 1,
946 'quality' => 1,
947 'standards' => 1,
958 ]; 948 ];
959 $tags = $this->privateLinkDB->bookmarksCountPerTag(); 949 $tags = $this->privateLinkDB->bookmarksCountPerTag();
960 950
@@ -968,7 +958,6 @@ class BookmarkFileServiceTest extends TestCase
968 public function testCountLinkPerTagAllWithFilter() 958 public function testCountLinkPerTagAllWithFilter()
969 { 959 {
970 $expected = [ 960 $expected = [
971 'gnu' => 2,
972 'hashtag' => 2, 961 'hashtag' => 2,
973 '-exclude' => 1, 962 '-exclude' => 1,
974 '.hidden' => 1, 963 '.hidden' => 1,
@@ -991,7 +980,6 @@ class BookmarkFileServiceTest extends TestCase
991 public function testCountLinkPerTagPublicWithFilter() 980 public function testCountLinkPerTagPublicWithFilter()
992 { 981 {
993 $expected = [ 982 $expected = [
994 'gnu' => 2,
995 'hashtag' => 2, 983 'hashtag' => 2,
996 '-exclude' => 1, 984 '-exclude' => 1,
997 '.hidden' => 1, 985 '.hidden' => 1,
@@ -1015,7 +1003,6 @@ class BookmarkFileServiceTest extends TestCase
1015 { 1003 {
1016 $expected = [ 1004 $expected = [
1017 'cartoon' => 1, 1005 'cartoon' => 1,
1018 'dev' => 1,
1019 'tag1' => 1, 1006 'tag1' => 1,
1020 'tag2' => 1, 1007 'tag2' => 1,
1021 'tag3' => 1, 1008 'tag3' => 1,
@@ -1056,6 +1043,10 @@ class BookmarkFileServiceTest extends TestCase
1056 'stallman' => 1, 1043 'stallman' => 1,
1057 'ut' => 1, 1044 'ut' => 1,
1058 'w3c' => 1, 1045 'w3c' => 1,
1046 'assurance' => 1,
1047 'coding-style' => 1,
1048 'quality' => 1,
1049 'standards' => 1,
1059 ]; 1050 ];
1060 $bookmark = new Bookmark(); 1051 $bookmark = new Bookmark();
1061 $bookmark->setTags(['newTagToCount', BookmarkMarkdownFormatter::NO_MD_TAG]); 1052 $bookmark->setTags(['newTagToCount', BookmarkMarkdownFormatter::NO_MD_TAG]);
@@ -1067,6 +1058,108 @@ class BookmarkFileServiceTest extends TestCase
1067 } 1058 }
1068 1059
1069 /** 1060 /**
1061 * Test find by dates in the middle of the datastore (sorted by dates) with a single bookmark as a result.
1062 */
1063 public function testFilterByDateMidTimePeriodSingleBookmark(): void
1064 {
1065 $bookmarks = $this->privateLinkDB->findByDate(
1066 DateTime::createFromFormat('Ymd_His', '20121206_150000'),
1067 DateTime::createFromFormat('Ymd_His', '20121206_160000'),
1068 $before,
1069 $after
1070 );
1071
1072 static::assertCount(1, $bookmarks);
1073
1074 static::assertSame(9, $bookmarks[0]->getId());
1075 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
1076 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_172539'), $after);
1077 }
1078
1079 /**
1080 * Test find by dates in the middle of the datastore (sorted by dates) with a multiple bookmarks as a result.
1081 */
1082 public function testFilterByDateMidTimePeriodMultipleBookmarks(): void
1083 {
1084 $bookmarks = $this->privateLinkDB->findByDate(
1085 DateTime::createFromFormat('Ymd_His', '20121206_150000'),
1086 DateTime::createFromFormat('Ymd_His', '20121206_180000'),
1087 $before,
1088 $after
1089 );
1090
1091 static::assertCount(2, $bookmarks);
1092
1093 static::assertSame(1, $bookmarks[0]->getId());
1094 static::assertSame(9, $bookmarks[1]->getId());
1095 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before);
1096 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_182539'), $after);
1097 }
1098
1099 /**
1100 * Test find by dates at the end of the datastore (sorted by dates).
1101 */
1102 public function testFilterByDateLastTimePeriod(): void
1103 {
1104 $after = new DateTime();
1105 $bookmarks = $this->privateLinkDB->findByDate(
1106 DateTime::createFromFormat('Ymd_His', '20150310_114640'),
1107 DateTime::createFromFormat('Ymd_His', '20450101_010101'),
1108 $before,
1109 $after
1110 );
1111
1112 static::assertCount(1, $bookmarks);
1113
1114 static::assertSame(41, $bookmarks[0]->getId());
1115 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20150310_114633'), $before);
1116 static::assertNull($after);
1117 }
1118
1119 /**
1120 * Test find by dates at the beginning of the datastore (sorted by dates).
1121 */
1122 public function testFilterByDateFirstTimePeriod(): void
1123 {
1124 $before = new DateTime();
1125 $bookmarks = $this->privateLinkDB->findByDate(
1126 DateTime::createFromFormat('Ymd_His', '20000101_101010'),
1127 DateTime::createFromFormat('Ymd_His', '20100309_110000'),
1128 $before,
1129 $after
1130 );
1131
1132 static::assertCount(1, $bookmarks);
1133
1134 static::assertSame(11, $bookmarks[0]->getId());
1135 static::assertNull($before);
1136 static::assertEquals(DateTime::createFromFormat('Ymd_His', '20100310_101010'), $after);
1137 }
1138
1139 /**
1140 * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
1141 */
1142 public function testGetLatestWithSticky(): void
1143 {
1144 $bookmark = $this->publicLinkDB->getLatest();
1145
1146 static::assertSame(41, $bookmark->getId());
1147 }
1148
1149 /**
1150 * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead.
1151 */
1152 public function testGetLatestEmptyDatastore(): void
1153 {
1154 unlink($this->conf->get('resource.datastore'));
1155 $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
1156
1157 $bookmark = $this->publicLinkDB->getLatest();
1158
1159 static::assertNull($bookmark);
1160 }
1161
1162 /**
1070 * Allows to test LinkDB's private methods 1163 * Allows to test LinkDB's private methods
1071 * 1164 *
1072 * @see 1165 * @see
diff --git a/tests/bookmark/BookmarkFilterTest.php b/tests/bookmark/BookmarkFilterTest.php
index d4c71cb9..574d8e3f 100644
--- a/tests/bookmark/BookmarkFilterTest.php
+++ b/tests/bookmark/BookmarkFilterTest.php
@@ -2,12 +2,11 @@
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use Exception; 5use malkusch\lock\mutex\NoMutex;
6use PHPUnit\Framework\TestCase;
7use ReferenceLinkDB; 6use ReferenceLinkDB;
8use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
9use Shaarli\Formatter\FormatterFactory;
10use Shaarli\History; 8use Shaarli\History;
9use Shaarli\TestCase;
11 10
12/** 11/**
13 * Class BookmarkFilterTest. 12 * Class BookmarkFilterTest.
@@ -36,14 +35,15 @@ class BookmarkFilterTest extends TestCase
36 /** 35 /**
37 * Instantiate linkFilter with ReferenceLinkDB data. 36 * Instantiate linkFilter with ReferenceLinkDB data.
38 */ 37 */
39 public static function setUpBeforeClass() 38 public static function setUpBeforeClass(): void
40 { 39 {
40 $mutex = new NoMutex();
41 $conf = new ConfigManager('tests/utils/config/configJson'); 41 $conf = new ConfigManager('tests/utils/config/configJson');
42 $conf->set('resource.datastore', self::$testDatastore); 42 $conf->set('resource.datastore', self::$testDatastore);
43 self::$refDB = new \ReferenceLinkDB(); 43 self::$refDB = new \ReferenceLinkDB();
44 self::$refDB->write(self::$testDatastore); 44 self::$refDB->write(self::$testDatastore);
45 $history = new History('sandbox/history.php'); 45 $history = new History('sandbox/history.php');
46 self::$bookmarkService = new \FakeBookmarkService($conf, $history, true); 46 self::$bookmarkService = new \FakeBookmarkService($conf, $history, $mutex, true);
47 self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks()); 47 self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks());
48 } 48 }
49 49
@@ -190,6 +190,17 @@ class BookmarkFilterTest extends TestCase
190 } 190 }
191 191
192 /** 192 /**
193 * Return bookmarks for a given day
194 */
195 public function testFilterDayRestrictedVisibility(): void
196 {
197 $this->assertEquals(
198 3,
199 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20121206', false, BookmarkFilter::$PUBLIC))
200 );
201 }
202
203 /**
193 * 404 - day not found 204 * 404 - day not found
194 */ 205 */
195 public function testFilterUnknownDay() 206 public function testFilterUnknownDay()
@@ -202,21 +213,23 @@ class BookmarkFilterTest extends TestCase
202 213
203 /** 214 /**
204 * Use an invalid date format 215 * Use an invalid date format
205 * @expectedException Exception
206 * @expectedExceptionMessageRegExp /Invalid date format/
207 */ 216 */
208 public function testFilterInvalidDayWithChars() 217 public function testFilterInvalidDayWithChars()
209 { 218 {
219 $this->expectException(\Exception::class);
220 $this->expectExceptionMessageRegExp('/Invalid date format/');
221
210 self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, 'Rainy day, dream away'); 222 self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, 'Rainy day, dream away');
211 } 223 }
212 224
213 /** 225 /**
214 * Use an invalid date format 226 * Use an invalid date format
215 * @expectedException Exception
216 * @expectedExceptionMessageRegExp /Invalid date format/
217 */ 227 */
218 public function testFilterInvalidDayDigits() 228 public function testFilterInvalidDayDigits()
219 { 229 {
230 $this->expectException(\Exception::class);
231 $this->expectExceptionMessageRegExp('/Invalid date format/');
232
220 self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20'); 233 self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20');
221 } 234 }
222 235
@@ -240,11 +253,11 @@ class BookmarkFilterTest extends TestCase
240 253
241 /** 254 /**
242 * No link for this hash 255 * No link for this hash
243 *
244 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
245 */ 256 */
246 public function testFilterUnknownSmallHash() 257 public function testFilterUnknownSmallHash()
247 { 258 {
259 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
260
248 self::$linkFilter->filter(BookmarkFilter::$FILTER_HASH, 'Iblaah'); 261 self::$linkFilter->filter(BookmarkFilter::$FILTER_HASH, 'Iblaah');
249 } 262 }
250 263
@@ -511,4 +524,43 @@ class BookmarkFilterTest extends TestCase
511 )) 524 ))
512 ); 525 );
513 } 526 }
527
528 /**
529 * Test search result highlights in every field of bookmark reference #9.
530 */
531 public function testFullTextSearchHighlight(): void
532 {
533 $bookmarks = self::$linkFilter->filter(
534 BookmarkFilter::$FILTER_TEXT,
535 '"psr-2" coding guide http fig "psr-2/" "This guide" basic standard. coding-style quality assurance'
536 );
537
538 static::assertCount(1, $bookmarks);
539 static::assertArrayHasKey(9, $bookmarks);
540
541 $bookmark = $bookmarks[9];
542 $expectedHighlights = [
543 'title' => [
544 ['start' => 0, 'end' => 5], // "psr-2"
545 ['start' => 7, 'end' => 13], // coding
546 ['start' => 20, 'end' => 25], // guide
547 ],
548 'description' => [
549 ['start' => 0, 'end' => 10], // "This guide"
550 ['start' => 45, 'end' => 50], // basic
551 ['start' => 58, 'end' => 67], // standard.
552 ],
553 'url' => [
554 ['start' => 0, 'end' => 4], // http
555 ['start' => 15, 'end' => 18], // fig
556 ['start' => 27, 'end' => 33], // "psr-2/"
557 ],
558 'tags' => [
559 ['start' => 0, 'end' => 12], // coding-style
560 ['start' => 23, 'end' => 30], // quality
561 ['start' => 31, 'end' => 40], // assurance
562 ],
563 ];
564 static::assertSame($expectedHighlights, $bookmark->getAdditionalContentEntry('search_highlight'));
565 }
514} 566}
diff --git a/tests/bookmark/BookmarkInitializerTest.php b/tests/bookmark/BookmarkInitializerTest.php
index d23eb069..0c8420ce 100644
--- a/tests/bookmark/BookmarkInitializerTest.php
+++ b/tests/bookmark/BookmarkInitializerTest.php
@@ -2,10 +2,10 @@
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use PHPUnit\Framework\TestCase; 5use malkusch\lock\mutex\NoMutex;
6use ReferenceLinkDB;
7use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
8use Shaarli\History; 7use Shaarli\History;
8use Shaarli\TestCase;
9 9
10/** 10/**
11 * Class BookmarkInitializerTest 11 * Class BookmarkInitializerTest
@@ -35,11 +35,15 @@ class BookmarkInitializerTest extends TestCase
35 /** @var BookmarkInitializer instance */ 35 /** @var BookmarkInitializer instance */
36 protected $initializer; 36 protected $initializer;
37 37
38 /** @var NoMutex */
39 protected $mutex;
40
38 /** 41 /**
39 * Initialize an empty BookmarkFileService 42 * Initialize an empty BookmarkFileService
40 */ 43 */
41 public function setUp() 44 public function setUp(): void
42 { 45 {
46 $this->mutex = new NoMutex();
43 if (file_exists(self::$testDatastore)) { 47 if (file_exists(self::$testDatastore)) {
44 unlink(self::$testDatastore); 48 unlink(self::$testDatastore);
45 } 49 }
@@ -48,72 +52,103 @@ class BookmarkInitializerTest extends TestCase
48 $this->conf = new ConfigManager(self::$testConf); 52 $this->conf = new ConfigManager(self::$testConf);
49 $this->conf->set('resource.datastore', self::$testDatastore); 53 $this->conf->set('resource.datastore', self::$testDatastore);
50 $this->history = new History('sandbox/history.php'); 54 $this->history = new History('sandbox/history.php');
51 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 55 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
52 56
53 $this->initializer = new BookmarkInitializer($this->bookmarkService); 57 $this->initializer = new BookmarkInitializer($this->bookmarkService);
54 } 58 }
55 59
56 /** 60 /**
57 * Test initialize() with an empty data store. 61 * Test initialize() with a data store containing bookmarks.
58 */ 62 */
59 public function testInitializeEmptyDataStore() 63 public function testInitializeNotEmptyDataStore(): void
60 { 64 {
61 $refDB = new \ReferenceLinkDB(); 65 $refDB = new \ReferenceLinkDB();
62 $refDB->write(self::$testDatastore); 66 $refDB->write(self::$testDatastore);
63 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 67 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
64 $this->initializer = new BookmarkInitializer($this->bookmarkService); 68 $this->initializer = new BookmarkInitializer($this->bookmarkService);
65 69
66 $this->initializer->initialize(); 70 $this->initializer->initialize();
67 71
68 $this->assertEquals($refDB->countLinks() + 2, $this->bookmarkService->count()); 72 $this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count());
73
69 $bookmark = $this->bookmarkService->get(43); 74 $bookmark = $this->bookmarkService->get(43);
70 $this->assertEquals(43, $bookmark->getId()); 75 $this->assertStringStartsWith(
71 $this->assertEquals('My secret stuff... - Pastebin.com', $bookmark->getTitle()); 76 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.',
77 $bookmark->getDescription()
78 );
72 $this->assertTrue($bookmark->isPrivate()); 79 $this->assertTrue($bookmark->isPrivate());
73 80
74 $bookmark = $this->bookmarkService->get(44); 81 $bookmark = $this->bookmarkService->get(44);
75 $this->assertEquals(44, $bookmark->getId()); 82 $this->assertStringStartsWith(
76 $this->assertEquals( 83 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.',
77 'The personal, minimalist, super-fast, database free, bookmarking service', 84 $bookmark->getDescription()
78 $bookmark->getTitle() 85 );
86 $this->assertTrue($bookmark->isPrivate());
87
88 $bookmark = $this->bookmarkService->get(45);
89 $this->assertStringStartsWith(
90 'Welcome to Shaarli!',
91 $bookmark->getDescription()
79 ); 92 );
80 $this->assertFalse($bookmark->isPrivate()); 93 $this->assertFalse($bookmark->isPrivate());
81 94
95 $this->bookmarkService->save();
96
82 // Reload from file 97 // Reload from file
83 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 98 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
84 $this->assertEquals($refDB->countLinks() + 2, $this->bookmarkService->count()); 99 $this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count());
100
85 $bookmark = $this->bookmarkService->get(43); 101 $bookmark = $this->bookmarkService->get(43);
86 $this->assertEquals(43, $bookmark->getId()); 102 $this->assertStringStartsWith(
87 $this->assertEquals('My secret stuff... - Pastebin.com', $bookmark->getTitle()); 103 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.',
104 $bookmark->getDescription()
105 );
88 $this->assertTrue($bookmark->isPrivate()); 106 $this->assertTrue($bookmark->isPrivate());
89 107
90 $bookmark = $this->bookmarkService->get(44); 108 $bookmark = $this->bookmarkService->get(44);
91 $this->assertEquals(44, $bookmark->getId()); 109 $this->assertStringStartsWith(
92 $this->assertEquals( 110 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.',
93 'The personal, minimalist, super-fast, database free, bookmarking service', 111 $bookmark->getDescription()
94 $bookmark->getTitle() 112 );
113 $this->assertTrue($bookmark->isPrivate());
114
115 $bookmark = $this->bookmarkService->get(45);
116 $this->assertStringStartsWith(
117 'Welcome to Shaarli!',
118 $bookmark->getDescription()
95 ); 119 );
96 $this->assertFalse($bookmark->isPrivate()); 120 $this->assertFalse($bookmark->isPrivate());
97 } 121 }
98 122
99 /** 123 /**
100 * Test initialize() with a data store containing bookmarks. 124 * Test initialize() with an a non existent datastore file .
101 */ 125 */
102 public function testInitializeNotEmptyDataStore() 126 public function testInitializeNonExistentDataStore(): void
103 { 127 {
128 $this->conf->set('resource.datastore', static::$testDatastore . '_empty');
129 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
130
104 $this->initializer->initialize(); 131 $this->initializer->initialize();
105 132
106 $this->assertEquals(2, $this->bookmarkService->count()); 133 $this->assertEquals(3, $this->bookmarkService->count());
107 $bookmark = $this->bookmarkService->get(0); 134 $bookmark = $this->bookmarkService->get(0);
108 $this->assertEquals(0, $bookmark->getId()); 135 $this->assertStringStartsWith(
109 $this->assertEquals('My secret stuff... - Pastebin.com', $bookmark->getTitle()); 136 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.',
137 $bookmark->getDescription()
138 );
110 $this->assertTrue($bookmark->isPrivate()); 139 $this->assertTrue($bookmark->isPrivate());
111 140
112 $bookmark = $this->bookmarkService->get(1); 141 $bookmark = $this->bookmarkService->get(1);
113 $this->assertEquals(1, $bookmark->getId()); 142 $this->assertStringStartsWith(
114 $this->assertEquals( 143 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.',
115 'The personal, minimalist, super-fast, database free, bookmarking service', 144 $bookmark->getDescription()
116 $bookmark->getTitle() 145 );
146 $this->assertTrue($bookmark->isPrivate());
147
148 $bookmark = $this->bookmarkService->get(2);
149 $this->assertStringStartsWith(
150 'Welcome to Shaarli!',
151 $bookmark->getDescription()
117 ); 152 );
118 $this->assertFalse($bookmark->isPrivate()); 153 $this->assertFalse($bookmark->isPrivate());
119 } 154 }
diff --git a/tests/bookmark/BookmarkTest.php b/tests/bookmark/BookmarkTest.php
index 9a3bbbfc..4c1ae25d 100644
--- a/tests/bookmark/BookmarkTest.php
+++ b/tests/bookmark/BookmarkTest.php
@@ -2,8 +2,8 @@
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use PHPUnit\Framework\TestCase;
6use Shaarli\Bookmark\Exception\InvalidBookmarkException; 5use Shaarli\Bookmark\Exception\InvalidBookmarkException;
6use Shaarli\TestCase;
7 7
8/** 8/**
9 * Class BookmarkTest 9 * Class BookmarkTest
@@ -124,8 +124,8 @@ class BookmarkTest extends TestCase
124 $this->assertEquals(1, $bookmark->getId()); 124 $this->assertEquals(1, $bookmark->getId());
125 $this->assertEquals('abc', $bookmark->getShortUrl()); 125 $this->assertEquals('abc', $bookmark->getShortUrl());
126 $this->assertEquals($date, $bookmark->getCreated()); 126 $this->assertEquals($date, $bookmark->getCreated());
127 $this->assertEquals('?abc', $bookmark->getUrl()); 127 $this->assertEquals('/shaare/abc', $bookmark->getUrl());
128 $this->assertEquals('?abc', $bookmark->getTitle()); 128 $this->assertEquals('/shaare/abc', $bookmark->getTitle());
129 $this->assertEquals('', $bookmark->getDescription()); 129 $this->assertEquals('', $bookmark->getDescription());
130 $this->assertEquals([], $bookmark->getTags()); 130 $this->assertEquals([], $bookmark->getTags());
131 $this->assertEquals('', $bookmark->getTagsString()); 131 $this->assertEquals('', $bookmark->getTagsString());
@@ -150,26 +150,7 @@ class BookmarkTest extends TestCase
150 $exception = $e; 150 $exception = $e;
151 } 151 }
152 $this->assertNotNull($exception); 152 $this->assertNotNull($exception);
153 $this->assertContains('- ID: '. PHP_EOL, $exception->getMessage()); 153 $this->assertContainsPolyfill('- ID: '. PHP_EOL, $exception->getMessage());
154 }
155
156 /**
157 * Test validate() with a a bookmark with a non integer ID.
158 */
159 public function testValidateNotValidStringId()
160 {
161 $bookmark = new Bookmark();
162 $bookmark->setId('str');
163 $bookmark->setShortUrl('abc');
164 $bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
165 $exception = null;
166 try {
167 $bookmark->validate();
168 } catch (InvalidBookmarkException $e) {
169 $exception = $e;
170 }
171 $this->assertNotNull($exception);
172 $this->assertContains('- ID: str'. PHP_EOL, $exception->getMessage());
173 } 154 }
174 155
175 /** 156 /**
@@ -188,7 +169,7 @@ class BookmarkTest extends TestCase
188 $exception = $e; 169 $exception = $e;
189 } 170 }
190 $this->assertNotNull($exception); 171 $this->assertNotNull($exception);
191 $this->assertContains('- ShortUrl: '. PHP_EOL, $exception->getMessage()); 172 $this->assertContainsPolyfill('- ShortUrl: '. PHP_EOL, $exception->getMessage());
192 } 173 }
193 174
194 /** 175 /**
@@ -207,26 +188,7 @@ class BookmarkTest extends TestCase
207 $exception = $e; 188 $exception = $e;
208 } 189 }
209 $this->assertNotNull($exception); 190 $this->assertNotNull($exception);
210 $this->assertContains('- Created: '. PHP_EOL, $exception->getMessage()); 191 $this->assertContainsPolyfill('- Created: '. PHP_EOL, $exception->getMessage());
211 }
212
213 /**
214 * Test validate() with a a bookmark with a bad created datetime.
215 */
216 public function testValidateNotValidBadCreated()
217 {
218 $bookmark = new Bookmark();
219 $bookmark->setId(1);
220 $bookmark->setShortUrl('abc');
221 $bookmark->setCreated('hi!');
222 $exception = null;
223 try {
224 $bookmark->validate();
225 } catch (InvalidBookmarkException $e) {
226 $exception = $e;
227 }
228 $this->assertNotNull($exception);
229 $this->assertContains('- Created: Not a DateTime object'. PHP_EOL, $exception->getMessage());
230 } 192 }
231 193
232 /** 194 /**
@@ -385,4 +347,48 @@ class BookmarkTest extends TestCase
385 $bookmark->deleteTag('nope'); 347 $bookmark->deleteTag('nope');
386 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags()); 348 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags());
387 } 349 }
350
351 /**
352 * Test shouldUpdateThumbnail() with bookmarks needing an update.
353 */
354 public function testShouldUpdateThumbnail(): void
355 {
356 $bookmark = (new Bookmark())->setUrl('http://domain.tld/with-image');
357
358 static::assertTrue($bookmark->shouldUpdateThumbnail());
359
360 $bookmark = (new Bookmark())
361 ->setUrl('http://domain.tld/with-image')
362 ->setThumbnail('unknown file')
363 ;
364
365 static::assertTrue($bookmark->shouldUpdateThumbnail());
366 }
367
368 /**
369 * Test shouldUpdateThumbnail() with bookmarks that should not update.
370 */
371 public function testShouldNotUpdateThumbnail(): void
372 {
373 $bookmark = (new Bookmark());
374
375 static::assertFalse($bookmark->shouldUpdateThumbnail());
376
377 $bookmark = (new Bookmark())
378 ->setUrl('ftp://domain.tld/other-protocol', ['ftp'])
379 ;
380
381 static::assertFalse($bookmark->shouldUpdateThumbnail());
382
383 $bookmark = (new Bookmark())
384 ->setUrl('http://domain.tld/with-image')
385 ->setThumbnail(__FILE__)
386 ;
387
388 static::assertFalse($bookmark->shouldUpdateThumbnail());
389
390 $bookmark = (new Bookmark())->setUrl('/shaare/abcdef');
391
392 static::assertFalse($bookmark->shouldUpdateThumbnail());
393 }
388} 394}
diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php
index 591976f2..3321242f 100644
--- a/tests/bookmark/LinkUtilsTest.php
+++ b/tests/bookmark/LinkUtilsTest.php
@@ -2,9 +2,7 @@
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Bookmark;
4 4
5use PHPUnit\Framework\TestCase; 5use Shaarli\TestCase;
6use ReferenceLinkDB;
7use Shaarli\Config\ConfigManager;
8 6
9require_once 'tests/utils/CurlUtils.php'; 7require_once 'tests/utils/CurlUtils.php';
10 8
@@ -45,6 +43,19 @@ class LinkUtilsTest extends TestCase
45 } 43 }
46 44
47 /** 45 /**
46 * Test headers_extract_charset() when the charset is found with odd quotes.
47 */
48 public function testHeadersExtractExistentCharsetWithQuotes()
49 {
50 $charset = 'x-MacCroatian';
51 $headers = 'text/html; charset="' . $charset . '"otherstuff="test"';
52 $this->assertEquals(strtolower($charset), header_extract_charset($headers));
53
54 $headers = 'text/html; charset=\'' . $charset . '\'otherstuff="test"';
55 $this->assertEquals(strtolower($charset), header_extract_charset($headers));
56 }
57
58 /**
48 * Test headers_extract_charset() when the charset is not found. 59 * Test headers_extract_charset() when the charset is not found.
49 */ 60 */
50 public function testHeadersExtractNonExistentCharset() 61 public function testHeadersExtractNonExistentCharset()
@@ -83,8 +94,78 @@ class LinkUtilsTest extends TestCase
83 public function testHtmlExtractExistentNameTag() 94 public function testHtmlExtractExistentNameTag()
84 { 95 {
85 $description = 'Bob and Alice share cookies.'; 96 $description = 'Bob and Alice share cookies.';
97
98 // Simple one line
86 $html = '<html><meta>stuff2</meta><meta name="description" content="' . $description . '"/></html>'; 99 $html = '<html><meta>stuff2</meta><meta name="description" content="' . $description . '"/></html>';
87 $this->assertEquals($description, html_extract_tag('description', $html)); 100 $this->assertEquals($description, html_extract_tag('description', $html));
101
102 // Simple OpenGraph
103 $html = '<meta property="og:description" content="' . $description . '">';
104 $this->assertEquals($description, html_extract_tag('description', $html));
105
106 // Simple reversed OpenGraph
107 $html = '<meta content="' . $description . '" property="og:description">';
108 $this->assertEquals($description, html_extract_tag('description', $html));
109
110 // ItemProp OpenGraph
111 $html = '<meta itemprop="og:description" content="' . $description . '">';
112 $this->assertEquals($description, html_extract_tag('description', $html));
113
114 // OpenGraph without quotes
115 $html = '<meta property=og:description content="' . $description . '">';
116 $this->assertEquals($description, html_extract_tag('description', $html));
117
118 // OpenGraph reversed without quotes
119 $html = '<meta content="' . $description . '" property=og:description>';
120 $this->assertEquals($description, html_extract_tag('description', $html));
121
122 // OpenGraph with noise
123 $html = '<meta tag1="content1" property="og:description" tag2="content2" content="' .
124 $description . '" tag3="content3">';
125 $this->assertEquals($description, html_extract_tag('description', $html));
126
127 // OpenGraph reversed with noise
128 $html = '<meta tag1="content1" content="' . $description . '" ' .
129 'tag3="content3" tag2="content2" property="og:description">';
130 $this->assertEquals($description, html_extract_tag('description', $html));
131
132 // OpenGraph multiple properties start
133 $html = '<meta property="unrelated og:description" content="' . $description . '">';
134 $this->assertEquals($description, html_extract_tag('description', $html));
135
136 // OpenGraph multiple properties end
137 $html = '<meta property="og:description unrelated" content="' . $description . '">';
138 $this->assertEquals($description, html_extract_tag('description', $html));
139
140 // OpenGraph multiple properties both end
141 $html = '<meta property="og:unrelated1 og:description og:unrelated2" content="' . $description . '">';
142 $this->assertEquals($description, html_extract_tag('description', $html));
143
144 // OpenGraph multiple properties both end with noise
145 $html = '<meta tag1="content1" property="og:unrelated1 og:description og:unrelated2" '.
146 'tag2="content2" content="' . $description . '" tag3="content3">';
147 $this->assertEquals($description, html_extract_tag('description', $html));
148
149 // OpenGraph reversed multiple properties start
150 $html = '<meta content="' . $description . '" property="unrelated og:description">';
151 $this->assertEquals($description, html_extract_tag('description', $html));
152
153 // OpenGraph reversed multiple properties end
154 $html = '<meta content="' . $description . '" property="og:description unrelated">';
155 $this->assertEquals($description, html_extract_tag('description', $html));
156
157 // OpenGraph reversed multiple properties both end
158 $html = '<meta content="' . $description . '" property="og:unrelated1 og:description og:unrelated2">';
159 $this->assertEquals($description, html_extract_tag('description', $html));
160
161 // OpenGraph reversed multiple properties both end with noise
162 $html = '<meta tag1="content1" content="' . $description . '" tag2="content2" '.
163 'property="og:unrelated1 og:description og:unrelated2" tag3="content3">';
164 $this->assertEquals($description, html_extract_tag('description', $html));
165
166 // Suggestion from #1375
167 $html = '<meta property="og:description" name="description" content="' . $description . '">';
168 $this->assertEquals($description, html_extract_tag('description', $html));
88 } 169 }
89 170
90 /** 171 /**
@@ -94,6 +175,25 @@ class LinkUtilsTest extends TestCase
94 { 175 {
95 $html = '<html><meta>stuff2</meta><meta name="image" content="img"/></html>'; 176 $html = '<html><meta>stuff2</meta><meta name="image" content="img"/></html>';
96 $this->assertFalse(html_extract_tag('description', $html)); 177 $this->assertFalse(html_extract_tag('description', $html));
178
179 // Partial meta tag
180 $html = '<meta content="Brief description">';
181 $this->assertFalse(html_extract_tag('description', $html));
182
183 $html = '<meta property="og:description">';
184 $this->assertFalse(html_extract_tag('description', $html));
185
186 $html = '<meta tag1="content1" property="og:description">';
187 $this->assertFalse(html_extract_tag('description', $html));
188
189 $html = '<meta property="og:description" tag1="content1">';
190 $this->assertFalse(html_extract_tag('description', $html));
191
192 $html = '<meta tag1="content1" content="Brief description">';
193 $this->assertFalse(html_extract_tag('description', $html));
194
195 $html = '<meta content="Brief description" tag1="content1">';
196 $this->assertFalse(html_extract_tag('description', $html));
97 } 197 }
98 198
99 /** 199 /**
@@ -116,60 +216,91 @@ class LinkUtilsTest extends TestCase
116 } 216 }
117 217
118 /** 218 /**
219 * Test the header callback with valid value
220 */
221 public function testCurlHeaderCallbackOk(): void
222 {
223 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ok');
224 $data = [
225 'HTTP/1.1 200 OK',
226 'Server: GitHub.com',
227 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
228 'Content-Type: text/html; charset=utf-8',
229 'Status: 200 OK',
230 ];
231
232 foreach ($data as $chunk) {
233 static::assertIsInt($callback(null, $chunk));
234 }
235
236 static::assertSame('utf-8', $charset);
237 }
238
239 /**
119 * Test the download callback with valid value 240 * Test the download callback with valid value
120 */ 241 */
121 public function testCurlDownloadCallbackOk() 242 public function testCurlDownloadCallbackOk(): void
122 { 243 {
244 $charset = 'utf-8';
123 $callback = get_curl_download_callback( 245 $callback = get_curl_download_callback(
124 $charset, 246 $charset,
125 $title, 247 $title,
126 $desc, 248 $desc,
127 $keywords, 249 $keywords,
128 false, 250 false
129 'ut_curl_getinfo_ok'
130 ); 251 );
252
131 $data = [ 253 $data = [
132 'HTTP/1.1 200 OK', 254 'th=device-width">'
133 'Server: GitHub.com',
134 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
135 'Content-Type: text/html; charset=utf-8',
136 'Status: 200 OK',
137 'end' => 'th=device-width">'
138 . '<title>Refactoring · GitHub</title>' 255 . '<title>Refactoring · GitHub</title>'
139 . '<link rel="search" type="application/opensea', 256 . '<link rel="search" type="application/opensea',
140 '<title>ignored</title>' 257 '<title>ignored</title>'
141 . '<meta name="description" content="desc" />' 258 . '<meta name="description" content="desc" />'
142 . '<meta name="keywords" content="key1,key2" />', 259 . '<meta name="keywords" content="key1,key2" />',
143 ]; 260 ];
144 foreach ($data as $key => $line) { 261
145 $ignore = null; 262 foreach ($data as $chunk) {
146 $expected = $key !== 'end' ? strlen($line) : false; 263 static::assertSame(strlen($chunk), $callback(null, $chunk));
147 $this->assertEquals($expected, $callback($ignore, $line));
148 if ($expected === false) {
149 break;
150 }
151 } 264 }
152 $this->assertEquals('utf-8', $charset); 265
153 $this->assertEquals('Refactoring · GitHub', $title); 266 static::assertSame('utf-8', $charset);
154 $this->assertEmpty($desc); 267 static::assertSame('Refactoring · GitHub', $title);
155 $this->assertEmpty($keywords); 268 static::assertEmpty($desc);
269 static::assertEmpty($keywords);
270 }
271
272 /**
273 * Test the header callback with valid value
274 */
275 public function testCurlHeaderCallbackNoCharset(): void
276 {
277 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_no_charset');
278 $data = [
279 'HTTP/1.1 200 OK',
280 ];
281
282 foreach ($data as $chunk) {
283 static::assertSame(strlen($chunk), $callback(null, $chunk));
284 }
285
286 static::assertFalse($charset);
156 } 287 }
157 288
158 /** 289 /**
159 * Test the download callback with valid values and no charset 290 * Test the download callback with valid values and no charset
160 */ 291 */
161 public function testCurlDownloadCallbackOkNoCharset() 292 public function testCurlDownloadCallbackOkNoCharset(): void
162 { 293 {
294 $charset = null;
163 $callback = get_curl_download_callback( 295 $callback = get_curl_download_callback(
164 $charset, 296 $charset,
165 $title, 297 $title,
166 $desc, 298 $desc,
167 $keywords, 299 $keywords,
168 false, 300 false
169 'ut_curl_getinfo_no_charset'
170 ); 301 );
302
171 $data = [ 303 $data = [
172 'HTTP/1.1 200 OK',
173 'end' => 'th=device-width">' 304 'end' => 'th=device-width">'
174 . '<title>Refactoring · GitHub</title>' 305 . '<title>Refactoring · GitHub</title>'
175 . '<link rel="search" type="application/opensea', 306 . '<link rel="search" type="application/opensea',
@@ -177,10 +308,11 @@ class LinkUtilsTest extends TestCase
177 . '<meta name="description" content="desc" />' 308 . '<meta name="description" content="desc" />'
178 . '<meta name="keywords" content="key1,key2" />', 309 . '<meta name="keywords" content="key1,key2" />',
179 ]; 310 ];
180 foreach ($data as $key => $line) { 311
181 $ignore = null; 312 foreach ($data as $chunk) {
182 $this->assertEquals(strlen($line), $callback($ignore, $line)); 313 static::assertSame(strlen($chunk), $callback(null, $chunk));
183 } 314 }
315
184 $this->assertEmpty($charset); 316 $this->assertEmpty($charset);
185 $this->assertEquals('Refactoring · GitHub', $title); 317 $this->assertEquals('Refactoring · GitHub', $title);
186 $this->assertEmpty($desc); 318 $this->assertEmpty($desc);
@@ -190,18 +322,18 @@ class LinkUtilsTest extends TestCase
190 /** 322 /**
191 * Test the download callback with valid values and no charset 323 * Test the download callback with valid values and no charset
192 */ 324 */
193 public function testCurlDownloadCallbackOkHtmlCharset() 325 public function testCurlDownloadCallbackOkHtmlCharset(): void
194 { 326 {
327 $charset = null;
195 $callback = get_curl_download_callback( 328 $callback = get_curl_download_callback(
196 $charset, 329 $charset,
197 $title, 330 $title,
198 $desc, 331 $desc,
199 $keywords, 332 $keywords,
200 false, 333 false
201 'ut_curl_getinfo_no_charset'
202 ); 334 );
335
203 $data = [ 336 $data = [
204 'HTTP/1.1 200 OK',
205 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />', 337 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
206 'end' => 'th=device-width">' 338 'end' => 'th=device-width">'
207 . '<title>Refactoring · GitHub</title>' 339 . '<title>Refactoring · GitHub</title>'
@@ -210,14 +342,10 @@ class LinkUtilsTest extends TestCase
210 . '<meta name="description" content="desc" />' 342 . '<meta name="description" content="desc" />'
211 . '<meta name="keywords" content="key1,key2" />', 343 . '<meta name="keywords" content="key1,key2" />',
212 ]; 344 ];
213 foreach ($data as $key => $line) { 345 foreach ($data as $chunk) {
214 $ignore = null; 346 static::assertSame(strlen($chunk), $callback(null, $chunk));
215 $expected = $key !== 'end' ? strlen($line) : false;
216 $this->assertEquals($expected, $callback($ignore, $line));
217 if ($expected === false) {
218 break;
219 }
220 } 347 }
348
221 $this->assertEquals('utf-8', $charset); 349 $this->assertEquals('utf-8', $charset);
222 $this->assertEquals('Refactoring · GitHub', $title); 350 $this->assertEquals('Refactoring · GitHub', $title);
223 $this->assertEmpty($desc); 351 $this->assertEmpty($desc);
@@ -227,25 +355,26 @@ class LinkUtilsTest extends TestCase
227 /** 355 /**
228 * Test the download callback with valid values and no title 356 * Test the download callback with valid values and no title
229 */ 357 */
230 public function testCurlDownloadCallbackOkNoTitle() 358 public function testCurlDownloadCallbackOkNoTitle(): void
231 { 359 {
360 $charset = 'utf-8';
232 $callback = get_curl_download_callback( 361 $callback = get_curl_download_callback(
233 $charset, 362 $charset,
234 $title, 363 $title,
235 $desc, 364 $desc,
236 $keywords, 365 $keywords,
237 false, 366 false
238 'ut_curl_getinfo_ok'
239 ); 367 );
368
240 $data = [ 369 $data = [
241 'HTTP/1.1 200 OK',
242 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea', 370 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea',
243 'ignored', 371 'ignored',
244 ]; 372 ];
245 foreach ($data as $key => $line) { 373
246 $ignore = null; 374 foreach ($data as $chunk) {
247 $this->assertEquals(strlen($line), $callback($ignore, $line)); 375 static::assertSame(strlen($chunk), $callback(null, $chunk));
248 } 376 }
377
249 $this->assertEquals('utf-8', $charset); 378 $this->assertEquals('utf-8', $charset);
250 $this->assertEmpty($title); 379 $this->assertEmpty($title);
251 $this->assertEmpty($desc); 380 $this->assertEmpty($desc);
@@ -253,81 +382,55 @@ class LinkUtilsTest extends TestCase
253 } 382 }
254 383
255 /** 384 /**
256 * Test the download callback with an invalid content type. 385 * Test the header callback with an invalid content type.
257 */ 386 */
258 public function testCurlDownloadCallbackInvalidContentType() 387 public function testCurlHeaderCallbackInvalidContentType(): void
259 { 388 {
260 $callback = get_curl_download_callback( 389 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ct_ko');
261 $charset, 390 $data = [
262 $title, 391 'HTTP/1.1 200 OK',
263 $desc, 392 ];
264 $keywords, 393
265 false, 394 static::assertFalse($callback(null, $data[0]));
266 'ut_curl_getinfo_ct_ko' 395 static::assertNull($charset);
267 );
268 $ignore = null;
269 $this->assertFalse($callback($ignore, ''));
270 $this->assertEmpty($charset);
271 $this->assertEmpty($title);
272 } 396 }
273 397
274 /** 398 /**
275 * Test the download callback with an invalid response code. 399 * Test the header callback with an invalid response code.
276 */ 400 */
277 public function testCurlDownloadCallbackInvalidResponseCode() 401 public function testCurlHeaderCallbackInvalidResponseCode(): void
278 { 402 {
279 $callback = $callback = get_curl_download_callback( 403 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rc_ko');
280 $charset, 404
281 $title, 405 static::assertFalse($callback(null, ''));
282 $desc, 406 static::assertNull($charset);
283 $keywords,
284 false,
285 'ut_curl_getinfo_rc_ko'
286 );
287 $ignore = null;
288 $this->assertFalse($callback($ignore, ''));
289 $this->assertEmpty($charset);
290 $this->assertEmpty($title);
291 } 407 }
292 408
293 /** 409 /**
294 * Test the download callback with an invalid content type and response code. 410 * Test the header callback with an invalid content type and response code.
295 */ 411 */
296 public function testCurlDownloadCallbackInvalidContentTypeAndResponseCode() 412 public function testCurlHeaderCallbackInvalidContentTypeAndResponseCode(): void
297 { 413 {
298 $callback = $callback = get_curl_download_callback( 414 $callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rs_ct_ko');
299 $charset, 415
300 $title, 416 static::assertFalse($callback(null, ''));
301 $desc, 417 static::assertNull($charset);
302 $keywords,
303 false,
304 'ut_curl_getinfo_rs_ct_ko'
305 );
306 $ignore = null;
307 $this->assertFalse($callback($ignore, ''));
308 $this->assertEmpty($charset);
309 $this->assertEmpty($title);
310 } 418 }
311 419
312 /** 420 /**
313 * Test the download callback with valid value, and retrieve_description option enabled. 421 * Test the download callback with valid value, and retrieve_description option enabled.
314 */ 422 */
315 public function testCurlDownloadCallbackOkWithDesc() 423 public function testCurlDownloadCallbackOkWithDesc(): void
316 { 424 {
425 $charset = 'utf-8';
317 $callback = get_curl_download_callback( 426 $callback = get_curl_download_callback(
318 $charset, 427 $charset,
319 $title, 428 $title,
320 $desc, 429 $desc,
321 $keywords, 430 $keywords,
322 true, 431 true
323 'ut_curl_getinfo_ok'
324 ); 432 );
325 $data = [ 433 $data = [
326 'HTTP/1.1 200 OK',
327 'Server: GitHub.com',
328 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
329 'Content-Type: text/html; charset=utf-8',
330 'Status: 200 OK',
331 'th=device-width">' 434 'th=device-width">'
332 . '<title>Refactoring · GitHub</title>' 435 . '<title>Refactoring · GitHub</title>'
333 . '<link rel="search" type="application/opensea', 436 . '<link rel="search" type="application/opensea',
@@ -335,14 +438,11 @@ class LinkUtilsTest extends TestCase
335 . '<meta name="description" content="link desc" />' 438 . '<meta name="description" content="link desc" />'
336 . '<meta name="keywords" content="key1,key2" />', 439 . '<meta name="keywords" content="key1,key2" />',
337 ]; 440 ];
338 foreach ($data as $key => $line) { 441
339 $ignore = null; 442 foreach ($data as $chunk) {
340 $expected = $key !== 'end' ? strlen($line) : false; 443 static::assertSame(strlen($chunk), $callback(null, $chunk));
341 $this->assertEquals($expected, $callback($ignore, $line));
342 if ($expected === false) {
343 break;
344 }
345 } 444 }
445
346 $this->assertEquals('utf-8', $charset); 446 $this->assertEquals('utf-8', $charset);
347 $this->assertEquals('Refactoring · GitHub', $title); 447 $this->assertEquals('Refactoring · GitHub', $title);
348 $this->assertEquals('link desc', $desc); 448 $this->assertEquals('link desc', $desc);
@@ -353,8 +453,9 @@ class LinkUtilsTest extends TestCase
353 * Test the download callback with valid value, and retrieve_description option enabled, 453 * Test the download callback with valid value, and retrieve_description option enabled,
354 * but no desc or keyword defined in the page. 454 * but no desc or keyword defined in the page.
355 */ 455 */
356 public function testCurlDownloadCallbackOkWithDescNotFound() 456 public function testCurlDownloadCallbackOkWithDescNotFound(): void
357 { 457 {
458 $charset = 'utf-8';
358 $callback = get_curl_download_callback( 459 $callback = get_curl_download_callback(
359 $charset, 460 $charset,
360 $title, 461 $title,
@@ -364,24 +465,16 @@ class LinkUtilsTest extends TestCase
364 'ut_curl_getinfo_ok' 465 'ut_curl_getinfo_ok'
365 ); 466 );
366 $data = [ 467 $data = [
367 'HTTP/1.1 200 OK',
368 'Server: GitHub.com',
369 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
370 'Content-Type: text/html; charset=utf-8',
371 'Status: 200 OK',
372 'th=device-width">' 468 'th=device-width">'
373 . '<title>Refactoring · GitHub</title>' 469 . '<title>Refactoring · GitHub</title>'
374 . '<link rel="search" type="application/opensea', 470 . '<link rel="search" type="application/opensea',
375 'end' => '<title>ignored</title>', 471 'end' => '<title>ignored</title>',
376 ]; 472 ];
377 foreach ($data as $key => $line) { 473
378 $ignore = null; 474 foreach ($data as $chunk) {
379 $expected = $key !== 'end' ? strlen($line) : false; 475 static::assertSame(strlen($chunk), $callback(null, $chunk));
380 $this->assertEquals($expected, $callback($ignore, $line));
381 if ($expected === false) {
382 break;
383 }
384 } 476 }
477
385 $this->assertEquals('utf-8', $charset); 478 $this->assertEquals('utf-8', $charset);
386 $this->assertEquals('Refactoring · GitHub', $title); 479 $this->assertEquals('Refactoring · GitHub', $title);
387 $this->assertEmpty($desc); 480 $this->assertEmpty($desc);
@@ -439,13 +532,13 @@ class LinkUtilsTest extends TestCase
439 カタカナ #カタカナ」カタカナ\n'; 532 カタカナ #カタカナ」カタカナ\n';
440 $autolinkedDescription = hashtag_autolink($rawDescription, $index); 533 $autolinkedDescription = hashtag_autolink($rawDescription, $index);
441 534
442 $this->assertContains($this->getHashtagLink('hashtag', $index), $autolinkedDescription); 535 $this->assertContainsPolyfill($this->getHashtagLink('hashtag', $index), $autolinkedDescription);
443 $this->assertNotContains(' #hashtag', $autolinkedDescription); 536 $this->assertNotContainsPolyfill(' #hashtag', $autolinkedDescription);
444 $this->assertNotContains('>#nothashtag', $autolinkedDescription); 537 $this->assertNotContainsPolyfill('>#nothashtag', $autolinkedDescription);
445 $this->assertContains($this->getHashtagLink('ашок', $index), $autolinkedDescription); 538 $this->assertContainsPolyfill($this->getHashtagLink('ашок', $index), $autolinkedDescription);
446 $this->assertContains($this->getHashtagLink('カタカナ', $index), $autolinkedDescription); 539 $this->assertContainsPolyfill($this->getHashtagLink('カタカナ', $index), $autolinkedDescription);
447 $this->assertContains($this->getHashtagLink('hashtag_hashtag', $index), $autolinkedDescription); 540 $this->assertContainsPolyfill($this->getHashtagLink('hashtag_hashtag', $index), $autolinkedDescription);
448 $this->assertNotContains($this->getHashtagLink('hashtag-nothashtag', $index), $autolinkedDescription); 541 $this->assertNotContainsPolyfill($this->getHashtagLink('hashtag-nothashtag', $index), $autolinkedDescription);
449 } 542 }
450 543
451 /** 544 /**
@@ -456,9 +549,9 @@ class LinkUtilsTest extends TestCase
456 $rawDescription = 'blabla #hashtag x#nothashtag'; 549 $rawDescription = 'blabla #hashtag x#nothashtag';
457 $autolinkedDescription = hashtag_autolink($rawDescription); 550 $autolinkedDescription = hashtag_autolink($rawDescription);
458 551
459 $this->assertContains($this->getHashtagLink('hashtag'), $autolinkedDescription); 552 $this->assertContainsPolyfill($this->getHashtagLink('hashtag'), $autolinkedDescription);
460 $this->assertNotContains(' #hashtag', $autolinkedDescription); 553 $this->assertNotContainsPolyfill(' #hashtag', $autolinkedDescription);
461 $this->assertNotContains('>#nothashtag', $autolinkedDescription); 554 $this->assertNotContainsPolyfill('>#nothashtag', $autolinkedDescription);
462 } 555 }
463 556
464 /** 557 /**
@@ -491,7 +584,7 @@ class LinkUtilsTest extends TestCase
491 */ 584 */
492 private function getHashtagLink($hashtag, $index = '') 585 private function getHashtagLink($hashtag, $index = '')
493 { 586 {
494 $hashtagLink = '<a href="' . $index . '?addtag=$1" title="Hashtag $1">#$1</a>'; 587 $hashtagLink = '<a href="' . $index . './add-tag/$1" title="Hashtag $1">#$1</a>';
495 return str_replace('$1', $hashtag, $hashtagLink); 588 return str_replace('$1', $hashtag, $hashtagLink);
496 } 589 }
497} 590}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 0afbcba6..3508a7b1 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -18,7 +18,19 @@ require_once 'application/bookmark/LinkUtils.php';
18require_once 'application/Utils.php'; 18require_once 'application/Utils.php';
19require_once 'application/http/UrlUtils.php'; 19require_once 'application/http/UrlUtils.php';
20require_once 'application/http/HttpUtils.php'; 20require_once 'application/http/HttpUtils.php';
21require_once 'application/feed/Cache.php'; 21require_once 'tests/TestCase.php';
22require_once 'tests/utils/ReferenceLinkDB.php'; 22require_once 'tests/container/ShaarliTestContainer.php';
23require_once 'tests/utils/ReferenceHistory.php'; 23require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php';
24require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php';
25require_once 'tests/updater/DummyUpdater.php';
24require_once 'tests/utils/FakeBookmarkService.php'; 26require_once 'tests/utils/FakeBookmarkService.php';
27require_once 'tests/utils/FakeConfigManager.php';
28require_once 'tests/utils/ReferenceHistory.php';
29require_once 'tests/utils/ReferenceLinkDB.php';
30require_once 'tests/utils/ReferenceSessionIdHashes.php';
31
32\ReferenceSessionIdHashes::genAllHashes();
33
34if (!defined('SHAARLI_MUTEX_FILE')) {
35 define('SHAARLI_MUTEX_FILE', __FILE__);
36}
diff --git a/tests/config/ConfigJsonTest.php b/tests/config/ConfigJsonTest.php
index 33160eb0..c0ba5b8f 100644
--- a/tests/config/ConfigJsonTest.php
+++ b/tests/config/ConfigJsonTest.php
@@ -4,14 +4,14 @@ namespace Shaarli\Config;
4/** 4/**
5 * Class ConfigJsonTest 5 * Class ConfigJsonTest
6 */ 6 */
7class ConfigJsonTest extends \PHPUnit\Framework\TestCase 7class ConfigJsonTest extends \Shaarli\TestCase
8{ 8{
9 /** 9 /**
10 * @var ConfigJson 10 * @var ConfigJson
11 */ 11 */
12 protected $configIO; 12 protected $configIO;
13 13
14 public function setUp() 14 protected function setUp(): void
15 { 15 {
16 $this->configIO = new ConfigJson(); 16 $this->configIO = new ConfigJson();
17 } 17 }
@@ -38,12 +38,12 @@ class ConfigJsonTest extends \PHPUnit\Framework\TestCase
38 38
39 /** 39 /**
40 * Read a non existent config file -> empty array. 40 * Read a non existent config file -> empty array.
41 *
42 * @expectedException \Exception
43 * @expectedExceptionMessageRegExp /An error occurred while parsing JSON configuration file \([\w\/\.]+\): error code #4/
44 */ 41 */
45 public function testReadInvalidJson() 42 public function testReadInvalidJson()
46 { 43 {
44 $this->expectException(\Exception::class);
45 $this->expectExceptionMessageRegExp('/An error occurred while parsing JSON configuration file \\([\\w\\/\\.]+\\): error code #4/');
46
47 $this->configIO->read('tests/utils/config/configInvalid.json.php'); 47 $this->configIO->read('tests/utils/config/configInvalid.json.php');
48 } 48 }
49 49
@@ -110,22 +110,11 @@ class ConfigJsonTest extends \PHPUnit\Framework\TestCase
110 110
111 /** 111 /**
112 * Write to invalid path. 112 * Write to invalid path.
113 *
114 * @expectedException \Shaarli\Exceptions\IOException
115 */
116 public function testWriteInvalidArray()
117 {
118 $conf = array('conf' => 'value');
119 @$this->configIO->write(array(), $conf);
120 }
121
122 /**
123 * Write to invalid path.
124 *
125 * @expectedException \Shaarli\Exceptions\IOException
126 */ 113 */
127 public function testWriteInvalidBlank() 114 public function testWriteInvalidBlank()
128 { 115 {
116 $this->expectException(\Shaarli\Exceptions\IOException::class);
117
129 $conf = array('conf' => 'value'); 118 $conf = array('conf' => 'value');
130 @$this->configIO->write('', $conf); 119 @$this->configIO->write('', $conf);
131 } 120 }
diff --git a/tests/config/ConfigManagerTest.php b/tests/config/ConfigManagerTest.php
index 33830bc9..65d8ba2c 100644
--- a/tests/config/ConfigManagerTest.php
+++ b/tests/config/ConfigManagerTest.php
@@ -7,14 +7,14 @@ namespace Shaarli\Config;
7 * Note: it only test the manager with ConfigJson, 7 * Note: it only test the manager with ConfigJson,
8 * ConfigPhp is only a workaround to handle the transition to JSON type. 8 * ConfigPhp is only a workaround to handle the transition to JSON type.
9 */ 9 */
10class ConfigManagerTest extends \PHPUnit\Framework\TestCase 10class ConfigManagerTest extends \Shaarli\TestCase
11{ 11{
12 /** 12 /**
13 * @var ConfigManager 13 * @var ConfigManager
14 */ 14 */
15 protected $conf; 15 protected $conf;
16 16
17 public function setUp() 17 protected function setUp(): void
18 { 18 {
19 $this->conf = new ConfigManager('tests/utils/config/configJson'); 19 $this->conf = new ConfigManager('tests/utils/config/configJson');
20 } 20 }
@@ -95,44 +95,44 @@ class ConfigManagerTest extends \PHPUnit\Framework\TestCase
95 95
96 /** 96 /**
97 * Set with an empty key. 97 * Set with an empty key.
98 *
99 * @expectedException \Exception
100 * @expectedExceptionMessageRegExp #^Invalid setting key parameter. String expected, got.*#
101 */ 98 */
102 public function testSetEmptyKey() 99 public function testSetEmptyKey()
103 { 100 {
101 $this->expectException(\Exception::class);
102 $this->expectExceptionMessageRegExp('#^Invalid setting key parameter. String expected, got.*#');
103
104 $this->conf->set('', 'stuff'); 104 $this->conf->set('', 'stuff');
105 } 105 }
106 106
107 /** 107 /**
108 * Set with an array key. 108 * Set with an array key.
109 *
110 * @expectedException \Exception
111 * @expectedExceptionMessageRegExp #^Invalid setting key parameter. String expected, got.*#
112 */ 109 */
113 public function testSetArrayKey() 110 public function testSetArrayKey()
114 { 111 {
112 $this->expectException(\Exception::class);
113 $this->expectExceptionMessageRegExp('#^Invalid setting key parameter. String expected, got.*#');
114
115 $this->conf->set(array('foo' => 'bar'), 'stuff'); 115 $this->conf->set(array('foo' => 'bar'), 'stuff');
116 } 116 }
117 117
118 /** 118 /**
119 * Remove with an empty key. 119 * Remove with an empty key.
120 *
121 * @expectedException \Exception
122 * @expectedExceptionMessageRegExp #^Invalid setting key parameter. String expected, got.*#
123 */ 120 */
124 public function testRmoveEmptyKey() 121 public function testRmoveEmptyKey()
125 { 122 {
123 $this->expectException(\Exception::class);
124 $this->expectExceptionMessageRegExp('#^Invalid setting key parameter. String expected, got.*#');
125
126 $this->conf->remove(''); 126 $this->conf->remove('');
127 } 127 }
128 128
129 /** 129 /**
130 * Try to write the config without mandatory parameter (e.g. 'login'). 130 * Try to write the config without mandatory parameter (e.g. 'login').
131 *
132 * @expectedException Shaarli\Config\Exception\MissingFieldConfigException
133 */ 131 */
134 public function testWriteMissingParameter() 132 public function testWriteMissingParameter()
135 { 133 {
134 $this->expectException(\Shaarli\Config\Exception\MissingFieldConfigException::class);
135
136 $this->conf->setConfigFile('tests/utils/config/configTmp'); 136 $this->conf->setConfigFile('tests/utils/config/configTmp');
137 $this->assertFalse(file_exists($this->conf->getConfigFileExt())); 137 $this->assertFalse(file_exists($this->conf->getConfigFileExt()));
138 $this->conf->reload(); 138 $this->conf->reload();
diff --git a/tests/config/ConfigPhpTest.php b/tests/config/ConfigPhpTest.php
index fb91b51b..7bf9fe64 100644
--- a/tests/config/ConfigPhpTest.php
+++ b/tests/config/ConfigPhpTest.php
@@ -8,14 +8,14 @@ namespace Shaarli\Config;
8 * which are kept between tests. 8 * which are kept between tests.
9 * @runTestsInSeparateProcesses 9 * @runTestsInSeparateProcesses
10 */ 10 */
11class ConfigPhpTest extends \PHPUnit\Framework\TestCase 11class ConfigPhpTest extends \Shaarli\TestCase
12{ 12{
13 /** 13 /**
14 * @var ConfigPhp 14 * @var ConfigPhp
15 */ 15 */
16 protected $configIO; 16 protected $configIO;
17 17
18 public function setUp() 18 protected function setUp(): void
19 { 19 {
20 $this->configIO = new ConfigPhp(); 20 $this->configIO = new ConfigPhp();
21 } 21 }
diff --git a/tests/config/ConfigPluginTest.php b/tests/config/ConfigPluginTest.php
index d7a70e68..fa72d8c4 100644
--- a/tests/config/ConfigPluginTest.php
+++ b/tests/config/ConfigPluginTest.php
@@ -2,13 +2,14 @@
2namespace Shaarli\Config; 2namespace Shaarli\Config;
3 3
4use Shaarli\Config\Exception\PluginConfigOrderException; 4use Shaarli\Config\Exception\PluginConfigOrderException;
5use Shaarli\Plugin\PluginManager;
5 6
6require_once 'application/config/ConfigPlugin.php'; 7require_once 'application/config/ConfigPlugin.php';
7 8
8/** 9/**
9 * Unitary tests for Shaarli config related functions 10 * Unitary tests for Shaarli config related functions
10 */ 11 */
11class ConfigPluginTest extends \PHPUnit\Framework\TestCase 12class ConfigPluginTest extends \Shaarli\TestCase
12{ 13{
13 /** 14 /**
14 * Test save_plugin_config with valid data. 15 * Test save_plugin_config with valid data.
@@ -17,32 +18,39 @@ class ConfigPluginTest extends \PHPUnit\Framework\TestCase
17 */ 18 */
18 public function testSavePluginConfigValid() 19 public function testSavePluginConfigValid()
19 { 20 {
20 $data = array( 21 $data = [
21 'order_plugin1' => 2, // no plugin related 22 'order_plugin1' => 2, // no plugin related
22 'plugin2' => 0, // new - at the end 23 'plugin2' => 0, // new - at the end
23 'plugin3' => 0, // 2nd 24 'plugin3' => 0, // 2nd
24 'order_plugin3' => 8, 25 'order_plugin3' => 8,
25 'plugin4' => 0, // 1st 26 'plugin4' => 0, // 1st
26 'order_plugin4' => 5, 27 'order_plugin4' => 5,
27 ); 28 ];
28 29
29 $expected = array( 30 $expected = [
30 'plugin3', 31 'plugin3',
31 'plugin4', 32 'plugin4',
32 'plugin2', 33 'plugin2',
33 ); 34 ];
35
36 mkdir($path = __DIR__ . '/folder');
37 PluginManager::$PLUGINS_PATH = $path;
38 array_map(function (string $plugin) use ($path) { touch($path . '/' . $plugin); }, $expected);
34 39
35 $out = save_plugin_config($data); 40 $out = save_plugin_config($data);
36 $this->assertEquals($expected, $out); 41 $this->assertEquals($expected, $out);
42
43 array_map(function (string $plugin) use ($path) { unlink($path . '/' . $plugin); }, $expected);
44 rmdir($path);
37 } 45 }
38 46
39 /** 47 /**
40 * Test save_plugin_config with invalid data. 48 * Test save_plugin_config with invalid data.
41 *
42 * @expectedException Shaarli\Config\Exception\PluginConfigOrderException
43 */ 49 */
44 public function testSavePluginConfigInvalid() 50 public function testSavePluginConfigInvalid()
45 { 51 {
52 $this->expectException(\Shaarli\Config\Exception\PluginConfigOrderException::class);
53
46 $data = array( 54 $data = array(
47 'plugin2' => 0, 55 'plugin2' => 0,
48 'plugin3' => 0, 56 'plugin3' => 0,
diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php
index 9b97ed6d..3d43c344 100644
--- a/tests/container/ContainerBuilderTest.php
+++ b/tests/container/ContainerBuilderTest.php
@@ -4,13 +4,27 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Container; 5namespace Shaarli\Container;
6 6
7use PHPUnit\Framework\TestCase; 7use Psr\Log\LoggerInterface;
8use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
10use Shaarli\Feed\FeedBuilder;
11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\Front\Controller\Visitor\ErrorController;
13use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
10use Shaarli\History; 14use Shaarli\History;
15use Shaarli\Http\HttpAccess;
16use Shaarli\Http\MetadataRetriever;
17use Shaarli\Netscape\NetscapeBookmarkUtils;
18use Shaarli\Plugin\PluginManager;
11use Shaarli\Render\PageBuilder; 19use Shaarli\Render\PageBuilder;
20use Shaarli\Render\PageCacheManager;
21use Shaarli\Security\CookieManager;
12use Shaarli\Security\LoginManager; 22use Shaarli\Security\LoginManager;
13use Shaarli\Security\SessionManager; 23use Shaarli\Security\SessionManager;
24use Shaarli\TestCase;
25use Shaarli\Thumbnailer;
26use Shaarli\Updater\Updater;
27use Slim\Http\Environment;
14 28
15class ContainerBuilderTest extends TestCase 29class ContainerBuilderTest extends TestCase
16{ 30{
@@ -26,24 +40,54 @@ class ContainerBuilderTest extends TestCase
26 /** @var ContainerBuilder */ 40 /** @var ContainerBuilder */
27 protected $containerBuilder; 41 protected $containerBuilder;
28 42
43 /** @var CookieManager */
44 protected $cookieManager;
45
29 public function setUp(): void 46 public function setUp(): void
30 { 47 {
31 $this->conf = new ConfigManager('tests/utils/config/configJson'); 48 $this->conf = new ConfigManager('tests/utils/config/configJson');
32 $this->sessionManager = $this->createMock(SessionManager::class); 49 $this->sessionManager = $this->createMock(SessionManager::class);
50 $this->cookieManager = $this->createMock(CookieManager::class);
51
33 $this->loginManager = $this->createMock(LoginManager::class); 52 $this->loginManager = $this->createMock(LoginManager::class);
53 $this->loginManager->method('isLoggedIn')->willReturn(true);
34 54
35 $this->containerBuilder = new ContainerBuilder($this->conf, $this->sessionManager, $this->loginManager); 55 $this->containerBuilder = new ContainerBuilder(
56 $this->conf,
57 $this->sessionManager,
58 $this->cookieManager,
59 $this->loginManager,
60 $this->createMock(LoggerInterface::class)
61 );
36 } 62 }
37 63
38 public function testBuildContainer(): void 64 public function testBuildContainer(): void
39 { 65 {
40 $container = $this->containerBuilder->build(); 66 $container = $this->containerBuilder->build();
41 67
68 static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
69 static::assertInstanceOf(CookieManager::class, $container->cookieManager);
42 static::assertInstanceOf(ConfigManager::class, $container->conf); 70 static::assertInstanceOf(ConfigManager::class, $container->conf);
43 static::assertInstanceOf(SessionManager::class, $container->sessionManager); 71 static::assertInstanceOf(ErrorController::class, $container->errorHandler);
44 static::assertInstanceOf(LoginManager::class, $container->loginManager); 72 static::assertInstanceOf(Environment::class, $container->environment);
73 static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder);
74 static::assertInstanceOf(FormatterFactory::class, $container->formatterFactory);
45 static::assertInstanceOf(History::class, $container->history); 75 static::assertInstanceOf(History::class, $container->history);
46 static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService); 76 static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
77 static::assertInstanceOf(LoginManager::class, $container->loginManager);
78 static::assertInstanceOf(LoggerInterface::class, $container->logger);
79 static::assertInstanceOf(MetadataRetriever::class, $container->metadataRetriever);
80 static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
47 static::assertInstanceOf(PageBuilder::class, $container->pageBuilder); 81 static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
82 static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager);
83 static::assertInstanceOf(ErrorController::class, $container->phpErrorHandler);
84 static::assertInstanceOf(ErrorNotFoundController::class, $container->notFoundHandler);
85 static::assertInstanceOf(PluginManager::class, $container->pluginManager);
86 static::assertInstanceOf(SessionManager::class, $container->sessionManager);
87 static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer);
88 static::assertInstanceOf(Updater::class, $container->updater);
89
90 // Set by the middleware
91 static::assertNull($container->basePath);
48 } 92 }
49} 93}
diff --git a/tests/container/ShaarliTestContainer.php b/tests/container/ShaarliTestContainer.php
new file mode 100644
index 00000000..7dbe914c
--- /dev/null
+++ b/tests/container/ShaarliTestContainer.php
@@ -0,0 +1,42 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Container;
6
7use PHPUnit\Framework\MockObject\MockObject;
8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Feed\FeedBuilder;
11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\History;
13use Shaarli\Http\HttpAccess;
14use Shaarli\Plugin\PluginManager;
15use Shaarli\Render\PageBuilder;
16use Shaarli\Render\PageCacheManager;
17use Shaarli\Security\LoginManager;
18use Shaarli\Security\SessionManager;
19use Shaarli\Thumbnailer;
20
21/**
22 * Test helper allowing auto-completion for MockObjects.
23 *
24 * @property mixed[] $environment $_SERVER automatically injected by Slim
25 * @property MockObject|ConfigManager $conf
26 * @property MockObject|SessionManager $sessionManager
27 * @property MockObject|LoginManager $loginManager
28 * @property MockObject|string $webPath
29 * @property MockObject|History $history
30 * @property MockObject|BookmarkServiceInterface $bookmarkService
31 * @property MockObject|PageBuilder $pageBuilder
32 * @property MockObject|PluginManager $pluginManager
33 * @property MockObject|FormatterFactory $formatterFactory
34 * @property MockObject|PageCacheManager $pageCacheManager
35 * @property MockObject|FeedBuilder $feedBuilder
36 * @property MockObject|Thumbnailer $thumbnailer
37 * @property MockObject|HttpAccess $httpAccess
38 */
39class ShaarliTestContainer extends ShaarliContainer
40{
41
42}
diff --git a/tests/feed/CachedPageTest.php b/tests/feed/CachedPageTest.php
index 363028a2..904db9dc 100644
--- a/tests/feed/CachedPageTest.php
+++ b/tests/feed/CachedPageTest.php
@@ -7,17 +7,17 @@ namespace Shaarli\Feed;
7/** 7/**
8 * Unitary tests for cached pages 8 * Unitary tests for cached pages
9 */ 9 */
10class CachedPageTest extends \PHPUnit\Framework\TestCase 10class CachedPageTest extends \Shaarli\TestCase
11{ 11{
12 // test cache directory 12 // test cache directory
13 protected static $testCacheDir = 'sandbox/pagecache'; 13 protected static $testCacheDir = 'sandbox/pagecache';
14 protected static $url = 'http://shaar.li/?do=atom'; 14 protected static $url = 'http://shaar.li/feed/atom';
15 protected static $filename; 15 protected static $filename;
16 16
17 /** 17 /**
18 * Create the cache directory if needed 18 * Create the cache directory if needed
19 */ 19 */
20 public static function setUpBeforeClass() 20 public static function setUpBeforeClass(): void
21 { 21 {
22 if (!is_dir(self::$testCacheDir)) { 22 if (!is_dir(self::$testCacheDir)) {
23 mkdir(self::$testCacheDir); 23 mkdir(self::$testCacheDir);
@@ -28,7 +28,7 @@ class CachedPageTest extends \PHPUnit\Framework\TestCase
28 /** 28 /**
29 * Reset the page cache 29 * Reset the page cache
30 */ 30 */
31 public function setUp() 31 protected function setUp(): void
32 { 32 {
33 if (file_exists(self::$filename)) { 33 if (file_exists(self::$filename)) {
34 unlink(self::$filename); 34 unlink(self::$filename);
@@ -42,8 +42,8 @@ class CachedPageTest extends \PHPUnit\Framework\TestCase
42 { 42 {
43 new CachedPage(self::$testCacheDir, '', true); 43 new CachedPage(self::$testCacheDir, '', true);
44 new CachedPage(self::$testCacheDir, '', false); 44 new CachedPage(self::$testCacheDir, '', false);
45 new CachedPage(self::$testCacheDir, 'http://shaar.li/?do=rss', true); 45 new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true);
46 new CachedPage(self::$testCacheDir, 'http://shaar.li/?do=atom', false); 46 new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false);
47 $this->addToAssertionCount(1); 47 $this->addToAssertionCount(1);
48 } 48 }
49 49
diff --git a/tests/feed/FeedBuilderTest.php b/tests/feed/FeedBuilderTest.php
index 54671891..6b9204eb 100644
--- a/tests/feed/FeedBuilderTest.php
+++ b/tests/feed/FeedBuilderTest.php
@@ -3,6 +3,7 @@
3namespace Shaarli\Feed; 3namespace Shaarli\Feed;
4 4
5use DateTime; 5use DateTime;
6use malkusch\lock\mutex\NoMutex;
6use ReferenceLinkDB; 7use ReferenceLinkDB;
7use Shaarli\Bookmark\Bookmark; 8use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\BookmarkFileService; 9use Shaarli\Bookmark\BookmarkFileService;
@@ -10,13 +11,14 @@ use Shaarli\Bookmark\LinkDB;
10use Shaarli\Config\ConfigManager; 11use Shaarli\Config\ConfigManager;
11use Shaarli\Formatter\FormatterFactory; 12use Shaarli\Formatter\FormatterFactory;
12use Shaarli\History; 13use Shaarli\History;
14use Shaarli\TestCase;
13 15
14/** 16/**
15 * FeedBuilderTest class. 17 * FeedBuilderTest class.
16 * 18 *
17 * Unit tests for FeedBuilder. 19 * Unit tests for FeedBuilder.
18 */ 20 */
19class FeedBuilderTest extends \PHPUnit\Framework\TestCase 21class FeedBuilderTest extends TestCase
20{ 22{
21 /** 23 /**
22 * @var string locale Basque (Spain). 24 * @var string locale Basque (Spain).
@@ -44,8 +46,9 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
44 /** 46 /**
45 * Called before every test method. 47 * Called before every test method.
46 */ 48 */
47 public static function setUpBeforeClass() 49 public static function setUpBeforeClass(): void
48 { 50 {
51 $mutex = new NoMutex();
49 $conf = new ConfigManager('tests/utils/config/configJson'); 52 $conf = new ConfigManager('tests/utils/config/configJson');
50 $conf->set('resource.datastore', self::$testDatastore); 53 $conf->set('resource.datastore', self::$testDatastore);
51 $refLinkDB = new \ReferenceLinkDB(); 54 $refLinkDB = new \ReferenceLinkDB();
@@ -53,35 +56,18 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
53 $history = new History('sandbox/history.php'); 56 $history = new History('sandbox/history.php');
54 $factory = new FormatterFactory($conf, true); 57 $factory = new FormatterFactory($conf, true);
55 self::$formatter = $factory->getFormatter(); 58 self::$formatter = $factory->getFormatter();
56 self::$bookmarkService = new BookmarkFileService($conf, $history, true); 59 self::$bookmarkService = new BookmarkFileService($conf, $history, $mutex, true);
57 60
58 self::$serverInfo = array( 61 self::$serverInfo = array(
59 'HTTPS' => 'Off', 62 'HTTPS' => 'Off',
60 'SERVER_NAME' => 'host.tld', 63 'SERVER_NAME' => 'host.tld',
61 'SERVER_PORT' => '80', 64 'SERVER_PORT' => '80',
62 'SCRIPT_NAME' => '/index.php', 65 'SCRIPT_NAME' => '/index.php',
63 'REQUEST_URI' => '/index.php?do=feed', 66 'REQUEST_URI' => '/feed/atom',
64 ); 67 );
65 } 68 }
66 69
67 /** 70 /**
68 * Test GetTypeLanguage().
69 */
70 public function testGetTypeLanguage()
71 {
72 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
73 $feedBuilder->setLocale(self::$LOCALE);
74 $this->assertEquals(self::$ATOM_LANGUAGUE, $feedBuilder->getTypeLanguage());
75 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
76 $feedBuilder->setLocale(self::$LOCALE);
77 $this->assertEquals(self::$RSS_LANGUAGE, $feedBuilder->getTypeLanguage());
78 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
79 $this->assertEquals('en', $feedBuilder->getTypeLanguage());
80 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
81 $this->assertEquals('en-en', $feedBuilder->getTypeLanguage());
82 }
83
84 /**
85 * Test buildData with RSS feed. 71 * Test buildData with RSS feed.
86 */ 72 */
87 public function testRSSBuildData() 73 public function testRSSBuildData()
@@ -89,35 +75,33 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
89 $feedBuilder = new FeedBuilder( 75 $feedBuilder = new FeedBuilder(
90 self::$bookmarkService, 76 self::$bookmarkService,
91 self::$formatter, 77 self::$formatter,
92 FeedBuilder::$FEED_RSS, 78 static::$serverInfo,
93 self::$serverInfo,
94 null,
95 false 79 false
96 ); 80 );
97 $feedBuilder->setLocale(self::$LOCALE); 81 $feedBuilder->setLocale(self::$LOCALE);
98 $data = $feedBuilder->buildData(); 82 $data = $feedBuilder->buildData(FeedBuilder::$FEED_RSS, null);
99 // Test headers (RSS) 83 // Test headers (RSS)
100 $this->assertEquals(self::$RSS_LANGUAGE, $data['language']); 84 $this->assertEquals(self::$RSS_LANGUAGE, $data['language']);
101 $this->assertRegExp('/Wed, 03 Aug 2016 09:30:33 \+\d{4}/', $data['last_update']); 85 $this->assertRegExp('/Wed, 03 Aug 2016 09:30:33 \+\d{4}/', $data['last_update']);
102 $this->assertEquals(true, $data['show_dates']); 86 $this->assertEquals(true, $data['show_dates']);
103 $this->assertEquals('http://host.tld/index.php?do=feed', $data['self_link']); 87 $this->assertEquals('http://host.tld/feed/atom', $data['self_link']);
104 $this->assertEquals('http://host.tld/', $data['index_url']); 88 $this->assertEquals('http://host.tld/', $data['index_url']);
105 $this->assertFalse($data['usepermalinks']); 89 $this->assertFalse($data['usepermalinks']);
106 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 90 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
107 91
108 // Test first not pinned link (note link) 92 // Test first not pinned link (note link)
109 $link = $data['links'][array_keys($data['links'])[2]]; 93 $link = $data['links'][array_keys($data['links'])[0]];
110 $this->assertEquals(41, $link['id']); 94 $this->assertEquals(41, $link['id']);
111 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 95 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
112 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']); 96 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['guid']);
113 $this->assertEquals('http://host.tld/?WDWyig', $link['url']); 97 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['url']);
114 $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']); 98 $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']);
115 $pub = DateTime::createFromFormat(DateTime::RSS, $link['pub_iso_date']); 99 $pub = DateTime::createFromFormat(DateTime::RSS, $link['pub_iso_date']);
116 $up = DateTime::createFromFormat(DateTime::ATOM, $link['up_iso_date']); 100 $up = DateTime::createFromFormat(DateTime::ATOM, $link['up_iso_date']);
117 $this->assertEquals($pub, $up); 101 $this->assertEquals($pub, $up);
118 $this->assertContains('Stallman has a beard', $link['description']); 102 $this->assertContainsPolyfill('Stallman has a beard', $link['description']);
119 $this->assertContains('Permalink', $link['description']); 103 $this->assertContainsPolyfill('Permalink', $link['description']);
120 $this->assertContains('http://host.tld/?WDWyig', $link['description']); 104 $this->assertContainsPolyfill('http://host.tld/shaare/WDWyig', $link['description']);
121 $this->assertEquals(1, count($link['taglist'])); 105 $this->assertEquals(1, count($link['taglist']));
122 $this->assertEquals('sTuff', $link['taglist'][0]); 106 $this->assertEquals('sTuff', $link['taglist'][0]);
123 107
@@ -140,16 +124,14 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
140 $feedBuilder = new FeedBuilder( 124 $feedBuilder = new FeedBuilder(
141 self::$bookmarkService, 125 self::$bookmarkService,
142 self::$formatter, 126 self::$formatter,
143 FeedBuilder::$FEED_ATOM, 127 static::$serverInfo,
144 self::$serverInfo,
145 null,
146 false 128 false
147 ); 129 );
148 $feedBuilder->setLocale(self::$LOCALE); 130 $feedBuilder->setLocale(self::$LOCALE);
149 $data = $feedBuilder->buildData(); 131 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
150 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 132 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
151 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']); 133 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']);
152 $link = $data['links'][array_keys($data['links'])[2]]; 134 $link = $data['links'][array_keys($data['links'])[0]];
153 $this->assertRegExp('/2015-03-10T11:46:51\+\d{2}:\d{2}/', $link['pub_iso_date']); 135 $this->assertRegExp('/2015-03-10T11:46:51\+\d{2}:\d{2}/', $link['pub_iso_date']);
154 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links'][8]['up_iso_date']); 136 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links'][8]['up_iso_date']);
155 } 137 }
@@ -166,13 +148,11 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
166 $feedBuilder = new FeedBuilder( 148 $feedBuilder = new FeedBuilder(
167 self::$bookmarkService, 149 self::$bookmarkService,
168 self::$formatter, 150 self::$formatter,
169 FeedBuilder::$FEED_ATOM, 151 static::$serverInfo,
170 self::$serverInfo,
171 $criteria,
172 false 152 false
173 ); 153 );
174 $feedBuilder->setLocale(self::$LOCALE); 154 $feedBuilder->setLocale(self::$LOCALE);
175 $data = $feedBuilder->buildData(); 155 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, $criteria);
176 $this->assertEquals(1, count($data['links'])); 156 $this->assertEquals(1, count($data['links']));
177 $link = array_shift($data['links']); 157 $link = array_shift($data['links']);
178 $this->assertEquals(41, $link['id']); 158 $this->assertEquals(41, $link['id']);
@@ -190,15 +170,13 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
190 $feedBuilder = new FeedBuilder( 170 $feedBuilder = new FeedBuilder(
191 self::$bookmarkService, 171 self::$bookmarkService,
192 self::$formatter, 172 self::$formatter,
193 FeedBuilder::$FEED_ATOM, 173 static::$serverInfo,
194 self::$serverInfo,
195 $criteria,
196 false 174 false
197 ); 175 );
198 $feedBuilder->setLocale(self::$LOCALE); 176 $feedBuilder->setLocale(self::$LOCALE);
199 $data = $feedBuilder->buildData(); 177 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, $criteria);
200 $this->assertEquals(3, count($data['links'])); 178 $this->assertEquals(3, count($data['links']));
201 $link = $data['links'][array_keys($data['links'])[2]]; 179 $link = $data['links'][array_keys($data['links'])[0]];
202 $this->assertEquals(41, $link['id']); 180 $this->assertEquals(41, $link['id']);
203 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 181 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
204 } 182 }
@@ -211,32 +189,30 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
211 $feedBuilder = new FeedBuilder( 189 $feedBuilder = new FeedBuilder(
212 self::$bookmarkService, 190 self::$bookmarkService,
213 self::$formatter, 191 self::$formatter,
214 FeedBuilder::$FEED_ATOM, 192 static::$serverInfo,
215 self::$serverInfo,
216 null,
217 false 193 false
218 ); 194 );
219 $feedBuilder->setLocale(self::$LOCALE); 195 $feedBuilder->setLocale(self::$LOCALE);
220 $feedBuilder->setUsePermalinks(true); 196 $feedBuilder->setUsePermalinks(true);
221 $data = $feedBuilder->buildData(); 197 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
222 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 198 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
223 $this->assertTrue($data['usepermalinks']); 199 $this->assertTrue($data['usepermalinks']);
224 // First link is a permalink 200 // First link is a permalink
225 $link = $data['links'][array_keys($data['links'])[2]]; 201 $link = $data['links'][array_keys($data['links'])[0]];
226 $this->assertEquals(41, $link['id']); 202 $this->assertEquals(41, $link['id']);
227 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 203 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
228 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']); 204 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['guid']);
229 $this->assertEquals('http://host.tld/?WDWyig', $link['url']); 205 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['url']);
230 $this->assertContains('Direct link', $link['description']); 206 $this->assertContainsPolyfill('Direct link', $link['description']);
231 $this->assertContains('http://host.tld/?WDWyig', $link['description']); 207 $this->assertContainsPolyfill('http://host.tld/shaare/WDWyig', $link['description']);
232 // Second link is a direct link 208 // Second link is a direct link
233 $link = $data['links'][array_keys($data['links'])[3]]; 209 $link = $data['links'][array_keys($data['links'])[1]];
234 $this->assertEquals(8, $link['id']); 210 $this->assertEquals(8, $link['id']);
235 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'), $link['created']); 211 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'), $link['created']);
236 $this->assertEquals('http://host.tld/?RttfEw', $link['guid']); 212 $this->assertEquals('http://host.tld/shaare/RttfEw', $link['guid']);
237 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']); 213 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
238 $this->assertContains('Direct link', $link['description']); 214 $this->assertContainsPolyfill('Direct link', $link['description']);
239 $this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']); 215 $this->assertContainsPolyfill('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']);
240 } 216 }
241 217
242 /** 218 /**
@@ -247,14 +223,12 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
247 $feedBuilder = new FeedBuilder( 223 $feedBuilder = new FeedBuilder(
248 self::$bookmarkService, 224 self::$bookmarkService,
249 self::$formatter, 225 self::$formatter,
250 FeedBuilder::$FEED_ATOM, 226 static::$serverInfo,
251 self::$serverInfo,
252 null,
253 false 227 false
254 ); 228 );
255 $feedBuilder->setLocale(self::$LOCALE); 229 $feedBuilder->setLocale(self::$LOCALE);
256 $feedBuilder->setHideDates(true); 230 $feedBuilder->setHideDates(true);
257 $data = $feedBuilder->buildData(); 231 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
258 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 232 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
259 $this->assertFalse($data['show_dates']); 233 $this->assertFalse($data['show_dates']);
260 234
@@ -262,14 +236,12 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
262 $feedBuilder = new FeedBuilder( 236 $feedBuilder = new FeedBuilder(
263 self::$bookmarkService, 237 self::$bookmarkService,
264 self::$formatter, 238 self::$formatter,
265 FeedBuilder::$FEED_ATOM, 239 static::$serverInfo,
266 self::$serverInfo,
267 null,
268 true 240 true
269 ); 241 );
270 $feedBuilder->setLocale(self::$LOCALE); 242 $feedBuilder->setLocale(self::$LOCALE);
271 $feedBuilder->setHideDates(true); 243 $feedBuilder->setHideDates(true);
272 $data = $feedBuilder->buildData(); 244 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
273 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 245 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
274 $this->assertTrue($data['show_dates']); 246 $this->assertTrue($data['show_dates']);
275 } 247 }
@@ -284,28 +256,26 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
284 'SERVER_NAME' => 'host.tld', 256 'SERVER_NAME' => 'host.tld',
285 'SERVER_PORT' => '8080', 257 'SERVER_PORT' => '8080',
286 'SCRIPT_NAME' => '/~user/shaarli/index.php', 258 'SCRIPT_NAME' => '/~user/shaarli/index.php',
287 'REQUEST_URI' => '/~user/shaarli/index.php?do=feed', 259 'REQUEST_URI' => '/~user/shaarli/feed/atom',
288 ); 260 );
289 $feedBuilder = new FeedBuilder( 261 $feedBuilder = new FeedBuilder(
290 self::$bookmarkService, 262 self::$bookmarkService,
291 self::$formatter, 263 self::$formatter,
292 FeedBuilder::$FEED_ATOM,
293 $serverInfo, 264 $serverInfo,
294 null,
295 false 265 false
296 ); 266 );
297 $feedBuilder->setLocale(self::$LOCALE); 267 $feedBuilder->setLocale(self::$LOCALE);
298 $data = $feedBuilder->buildData(); 268 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
299 269
300 $this->assertEquals( 270 $this->assertEquals(
301 'http://host.tld:8080/~user/shaarli/index.php?do=feed', 271 'http://host.tld:8080/~user/shaarli/feed/atom',
302 $data['self_link'] 272 $data['self_link']
303 ); 273 );
304 274
305 // Test first link (note link) 275 // Test first link (note link)
306 $link = $data['links'][array_keys($data['links'])[2]]; 276 $link = $data['links'][array_keys($data['links'])[0]];
307 $this->assertEquals('http://host.tld:8080/~user/shaarli/?WDWyig', $link['guid']); 277 $this->assertEquals('http://host.tld:8080/~user/shaarli/shaare/WDWyig', $link['guid']);
308 $this->assertEquals('http://host.tld:8080/~user/shaarli/?WDWyig', $link['url']); 278 $this->assertEquals('http://host.tld:8080/~user/shaarli/shaare/WDWyig', $link['url']);
309 $this->assertContains('http://host.tld:8080/~user/shaarli/?addtag=hashtag', $link['description']); 279 $this->assertContainsPolyfill('http://host.tld:8080/~user/shaarli/./add-tag/hashtag', $link['description']);
310 } 280 }
311} 281}
diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php
index 382a560e..3fc6f8dc 100644
--- a/tests/formatter/BookmarkDefaultFormatterTest.php
+++ b/tests/formatter/BookmarkDefaultFormatterTest.php
@@ -3,9 +3,9 @@
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use DateTime; 5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\TestCase;
9 9
10/** 10/**
11 * Class BookmarkDefaultFormatterTest 11 * Class BookmarkDefaultFormatterTest
@@ -25,7 +25,7 @@ class BookmarkDefaultFormatterTest extends TestCase
25 /** 25 /**
26 * Initialize formatter instance. 26 * Initialize formatter instance.
27 */ 27 */
28 public function setUp() 28 protected function setUp(): void
29 { 29 {
30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php'); 30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
31 $this->conf = new ConfigManager(self::$testConf); 31 $this->conf = new ConfigManager(self::$testConf);
@@ -123,7 +123,7 @@ class BookmarkDefaultFormatterTest extends TestCase
123 $description[0] = 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'; 123 $description[0] = 'This a &lt;strong&gt;description&lt;/strong&gt;<br />';
124 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash'; 124 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
125 $description[1] = 'text <a href="'. $url .'">'. $url .'</a> more text<br />'; 125 $description[1] = 'text <a href="'. $url .'">'. $url .'</a> more text<br />';
126 $description[2] = 'Also, there is an <a href="?addtag=hashtag" '. 126 $description[2] = 'Also, there is an <a href="./add-tag/hashtag" '.
127 'title="Hashtag hashtag">#hashtag</a> added<br />'; 127 'title="Hashtag hashtag">#hashtag</a> added<br />';
128 $description[3] = '&nbsp; &nbsp; A &nbsp;N &nbsp;D KEEP &nbsp; &nbsp; '. 128 $description[3] = '&nbsp; &nbsp; A &nbsp;N &nbsp;D KEEP &nbsp; &nbsp; '.
129 'SPACES &nbsp; &nbsp;! &nbsp; <br />'; 129 'SPACES &nbsp; &nbsp;! &nbsp; <br />';
@@ -148,7 +148,7 @@ class BookmarkDefaultFormatterTest extends TestCase
148 $this->assertEquals($root . $short, $link['url']); 148 $this->assertEquals($root . $short, $link['url']);
149 $this->assertEquals($root . $short, $link['real_url']); 149 $this->assertEquals($root . $short, $link['real_url']);
150 $this->assertEquals( 150 $this->assertEquals(
151 'Text <a href="'. $root .'?addtag=hashtag" title="Hashtag hashtag">'. 151 'Text <a href="'. $root .'./add-tag/hashtag" title="Hashtag hashtag">'.
152 '#hashtag</a> more text', 152 '#hashtag</a> more text',
153 $link['description'] 153 $link['description']
154 ); 154 );
@@ -174,4 +174,119 @@ class BookmarkDefaultFormatterTest extends TestCase
174 $this->assertSame($tags, $link['taglist']); 174 $this->assertSame($tags, $link['taglist']);
175 $this->assertSame(implode(' ', $tags), $link['tags']); 175 $this->assertSame(implode(' ', $tags), $link['tags']);
176 } 176 }
177
178 /**
179 * Test formatTitleHtml with search result highlight.
180 */
181 public function testFormatTitleHtmlWithSearchHighlight(): void
182 {
183 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
184
185 $bookmark = new Bookmark();
186 $bookmark->setTitle('PSR-2: Coding Style Guide');
187 $bookmark->addAdditionalContentEntry(
188 'search_highlight',
189 ['title' => [
190 ['start' => 0, 'end' => 5], // "psr-2"
191 ['start' => 7, 'end' => 13], // coding
192 ['start' => 20, 'end' => 25], // guide
193 ]]
194 );
195
196 $link = $this->formatter->format($bookmark);
197
198 $this->assertSame(
199 '<span class="search-highlight">PSR-2</span>: ' .
200 '<span class="search-highlight">Coding</span> Style ' .
201 '<span class="search-highlight">Guide</span>',
202 $link['title_html']
203 );
204 }
205
206 /**
207 * Test formatDescription with search result highlight.
208 */
209 public function testFormatDescriptionWithSearchHighlight(): void
210 {
211 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
212
213 $bookmark = new Bookmark();
214 $bookmark->setDescription('This guide extends and expands on PSR-1, the basic coding standard.');
215 $bookmark->addAdditionalContentEntry(
216 'search_highlight',
217 ['description' => [
218 ['start' => 0, 'end' => 10], // "This guide"
219 ['start' => 45, 'end' => 50], // basic
220 ['start' => 58, 'end' => 67], // standard.
221 ]]
222 );
223
224 $link = $this->formatter->format($bookmark);
225
226 $this->assertSame(
227 '<span class="search-highlight">This guide</span> extends and expands on PSR-1, the ' .
228 '<span class="search-highlight">basic</span> coding ' .
229 '<span class="search-highlight">standard.</span>',
230 $link['description']
231 );
232 }
233
234 /**
235 * Test formatUrlHtml with search result highlight.
236 */
237 public function testFormatUrlHtmlWithSearchHighlight(): void
238 {
239 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
240
241 $bookmark = new Bookmark();
242 $bookmark->setUrl('http://www.php-fig.org/psr/psr-2/');
243 $bookmark->addAdditionalContentEntry(
244 'search_highlight',
245 ['url' => [
246 ['start' => 0, 'end' => 4], // http
247 ['start' => 15, 'end' => 18], // fig
248 ['start' => 27, 'end' => 33], // "psr-2/"
249 ]]
250 );
251
252 $link = $this->formatter->format($bookmark);
253
254 $this->assertSame(
255 '<span class="search-highlight">http</span>://www.php-' .
256 '<span class="search-highlight">fig</span>.org/psr/' .
257 '<span class="search-highlight">psr-2/</span>',
258 $link['url_html']
259 );
260 }
261
262 /**
263 * Test formatTagListHtml with search result highlight.
264 */
265 public function testFormatTagListHtmlWithSearchHighlight(): void
266 {
267 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
268
269 $bookmark = new Bookmark();
270 $bookmark->setTagsString('coding-style standards quality assurance');
271 $bookmark->addAdditionalContentEntry(
272 'search_highlight',
273 ['tags' => [
274 ['start' => 0, 'end' => 12], // coding-style
275 ['start' => 23, 'end' => 30], // quality
276 ['start' => 31, 'end' => 40], // assurance
277 ],]
278 );
279
280 $link = $this->formatter->format($bookmark);
281
282 $this->assertSame(
283 [
284 '<span class="search-highlight">coding-style</span>',
285 'standards',
286 '<span class="search-highlight">quality</span>',
287 '<span class="search-highlight">assurance</span>',
288 ],
289 $link['taglist_html']
290 );
291 }
177} 292}
diff --git a/tests/formatter/BookmarkMarkdownExtraFormatterTest.php b/tests/formatter/BookmarkMarkdownExtraFormatterTest.php
new file mode 100644
index 00000000..d4941ef3
--- /dev/null
+++ b/tests/formatter/BookmarkMarkdownExtraFormatterTest.php
@@ -0,0 +1,162 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9
10/**
11 * Class BookmarkMarkdownExtraFormatterTest
12 * @package Shaarli\Formatter
13 */
14class BookmarkMarkdownExtraFormatterTest extends TestCase
15{
16 /** @var string Path of test config file */
17 protected static $testConf = 'sandbox/config';
18
19 /** @var BookmarkFormatter */
20 protected $formatter;
21
22 /** @var ConfigManager instance */
23 protected $conf;
24
25 /**
26 * Initialize formatter instance.
27 */
28 public function setUp(): void
29 {
30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
31 $this->conf = new ConfigManager(self::$testConf);
32 $this->formatter = new BookmarkMarkdownExtraFormatter($this->conf, true);
33 }
34
35 /**
36 * Test formatting a bookmark with all its attribute filled.
37 */
38 public function testFormatExtra(): void
39 {
40 $bookmark = new Bookmark();
41 $bookmark->setId($id = 11);
42 $bookmark->setShortUrl($short = 'abcdef');
43 $bookmark->setUrl('https://sub.domain.tld?query=here&for=real#hash');
44 $bookmark->setTitle($title = 'This is a <strong>bookmark</strong>');
45 $bookmark->setDescription('<h2>Content</h2><p>`Here is some content</p>');
46 $bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']);
47 $bookmark->setThumbnail('http://domain2.tdl2/?type=img&name=file.png');
48 $bookmark->setSticky(true);
49 $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190521_190412'));
50 $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190521_191213'));
51 $bookmark->setPrivate(true);
52
53 $link = $this->formatter->format($bookmark);
54 $this->assertEquals($id, $link['id']);
55 $this->assertEquals($short, $link['shorturl']);
56 $this->assertEquals('https://sub.domain.tld?query=here&amp;for=real#hash', $link['url']);
57 $this->assertEquals(
58 'https://sub.domain.tld?query=here&amp;for=real#hash',
59 $link['real_url']
60 );
61 $this->assertEquals('This is a &lt;strong&gt;bookmark&lt;/strong&gt;', $link['title']);
62 $this->assertEquals(
63 '<div class="markdown"><p>'.
64 '&lt;h2&gt;Content&lt;/h2&gt;&lt;p&gt;`Here is some content&lt;/p&gt;'.
65 '</p></div>',
66 $link['description']
67 );
68 $tags[3] = '&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;';
69 $this->assertEquals($tags, $link['taglist']);
70 $this->assertEquals(implode(' ', $tags), $link['tags']);
71 $this->assertEquals(
72 'http://domain2.tdl2/?type=img&amp;name=file.png',
73 $link['thumbnail']
74 );
75 $this->assertEquals($created, $link['created']);
76 $this->assertEquals($created->getTimestamp(), $link['timestamp']);
77 $this->assertEquals($updated, $link['updated']);
78 $this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
79 $this->assertTrue($link['private']);
80 $this->assertTrue($link['sticky']);
81 $this->assertEquals('private', $link['class']);
82 }
83
84 /**
85 * Test formatting a bookmark with all its attribute filled.
86 */
87 public function testFormatExtraMinimal(): void
88 {
89 $bookmark = new Bookmark();
90
91 $link = $this->formatter->format($bookmark);
92 $this->assertEmpty($link['id']);
93 $this->assertEmpty($link['shorturl']);
94 $this->assertEmpty($link['url']);
95 $this->assertEmpty($link['real_url']);
96 $this->assertEmpty($link['title']);
97 $this->assertEmpty($link['description']);
98 $this->assertEmpty($link['taglist']);
99 $this->assertEmpty($link['tags']);
100 $this->assertEmpty($link['thumbnail']);
101 $this->assertEmpty($link['created']);
102 $this->assertEmpty($link['timestamp']);
103 $this->assertEmpty($link['updated']);
104 $this->assertEmpty($link['updated_timestamp']);
105 $this->assertFalse($link['private']);
106 $this->assertFalse($link['sticky']);
107 $this->assertEmpty($link['class']);
108 }
109
110 /**
111 * Make sure that the description is properly formatted by the default formatter.
112 */
113 public function testFormatExtrraDescription(): void
114 {
115 $description = 'This a <strong>description</strong>'. PHP_EOL;
116 $description .= 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL;
117 $description .= 'Also, there is an #hashtag added'. PHP_EOL;
118 $description .= ' A N D KEEP SPACES ! '. PHP_EOL;
119 $description .= '# Header {.class}'. PHP_EOL;
120
121 $bookmark = new Bookmark();
122 $bookmark->setDescription($description);
123 $link = $this->formatter->format($bookmark);
124
125 $description = '<div class="markdown"><p>';
126 $description .= 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'. PHP_EOL;
127 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
128 $description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL;
129 $description .= 'Also, there is an <a href="./add-tag/hashtag">#hashtag</a> added<br />'. PHP_EOL;
130 $description .= 'A N D KEEP SPACES ! </p>' . PHP_EOL;
131 $description .= '<h1 class="class">Header</h1>';
132 $description .= '</div>';
133
134 $this->assertEquals($description, $link['description']);
135 }
136
137 /**
138 * Test formatting URL with an index_url set
139 * It should prepend relative links.
140 */
141 public function testFormatExtraNoteWithIndexUrl(): void
142 {
143 $bookmark = new Bookmark();
144 $bookmark->setUrl($short = '?abcdef');
145 $description = 'Text #hashtag more text';
146 $bookmark->setDescription($description);
147
148 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
149
150 $description = '<div class="markdown"><p>';
151 $description .= 'Text <a href="'. $root .'./add-tag/hashtag">#hashtag</a> more text';
152 $description .= '</p></div>';
153
154 $link = $this->formatter->format($bookmark);
155 $this->assertEquals($root . $short, $link['url']);
156 $this->assertEquals($root . $short, $link['real_url']);
157 $this->assertEquals(
158 $description,
159 $link['description']
160 );
161 }
162}
diff --git a/tests/formatter/BookmarkMarkdownFormatterTest.php b/tests/formatter/BookmarkMarkdownFormatterTest.php
index f1f12c04..ab6b4080 100644
--- a/tests/formatter/BookmarkMarkdownFormatterTest.php
+++ b/tests/formatter/BookmarkMarkdownFormatterTest.php
@@ -3,9 +3,9 @@
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use DateTime; 5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\TestCase;
9 9
10/** 10/**
11 * Class BookmarkMarkdownFormatterTest 11 * Class BookmarkMarkdownFormatterTest
@@ -25,7 +25,7 @@ class BookmarkMarkdownFormatterTest extends TestCase
25 /** 25 /**
26 * Initialize formatter instance. 26 * Initialize formatter instance.
27 */ 27 */
28 public function setUp() 28 protected function setUp(): void
29 { 29 {
30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php'); 30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
31 $this->conf = new ConfigManager(self::$testConf); 31 $this->conf = new ConfigManager(self::$testConf);
@@ -125,7 +125,7 @@ class BookmarkMarkdownFormatterTest extends TestCase
125 $description .= 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'. PHP_EOL; 125 $description .= 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'. PHP_EOL;
126 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash'; 126 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
127 $description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL; 127 $description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL;
128 $description .= 'Also, there is an <a href="?addtag=hashtag">#hashtag</a> added<br />'. PHP_EOL; 128 $description .= 'Also, there is an <a href="./add-tag/hashtag">#hashtag</a> added<br />'. PHP_EOL;
129 $description .= 'A N D KEEP SPACES ! '; 129 $description .= 'A N D KEEP SPACES ! ';
130 $description .= '</p></div>'; 130 $description .= '</p></div>';
131 131
@@ -146,7 +146,7 @@ class BookmarkMarkdownFormatterTest extends TestCase
146 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/'); 146 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
147 147
148 $description = '<div class="markdown"><p>'; 148 $description = '<div class="markdown"><p>';
149 $description .= 'Text <a href="'. $root .'?addtag=hashtag">#hashtag</a> more text'; 149 $description .= 'Text <a href="'. $root .'./add-tag/hashtag">#hashtag</a> more text';
150 $description .= '</p></div>'; 150 $description .= '</p></div>';
151 151
152 $link = $this->formatter->format($bookmark); 152 $link = $this->formatter->format($bookmark);
diff --git a/tests/formatter/BookmarkRawFormatterTest.php b/tests/formatter/BookmarkRawFormatterTest.php
index 4491b035..c76bb7b9 100644
--- a/tests/formatter/BookmarkRawFormatterTest.php
+++ b/tests/formatter/BookmarkRawFormatterTest.php
@@ -3,9 +3,9 @@
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use DateTime; 5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\TestCase;
9 9
10/** 10/**
11 * Class BookmarkRawFormatterTest 11 * Class BookmarkRawFormatterTest
@@ -25,7 +25,7 @@ class BookmarkRawFormatterTest extends TestCase
25 /** 25 /**
26 * Initialize formatter instance. 26 * Initialize formatter instance.
27 */ 27 */
28 public function setUp() 28 protected function setUp(): void
29 { 29 {
30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php'); 30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
31 $this->conf = new ConfigManager(self::$testConf); 31 $this->conf = new ConfigManager(self::$testConf);
diff --git a/tests/formatter/FormatterFactoryTest.php b/tests/formatter/FormatterFactoryTest.php
index 5adf3ffd..ae476cb5 100644
--- a/tests/formatter/FormatterFactoryTest.php
+++ b/tests/formatter/FormatterFactoryTest.php
@@ -2,8 +2,8 @@
2 2
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use PHPUnit\Framework\TestCase;
6use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
6use Shaarli\TestCase;
7 7
8/** 8/**
9 * Class FormatterFactoryTest 9 * Class FormatterFactoryTest
@@ -24,7 +24,7 @@ class FormatterFactoryTest extends TestCase
24 /** 24 /**
25 * Initialize FormatterFactory instance 25 * Initialize FormatterFactory instance
26 */ 26 */
27 public function setUp() 27 protected function setUp(): void
28 { 28 {
29 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php'); 29 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
30 $this->conf = new ConfigManager(self::$testConf); 30 $this->conf = new ConfigManager(self::$testConf);
diff --git a/tests/front/ShaarliAdminMiddlewareTest.php b/tests/front/ShaarliAdminMiddlewareTest.php
new file mode 100644
index 00000000..44025f11
--- /dev/null
+++ b/tests/front/ShaarliAdminMiddlewareTest.php
@@ -0,0 +1,100 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Container\ShaarliContainer;
9use Shaarli\Security\LoginManager;
10use Shaarli\TestCase;
11use Shaarli\Updater\Updater;
12use Slim\Http\Request;
13use Slim\Http\Response;
14use Slim\Http\Uri;
15
16class ShaarliAdminMiddlewareTest extends TestCase
17{
18 protected const TMP_MOCK_FILE = '.tmp';
19
20 /** @var ShaarliContainer */
21 protected $container;
22
23 /** @var ShaarliMiddleware */
24 protected $middleware;
25
26 public function setUp(): void
27 {
28 $this->container = $this->createMock(ShaarliContainer::class);
29
30 touch(static::TMP_MOCK_FILE);
31
32 $this->container->conf = $this->createMock(ConfigManager::class);
33 $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
34
35 $this->container->loginManager = $this->createMock(LoginManager::class);
36 $this->container->updater = $this->createMock(Updater::class);
37
38 $this->container->environment = ['REQUEST_URI' => 'http://shaarli/subfolder/path'];
39
40 $this->middleware = new ShaarliAdminMiddleware($this->container);
41 }
42
43 public function tearDown(): void
44 {
45 unlink(static::TMP_MOCK_FILE);
46 }
47
48 /**
49 * Try to access an admin controller while logged out -> redirected to login page.
50 */
51 public function testMiddlewareWhileLoggedOut(): void
52 {
53 $this->container->loginManager->expects(static::once())->method('isLoggedIn')->willReturn(false);
54
55 $request = $this->createMock(Request::class);
56 $request->method('getUri')->willReturnCallback(function (): Uri {
57 $uri = $this->createMock(Uri::class);
58 $uri->method('getBasePath')->willReturn('/subfolder');
59
60 return $uri;
61 });
62
63 $response = new Response();
64
65 /** @var Response $result */
66 $result = $this->middleware->__invoke($request, $response, function () {});
67
68 static::assertSame(302, $result->getStatusCode());
69 static::assertSame(
70 '/subfolder/login?returnurl=' . urlencode('http://shaarli/subfolder/path'),
71 $result->getHeader('location')[0]
72 );
73 }
74
75 /**
76 * Process controller while logged in.
77 */
78 public function testMiddlewareWhileLoggedIn(): void
79 {
80 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
81
82 $request = $this->createMock(Request::class);
83 $request->method('getUri')->willReturnCallback(function (): Uri {
84 $uri = $this->createMock(Uri::class);
85 $uri->method('getBasePath')->willReturn('/subfolder');
86
87 return $uri;
88 });
89
90 $response = new Response();
91 $controller = function (Request $request, Response $response): Response {
92 return $response->withStatus(418); // I'm a tea pot
93 };
94
95 /** @var Response $result */
96 $result = $this->middleware->__invoke($request, $response, $controller);
97
98 static::assertSame(418, $result->getStatusCode());
99 }
100}
diff --git a/tests/front/ShaarliMiddlewareTest.php b/tests/front/ShaarliMiddlewareTest.php
index 80974f37..655c5bba 100644
--- a/tests/front/ShaarliMiddlewareTest.php
+++ b/tests/front/ShaarliMiddlewareTest.php
@@ -4,16 +4,23 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Front; 5namespace Shaarli\Front;
6 6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
9use Shaarli\Container\ShaarliContainer; 8use Shaarli\Container\ShaarliContainer;
10use Shaarli\Front\Exception\LoginBannedException; 9use Shaarli\Front\Exception\LoginBannedException;
10use Shaarli\Front\Exception\UnauthorizedException;
11use Shaarli\Render\PageBuilder; 11use Shaarli\Render\PageBuilder;
12use Shaarli\Render\PageCacheManager;
13use Shaarli\Security\LoginManager;
14use Shaarli\TestCase;
15use Shaarli\Updater\Updater;
12use Slim\Http\Request; 16use Slim\Http\Request;
13use Slim\Http\Response; 17use Slim\Http\Response;
18use Slim\Http\Uri;
14 19
15class ShaarliMiddlewareTest extends TestCase 20class ShaarliMiddlewareTest extends TestCase
16{ 21{
22 protected const TMP_MOCK_FILE = '.tmp';
23
17 /** @var ShaarliContainer */ 24 /** @var ShaarliContainer */
18 protected $container; 25 protected $container;
19 26
@@ -23,12 +30,37 @@ class ShaarliMiddlewareTest extends TestCase
23 public function setUp(): void 30 public function setUp(): void
24 { 31 {
25 $this->container = $this->createMock(ShaarliContainer::class); 32 $this->container = $this->createMock(ShaarliContainer::class);
33
34 touch(static::TMP_MOCK_FILE);
35
36 $this->container->conf = $this->createMock(ConfigManager::class);
37 $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
38
39 $this->container->loginManager = $this->createMock(LoginManager::class);
40
41 $this->container->environment = ['REQUEST_URI' => 'http://shaarli/subfolder/path'];
42
26 $this->middleware = new ShaarliMiddleware($this->container); 43 $this->middleware = new ShaarliMiddleware($this->container);
27 } 44 }
28 45
46 public function tearDown(): void
47 {
48 unlink(static::TMP_MOCK_FILE);
49 }
50
51 /**
52 * Test middleware execution with valid controller call
53 */
29 public function testMiddlewareExecution(): void 54 public function testMiddlewareExecution(): void
30 { 55 {
31 $request = $this->createMock(Request::class); 56 $request = $this->createMock(Request::class);
57 $request->method('getUri')->willReturnCallback(function (): Uri {
58 $uri = $this->createMock(Uri::class);
59 $uri->method('getBasePath')->willReturn('/subfolder');
60
61 return $uri;
62 });
63
32 $response = new Response(); 64 $response = new Response();
33 $controller = function (Request $request, Response $response): Response { 65 $controller = function (Request $request, Response $response): Response {
34 return $response->withStatus(418); // I'm a tea pot 66 return $response->withStatus(418); // I'm a tea pot
@@ -41,9 +73,20 @@ class ShaarliMiddlewareTest extends TestCase
41 static::assertSame(418, $result->getStatusCode()); 73 static::assertSame(418, $result->getStatusCode());
42 } 74 }
43 75
44 public function testMiddlewareExecutionWithException(): void 76 /**
77 * Test middleware execution with controller throwing a known front exception.
78 * The exception should be thrown to be later handled by the error handler.
79 */
80 public function testMiddlewareExecutionWithFrontException(): void
45 { 81 {
46 $request = $this->createMock(Request::class); 82 $request = $this->createMock(Request::class);
83 $request->method('getUri')->willReturnCallback(function (): Uri {
84 $uri = $this->createMock(Uri::class);
85 $uri->method('getBasePath')->willReturn('/subfolder');
86
87 return $uri;
88 });
89
47 $response = new Response(); 90 $response = new Response();
48 $controller = function (): void { 91 $controller = function (): void {
49 $exception = new LoginBannedException(); 92 $exception = new LoginBannedException();
@@ -57,14 +100,122 @@ class ShaarliMiddlewareTest extends TestCase
57 }); 100 });
58 $this->container->pageBuilder = $pageBuilder; 101 $this->container->pageBuilder = $pageBuilder;
59 102
60 $conf = $this->createMock(ConfigManager::class); 103 $this->expectException(LoginBannedException::class);
61 $this->container->conf = $conf; 104
105 $this->middleware->__invoke($request, $response, $controller);
106 }
107
108 /**
109 * Test middleware execution with controller throwing a not authorized exception
110 * The middle should send a redirection response to the login page.
111 */
112 public function testMiddlewareExecutionWithUnauthorizedException(): void
113 {
114 $request = $this->createMock(Request::class);
115 $request->method('getUri')->willReturnCallback(function (): Uri {
116 $uri = $this->createMock(Uri::class);
117 $uri->method('getBasePath')->willReturn('/subfolder');
118
119 return $uri;
120 });
121
122 $response = new Response();
123 $controller = function (): void {
124 throw new UnauthorizedException();
125 };
126
127 /** @var Response $result */
128 $result = $this->middleware->__invoke($request, $response, $controller);
129
130 static::assertSame(302, $result->getStatusCode());
131 static::assertSame(
132 '/subfolder/login?returnurl=' . urlencode('http://shaarli/subfolder/path'),
133 $result->getHeader('location')[0]
134 );
135 }
136
137 /**
138 * Test middleware execution with controller throwing a not authorized exception.
139 * The exception should be thrown to be later handled by the error handler.
140 */
141 public function testMiddlewareExecutionWithServerException(): void
142 {
143 $request = $this->createMock(Request::class);
144 $request->method('getUri')->willReturnCallback(function (): Uri {
145 $uri = $this->createMock(Uri::class);
146 $uri->method('getBasePath')->willReturn('/subfolder');
147
148 return $uri;
149 });
150
151 $dummyException = new class() extends \Exception {};
152
153 $response = new Response();
154 $controller = function () use ($dummyException): void {
155 throw $dummyException;
156 };
157
158 $parameters = [];
159 $this->container->pageBuilder = $this->createMock(PageBuilder::class);
160 $this->container->pageBuilder->method('render')->willReturnCallback(function (string $message): string {
161 return $message;
162 });
163 $this->container->pageBuilder
164 ->method('assign')
165 ->willReturnCallback(function (string $key, string $value) use (&$parameters): void {
166 $parameters[$key] = $value;
167 })
168 ;
169
170 $this->expectException(get_class($dummyException));
171
172 $this->middleware->__invoke($request, $response, $controller);
173 }
174
175 public function testMiddlewareExecutionWithUpdates(): void
176 {
177 $request = $this->createMock(Request::class);
178 $request->method('getUri')->willReturnCallback(function (): Uri {
179 $uri = $this->createMock(Uri::class);
180 $uri->method('getBasePath')->willReturn('/subfolder');
181
182 return $uri;
183 });
184
185 $response = new Response();
186 $controller = function (Request $request, Response $response): Response {
187 return $response->withStatus(418); // I'm a tea pot
188 };
189
190 $this->container->loginManager = $this->createMock(LoginManager::class);
191 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
192
193 $this->container->conf = $this->createMock(ConfigManager::class);
194 $this->container->conf->method('get')->willReturnCallback(function (string $key): string {
195 return $key;
196 });
197 $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
198
199 $this->container->pageCacheManager = $this->createMock(PageCacheManager::class);
200 $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches');
201
202 $this->container->updater = $this->createMock(Updater::class);
203 $this->container->updater
204 ->expects(static::once())
205 ->method('update')
206 ->willReturn(['update123'])
207 ;
208 $this->container->updater->method('getDoneUpdates')->willReturn($updates = ['update123', 'other']);
209 $this->container->updater
210 ->expects(static::once())
211 ->method('writeUpdates')
212 ->with('resource.updates', $updates)
213 ;
62 214
63 /** @var Response $result */ 215 /** @var Response $result */
64 $result = $this->middleware->__invoke($request, $response, $controller); 216 $result = $this->middleware->__invoke($request, $response, $controller);
65 217
66 static::assertInstanceOf(Response::class, $result); 218 static::assertInstanceOf(Response::class, $result);
67 static::assertSame(401, $result->getStatusCode()); 219 static::assertSame(418, $result->getStatusCode());
68 static::assertContains('error', (string) $result->getBody());
69 } 220 }
70} 221}
diff --git a/tests/front/controller/LoginControllerTest.php b/tests/front/controller/LoginControllerTest.php
deleted file mode 100644
index 8cf8ece7..00000000
--- a/tests/front/controller/LoginControllerTest.php
+++ /dev/null
@@ -1,178 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Container\ShaarliContainer;
11use Shaarli\Front\Exception\LoginBannedException;
12use Shaarli\Plugin\PluginManager;
13use Shaarli\Render\PageBuilder;
14use Shaarli\Security\LoginManager;
15use Slim\Http\Request;
16use Slim\Http\Response;
17
18class LoginControllerTest extends TestCase
19{
20 /** @var ShaarliContainer */
21 protected $container;
22
23 /** @var LoginController */
24 protected $controller;
25
26 public function setUp(): void
27 {
28 $this->container = $this->createMock(ShaarliContainer::class);
29 $this->controller = new LoginController($this->container);
30 }
31
32 public function testValidControllerInvoke(): void
33 {
34 $this->createValidContainerMockSet();
35
36 $request = $this->createMock(Request::class);
37 $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
38 $response = new Response();
39
40 $assignedVariables = [];
41 $this->container->pageBuilder
42 ->method('assign')
43 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
44 $assignedVariables[$key] = $value;
45
46 return $this;
47 })
48 ;
49
50 $result = $this->controller->index($request, $response);
51
52 static::assertInstanceOf(Response::class, $result);
53 static::assertSame(200, $result->getStatusCode());
54 static::assertSame('loginform', (string) $result->getBody());
55
56 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
57 static::assertSame(true, $assignedVariables['remember_user_default']);
58 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
59 }
60
61 public function testValidControllerInvokeWithUserName(): void
62 {
63 $this->createValidContainerMockSet();
64
65 $request = $this->createMock(Request::class);
66 $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
67 $request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>');
68 $response = new Response();
69
70 $assignedVariables = [];
71 $this->container->pageBuilder
72 ->method('assign')
73 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
74 $assignedVariables[$key] = $value;
75
76 return $this;
77 })
78 ;
79
80 $result = $this->controller->index($request, $response);
81
82 static::assertInstanceOf(Response::class, $result);
83 static::assertSame(200, $result->getStatusCode());
84 static::assertSame('loginform', (string) $result->getBody());
85
86 static::assertSame('myUser&gt;', $assignedVariables['username']);
87 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
88 static::assertSame(true, $assignedVariables['remember_user_default']);
89 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
90 }
91
92 public function testLoginControllerWhileLoggedIn(): void
93 {
94 $request = $this->createMock(Request::class);
95 $response = new Response();
96
97 $loginManager = $this->createMock(LoginManager::class);
98 $loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true);
99 $this->container->loginManager = $loginManager;
100
101 $result = $this->controller->index($request, $response);
102
103 static::assertInstanceOf(Response::class, $result);
104 static::assertSame(302, $result->getStatusCode());
105 static::assertSame(['./'], $result->getHeader('Location'));
106 }
107
108 public function testLoginControllerOpenShaarli(): void
109 {
110 $this->createValidContainerMockSet();
111
112 $request = $this->createMock(Request::class);
113 $response = new Response();
114
115 $conf = $this->createMock(ConfigManager::class);
116 $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
117 if ($parameter === 'security.open_shaarli') {
118 return true;
119 }
120 return $default;
121 });
122 $this->container->conf = $conf;
123
124 $result = $this->controller->index($request, $response);
125
126 static::assertInstanceOf(Response::class, $result);
127 static::assertSame(302, $result->getStatusCode());
128 static::assertSame(['./'], $result->getHeader('Location'));
129 }
130
131 public function testLoginControllerWhileBanned(): void
132 {
133 $this->createValidContainerMockSet();
134
135 $request = $this->createMock(Request::class);
136 $response = new Response();
137
138 $loginManager = $this->createMock(LoginManager::class);
139 $loginManager->method('isLoggedIn')->willReturn(false);
140 $loginManager->method('canLogin')->willReturn(false);
141 $this->container->loginManager = $loginManager;
142
143 $this->expectException(LoginBannedException::class);
144
145 $this->controller->index($request, $response);
146 }
147
148 protected function createValidContainerMockSet(): void
149 {
150 // User logged out
151 $loginManager = $this->createMock(LoginManager::class);
152 $loginManager->method('isLoggedIn')->willReturn(false);
153 $loginManager->method('canLogin')->willReturn(true);
154 $this->container->loginManager = $loginManager;
155
156 // Config
157 $conf = $this->createMock(ConfigManager::class);
158 $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
159 return $default;
160 });
161 $this->container->conf = $conf;
162
163 // PageBuilder
164 $pageBuilder = $this->createMock(PageBuilder::class);
165 $pageBuilder
166 ->method('render')
167 ->willReturnCallback(function (string $template): string {
168 return $template;
169 })
170 ;
171 $this->container->pageBuilder = $pageBuilder;
172
173 $pluginManager = $this->createMock(PluginManager::class);
174 $this->container->pluginManager = $pluginManager;
175 $bookmarkService = $this->createMock(BookmarkServiceInterface::class);
176 $this->container->bookmarkService = $bookmarkService;
177 }
178}
diff --git a/tests/front/controller/ShaarliControllerTest.php b/tests/front/controller/ShaarliControllerTest.php
deleted file mode 100644
index 6fa3feb9..00000000
--- a/tests/front/controller/ShaarliControllerTest.php
+++ /dev/null
@@ -1,116 +0,0 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkFilter;
9use Shaarli\Bookmark\BookmarkServiceInterface;
10use Shaarli\Container\ShaarliContainer;
11use Shaarli\Plugin\PluginManager;
12use Shaarli\Render\PageBuilder;
13use Shaarli\Security\LoginManager;
14
15/**
16 * Class ShaarliControllerTest
17 *
18 * This class is used to test default behavior of ShaarliController abstract class.
19 * It uses a dummy non abstract controller.
20 */
21class ShaarliControllerTest extends TestCase
22{
23 /** @var ShaarliContainer */
24 protected $container;
25
26 /** @var LoginController */
27 protected $controller;
28
29 /** @var mixed[] List of variable assigned to the template */
30 protected $assignedValues;
31
32 public function setUp(): void
33 {
34 $this->container = $this->createMock(ShaarliContainer::class);
35 $this->controller = new class($this->container) extends ShaarliController
36 {
37 public function assignView(string $key, $value): ShaarliController
38 {
39 return parent::assignView($key, $value);
40 }
41
42 public function render(string $template): string
43 {
44 return parent::render($template);
45 }
46 };
47 $this->assignedValues = [];
48 }
49
50 public function testAssignView(): void
51 {
52 $this->createValidContainerMockSet();
53
54 $self = $this->controller->assignView('variableName', 'variableValue');
55
56 static::assertInstanceOf(ShaarliController::class, $self);
57 static::assertSame('variableValue', $this->assignedValues['variableName']);
58 }
59
60 public function testRender(): void
61 {
62 $this->createValidContainerMockSet();
63
64 $render = $this->controller->render('templateName');
65
66 static::assertSame('templateName', $render);
67
68 static::assertSame(10, $this->assignedValues['linkcount']);
69 static::assertSame(5, $this->assignedValues['privateLinkcount']);
70 static::assertSame(['error'], $this->assignedValues['plugin_errors']);
71
72 static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']);
73 static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']);
74 static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']);
75 static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']);
76 static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']);
77 static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']);
78 }
79
80 protected function createValidContainerMockSet(): void
81 {
82 $pageBuilder = $this->createMock(PageBuilder::class);
83 $pageBuilder
84 ->method('assign')
85 ->willReturnCallback(function (string $key, $value): void {
86 $this->assignedValues[$key] = $value;
87 });
88 $pageBuilder
89 ->method('render')
90 ->willReturnCallback(function (string $template): string {
91 return $template;
92 });
93 $this->container->pageBuilder = $pageBuilder;
94
95 $bookmarkService = $this->createMock(BookmarkServiceInterface::class);
96 $bookmarkService
97 ->method('count')
98 ->willReturnCallback(function (string $visibility): int {
99 return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10;
100 });
101 $this->container->bookmarkService = $bookmarkService;
102
103 $pluginManager = $this->createMock(PluginManager::class);
104 $pluginManager
105 ->method('executeHooks')
106 ->willReturnCallback(function (string $hook, array &$data, array $params): array {
107 return $data[$hook] = $params;
108 });
109 $pluginManager->method('getErrors')->willReturn(['error']);
110 $this->container->pluginManager = $pluginManager;
111
112 $loginManager = $this->createMock(LoginManager::class);
113 $loginManager->method('isLoggedIn')->willReturn(true);
114 $this->container->loginManager = $loginManager;
115 }
116}
diff --git a/tests/front/controller/admin/ConfigureControllerTest.php b/tests/front/controller/admin/ConfigureControllerTest.php
new file mode 100644
index 00000000..d82db0a7
--- /dev/null
+++ b/tests/front/controller/admin/ConfigureControllerTest.php
@@ -0,0 +1,252 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Front\Exception\WrongTokenException;
9use Shaarli\Security\SessionManager;
10use Shaarli\TestCase;
11use Shaarli\Thumbnailer;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class ConfigureControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var ConfigureController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->controller = new ConfigureController($this->container);
27 }
28
29 /**
30 * Test displaying configure page - it should display all config variables
31 */
32 public function testIndex(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $response = new Response();
39
40 $this->container->conf = $this->createMock(ConfigManager::class);
41 $this->container->conf->method('get')->willReturnCallback(function (string $key) {
42 return $key;
43 });
44
45 $result = $this->controller->index($request, $response);
46
47 static::assertSame(200, $result->getStatusCode());
48 static::assertSame('configure', (string) $result->getBody());
49
50 static::assertSame('Configure - general.title', $assignedVariables['pagetitle']);
51 static::assertSame('general.title', $assignedVariables['title']);
52 static::assertSame('resource.theme', $assignedVariables['theme']);
53 static::assertEmpty($assignedVariables['theme_available']);
54 static::assertSame(['default', 'markdown', 'markdownExtra'], $assignedVariables['formatter_available']);
55 static::assertNotEmpty($assignedVariables['continents']);
56 static::assertNotEmpty($assignedVariables['cities']);
57 static::assertSame('general.retrieve_description', $assignedVariables['retrieve_description']);
58 static::assertSame('privacy.default_private_links', $assignedVariables['private_links_default']);
59 static::assertSame('security.session_protection_disabled', $assignedVariables['session_protection_disabled']);
60 static::assertSame('feed.rss_permalinks', $assignedVariables['enable_rss_permalinks']);
61 static::assertSame('updates.check_updates', $assignedVariables['enable_update_check']);
62 static::assertSame('privacy.hide_public_links', $assignedVariables['hide_public_links']);
63 static::assertSame('api.enabled', $assignedVariables['api_enabled']);
64 static::assertSame('api.secret', $assignedVariables['api_secret']);
65 static::assertCount(5, $assignedVariables['languages']);
66 static::assertArrayHasKey('gd_enabled', $assignedVariables);
67 static::assertSame('thumbnails.mode', $assignedVariables['thumbnails_mode']);
68 }
69
70 /**
71 * Test posting a new config - make sure that everything is saved properly, without errors.
72 */
73 public function testSaveNewConfig(): void
74 {
75 $session = [];
76 $this->assignSessionVars($session);
77
78 $parameters = [
79 'token' => 'token',
80 'continent' => 'Europe',
81 'city' => 'Moscow',
82 'title' => 'Shaarli',
83 'titleLink' => './',
84 'retrieveDescription' => 'on',
85 'theme' => 'vintage',
86 'disablesessionprotection' => null,
87 'privateLinkByDefault' => true,
88 'enableRssPermalinks' => true,
89 'updateCheck' => false,
90 'hidePublicLinks' => 'on',
91 'enableApi' => 'on',
92 'apiSecret' => 'abcdef',
93 'formatter' => 'markdown',
94 'language' => 'fr',
95 'enableThumbnails' => Thumbnailer::MODE_NONE,
96 ];
97
98 $parametersConfigMapping = [
99 'general.timezone' => $parameters['continent'] . '/' . $parameters['city'],
100 'general.title' => $parameters['title'],
101 'general.header_link' => $parameters['titleLink'],
102 'general.retrieve_description' => !!$parameters['retrieveDescription'],
103 'resource.theme' => $parameters['theme'],
104 'security.session_protection_disabled' => !!$parameters['disablesessionprotection'],
105 'privacy.default_private_links' => !!$parameters['privateLinkByDefault'],
106 'feed.rss_permalinks' => !!$parameters['enableRssPermalinks'],
107 'updates.check_updates' => !!$parameters['updateCheck'],
108 'privacy.hide_public_links' => !!$parameters['hidePublicLinks'],
109 'api.enabled' => !!$parameters['enableApi'],
110 'api.secret' => $parameters['apiSecret'],
111 'formatter' => $parameters['formatter'],
112 'translation.language' => $parameters['language'],
113 'thumbnails.mode' => $parameters['enableThumbnails'],
114 ];
115
116 $request = $this->createMock(Request::class);
117 $request
118 ->expects(static::atLeastOnce())
119 ->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
120 if (false === array_key_exists($key, $parameters)) {
121 static::fail('unknown key: ' . $key);
122 }
123
124 return $parameters[$key];
125 }
126 );
127
128 $response = new Response();
129
130 $this->container->conf = $this->createMock(ConfigManager::class);
131 $this->container->conf
132 ->expects(static::atLeastOnce())
133 ->method('set')
134 ->willReturnCallback(function (string $key, $value) use ($parametersConfigMapping): void {
135 if (false === array_key_exists($key, $parametersConfigMapping)) {
136 static::fail('unknown key: ' . $key);
137 }
138
139 static::assertSame($parametersConfigMapping[$key], $value);
140 }
141 );
142
143 $result = $this->controller->save($request, $response);
144 static::assertSame(302, $result->getStatusCode());
145 static::assertSame(['/subfolder/admin/configure'], $result->getHeader('Location'));
146
147 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
148 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
149 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
150 static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
151 }
152
153 /**
154 * Test posting a new config - wrong token.
155 */
156 public function testSaveNewConfigWrongToken(): void
157 {
158 $this->container->sessionManager = $this->createMock(SessionManager::class);
159 $this->container->sessionManager->method('checkToken')->willReturn(false);
160
161 $this->container->conf->expects(static::never())->method('set');
162 $this->container->conf->expects(static::never())->method('write');
163
164 $request = $this->createMock(Request::class);
165 $response = new Response();
166
167 $this->expectException(WrongTokenException::class);
168
169 $this->controller->save($request, $response);
170 }
171
172 /**
173 * Test posting a new config - thumbnail activation.
174 */
175 public function testSaveNewConfigThumbnailsActivation(): void
176 {
177 $session = [];
178 $this->assignSessionVars($session);
179
180 $request = $this->createMock(Request::class);
181 $request
182 ->expects(static::atLeastOnce())
183 ->method('getParam')->willReturnCallback(function (string $key) {
184 if ('enableThumbnails' === $key) {
185 return Thumbnailer::MODE_ALL;
186 }
187
188 return $key;
189 })
190 ;
191 $response = new Response();
192
193 $result = $this->controller->save($request, $response);
194
195 static::assertSame(302, $result->getStatusCode());
196 static::assertSame(['/subfolder/admin/configure'], $result->getHeader('Location'));
197
198 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
199 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
200 static::assertStringContainsString(
201 'You have enabled or changed thumbnails mode',
202 $session[SessionManager::KEY_WARNING_MESSAGES][0]
203 );
204 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
205 static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
206 }
207
208 /**
209 * Test posting a new config - thumbnail activation.
210 */
211 public function testSaveNewConfigThumbnailsAlreadyActive(): void
212 {
213 $session = [];
214 $this->assignSessionVars($session);
215
216 $request = $this->createMock(Request::class);
217 $request
218 ->expects(static::atLeastOnce())
219 ->method('getParam')->willReturnCallback(function (string $key) {
220 if ('enableThumbnails' === $key) {
221 return Thumbnailer::MODE_ALL;
222 }
223
224 return $key;
225 })
226 ;
227 $response = new Response();
228
229 $this->container->conf = $this->createMock(ConfigManager::class);
230 $this->container->conf
231 ->expects(static::atLeastOnce())
232 ->method('get')
233 ->willReturnCallback(function (string $key): string {
234 if ('thumbnails.mode' === $key) {
235 return Thumbnailer::MODE_ALL;
236 }
237
238 return $key;
239 })
240 ;
241
242 $result = $this->controller->save($request, $response);
243
244 static::assertSame(302, $result->getStatusCode());
245 static::assertSame(['/subfolder/admin/configure'], $result->getHeader('Location'));
246
247 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
248 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
249 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
250 static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
251 }
252}
diff --git a/tests/front/controller/admin/ExportControllerTest.php b/tests/front/controller/admin/ExportControllerTest.php
new file mode 100644
index 00000000..0e6f2762
--- /dev/null
+++ b/tests/front/controller/admin/ExportControllerTest.php
@@ -0,0 +1,163 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Formatter\BookmarkFormatter;
9use Shaarli\Formatter\BookmarkRawFormatter;
10use Shaarli\Netscape\NetscapeBookmarkUtils;
11use Shaarli\Security\SessionManager;
12use Shaarli\TestCase;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class ExportControllerTest extends TestCase
17{
18 use FrontAdminControllerMockHelper;
19
20 /** @var ExportController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->controller = new ExportController($this->container);
28 }
29
30 /**
31 * Test displaying export page
32 */
33 public function testIndex(): void
34 {
35 $assignedVariables = [];
36 $this->assignTemplateVars($assignedVariables);
37
38 $request = $this->createMock(Request::class);
39 $response = new Response();
40
41 $result = $this->controller->index($request, $response);
42
43 static::assertSame(200, $result->getStatusCode());
44 static::assertSame('export', (string) $result->getBody());
45
46 static::assertSame('Export - Shaarli', $assignedVariables['pagetitle']);
47 }
48
49 /**
50 * Test posting an export request
51 */
52 public function testExportDefault(): void
53 {
54 $assignedVariables = [];
55 $this->assignTemplateVars($assignedVariables);
56
57 $parameters = [
58 'selection' => 'all',
59 'prepend_note_url' => 'on',
60 ];
61
62 $request = $this->createMock(Request::class);
63 $request->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
64 return $parameters[$key] ?? null;
65 });
66 $response = new Response();
67
68 $bookmarks = [
69 (new Bookmark())->setUrl('http://link1.tld')->setTitle('Title 1'),
70 (new Bookmark())->setUrl('http://link2.tld')->setTitle('Title 2'),
71 ];
72
73 $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
74 $this->container->netscapeBookmarkUtils
75 ->expects(static::once())
76 ->method('filterAndFormat')
77 ->willReturnCallback(
78 function (
79 BookmarkFormatter $formatter,
80 string $selection,
81 bool $prependNoteUrl,
82 string $indexUrl
83 ) use ($parameters, $bookmarks): array {
84 static::assertInstanceOf(BookmarkRawFormatter::class, $formatter);
85 static::assertSame($parameters['selection'], $selection);
86 static::assertTrue($prependNoteUrl);
87 static::assertSame('http://shaarli/subfolder/', $indexUrl);
88
89 return $bookmarks;
90 }
91 )
92 ;
93
94 $result = $this->controller->export($request, $response);
95
96 static::assertSame(200, $result->getStatusCode());
97 static::assertSame('export.bookmarks', (string) $result->getBody());
98 static::assertSame(['text/html; charset=utf-8'], $result->getHeader('content-type'));
99 static::assertRegExp(
100 '/attachment; filename=bookmarks_all_[\d]{8}_[\d]{6}\.html/',
101 $result->getHeader('content-disposition')[0]
102 );
103
104 static::assertNotEmpty($assignedVariables['date']);
105 static::assertSame(PHP_EOL, $assignedVariables['eol']);
106 static::assertSame('all', $assignedVariables['selection']);
107 static::assertSame($bookmarks, $assignedVariables['links']);
108 }
109
110 /**
111 * Test posting an export request - without selection parameter
112 */
113 public function testExportSelectionMissing(): void
114 {
115 $request = $this->createMock(Request::class);
116 $response = new Response();
117
118 $this->container->sessionManager
119 ->expects(static::once())
120 ->method('setSessionParameter')
121 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Please select an export mode.'])
122 ;
123
124 $result = $this->controller->export($request, $response);
125
126 static::assertSame(302, $result->getStatusCode());
127 static::assertSame(['/subfolder/admin/export'], $result->getHeader('location'));
128 }
129
130 /**
131 * Test posting an export request - without selection parameter
132 */
133 public function testExportErrorEncountered(): void
134 {
135 $parameters = [
136 'selection' => 'all',
137 ];
138
139 $request = $this->createMock(Request::class);
140 $request->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
141 return $parameters[$key] ?? null;
142 });
143 $response = new Response();
144
145 $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
146 $this->container->netscapeBookmarkUtils
147 ->expects(static::once())
148 ->method('filterAndFormat')
149 ->willThrowException(new \Exception($message = 'error message'));
150 ;
151
152 $this->container->sessionManager
153 ->expects(static::once())
154 ->method('setSessionParameter')
155 ->with(SessionManager::KEY_ERROR_MESSAGES, [$message])
156 ;
157
158 $result = $this->controller->export($request, $response);
159
160 static::assertSame(302, $result->getStatusCode());
161 static::assertSame(['/subfolder/admin/export'], $result->getHeader('location'));
162 }
163}
diff --git a/tests/front/controller/admin/FrontAdminControllerMockHelper.php b/tests/front/controller/admin/FrontAdminControllerMockHelper.php
new file mode 100644
index 00000000..2b9f2ef1
--- /dev/null
+++ b/tests/front/controller/admin/FrontAdminControllerMockHelper.php
@@ -0,0 +1,56 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Container\ShaarliTestContainer;
8use Shaarli\Front\Controller\Visitor\FrontControllerMockHelper;
9use Shaarli\History;
10
11/**
12 * Trait FrontControllerMockHelper
13 *
14 * Helper trait used to initialize the ShaarliContainer and mock its services for admin controller tests.
15 *
16 * @property ShaarliTestContainer $container
17 */
18trait FrontAdminControllerMockHelper
19{
20 use FrontControllerMockHelper {
21 FrontControllerMockHelper::createContainer as parentCreateContainer;
22 }
23
24 /**
25 * Mock the container instance
26 */
27 protected function createContainer(): void
28 {
29 $this->parentCreateContainer();
30
31 $this->container->history = $this->createMock(History::class);
32
33 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
34 $this->container->sessionManager->method('checkToken')->willReturn(true);
35 }
36
37
38 /**
39 * Pass a reference of an array which will be populated by `sessionManager->setSessionParameter`
40 * calls during execution.
41 *
42 * @param mixed $variables Array reference to populate.
43 */
44 protected function assignSessionVars(array &$variables): void
45 {
46 $this->container->sessionManager
47 ->expects(static::atLeastOnce())
48 ->method('setSessionParameter')
49 ->willReturnCallback(function ($key, $value) use (&$variables) {
50 $variables[$key] = $value;
51
52 return $this->container->sessionManager;
53 })
54 ;
55 }
56}
diff --git a/tests/front/controller/admin/ImportControllerTest.php b/tests/front/controller/admin/ImportControllerTest.php
new file mode 100644
index 00000000..c266caa5
--- /dev/null
+++ b/tests/front/controller/admin/ImportControllerTest.php
@@ -0,0 +1,148 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Psr\Http\Message\UploadedFileInterface;
8use Shaarli\Netscape\NetscapeBookmarkUtils;
9use Shaarli\Security\SessionManager;
10use Shaarli\TestCase;
11use Slim\Http\Request;
12use Slim\Http\Response;
13use Slim\Http\UploadedFile;
14
15class ImportControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var ImportController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->controller = new ImportController($this->container);
27 }
28
29 /**
30 * Test displaying import page
31 */
32 public function testIndex(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $response = new Response();
39
40 $result = $this->controller->index($request, $response);
41
42 static::assertSame(200, $result->getStatusCode());
43 static::assertSame('import', (string) $result->getBody());
44
45 static::assertSame('Import - Shaarli', $assignedVariables['pagetitle']);
46 static::assertIsInt($assignedVariables['maxfilesize']);
47 static::assertRegExp('/\d+[KM]iB/', $assignedVariables['maxfilesizeHuman']);
48 }
49
50 /**
51 * Test importing a file with default and valid parameters
52 */
53 public function testImportDefault(): void
54 {
55 $parameters = [
56 'abc' => 'def',
57 'other' => 'param',
58 ];
59
60 $requestFile = new UploadedFile('file', 'name', 'type', 123);
61
62 $request = $this->createMock(Request::class);
63 $request->method('getParams')->willReturnCallback(function () use ($parameters) {
64 return $parameters;
65 });
66 $request->method('getUploadedFiles')->willReturn(['filetoupload' => $requestFile]);
67 $response = new Response();
68
69 $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
70 $this->container->netscapeBookmarkUtils
71 ->expects(static::once())
72 ->method('import')
73 ->willReturnCallback(
74 function (
75 array $post,
76 UploadedFileInterface $file
77 ) use ($parameters, $requestFile): string {
78 static::assertSame($parameters, $post);
79 static::assertSame($requestFile, $file);
80
81 return 'status';
82 }
83 )
84 ;
85
86 $this->container->sessionManager
87 ->expects(static::once())
88 ->method('setSessionParameter')
89 ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['status'])
90 ;
91
92 $result = $this->controller->import($request, $response);
93
94 static::assertSame(302, $result->getStatusCode());
95 static::assertSame(['/subfolder/admin/import'], $result->getHeader('location'));
96 }
97
98 /**
99 * Test posting an import request - without import file
100 */
101 public function testImportFileMissing(): void
102 {
103 $request = $this->createMock(Request::class);
104 $response = new Response();
105
106 $this->container->sessionManager
107 ->expects(static::once())
108 ->method('setSessionParameter')
109 ->with(SessionManager::KEY_ERROR_MESSAGES, ['No import file provided.'])
110 ;
111
112 $result = $this->controller->import($request, $response);
113
114 static::assertSame(302, $result->getStatusCode());
115 static::assertSame(['/subfolder/admin/import'], $result->getHeader('location'));
116 }
117
118 /**
119 * Test posting an import request - with an empty file
120 */
121 public function testImportEmptyFile(): void
122 {
123 $requestFile = new UploadedFile('file', 'name', 'type', 0);
124
125 $request = $this->createMock(Request::class);
126 $request->method('getUploadedFiles')->willReturn(['filetoupload' => $requestFile]);
127 $response = new Response();
128
129 $this->container->netscapeBookmarkUtils = $this->createMock(NetscapeBookmarkUtils::class);
130 $this->container->netscapeBookmarkUtils->expects(static::never())->method('filterAndFormat');
131
132 $this->container->sessionManager
133 ->expects(static::once())
134 ->method('setSessionParameter')
135 ->willReturnCallback(function (string $key, array $value): SessionManager {
136 static::assertSame(SessionManager::KEY_ERROR_MESSAGES, $key);
137 static::assertStringStartsWith('The file you are trying to upload is probably bigger', $value[0]);
138
139 return $this->container->sessionManager;
140 })
141 ;
142
143 $result = $this->controller->import($request, $response);
144
145 static::assertSame(302, $result->getStatusCode());
146 static::assertSame(['/subfolder/admin/import'], $result->getHeader('location'));
147 }
148}
diff --git a/tests/front/controller/admin/LogoutControllerTest.php b/tests/front/controller/admin/LogoutControllerTest.php
new file mode 100644
index 00000000..94e53019
--- /dev/null
+++ b/tests/front/controller/admin/LogoutControllerTest.php
@@ -0,0 +1,50 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Security\CookieManager;
8use Shaarli\Security\SessionManager;
9use Shaarli\TestCase;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13class LogoutControllerTest extends TestCase
14{
15 use FrontAdminControllerMockHelper;
16
17 /** @var LogoutController */
18 protected $controller;
19
20 public function setUp(): void
21 {
22 $this->createContainer();
23
24 $this->controller = new LogoutController($this->container);
25 }
26
27 public function testValidControllerInvoke(): void
28 {
29 $request = $this->createMock(Request::class);
30 $response = new Response();
31
32 $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches');
33
34 $this->container->sessionManager = $this->createMock(SessionManager::class);
35 $this->container->sessionManager->expects(static::once())->method('logout');
36
37 $this->container->cookieManager = $this->createMock(CookieManager::class);
38 $this->container->cookieManager
39 ->expects(static::once())
40 ->method('setCookieParameter')
41 ->with(CookieManager::STAY_SIGNED_IN, 'false', 0, '/subfolder/')
42 ;
43
44 $result = $this->controller->index($request, $response);
45
46 static::assertInstanceOf(Response::class, $result);
47 static::assertSame(302, $result->getStatusCode());
48 static::assertSame(['/subfolder/'], $result->getHeader('location'));
49 }
50}
diff --git a/tests/front/controller/admin/ManageTagControllerTest.php b/tests/front/controller/admin/ManageTagControllerTest.php
new file mode 100644
index 00000000..8a0ff7a9
--- /dev/null
+++ b/tests/front/controller/admin/ManageTagControllerTest.php
@@ -0,0 +1,272 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\BookmarkFilter;
9use Shaarli\Front\Exception\WrongTokenException;
10use Shaarli\Security\SessionManager;
11use Shaarli\TestCase;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class ManageTagControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var ManageTagController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->controller = new ManageTagController($this->container);
27 }
28
29 /**
30 * Test displaying manage tag page
31 */
32 public function testIndex(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $request->method('getParam')->with('fromtag')->willReturn('fromtag');
39 $response = new Response();
40
41 $result = $this->controller->index($request, $response);
42
43 static::assertSame(200, $result->getStatusCode());
44 static::assertSame('changetag', (string) $result->getBody());
45
46 static::assertSame('fromtag', $assignedVariables['fromtag']);
47 static::assertSame('Manage tags - Shaarli', $assignedVariables['pagetitle']);
48 }
49
50 /**
51 * Test posting a tag update - rename tag - valid info provided.
52 */
53 public function testSaveRenameTagValid(): void
54 {
55 $session = [];
56 $this->assignSessionVars($session);
57
58 $requestParameters = [
59 'renametag' => 'rename',
60 'fromtag' => 'old-tag',
61 'totag' => 'new-tag',
62 ];
63 $request = $this->createMock(Request::class);
64 $request
65 ->expects(static::atLeastOnce())
66 ->method('getParam')
67 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
68 return $requestParameters[$key] ?? null;
69 })
70 ;
71 $response = new Response();
72
73 $bookmark1 = $this->createMock(Bookmark::class);
74 $bookmark2 = $this->createMock(Bookmark::class);
75 $this->container->bookmarkService
76 ->expects(static::once())
77 ->method('search')
78 ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
79 ->willReturnCallback(function () use ($bookmark1, $bookmark2): array {
80 $bookmark1->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
81 $bookmark2->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
82
83 return [$bookmark1, $bookmark2];
84 })
85 ;
86 $this->container->bookmarkService
87 ->expects(static::exactly(2))
88 ->method('set')
89 ->withConsecutive([$bookmark1, false], [$bookmark2, false])
90 ;
91 $this->container->bookmarkService->expects(static::once())->method('save');
92
93 $result = $this->controller->save($request, $response);
94
95 static::assertSame(302, $result->getStatusCode());
96 static::assertSame(['/subfolder/?searchtags=new-tag'], $result->getHeader('location'));
97
98 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
99 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
100 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
101 static::assertSame(['The tag was renamed in 2 bookmarks.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
102 }
103
104 /**
105 * Test posting a tag update - delete tag - valid info provided.
106 */
107 public function testSaveDeleteTagValid(): void
108 {
109 $session = [];
110 $this->assignSessionVars($session);
111
112 $requestParameters = [
113 'deletetag' => 'delete',
114 'fromtag' => 'old-tag',
115 ];
116 $request = $this->createMock(Request::class);
117 $request
118 ->expects(static::atLeastOnce())
119 ->method('getParam')
120 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
121 return $requestParameters[$key] ?? null;
122 })
123 ;
124 $response = new Response();
125
126 $bookmark1 = $this->createMock(Bookmark::class);
127 $bookmark2 = $this->createMock(Bookmark::class);
128 $this->container->bookmarkService
129 ->expects(static::once())
130 ->method('search')
131 ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
132 ->willReturnCallback(function () use ($bookmark1, $bookmark2): array {
133 $bookmark1->expects(static::once())->method('deleteTag')->with('old-tag');
134 $bookmark2->expects(static::once())->method('deleteTag')->with('old-tag');
135
136 return [$bookmark1, $bookmark2];
137 })
138 ;
139 $this->container->bookmarkService
140 ->expects(static::exactly(2))
141 ->method('set')
142 ->withConsecutive([$bookmark1, false], [$bookmark2, false])
143 ;
144 $this->container->bookmarkService->expects(static::once())->method('save');
145
146 $result = $this->controller->save($request, $response);
147
148 static::assertSame(302, $result->getStatusCode());
149 static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
150
151 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
152 static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
153 static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
154 static::assertSame(['The tag was removed from 2 bookmarks.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
155 }
156
157 /**
158 * Test posting a tag update - wrong token.
159 */
160 public function testSaveWrongToken(): void
161 {
162 $this->container->sessionManager = $this->createMock(SessionManager::class);
163 $this->container->sessionManager->method('checkToken')->willReturn(false);
164
165 $this->container->conf->expects(static::never())->method('set');
166 $this->container->conf->expects(static::never())->method('write');
167
168 $request = $this->createMock(Request::class);
169 $response = new Response();
170
171 $this->expectException(WrongTokenException::class);
172
173 $this->controller->save($request, $response);
174 }
175
176 /**
177 * Test posting a tag update - rename tag - missing "FROM" tag.
178 */
179 public function testSaveRenameTagMissingFrom(): void
180 {
181 $session = [];
182 $this->assignSessionVars($session);
183
184 $requestParameters = [
185 'renametag' => 'rename',
186 ];
187 $request = $this->createMock(Request::class);
188 $request
189 ->expects(static::atLeastOnce())
190 ->method('getParam')
191 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
192 return $requestParameters[$key] ?? null;
193 })
194 ;
195 $response = new Response();
196
197 $result = $this->controller->save($request, $response);
198
199 static::assertSame(302, $result->getStatusCode());
200 static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
201
202 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
203 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
204 static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
205 static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
206 }
207
208 /**
209 * Test posting a tag update - delete tag - missing "FROM" tag.
210 */
211 public function testSaveDeleteTagMissingFrom(): void
212 {
213 $session = [];
214 $this->assignSessionVars($session);
215
216 $requestParameters = [
217 'deletetag' => 'delete',
218 ];
219 $request = $this->createMock(Request::class);
220 $request
221 ->expects(static::atLeastOnce())
222 ->method('getParam')
223 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
224 return $requestParameters[$key] ?? null;
225 })
226 ;
227 $response = new Response();
228
229 $result = $this->controller->save($request, $response);
230
231 static::assertSame(302, $result->getStatusCode());
232 static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
233
234 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
235 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
236 static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
237 static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
238 }
239
240 /**
241 * Test posting a tag update - rename tag - missing "TO" tag.
242 */
243 public function testSaveRenameTagMissingTo(): void
244 {
245 $session = [];
246 $this->assignSessionVars($session);
247
248 $requestParameters = [
249 'renametag' => 'rename',
250 'fromtag' => 'old-tag'
251 ];
252 $request = $this->createMock(Request::class);
253 $request
254 ->expects(static::atLeastOnce())
255 ->method('getParam')
256 ->willReturnCallback(function (string $key) use ($requestParameters): ?string {
257 return $requestParameters[$key] ?? null;
258 })
259 ;
260 $response = new Response();
261
262 $result = $this->controller->save($request, $response);
263
264 static::assertSame(302, $result->getStatusCode());
265 static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
266
267 static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
268 static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
269 static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
270 static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
271 }
272}
diff --git a/tests/front/controller/admin/PasswordControllerTest.php b/tests/front/controller/admin/PasswordControllerTest.php
new file mode 100644
index 00000000..58f47b49
--- /dev/null
+++ b/tests/front/controller/admin/PasswordControllerTest.php
@@ -0,0 +1,203 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Front\Exception\OpenShaarliPasswordException;
9use Shaarli\Front\Exception\WrongTokenException;
10use Shaarli\Security\SessionManager;
11use Shaarli\TestCase;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class PasswordControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var PasswordController */
20 protected $controller;
21
22 /** @var mixed[] Variables assigned to the template */
23 protected $assignedVariables = [];
24
25 public function setUp(): void
26 {
27 $this->createContainer();
28 $this->assignTemplateVars($this->assignedVariables);
29
30 $this->controller = new PasswordController($this->container);
31 }
32
33 /**
34 * Test displaying the change password page.
35 */
36 public function testGetPage(): void
37 {
38 $request = $this->createMock(Request::class);
39 $response = new Response();
40
41 $result = $this->controller->index($request, $response);
42
43 static::assertSame(200, $result->getStatusCode());
44 static::assertSame('changepassword', (string) $result->getBody());
45 static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
46 }
47
48 /**
49 * Change the password with valid parameters
50 */
51 public function testPostNewPasswordDefault(): void
52 {
53 $request = $this->createMock(Request::class);
54 $request->method('getParam')->willReturnCallback(function (string $key): string {
55 if ('oldpassword' === $key) {
56 return 'old';
57 }
58 if ('setpassword' === $key) {
59 return 'new';
60 }
61
62 return $key;
63 });
64 $response = new Response();
65
66 $this->container->conf = $this->createMock(ConfigManager::class);
67 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
68 if ('credentials.hash' === $key) {
69 return sha1('old' . 'credentials.login' . 'credentials.salt');
70 }
71
72 return strpos($key, 'credentials') !== false ? $key : $default;
73 });
74 $this->container->conf->expects(static::once())->method('write')->with(true);
75
76 $this->container->conf
77 ->method('set')
78 ->willReturnCallback(function (string $key, string $value) {
79 if ('credentials.hash' === $key) {
80 static::assertSame(sha1('new' . 'credentials.login' . 'credentials.salt'), $value);
81 }
82 })
83 ;
84
85 $result = $this->controller->change($request, $response);
86
87 static::assertSame(200, $result->getStatusCode());
88 static::assertSame('changepassword', (string) $result->getBody());
89 static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
90 }
91
92 /**
93 * Change the password with a wrong existing password
94 */
95 public function testPostNewPasswordWrongOldPassword(): void
96 {
97 $request = $this->createMock(Request::class);
98 $request->method('getParam')->willReturnCallback(function (string $key): string {
99 if ('oldpassword' === $key) {
100 return 'wrong';
101 }
102 if ('setpassword' === $key) {
103 return 'new';
104 }
105
106 return $key;
107 });
108 $response = new Response();
109
110 $this->container->conf = $this->createMock(ConfigManager::class);
111 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
112 if ('credentials.hash' === $key) {
113 return sha1('old' . 'credentials.login' . 'credentials.salt');
114 }
115
116 return strpos($key, 'credentials') !== false ? $key : $default;
117 });
118
119 $this->container->conf->expects(static::never())->method('set');
120 $this->container->conf->expects(static::never())->method('write');
121
122 $this->container->sessionManager
123 ->expects(static::once())
124 ->method('setSessionParameter')
125 ->with(SessionManager::KEY_ERROR_MESSAGES, ['The old password is not correct.'])
126 ;
127
128 $result = $this->controller->change($request, $response);
129
130 static::assertSame(400, $result->getStatusCode());
131 static::assertSame('changepassword', (string) $result->getBody());
132 static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
133 }
134
135 /**
136 * Change the password with a wrong existing password
137 */
138 public function testPostNewPasswordWrongToken(): void
139 {
140 $this->container->sessionManager = $this->createMock(SessionManager::class);
141 $this->container->sessionManager->method('checkToken')->willReturn(false);
142
143 $this->container->conf->expects(static::never())->method('set');
144 $this->container->conf->expects(static::never())->method('write');
145
146 $request = $this->createMock(Request::class);
147 $response = new Response();
148
149 $this->expectException(WrongTokenException::class);
150
151 $this->controller->change($request, $response);
152 }
153
154 /**
155 * Change the password with an empty new password
156 */
157 public function testPostNewEmptyPassword(): void
158 {
159 $this->container->sessionManager
160 ->expects(static::once())
161 ->method('setSessionParameter')
162 ->with(SessionManager::KEY_ERROR_MESSAGES, ['You must provide the current and new password to change it.'])
163 ;
164
165 $this->container->conf->expects(static::never())->method('set');
166 $this->container->conf->expects(static::never())->method('write');
167
168 $request = $this->createMock(Request::class);
169 $request->method('getParam')->willReturnCallback(function (string $key): string {
170 if ('oldpassword' === $key) {
171 return 'old';
172 }
173 if ('setpassword' === $key) {
174 return '';
175 }
176
177 return $key;
178 });
179 $response = new Response();
180
181 $result = $this->controller->change($request, $response);
182
183 static::assertSame(400, $result->getStatusCode());
184 static::assertSame('changepassword', (string) $result->getBody());
185 static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
186 }
187
188 /**
189 * Change the password on an open shaarli
190 */
191 public function testPostNewPasswordOnOpenShaarli(): void
192 {
193 $this->container->conf = $this->createMock(ConfigManager::class);
194 $this->container->conf->method('get')->with('security.open_shaarli')->willReturn(true);
195
196 $request = $this->createMock(Request::class);
197 $response = new Response();
198
199 $this->expectException(OpenShaarliPasswordException::class);
200
201 $this->controller->change($request, $response);
202 }
203}
diff --git a/tests/front/controller/admin/PluginsControllerTest.php b/tests/front/controller/admin/PluginsControllerTest.php
new file mode 100644
index 00000000..974d614d
--- /dev/null
+++ b/tests/front/controller/admin/PluginsControllerTest.php
@@ -0,0 +1,205 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Front\Exception\WrongTokenException;
9use Shaarli\Plugin\PluginManager;
10use Shaarli\Security\SessionManager;
11use Shaarli\TestCase;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class PluginsControllerTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 const PLUGIN_NAMES = ['plugin1', 'plugin2', 'plugin3', 'plugin4'];
20
21 /** @var PluginsController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->createContainer();
27
28 $this->controller = new PluginsController($this->container);
29
30 mkdir($path = __DIR__ . '/folder');
31 PluginManager::$PLUGINS_PATH = $path;
32 array_map(function (string $plugin) use ($path) { touch($path . '/' . $plugin); }, static::PLUGIN_NAMES);
33 }
34
35 public function tearDown(): void
36 {
37 $path = __DIR__ . '/folder';
38 array_map(function (string $plugin) use ($path) { unlink($path . '/' . $plugin); }, static::PLUGIN_NAMES);
39 rmdir($path);
40 }
41
42 /**
43 * Test displaying plugins admin page
44 */
45 public function testIndex(): void
46 {
47 $assignedVariables = [];
48 $this->assignTemplateVars($assignedVariables);
49
50 $request = $this->createMock(Request::class);
51 $response = new Response();
52
53 $data = [
54 'plugin1' => ['order' => 2, 'other' => 'field'],
55 'plugin2' => ['order' => 1],
56 'plugin3' => ['order' => false, 'abc' => 'def'],
57 'plugin4' => [],
58 ];
59
60 $this->container->pluginManager
61 ->expects(static::once())
62 ->method('getPluginsMeta')
63 ->willReturn($data);
64
65 $result = $this->controller->index($request, $response);
66
67 static::assertSame(200, $result->getStatusCode());
68 static::assertSame('pluginsadmin', (string) $result->getBody());
69
70 static::assertSame('Plugin Administration - Shaarli', $assignedVariables['pagetitle']);
71 static::assertSame(
72 ['plugin2' => $data['plugin2'], 'plugin1' => $data['plugin1']],
73 $assignedVariables['enabledPlugins']
74 );
75 static::assertSame(
76 ['plugin3' => $data['plugin3'], 'plugin4' => $data['plugin4']],
77 $assignedVariables['disabledPlugins']
78 );
79 }
80
81 /**
82 * Test save plugins admin page
83 */
84 public function testSaveEnabledPlugins(): void
85 {
86 $parameters = [
87 'plugin1' => 'on',
88 'order_plugin1' => '2',
89 'plugin2' => 'on',
90 ];
91
92 $request = $this->createMock(Request::class);
93 $request
94 ->expects(static::atLeastOnce())
95 ->method('getParams')
96 ->willReturnCallback(function () use ($parameters): array {
97 return $parameters;
98 })
99 ;
100 $response = new Response();
101
102 $this->container->pluginManager
103 ->expects(static::once())
104 ->method('executeHooks')
105 ->with('save_plugin_parameters', $parameters)
106 ;
107 $this->container->conf
108 ->expects(static::atLeastOnce())
109 ->method('set')
110 ->with('general.enabled_plugins', ['plugin1', 'plugin2'])
111 ;
112
113 $result = $this->controller->save($request, $response);
114
115 static::assertSame(302, $result->getStatusCode());
116 static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
117 }
118
119 /**
120 * Test save plugin parameters
121 */
122 public function testSavePluginParameters(): void
123 {
124 $parameters = [
125 'parameters_form' => true,
126 'parameter1' => 'blip',
127 'parameter2' => 'blop',
128 'token' => 'this parameter should not be saved'
129 ];
130
131 $request = $this->createMock(Request::class);
132 $request
133 ->expects(static::atLeastOnce())
134 ->method('getParams')
135 ->willReturnCallback(function () use ($parameters): array {
136 return $parameters;
137 })
138 ;
139 $response = new Response();
140
141 $this->container->pluginManager
142 ->expects(static::once())
143 ->method('executeHooks')
144 ->with('save_plugin_parameters', $parameters)
145 ;
146 $this->container->conf
147 ->expects(static::exactly(2))
148 ->method('set')
149 ->withConsecutive(['plugins.parameter1', 'blip'], ['plugins.parameter2', 'blop'])
150 ;
151
152 $result = $this->controller->save($request, $response);
153
154 static::assertSame(302, $result->getStatusCode());
155 static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
156 }
157
158 /**
159 * Test save plugin parameters - error encountered
160 */
161 public function testSaveWithError(): void
162 {
163 $request = $this->createMock(Request::class);
164 $response = new Response();
165
166 $this->container->conf = $this->createMock(ConfigManager::class);
167 $this->container->conf
168 ->expects(static::atLeastOnce())
169 ->method('write')
170 ->willThrowException(new \Exception($message = 'error message'))
171 ;
172
173 $this->container->sessionManager = $this->createMock(SessionManager::class);
174 $this->container->sessionManager->method('checkToken')->willReturn(true);
175 $this->container->sessionManager
176 ->expects(static::once())
177 ->method('setSessionParameter')
178 ->with(
179 SessionManager::KEY_ERROR_MESSAGES,
180 ['Error while saving plugin configuration: ' . PHP_EOL . $message]
181 )
182 ;
183
184 $result = $this->controller->save($request, $response);
185
186 static::assertSame(302, $result->getStatusCode());
187 static::assertSame(['/subfolder/admin/plugins'], $result->getHeader('location'));
188 }
189
190 /**
191 * Test save plugin parameters - wrong token
192 */
193 public function testSaveWrongToken(): void
194 {
195 $this->container->sessionManager = $this->createMock(SessionManager::class);
196 $this->container->sessionManager->method('checkToken')->willReturn(false);
197
198 $request = $this->createMock(Request::class);
199 $response = new Response();
200
201 $this->expectException(WrongTokenException::class);
202
203 $this->controller->save($request, $response);
204 }
205}
diff --git a/tests/front/controller/admin/ServerControllerTest.php b/tests/front/controller/admin/ServerControllerTest.php
new file mode 100644
index 00000000..355cce7d
--- /dev/null
+++ b/tests/front/controller/admin/ServerControllerTest.php
@@ -0,0 +1,184 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Security\SessionManager;
9use Shaarli\TestCase;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13/**
14 * Test Server administration controller.
15 */
16class ServerControllerTest extends TestCase
17{
18 use FrontAdminControllerMockHelper;
19
20 /** @var ServerController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->controller = new ServerController($this->container);
28
29 // initialize dummy cache
30 @mkdir('sandbox/');
31 foreach (['pagecache', 'tmp', 'cache'] as $folder) {
32 @mkdir('sandbox/' . $folder);
33 @touch('sandbox/' . $folder . '/.htaccess');
34 @touch('sandbox/' . $folder . '/1');
35 @touch('sandbox/' . $folder . '/2');
36 }
37 }
38
39 public function tearDown(): void
40 {
41 foreach (['pagecache', 'tmp', 'cache'] as $folder) {
42 @unlink('sandbox/' . $folder . '/.htaccess');
43 @unlink('sandbox/' . $folder . '/1');
44 @unlink('sandbox/' . $folder . '/2');
45 @rmdir('sandbox/' . $folder);
46 }
47 }
48
49 /**
50 * Test default display of server administration page.
51 */
52 public function testIndex(): void
53 {
54 $request = $this->createMock(Request::class);
55 $response = new Response();
56
57 // Save RainTPL assigned variables
58 $assignedVariables = [];
59 $this->assignTemplateVars($assignedVariables);
60
61 $result = $this->controller->index($request, $response);
62
63 static::assertSame(200, $result->getStatusCode());
64 static::assertSame('server', (string) $result->getBody());
65
66 static::assertSame(PHP_VERSION, $assignedVariables['php_version']);
67 static::assertArrayHasKey('php_has_reached_eol', $assignedVariables);
68 static::assertArrayHasKey('php_eol', $assignedVariables);
69 static::assertArrayHasKey('php_extensions', $assignedVariables);
70 static::assertArrayHasKey('permissions', $assignedVariables);
71 static::assertEmpty($assignedVariables['permissions']);
72
73 static::assertRegExp(
74 '#https://github\.com/shaarli/Shaarli/releases/tag/v\d+\.\d+\.\d+#',
75 $assignedVariables['release_url']
76 );
77 static::assertRegExp('#v\d+\.\d+\.\d+#', $assignedVariables['latest_version']);
78 static::assertRegExp('#(v\d+\.\d+\.\d+|dev)#', $assignedVariables['current_version']);
79 static::assertArrayHasKey('index_url', $assignedVariables);
80 static::assertArrayHasKey('client_ip', $assignedVariables);
81 static::assertArrayHasKey('trusted_proxies', $assignedVariables);
82
83 static::assertSame('Server administration - Shaarli', $assignedVariables['pagetitle']);
84 }
85
86 /**
87 * Test clearing the main cache
88 */
89 public function testClearMainCache(): void
90 {
91 $this->container->conf = $this->createMock(ConfigManager::class);
92 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
93 if ($key === 'resource.page_cache') {
94 return 'sandbox/pagecache';
95 } elseif ($key === 'resource.raintpl_tmp') {
96 return 'sandbox/tmp';
97 } elseif ($key === 'resource.thumbnails_cache') {
98 return 'sandbox/cache';
99 } else {
100 return $default;
101 }
102 });
103
104 $this->container->sessionManager
105 ->expects(static::once())
106 ->method('setSessionParameter')
107 ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['Shaarli\'s cache folder has been cleared!'])
108 ;
109
110 $request = $this->createMock(Request::class);
111 $request->method('getQueryParam')->with('type')->willReturn('main');
112 $response = new Response();
113
114 $result = $this->controller->clearCache($request, $response);
115
116 static::assertSame(302, $result->getStatusCode());
117 static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location'));
118
119 static::assertFileNotExists('sandbox/pagecache/1');
120 static::assertFileNotExists('sandbox/pagecache/2');
121 static::assertFileNotExists('sandbox/tmp/1');
122 static::assertFileNotExists('sandbox/tmp/2');
123
124 static::assertFileExists('sandbox/pagecache/.htaccess');
125 static::assertFileExists('sandbox/tmp/.htaccess');
126 static::assertFileExists('sandbox/cache');
127 static::assertFileExists('sandbox/cache/.htaccess');
128 static::assertFileExists('sandbox/cache/1');
129 static::assertFileExists('sandbox/cache/2');
130 }
131
132 /**
133 * Test clearing thumbnails cache
134 */
135 public function testClearThumbnailsCache(): void
136 {
137 $this->container->conf = $this->createMock(ConfigManager::class);
138 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
139 if ($key === 'resource.page_cache') {
140 return 'sandbox/pagecache';
141 } elseif ($key === 'resource.raintpl_tmp') {
142 return 'sandbox/tmp';
143 } elseif ($key === 'resource.thumbnails_cache') {
144 return 'sandbox/cache';
145 } else {
146 return $default;
147 }
148 });
149
150 $this->container->sessionManager
151 ->expects(static::once())
152 ->method('setSessionParameter')
153 ->willReturnCallback(function (string $key, array $value): SessionManager {
154 static::assertSame(SessionManager::KEY_WARNING_MESSAGES, $key);
155 static::assertCount(1, $value);
156 static::assertStringStartsWith('Thumbnails cache has been cleared.', $value[0]);
157
158 return $this->container->sessionManager;
159 });
160 ;
161
162 $request = $this->createMock(Request::class);
163 $request->method('getQueryParam')->with('type')->willReturn('thumbnails');
164 $response = new Response();
165
166 $result = $this->controller->clearCache($request, $response);
167
168 static::assertSame(302, $result->getStatusCode());
169 static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location'));
170
171 static::assertFileNotExists('sandbox/cache/1');
172 static::assertFileNotExists('sandbox/cache/2');
173
174 static::assertFileExists('sandbox/cache/.htaccess');
175 static::assertFileExists('sandbox/pagecache');
176 static::assertFileExists('sandbox/pagecache/.htaccess');
177 static::assertFileExists('sandbox/pagecache/1');
178 static::assertFileExists('sandbox/pagecache/2');
179 static::assertFileExists('sandbox/tmp');
180 static::assertFileExists('sandbox/tmp/.htaccess');
181 static::assertFileExists('sandbox/tmp/1');
182 static::assertFileExists('sandbox/tmp/2');
183 }
184}
diff --git a/tests/front/controller/admin/SessionFilterControllerTest.php b/tests/front/controller/admin/SessionFilterControllerTest.php
new file mode 100644
index 00000000..712a625b
--- /dev/null
+++ b/tests/front/controller/admin/SessionFilterControllerTest.php
@@ -0,0 +1,177 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Security\LoginManager;
8use Shaarli\Security\SessionManager;
9use Shaarli\TestCase;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13class SessionFilterControllerTest extends TestCase
14{
15 use FrontAdminControllerMockHelper;
16
17 /** @var SessionFilterController */
18 protected $controller;
19
20 public function setUp(): void
21 {
22 $this->createContainer();
23
24 $this->controller = new SessionFilterController($this->container);
25 }
26
27 /**
28 * Visibility - Default call for private filter while logged in without current value
29 */
30 public function testVisibility(): void
31 {
32 $arg = ['visibility' => 'private'];
33
34 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller/?searchtag=abc';
35
36 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
37 $this->container->sessionManager
38 ->expects(static::once())
39 ->method('setSessionParameter')
40 ->with(SessionManager::KEY_VISIBILITY, 'private')
41 ;
42
43 $request = $this->createMock(Request::class);
44 $response = new Response();
45
46 $result = $this->controller->visibility($request, $response, $arg);
47
48 static::assertInstanceOf(Response::class, $result);
49 static::assertSame(302, $result->getStatusCode());
50 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
51 }
52
53 /**
54 * Visibility - Toggle off private visibility
55 */
56 public function testVisibilityToggleOff(): void
57 {
58 $arg = ['visibility' => 'private'];
59
60 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller/?searchtag=abc';
61
62 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
63 $this->container->sessionManager
64 ->method('getSessionParameter')
65 ->with(SessionManager::KEY_VISIBILITY)
66 ->willReturn('private')
67 ;
68 $this->container->sessionManager
69 ->expects(static::never())
70 ->method('setSessionParameter')
71 ;
72 $this->container->sessionManager
73 ->expects(static::once())
74 ->method('deleteSessionParameter')
75 ->with(SessionManager::KEY_VISIBILITY)
76 ;
77
78 $request = $this->createMock(Request::class);
79 $response = new Response();
80
81 $result = $this->controller->visibility($request, $response, $arg);
82
83 static::assertInstanceOf(Response::class, $result);
84 static::assertSame(302, $result->getStatusCode());
85 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
86 }
87
88 /**
89 * Visibility - Change private to public
90 */
91 public function testVisibilitySwitch(): void
92 {
93 $arg = ['visibility' => 'private'];
94
95 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
96 $this->container->sessionManager
97 ->method('getSessionParameter')
98 ->with(SessionManager::KEY_VISIBILITY)
99 ->willReturn('public')
100 ;
101 $this->container->sessionManager
102 ->expects(static::once())
103 ->method('setSessionParameter')
104 ->with(SessionManager::KEY_VISIBILITY, 'private')
105 ;
106
107 $request = $this->createMock(Request::class);
108 $response = new Response();
109
110 $result = $this->controller->visibility($request, $response, $arg);
111
112 static::assertInstanceOf(Response::class, $result);
113 static::assertSame(302, $result->getStatusCode());
114 static::assertSame(['/subfolder/'], $result->getHeader('location'));
115 }
116
117 /**
118 * Visibility - With invalid value - should remove any visibility setting
119 */
120 public function testVisibilityInvalidValue(): void
121 {
122 $arg = ['visibility' => 'test'];
123
124 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller/?searchtag=abc';
125
126 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
127 $this->container->sessionManager
128 ->expects(static::never())
129 ->method('setSessionParameter')
130 ;
131 $this->container->sessionManager
132 ->expects(static::once())
133 ->method('deleteSessionParameter')
134 ->with(SessionManager::KEY_VISIBILITY)
135 ;
136
137 $request = $this->createMock(Request::class);
138 $response = new Response();
139
140 $result = $this->controller->visibility($request, $response, $arg);
141
142 static::assertInstanceOf(Response::class, $result);
143 static::assertSame(302, $result->getStatusCode());
144 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
145 }
146
147 /**
148 * Visibility - Try to change visibility while logged out
149 */
150 public function testVisibilityLoggedOut(): void
151 {
152 $arg = ['visibility' => 'test'];
153
154 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller/?searchtag=abc';
155
156 $this->container->loginManager = $this->createMock(LoginManager::class);
157 $this->container->loginManager->method('isLoggedIn')->willReturn(false);
158 $this->container->sessionManager
159 ->expects(static::never())
160 ->method('setSessionParameter')
161 ;
162 $this->container->sessionManager
163 ->expects(static::never())
164 ->method('deleteSessionParameter')
165 ->with(SessionManager::KEY_VISIBILITY)
166 ;
167
168 $request = $this->createMock(Request::class);
169 $response = new Response();
170
171 $result = $this->controller->visibility($request, $response, $arg);
172
173 static::assertInstanceOf(Response::class, $result);
174 static::assertSame(302, $result->getStatusCode());
175 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
176 }
177}
diff --git a/tests/front/controller/admin/ShaareAddControllerTest.php b/tests/front/controller/admin/ShaareAddControllerTest.php
new file mode 100644
index 00000000..a27ebe64
--- /dev/null
+++ b/tests/front/controller/admin/ShaareAddControllerTest.php
@@ -0,0 +1,97 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Formatter\BookmarkMarkdownFormatter;
9use Shaarli\Http\HttpAccess;
10use Shaarli\TestCase;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class ShaareAddControllerTest extends TestCase
15{
16 use FrontAdminControllerMockHelper;
17
18 /** @var ShaareAddController */
19 protected $controller;
20
21 public function setUp(): void
22 {
23 $this->createContainer();
24
25 $this->container->httpAccess = $this->createMock(HttpAccess::class);
26 $this->controller = new ShaareAddController($this->container);
27 }
28
29 /**
30 * Test displaying add link page
31 */
32 public function testAddShaare(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $response = new Response();
39
40 $expectedTags = [
41 'tag1' => 32,
42 'tag2' => 24,
43 'tag3' => 1,
44 ];
45 $this->container->bookmarkService
46 ->expects(static::once())
47 ->method('bookmarksCountPerTag')
48 ->willReturn($expectedTags)
49 ;
50 $expectedTags = array_merge($expectedTags, [BookmarkMarkdownFormatter::NO_MD_TAG => 1]);
51
52 $this->container->conf = $this->createMock(ConfigManager::class);
53 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
54 return $key === 'formatter' ? 'markdown' : $default;
55 });
56
57 $result = $this->controller->addShaare($request, $response);
58
59 static::assertSame(200, $result->getStatusCode());
60 static::assertSame('addlink', (string) $result->getBody());
61
62 static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']);
63 static::assertFalse($assignedVariables['default_private_links']);
64 static::assertTrue($assignedVariables['async_metadata']);
65 static::assertSame($expectedTags, $assignedVariables['tags']);
66 }
67
68 /**
69 * Test displaying add link page
70 */
71 public function testAddShaareWithoutMd(): void
72 {
73 $assignedVariables = [];
74 $this->assignTemplateVars($assignedVariables);
75
76 $request = $this->createMock(Request::class);
77 $response = new Response();
78
79 $expectedTags = [
80 'tag1' => 32,
81 'tag2' => 24,
82 'tag3' => 1,
83 ];
84 $this->container->bookmarkService
85 ->expects(static::once())
86 ->method('bookmarksCountPerTag')
87 ->willReturn($expectedTags)
88 ;
89
90 $result = $this->controller->addShaare($request, $response);
91
92 static::assertSame(200, $result->getStatusCode());
93 static::assertSame('addlink', (string) $result->getBody());
94
95 static::assertSame($expectedTags, $assignedVariables['tags']);
96 }
97}
diff --git a/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php
new file mode 100644
index 00000000..28b1c023
--- /dev/null
+++ b/tests/front/controller/admin/ShaareManageControllerTest/ChangeVisibilityBookmarkTest.php
@@ -0,0 +1,418 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\BookmarkRawFormatter;
11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
13use Shaarli\Front\Controller\Admin\ShaareManageController;
14use Shaarli\Http\HttpAccess;
15use Shaarli\Security\SessionManager;
16use Shaarli\TestCase;
17use Slim\Http\Request;
18use Slim\Http\Response;
19
20class ChangeVisibilityBookmarkTest extends TestCase
21{
22 use FrontAdminControllerMockHelper;
23
24 /** @var ShaareManageController */
25 protected $controller;
26
27 public function setUp(): void
28 {
29 $this->createContainer();
30
31 $this->container->httpAccess = $this->createMock(HttpAccess::class);
32 $this->controller = new ShaareManageController($this->container);
33 }
34
35 /**
36 * Change bookmark visibility - Set private - Single public bookmark with valid parameters
37 */
38 public function testSetSingleBookmarkPrivate(): void
39 {
40 $parameters = ['id' => '123', 'newVisibility' => 'private'];
41
42 $request = $this->createMock(Request::class);
43 $request
44 ->method('getParam')
45 ->willReturnCallback(function (string $key) use ($parameters): ?string {
46 return $parameters[$key] ?? null;
47 })
48 ;
49 $response = new Response();
50
51 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(false);
52
53 static::assertFalse($bookmark->isPrivate());
54
55 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
56 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
57 $this->container->bookmarkService->expects(static::once())->method('save');
58 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
59 $this->container->formatterFactory
60 ->expects(static::once())
61 ->method('getFormatter')
62 ->with('raw')
63 ->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
64 return new BookmarkRawFormatter($this->container->conf, true);
65 })
66 ;
67
68 // Make sure that PluginManager hook is triggered
69 $this->container->pluginManager
70 ->expects(static::once())
71 ->method('executeHooks')
72 ->with('save_link')
73 ;
74
75 $result = $this->controller->changeVisibility($request, $response);
76
77 static::assertTrue($bookmark->isPrivate());
78
79 static::assertSame(302, $result->getStatusCode());
80 static::assertSame(['/subfolder/'], $result->getHeader('location'));
81 }
82
83 /**
84 * Change bookmark visibility - Set public - Single private bookmark with valid parameters
85 */
86 public function testSetSingleBookmarkPublic(): void
87 {
88 $parameters = ['id' => '123', 'newVisibility' => 'public'];
89
90 $request = $this->createMock(Request::class);
91 $request
92 ->method('getParam')
93 ->willReturnCallback(function (string $key) use ($parameters): ?string {
94 return $parameters[$key] ?? null;
95 })
96 ;
97 $response = new Response();
98
99 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true);
100
101 static::assertTrue($bookmark->isPrivate());
102
103 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
104 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
105 $this->container->bookmarkService->expects(static::once())->method('save');
106 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
107 $this->container->formatterFactory
108 ->expects(static::once())
109 ->method('getFormatter')
110 ->with('raw')
111 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
112 ;
113
114 // Make sure that PluginManager hook is triggered
115 $this->container->pluginManager
116 ->expects(static::once())
117 ->method('executeHooks')
118 ->with('save_link')
119 ;
120
121 $result = $this->controller->changeVisibility($request, $response);
122
123 static::assertFalse($bookmark->isPrivate());
124
125 static::assertSame(302, $result->getStatusCode());
126 static::assertSame(['/subfolder/'], $result->getHeader('location'));
127 }
128
129 /**
130 * Change bookmark visibility - Set private on single already private bookmark
131 */
132 public function testSetSinglePrivateBookmarkPrivate(): void
133 {
134 $parameters = ['id' => '123', 'newVisibility' => 'private'];
135
136 $request = $this->createMock(Request::class);
137 $request
138 ->method('getParam')
139 ->willReturnCallback(function (string $key) use ($parameters): ?string {
140 return $parameters[$key] ?? null;
141 })
142 ;
143 $response = new Response();
144
145 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true);
146
147 static::assertTrue($bookmark->isPrivate());
148
149 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
150 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
151 $this->container->bookmarkService->expects(static::once())->method('save');
152 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
153 $this->container->formatterFactory
154 ->expects(static::once())
155 ->method('getFormatter')
156 ->with('raw')
157 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
158 ;
159
160 // Make sure that PluginManager hook is triggered
161 $this->container->pluginManager
162 ->expects(static::once())
163 ->method('executeHooks')
164 ->with('save_link')
165 ;
166
167 $result = $this->controller->changeVisibility($request, $response);
168
169 static::assertTrue($bookmark->isPrivate());
170
171 static::assertSame(302, $result->getStatusCode());
172 static::assertSame(['/subfolder/'], $result->getHeader('location'));
173 }
174
175 /**
176 * Change bookmark visibility - Set multiple bookmarks private
177 */
178 public function testSetMultipleBookmarksPrivate(): void
179 {
180 $parameters = ['id' => '123 456 789', 'newVisibility' => 'private'];
181
182 $request = $this->createMock(Request::class);
183 $request
184 ->method('getParam')
185 ->willReturnCallback(function (string $key) use ($parameters): ?string {
186 return $parameters[$key] ?? null;
187 })
188 ;
189 $response = new Response();
190
191 $bookmarks = [
192 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(false),
193 (new Bookmark())->setId(456)->setUrl('http://domain.tld')->setTitle('Title 456')->setPrivate(true),
194 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789')->setPrivate(false),
195 ];
196
197 $this->container->bookmarkService
198 ->expects(static::exactly(3))
199 ->method('get')
200 ->withConsecutive([123], [456], [789])
201 ->willReturnOnConsecutiveCalls(...$bookmarks)
202 ;
203 $this->container->bookmarkService
204 ->expects(static::exactly(3))
205 ->method('set')
206 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
207 return [$bookmark, false];
208 }, $bookmarks))
209 ;
210 $this->container->bookmarkService->expects(static::once())->method('save');
211 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
212 $this->container->formatterFactory
213 ->expects(static::once())
214 ->method('getFormatter')
215 ->with('raw')
216 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
217 ;
218
219 // Make sure that PluginManager hook is triggered
220 $this->container->pluginManager
221 ->expects(static::exactly(3))
222 ->method('executeHooks')
223 ->with('save_link')
224 ;
225
226 $result = $this->controller->changeVisibility($request, $response);
227
228 static::assertTrue($bookmarks[0]->isPrivate());
229 static::assertTrue($bookmarks[1]->isPrivate());
230 static::assertTrue($bookmarks[2]->isPrivate());
231
232 static::assertSame(302, $result->getStatusCode());
233 static::assertSame(['/subfolder/'], $result->getHeader('location'));
234 }
235
236 /**
237 * Change bookmark visibility - Single bookmark not found.
238 */
239 public function testChangeVisibilitySingleBookmarkNotFound(): void
240 {
241 $parameters = ['id' => '123', 'newVisibility' => 'private'];
242
243 $request = $this->createMock(Request::class);
244 $request
245 ->method('getParam')
246 ->willReturnCallback(function (string $key) use ($parameters): ?string {
247 return $parameters[$key] ?? null;
248 })
249 ;
250 $response = new Response();
251
252 $this->container->bookmarkService
253 ->expects(static::once())
254 ->method('get')
255 ->willThrowException(new BookmarkNotFoundException())
256 ;
257 $this->container->bookmarkService->expects(static::never())->method('set');
258 $this->container->bookmarkService->expects(static::never())->method('save');
259 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
260 $this->container->formatterFactory
261 ->expects(static::once())
262 ->method('getFormatter')
263 ->with('raw')
264 ->willReturn(new BookmarkRawFormatter($this->container->conf, true))
265 ;
266
267 // Make sure that PluginManager hook is not triggered
268 $this->container->pluginManager
269 ->expects(static::never())
270 ->method('executeHooks')
271 ->with('save_link')
272 ;
273
274 $result = $this->controller->changeVisibility($request, $response);
275
276 static::assertSame(302, $result->getStatusCode());
277 static::assertSame(['/subfolder/'], $result->getHeader('location'));
278 }
279
280 /**
281 * Change bookmark visibility - Multiple bookmarks with one not found.
282 */
283 public function testChangeVisibilityMultipleBookmarksOneNotFound(): void
284 {
285 $parameters = ['id' => '123 456 789', 'newVisibility' => 'public'];
286
287 $request = $this->createMock(Request::class);
288 $request
289 ->method('getParam')
290 ->willReturnCallback(function (string $key) use ($parameters): ?string {
291 return $parameters[$key] ?? null;
292 })
293 ;
294 $response = new Response();
295
296 $bookmarks = [
297 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')->setPrivate(true),
298 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789')->setPrivate(false),
299 ];
300
301 $this->container->bookmarkService
302 ->expects(static::exactly(3))
303 ->method('get')
304 ->withConsecutive([123], [456], [789])
305 ->willReturnCallback(function (int $id) use ($bookmarks): Bookmark {
306 if ($id === 123) {
307 return $bookmarks[0];
308 }
309 if ($id === 789) {
310 return $bookmarks[1];
311 }
312 throw new BookmarkNotFoundException();
313 })
314 ;
315 $this->container->bookmarkService
316 ->expects(static::exactly(2))
317 ->method('set')
318 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
319 return [$bookmark, false];
320 }, $bookmarks))
321 ;
322 $this->container->bookmarkService->expects(static::once())->method('save');
323
324 // Make sure that PluginManager hook is not triggered
325 $this->container->pluginManager
326 ->expects(static::exactly(2))
327 ->method('executeHooks')
328 ->with('save_link')
329 ;
330
331 $this->container->sessionManager
332 ->expects(static::once())
333 ->method('setSessionParameter')
334 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 456 could not be found.'])
335 ;
336
337 $result = $this->controller->changeVisibility($request, $response);
338
339 static::assertSame(302, $result->getStatusCode());
340 static::assertSame(['/subfolder/'], $result->getHeader('location'));
341 }
342
343 /**
344 * Change bookmark visibility - Invalid ID
345 */
346 public function testChangeVisibilityInvalidId(): void
347 {
348 $parameters = ['id' => 'nope not an ID', 'newVisibility' => 'private'];
349
350 $request = $this->createMock(Request::class);
351 $request
352 ->method('getParam')
353 ->willReturnCallback(function (string $key) use ($parameters): ?string {
354 return $parameters[$key] ?? null;
355 })
356 ;
357 $response = new Response();
358
359 $this->container->sessionManager
360 ->expects(static::once())
361 ->method('setSessionParameter')
362 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
363 ;
364
365 $result = $this->controller->changeVisibility($request, $response);
366
367 static::assertSame(302, $result->getStatusCode());
368 static::assertSame(['/subfolder/'], $result->getHeader('location'));
369 }
370
371 /**
372 * Change bookmark visibility - Empty ID
373 */
374 public function testChangeVisibilityEmptyId(): void
375 {
376 $request = $this->createMock(Request::class);
377 $response = new Response();
378
379 $this->container->sessionManager
380 ->expects(static::once())
381 ->method('setSessionParameter')
382 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
383 ;
384
385 $result = $this->controller->changeVisibility($request, $response);
386
387 static::assertSame(302, $result->getStatusCode());
388 static::assertSame(['/subfolder/'], $result->getHeader('location'));
389 }
390
391 /**
392 * Change bookmark visibility - with invalid visibility
393 */
394 public function testChangeVisibilityWithInvalidVisibility(): void
395 {
396 $parameters = ['id' => '123', 'newVisibility' => 'invalid'];
397
398 $request = $this->createMock(Request::class);
399 $request
400 ->method('getParam')
401 ->willReturnCallback(function (string $key) use ($parameters): ?string {
402 return $parameters[$key] ?? null;
403 })
404 ;
405 $response = new Response();
406
407 $this->container->sessionManager
408 ->expects(static::once())
409 ->method('setSessionParameter')
410 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid visibility provided.'])
411 ;
412
413 $result = $this->controller->changeVisibility($request, $response);
414
415 static::assertSame(302, $result->getStatusCode());
416 static::assertSame(['/subfolder/'], $result->getHeader('location'));
417 }
418}
diff --git a/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php
new file mode 100644
index 00000000..770a16d7
--- /dev/null
+++ b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php
@@ -0,0 +1,380 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Formatter\BookmarkFormatter;
10use Shaarli\Formatter\FormatterFactory;
11use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
12use Shaarli\Front\Controller\Admin\ShaareManageController;
13use Shaarli\Http\HttpAccess;
14use Shaarli\Security\SessionManager;
15use Shaarli\TestCase;
16use Slim\Http\Request;
17use Slim\Http\Response;
18
19class DeleteBookmarkTest extends TestCase
20{
21 use FrontAdminControllerMockHelper;
22
23 /** @var ShaareManageController */
24 protected $controller;
25
26 public function setUp(): void
27 {
28 $this->createContainer();
29
30 $this->container->httpAccess = $this->createMock(HttpAccess::class);
31 $this->controller = new ShaareManageController($this->container);
32 }
33
34 /**
35 * Delete bookmark - Single bookmark with valid parameters
36 */
37 public function testDeleteSingleBookmark(): void
38 {
39 $parameters = ['id' => '123'];
40
41 $request = $this->createMock(Request::class);
42 $request
43 ->method('getParam')
44 ->willReturnCallback(function (string $key) use ($parameters): ?string {
45 return $parameters[$key] ?? null;
46 })
47 ;
48 $response = new Response();
49
50 $bookmark = (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123');
51
52 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
53 $this->container->bookmarkService->expects(static::once())->method('remove')->with($bookmark, false);
54 $this->container->bookmarkService->expects(static::once())->method('save');
55 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
56 $this->container->formatterFactory
57 ->expects(static::once())
58 ->method('getFormatter')
59 ->with('raw')
60 ->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
61 $formatter = $this->createMock(BookmarkFormatter::class);
62 $formatter
63 ->expects(static::once())
64 ->method('format')
65 ->with($bookmark)
66 ->willReturn(['formatted' => $bookmark])
67 ;
68
69 return $formatter;
70 })
71 ;
72
73 // Make sure that PluginManager hook is triggered
74 $this->container->pluginManager
75 ->expects(static::once())
76 ->method('executeHooks')
77 ->with('delete_link', ['formatted' => $bookmark])
78 ;
79
80 $result = $this->controller->deleteBookmark($request, $response);
81
82 static::assertSame(302, $result->getStatusCode());
83 static::assertSame(['/subfolder/'], $result->getHeader('location'));
84 }
85
86 /**
87 * Delete bookmark - Multiple bookmarks with valid parameters
88 */
89 public function testDeleteMultipleBookmarks(): void
90 {
91 $parameters = ['id' => '123 456 789'];
92
93 $request = $this->createMock(Request::class);
94 $request
95 ->method('getParam')
96 ->willReturnCallback(function (string $key) use ($parameters): ?string {
97 return $parameters[$key] ?? null;
98 })
99 ;
100 $response = new Response();
101
102 $bookmarks = [
103 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123'),
104 (new Bookmark())->setId(456)->setUrl('http://domain.tld')->setTitle('Title 456'),
105 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789'),
106 ];
107
108 $this->container->bookmarkService
109 ->expects(static::exactly(3))
110 ->method('get')
111 ->withConsecutive([123], [456], [789])
112 ->willReturnOnConsecutiveCalls(...$bookmarks)
113 ;
114 $this->container->bookmarkService
115 ->expects(static::exactly(3))
116 ->method('remove')
117 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
118 return [$bookmark, false];
119 }, $bookmarks))
120 ;
121 $this->container->bookmarkService->expects(static::once())->method('save');
122 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
123 $this->container->formatterFactory
124 ->expects(static::once())
125 ->method('getFormatter')
126 ->with('raw')
127 ->willReturnCallback(function () use ($bookmarks): BookmarkFormatter {
128 $formatter = $this->createMock(BookmarkFormatter::class);
129
130 $formatter
131 ->expects(static::exactly(3))
132 ->method('format')
133 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
134 return [$bookmark];
135 }, $bookmarks))
136 ->willReturnOnConsecutiveCalls(...array_map(function (Bookmark $bookmark): array {
137 return ['formatted' => $bookmark];
138 }, $bookmarks))
139 ;
140
141 return $formatter;
142 })
143 ;
144
145 // Make sure that PluginManager hook is triggered
146 $this->container->pluginManager
147 ->expects(static::exactly(3))
148 ->method('executeHooks')
149 ->with('delete_link')
150 ;
151
152 $result = $this->controller->deleteBookmark($request, $response);
153
154 static::assertSame(302, $result->getStatusCode());
155 static::assertSame(['/subfolder/'], $result->getHeader('location'));
156 }
157
158 /**
159 * Delete bookmark - Single bookmark not found in the data store
160 */
161 public function testDeleteSingleBookmarkNotFound(): void
162 {
163 $parameters = ['id' => '123'];
164
165 $request = $this->createMock(Request::class);
166 $request
167 ->method('getParam')
168 ->willReturnCallback(function (string $key) use ($parameters): ?string {
169 return $parameters[$key] ?? null;
170 })
171 ;
172 $response = new Response();
173
174 $this->container->bookmarkService
175 ->expects(static::once())
176 ->method('get')
177 ->willThrowException(new BookmarkNotFoundException())
178 ;
179 $this->container->bookmarkService->expects(static::never())->method('remove');
180 $this->container->bookmarkService->expects(static::never())->method('save');
181 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
182 $this->container->formatterFactory
183 ->expects(static::once())
184 ->method('getFormatter')
185 ->with('raw')
186 ->willReturnCallback(function (): BookmarkFormatter {
187 $formatter = $this->createMock(BookmarkFormatter::class);
188
189 $formatter->expects(static::never())->method('format');
190
191 return $formatter;
192 })
193 ;
194 // Make sure that PluginManager hook is not triggered
195 $this->container->pluginManager
196 ->expects(static::never())
197 ->method('executeHooks')
198 ->with('delete_link')
199 ;
200
201 $result = $this->controller->deleteBookmark($request, $response);
202
203 static::assertSame(302, $result->getStatusCode());
204 static::assertSame(['/subfolder/'], $result->getHeader('location'));
205 }
206
207 /**
208 * Delete bookmark - Multiple bookmarks with one not found in the data store
209 */
210 public function testDeleteMultipleBookmarksOneNotFound(): void
211 {
212 $parameters = ['id' => '123 456 789'];
213
214 $request = $this->createMock(Request::class);
215 $request
216 ->method('getParam')
217 ->willReturnCallback(function (string $key) use ($parameters): ?string {
218 return $parameters[$key] ?? null;
219 })
220 ;
221 $response = new Response();
222
223 $bookmarks = [
224 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123'),
225 (new Bookmark())->setId(789)->setUrl('http://domain.tld')->setTitle('Title 789'),
226 ];
227
228 $this->container->bookmarkService
229 ->expects(static::exactly(3))
230 ->method('get')
231 ->withConsecutive([123], [456], [789])
232 ->willReturnCallback(function (int $id) use ($bookmarks): Bookmark {
233 if ($id === 123) {
234 return $bookmarks[0];
235 }
236 if ($id === 789) {
237 return $bookmarks[1];
238 }
239 throw new BookmarkNotFoundException();
240 })
241 ;
242 $this->container->bookmarkService
243 ->expects(static::exactly(2))
244 ->method('remove')
245 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
246 return [$bookmark, false];
247 }, $bookmarks))
248 ;
249 $this->container->bookmarkService->expects(static::once())->method('save');
250 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
251 $this->container->formatterFactory
252 ->expects(static::once())
253 ->method('getFormatter')
254 ->with('raw')
255 ->willReturnCallback(function () use ($bookmarks): BookmarkFormatter {
256 $formatter = $this->createMock(BookmarkFormatter::class);
257
258 $formatter
259 ->expects(static::exactly(2))
260 ->method('format')
261 ->withConsecutive(...array_map(function (Bookmark $bookmark): array {
262 return [$bookmark];
263 }, $bookmarks))
264 ->willReturnOnConsecutiveCalls(...array_map(function (Bookmark $bookmark): array {
265 return ['formatted' => $bookmark];
266 }, $bookmarks))
267 ;
268
269 return $formatter;
270 })
271 ;
272
273 // Make sure that PluginManager hook is not triggered
274 $this->container->pluginManager
275 ->expects(static::exactly(2))
276 ->method('executeHooks')
277 ->with('delete_link')
278 ;
279
280 $this->container->sessionManager
281 ->expects(static::once())
282 ->method('setSessionParameter')
283 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 456 could not be found.'])
284 ;
285
286 $result = $this->controller->deleteBookmark($request, $response);
287
288 static::assertSame(302, $result->getStatusCode());
289 static::assertSame(['/subfolder/'], $result->getHeader('location'));
290 }
291
292 /**
293 * Delete bookmark - Invalid ID
294 */
295 public function testDeleteInvalidId(): void
296 {
297 $parameters = ['id' => 'nope not an ID'];
298
299 $request = $this->createMock(Request::class);
300 $request
301 ->method('getParam')
302 ->willReturnCallback(function (string $key) use ($parameters): ?string {
303 return $parameters[$key] ?? null;
304 })
305 ;
306 $response = new Response();
307
308 $this->container->sessionManager
309 ->expects(static::once())
310 ->method('setSessionParameter')
311 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
312 ;
313
314 $result = $this->controller->deleteBookmark($request, $response);
315
316 static::assertSame(302, $result->getStatusCode());
317 static::assertSame(['/subfolder/'], $result->getHeader('location'));
318 }
319
320 /**
321 * Delete bookmark - Empty ID
322 */
323 public function testDeleteEmptyId(): void
324 {
325 $request = $this->createMock(Request::class);
326 $response = new Response();
327
328 $this->container->sessionManager
329 ->expects(static::once())
330 ->method('setSessionParameter')
331 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
332 ;
333
334 $result = $this->controller->deleteBookmark($request, $response);
335
336 static::assertSame(302, $result->getStatusCode());
337 static::assertSame(['/subfolder/'], $result->getHeader('location'));
338 }
339
340 /**
341 * Delete bookmark - from bookmarklet
342 */
343 public function testDeleteBookmarkFromBookmarklet(): void
344 {
345 $parameters = [
346 'id' => '123',
347 'source' => 'bookmarklet',
348 ];
349
350 $request = $this->createMock(Request::class);
351 $request
352 ->method('getParam')
353 ->willReturnCallback(function (string $key) use ($parameters): ?string {
354 return $parameters[$key] ?? null;
355 })
356 ;
357 $response = new Response();
358
359 $this->container->bookmarkService->method('get')->with('123')->willReturn(
360 (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')
361 );
362
363 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
364 $this->container->formatterFactory
365 ->expects(static::once())
366 ->method('getFormatter')
367 ->willReturnCallback(function (): BookmarkFormatter {
368 $formatter = $this->createMock(BookmarkFormatter::class);
369 $formatter->method('format')->willReturn(['formatted']);
370
371 return $formatter;
372 })
373 ;
374
375 $result = $this->controller->deleteBookmark($request, $response);
376
377 static::assertSame(200, $result->getStatusCode());
378 static::assertSame('<script>self.close();</script>', (string) $result->getBody('location'));
379 }
380}
diff --git a/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php
new file mode 100644
index 00000000..b89206ce
--- /dev/null
+++ b/tests/front/controller/admin/ShaareManageControllerTest/PinBookmarkTest.php
@@ -0,0 +1,145 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ShaareManageController;
11use Shaarli\Http\HttpAccess;
12use Shaarli\Security\SessionManager;
13use Shaarli\TestCase;
14use Slim\Http\Request;
15use Slim\Http\Response;
16
17class PinBookmarkTest extends TestCase
18{
19 use FrontAdminControllerMockHelper;
20
21 /** @var ShaareManageController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->createContainer();
27
28 $this->container->httpAccess = $this->createMock(HttpAccess::class);
29 $this->controller = new ShaareManageController($this->container);
30 }
31
32 /**
33 * Test pin bookmark - with valid input
34 *
35 * @dataProvider initialStickyValuesProvider()
36 */
37 public function testPinBookmarkIsStickyNull(?bool $sticky, bool $expectedValue): void
38 {
39 $id = 123;
40
41 $request = $this->createMock(Request::class);
42 $response = new Response();
43
44 $bookmark = (new Bookmark())
45 ->setId(123)
46 ->setUrl('http://domain.tld')
47 ->setTitle('Title 123')
48 ->setSticky($sticky)
49 ;
50
51 $this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
52 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, true);
53
54 // Make sure that PluginManager hook is triggered
55 $this->container->pluginManager
56 ->expects(static::once())
57 ->method('executeHooks')
58 ->with('save_link')
59 ;
60
61 $result = $this->controller->pinBookmark($request, $response, ['id' => (string) $id]);
62
63 static::assertSame(302, $result->getStatusCode());
64 static::assertSame(['/subfolder/'], $result->getHeader('location'));
65
66 static::assertSame($expectedValue, $bookmark->isSticky());
67 }
68
69 public function initialStickyValuesProvider(): array
70 {
71 // [initialStickyState, isStickyAfterPin]
72 return [[null, true], [false, true], [true, false]];
73 }
74
75 /**
76 * Test pin bookmark - invalid bookmark ID
77 */
78 public function testDisplayEditFormInvalidId(): void
79 {
80 $id = 'invalid';
81
82 $request = $this->createMock(Request::class);
83 $response = new Response();
84
85 $this->container->sessionManager
86 ->expects(static::once())
87 ->method('setSessionParameter')
88 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier invalid could not be found.'])
89 ;
90
91 $result = $this->controller->pinBookmark($request, $response, ['id' => $id]);
92
93 static::assertSame(302, $result->getStatusCode());
94 static::assertSame(['/subfolder/'], $result->getHeader('location'));
95 }
96
97 /**
98 * Test pin bookmark - Bookmark ID not provided
99 */
100 public function testDisplayEditFormIdNotProvided(): void
101 {
102 $request = $this->createMock(Request::class);
103 $response = new Response();
104
105 $this->container->sessionManager
106 ->expects(static::once())
107 ->method('setSessionParameter')
108 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier could not be found.'])
109 ;
110
111 $result = $this->controller->pinBookmark($request, $response, []);
112
113 static::assertSame(302, $result->getStatusCode());
114 static::assertSame(['/subfolder/'], $result->getHeader('location'));
115 }
116
117 /**
118 * Test pin bookmark - bookmark not found
119 */
120 public function testDisplayEditFormBookmarkNotFound(): void
121 {
122 $id = 123;
123
124 $request = $this->createMock(Request::class);
125 $response = new Response();
126
127 $this->container->bookmarkService
128 ->expects(static::once())
129 ->method('get')
130 ->with($id)
131 ->willThrowException(new BookmarkNotFoundException())
132 ;
133
134 $this->container->sessionManager
135 ->expects(static::once())
136 ->method('setSessionParameter')
137 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 123 could not be found.'])
138 ;
139
140 $result = $this->controller->pinBookmark($request, $response, ['id' => (string) $id]);
141
142 static::assertSame(302, $result->getStatusCode());
143 static::assertSame(['/subfolder/'], $result->getHeader('location'));
144 }
145}
diff --git a/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php
new file mode 100644
index 00000000..ae61dfb7
--- /dev/null
+++ b/tests/front/controller/admin/ShaareManageControllerTest/SharePrivateTest.php
@@ -0,0 +1,139 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
9use Shaarli\Front\Controller\Admin\ShaareManageController;
10use Shaarli\Http\HttpAccess;
11use Shaarli\TestCase;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15/**
16 * Test GET /admin/shaare/private/{hash}
17 */
18class SharePrivateTest extends TestCase
19{
20 use FrontAdminControllerMockHelper;
21
22 /** @var ShaareManageController */
23 protected $controller;
24
25 public function setUp(): void
26 {
27 $this->createContainer();
28
29 $this->container->httpAccess = $this->createMock(HttpAccess::class);
30 $this->controller = new ShaareManageController($this->container);
31 }
32
33 /**
34 * Test shaare private with a private bookmark which does not have a key yet.
35 */
36 public function testSharePrivateWithNewPrivateBookmark(): void
37 {
38 $hash = 'abcdcef';
39 $request = $this->createMock(Request::class);
40 $response = new Response();
41
42 $bookmark = (new Bookmark())
43 ->setId(123)
44 ->setUrl('http://domain.tld')
45 ->setTitle('Title 123')
46 ->setPrivate(true)
47 ;
48
49 $this->container->bookmarkService
50 ->expects(static::once())
51 ->method('findByHash')
52 ->with($hash)
53 ->willReturn($bookmark)
54 ;
55 $this->container->bookmarkService
56 ->expects(static::once())
57 ->method('set')
58 ->with($bookmark, true)
59 ->willReturnCallback(function (Bookmark $bookmark): Bookmark {
60 static::assertSame(32, strlen($bookmark->getAdditionalContentEntry('private_key')));
61
62 return $bookmark;
63 })
64 ;
65
66 $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]);
67
68 static::assertSame(302, $result->getStatusCode());
69 static::assertRegExp('#/subfolder/shaare/' . $hash . '\?key=\w{32}#', $result->getHeaderLine('Location'));
70 }
71
72 /**
73 * Test shaare private with a private bookmark which does already have a key.
74 */
75 public function testSharePrivateWithExistingPrivateBookmark(): void
76 {
77 $hash = 'abcdcef';
78 $existingKey = 'this is a private key';
79 $request = $this->createMock(Request::class);
80 $response = new Response();
81
82 $bookmark = (new Bookmark())
83 ->setId(123)
84 ->setUrl('http://domain.tld')
85 ->setTitle('Title 123')
86 ->setPrivate(true)
87 ->addAdditionalContentEntry('private_key', $existingKey)
88 ;
89
90 $this->container->bookmarkService
91 ->expects(static::once())
92 ->method('findByHash')
93 ->with($hash)
94 ->willReturn($bookmark)
95 ;
96 $this->container->bookmarkService
97 ->expects(static::never())
98 ->method('set')
99 ;
100
101 $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]);
102
103 static::assertSame(302, $result->getStatusCode());
104 static::assertSame('/subfolder/shaare/' . $hash . '?key=' . $existingKey, $result->getHeaderLine('Location'));
105 }
106
107 /**
108 * Test shaare private with a public bookmark.
109 */
110 public function testSharePrivateWithPublicBookmark(): void
111 {
112 $hash = 'abcdcef';
113 $request = $this->createMock(Request::class);
114 $response = new Response();
115
116 $bookmark = (new Bookmark())
117 ->setId(123)
118 ->setUrl('http://domain.tld')
119 ->setTitle('Title 123')
120 ->setPrivate(false)
121 ;
122
123 $this->container->bookmarkService
124 ->expects(static::once())
125 ->method('findByHash')
126 ->with($hash)
127 ->willReturn($bookmark)
128 ;
129 $this->container->bookmarkService
130 ->expects(static::never())
131 ->method('set')
132 ;
133
134 $result = $this->controller->sharePrivate($request, $response, ['hash' => $hash]);
135
136 static::assertSame(302, $result->getStatusCode());
137 static::assertSame('/subfolder/shaare/' . $hash, $result->getHeaderLine('Location'));
138 }
139}
diff --git a/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php
new file mode 100644
index 00000000..ce8e112b
--- /dev/null
+++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateBatchFormTest.php
@@ -0,0 +1,63 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6
7use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
8use Shaarli\Front\Controller\Admin\ShaarePublishController;
9use Shaarli\Http\HttpAccess;
10use Shaarli\Http\MetadataRetriever;
11use Shaarli\TestCase;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class DisplayCreateBatchFormTest extends TestCase
16{
17 use FrontAdminControllerMockHelper;
18
19 /** @var ShaarePublishController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->container->httpAccess = $this->createMock(HttpAccess::class);
27 $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class);
28 $this->controller = new ShaarePublishController($this->container);
29 }
30
31 /**
32 * TODO
33 */
34 public function testDisplayCreateFormBatch(): void
35 {
36 $urls = [
37 'https://domain1.tld/url1',
38 'https://domain2.tld/url2',
39 ' ',
40 'https://domain3.tld/url3',
41 ];
42
43 $request = $this->createMock(Request::class);
44 $request->method('getParam')->willReturnCallback(function (string $key) use ($urls): ?string {
45 return $key === 'urls' ? implode(PHP_EOL, $urls) : null;
46 });
47 $response = new Response();
48
49 $assignedVariables = [];
50 $this->assignTemplateVars($assignedVariables);
51
52 $result = $this->controller->displayCreateBatchForms($request, $response);
53
54 static::assertSame(200, $result->getStatusCode());
55 static::assertSame('editlink.batch', (string) $result->getBody());
56
57 static::assertTrue($assignedVariables['batch_mode']);
58 static::assertCount(3, $assignedVariables['links']);
59 static::assertSame($urls[0], $assignedVariables['links'][0]['link']['url']);
60 static::assertSame($urls[1], $assignedVariables['links'][1]['link']['url']);
61 static::assertSame($urls[3], $assignedVariables['links'][2]['link']['url']);
62 }
63}
diff --git a/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php
new file mode 100644
index 00000000..f20b1def
--- /dev/null
+++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayCreateFormTest.php
@@ -0,0 +1,367 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ShaarePublishController;
11use Shaarli\Http\HttpAccess;
12use Shaarli\Http\MetadataRetriever;
13use Shaarli\TestCase;
14use Slim\Http\Request;
15use Slim\Http\Response;
16
17class DisplayCreateFormTest extends TestCase
18{
19 use FrontAdminControllerMockHelper;
20
21 /** @var ShaarePublishController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->createContainer();
27
28 $this->container->httpAccess = $this->createMock(HttpAccess::class);
29 $this->container->metadataRetriever = $this->createMock(MetadataRetriever::class);
30 $this->controller = new ShaarePublishController($this->container);
31 }
32
33 /**
34 * Test displaying bookmark create form
35 * Ensure that every step of the standard workflow works properly.
36 */
37 public function testDisplayCreateFormWithUrlAndWithMetadataRetrieval(): void
38 {
39 $this->container->environment = [
40 'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
41 ];
42
43 $assignedVariables = [];
44 $this->assignTemplateVars($assignedVariables);
45
46 $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
47 $expectedUrl = str_replace('&utm_ad=pay', '', $url);
48 $remoteTitle = 'Remote Title';
49 $remoteDesc = 'Sometimes the meta description is relevant.';
50 $remoteTags = 'abc def';
51
52 $request = $this->createMock(Request::class);
53 $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
54 return $key === 'post' ? $url : null;
55 });
56 $response = new Response();
57
58 $this->container->conf = $this->createMock(ConfigManager::class);
59 $this->container->conf->method('get')->willReturnCallback(function (string $param, $default) {
60 if ($param === 'general.enable_async_metadata') {
61 return false;
62 }
63
64 return $default;
65 });
66
67 $this->container->metadataRetriever->expects(static::once())->method('retrieve')->willReturn([
68 'title' => $remoteTitle,
69 'description' => $remoteDesc,
70 'tags' => $remoteTags,
71 ]);
72
73 $this->container->bookmarkService
74 ->expects(static::once())
75 ->method('bookmarksCountPerTag')
76 ->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
77 ;
78
79 // Make sure that PluginManager hook is triggered
80 $this->container->pluginManager
81 ->expects(static::atLeastOnce())
82 ->method('executeHooks')
83 ->withConsecutive(['render_editlink'], ['render_includes'])
84 ->willReturnCallback(function (string $hook, array $data) use ($remoteTitle, $remoteDesc): array {
85 if ('render_editlink' === $hook) {
86 static::assertSame($remoteTitle, $data['link']['title']);
87 static::assertSame($remoteDesc, $data['link']['description']);
88 }
89
90 return $data;
91 })
92 ;
93
94 $result = $this->controller->displayCreateForm($request, $response);
95
96 static::assertSame(200, $result->getStatusCode());
97 static::assertSame('editlink', (string) $result->getBody());
98
99 static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
100
101 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
102 static::assertSame($remoteTitle, $assignedVariables['link']['title']);
103 static::assertSame($remoteDesc, $assignedVariables['link']['description']);
104 static::assertSame($remoteTags, $assignedVariables['link']['tags']);
105 static::assertFalse($assignedVariables['link']['private']);
106
107 static::assertTrue($assignedVariables['link_is_new']);
108 static::assertSame($referer, $assignedVariables['http_referer']);
109 static::assertSame($tags, $assignedVariables['tags']);
110 static::assertArrayHasKey('source', $assignedVariables);
111 static::assertArrayHasKey('default_private_links', $assignedVariables);
112 static::assertArrayHasKey('async_metadata', $assignedVariables);
113 static::assertArrayHasKey('retrieve_description', $assignedVariables);
114 }
115
116 /**
117 * Test displaying bookmark create form without any external metadata retrieval attempt
118 */
119 public function testDisplayCreateFormWithUrlAndWithoutMetadata(): void
120 {
121 $this->container->environment = [
122 'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
123 ];
124
125 $assignedVariables = [];
126 $this->assignTemplateVars($assignedVariables);
127
128 $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
129 $expectedUrl = str_replace('&utm_ad=pay', '', $url);
130
131 $request = $this->createMock(Request::class);
132 $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
133 return $key === 'post' ? $url : null;
134 });
135 $response = new Response();
136
137 $this->container->metadataRetriever->expects(static::never())->method('retrieve');
138
139 $this->container->bookmarkService
140 ->expects(static::once())
141 ->method('bookmarksCountPerTag')
142 ->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
143 ;
144
145 // Make sure that PluginManager hook is triggered
146 $this->container->pluginManager
147 ->expects(static::atLeastOnce())
148 ->method('executeHooks')
149 ->withConsecutive(['render_editlink'], ['render_includes'])
150 ->willReturnCallback(function (string $hook, array $data): array {
151 if ('render_editlink' === $hook) {
152 static::assertSame('', $data['link']['title']);
153 static::assertSame('', $data['link']['description']);
154 }
155
156 return $data;
157 })
158 ;
159
160 $result = $this->controller->displayCreateForm($request, $response);
161
162 static::assertSame(200, $result->getStatusCode());
163 static::assertSame('editlink', (string) $result->getBody());
164
165 static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
166
167 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
168 static::assertSame('', $assignedVariables['link']['title']);
169 static::assertSame('', $assignedVariables['link']['description']);
170 static::assertSame('', $assignedVariables['link']['tags']);
171 static::assertFalse($assignedVariables['link']['private']);
172
173 static::assertTrue($assignedVariables['link_is_new']);
174 static::assertSame($referer, $assignedVariables['http_referer']);
175 static::assertSame($tags, $assignedVariables['tags']);
176 static::assertArrayHasKey('source', $assignedVariables);
177 static::assertArrayHasKey('default_private_links', $assignedVariables);
178 static::assertArrayHasKey('async_metadata', $assignedVariables);
179 static::assertArrayHasKey('retrieve_description', $assignedVariables);
180 }
181
182 /**
183 * Test displaying bookmark create form
184 * Ensure all available query parameters are handled properly.
185 */
186 public function testDisplayCreateFormWithFullParameters(): void
187 {
188 $assignedVariables = [];
189 $this->assignTemplateVars($assignedVariables);
190
191 $parameters = [
192 'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
193 'title' => 'Provided Title',
194 'description' => 'Provided description.',
195 'tags' => 'abc def',
196 'private' => '1',
197 'source' => 'apps',
198 ];
199 $expectedUrl = str_replace('&utm_ad=pay', '', $parameters['post']);
200
201 $request = $this->createMock(Request::class);
202 $request
203 ->method('getParam')
204 ->willReturnCallback(function (string $key) use ($parameters): ?string {
205 return $parameters[$key] ?? null;
206 });
207 $response = new Response();
208
209 $result = $this->controller->displayCreateForm($request, $response);
210
211 static::assertSame(200, $result->getStatusCode());
212 static::assertSame('editlink', (string) $result->getBody());
213
214 static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
215
216 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
217 static::assertSame($parameters['title'], $assignedVariables['link']['title']);
218 static::assertSame($parameters['description'], $assignedVariables['link']['description']);
219 static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
220 static::assertTrue($assignedVariables['link']['private']);
221 static::assertTrue($assignedVariables['link_is_new']);
222 static::assertSame($parameters['source'], $assignedVariables['source']);
223 }
224
225 /**
226 * Test displaying bookmark create form
227 * Without any parameter.
228 */
229 public function testDisplayCreateFormEmpty(): void
230 {
231 $assignedVariables = [];
232 $this->assignTemplateVars($assignedVariables);
233
234 $request = $this->createMock(Request::class);
235 $response = new Response();
236
237 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
238 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
239
240 $result = $this->controller->displayCreateForm($request, $response);
241
242 static::assertSame(200, $result->getStatusCode());
243 static::assertSame('editlink', (string) $result->getBody());
244 static::assertSame('', $assignedVariables['link']['url']);
245 static::assertSame('Note: ', $assignedVariables['link']['title']);
246 static::assertSame('', $assignedVariables['link']['description']);
247 static::assertSame('', $assignedVariables['link']['tags']);
248 static::assertFalse($assignedVariables['link']['private']);
249 static::assertTrue($assignedVariables['link_is_new']);
250 }
251
252 /**
253 * Test displaying bookmark create form
254 * URL not using HTTP protocol: do not try to retrieve the title
255 */
256 public function testDisplayCreateFormNotHttp(): void
257 {
258 $assignedVariables = [];
259 $this->assignTemplateVars($assignedVariables);
260
261 $url = 'magnet://kubuntu.torrent';
262 $request = $this->createMock(Request::class);
263 $request
264 ->method('getParam')
265 ->willReturnCallback(function (string $key) use ($url): ?string {
266 return $key === 'post' ? $url : null;
267 });
268 $response = new Response();
269
270 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
271 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
272
273 $result = $this->controller->displayCreateForm($request, $response);
274
275 static::assertSame(200, $result->getStatusCode());
276 static::assertSame('editlink', (string) $result->getBody());
277 static::assertSame($url, $assignedVariables['link']['url']);
278 static::assertTrue($assignedVariables['link_is_new']);
279 }
280
281 /**
282 * Test displaying bookmark create form
283 * When markdown formatter is enabled, the no markdown tag should be added to existing tags.
284 */
285 public function testDisplayCreateFormWithMarkdownEnabled(): void
286 {
287 $assignedVariables = [];
288 $this->assignTemplateVars($assignedVariables);
289
290 $this->container->conf = $this->createMock(ConfigManager::class);
291 $this->container->conf
292 ->expects(static::atLeastOnce())
293 ->method('get')->willReturnCallback(function (string $key): ?string {
294 if ($key === 'formatter') {
295 return 'markdown';
296 }
297
298 return $key;
299 })
300 ;
301
302 $request = $this->createMock(Request::class);
303 $response = new Response();
304
305 $result = $this->controller->displayCreateForm($request, $response);
306
307 static::assertSame(200, $result->getStatusCode());
308 static::assertSame('editlink', (string) $result->getBody());
309 static::assertSame(['nomarkdown' => 1], $assignedVariables['tags']);
310 }
311
312 /**
313 * Test displaying bookmark create form
314 * When an existing URL is submitted, we want to edit the existing link.
315 */
316 public function testDisplayCreateFormWithExistingUrl(): void
317 {
318 $assignedVariables = [];
319 $this->assignTemplateVars($assignedVariables);
320
321 $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
322 $expectedUrl = str_replace('&utm_ad=pay', '', $url);
323
324 $request = $this->createMock(Request::class);
325 $request
326 ->method('getParam')
327 ->willReturnCallback(function (string $key) use ($url): ?string {
328 return $key === 'post' ? $url : null;
329 });
330 $response = new Response();
331
332 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
333 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
334
335 $this->container->bookmarkService
336 ->expects(static::once())
337 ->method('findByUrl')
338 ->with($expectedUrl)
339 ->willReturn(
340 (new Bookmark())
341 ->setId($id = 23)
342 ->setUrl($expectedUrl)
343 ->setTitle($title = 'Bookmark Title')
344 ->setDescription($description = 'Bookmark description.')
345 ->setTags($tags = ['abc', 'def'])
346 ->setPrivate(true)
347 ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
348 )
349 ;
350
351 $result = $this->controller->displayCreateForm($request, $response);
352
353 static::assertSame(200, $result->getStatusCode());
354 static::assertSame('editlink', (string) $result->getBody());
355
356 static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
357 static::assertFalse($assignedVariables['link_is_new']);
358
359 static::assertSame($id, $assignedVariables['link']['id']);
360 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
361 static::assertSame($title, $assignedVariables['link']['title']);
362 static::assertSame($description, $assignedVariables['link']['description']);
363 static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
364 static::assertTrue($assignedVariables['link']['private']);
365 static::assertSame($createdAt, $assignedVariables['link']['created']);
366 }
367}
diff --git a/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php
new file mode 100644
index 00000000..da393e49
--- /dev/null
+++ b/tests/front/controller/admin/ShaarePublishControllerTest/DisplayEditFormTest.php
@@ -0,0 +1,155 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ShaarePublishController;
11use Shaarli\Http\HttpAccess;
12use Shaarli\Security\SessionManager;
13use Shaarli\TestCase;
14use Slim\Http\Request;
15use Slim\Http\Response;
16
17class DisplayEditFormTest extends TestCase
18{
19 use FrontAdminControllerMockHelper;
20
21 /** @var ShaarePublishController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->createContainer();
27
28 $this->container->httpAccess = $this->createMock(HttpAccess::class);
29 $this->controller = new ShaarePublishController($this->container);
30 }
31
32 /**
33 * Test displaying bookmark edit form
34 * When an existing ID is provided, ensure that default workflow works properly.
35 */
36 public function testDisplayEditFormDefault(): void
37 {
38 $assignedVariables = [];
39 $this->assignTemplateVars($assignedVariables);
40
41 $id = 11;
42
43 $request = $this->createMock(Request::class);
44 $response = new Response();
45
46 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
47 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
48
49 $this->container->bookmarkService
50 ->expects(static::once())
51 ->method('get')
52 ->with($id)
53 ->willReturn(
54 (new Bookmark())
55 ->setId($id)
56 ->setUrl($url = 'http://domain.tld')
57 ->setTitle($title = 'Bookmark Title')
58 ->setDescription($description = 'Bookmark description.')
59 ->setTags($tags = ['abc', 'def'])
60 ->setPrivate(true)
61 ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
62 )
63 ;
64
65 $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
66
67 static::assertSame(200, $result->getStatusCode());
68 static::assertSame('editlink', (string) $result->getBody());
69
70 static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
71 static::assertFalse($assignedVariables['link_is_new']);
72
73 static::assertSame($id, $assignedVariables['link']['id']);
74 static::assertSame($url, $assignedVariables['link']['url']);
75 static::assertSame($title, $assignedVariables['link']['title']);
76 static::assertSame($description, $assignedVariables['link']['description']);
77 static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
78 static::assertTrue($assignedVariables['link']['private']);
79 static::assertSame($createdAt, $assignedVariables['link']['created']);
80 }
81
82 /**
83 * Test displaying bookmark edit form
84 * Invalid ID provided.
85 */
86 public function testDisplayEditFormInvalidId(): void
87 {
88 $id = 'invalid';
89
90 $request = $this->createMock(Request::class);
91 $response = new Response();
92
93 $this->container->sessionManager
94 ->expects(static::once())
95 ->method('setSessionParameter')
96 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier invalid could not be found.'])
97 ;
98
99 $result = $this->controller->displayEditForm($request, $response, ['id' => $id]);
100
101 static::assertSame(302, $result->getStatusCode());
102 static::assertSame(['/subfolder/'], $result->getHeader('location'));
103 }
104
105 /**
106 * Test displaying bookmark edit form
107 * ID not provided.
108 */
109 public function testDisplayEditFormIdNotProvided(): void
110 {
111 $request = $this->createMock(Request::class);
112 $response = new Response();
113
114 $this->container->sessionManager
115 ->expects(static::once())
116 ->method('setSessionParameter')
117 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier could not be found.'])
118 ;
119
120 $result = $this->controller->displayEditForm($request, $response, []);
121
122 static::assertSame(302, $result->getStatusCode());
123 static::assertSame(['/subfolder/'], $result->getHeader('location'));
124 }
125
126 /**
127 * Test displaying bookmark edit form
128 * Bookmark not found.
129 */
130 public function testDisplayEditFormBookmarkNotFound(): void
131 {
132 $id = 123;
133
134 $request = $this->createMock(Request::class);
135 $response = new Response();
136
137 $this->container->bookmarkService
138 ->expects(static::once())
139 ->method('get')
140 ->with($id)
141 ->willThrowException(new BookmarkNotFoundException())
142 ;
143
144 $this->container->sessionManager
145 ->expects(static::once())
146 ->method('setSessionParameter')
147 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Bookmark with identifier 123 could not be found.'])
148 ;
149
150 $result = $this->controller->displayEditForm($request, $response, ['id' => (string) $id]);
151
152 static::assertSame(302, $result->getStatusCode());
153 static::assertSame(['/subfolder/'], $result->getHeader('location'));
154 }
155}
diff --git a/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php
new file mode 100644
index 00000000..b6a861bc
--- /dev/null
+++ b/tests/front/controller/admin/ShaarePublishControllerTest/SaveBookmarkTest.php
@@ -0,0 +1,369 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ShaarePublishControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ShaarePublishController;
11use Shaarli\Front\Exception\WrongTokenException;
12use Shaarli\Http\HttpAccess;
13use Shaarli\Security\SessionManager;
14use Shaarli\TestCase;
15use Shaarli\Thumbnailer;
16use Slim\Http\Request;
17use Slim\Http\Response;
18
19class SaveBookmarkTest extends TestCase
20{
21 use FrontAdminControllerMockHelper;
22
23 /** @var ShaarePublishController */
24 protected $controller;
25
26 public function setUp(): void
27 {
28 $this->createContainer();
29
30 $this->container->httpAccess = $this->createMock(HttpAccess::class);
31 $this->controller = new ShaarePublishController($this->container);
32 }
33
34 /**
35 * Test save a new bookmark
36 */
37 public function testSaveBookmark(): void
38 {
39 $id = 21;
40 $parameters = [
41 'lf_url' => 'http://url.tld/other?part=3#hash',
42 'lf_title' => 'Provided Title',
43 'lf_description' => 'Provided description.',
44 'lf_tags' => 'abc def',
45 'lf_private' => '1',
46 'returnurl' => 'http://shaarli/subfolder/admin/add-shaare'
47 ];
48
49 $request = $this->createMock(Request::class);
50 $request
51 ->method('getParam')
52 ->willReturnCallback(function (string $key) use ($parameters): ?string {
53 return $parameters[$key] ?? null;
54 })
55 ;
56 $response = new Response();
57
58 $checkBookmark = function (Bookmark $bookmark) use ($parameters) {
59 static::assertSame($parameters['lf_url'], $bookmark->getUrl());
60 static::assertSame($parameters['lf_title'], $bookmark->getTitle());
61 static::assertSame($parameters['lf_description'], $bookmark->getDescription());
62 static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
63 static::assertTrue($bookmark->isPrivate());
64 };
65
66 $this->container->bookmarkService
67 ->expects(static::once())
68 ->method('addOrSet')
69 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark {
70 static::assertFalse($save);
71
72 $checkBookmark($bookmark);
73
74 $bookmark->setId($id);
75
76 return $bookmark;
77 })
78 ;
79 $this->container->bookmarkService
80 ->expects(static::once())
81 ->method('set')
82 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark {
83 static::assertTrue($save);
84
85 $checkBookmark($bookmark);
86
87 static::assertSame($id, $bookmark->getId());
88
89 return $bookmark;
90 })
91 ;
92
93 // Make sure that PluginManager hook is triggered
94 $this->container->pluginManager
95 ->expects(static::atLeastOnce())
96 ->method('executeHooks')
97 ->withConsecutive(['save_link'])
98 ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
99 if ('save_link' === $hook) {
100 static::assertSame($id, $data['id']);
101 static::assertSame($parameters['lf_url'], $data['url']);
102 static::assertSame($parameters['lf_title'], $data['title']);
103 static::assertSame($parameters['lf_description'], $data['description']);
104 static::assertSame($parameters['lf_tags'], $data['tags']);
105 static::assertTrue($data['private']);
106 }
107
108 return $data;
109 })
110 ;
111
112 $result = $this->controller->save($request, $response);
113
114 static::assertSame(302, $result->getStatusCode());
115 static::assertRegExp('@/subfolder/#[\w\-]{6}@', $result->getHeader('location')[0]);
116 }
117
118
119 /**
120 * Test save an existing bookmark
121 */
122 public function testSaveExistingBookmark(): void
123 {
124 $id = 21;
125 $parameters = [
126 'lf_id' => (string) $id,
127 'lf_url' => 'http://url.tld/other?part=3#hash',
128 'lf_title' => 'Provided Title',
129 'lf_description' => 'Provided description.',
130 'lf_tags' => 'abc def',
131 'lf_private' => '1',
132 'returnurl' => 'http://shaarli/subfolder/?page=2'
133 ];
134
135 $request = $this->createMock(Request::class);
136 $request
137 ->method('getParam')
138 ->willReturnCallback(function (string $key) use ($parameters): ?string {
139 return $parameters[$key] ?? null;
140 })
141 ;
142 $response = new Response();
143
144 $checkBookmark = function (Bookmark $bookmark) use ($parameters, $id) {
145 static::assertSame($id, $bookmark->getId());
146 static::assertSame($parameters['lf_url'], $bookmark->getUrl());
147 static::assertSame($parameters['lf_title'], $bookmark->getTitle());
148 static::assertSame($parameters['lf_description'], $bookmark->getDescription());
149 static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
150 static::assertTrue($bookmark->isPrivate());
151 };
152
153 $this->container->bookmarkService->expects(static::atLeastOnce())->method('exists')->willReturn(true);
154 $this->container->bookmarkService
155 ->expects(static::once())
156 ->method('get')
157 ->willReturn((new Bookmark())->setId($id)->setUrl('http://other.url'))
158 ;
159 $this->container->bookmarkService
160 ->expects(static::once())
161 ->method('addOrSet')
162 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark {
163 static::assertFalse($save);
164
165 $checkBookmark($bookmark);
166
167 return $bookmark;
168 })
169 ;
170 $this->container->bookmarkService
171 ->expects(static::once())
172 ->method('set')
173 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): Bookmark {
174 static::assertTrue($save);
175
176 $checkBookmark($bookmark);
177
178 static::assertSame($id, $bookmark->getId());
179
180 return $bookmark;
181 })
182 ;
183
184 // Make sure that PluginManager hook is triggered
185 $this->container->pluginManager
186 ->expects(static::atLeastOnce())
187 ->method('executeHooks')
188 ->withConsecutive(['save_link'])
189 ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
190 if ('save_link' === $hook) {
191 static::assertSame($id, $data['id']);
192 static::assertSame($parameters['lf_url'], $data['url']);
193 static::assertSame($parameters['lf_title'], $data['title']);
194 static::assertSame($parameters['lf_description'], $data['description']);
195 static::assertSame($parameters['lf_tags'], $data['tags']);
196 static::assertTrue($data['private']);
197 }
198
199 return $data;
200 })
201 ;
202
203 $result = $this->controller->save($request, $response);
204
205 static::assertSame(302, $result->getStatusCode());
206 static::assertRegExp('@/subfolder/\?page=2#[\w\-]{6}@', $result->getHeader('location')[0]);
207 }
208
209 /**
210 * Test save a bookmark - try to retrieve the thumbnail
211 */
212 public function testSaveBookmarkWithThumbnailSync(): void
213 {
214 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
215
216 $request = $this->createMock(Request::class);
217 $request
218 ->method('getParam')
219 ->willReturnCallback(function (string $key) use ($parameters): ?string {
220 return $parameters[$key] ?? null;
221 })
222 ;
223 $response = new Response();
224
225 $this->container->conf = $this->createMock(ConfigManager::class);
226 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
227 if ($key === 'thumbnails.mode') {
228 return Thumbnailer::MODE_ALL;
229 } elseif ($key === 'general.enable_async_metadata') {
230 return false;
231 }
232
233 return $default;
234 });
235
236 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
237 $this->container->thumbnailer
238 ->expects(static::once())
239 ->method('get')
240 ->with($parameters['lf_url'])
241 ->willReturn($thumb = 'http://thumb.url')
242 ;
243
244 $this->container->bookmarkService
245 ->expects(static::once())
246 ->method('addOrSet')
247 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): Bookmark {
248 static::assertSame($thumb, $bookmark->getThumbnail());
249
250 return $bookmark;
251 })
252 ;
253
254 $result = $this->controller->save($request, $response);
255
256 static::assertSame(302, $result->getStatusCode());
257 }
258
259 /**
260 * Test save a bookmark - with ID #0
261 */
262 public function testSaveBookmarkWithIdZero(): void
263 {
264 $parameters = ['lf_id' => '0'];
265
266 $request = $this->createMock(Request::class);
267 $request
268 ->method('getParam')
269 ->willReturnCallback(function (string $key) use ($parameters): ?string {
270 return $parameters[$key] ?? null;
271 })
272 ;
273 $response = new Response();
274
275 $this->container->bookmarkService->expects(static::once())->method('exists')->with(0)->willReturn(true);
276 $this->container->bookmarkService->expects(static::once())->method('get')->with(0)->willReturn(new Bookmark());
277
278 $result = $this->controller->save($request, $response);
279
280 static::assertSame(302, $result->getStatusCode());
281 }
282
283 /**
284 * Test save a bookmark - do not attempt to retrieve thumbnails if async mode is enabled.
285 */
286 public function testSaveBookmarkWithThumbnailAsync(): void
287 {
288 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
289
290 $request = $this->createMock(Request::class);
291 $request
292 ->method('getParam')
293 ->willReturnCallback(function (string $key) use ($parameters): ?string {
294 return $parameters[$key] ?? null;
295 })
296 ;
297 $response = new Response();
298
299 $this->container->conf = $this->createMock(ConfigManager::class);
300 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
301 if ($key === 'thumbnails.mode') {
302 return Thumbnailer::MODE_ALL;
303 } elseif ($key === 'general.enable_async_metadata') {
304 return true;
305 }
306
307 return $default;
308 });
309
310 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
311 $this->container->thumbnailer->expects(static::never())->method('get');
312
313 $this->container->bookmarkService
314 ->expects(static::once())
315 ->method('addOrSet')
316 ->willReturnCallback(function (Bookmark $bookmark): Bookmark {
317 static::assertNull($bookmark->getThumbnail());
318
319 return $bookmark;
320 })
321 ;
322
323 $result = $this->controller->save($request, $response);
324
325 static::assertSame(302, $result->getStatusCode());
326 }
327
328 /**
329 * Change the password with a wrong existing password
330 */
331 public function testSaveBookmarkFromBookmarklet(): void
332 {
333 $parameters = ['source' => 'bookmarklet'];
334
335 $request = $this->createMock(Request::class);
336 $request
337 ->method('getParam')
338 ->willReturnCallback(function (string $key) use ($parameters): ?string {
339 return $parameters[$key] ?? null;
340 })
341 ;
342 $response = new Response();
343
344 $result = $this->controller->save($request, $response);
345
346 static::assertSame(200, $result->getStatusCode());
347 static::assertSame('<script>self.close();</script>', (string) $result->getBody());
348 }
349
350 /**
351 * Change the password with a wrong existing password
352 */
353 public function testSaveBookmarkWrongToken(): void
354 {
355 $this->container->sessionManager = $this->createMock(SessionManager::class);
356 $this->container->sessionManager->method('checkToken')->willReturn(false);
357
358 $this->container->bookmarkService->expects(static::never())->method('addOrSet');
359 $this->container->bookmarkService->expects(static::never())->method('set');
360
361 $request = $this->createMock(Request::class);
362 $response = new Response();
363
364 $this->expectException(WrongTokenException::class);
365
366 $this->controller->save($request, $response);
367 }
368
369}
diff --git a/tests/front/controller/admin/ShaarliAdminControllerTest.php b/tests/front/controller/admin/ShaarliAdminControllerTest.php
new file mode 100644
index 00000000..486d5d2d
--- /dev/null
+++ b/tests/front/controller/admin/ShaarliAdminControllerTest.php
@@ -0,0 +1,184 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Front\Exception\WrongTokenException;
8use Shaarli\Security\SessionManager;
9use Shaarli\TestCase;
10use Slim\Http\Request;
11
12/**
13 * Class ShaarliControllerTest
14 *
15 * This class is used to test default behavior of ShaarliAdminController abstract class.
16 * It uses a dummy non abstract controller.
17 */
18class ShaarliAdminControllerTest extends TestCase
19{
20 use FrontAdminControllerMockHelper;
21
22 /** @var ShaarliAdminController */
23 protected $controller;
24
25 public function setUp(): void
26 {
27 $this->createContainer();
28
29 $this->controller = new class($this->container) extends ShaarliAdminController
30 {
31 public function checkToken(Request $request): bool
32 {
33 return parent::checkToken($request);
34 }
35
36 public function saveSuccessMessage(string $message): void
37 {
38 parent::saveSuccessMessage($message);
39 }
40
41 public function saveWarningMessage(string $message): void
42 {
43 parent::saveWarningMessage($message);
44 }
45
46 public function saveErrorMessage(string $message): void
47 {
48 parent::saveErrorMessage($message);
49 }
50 };
51 }
52
53 /**
54 * Trigger controller's checkToken with a valid token.
55 */
56 public function testCheckTokenWithValidToken(): void
57 {
58 $request = $this->createMock(Request::class);
59 $request->method('getParam')->with('token')->willReturn($token = '12345');
60
61 $this->container->sessionManager = $this->createMock(SessionManager::class);
62 $this->container->sessionManager->method('checkToken')->with($token)->willReturn(true);
63
64 static::assertTrue($this->controller->checkToken($request));
65 }
66
67 /**
68 * Trigger controller's checkToken with na valid token should raise an exception.
69 */
70 public function testCheckTokenWithNotValidToken(): void
71 {
72 $request = $this->createMock(Request::class);
73 $request->method('getParam')->with('token')->willReturn($token = '12345');
74
75 $this->container->sessionManager = $this->createMock(SessionManager::class);
76 $this->container->sessionManager->method('checkToken')->with($token)->willReturn(false);
77
78 $this->expectException(WrongTokenException::class);
79
80 $this->controller->checkToken($request);
81 }
82
83 /**
84 * Test saveSuccessMessage() with a first message.
85 */
86 public function testSaveSuccessMessage(): void
87 {
88 $this->container->sessionManager
89 ->expects(static::once())
90 ->method('setSessionParameter')
91 ->with(SessionManager::KEY_SUCCESS_MESSAGES, [$message = 'bravo!'])
92 ;
93
94 $this->controller->saveSuccessMessage($message);
95 }
96
97 /**
98 * Test saveSuccessMessage() with existing messages.
99 */
100 public function testSaveSuccessMessageWithExistingMessages(): void
101 {
102 $this->container->sessionManager
103 ->expects(static::once())
104 ->method('getSessionParameter')
105 ->with(SessionManager::KEY_SUCCESS_MESSAGES)
106 ->willReturn(['success1', 'success2'])
107 ;
108 $this->container->sessionManager
109 ->expects(static::once())
110 ->method('setSessionParameter')
111 ->with(SessionManager::KEY_SUCCESS_MESSAGES, ['success1', 'success2', $message = 'bravo!'])
112 ;
113
114 $this->controller->saveSuccessMessage($message);
115 }
116
117 /**
118 * Test saveWarningMessage() with a first message.
119 */
120 public function testSaveWarningMessage(): void
121 {
122 $this->container->sessionManager
123 ->expects(static::once())
124 ->method('setSessionParameter')
125 ->with(SessionManager::KEY_WARNING_MESSAGES, [$message = 'warning!'])
126 ;
127
128 $this->controller->saveWarningMessage($message);
129 }
130
131 /**
132 * Test saveWarningMessage() with existing messages.
133 */
134 public function testSaveWarningMessageWithExistingMessages(): void
135 {
136 $this->container->sessionManager
137 ->expects(static::once())
138 ->method('getSessionParameter')
139 ->with(SessionManager::KEY_WARNING_MESSAGES)
140 ->willReturn(['warning1', 'warning2'])
141 ;
142 $this->container->sessionManager
143 ->expects(static::once())
144 ->method('setSessionParameter')
145 ->with(SessionManager::KEY_WARNING_MESSAGES, ['warning1', 'warning2', $message = 'warning!'])
146 ;
147
148 $this->controller->saveWarningMessage($message);
149 }
150
151 /**
152 * Test saveErrorMessage() with a first message.
153 */
154 public function testSaveErrorMessage(): void
155 {
156 $this->container->sessionManager
157 ->expects(static::once())
158 ->method('setSessionParameter')
159 ->with(SessionManager::KEY_ERROR_MESSAGES, [$message = 'error!'])
160 ;
161
162 $this->controller->saveErrorMessage($message);
163 }
164
165 /**
166 * Test saveErrorMessage() with existing messages.
167 */
168 public function testSaveErrorMessageWithExistingMessages(): void
169 {
170 $this->container->sessionManager
171 ->expects(static::once())
172 ->method('getSessionParameter')
173 ->with(SessionManager::KEY_ERROR_MESSAGES)
174 ->willReturn(['error1', 'error2'])
175 ;
176 $this->container->sessionManager
177 ->expects(static::once())
178 ->method('setSessionParameter')
179 ->with(SessionManager::KEY_ERROR_MESSAGES, ['error1', 'error2', $message = 'error!'])
180 ;
181
182 $this->controller->saveErrorMessage($message);
183 }
184}
diff --git a/tests/front/controller/admin/ThumbnailsControllerTest.php b/tests/front/controller/admin/ThumbnailsControllerTest.php
new file mode 100644
index 00000000..e5749654
--- /dev/null
+++ b/tests/front/controller/admin/ThumbnailsControllerTest.php
@@ -0,0 +1,156 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\TestCase;
10use Shaarli\Thumbnailer;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class ThumbnailsControllerTest extends TestCase
15{
16 use FrontAdminControllerMockHelper;
17
18 /** @var ThumbnailsController */
19 protected $controller;
20
21 public function setUp(): void
22 {
23 $this->createContainer();
24
25 $this->controller = new ThumbnailsController($this->container);
26 }
27
28 /**
29 * Test displaying the thumbnails update page
30 * Note that only non-note and HTTP bookmarks should be returned.
31 */
32 public function testIndex(): void
33 {
34 $assignedVariables = [];
35 $this->assignTemplateVars($assignedVariables);
36
37 $request = $this->createMock(Request::class);
38 $response = new Response();
39
40 $this->container->bookmarkService
41 ->expects(static::once())
42 ->method('search')
43 ->willReturn([
44 (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
45 (new Bookmark())->setId(2)->setUrl('?abcdef')->setTitle('Note 1'),
46 (new Bookmark())->setId(3)->setUrl('http://url2.tld')->setTitle('Title 2'),
47 (new Bookmark())->setId(4)->setUrl('ftp://domain.tld', ['ftp'])->setTitle('FTP'),
48 ])
49 ;
50
51 $result = $this->controller->index($request, $response);
52
53 static::assertSame(200, $result->getStatusCode());
54 static::assertSame('thumbnails', (string) $result->getBody());
55
56 static::assertSame('Thumbnails update - Shaarli', $assignedVariables['pagetitle']);
57 static::assertSame([1, 3], $assignedVariables['ids']);
58 }
59
60 /**
61 * Test updating a bookmark thumbnail with valid parameters
62 */
63 public function testAjaxUpdateValid(): void
64 {
65 $request = $this->createMock(Request::class);
66 $response = new Response();
67
68 $bookmark = (new Bookmark())
69 ->setId($id = 123)
70 ->setUrl($url = 'http://url1.tld')
71 ->setTitle('Title 1')
72 ->setThumbnail(false)
73 ;
74
75 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
76 $this->container->thumbnailer
77 ->expects(static::once())
78 ->method('get')
79 ->with($url)
80 ->willReturn($thumb = 'http://img.tld/pic.png')
81 ;
82
83 $this->container->bookmarkService
84 ->expects(static::once())
85 ->method('get')
86 ->with($id)
87 ->willReturn($bookmark)
88 ;
89 $this->container->bookmarkService
90 ->expects(static::once())
91 ->method('set')
92 ->willReturnCallback(function (Bookmark $bookmark) use ($thumb): Bookmark {
93 static::assertSame($thumb, $bookmark->getThumbnail());
94
95 return $bookmark;
96 })
97 ;
98
99 $result = $this->controller->ajaxUpdate($request, $response, ['id' => (string) $id]);
100
101 static::assertSame(200, $result->getStatusCode());
102
103 $payload = json_decode((string) $result->getBody(), true);
104
105 static::assertSame($id, $payload['id']);
106 static::assertSame($url, $payload['url']);
107 static::assertSame($thumb, $payload['thumbnail']);
108 }
109
110 /**
111 * Test updating a bookmark thumbnail - Invalid ID
112 */
113 public function testAjaxUpdateInvalidId(): void
114 {
115 $request = $this->createMock(Request::class);
116 $response = new Response();
117
118 $result = $this->controller->ajaxUpdate($request, $response, ['id' => 'nope']);
119
120 static::assertSame(400, $result->getStatusCode());
121 }
122
123 /**
124 * Test updating a bookmark thumbnail - No ID
125 */
126 public function testAjaxUpdateNoId(): void
127 {
128 $request = $this->createMock(Request::class);
129 $response = new Response();
130
131 $result = $this->controller->ajaxUpdate($request, $response, []);
132
133 static::assertSame(400, $result->getStatusCode());
134 }
135
136 /**
137 * Test updating a bookmark thumbnail with valid parameters
138 */
139 public function testAjaxUpdateBookmarkNotFound(): void
140 {
141 $id = 123;
142 $request = $this->createMock(Request::class);
143 $response = new Response();
144
145 $this->container->bookmarkService
146 ->expects(static::once())
147 ->method('get')
148 ->with($id)
149 ->willThrowException(new BookmarkNotFoundException())
150 ;
151
152 $result = $this->controller->ajaxUpdate($request, $response, ['id' => (string) $id]);
153
154 static::assertSame(404, $result->getStatusCode());
155 }
156}
diff --git a/tests/front/controller/admin/TokenControllerTest.php b/tests/front/controller/admin/TokenControllerTest.php
new file mode 100644
index 00000000..d2f0907f
--- /dev/null
+++ b/tests/front/controller/admin/TokenControllerTest.php
@@ -0,0 +1,41 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\TestCase;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11class TokenControllerTest extends TestCase
12{
13 use FrontAdminControllerMockHelper;
14
15 /** @var TokenController */
16 protected $controller;
17
18 public function setUp(): void
19 {
20 $this->createContainer();
21
22 $this->controller = new TokenController($this->container);
23 }
24
25 public function testGetToken(): void
26 {
27 $request = $this->createMock(Request::class);
28 $response = new Response();
29
30 $this->container->sessionManager
31 ->expects(static::once())
32 ->method('generateToken')
33 ->willReturn($token = 'token1234')
34 ;
35
36 $result = $this->controller->getToken($request, $response);
37
38 static::assertSame(200, $result->getStatusCode());
39 static::assertSame($token, (string) $result->getBody());
40 }
41}
diff --git a/tests/front/controller/admin/ToolsControllerTest.php b/tests/front/controller/admin/ToolsControllerTest.php
new file mode 100644
index 00000000..e82f8b14
--- /dev/null
+++ b/tests/front/controller/admin/ToolsControllerTest.php
@@ -0,0 +1,69 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\TestCase;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11class ToolsControllerTest extends TestCase
12{
13 use FrontAdminControllerMockHelper;
14
15 /** @var ToolsController */
16 protected $controller;
17
18 public function setUp(): void
19 {
20 $this->createContainer();
21
22 $this->controller = new ToolsController($this->container);
23 }
24
25 public function testDefaultInvokeWithHttps(): void
26 {
27 $request = $this->createMock(Request::class);
28 $response = new Response();
29
30 $this->container->environment = [
31 'SERVER_NAME' => 'shaarli',
32 'SERVER_PORT' => 443,
33 'HTTPS' => 'on',
34 ];
35
36 // Save RainTPL assigned variables
37 $assignedVariables = [];
38 $this->assignTemplateVars($assignedVariables);
39
40 $result = $this->controller->index($request, $response);
41
42 static::assertSame(200, $result->getStatusCode());
43 static::assertSame('tools', (string) $result->getBody());
44 static::assertSame('https://shaarli/', $assignedVariables['pageabsaddr']);
45 static::assertTrue($assignedVariables['sslenabled']);
46 }
47
48 public function testDefaultInvokeWithoutHttps(): void
49 {
50 $request = $this->createMock(Request::class);
51 $response = new Response();
52
53 $this->container->environment = [
54 'SERVER_NAME' => 'shaarli',
55 'SERVER_PORT' => 80,
56 ];
57
58 // Save RainTPL assigned variables
59 $assignedVariables = [];
60 $this->assignTemplateVars($assignedVariables);
61
62 $result = $this->controller->index($request, $response);
63
64 static::assertSame(200, $result->getStatusCode());
65 static::assertSame('tools', (string) $result->getBody());
66 static::assertSame('http://shaarli/', $assignedVariables['pageabsaddr']);
67 static::assertFalse($assignedVariables['sslenabled']);
68 }
69}
diff --git a/tests/front/controller/visitor/BookmarkListControllerTest.php b/tests/front/controller/visitor/BookmarkListControllerTest.php
new file mode 100644
index 00000000..5cbc8c73
--- /dev/null
+++ b/tests/front/controller/visitor/BookmarkListControllerTest.php
@@ -0,0 +1,532 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Security\LoginManager;
11use Shaarli\TestCase;
12use Shaarli\Thumbnailer;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class BookmarkListControllerTest extends TestCase
17{
18 use FrontControllerMockHelper;
19
20 /** @var BookmarkListController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->controller = new BookmarkListController($this->container);
28 }
29
30 /**
31 * Test rendering list of bookmarks with default parameters (first page).
32 */
33 public function testIndexDefaultFirstPage(): void
34 {
35 $assignedVariables = [];
36 $this->assignTemplateVars($assignedVariables);
37
38 $request = $this->createMock(Request::class);
39 $response = new Response();
40
41 $this->container->bookmarkService
42 ->expects(static::once())
43 ->method('search')
44 ->with(
45 ['searchtags' => '', 'searchterm' => ''],
46 null,
47 false,
48 false
49 )
50 ->willReturn([
51 (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
52 (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
53 (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
54 ]
55 );
56
57 $this->container->sessionManager
58 ->method('getSessionParameter')
59 ->willReturnCallback(function (string $parameter, $default = null) {
60 if ('LINKS_PER_PAGE' === $parameter) {
61 return 2;
62 }
63
64 return $default;
65 })
66 ;
67
68 $result = $this->controller->index($request, $response);
69
70 static::assertSame(200, $result->getStatusCode());
71 static::assertSame('linklist', (string) $result->getBody());
72
73 static::assertSame('Shaarli', $assignedVariables['pagetitle']);
74 static::assertSame('?page=2', $assignedVariables['previous_page_url']);
75 static::assertSame('', $assignedVariables['next_page_url']);
76 static::assertSame(2, $assignedVariables['page_max']);
77 static::assertSame('', $assignedVariables['search_tags']);
78 static::assertSame(3, $assignedVariables['result_count']);
79 static::assertSame(1, $assignedVariables['page_current']);
80 static::assertSame('', $assignedVariables['search_term']);
81 static::assertNull($assignedVariables['visibility']);
82 static::assertCount(2, $assignedVariables['links']);
83
84 $link = $assignedVariables['links'][0];
85
86 static::assertSame(1, $link['id']);
87 static::assertSame('http://url1.tld', $link['url']);
88 static::assertSame('Title 1', $link['title']);
89
90 $link = $assignedVariables['links'][1];
91
92 static::assertSame(2, $link['id']);
93 static::assertSame('http://url2.tld', $link['url']);
94 static::assertSame('Title 2', $link['title']);
95 }
96
97 /**
98 * Test rendering list of bookmarks with default parameters (second page).
99 */
100 public function testIndexDefaultSecondPage(): void
101 {
102 $assignedVariables = [];
103 $this->assignTemplateVars($assignedVariables);
104
105 $request = $this->createMock(Request::class);
106 $request->method('getParam')->willReturnCallback(function (string $key) {
107 if ('page' === $key) {
108 return '2';
109 }
110
111 return null;
112 });
113 $response = new Response();
114
115 $this->container->bookmarkService
116 ->expects(static::once())
117 ->method('search')
118 ->with(
119 ['searchtags' => '', 'searchterm' => ''],
120 null,
121 false,
122 false
123 )
124 ->willReturn([
125 (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
126 (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
127 (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
128 ])
129 ;
130
131 $this->container->sessionManager
132 ->method('getSessionParameter')
133 ->willReturnCallback(function (string $parameter, $default = null) {
134 if ('LINKS_PER_PAGE' === $parameter) {
135 return 2;
136 }
137
138 return $default;
139 })
140 ;
141
142 $result = $this->controller->index($request, $response);
143
144 static::assertSame(200, $result->getStatusCode());
145 static::assertSame('linklist', (string) $result->getBody());
146
147 static::assertSame('Shaarli', $assignedVariables['pagetitle']);
148 static::assertSame('', $assignedVariables['previous_page_url']);
149 static::assertSame('?page=1', $assignedVariables['next_page_url']);
150 static::assertSame(2, $assignedVariables['page_max']);
151 static::assertSame('', $assignedVariables['search_tags']);
152 static::assertSame(3, $assignedVariables['result_count']);
153 static::assertSame(2, $assignedVariables['page_current']);
154 static::assertSame('', $assignedVariables['search_term']);
155 static::assertNull($assignedVariables['visibility']);
156 static::assertCount(1, $assignedVariables['links']);
157
158 $link = $assignedVariables['links'][2];
159
160 static::assertSame(3, $link['id']);
161 static::assertSame('http://url3.tld', $link['url']);
162 static::assertSame('Title 3', $link['title']);
163 }
164
165 /**
166 * Test rendering list of bookmarks with filters.
167 */
168 public function testIndexDefaultWithFilters(): void
169 {
170 $assignedVariables = [];
171 $this->assignTemplateVars($assignedVariables);
172
173 $request = $this->createMock(Request::class);
174 $request->method('getParam')->willReturnCallback(function (string $key) {
175 if ('searchtags' === $key) {
176 return 'abc def';
177 }
178 if ('searchterm' === $key) {
179 return 'ghi jkl';
180 }
181
182 return null;
183 });
184 $response = new Response();
185
186 $this->container->sessionManager
187 ->method('getSessionParameter')
188 ->willReturnCallback(function (string $key, $default) {
189 if ('LINKS_PER_PAGE' === $key) {
190 return 2;
191 }
192 if ('visibility' === $key) {
193 return 'private';
194 }
195 if ('untaggedonly' === $key) {
196 return true;
197 }
198
199 return $default;
200 })
201 ;
202
203 $this->container->bookmarkService
204 ->expects(static::once())
205 ->method('search')
206 ->with(
207 ['searchtags' => 'abc def', 'searchterm' => 'ghi jkl'],
208 'private',
209 false,
210 true
211 )
212 ->willReturn([
213 (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
214 (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
215 (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
216 ])
217 ;
218
219 $result = $this->controller->index($request, $response);
220
221 static::assertSame(200, $result->getStatusCode());
222 static::assertSame('linklist', (string) $result->getBody());
223
224 static::assertSame('Search: ghi jkl [abc] [def] - Shaarli', $assignedVariables['pagetitle']);
225 static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc+def', $assignedVariables['previous_page_url']);
226 }
227
228 /**
229 * Test displaying a permalink with valid parameters
230 */
231 public function testPermalinkValid(): void
232 {
233 $hash = 'abcdef';
234
235 $assignedVariables = [];
236 $this->assignTemplateVars($assignedVariables);
237
238 $request = $this->createMock(Request::class);
239 $response = new Response();
240
241 $this->container->bookmarkService
242 ->expects(static::once())
243 ->method('findByHash')
244 ->with($hash)
245 ->willReturn((new Bookmark())->setId(123)->setTitle('Title 1')->setUrl('http://url1.tld'))
246 ;
247
248 $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
249
250 static::assertSame(200, $result->getStatusCode());
251 static::assertSame('linklist', (string) $result->getBody());
252
253 static::assertSame('Title 1 - Shaarli', $assignedVariables['pagetitle']);
254 static::assertCount(1, $assignedVariables['links']);
255
256 $link = $assignedVariables['links'][0];
257
258 static::assertSame(123, $link['id']);
259 static::assertSame('http://url1.tld', $link['url']);
260 static::assertSame('Title 1', $link['title']);
261 }
262
263 /**
264 * Test displaying a permalink with an unknown small hash : renders a 404 template error
265 */
266 public function testPermalinkNotFound(): void
267 {
268 $hash = 'abcdef';
269
270 $assignedVariables = [];
271 $this->assignTemplateVars($assignedVariables);
272
273 $request = $this->createMock(Request::class);
274 $response = new Response();
275
276 $this->container->bookmarkService
277 ->expects(static::once())
278 ->method('findByHash')
279 ->with($hash)
280 ->willThrowException(new BookmarkNotFoundException())
281 ;
282
283 $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
284
285 static::assertSame(200, $result->getStatusCode());
286 static::assertSame('404', (string) $result->getBody());
287
288 static::assertSame(
289 'The link you are trying to reach does not exist or has been deleted.',
290 $assignedVariables['error_message']
291 );
292 }
293
294 /**
295 * Test GET /shaare/{hash}?key={key} - Find a link by hash using a private link.
296 */
297 public function testPermalinkWithPrivateKey(): void
298 {
299 $hash = 'abcdef';
300 $privateKey = 'this is a private key';
301
302 $assignedVariables = [];
303 $this->assignTemplateVars($assignedVariables);
304
305 $request = $this->createMock(Request::class);
306 $request->method('getParam')->willReturnCallback(function (string $key, $default = null) use ($privateKey) {
307 return $key === 'key' ? $privateKey : $default;
308 });
309 $response = new Response();
310
311 $this->container->bookmarkService
312 ->expects(static::once())
313 ->method('findByHash')
314 ->with($hash, $privateKey)
315 ->willReturn((new Bookmark())->setId(123)->setTitle('Title 1')->setUrl('http://url1.tld'))
316 ;
317
318 $result = $this->controller->permalink($request, $response, ['hash' => $hash]);
319
320 static::assertSame(200, $result->getStatusCode());
321 static::assertSame('linklist', (string) $result->getBody());
322 static::assertCount(1, $assignedVariables['links']);
323 }
324
325 /**
326 * Test getting link list with thumbnail updates.
327 * -> 2 thumbnails update, only 1 datastore write
328 */
329 public function testThumbnailUpdateFromLinkList(): void
330 {
331 $request = $this->createMock(Request::class);
332 $response = new Response();
333
334 $this->container->loginManager = $this->createMock(LoginManager::class);
335 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
336
337 $this->container->conf = $this->createMock(ConfigManager::class);
338 $this->container->conf
339 ->method('get')
340 ->willReturnCallback(function (string $key, $default) {
341 if ($key === 'thumbnails.mode') {
342 return Thumbnailer::MODE_ALL;
343 } elseif ($key === 'general.enable_async_metadata') {
344 return false;
345 }
346
347 return $default;
348 })
349 ;
350
351 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
352 $this->container->thumbnailer
353 ->expects(static::exactly(2))
354 ->method('get')
355 ->withConsecutive(['https://url2.tld'], ['https://url4.tld'])
356 ;
357
358 $this->container->bookmarkService
359 ->expects(static::once())
360 ->method('search')
361 ->willReturn([
362 (new Bookmark())->setId(1)->setUrl('https://url1.tld')->setTitle('Title 1')->setThumbnail(false),
363 $b1 = (new Bookmark())->setId(2)->setUrl('https://url2.tld')->setTitle('Title 2'),
364 (new Bookmark())->setId(3)->setUrl('https://url3.tld')->setTitle('Title 3')->setThumbnail(false),
365 $b2 = (new Bookmark())->setId(2)->setUrl('https://url4.tld')->setTitle('Title 4'),
366 (new Bookmark())->setId(2)->setUrl('ftp://url5.tld', ['ftp'])->setTitle('Title 5'),
367 ])
368 ;
369 $this->container->bookmarkService
370 ->expects(static::exactly(2))
371 ->method('set')
372 ->withConsecutive([$b1, false], [$b2, false])
373 ;
374 $this->container->bookmarkService->expects(static::once())->method('save');
375
376 $result = $this->controller->index($request, $response);
377
378 static::assertSame(200, $result->getStatusCode());
379 static::assertSame('linklist', (string) $result->getBody());
380 }
381
382 /**
383 * Test getting a permalink with thumbnail update.
384 */
385 public function testThumbnailUpdateFromPermalink(): void
386 {
387 $request = $this->createMock(Request::class);
388 $response = new Response();
389
390 $this->container->loginManager = $this->createMock(LoginManager::class);
391 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
392
393 $this->container->conf = $this->createMock(ConfigManager::class);
394 $this->container->conf
395 ->method('get')
396 ->willReturnCallback(function (string $key, $default) {
397 if ($key === 'thumbnails.mode') {
398 return Thumbnailer::MODE_ALL;
399 } elseif ($key === 'general.enable_async_metadata') {
400 return false;
401 }
402
403 return $default;
404 })
405 ;
406
407 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
408 $this->container->thumbnailer->expects(static::once())->method('get')->withConsecutive(['https://url.tld']);
409
410 $this->container->bookmarkService
411 ->expects(static::once())
412 ->method('findByHash')
413 ->willReturn($bookmark = (new Bookmark())->setId(2)->setUrl('https://url.tld')->setTitle('Title 1'))
414 ;
415 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, true);
416 $this->container->bookmarkService->expects(static::never())->method('save');
417
418 $result = $this->controller->permalink($request, $response, ['hash' => 'abc']);
419
420 static::assertSame(200, $result->getStatusCode());
421 static::assertSame('linklist', (string) $result->getBody());
422 }
423
424 /**
425 * Test getting a permalink with thumbnail update with async setting: no update should run.
426 */
427 public function testThumbnailUpdateFromPermalinkAsync(): void
428 {
429 $request = $this->createMock(Request::class);
430 $response = new Response();
431
432 $this->container->loginManager = $this->createMock(LoginManager::class);
433 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
434
435 $this->container->conf = $this->createMock(ConfigManager::class);
436 $this->container->conf
437 ->method('get')
438 ->willReturnCallback(function (string $key, $default) {
439 if ($key === 'thumbnails.mode') {
440 return Thumbnailer::MODE_ALL;
441 } elseif ($key === 'general.enable_async_metadata') {
442 return true;
443 }
444
445 return $default;
446 })
447 ;
448
449 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
450 $this->container->thumbnailer->expects(static::never())->method('get');
451
452 $this->container->bookmarkService
453 ->expects(static::once())
454 ->method('findByHash')
455 ->willReturn((new Bookmark())->setId(2)->setUrl('https://url.tld')->setTitle('Title 1'))
456 ;
457 $this->container->bookmarkService->expects(static::never())->method('set');
458 $this->container->bookmarkService->expects(static::never())->method('save');
459
460 $result = $this->controller->permalink($request, $response, ['hash' => 'abc']);
461
462 static::assertSame(200, $result->getStatusCode());
463 }
464
465 /**
466 * Trigger legacy controller in link list controller: permalink
467 */
468 public function testLegacyControllerPermalink(): void
469 {
470 $hash = 'abcdef';
471 $this->container->environment['QUERY_STRING'] = $hash;
472
473 $request = $this->createMock(Request::class);
474 $response = new Response();
475
476 $result = $this->controller->index($request, $response);
477
478 static::assertSame(302, $result->getStatusCode());
479 static::assertSame('/subfolder/shaare/' . $hash, $result->getHeader('location')[0]);
480 }
481
482 /**
483 * Trigger legacy controller in link list controller: ?do= query parameter
484 */
485 public function testLegacyControllerDoPage(): void
486 {
487 $request = $this->createMock(Request::class);
488 $request->method('getQueryParam')->with('do')->willReturn('picwall');
489 $response = new Response();
490
491 $result = $this->controller->index($request, $response);
492
493 static::assertSame(302, $result->getStatusCode());
494 static::assertSame('/subfolder/picture-wall', $result->getHeader('location')[0]);
495 }
496
497 /**
498 * Trigger legacy controller in link list controller: ?do= query parameter with unknown legacy route
499 */
500 public function testLegacyControllerUnknownDoPage(): void
501 {
502 $request = $this->createMock(Request::class);
503 $request->method('getQueryParam')->with('do')->willReturn('nope');
504 $response = new Response();
505
506 $result = $this->controller->index($request, $response);
507
508 static::assertSame(200, $result->getStatusCode());
509 static::assertSame('linklist', (string) $result->getBody());
510 }
511
512 /**
513 * Trigger legacy controller in link list controller: other GET route (e.g. ?post)
514 */
515 public function testLegacyControllerGetParameter(): void
516 {
517 $request = $this->createMock(Request::class);
518 $request->method('getQueryParams')->willReturn(['post' => $url = 'http://url.tld']);
519 $response = new Response();
520
521 $this->container->loginManager = $this->createMock(LoginManager::class);
522 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
523
524 $result = $this->controller->index($request, $response);
525
526 static::assertSame(302, $result->getStatusCode());
527 static::assertSame(
528 '/subfolder/admin/shaare?post=' . urlencode($url),
529 $result->getHeader('location')[0]
530 );
531 }
532}
diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php
new file mode 100644
index 00000000..758e7219
--- /dev/null
+++ b/tests/front/controller/visitor/DailyControllerTest.php
@@ -0,0 +1,716 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Feed\CachedPage;
9use Shaarli\TestCase;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13class DailyControllerTest extends TestCase
14{
15 use FrontControllerMockHelper;
16
17 /** @var DailyController */
18 protected $controller;
19
20 public function setUp(): void
21 {
22 $this->createContainer();
23
24 $this->controller = new DailyController($this->container);
25 DailyController::$DAILY_RSS_NB_DAYS = 2;
26 }
27
28 public function testValidIndexControllerInvokeDefault(): void
29 {
30 $currentDay = new \DateTimeImmutable('2020-05-13');
31 $previousDate = new \DateTime('2 days ago 00:00:00');
32 $nextDate = new \DateTime('today 00:00:00');
33
34 $request = $this->createMock(Request::class);
35 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
36 return $key === 'day' ? $currentDay->format('Ymd') : null;
37 });
38 $response = new Response();
39
40 // Save RainTPL assigned variables
41 $assignedVariables = [];
42 $this->assignTemplateVars($assignedVariables);
43
44 $this->container->bookmarkService
45 ->expects(static::once())
46 ->method('findByDate')
47 ->willReturnCallback(
48 function ($from, $to, &$previous, &$next) use ($currentDay, $previousDate, $nextDate): array {
49 $previous = $previousDate;
50 $next = $nextDate;
51
52 return [
53 (new Bookmark())
54 ->setId(1)
55 ->setUrl('http://url.tld')
56 ->setTitle(static::generateString(50))
57 ->setDescription(static::generateString(500))
58 ,
59 (new Bookmark())
60 ->setId(2)
61 ->setUrl('http://url2.tld')
62 ->setTitle(static::generateString(50))
63 ->setDescription(static::generateString(500))
64 ,
65 (new Bookmark())
66 ->setId(3)
67 ->setUrl('http://url3.tld')
68 ->setTitle(static::generateString(50))
69 ->setDescription(static::generateString(500))
70 ,
71 ];
72 }
73 )
74 ;
75
76 // Make sure that PluginManager hook is triggered
77 $this->container->pluginManager
78 ->expects(static::atLeastOnce())
79 ->method('executeHooks')
80 ->withConsecutive(['render_daily'])
81 ->willReturnCallback(
82 function (string $hook, array $data, array $param) use ($currentDay, $previousDate, $nextDate): array {
83 if ('render_daily' === $hook) {
84 static::assertArrayHasKey('linksToDisplay', $data);
85 static::assertCount(3, $data['linksToDisplay']);
86 static::assertSame(1, $data['linksToDisplay'][0]['id']);
87 static::assertSame($currentDay->getTimestamp(), $data['day']);
88 static::assertSame($previousDate->format('Ymd'), $data['previousday']);
89 static::assertSame($nextDate->format('Ymd'), $data['nextday']);
90
91 static::assertArrayHasKey('loggedin', $param);
92 }
93
94 return $data;
95 }
96 )
97 ;
98
99 $result = $this->controller->index($request, $response);
100
101 static::assertSame(200, $result->getStatusCode());
102 static::assertSame('daily', (string) $result->getBody());
103 static::assertSame(
104 'Daily - '. format_date($currentDay, false, true) .' - Shaarli',
105 $assignedVariables['pagetitle']
106 );
107 static::assertEquals($currentDay, $assignedVariables['dayDate']);
108 static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']);
109 static::assertSame($previousDate->format('Ymd'), $assignedVariables['previousday']);
110 static::assertSame($nextDate->format('Ymd'), $assignedVariables['nextday']);
111 static::assertSame('day', $assignedVariables['type']);
112 static::assertSame('May 13, 2020', $assignedVariables['dayDesc']);
113 static::assertSame('Daily', $assignedVariables['localizedType']);
114 static::assertCount(3, $assignedVariables['linksToDisplay']);
115
116 $link = $assignedVariables['linksToDisplay'][0];
117
118 static::assertSame(1, $link['id']);
119 static::assertSame('http://url.tld', $link['url']);
120 static::assertNotEmpty($link['title']);
121 static::assertNotEmpty($link['description']);
122 static::assertNotEmpty($link['formatedDescription']);
123
124 $link = $assignedVariables['linksToDisplay'][1];
125
126 static::assertSame(2, $link['id']);
127 static::assertSame('http://url2.tld', $link['url']);
128 static::assertNotEmpty($link['title']);
129 static::assertNotEmpty($link['description']);
130 static::assertNotEmpty($link['formatedDescription']);
131
132 $link = $assignedVariables['linksToDisplay'][2];
133
134 static::assertSame(3, $link['id']);
135 static::assertSame('http://url3.tld', $link['url']);
136 static::assertNotEmpty($link['title']);
137 static::assertNotEmpty($link['description']);
138 static::assertNotEmpty($link['formatedDescription']);
139
140 static::assertCount(3, $assignedVariables['cols']);
141 static::assertCount(1, $assignedVariables['cols'][0]);
142 static::assertCount(1, $assignedVariables['cols'][1]);
143 static::assertCount(1, $assignedVariables['cols'][2]);
144
145 $link = $assignedVariables['cols'][0][0];
146
147 static::assertSame(1, $link['id']);
148 static::assertSame('http://url.tld', $link['url']);
149 static::assertNotEmpty($link['title']);
150 static::assertNotEmpty($link['description']);
151 static::assertNotEmpty($link['formatedDescription']);
152
153 $link = $assignedVariables['cols'][1][0];
154
155 static::assertSame(2, $link['id']);
156 static::assertSame('http://url2.tld', $link['url']);
157 static::assertNotEmpty($link['title']);
158 static::assertNotEmpty($link['description']);
159 static::assertNotEmpty($link['formatedDescription']);
160
161 $link = $assignedVariables['cols'][2][0];
162
163 static::assertSame(3, $link['id']);
164 static::assertSame('http://url3.tld', $link['url']);
165 static::assertNotEmpty($link['title']);
166 static::assertNotEmpty($link['description']);
167 static::assertNotEmpty($link['formatedDescription']);
168 }
169
170 /**
171 * Daily page - test that everything goes fine with no future or past bookmarks
172 */
173 public function testValidIndexControllerInvokeNoFutureOrPast(): void
174 {
175 $currentDay = new \DateTimeImmutable('2020-05-13');
176
177 $request = $this->createMock(Request::class);
178 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
179 return $key === 'day' ? $currentDay->format('Ymd') : null;
180 });
181 $response = new Response();
182
183 // Save RainTPL assigned variables
184 $assignedVariables = [];
185 $this->assignTemplateVars($assignedVariables);
186
187 $this->container->bookmarkService
188 ->expects(static::once())
189 ->method('findByDate')
190 ->willReturnCallback(function () use ($currentDay): array {
191 return [
192 (new Bookmark())
193 ->setId(1)
194 ->setUrl('http://url.tld')
195 ->setTitle(static::generateString(50))
196 ->setDescription(static::generateString(500))
197 ,
198 ];
199 })
200 ;
201
202 // Make sure that PluginManager hook is triggered
203 $this->container->pluginManager
204 ->expects(static::atLeastOnce())
205 ->method('executeHooks')
206 ->withConsecutive(['render_daily'])
207 ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array {
208 if ('render_daily' === $hook) {
209 static::assertArrayHasKey('linksToDisplay', $data);
210 static::assertCount(1, $data['linksToDisplay']);
211 static::assertSame(1, $data['linksToDisplay'][0]['id']);
212 static::assertSame($currentDay->getTimestamp(), $data['day']);
213 static::assertEmpty($data['previousday']);
214 static::assertEmpty($data['nextday']);
215
216 static::assertArrayHasKey('loggedin', $param);
217 }
218
219 return $data;
220 });
221
222 $result = $this->controller->index($request, $response);
223
224 static::assertSame(200, $result->getStatusCode());
225 static::assertSame('daily', (string) $result->getBody());
226 static::assertSame(
227 'Daily - '. format_date($currentDay, false, true) .' - Shaarli',
228 $assignedVariables['pagetitle']
229 );
230 static::assertCount(1, $assignedVariables['linksToDisplay']);
231
232 $link = $assignedVariables['linksToDisplay'][0];
233 static::assertSame(1, $link['id']);
234 }
235
236 /**
237 * Daily page - test that height adjustment in columns is working
238 */
239 public function testValidIndexControllerInvokeHeightAdjustment(): void
240 {
241 $currentDay = new \DateTimeImmutable('2020-05-13');
242
243 $request = $this->createMock(Request::class);
244 $response = new Response();
245
246 // Save RainTPL assigned variables
247 $assignedVariables = [];
248 $this->assignTemplateVars($assignedVariables);
249
250 $this->container->bookmarkService
251 ->expects(static::once())
252 ->method('findByDate')
253 ->willReturnCallback(function () use ($currentDay): array {
254 return [
255 (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'),
256 (new Bookmark())
257 ->setId(2)
258 ->setUrl('http://url.tld')
259 ->setTitle(static::generateString(50))
260 ->setDescription(static::generateString(5000))
261 ,
262 (new Bookmark())->setId(3)->setUrl('http://url.tld')->setTitle('title'),
263 (new Bookmark())->setId(4)->setUrl('http://url.tld')->setTitle('title'),
264 (new Bookmark())->setId(5)->setUrl('http://url.tld')->setTitle('title'),
265 (new Bookmark())->setId(6)->setUrl('http://url.tld')->setTitle('title'),
266 (new Bookmark())->setId(7)->setUrl('http://url.tld')->setTitle('title'),
267 ];
268 })
269 ;
270
271 // Make sure that PluginManager hook is triggered
272 $this->container->pluginManager
273 ->expects(static::atLeastOnce())
274 ->method('executeHooks')
275 ->willReturnCallback(function (string $hook, array $data, array $param): array {
276 return $data;
277 })
278 ;
279
280 $result = $this->controller->index($request, $response);
281
282 static::assertSame(200, $result->getStatusCode());
283 static::assertSame('daily', (string) $result->getBody());
284 static::assertCount(7, $assignedVariables['linksToDisplay']);
285
286 $columnIds = function (array $column): array {
287 return array_map(function (array $item): int { return $item['id']; }, $column);
288 };
289
290 static::assertSame([1, 4, 6], $columnIds($assignedVariables['cols'][0]));
291 static::assertSame([2], $columnIds($assignedVariables['cols'][1]));
292 static::assertSame([3, 5, 7], $columnIds($assignedVariables['cols'][2]));
293 }
294
295 /**
296 * Daily page - no bookmark
297 */
298 public function testValidIndexControllerInvokeNoBookmark(): void
299 {
300 $request = $this->createMock(Request::class);
301 $response = new Response();
302
303 // Save RainTPL assigned variables
304 $assignedVariables = [];
305 $this->assignTemplateVars($assignedVariables);
306
307 // Links dataset: 2 links with thumbnails
308 $this->container->bookmarkService
309 ->expects(static::once())
310 ->method('findByDate')
311 ->willReturnCallback(function (): array {
312 return [];
313 })
314 ;
315
316 // Make sure that PluginManager hook is triggered
317 $this->container->pluginManager
318 ->expects(static::atLeastOnce())
319 ->method('executeHooks')
320 ->willReturnCallback(function (string $hook, array $data, array $param): array {
321 return $data;
322 })
323 ;
324
325 $result = $this->controller->index($request, $response);
326
327 static::assertSame(200, $result->getStatusCode());
328 static::assertSame('daily', (string) $result->getBody());
329 static::assertCount(0, $assignedVariables['linksToDisplay']);
330 static::assertSame('Today - ' . (new \DateTime())->format('F d, Y'), $assignedVariables['dayDesc']);
331 static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
332 static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']);
333 }
334
335 /**
336 * Daily RSS - default behaviour
337 */
338 public function testValidRssControllerInvokeDefault(): void
339 {
340 $dates = [
341 new \DateTimeImmutable('2020-05-17'),
342 new \DateTimeImmutable('2020-05-15'),
343 new \DateTimeImmutable('2020-05-13'),
344 new \DateTimeImmutable('+1 month'),
345 ];
346
347 $request = $this->createMock(Request::class);
348 $response = new Response();
349
350 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
351 (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
352 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
353 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
354 (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
355 (new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'),
356 ]);
357
358 $this->container->pageCacheManager
359 ->expects(static::once())
360 ->method('getCachePage')
361 ->willReturnCallback(function (): CachedPage {
362 $cachedPage = $this->createMock(CachedPage::class);
363 $cachedPage->expects(static::once())->method('cache')->with('dailyrss');
364
365 return $cachedPage;
366 }
367 );
368
369 // Save RainTPL assigned variables
370 $assignedVariables = [];
371 $this->assignTemplateVars($assignedVariables);
372
373 $result = $this->controller->rss($request, $response);
374
375 static::assertSame(200, $result->getStatusCode());
376 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
377 static::assertSame('dailyrss', (string) $result->getBody());
378 static::assertSame('Shaarli', $assignedVariables['title']);
379 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
380 static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
381 static::assertFalse($assignedVariables['hide_timestamps']);
382 static::assertCount(3, $assignedVariables['days']);
383
384 $day = $assignedVariables['days'][$dates[0]->format('Ymd')];
385 $date = $dates[0]->setTime(23, 59, 59);
386
387 static::assertEquals($date, $day['date']);
388 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
389 static::assertSame(format_date($date, false), $day['date_human']);
390 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
391 static::assertCount(1, $day['links']);
392 static::assertSame(1, $day['links'][0]['id']);
393 static::assertSame('http://domain.tld/1', $day['links'][0]['url']);
394 static::assertEquals($dates[0], $day['links'][0]['created']);
395
396 $day = $assignedVariables['days'][$dates[1]->format('Ymd')];
397 $date = $dates[1]->setTime(23, 59, 59);
398
399 static::assertEquals($date, $day['date']);
400 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
401 static::assertSame(format_date($date, false), $day['date_human']);
402 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
403 static::assertCount(2, $day['links']);
404
405 static::assertSame(2, $day['links'][0]['id']);
406 static::assertSame('http://domain.tld/2', $day['links'][0]['url']);
407 static::assertEquals($dates[1], $day['links'][0]['created']);
408 static::assertSame(3, $day['links'][1]['id']);
409 static::assertSame('http://domain.tld/3', $day['links'][1]['url']);
410 static::assertEquals($dates[1], $day['links'][1]['created']);
411
412 $day = $assignedVariables['days'][$dates[2]->format('Ymd')];
413 $date = $dates[2]->setTime(23, 59, 59);
414
415 static::assertEquals($date, $day['date']);
416 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
417 static::assertSame(format_date($date, false), $day['date_human']);
418 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[2]->format('Ymd'), $day['absolute_url']);
419 static::assertCount(1, $day['links']);
420 static::assertSame(4, $day['links'][0]['id']);
421 static::assertSame('http://domain.tld/4', $day['links'][0]['url']);
422 static::assertEquals($dates[2], $day['links'][0]['created']);
423 }
424
425 /**
426 * Daily RSS - trigger cache rendering
427 */
428 public function testValidRssControllerInvokeTriggerCache(): void
429 {
430 $request = $this->createMock(Request::class);
431 $response = new Response();
432
433 $this->container->pageCacheManager->method('getCachePage')->willReturnCallback(function (): CachedPage {
434 $cachedPage = $this->createMock(CachedPage::class);
435 $cachedPage->method('cachedVersion')->willReturn('this is cache!');
436
437 return $cachedPage;
438 });
439
440 $this->container->bookmarkService->expects(static::never())->method('search');
441
442 $result = $this->controller->rss($request, $response);
443
444 static::assertSame(200, $result->getStatusCode());
445 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
446 static::assertSame('this is cache!', (string) $result->getBody());
447 }
448
449 /**
450 * Daily RSS - No bookmark
451 */
452 public function testValidRssControllerInvokeNoBookmark(): void
453 {
454 $request = $this->createMock(Request::class);
455 $response = new Response();
456
457 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([]);
458
459 // Save RainTPL assigned variables
460 $assignedVariables = [];
461 $this->assignTemplateVars($assignedVariables);
462
463 $result = $this->controller->rss($request, $response);
464
465 static::assertSame(200, $result->getStatusCode());
466 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
467 static::assertSame('dailyrss', (string) $result->getBody());
468 static::assertSame('Shaarli', $assignedVariables['title']);
469 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
470 static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
471 static::assertFalse($assignedVariables['hide_timestamps']);
472 static::assertCount(0, $assignedVariables['days']);
473 }
474
475 /**
476 * Test simple display index with week parameter
477 */
478 public function testSimpleIndexWeekly(): void
479 {
480 $currentDay = new \DateTimeImmutable('2020-05-13');
481 $expectedDay = new \DateTimeImmutable('2020-05-11');
482
483 $request = $this->createMock(Request::class);
484 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
485 return $key === 'week' ? $currentDay->format('YW') : null;
486 });
487 $response = new Response();
488
489 // Save RainTPL assigned variables
490 $assignedVariables = [];
491 $this->assignTemplateVars($assignedVariables);
492
493 $this->container->bookmarkService
494 ->expects(static::once())
495 ->method('findByDate')
496 ->willReturnCallback(
497 function (): array {
498 return [
499 (new Bookmark())
500 ->setId(1)
501 ->setUrl('http://url.tld')
502 ->setTitle(static::generateString(50))
503 ->setDescription(static::generateString(500))
504 ,
505 (new Bookmark())
506 ->setId(2)
507 ->setUrl('http://url2.tld')
508 ->setTitle(static::generateString(50))
509 ->setDescription(static::generateString(500))
510 ,
511 ];
512 }
513 )
514 ;
515
516 $result = $this->controller->index($request, $response);
517
518 static::assertSame(200, $result->getStatusCode());
519 static::assertSame('daily', (string) $result->getBody());
520 static::assertSame(
521 'Weekly - Week 20 (May 11, 2020) - Shaarli',
522 $assignedVariables['pagetitle']
523 );
524
525 static::assertCount(2, $assignedVariables['linksToDisplay']);
526 static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
527 static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
528 static::assertSame('', $assignedVariables['previousday']);
529 static::assertSame('', $assignedVariables['nextday']);
530 static::assertSame('Week 20 (May 11, 2020)', $assignedVariables['dayDesc']);
531 static::assertSame('week', $assignedVariables['type']);
532 static::assertSame('Weekly', $assignedVariables['localizedType']);
533 }
534
535 /**
536 * Test simple display index with month parameter
537 */
538 public function testSimpleIndexMonthly(): void
539 {
540 $currentDay = new \DateTimeImmutable('2020-05-13');
541 $expectedDay = new \DateTimeImmutable('2020-05-01');
542
543 $request = $this->createMock(Request::class);
544 $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string {
545 return $key === 'month' ? $currentDay->format('Ym') : null;
546 });
547 $response = new Response();
548
549 // Save RainTPL assigned variables
550 $assignedVariables = [];
551 $this->assignTemplateVars($assignedVariables);
552
553 $this->container->bookmarkService
554 ->expects(static::once())
555 ->method('findByDate')
556 ->willReturnCallback(
557 function (): array {
558 return [
559 (new Bookmark())
560 ->setId(1)
561 ->setUrl('http://url.tld')
562 ->setTitle(static::generateString(50))
563 ->setDescription(static::generateString(500))
564 ,
565 (new Bookmark())
566 ->setId(2)
567 ->setUrl('http://url2.tld')
568 ->setTitle(static::generateString(50))
569 ->setDescription(static::generateString(500))
570 ,
571 ];
572 }
573 )
574 ;
575
576 $result = $this->controller->index($request, $response);
577
578 static::assertSame(200, $result->getStatusCode());
579 static::assertSame('daily', (string) $result->getBody());
580 static::assertSame(
581 'Monthly - May, 2020 - Shaarli',
582 $assignedVariables['pagetitle']
583 );
584
585 static::assertCount(2, $assignedVariables['linksToDisplay']);
586 static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']);
587 static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
588 static::assertSame('', $assignedVariables['previousday']);
589 static::assertSame('', $assignedVariables['nextday']);
590 static::assertSame('May, 2020', $assignedVariables['dayDesc']);
591 static::assertSame('month', $assignedVariables['type']);
592 static::assertSame('Monthly', $assignedVariables['localizedType']);
593 }
594
595 /**
596 * Test simple display RSS with week parameter
597 */
598 public function testSimpleRssWeekly(): void
599 {
600 $dates = [
601 new \DateTimeImmutable('2020-05-19'),
602 new \DateTimeImmutable('2020-05-13'),
603 ];
604 $expectedDates = [
605 new \DateTimeImmutable('2020-05-24 23:59:59'),
606 new \DateTimeImmutable('2020-05-17 23:59:59'),
607 ];
608
609 $this->container->environment['QUERY_STRING'] = 'week';
610 $request = $this->createMock(Request::class);
611 $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
612 return $key === 'week' ? '' : null;
613 });
614 $response = new Response();
615
616 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
617 (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
618 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
619 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
620 ]);
621
622 // Save RainTPL assigned variables
623 $assignedVariables = [];
624 $this->assignTemplateVars($assignedVariables);
625
626 $result = $this->controller->rss($request, $response);
627
628 static::assertSame(200, $result->getStatusCode());
629 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
630 static::assertSame('dailyrss', (string) $result->getBody());
631 static::assertSame('Shaarli', $assignedVariables['title']);
632 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
633 static::assertSame('http://shaarli/subfolder/daily-rss?week', $assignedVariables['page_url']);
634 static::assertFalse($assignedVariables['hide_timestamps']);
635 static::assertCount(2, $assignedVariables['days']);
636
637 $day = $assignedVariables['days'][$dates[0]->format('YW')];
638 $date = $expectedDates[0];
639
640 static::assertEquals($date, $day['date']);
641 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
642 static::assertSame('Week 21 (May 18, 2020)', $day['date_human']);
643 static::assertSame('http://shaarli/subfolder/daily?week='. $dates[0]->format('YW'), $day['absolute_url']);
644 static::assertCount(1, $day['links']);
645
646 $day = $assignedVariables['days'][$dates[1]->format('YW')];
647 $date = $expectedDates[1];
648
649 static::assertEquals($date, $day['date']);
650 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
651 static::assertSame('Week 20 (May 11, 2020)', $day['date_human']);
652 static::assertSame('http://shaarli/subfolder/daily?week='. $dates[1]->format('YW'), $day['absolute_url']);
653 static::assertCount(2, $day['links']);
654 }
655
656 /**
657 * Test simple display RSS with month parameter
658 */
659 public function testSimpleRssMonthly(): void
660 {
661 $dates = [
662 new \DateTimeImmutable('2020-05-19'),
663 new \DateTimeImmutable('2020-04-13'),
664 ];
665 $expectedDates = [
666 new \DateTimeImmutable('2020-05-31 23:59:59'),
667 new \DateTimeImmutable('2020-04-30 23:59:59'),
668 ];
669
670 $this->container->environment['QUERY_STRING'] = 'month';
671 $request = $this->createMock(Request::class);
672 $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string {
673 return $key === 'month' ? '' : null;
674 });
675 $response = new Response();
676
677 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
678 (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
679 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
680 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
681 ]);
682
683 // Save RainTPL assigned variables
684 $assignedVariables = [];
685 $this->assignTemplateVars($assignedVariables);
686
687 $result = $this->controller->rss($request, $response);
688
689 static::assertSame(200, $result->getStatusCode());
690 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
691 static::assertSame('dailyrss', (string) $result->getBody());
692 static::assertSame('Shaarli', $assignedVariables['title']);
693 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
694 static::assertSame('http://shaarli/subfolder/daily-rss?month', $assignedVariables['page_url']);
695 static::assertFalse($assignedVariables['hide_timestamps']);
696 static::assertCount(2, $assignedVariables['days']);
697
698 $day = $assignedVariables['days'][$dates[0]->format('Ym')];
699 $date = $expectedDates[0];
700
701 static::assertEquals($date, $day['date']);
702 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
703 static::assertSame('May, 2020', $day['date_human']);
704 static::assertSame('http://shaarli/subfolder/daily?month='. $dates[0]->format('Ym'), $day['absolute_url']);
705 static::assertCount(1, $day['links']);
706
707 $day = $assignedVariables['days'][$dates[1]->format('Ym')];
708 $date = $expectedDates[1];
709
710 static::assertEquals($date, $day['date']);
711 static::assertSame($date->format(\DateTime::RSS), $day['date_rss']);
712 static::assertSame('April, 2020', $day['date_human']);
713 static::assertSame('http://shaarli/subfolder/daily?month='. $dates[1]->format('Ym'), $day['absolute_url']);
714 static::assertCount(2, $day['links']);
715 }
716}
diff --git a/tests/front/controller/visitor/ErrorControllerTest.php b/tests/front/controller/visitor/ErrorControllerTest.php
new file mode 100644
index 00000000..75408cf4
--- /dev/null
+++ b/tests/front/controller/visitor/ErrorControllerTest.php
@@ -0,0 +1,70 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Front\Exception\ShaarliFrontException;
8use Shaarli\TestCase;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class ErrorControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var ErrorController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new ErrorController($this->container);
24 }
25
26 /**
27 * Test displaying error with a ShaarliFrontException: display exception message and use its code for HTTTP code
28 */
29 public function testDisplayFrontExceptionError(): void
30 {
31 $request = $this->createMock(Request::class);
32 $response = new Response();
33
34 $message = 'error message';
35 $errorCode = 418;
36
37 // Save RainTPL assigned variables
38 $assignedVariables = [];
39 $this->assignTemplateVars($assignedVariables);
40
41 $result = ($this->controller)(
42 $request,
43 $response,
44 new class($message, $errorCode) extends ShaarliFrontException {}
45 );
46
47 static::assertSame($errorCode, $result->getStatusCode());
48 static::assertSame($message, $assignedVariables['message']);
49 static::assertArrayNotHasKey('stacktrace', $assignedVariables);
50 }
51
52 /**
53 * Test displaying error with any exception (no debug): only display an error occurred with HTTP 500.
54 */
55 public function testDisplayAnyExceptionErrorNoDebug(): void
56 {
57 $request = $this->createMock(Request::class);
58 $response = new Response();
59
60 // Save RainTPL assigned variables
61 $assignedVariables = [];
62 $this->assignTemplateVars($assignedVariables);
63
64 $result = ($this->controller)($request, $response, new \Exception('abc'));
65
66 static::assertSame(500, $result->getStatusCode());
67 static::assertSame('An unexpected error occurred.', $assignedVariables['message']);
68 static::assertArrayNotHasKey('stacktrace', $assignedVariables);
69 }
70}
diff --git a/tests/front/controller/visitor/ErrorNotFoundControllerTest.php b/tests/front/controller/visitor/ErrorNotFoundControllerTest.php
new file mode 100644
index 00000000..a1cbbecf
--- /dev/null
+++ b/tests/front/controller/visitor/ErrorNotFoundControllerTest.php
@@ -0,0 +1,81 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\TestCase;
8use Slim\Http\Request;
9use Slim\Http\Response;
10use Slim\Http\Uri;
11
12class ErrorNotFoundControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var ErrorNotFoundController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new ErrorNotFoundController($this->container);
24 }
25
26 /**
27 * Test displaying 404 error
28 */
29 public function testDisplayNotFoundError(): void
30 {
31 $request = $this->createMock(Request::class);
32 $request->expects(static::once())->method('getRequestTarget')->willReturn('/');
33 $request->method('getUri')->willReturnCallback(function (): Uri {
34 $uri = $this->createMock(Uri::class);
35 $uri->method('getBasePath')->willReturn('/subfolder');
36
37 return $uri;
38 });
39
40 $response = new Response();
41
42 // Save RainTPL assigned variables
43 $assignedVariables = [];
44 $this->assignTemplateVars($assignedVariables);
45
46 $result = ($this->controller)(
47 $request,
48 $response
49 );
50
51 static::assertSame(404, $result->getStatusCode());
52 static::assertSame('404', (string) $result->getBody());
53 static::assertSame('Requested page could not be found.', $assignedVariables['error_message']);
54 }
55
56 /**
57 * Test displaying 404 error from REST API
58 */
59 public function testDisplayNotFoundErrorFromAPI(): void
60 {
61 $request = $this->createMock(Request::class);
62 $request->expects(static::once())->method('getRequestTarget')->willReturn('/sufolder/api/v1/links');
63 $request->method('getUri')->willReturnCallback(function (): Uri {
64 $uri = $this->createMock(Uri::class);
65 $uri->method('getBasePath')->willReturn('/subfolder');
66
67 return $uri;
68 });
69
70 $response = new Response();
71
72 // Save RainTPL assigned variables
73 $assignedVariables = [];
74 $this->assignTemplateVars($assignedVariables);
75
76 $result = ($this->controller)($request, $response);
77
78 static::assertSame(404, $result->getStatusCode());
79 static::assertSame([], $assignedVariables);
80 }
81}
diff --git a/tests/front/controller/visitor/FeedControllerTest.php b/tests/front/controller/visitor/FeedControllerTest.php
new file mode 100644
index 00000000..4ae7c925
--- /dev/null
+++ b/tests/front/controller/visitor/FeedControllerTest.php
@@ -0,0 +1,151 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Feed\FeedBuilder;
8use Shaarli\TestCase;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class FeedControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var FeedController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->container->feedBuilder = $this->createMock(FeedBuilder::class);
24
25 $this->controller = new FeedController($this->container);
26 }
27
28 /**
29 * Feed Controller - RSS default behaviour
30 */
31 public function testDefaultRssController(): void
32 {
33 $request = $this->createMock(Request::class);
34 $response = new Response();
35
36 $this->container->feedBuilder->expects(static::once())->method('setLocale');
37 $this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false);
38 $this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true);
39
40 // Save RainTPL assigned variables
41 $assignedVariables = [];
42 $this->assignTemplateVars($assignedVariables);
43
44 $this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']);
45
46 // Make sure that PluginManager hook is triggered
47 $this->container->pluginManager
48 ->expects(static::atLeastOnce())
49 ->method('executeHooks')
50 ->withConsecutive(['render_feed'])
51 ->willReturnCallback(function (string $hook, array $data, array $param): void {
52 if ('render_feed' === $hook) {
53 static::assertSame('data', $data['content']);
54
55 static::assertArrayHasKey('loggedin', $param);
56 static::assertSame('feed.rss', $param['target']);
57 }
58 })
59 ;
60
61 $result = $this->controller->rss($request, $response);
62
63 static::assertSame(200, $result->getStatusCode());
64 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
65 static::assertSame('feed.rss', (string) $result->getBody());
66 static::assertSame('data', $assignedVariables['content']);
67 }
68
69 /**
70 * Feed Controller - ATOM default behaviour
71 */
72 public function testDefaultAtomController(): void
73 {
74 $request = $this->createMock(Request::class);
75 $response = new Response();
76
77 $this->container->feedBuilder->expects(static::once())->method('setLocale');
78 $this->container->feedBuilder->expects(static::once())->method('setHideDates')->with(false);
79 $this->container->feedBuilder->expects(static::once())->method('setUsePermalinks')->with(true);
80
81 // Save RainTPL assigned variables
82 $assignedVariables = [];
83 $this->assignTemplateVars($assignedVariables);
84
85 $this->container->feedBuilder->method('buildData')->willReturn(['content' => 'data']);
86
87 // Make sure that PluginManager hook is triggered
88 $this->container->pluginManager
89 ->expects(static::atLeastOnce())
90 ->method('executeHooks')
91 ->withConsecutive(['render_feed'])
92 ->willReturnCallback(function (string $hook, array $data, array $param): void {
93 if ('render_feed' === $hook) {
94 static::assertSame('data', $data['content']);
95
96 static::assertArrayHasKey('loggedin', $param);
97 static::assertSame('feed.atom', $param['target']);
98 }
99 })
100 ;
101
102 $result = $this->controller->atom($request, $response);
103
104 static::assertSame(200, $result->getStatusCode());
105 static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]);
106 static::assertSame('feed.atom', (string) $result->getBody());
107 static::assertSame('data', $assignedVariables['content']);
108 }
109
110 /**
111 * Feed Controller - ATOM with parameters
112 */
113 public function testAtomControllerWithParameters(): void
114 {
115 $request = $this->createMock(Request::class);
116 $request->method('getParams')->willReturn(['parameter' => 'value']);
117 $response = new Response();
118
119 // Save RainTPL assigned variables
120 $assignedVariables = [];
121 $this->assignTemplateVars($assignedVariables);
122
123 $this->container->feedBuilder
124 ->method('buildData')
125 ->with('atom', ['parameter' => 'value'])
126 ->willReturn(['content' => 'data'])
127 ;
128
129 // Make sure that PluginManager hook is triggered
130 $this->container->pluginManager
131 ->expects(static::atLeastOnce())
132 ->method('executeHooks')
133 ->withConsecutive(['render_feed'])
134 ->willReturnCallback(function (string $hook, array $data, array $param): void {
135 if ('render_feed' === $hook) {
136 static::assertSame('data', $data['content']);
137
138 static::assertArrayHasKey('loggedin', $param);
139 static::assertSame('feed.atom', $param['target']);
140 }
141 })
142 ;
143
144 $result = $this->controller->atom($request, $response);
145
146 static::assertSame(200, $result->getStatusCode());
147 static::assertStringContainsString('application/atom', $result->getHeader('Content-Type')[0]);
148 static::assertSame('feed.atom', (string) $result->getBody());
149 static::assertSame('data', $assignedVariables['content']);
150 }
151}
diff --git a/tests/front/controller/visitor/FrontControllerMockHelper.php b/tests/front/controller/visitor/FrontControllerMockHelper.php
new file mode 100644
index 00000000..fc0bb7d1
--- /dev/null
+++ b/tests/front/controller/visitor/FrontControllerMockHelper.php
@@ -0,0 +1,118 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Container\ShaarliTestContainer;
10use Shaarli\Formatter\BookmarkFormatter;
11use Shaarli\Formatter\BookmarkRawFormatter;
12use Shaarli\Formatter\FormatterFactory;
13use Shaarli\Plugin\PluginManager;
14use Shaarli\Render\PageBuilder;
15use Shaarli\Render\PageCacheManager;
16use Shaarli\Security\LoginManager;
17use Shaarli\Security\SessionManager;
18
19/**
20 * Trait FrontControllerMockHelper
21 *
22 * Helper trait used to initialize the ShaarliContainer and mock its services for controller tests.
23 *
24 * @property ShaarliTestContainer $container
25 * @package Shaarli\Front\Controller
26 */
27trait FrontControllerMockHelper
28{
29 /** @var ShaarliTestContainer */
30 protected $container;
31
32 /**
33 * Mock the container instance and initialize container's services used by tests
34 */
35 protected function createContainer(): void
36 {
37 $this->container = $this->createMock(ShaarliTestContainer::class);
38
39 $this->container->loginManager = $this->createMock(LoginManager::class);
40
41 // Config
42 $this->container->conf = $this->createMock(ConfigManager::class);
43 $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
44 return $default === null ? $parameter : $default;
45 });
46
47 // PageBuilder
48 $this->container->pageBuilder = $this->createMock(PageBuilder::class);
49 $this->container->pageBuilder
50 ->method('render')
51 ->willReturnCallback(function (string $template): string {
52 return $template;
53 })
54 ;
55
56 // Plugin Manager
57 $this->container->pluginManager = $this->createMock(PluginManager::class);
58
59 // BookmarkService
60 $this->container->bookmarkService = $this->createMock(BookmarkServiceInterface::class);
61
62 // Formatter
63 $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
64 $this->container->formatterFactory
65 ->method('getFormatter')
66 ->willReturnCallback(function (): BookmarkFormatter {
67 return new BookmarkRawFormatter($this->container->conf, true);
68 })
69 ;
70
71 // CacheManager
72 $this->container->pageCacheManager = $this->createMock(PageCacheManager::class);
73
74 // SessionManager
75 $this->container->sessionManager = $this->createMock(SessionManager::class);
76
77 // $_SERVER
78 $this->container->environment = [
79 'SERVER_NAME' => 'shaarli',
80 'SERVER_PORT' => '80',
81 'REQUEST_URI' => '/subfolder/daily-rss',
82 'REMOTE_ADDR' => '1.2.3.4',
83 'SCRIPT_NAME' => '/subfolder/index.php',
84 ];
85
86 $this->container->basePath = '/subfolder';
87 }
88
89 /**
90 * Pass a reference of an array which will be populated by `pageBuilder->assign` calls during execution.
91 *
92 * @param mixed $variables Array reference to populate.
93 */
94 protected function assignTemplateVars(array &$variables): void
95 {
96 $this->container->pageBuilder
97 ->method('assign')
98 ->willReturnCallback(function ($key, $value) use (&$variables) {
99 $variables[$key] = $value;
100
101 return $this;
102 })
103 ;
104 }
105
106 protected static function generateString(int $length): string
107 {
108 // bin2hex(random_bytes) generates string twice as long as given parameter
109 $length = (int) ceil($length / 2);
110
111 return bin2hex(random_bytes($length));
112 }
113
114 /**
115 * Force to be used in PHPUnit context.
116 */
117 protected abstract function isInTestsContext(): bool;
118}
diff --git a/tests/front/controller/visitor/InstallControllerTest.php b/tests/front/controller/visitor/InstallControllerTest.php
new file mode 100644
index 00000000..2105ed77
--- /dev/null
+++ b/tests/front/controller/visitor/InstallControllerTest.php
@@ -0,0 +1,304 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Front\Exception\AlreadyInstalledException;
9use Shaarli\Security\SessionManager;
10use Shaarli\TestCase;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class InstallControllerTest extends TestCase
15{
16 use FrontControllerMockHelper;
17
18 const MOCK_FILE = '.tmp';
19
20 /** @var InstallController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->container->conf = $this->createMock(ConfigManager::class);
28 $this->container->conf->method('getConfigFileExt')->willReturn(static::MOCK_FILE);
29 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
30 if ($key === 'resource.raintpl_tpl') {
31 return '.';
32 }
33
34 return $default ?? $key;
35 });
36
37 $this->controller = new InstallController($this->container);
38 }
39
40 protected function tearDown(): void
41 {
42 if (file_exists(static::MOCK_FILE)) {
43 unlink(static::MOCK_FILE);
44 }
45 }
46
47 /**
48 * Test displaying install page with valid session.
49 */
50 public function testInstallIndexWithValidSession(): void
51 {
52 $assignedVariables = [];
53 $this->assignTemplateVars($assignedVariables);
54
55 $request = $this->createMock(Request::class);
56 $response = new Response();
57
58 $this->container->sessionManager = $this->createMock(SessionManager::class);
59 $this->container->sessionManager
60 ->method('getSessionParameter')
61 ->willReturnCallback(function (string $key, $default) {
62 return $key === 'session_tested' ? 'Working' : $default;
63 })
64 ;
65
66 $result = $this->controller->index($request, $response);
67
68 static::assertSame(200, $result->getStatusCode());
69 static::assertSame('install', (string) $result->getBody());
70
71 static::assertIsArray($assignedVariables['continents']);
72 static::assertSame('Africa', $assignedVariables['continents'][0]);
73 static::assertSame('UTC', $assignedVariables['continents']['selected']);
74
75 static::assertIsArray($assignedVariables['cities']);
76 static::assertSame(['continent' => 'Africa', 'city' => 'Abidjan'], $assignedVariables['cities'][0]);
77 static::assertSame('UTC', $assignedVariables['continents']['selected']);
78
79 static::assertIsArray($assignedVariables['languages']);
80 static::assertSame('Automatic', $assignedVariables['languages']['auto']);
81 static::assertSame('French', $assignedVariables['languages']['fr']);
82
83 static::assertSame(PHP_VERSION, $assignedVariables['php_version']);
84 static::assertArrayHasKey('php_has_reached_eol', $assignedVariables);
85 static::assertArrayHasKey('php_eol', $assignedVariables);
86 static::assertArrayHasKey('php_extensions', $assignedVariables);
87 static::assertArrayHasKey('permissions', $assignedVariables);
88 static::assertEmpty($assignedVariables['permissions']);
89
90 static::assertSame('Install Shaarli', $assignedVariables['pagetitle']);
91 }
92
93 /**
94 * Instantiate the install controller with an existing config file: exception.
95 */
96 public function testInstallWithExistingConfigFile(): void
97 {
98 $this->expectException(AlreadyInstalledException::class);
99
100 touch(static::MOCK_FILE);
101
102 $this->controller = new InstallController($this->container);
103 }
104
105 /**
106 * Call controller without session yet defined, redirect to test session install page.
107 */
108 public function testInstallRedirectToSessionTest(): void
109 {
110 $request = $this->createMock(Request::class);
111 $response = new Response();
112
113 $this->container->sessionManager = $this->createMock(SessionManager::class);
114 $this->container->sessionManager
115 ->expects(static::once())
116 ->method('setSessionParameter')
117 ->with(InstallController::SESSION_TEST_KEY, InstallController::SESSION_TEST_VALUE)
118 ;
119
120 $result = $this->controller->index($request, $response);
121
122 static::assertSame(302, $result->getStatusCode());
123 static::assertSame('/subfolder/install/session-test', $result->getHeader('location')[0]);
124 }
125
126 /**
127 * Call controller in session test mode: valid session then redirect to install page.
128 */
129 public function testInstallSessionTestValid(): void
130 {
131 $request = $this->createMock(Request::class);
132 $response = new Response();
133
134 $this->container->sessionManager = $this->createMock(SessionManager::class);
135 $this->container->sessionManager
136 ->method('getSessionParameter')
137 ->with(InstallController::SESSION_TEST_KEY)
138 ->willReturn(InstallController::SESSION_TEST_VALUE)
139 ;
140
141 $result = $this->controller->sessionTest($request, $response);
142
143 static::assertSame(302, $result->getStatusCode());
144 static::assertSame('/subfolder/install', $result->getHeader('location')[0]);
145 }
146
147 /**
148 * Call controller in session test mode: invalid session then redirect to error page.
149 */
150 public function testInstallSessionTestError(): void
151 {
152 $assignedVars = [];
153 $this->assignTemplateVars($assignedVars);
154
155 $request = $this->createMock(Request::class);
156 $response = new Response();
157
158 $this->container->sessionManager = $this->createMock(SessionManager::class);
159 $this->container->sessionManager
160 ->method('getSessionParameter')
161 ->with(InstallController::SESSION_TEST_KEY)
162 ->willReturn('KO')
163 ;
164
165 $result = $this->controller->sessionTest($request, $response);
166
167 static::assertSame(200, $result->getStatusCode());
168 static::assertSame('error', (string) $result->getBody());
169 static::assertStringStartsWith(
170 '<pre>Sessions do not seem to work correctly on your server',
171 $assignedVars['message']
172 );
173 }
174
175 /**
176 * Test saving valid data from install form. Also initialize datastore.
177 */
178 public function testSaveInstallValid(): void
179 {
180 $providedParameters = [
181 'continent' => 'Europe',
182 'city' => 'Berlin',
183 'setlogin' => 'bob',
184 'setpassword' => 'password',
185 'title' => 'Shaarli',
186 'language' => 'fr',
187 'updateCheck' => true,
188 'enableApi' => true,
189 ];
190
191 $expectedSettings = [
192 'general.timezone' => 'Europe/Berlin',
193 'credentials.login' => 'bob',
194 'credentials.salt' => '_NOT_EMPTY',
195 'credentials.hash' => '_NOT_EMPTY',
196 'general.title' => 'Shaarli',
197 'translation.language' => 'en',
198 'updates.check_updates' => true,
199 'api.enabled' => true,
200 'api.secret' => '_NOT_EMPTY',
201 'general.header_link' => '/subfolder',
202 ];
203
204 $request = $this->createMock(Request::class);
205 $request->method('getParam')->willReturnCallback(function (string $key) use ($providedParameters) {
206 return $providedParameters[$key] ?? null;
207 });
208 $response = new Response();
209
210 $this->container->conf = $this->createMock(ConfigManager::class);
211 $this->container->conf
212 ->method('get')
213 ->willReturnCallback(function (string $key, $value) {
214 if ($key === 'credentials.login') {
215 return 'bob';
216 } elseif ($key === 'credentials.salt') {
217 return 'salt';
218 }
219
220 return $value;
221 })
222 ;
223 $this->container->conf
224 ->expects(static::exactly(count($expectedSettings)))
225 ->method('set')
226 ->willReturnCallback(function (string $key, $value) use ($expectedSettings) {
227 if ($expectedSettings[$key] ?? null === '_NOT_EMPTY') {
228 static::assertNotEmpty($value);
229 } else {
230 static::assertSame($expectedSettings[$key], $value);
231 }
232 })
233 ;
234 $this->container->conf->expects(static::once())->method('write');
235
236 $this->container->sessionManager
237 ->expects(static::once())
238 ->method('setSessionParameter')
239 ->with(SessionManager::KEY_SUCCESS_MESSAGES)
240 ;
241
242 $result = $this->controller->save($request, $response);
243
244 static::assertSame(302, $result->getStatusCode());
245 static::assertSame('/subfolder/login', $result->getHeader('location')[0]);
246 }
247
248 /**
249 * Test default settings (timezone and title).
250 * Also check that bookmarks are not initialized if
251 */
252 public function testSaveInstallDefaultValues(): void
253 {
254 $confSettings = [];
255
256 $request = $this->createMock(Request::class);
257 $response = new Response();
258
259 $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) {
260 $confSettings[$key] = $value;
261 });
262
263 $result = $this->controller->save($request, $response);
264
265 static::assertSame(302, $result->getStatusCode());
266 static::assertSame('/subfolder/login', $result->getHeader('location')[0]);
267
268 static::assertSame('UTC', $confSettings['general.timezone']);
269 static::assertSame('Shared bookmarks on http://shaarli/subfolder/', $confSettings['general.title']);
270 }
271
272 /**
273 * Same test as testSaveInstallDefaultValues() but for an instance install in root directory.
274 */
275 public function testSaveInstallDefaultValuesWithoutSubfolder(): void
276 {
277 $confSettings = [];
278
279 $this->container->environment = [
280 'SERVER_NAME' => 'shaarli',
281 'SERVER_PORT' => '80',
282 'REQUEST_URI' => '/install',
283 'REMOTE_ADDR' => '1.2.3.4',
284 'SCRIPT_NAME' => '/index.php',
285 ];
286
287 $this->container->basePath = '';
288
289 $request = $this->createMock(Request::class);
290 $response = new Response();
291
292 $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) {
293 $confSettings[$key] = $value;
294 });
295
296 $result = $this->controller->save($request, $response);
297
298 static::assertSame(302, $result->getStatusCode());
299 static::assertSame('/login', $result->getHeader('location')[0]);
300
301 static::assertSame('UTC', $confSettings['general.timezone']);
302 static::assertSame('Shared bookmarks on http://shaarli/', $confSettings['general.title']);
303 }
304}
diff --git a/tests/front/controller/visitor/LoginControllerTest.php b/tests/front/controller/visitor/LoginControllerTest.php
new file mode 100644
index 00000000..00d9eab3
--- /dev/null
+++ b/tests/front/controller/visitor/LoginControllerTest.php
@@ -0,0 +1,404 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Front\Exception\LoginBannedException;
9use Shaarli\Front\Exception\WrongTokenException;
10use Shaarli\Render\TemplatePage;
11use Shaarli\Security\CookieManager;
12use Shaarli\Security\SessionManager;
13use Shaarli\TestCase;
14use Slim\Http\Request;
15use Slim\Http\Response;
16
17class LoginControllerTest extends TestCase
18{
19 use FrontControllerMockHelper;
20
21 /** @var LoginController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->createContainer();
27
28 $this->container->cookieManager = $this->createMock(CookieManager::class);
29 $this->container->sessionManager->method('checkToken')->willReturn(true);
30
31 $this->controller = new LoginController($this->container);
32 }
33
34 /**
35 * Test displaying login form with valid parameters.
36 */
37 public function testValidControllerInvoke(): void
38 {
39 $request = $this->createMock(Request::class);
40 $request
41 ->expects(static::atLeastOnce())
42 ->method('getParam')
43 ->willReturnCallback(function (string $key) {
44 return 'returnurl' === $key ? '> referer' : null;
45 })
46 ;
47 $response = new Response();
48
49 $assignedVariables = [];
50 $this->container->pageBuilder
51 ->method('assign')
52 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
53 $assignedVariables[$key] = $value;
54
55 return $this;
56 })
57 ;
58
59 $this->container->loginManager->method('canLogin')->willReturn(true);
60
61 $result = $this->controller->index($request, $response);
62
63 static::assertInstanceOf(Response::class, $result);
64 static::assertSame(200, $result->getStatusCode());
65 static::assertSame(TemplatePage::LOGIN, (string) $result->getBody());
66
67 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
68 static::assertSame(true, $assignedVariables['remember_user_default']);
69 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
70 }
71
72 /**
73 * Test displaying login form with username defined in the request.
74 */
75 public function testValidControllerInvokeWithUserName(): void
76 {
77 $this->container->environment = ['HTTP_REFERER' => '> referer'];
78
79 $request = $this->createMock(Request::class);
80 $request
81 ->expects(static::atLeastOnce())
82 ->method('getParam')
83 ->willReturnCallback(function (string $key, $default) {
84 if ('login' === $key) {
85 return 'myUser>';
86 }
87
88 return $default;
89 })
90 ;
91 $response = new Response();
92
93 $assignedVariables = [];
94 $this->container->pageBuilder
95 ->method('assign')
96 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
97 $assignedVariables[$key] = $value;
98
99 return $this;
100 })
101 ;
102
103 $this->container->loginManager->expects(static::once())->method('canLogin')->willReturn(true);
104
105 $result = $this->controller->index($request, $response);
106
107 static::assertInstanceOf(Response::class, $result);
108 static::assertSame(200, $result->getStatusCode());
109 static::assertSame('loginform', (string) $result->getBody());
110
111 static::assertSame('myUser&gt;', $assignedVariables['username']);
112 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
113 static::assertSame(true, $assignedVariables['remember_user_default']);
114 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
115 }
116
117 /**
118 * Test displaying login page while being logged in.
119 */
120 public function testLoginControllerWhileLoggedIn(): void
121 {
122 $request = $this->createMock(Request::class);
123 $response = new Response();
124
125 $this->container->loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true);
126
127 $result = $this->controller->index($request, $response);
128
129 static::assertInstanceOf(Response::class, $result);
130 static::assertSame(302, $result->getStatusCode());
131 static::assertSame(['/subfolder/'], $result->getHeader('Location'));
132 }
133
134 /**
135 * Test displaying login page with open shaarli configured: redirect to homepage.
136 */
137 public function testLoginControllerOpenShaarli(): void
138 {
139 $request = $this->createMock(Request::class);
140 $response = new Response();
141
142 $conf = $this->createMock(ConfigManager::class);
143 $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
144 if ($parameter === 'security.open_shaarli') {
145 return true;
146 }
147 return $default;
148 });
149 $this->container->conf = $conf;
150
151 $result = $this->controller->index($request, $response);
152
153 static::assertInstanceOf(Response::class, $result);
154 static::assertSame(302, $result->getStatusCode());
155 static::assertSame(['/subfolder/'], $result->getHeader('Location'));
156 }
157
158 /**
159 * Test displaying login page while being banned.
160 */
161 public function testLoginControllerWhileBanned(): void
162 {
163 $request = $this->createMock(Request::class);
164 $response = new Response();
165
166 $this->container->loginManager->method('isLoggedIn')->willReturn(false);
167 $this->container->loginManager->method('canLogin')->willReturn(false);
168
169 $this->expectException(LoginBannedException::class);
170
171 $this->controller->index($request, $response);
172 }
173
174 /**
175 * Test processing login with valid parameters.
176 */
177 public function testProcessLoginWithValidParameters(): void
178 {
179 $parameters = [
180 'login' => 'bob',
181 'password' => 'pass',
182 ];
183 $request = $this->createMock(Request::class);
184 $request
185 ->expects(static::atLeastOnce())
186 ->method('getParam')
187 ->willReturnCallback(function (string $key) use ($parameters) {
188 return $parameters[$key] ?? null;
189 })
190 ;
191 $response = new Response();
192
193 $this->container->loginManager->method('canLogin')->willReturn(true);
194 $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin');
195 $this->container->loginManager
196 ->expects(static::once())
197 ->method('checkCredentials')
198 ->with('1.2.3.4', 'bob', 'pass')
199 ->willReturn(true)
200 ;
201 $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
202
203 $this->container->sessionManager->expects(static::never())->method('extendSession');
204 $this->container->sessionManager->expects(static::once())->method('destroy');
205 $this->container->sessionManager
206 ->expects(static::once())
207 ->method('cookieParameters')
208 ->with(0, '/subfolder/', 'shaarli')
209 ;
210 $this->container->sessionManager->expects(static::once())->method('start');
211 $this->container->sessionManager->expects(static::once())->method('regenerateId')->with(true);
212
213 $result = $this->controller->login($request, $response);
214
215 static::assertSame(302, $result->getStatusCode());
216 static::assertSame('/subfolder/', $result->getHeader('location')[0]);
217 }
218
219 /**
220 * Test processing login with return URL.
221 */
222 public function testProcessLoginWithReturnUrl(): void
223 {
224 $parameters = [
225 'returnurl' => 'http://shaarli/subfolder/admin/shaare',
226 ];
227 $request = $this->createMock(Request::class);
228 $request
229 ->expects(static::atLeastOnce())
230 ->method('getParam')
231 ->willReturnCallback(function (string $key) use ($parameters) {
232 return $parameters[$key] ?? null;
233 })
234 ;
235 $response = new Response();
236
237 $this->container->loginManager->method('canLogin')->willReturn(true);
238 $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin');
239 $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(true);
240 $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
241
242 $result = $this->controller->login($request, $response);
243
244 static::assertSame(302, $result->getStatusCode());
245 static::assertSame('/subfolder/admin/shaare', $result->getHeader('location')[0]);
246 }
247
248 /**
249 * Test processing login with remember me session enabled.
250 */
251 public function testProcessLoginLongLastingSession(): void
252 {
253 $parameters = [
254 'longlastingsession' => true,
255 ];
256 $request = $this->createMock(Request::class);
257 $request
258 ->expects(static::atLeastOnce())
259 ->method('getParam')
260 ->willReturnCallback(function (string $key) use ($parameters) {
261 return $parameters[$key] ?? null;
262 })
263 ;
264 $response = new Response();
265
266 $this->container->loginManager->method('canLogin')->willReturn(true);
267 $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin');
268 $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(true);
269 $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
270
271 $this->container->sessionManager->expects(static::once())->method('destroy');
272 $this->container->sessionManager
273 ->expects(static::once())
274 ->method('cookieParameters')
275 ->with(42, '/subfolder/', 'shaarli')
276 ;
277 $this->container->sessionManager->expects(static::once())->method('start');
278 $this->container->sessionManager->expects(static::once())->method('regenerateId')->with(true);
279 $this->container->sessionManager->expects(static::once())->method('extendSession')->willReturn(42);
280
281 $this->container->cookieManager = $this->createMock(CookieManager::class);
282 $this->container->cookieManager
283 ->expects(static::once())
284 ->method('setCookieParameter')
285 ->willReturnCallback(function (string $name): CookieManager {
286 static::assertSame(CookieManager::STAY_SIGNED_IN, $name);
287
288 return $this->container->cookieManager;
289 })
290 ;
291
292 $result = $this->controller->login($request, $response);
293
294 static::assertSame(302, $result->getStatusCode());
295 static::assertSame('/subfolder/', $result->getHeader('location')[0]);
296 }
297
298 /**
299 * Test processing login with invalid credentials
300 */
301 public function testProcessLoginWrongCredentials(): void
302 {
303 $parameters = [
304 'returnurl' => 'http://shaarli/subfolder/admin/shaare',
305 ];
306 $request = $this->createMock(Request::class);
307 $request
308 ->expects(static::atLeastOnce())
309 ->method('getParam')
310 ->willReturnCallback(function (string $key) use ($parameters) {
311 return $parameters[$key] ?? null;
312 })
313 ;
314 $response = new Response();
315
316 $this->container->loginManager->method('canLogin')->willReturn(true);
317 $this->container->loginManager->expects(static::once())->method('handleFailedLogin');
318 $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(false);
319
320 $this->container->sessionManager
321 ->expects(static::once())
322 ->method('setSessionParameter')
323 ->with(SessionManager::KEY_ERROR_MESSAGES, ['Wrong login/password.'])
324 ;
325
326 $result = $this->controller->login($request, $response);
327
328 static::assertSame(200, $result->getStatusCode());
329 static::assertSame(TemplatePage::LOGIN, (string) $result->getBody());
330 }
331
332 /**
333 * Test processing login with wrong token
334 */
335 public function testProcessLoginWrongToken(): void
336 {
337 $request = $this->createMock(Request::class);
338 $response = new Response();
339
340 $this->container->sessionManager = $this->createMock(SessionManager::class);
341 $this->container->sessionManager->method('checkToken')->willReturn(false);
342
343 $this->expectException(WrongTokenException::class);
344
345 $this->controller->login($request, $response);
346 }
347
348 /**
349 * Test processing login with wrong token
350 */
351 public function testProcessLoginAlreadyLoggedIn(): void
352 {
353 $request = $this->createMock(Request::class);
354 $response = new Response();
355
356 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
357 $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin');
358 $this->container->loginManager->expects(static::never())->method('handleFailedLogin');
359
360 $result = $this->controller->login($request, $response);
361
362 static::assertSame(302, $result->getStatusCode());
363 static::assertSame('/subfolder/', $result->getHeader('location')[0]);
364 }
365
366 /**
367 * Test processing login with wrong token
368 */
369 public function testProcessLoginInOpenShaarli(): void
370 {
371 $request = $this->createMock(Request::class);
372 $response = new Response();
373
374 $this->container->conf = $this->createMock(ConfigManager::class);
375 $this->container->conf->method('get')->willReturnCallback(function (string $key, $value) {
376 return 'security.open_shaarli' === $key ? true : $value;
377 });
378
379 $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin');
380 $this->container->loginManager->expects(static::never())->method('handleFailedLogin');
381
382 $result = $this->controller->login($request, $response);
383
384 static::assertSame(302, $result->getStatusCode());
385 static::assertSame('/subfolder/', $result->getHeader('location')[0]);
386 }
387
388 /**
389 * Test processing login while being banned
390 */
391 public function testProcessLoginWhileBanned(): void
392 {
393 $request = $this->createMock(Request::class);
394 $response = new Response();
395
396 $this->container->loginManager->method('canLogin')->willReturn(false);
397 $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin');
398 $this->container->loginManager->expects(static::never())->method('handleFailedLogin');
399
400 $this->expectException(LoginBannedException::class);
401
402 $this->controller->login($request, $response);
403 }
404}
diff --git a/tests/front/controller/visitor/OpenSearchControllerTest.php b/tests/front/controller/visitor/OpenSearchControllerTest.php
new file mode 100644
index 00000000..42d876c3
--- /dev/null
+++ b/tests/front/controller/visitor/OpenSearchControllerTest.php
@@ -0,0 +1,44 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\TestCase;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11class OpenSearchControllerTest extends TestCase
12{
13 use FrontControllerMockHelper;
14
15 /** @var OpenSearchController */
16 protected $controller;
17
18 public function setUp(): void
19 {
20 $this->createContainer();
21
22 $this->controller = new OpenSearchController($this->container);
23 }
24
25 public function testOpenSearchController(): void
26 {
27 $request = $this->createMock(Request::class);
28 $response = new Response();
29
30 // Save RainTPL assigned variables
31 $assignedVariables = [];
32 $this->assignTemplateVars($assignedVariables);
33
34 $result = $this->controller->index($request, $response);
35
36 static::assertSame(200, $result->getStatusCode());
37 static::assertStringContainsString(
38 'application/opensearchdescription+xml',
39 $result->getHeader('Content-Type')[0]
40 );
41 static::assertSame('opensearch', (string) $result->getBody());
42 static::assertSame('http://shaarli/subfolder/', $assignedVariables['serverurl']);
43 }
44}
diff --git a/tests/front/controller/visitor/PictureWallControllerTest.php b/tests/front/controller/visitor/PictureWallControllerTest.php
new file mode 100644
index 00000000..b868231d
--- /dev/null
+++ b/tests/front/controller/visitor/PictureWallControllerTest.php
@@ -0,0 +1,123 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\ThumbnailsDisabledException;
10use Shaarli\TestCase;
11use Shaarli\Thumbnailer;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class PictureWallControllerTest extends TestCase
16{
17 use FrontControllerMockHelper;
18
19 /** @var PictureWallController */
20 protected $controller;
21
22 public function setUp(): void
23 {
24 $this->createContainer();
25
26 $this->controller = new PictureWallController($this->container);
27 }
28
29 public function testValidControllerInvokeDefault(): void
30 {
31 $request = $this->createMock(Request::class);
32 $request->expects(static::once())->method('getQueryParams')->willReturn([]);
33 $response = new Response();
34
35 // ConfigManager: thumbnails are enabled
36 $this->container->conf = $this->createMock(ConfigManager::class);
37 $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
38 if ($parameter === 'thumbnails.mode') {
39 return Thumbnailer::MODE_COMMON;
40 }
41
42 return $default;
43 });
44
45 // Save RainTPL assigned variables
46 $assignedVariables = [];
47 $this->assignTemplateVars($assignedVariables);
48
49 // Links dataset: 2 links with thumbnails
50 $this->container->bookmarkService
51 ->expects(static::once())
52 ->method('search')
53 ->willReturnCallback(function (array $parameters, ?string $visibility): array {
54 // Visibility is set through the container, not the call
55 static::assertNull($visibility);
56
57 // No query parameters
58 if (count($parameters) === 0) {
59 return [
60 (new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'),
61 (new Bookmark())->setId(2)->setUrl('http://url2.tld'),
62 (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'),
63 ];
64 }
65 })
66 ;
67
68 // Make sure that PluginManager hook is triggered
69 $this->container->pluginManager
70 ->expects(static::atLeastOnce())
71 ->method('executeHooks')
72 ->withConsecutive(['render_picwall'])
73 ->willReturnCallback(function (string $hook, array $data, array $param): array {
74 if ('render_picwall' === $hook) {
75 static::assertArrayHasKey('linksToDisplay', $data);
76 static::assertCount(2, $data['linksToDisplay']);
77 static::assertSame(1, $data['linksToDisplay'][0]['id']);
78 static::assertSame(3, $data['linksToDisplay'][1]['id']);
79 static::assertArrayHasKey('loggedin', $param);
80 }
81
82 return $data;
83 });
84
85 $result = $this->controller->index($request, $response);
86
87 static::assertSame(200, $result->getStatusCode());
88 static::assertSame('picwall', (string) $result->getBody());
89 static::assertSame('Picture wall - Shaarli', $assignedVariables['pagetitle']);
90 static::assertCount(2, $assignedVariables['linksToDisplay']);
91
92 $link = $assignedVariables['linksToDisplay'][0];
93
94 static::assertSame(1, $link['id']);
95 static::assertSame('http://url.tld', $link['url']);
96 static::assertSame('thumb1', $link['thumbnail']);
97
98 $link = $assignedVariables['linksToDisplay'][1];
99
100 static::assertSame(3, $link['id']);
101 static::assertSame('http://url3.tld', $link['url']);
102 static::assertSame('thumb2', $link['thumbnail']);
103 }
104
105 public function testControllerWithThumbnailsDisabled(): void
106 {
107 $this->expectException(ThumbnailsDisabledException::class);
108
109 $request = $this->createMock(Request::class);
110 $response = new Response();
111
112 // ConfigManager: thumbnails are disabled
113 $this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
114 if ($parameter === 'thumbnails.mode') {
115 return Thumbnailer::MODE_NONE;
116 }
117
118 return $default;
119 });
120
121 $this->controller->index($request, $response);
122 }
123}
diff --git a/tests/front/controller/visitor/PublicSessionFilterControllerTest.php b/tests/front/controller/visitor/PublicSessionFilterControllerTest.php
new file mode 100644
index 00000000..7e3b00af
--- /dev/null
+++ b/tests/front/controller/visitor/PublicSessionFilterControllerTest.php
@@ -0,0 +1,122 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Security\SessionManager;
8use Shaarli\TestCase;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class PublicSessionFilterControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var PublicSessionFilterController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new PublicSessionFilterController($this->container);
24 }
25
26 /**
27 * Link per page - Default call with valid parameter and a referer.
28 */
29 public function testLinksPerPage(): void
30 {
31 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller/?searchtag=abc';
32
33 $request = $this->createMock(Request::class);
34 $request->method('getParam')->with('nb')->willReturn('8');
35 $response = new Response();
36
37 $this->container->sessionManager
38 ->expects(static::once())
39 ->method('setSessionParameter')
40 ->with(SessionManager::KEY_LINKS_PER_PAGE, 8)
41 ;
42
43 $result = $this->controller->linksPerPage($request, $response);
44
45 static::assertInstanceOf(Response::class, $result);
46 static::assertSame(302, $result->getStatusCode());
47 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
48 }
49
50 /**
51 * Link per page - Invalid value, should use default value (20)
52 */
53 public function testLinksPerPageNotValid(): void
54 {
55 $request = $this->createMock(Request::class);
56 $request->method('getParam')->with('nb')->willReturn('test');
57 $response = new Response();
58
59 $this->container->sessionManager
60 ->expects(static::once())
61 ->method('setSessionParameter')
62 ->with(SessionManager::KEY_LINKS_PER_PAGE, 20)
63 ;
64
65 $result = $this->controller->linksPerPage($request, $response);
66
67 static::assertInstanceOf(Response::class, $result);
68 static::assertSame(302, $result->getStatusCode());
69 static::assertSame(['/subfolder/'], $result->getHeader('location'));
70 }
71
72 /**
73 * Untagged only - valid call
74 */
75 public function testUntaggedOnly(): void
76 {
77 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller/?searchtag=abc';
78
79 $request = $this->createMock(Request::class);
80 $response = new Response();
81
82 $this->container->sessionManager
83 ->expects(static::once())
84 ->method('setSessionParameter')
85 ->with(SessionManager::KEY_UNTAGGED_ONLY, true)
86 ;
87
88 $result = $this->controller->untaggedOnly($request, $response);
89
90 static::assertInstanceOf(Response::class, $result);
91 static::assertSame(302, $result->getStatusCode());
92 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
93 }
94
95 /**
96 * Untagged only - toggle off
97 */
98 public function testUntaggedOnlyToggleOff(): void
99 {
100 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller/?searchtag=abc';
101
102 $request = $this->createMock(Request::class);
103 $response = new Response();
104
105 $this->container->sessionManager
106 ->method('getSessionParameter')
107 ->with(SessionManager::KEY_UNTAGGED_ONLY)
108 ->willReturn(true)
109 ;
110 $this->container->sessionManager
111 ->expects(static::once())
112 ->method('setSessionParameter')
113 ->with(SessionManager::KEY_UNTAGGED_ONLY, false)
114 ;
115
116 $result = $this->controller->untaggedOnly($request, $response);
117
118 static::assertInstanceOf(Response::class, $result);
119 static::assertSame(302, $result->getStatusCode());
120 static::assertSame(['/subfolder/controller/?searchtag=abc'], $result->getHeader('location'));
121 }
122}
diff --git a/tests/front/controller/visitor/ShaarliVisitorControllerTest.php b/tests/front/controller/visitor/ShaarliVisitorControllerTest.php
new file mode 100644
index 00000000..935ec24e
--- /dev/null
+++ b/tests/front/controller/visitor/ShaarliVisitorControllerTest.php
@@ -0,0 +1,246 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\TestCase;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class ShaarliControllerTest
14 *
15 * This class is used to test default behavior of ShaarliVisitorController abstract class.
16 * It uses a dummy non abstract controller.
17 */
18class ShaarliVisitorControllerTest extends TestCase
19{
20 use FrontControllerMockHelper;
21
22 /** @var LoginController */
23 protected $controller;
24
25 /** @var mixed[] List of variable assigned to the template */
26 protected $assignedValues;
27
28 /** @var Request */
29 protected $request;
30
31 public function setUp(): void
32 {
33 $this->createContainer();
34
35 $this->controller = new class($this->container) extends ShaarliVisitorController
36 {
37 public function assignView(string $key, $value): ShaarliVisitorController
38 {
39 return parent::assignView($key, $value);
40 }
41
42 public function render(string $template): string
43 {
44 return parent::render($template);
45 }
46
47 public function redirectFromReferer(
48 Request $request,
49 Response $response,
50 array $loopTerms = [],
51 array $clearParams = [],
52 string $anchor = null
53 ): Response {
54 return parent::redirectFromReferer($request, $response, $loopTerms, $clearParams, $anchor);
55 }
56 };
57 $this->assignedValues = [];
58
59 $this->request = $this->createMock(Request::class);
60 }
61
62 public function testAssignView(): void
63 {
64 $this->assignTemplateVars($this->assignedValues);
65
66 $self = $this->controller->assignView('variableName', 'variableValue');
67
68 static::assertInstanceOf(ShaarliVisitorController::class, $self);
69 static::assertSame('variableValue', $this->assignedValues['variableName']);
70 }
71
72 public function testRender(): void
73 {
74 $this->assignTemplateVars($this->assignedValues);
75
76 $this->container->bookmarkService
77 ->method('count')
78 ->willReturnCallback(function (string $visibility): int {
79 return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10;
80 })
81 ;
82
83 $this->container->pluginManager
84 ->method('executeHooks')
85 ->willReturnCallback(function (string $hook, array &$data, array $params): array {
86 return $data[$hook] = $params;
87 });
88 $this->container->pluginManager->method('getErrors')->willReturn(['error']);
89
90 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
91
92 $render = $this->controller->render('templateName');
93
94 static::assertSame('templateName', $render);
95
96 static::assertSame(10, $this->assignedValues['linkcount']);
97 static::assertSame(5, $this->assignedValues['privateLinkcount']);
98 static::assertSame(['error'], $this->assignedValues['plugin_errors']);
99
100 static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']);
101 static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']);
102 static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']);
103 static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']);
104 static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']);
105 static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']);
106 }
107
108 /**
109 * Test redirectFromReferer() - Default behaviour
110 */
111 public function testRedirectFromRefererDefault(): void
112 {
113 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller?query=param&other=2';
114
115 $response = new Response();
116
117 $result = $this->controller->redirectFromReferer($this->request, $response);
118
119 static::assertSame(302, $result->getStatusCode());
120 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
121 }
122
123 /**
124 * Test redirectFromReferer() - With a loop term not matched in the referer
125 */
126 public function testRedirectFromRefererWithUnmatchedLoopTerm(): void
127 {
128 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller?query=param&other=2';
129
130 $response = new Response();
131
132 $result = $this->controller->redirectFromReferer($this->request, $response, ['nope']);
133
134 static::assertSame(302, $result->getStatusCode());
135 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
136 }
137
138 /**
139 * Test redirectFromReferer() - With a loop term matching the referer in its path -> redirect to default
140 */
141 public function testRedirectFromRefererWithMatchingLoopTermInPath(): void
142 {
143 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller?query=param&other=2';
144
145 $response = new Response();
146
147 $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'controller']);
148
149 static::assertSame(302, $result->getStatusCode());
150 static::assertSame(['/subfolder/'], $result->getHeader('location'));
151 }
152
153 /**
154 * Test redirectFromReferer() - With a loop term matching the referer in its query parameters -> redirect to default
155 */
156 public function testRedirectFromRefererWithMatchingLoopTermInQueryParam(): void
157 {
158 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller?query=param&other=2';
159
160 $response = new Response();
161
162 $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'other']);
163
164 static::assertSame(302, $result->getStatusCode());
165 static::assertSame(['/subfolder/'], $result->getHeader('location'));
166 }
167
168 /**
169 * Test redirectFromReferer() - With a loop term matching the referer in its query value
170 * -> we do not block redirection for query parameter values.
171 */
172 public function testRedirectFromRefererWithMatchingLoopTermInQueryValue(): void
173 {
174 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller?query=param&other=2';
175
176 $response = new Response();
177
178 $result = $this->controller->redirectFromReferer($this->request, $response, ['nope', 'param']);
179
180 static::assertSame(302, $result->getStatusCode());
181 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
182 }
183
184 /**
185 * Test redirectFromReferer() - With a loop term matching the referer in its domain name
186 * -> we do not block redirection for shaarli's hosts
187 */
188 public function testRedirectFromRefererWithLoopTermInDomain(): void
189 {
190 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller?query=param&other=2';
191
192 $response = new Response();
193
194 $result = $this->controller->redirectFromReferer($this->request, $response, ['shaarli']);
195
196 static::assertSame(302, $result->getStatusCode());
197 static::assertSame(['/subfolder/controller?query=param&other=2'], $result->getHeader('location'));
198 }
199
200 /**
201 * Test redirectFromReferer() - With a loop term matching a query parameter AND clear this query param
202 * -> the param should be cleared before checking if it matches the redir loop terms
203 */
204 public function testRedirectFromRefererWithMatchingClearedParam(): void
205 {
206 $this->container->environment['HTTP_REFERER'] = 'http://shaarli/subfolder/controller?query=param&other=2';
207
208 $response = new Response();
209
210 $result = $this->controller->redirectFromReferer($this->request, $response, ['query'], ['query']);
211
212 static::assertSame(302, $result->getStatusCode());
213 static::assertSame(['/subfolder/controller?other=2'], $result->getHeader('location'));
214 }
215
216 /**
217 * Test redirectFromReferer() - From another domain -> we ignore the given referrer.
218 */
219 public function testRedirectExternalReferer(): void
220 {
221 $this->container->environment['HTTP_REFERER'] = 'http://other.domain.tld/controller?query=param&other=2';
222
223 $response = new Response();
224
225 $result = $this->controller->redirectFromReferer($this->request, $response, ['query'], ['query']);
226
227 static::assertSame(302, $result->getStatusCode());
228 static::assertSame(['/subfolder/'], $result->getHeader('location'));
229 }
230
231 /**
232 * Test redirectFromReferer() - From another domain -> we ignore the given referrer.
233 */
234 public function testRedirectExternalRefererExplicitDomainName(): void
235 {
236 $this->container->environment['SERVER_NAME'] = 'my.shaarli.tld';
237 $this->container->environment['HTTP_REFERER'] = 'http://your.shaarli.tld/controller?query=param&other=2';
238
239 $response = new Response();
240
241 $result = $this->controller->redirectFromReferer($this->request, $response, ['query'], ['query']);
242
243 static::assertSame(302, $result->getStatusCode());
244 static::assertSame(['/subfolder/'], $result->getHeader('location'));
245 }
246}
diff --git a/tests/front/controller/visitor/TagCloudControllerTest.php b/tests/front/controller/visitor/TagCloudControllerTest.php
new file mode 100644
index 00000000..9305612e
--- /dev/null
+++ b/tests/front/controller/visitor/TagCloudControllerTest.php
@@ -0,0 +1,381 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\TestCase;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class TagCloudControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var TagCloudController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new TagCloudController($this->container);
24 }
25
26 /**
27 * Tag Cloud - default parameters
28 */
29 public function testValidCloudControllerInvokeDefault(): void
30 {
31 $allTags = [
32 'ghi' => 1,
33 'abc' => 3,
34 'def' => 12,
35 ];
36 $expectedOrder = ['abc', 'def', 'ghi'];
37
38 $request = $this->createMock(Request::class);
39 $response = new Response();
40
41 // Save RainTPL assigned variables
42 $assignedVariables = [];
43 $this->assignTemplateVars($assignedVariables);
44
45 $this->container->bookmarkService
46 ->expects(static::once())
47 ->method('bookmarksCountPerTag')
48 ->with([], null)
49 ->willReturnCallback(function () use ($allTags): array {
50 return $allTags;
51 })
52 ;
53
54 // Make sure that PluginManager hook is triggered
55 $this->container->pluginManager
56 ->expects(static::atLeastOnce())
57 ->method('executeHooks')
58 ->withConsecutive(['render_tagcloud'])
59 ->willReturnCallback(function (string $hook, array $data, array $param): array {
60 if ('render_tagcloud' === $hook) {
61 static::assertSame('', $data['search_tags']);
62 static::assertCount(3, $data['tags']);
63
64 static::assertArrayHasKey('loggedin', $param);
65 }
66
67 return $data;
68 })
69 ;
70
71 $result = $this->controller->cloud($request, $response);
72
73 static::assertSame(200, $result->getStatusCode());
74 static::assertSame('tag.cloud', (string) $result->getBody());
75 static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']);
76
77 static::assertSame('', $assignedVariables['search_tags']);
78 static::assertCount(3, $assignedVariables['tags']);
79 static::assertSame($expectedOrder, array_keys($assignedVariables['tags']));
80
81 foreach ($allTags as $tag => $count) {
82 static::assertArrayHasKey($tag, $assignedVariables['tags']);
83 static::assertSame($count, $assignedVariables['tags'][$tag]['count']);
84 static::assertGreaterThan(0, $assignedVariables['tags'][$tag]['size']);
85 static::assertLessThan(5, $assignedVariables['tags'][$tag]['size']);
86 }
87 }
88
89 /**
90 * Tag Cloud - Additional parameters:
91 * - logged in
92 * - visibility private
93 * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
94 */
95 public function testValidCloudControllerInvokeWithParameters(): void
96 {
97 $request = $this->createMock(Request::class);
98 $request
99 ->method('getQueryParam')
100 ->with()
101 ->willReturnCallback(function (string $key): ?string {
102 if ('searchtags' === $key) {
103 return 'ghi def';
104 }
105
106 return null;
107 })
108 ;
109 $response = new Response();
110
111 // Save RainTPL assigned variables
112 $assignedVariables = [];
113 $this->assignTemplateVars($assignedVariables);
114
115 $this->container->loginManager->method('isLoggedin')->willReturn(true);
116 $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private');
117
118 $this->container->bookmarkService
119 ->expects(static::once())
120 ->method('bookmarksCountPerTag')
121 ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE)
122 ->willReturnCallback(function (): array {
123 return ['abc' => 3];
124 })
125 ;
126
127 // Make sure that PluginManager hook is triggered
128 $this->container->pluginManager
129 ->expects(static::atLeastOnce())
130 ->method('executeHooks')
131 ->withConsecutive(['render_tagcloud'])
132 ->willReturnCallback(function (string $hook, array $data, array $param): array {
133 if ('render_tagcloud' === $hook) {
134 static::assertSame('ghi def', $data['search_tags']);
135 static::assertCount(1, $data['tags']);
136
137 static::assertArrayHasKey('loggedin', $param);
138 }
139
140 return $data;
141 })
142 ;
143
144 $result = $this->controller->cloud($request, $response);
145
146 static::assertSame(200, $result->getStatusCode());
147 static::assertSame('tag.cloud', (string) $result->getBody());
148 static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']);
149
150 static::assertSame('ghi def', $assignedVariables['search_tags']);
151 static::assertCount(1, $assignedVariables['tags']);
152
153 static::assertArrayHasKey('abc', $assignedVariables['tags']);
154 static::assertSame(3, $assignedVariables['tags']['abc']['count']);
155 static::assertGreaterThan(0, $assignedVariables['tags']['abc']['size']);
156 static::assertLessThan(5, $assignedVariables['tags']['abc']['size']);
157 }
158
159 /**
160 * Tag Cloud - empty
161 */
162 public function testEmptyCloud(): void
163 {
164 $request = $this->createMock(Request::class);
165 $response = new Response();
166
167 // Save RainTPL assigned variables
168 $assignedVariables = [];
169 $this->assignTemplateVars($assignedVariables);
170
171 $this->container->bookmarkService
172 ->expects(static::once())
173 ->method('bookmarksCountPerTag')
174 ->with([], null)
175 ->willReturnCallback(function (array $parameters, ?string $visibility): array {
176 return [];
177 })
178 ;
179
180 // Make sure that PluginManager hook is triggered
181 $this->container->pluginManager
182 ->expects(static::atLeastOnce())
183 ->method('executeHooks')
184 ->withConsecutive(['render_tagcloud'])
185 ->willReturnCallback(function (string $hook, array $data, array $param): array {
186 if ('render_tagcloud' === $hook) {
187 static::assertSame('', $data['search_tags']);
188 static::assertCount(0, $data['tags']);
189
190 static::assertArrayHasKey('loggedin', $param);
191 }
192
193 return $data;
194 })
195 ;
196
197 $result = $this->controller->cloud($request, $response);
198
199 static::assertSame(200, $result->getStatusCode());
200 static::assertSame('tag.cloud', (string) $result->getBody());
201 static::assertSame('Tag cloud - Shaarli', $assignedVariables['pagetitle']);
202
203 static::assertSame('', $assignedVariables['search_tags']);
204 static::assertCount(0, $assignedVariables['tags']);
205 }
206
207 /**
208 * Tag List - Default sort is by usage DESC
209 */
210 public function testValidListControllerInvokeDefault(): void
211 {
212 $allTags = [
213 'def' => 12,
214 'abc' => 3,
215 'ghi' => 1,
216 ];
217
218 $request = $this->createMock(Request::class);
219 $response = new Response();
220
221 // Save RainTPL assigned variables
222 $assignedVariables = [];
223 $this->assignTemplateVars($assignedVariables);
224
225 $this->container->bookmarkService
226 ->expects(static::once())
227 ->method('bookmarksCountPerTag')
228 ->with([], null)
229 ->willReturnCallback(function () use ($allTags): array {
230 return $allTags;
231 })
232 ;
233
234 // Make sure that PluginManager hook is triggered
235 $this->container->pluginManager
236 ->expects(static::atLeastOnce())
237 ->method('executeHooks')
238 ->withConsecutive(['render_taglist'])
239 ->willReturnCallback(function (string $hook, array $data, array $param): array {
240 if ('render_taglist' === $hook) {
241 static::assertSame('', $data['search_tags']);
242 static::assertCount(3, $data['tags']);
243
244 static::assertArrayHasKey('loggedin', $param);
245 }
246
247 return $data;
248 })
249 ;
250
251 $result = $this->controller->list($request, $response);
252
253 static::assertSame(200, $result->getStatusCode());
254 static::assertSame('tag.list', (string) $result->getBody());
255 static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
256
257 static::assertSame('', $assignedVariables['search_tags']);
258 static::assertCount(3, $assignedVariables['tags']);
259
260 foreach ($allTags as $tag => $count) {
261 static::assertSame($count, $assignedVariables['tags'][$tag]);
262 }
263 }
264
265 /**
266 * Tag List - Additional parameters:
267 * - logged in
268 * - visibility private
269 * - search tags: `ghi` and `def` (note that filtered tags are not displayed anymore)
270 * - sort alphabetically
271 */
272 public function testValidListControllerInvokeWithParameters(): void
273 {
274 $request = $this->createMock(Request::class);
275 $request
276 ->method('getQueryParam')
277 ->with()
278 ->willReturnCallback(function (string $key): ?string {
279 if ('searchtags' === $key) {
280 return 'ghi def';
281 } elseif ('sort' === $key) {
282 return 'alpha';
283 }
284
285 return null;
286 })
287 ;
288 $response = new Response();
289
290 // Save RainTPL assigned variables
291 $assignedVariables = [];
292 $this->assignTemplateVars($assignedVariables);
293
294 $this->container->loginManager->method('isLoggedin')->willReturn(true);
295 $this->container->sessionManager->expects(static::once())->method('getSessionParameter')->willReturn('private');
296
297 $this->container->bookmarkService
298 ->expects(static::once())
299 ->method('bookmarksCountPerTag')
300 ->with(['ghi', 'def'], BookmarkFilter::$PRIVATE)
301 ->willReturnCallback(function (): array {
302 return ['abc' => 3];
303 })
304 ;
305
306 // Make sure that PluginManager hook is triggered
307 $this->container->pluginManager
308 ->expects(static::atLeastOnce())
309 ->method('executeHooks')
310 ->withConsecutive(['render_taglist'])
311 ->willReturnCallback(function (string $hook, array $data, array $param): array {
312 if ('render_taglist' === $hook) {
313 static::assertSame('ghi def', $data['search_tags']);
314 static::assertCount(1, $data['tags']);
315
316 static::assertArrayHasKey('loggedin', $param);
317 }
318
319 return $data;
320 })
321 ;
322
323 $result = $this->controller->list($request, $response);
324
325 static::assertSame(200, $result->getStatusCode());
326 static::assertSame('tag.list', (string) $result->getBody());
327 static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']);
328
329 static::assertSame('ghi def', $assignedVariables['search_tags']);
330 static::assertCount(1, $assignedVariables['tags']);
331 static::assertSame(3, $assignedVariables['tags']['abc']);
332 }
333
334 /**
335 * Tag List - empty
336 */
337 public function testEmptyList(): void
338 {
339 $request = $this->createMock(Request::class);
340 $response = new Response();
341
342 // Save RainTPL assigned variables
343 $assignedVariables = [];
344 $this->assignTemplateVars($assignedVariables);
345
346 $this->container->bookmarkService
347 ->expects(static::once())
348 ->method('bookmarksCountPerTag')
349 ->with([], null)
350 ->willReturnCallback(function (array $parameters, ?string $visibility): array {
351 return [];
352 })
353 ;
354
355 // Make sure that PluginManager hook is triggered
356 $this->container->pluginManager
357 ->expects(static::atLeastOnce())
358 ->method('executeHooks')
359 ->withConsecutive(['render_taglist'])
360 ->willReturnCallback(function (string $hook, array $data, array $param): array {
361 if ('render_taglist' === $hook) {
362 static::assertSame('', $data['search_tags']);
363 static::assertCount(0, $data['tags']);
364
365 static::assertArrayHasKey('loggedin', $param);
366 }
367
368 return $data;
369 })
370 ;
371
372 $result = $this->controller->list($request, $response);
373
374 static::assertSame(200, $result->getStatusCode());
375 static::assertSame('tag.list', (string) $result->getBody());
376 static::assertSame('Tag list - Shaarli', $assignedVariables['pagetitle']);
377
378 static::assertSame('', $assignedVariables['search_tags']);
379 static::assertCount(0, $assignedVariables['tags']);
380 }
381}
diff --git a/tests/front/controller/visitor/TagControllerTest.php b/tests/front/controller/visitor/TagControllerTest.php
new file mode 100644
index 00000000..750ea02d
--- /dev/null
+++ b/tests/front/controller/visitor/TagControllerTest.php
@@ -0,0 +1,215 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\TestCase;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11class TagControllerTest extends TestCase
12{
13 use FrontControllerMockHelper;
14
15 /** @var TagController */ protected $controller;
16
17 public function setUp(): void
18 {
19 $this->createContainer();
20
21 $this->controller = new TagController($this->container);
22 }
23
24 public function testAddTagWithReferer(): void
25 {
26 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/'];
27
28 $request = $this->createMock(Request::class);
29 $response = new Response();
30
31 $tags = ['newTag' => 'abc'];
32
33 $result = $this->controller->addTag($request, $response, $tags);
34
35 static::assertInstanceOf(Response::class, $result);
36 static::assertSame(302, $result->getStatusCode());
37 static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location'));
38 }
39
40 public function testAddTagWithRefererAndExistingSearch(): void
41 {
42 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
43
44 $request = $this->createMock(Request::class);
45 $response = new Response();
46
47 $tags = ['newTag' => 'abc'];
48
49 $result = $this->controller->addTag($request, $response, $tags);
50
51 static::assertInstanceOf(Response::class, $result);
52 static::assertSame(302, $result->getStatusCode());
53 static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
54 }
55
56 public function testAddTagWithoutRefererAndExistingSearch(): void
57 {
58 $request = $this->createMock(Request::class);
59 $response = new Response();
60
61 $tags = ['newTag' => 'abc'];
62
63 $result = $this->controller->addTag($request, $response, $tags);
64
65 static::assertInstanceOf(Response::class, $result);
66 static::assertSame(302, $result->getStatusCode());
67 static::assertSame(['/subfolder/?searchtags=abc'], $result->getHeader('location'));
68 }
69
70 public function testAddTagRemoveLegacyQueryParam(): void
71 {
72 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&addtag=abc'];
73
74 $request = $this->createMock(Request::class);
75 $response = new Response();
76
77 $tags = ['newTag' => 'abc'];
78
79 $result = $this->controller->addTag($request, $response, $tags);
80
81 static::assertInstanceOf(Response::class, $result);
82 static::assertSame(302, $result->getStatusCode());
83 static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
84 }
85
86 public function testAddTagResetPagination(): void
87 {
88 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&page=12'];
89
90 $request = $this->createMock(Request::class);
91 $response = new Response();
92
93 $tags = ['newTag' => 'abc'];
94
95 $result = $this->controller->addTag($request, $response, $tags);
96
97 static::assertInstanceOf(Response::class, $result);
98 static::assertSame(302, $result->getStatusCode());
99 static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
100 }
101
102 public function testAddTagWithRefererAndEmptySearch(): void
103 {
104 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags='];
105
106 $request = $this->createMock(Request::class);
107 $response = new Response();
108
109 $tags = ['newTag' => 'abc'];
110
111 $result = $this->controller->addTag($request, $response, $tags);
112
113 static::assertInstanceOf(Response::class, $result);
114 static::assertSame(302, $result->getStatusCode());
115 static::assertSame(['/controller/?searchtags=abc'], $result->getHeader('location'));
116 }
117
118 public function testAddTagWithoutNewTagWithReferer(): void
119 {
120 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
121
122 $request = $this->createMock(Request::class);
123 $response = new Response();
124
125 $result = $this->controller->addTag($request, $response, []);
126
127 static::assertInstanceOf(Response::class, $result);
128 static::assertSame(302, $result->getStatusCode());
129 static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location'));
130 }
131
132 public function testAddTagWithoutNewTagWithoutReferer(): void
133 {
134 $request = $this->createMock(Request::class);
135 $response = new Response();
136
137 $result = $this->controller->addTag($request, $response, []);
138
139 static::assertInstanceOf(Response::class, $result);
140 static::assertSame(302, $result->getStatusCode());
141 static::assertSame(['/subfolder/'], $result->getHeader('location'));
142 }
143
144 public function testRemoveTagWithoutMatchingTag(): void
145 {
146 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
147
148 $request = $this->createMock(Request::class);
149 $response = new Response();
150
151 $tags = ['tag' => 'abc'];
152
153 $result = $this->controller->removeTag($request, $response, $tags);
154
155 static::assertInstanceOf(Response::class, $result);
156 static::assertSame(302, $result->getStatusCode());
157 static::assertSame(['/controller/?searchtags=def'], $result->getHeader('location'));
158 }
159
160 public function testRemoveTagWithoutTagsearch(): void
161 {
162 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/'];
163
164 $request = $this->createMock(Request::class);
165 $response = new Response();
166
167 $tags = ['tag' => 'abc'];
168
169 $result = $this->controller->removeTag($request, $response, $tags);
170
171 static::assertInstanceOf(Response::class, $result);
172 static::assertSame(302, $result->getStatusCode());
173 static::assertSame(['/controller/'], $result->getHeader('location'));
174 }
175
176 public function testRemoveTagWithoutReferer(): void
177 {
178 $request = $this->createMock(Request::class);
179 $response = new Response();
180
181 $tags = ['tag' => 'abc'];
182
183 $result = $this->controller->removeTag($request, $response, $tags);
184
185 static::assertInstanceOf(Response::class, $result);
186 static::assertSame(302, $result->getStatusCode());
187 static::assertSame(['/subfolder/'], $result->getHeader('location'));
188 }
189
190 public function testRemoveTagWithoutTag(): void
191 {
192 $this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtag=abc'];
193
194 $request = $this->createMock(Request::class);
195 $response = new Response();
196
197 $result = $this->controller->removeTag($request, $response, []);
198
199 static::assertInstanceOf(Response::class, $result);
200 static::assertSame(302, $result->getStatusCode());
201 static::assertSame(['/controller/?searchtag=abc'], $result->getHeader('location'));
202 }
203
204 public function testRemoveTagWithoutTagWithoutReferer(): void
205 {
206 $request = $this->createMock(Request::class);
207 $response = new Response();
208
209 $result = $this->controller->removeTag($request, $response, []);
210
211 static::assertInstanceOf(Response::class, $result);
212 static::assertSame(302, $result->getStatusCode());
213 static::assertSame(['/subfolder/'], $result->getHeader('location'));
214 }
215}
diff --git a/tests/ApplicationUtilsTest.php b/tests/helper/ApplicationUtilsTest.php
index 15388970..654857b9 100644
--- a/tests/ApplicationUtilsTest.php
+++ b/tests/helper/ApplicationUtilsTest.php
@@ -1,14 +1,15 @@
1<?php 1<?php
2namespace Shaarli; 2namespace Shaarli\Helper;
3 3
4use Shaarli\Config\ConfigManager; 4use Shaarli\Config\ConfigManager;
5use Shaarli\FakeApplicationUtils;
5 6
6require_once 'tests/utils/FakeApplicationUtils.php'; 7require_once 'tests/utils/FakeApplicationUtils.php';
7 8
8/** 9/**
9 * Unitary tests for Shaarli utilities 10 * Unitary tests for Shaarli utilities
10 */ 11 */
11class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase 12class ApplicationUtilsTest extends \Shaarli\TestCase
12{ 13{
13 protected static $testUpdateFile = 'sandbox/update.txt'; 14 protected static $testUpdateFile = 'sandbox/update.txt';
14 protected static $testVersion = '0.5.0'; 15 protected static $testVersion = '0.5.0';
@@ -17,7 +18,7 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
17 /** 18 /**
18 * Reset test data for each test 19 * Reset test data for each test
19 */ 20 */
20 public function setUp() 21 protected function setUp(): void
21 { 22 {
22 FakeApplicationUtils::$VERSION_CODE = ''; 23 FakeApplicationUtils::$VERSION_CODE = '';
23 if (file_exists(self::$testUpdateFile)) { 24 if (file_exists(self::$testUpdateFile)) {
@@ -28,7 +29,7 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
28 /** 29 /**
29 * Remove test version file if it exists 30 * Remove test version file if it exists
30 */ 31 */
31 public function tearDown() 32 protected function tearDown(): void
32 { 33 {
33 if (is_file('sandbox/version.php')) { 34 if (is_file('sandbox/version.php')) {
34 unlink('sandbox/version.php'); 35 unlink('sandbox/version.php');
@@ -144,11 +145,12 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
144 145
145 /** 146 /**
146 * Test update checks - invalid Git branch 147 * Test update checks - invalid Git branch
147 * @expectedException Exception
148 * @expectedExceptionMessageRegExp /Invalid branch selected for updates/
149 */ 148 */
150 public function testCheckUpdateInvalidGitBranch() 149 public function testCheckUpdateInvalidGitBranch()
151 { 150 {
151 $this->expectException(\Exception::class);
152 $this->expectExceptionMessageRegExp('/Invalid branch selected for updates/');
153
152 ApplicationUtils::checkUpdate('', 'null', 0, true, true, 'unstable'); 154 ApplicationUtils::checkUpdate('', 'null', 0, true, true, 'unstable');
153 } 155 }
154 156
@@ -260,21 +262,23 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
260 262
261 /** 263 /**
262 * Check a unsupported PHP version 264 * Check a unsupported PHP version
263 * @expectedException Exception
264 * @expectedExceptionMessageRegExp /Your PHP version is obsolete/
265 */ 265 */
266 public function testCheckSupportedPHPVersion51() 266 public function testCheckSupportedPHPVersion51()
267 { 267 {
268 $this->expectException(\Exception::class);
269 $this->expectExceptionMessageRegExp('/Your PHP version is obsolete/');
270
268 $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.1.0')); 271 $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.1.0'));
269 } 272 }
270 273
271 /** 274 /**
272 * Check another unsupported PHP version 275 * Check another unsupported PHP version
273 * @expectedException Exception
274 * @expectedExceptionMessageRegExp /Your PHP version is obsolete/
275 */ 276 */
276 public function testCheckSupportedPHPVersion52() 277 public function testCheckSupportedPHPVersion52()
277 { 278 {
279 $this->expectException(\Exception::class);
280 $this->expectExceptionMessageRegExp('/Your PHP version is obsolete/');
281
278 $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.2')); 282 $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.2'));
279 } 283 }
280 284
@@ -337,6 +341,35 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
337 } 341 }
338 342
339 /** 343 /**
344 * Checks resource permissions in minimal mode.
345 */
346 public function testCheckCurrentResourcePermissionsErrorsMinimalMode(): void
347 {
348 $conf = new ConfigManager('');
349 $conf->set('resource.thumbnails_cache', 'null/cache');
350 $conf->set('resource.config', 'null/data/config.php');
351 $conf->set('resource.data_dir', 'null/data');
352 $conf->set('resource.datastore', 'null/data/store.php');
353 $conf->set('resource.ban_file', 'null/data/ipbans.php');
354 $conf->set('resource.log', 'null/data/log.txt');
355 $conf->set('resource.page_cache', 'null/pagecache');
356 $conf->set('resource.raintpl_tmp', 'null/tmp');
357 $conf->set('resource.raintpl_tpl', 'null/tpl');
358 $conf->set('resource.raintpl_theme', 'null/tpl/default');
359 $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt');
360
361 static::assertSame(
362 [
363 '"null/tpl" directory is not readable',
364 '"null/tpl/default" directory is not readable',
365 '"null/tmp" directory is not readable',
366 '"null/tmp" directory is not writable'
367 ],
368 ApplicationUtils::checkResourcePermissions($conf, true)
369 );
370 }
371
372 /**
340 * Check update with 'dev' as curent version (master branch). 373 * Check update with 'dev' as curent version (master branch).
341 * It should always return false. 374 * It should always return false.
342 */ 375 */
@@ -346,4 +379,37 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
346 ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true) 379 ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true)
347 ); 380 );
348 } 381 }
382
383 /**
384 * Basic test of getPhpExtensionsRequirement()
385 */
386 public function testGetPhpExtensionsRequirementSimple(): void
387 {
388 static::assertCount(8, ApplicationUtils::getPhpExtensionsRequirement());
389 static::assertSame([
390 'name' => 'json',
391 'required' => true,
392 'desc' => 'Configuration parsing',
393 'loaded' => true,
394 ], ApplicationUtils::getPhpExtensionsRequirement()[0]);
395 }
396
397 /**
398 * Test getPhpEol with a known version: 7.4 -> 2022
399 */
400 public function testGetKnownPhpEol(): void
401 {
402 static::assertSame('2022-11-28', ApplicationUtils::getPhpEol('7.4.7'));
403 }
404
405 /**
406 * Test getPhpEol with an unknown version: 7.4 -> 2022
407 */
408 public function testGetUnknownPhpEol(): void
409 {
410 static::assertSame(
411 (((int) (new \DateTime())->format('Y')) + 2) . (new \DateTime())->format('-m-d'),
412 ApplicationUtils::getPhpEol('7.51.34')
413 );
414 }
349} 415}
diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php
new file mode 100644
index 00000000..e0378491
--- /dev/null
+++ b/tests/helper/DailyPageHelperTest.php
@@ -0,0 +1,262 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Helper;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\TestCase;
9use Slim\Http\Request;
10
11class DailyPageHelperTest extends TestCase
12{
13 /**
14 * @dataProvider getRequestedTypes
15 */
16 public function testExtractRequestedType(array $queryParams, string $expectedType): void
17 {
18 $request = $this->createMock(Request::class);
19 $request->method('getQueryParam')->willReturnCallback(function ($key) use ($queryParams): ?string {
20 return $queryParams[$key] ?? null;
21 });
22
23 $type = DailyPageHelper::extractRequestedType($request);
24
25 static::assertSame($type, $expectedType);
26 }
27
28 /**
29 * @dataProvider getRequestedDateTimes
30 */
31 public function testExtractRequestedDateTime(
32 string $type,
33 string $input,
34 ?Bookmark $bookmark,
35 \DateTimeInterface $expectedDateTime,
36 string $compareFormat = 'Ymd'
37 ): void {
38 $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
39
40 static::assertSame($dateTime->format($compareFormat), $expectedDateTime->format($compareFormat));
41 }
42
43 public function testExtractRequestedDateTimeExceptionUnknownType(): void
44 {
45 $this->expectException(\Exception::class);
46 $this->expectExceptionMessage('Unsupported daily format type');
47
48 DailyPageHelper::extractRequestedDateTime('nope', null, null);
49 }
50
51 /**
52 * @dataProvider getFormatsByType
53 */
54 public function testGetFormatByType(string $type, string $expectedFormat): void
55 {
56 $format = DailyPageHelper::getFormatByType($type);
57
58 static::assertSame($expectedFormat, $format);
59 }
60
61 public function testGetFormatByTypeExceptionUnknownType(): void
62 {
63 $this->expectException(\Exception::class);
64 $this->expectExceptionMessage('Unsupported daily format type');
65
66 DailyPageHelper::getFormatByType('nope');
67 }
68
69 /**
70 * @dataProvider getStartDatesByType
71 */
72 public function testGetStartDatesByType(
73 string $type,
74 \DateTimeImmutable $dateTime,
75 \DateTimeInterface $expectedDateTime
76 ): void {
77 $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
78
79 static::assertEquals($expectedDateTime, $startDateTime);
80 }
81
82 public function testGetStartDatesByTypeExceptionUnknownType(): void
83 {
84 $this->expectException(\Exception::class);
85 $this->expectExceptionMessage('Unsupported daily format type');
86
87 DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable());
88 }
89
90 /**
91 * @dataProvider getEndDatesByType
92 */
93 public function testGetEndDatesByType(
94 string $type,
95 \DateTimeImmutable $dateTime,
96 \DateTimeInterface $expectedDateTime
97 ): void {
98 $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
99
100 static::assertEquals($expectedDateTime, $endDateTime);
101 }
102
103 public function testGetEndDatesByTypeExceptionUnknownType(): void
104 {
105 $this->expectException(\Exception::class);
106 $this->expectExceptionMessage('Unsupported daily format type');
107
108 DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable());
109 }
110
111 /**
112 * @dataProvider getDescriptionsByType
113 */
114 public function testGeDescriptionsByType(
115 string $type,
116 \DateTimeImmutable $dateTime,
117 string $expectedDescription
118 ): void {
119 $description = DailyPageHelper::getDescriptionByType($type, $dateTime);
120
121 static::assertEquals($expectedDescription, $description);
122 }
123
124 public function getDescriptionByTypeExceptionUnknownType(): void
125 {
126 $this->expectException(\Exception::class);
127 $this->expectExceptionMessage('Unsupported daily format type');
128
129 DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable());
130 }
131
132 /**
133 * @dataProvider getRssLengthsByType
134 */
135 public function testGeRssLengthsByType(string $type): void {
136 $length = DailyPageHelper::getRssLengthByType($type);
137
138 static::assertIsInt($length);
139 }
140
141 public function testGeRssLengthsByTypeExceptionUnknownType(): void
142 {
143 $this->expectException(\Exception::class);
144 $this->expectExceptionMessage('Unsupported daily format type');
145
146 DailyPageHelper::getRssLengthByType('nope');
147 }
148
149 /**
150 * Data provider for testExtractRequestedType() test method.
151 */
152 public function getRequestedTypes(): array
153 {
154 return [
155 [['month' => null], DailyPageHelper::DAY],
156 [['month' => ''], DailyPageHelper::MONTH],
157 [['month' => 'content'], DailyPageHelper::MONTH],
158 [['week' => null], DailyPageHelper::DAY],
159 [['week' => ''], DailyPageHelper::WEEK],
160 [['week' => 'content'], DailyPageHelper::WEEK],
161 [['day' => null], DailyPageHelper::DAY],
162 [['day' => ''], DailyPageHelper::DAY],
163 [['day' => 'content'], DailyPageHelper::DAY],
164 ];
165 }
166
167 /**
168 * Data provider for testExtractRequestedDateTime() test method.
169 */
170 public function getRequestedDateTimes(): array
171 {
172 return [
173 [DailyPageHelper::DAY, '20201013', null, new \DateTime('2020-10-13')],
174 [
175 DailyPageHelper::DAY,
176 '',
177 (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
178 $date,
179 ],
180 [DailyPageHelper::DAY, '', null, new \DateTime()],
181 [DailyPageHelper::WEEK, '202030', null, new \DateTime('2020-07-20')],
182 [
183 DailyPageHelper::WEEK,
184 '',
185 (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
186 new \DateTime('2020-10-13'),
187 ],
188 [DailyPageHelper::WEEK, '', null, new \DateTime(), 'Ym'],
189 [DailyPageHelper::MONTH, '202008', null, new \DateTime('2020-08-01'), 'Ym'],
190 [
191 DailyPageHelper::MONTH,
192 '',
193 (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')),
194 new \DateTime('2020-10-13'),
195 'Ym'
196 ],
197 [DailyPageHelper::MONTH, '', null, new \DateTime(), 'Ym'],
198 ];
199 }
200
201 /**
202 * Data provider for testGetFormatByType() test method.
203 */
204 public function getFormatsByType(): array
205 {
206 return [
207 [DailyPageHelper::DAY, 'Ymd'],
208 [DailyPageHelper::WEEK, 'YW'],
209 [DailyPageHelper::MONTH, 'Ym'],
210 ];
211 }
212
213 /**
214 * Data provider for testGetStartDatesByType() test method.
215 */
216 public function getStartDatesByType(): array
217 {
218 return [
219 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
220 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
221 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
222 ];
223 }
224
225 /**
226 * Data provider for testGetEndDatesByType() test method.
227 */
228 public function getEndDatesByType(): array
229 {
230 return [
231 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
232 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
233 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
234 ];
235 }
236
237 /**
238 * Data provider for testGetDescriptionsByType() test method.
239 */
240 public function getDescriptionsByType(): array
241 {
242 return [
243 [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F d, Y')],
244 [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F d, Y')],
245 [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
246 [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
247 [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
248 ];
249 }
250
251 /**
252 * Data provider for testGetDescriptionsByType() test method.
253 */
254 public function getRssLengthsByType(): array
255 {
256 return [
257 [DailyPageHelper::DAY],
258 [DailyPageHelper::WEEK],
259 [DailyPageHelper::MONTH],
260 ];
261 }
262}
diff --git a/tests/helper/FileUtilsTest.php b/tests/helper/FileUtilsTest.php
new file mode 100644
index 00000000..8035f79c
--- /dev/null
+++ b/tests/helper/FileUtilsTest.php
@@ -0,0 +1,197 @@
1<?php
2
3namespace Shaarli\Helper;
4
5use Exception;
6use Shaarli\Exceptions\IOException;
7use Shaarli\TestCase;
8
9/**
10 * Class FileUtilsTest
11 *
12 * Test file utility class.
13 */
14class FileUtilsTest extends TestCase
15{
16 /**
17 * @var string Test file path.
18 */
19 protected static $file = 'sandbox/flat.db';
20
21 protected function setUp(): void
22 {
23 @mkdir('sandbox');
24 mkdir('sandbox/folder2');
25 touch('sandbox/file1');
26 touch('sandbox/file2');
27 mkdir('sandbox/folder1');
28 touch('sandbox/folder1/file1');
29 touch('sandbox/folder1/file2');
30 mkdir('sandbox/folder3');
31 mkdir('/tmp/shaarli-to-delete');
32 }
33
34 /**
35 * Delete test file after every test.
36 */
37 protected function tearDown(): void
38 {
39 @unlink(self::$file);
40
41 @unlink('sandbox/folder1/file1');
42 @unlink('sandbox/folder1/file2');
43 @rmdir('sandbox/folder1');
44 @unlink('sandbox/file1');
45 @unlink('sandbox/file2');
46 @rmdir('sandbox/folder2');
47 @rmdir('sandbox/folder3');
48 @rmdir('/tmp/shaarli-to-delete');
49 }
50
51 /**
52 * Test writeDB, then readDB with different data.
53 */
54 public function testSimpleWriteRead()
55 {
56 $data = ['blue', 'red'];
57 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
58 $this->assertTrue(startsWith(file_get_contents(self::$file), '<?php /*'));
59 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
60
61 $data = 0;
62 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
63 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
64
65 $data = null;
66 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
67 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
68
69 $data = false;
70 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
71 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
72 }
73
74 /**
75 * File not writable: raise an exception.
76 */
77 public function testWriteWithoutPermission()
78 {
79 $this->expectException(\Shaarli\Exceptions\IOException::class);
80 $this->expectExceptionMessage('Error accessing "sandbox/flat.db"');
81
82 touch(self::$file);
83 chmod(self::$file, 0440);
84 FileUtils::writeFlatDB(self::$file, null);
85 }
86
87 /**
88 * Folder non existent: raise an exception.
89 */
90 public function testWriteFolderDoesNotExist()
91 {
92 $this->expectException(\Shaarli\Exceptions\IOException::class);
93 $this->expectExceptionMessage('Error accessing "nopefolder"');
94
95 FileUtils::writeFlatDB('nopefolder/file', null);
96 }
97
98 /**
99 * Folder non writable: raise an exception.
100 */
101 public function testWriteFolderPermission()
102 {
103 $this->expectException(\Shaarli\Exceptions\IOException::class);
104 $this->expectExceptionMessage('Error accessing "sandbox"');
105
106 chmod(dirname(self::$file), 0555);
107 try {
108 FileUtils::writeFlatDB(self::$file, null);
109 } catch (Exception $e) {
110 chmod(dirname(self::$file), 0755);
111 throw $e;
112 }
113 }
114
115 /**
116 * Read non existent file, use default parameter.
117 */
118 public function testReadNotExistentFile()
119 {
120 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
121 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
122 }
123
124 /**
125 * Read non readable file, use default parameter.
126 */
127 public function testReadNotReadable()
128 {
129 touch(self::$file);
130 chmod(self::$file, 0220);
131 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
132 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
133 }
134
135 /**
136 * Test clearFolder with self delete and excluded files
137 */
138 public function testClearFolderSelfDeleteWithExclusion(): void
139 {
140 FileUtils::clearFolder('sandbox', true, ['file2']);
141
142 static::assertFileExists('sandbox/folder1/file2');
143 static::assertFileExists('sandbox/folder1');
144 static::assertFileExists('sandbox/file2');
145 static::assertFileExists('sandbox');
146
147 static::assertFileNotExists('sandbox/folder1/file1');
148 static::assertFileNotExists('sandbox/file1');
149 static::assertFileNotExists('sandbox/folder3');
150 }
151
152 /**
153 * Test clearFolder with self delete and excluded files
154 */
155 public function testClearFolderSelfDeleteWithoutExclusion(): void
156 {
157 FileUtils::clearFolder('sandbox', true);
158
159 static::assertFileNotExists('sandbox');
160 }
161
162 /**
163 * Test clearFolder with self delete and excluded files
164 */
165 public function testClearFolderNoSelfDeleteWithoutExclusion(): void
166 {
167 FileUtils::clearFolder('sandbox', false);
168
169 static::assertFileExists('sandbox');
170
171 // 2 because '.' and '..'
172 static::assertCount(2, new \DirectoryIterator('sandbox'));
173 }
174
175 /**
176 * Test clearFolder on a file instead of a folder
177 */
178 public function testClearFolderOnANonDirectory(): void
179 {
180 $this->expectException(IOException::class);
181 $this->expectExceptionMessage('Provided path is not a directory.');
182
183 FileUtils::clearFolder('sandbox/file1', false);
184 }
185
186 /**
187 * Test clearFolder on a file instead of a folder
188 */
189 public function testClearFolderOutsideOfShaarliDirectory(): void
190 {
191 $this->expectException(IOException::class);
192 $this->expectExceptionMessage('Trying to delete a folder outside of Shaarli path.');
193
194
195 FileUtils::clearFolder('/tmp/shaarli-to-delete', true);
196 }
197}
diff --git a/tests/http/HttpUtils/ClientIpIdTest.php b/tests/http/HttpUtils/ClientIpIdTest.php
index 982e57e0..3a0fcf30 100644
--- a/tests/http/HttpUtils/ClientIpIdTest.php
+++ b/tests/http/HttpUtils/ClientIpIdTest.php
@@ -10,7 +10,7 @@ require_once 'application/http/HttpUtils.php';
10/** 10/**
11 * Unitary tests for client_ip_id() 11 * Unitary tests for client_ip_id()
12 */ 12 */
13class ClientIpIdTest extends \PHPUnit\Framework\TestCase 13class ClientIpIdTest extends \Shaarli\TestCase
14{ 14{
15 /** 15 /**
16 * Get a remote client ID based on its IP 16 * Get a remote client ID based on its IP
diff --git a/tests/http/HttpUtils/GetHttpUrlTest.php b/tests/http/HttpUtils/GetHttpUrlTest.php
index 3dc5bc9b..a868ac02 100644
--- a/tests/http/HttpUtils/GetHttpUrlTest.php
+++ b/tests/http/HttpUtils/GetHttpUrlTest.php
@@ -10,7 +10,7 @@ require_once 'application/http/HttpUtils.php';
10/** 10/**
11 * Unitary tests for get_http_response() 11 * Unitary tests for get_http_response()
12 */ 12 */
13class GetHttpUrlTest extends \PHPUnit\Framework\TestCase 13class GetHttpUrlTest extends \Shaarli\TestCase
14{ 14{
15 /** 15 /**
16 * Get an invalid local URL 16 * Get an invalid local URL
diff --git a/tests/http/HttpUtils/GetIpAdressFromProxyTest.php b/tests/http/HttpUtils/GetIpAdressFromProxyTest.php
index fe3a639e..60cdb992 100644
--- a/tests/http/HttpUtils/GetIpAdressFromProxyTest.php
+++ b/tests/http/HttpUtils/GetIpAdressFromProxyTest.php
@@ -7,7 +7,7 @@ require_once 'application/http/HttpUtils.php';
7/** 7/**
8 * Unitary tests for getIpAddressFromProxy() 8 * Unitary tests for getIpAddressFromProxy()
9 */ 9 */
10class GetIpAdressFromProxyTest extends \PHPUnit\Framework\TestCase 10class GetIpAdressFromProxyTest extends \Shaarli\TestCase
11{ 11{
12 12
13 /** 13 /**
diff --git a/tests/http/HttpUtils/IndexUrlTest.php b/tests/http/HttpUtils/IndexUrlTest.php
index bcbe59cb..f283d119 100644
--- a/tests/http/HttpUtils/IndexUrlTest.php
+++ b/tests/http/HttpUtils/IndexUrlTest.php
@@ -5,12 +5,14 @@
5 5
6namespace Shaarli\Http; 6namespace Shaarli\Http;
7 7
8use Shaarli\TestCase;
9
8require_once 'application/http/HttpUtils.php'; 10require_once 'application/http/HttpUtils.php';
9 11
10/** 12/**
11 * Unitary tests for index_url() 13 * Unitary tests for index_url()
12 */ 14 */
13class IndexUrlTest extends \PHPUnit\Framework\TestCase 15class IndexUrlTest extends TestCase
14{ 16{
15 /** 17 /**
16 * If on the main page, remove "index.php" from the URL resource 18 * If on the main page, remove "index.php" from the URL resource
@@ -71,4 +73,68 @@ class IndexUrlTest extends \PHPUnit\Framework\TestCase
71 ) 73 )
72 ); 74 );
73 } 75 }
76
77 /**
78 * The route is stored in REQUEST_URI
79 */
80 public function testPageUrlWithRoute()
81 {
82 $this->assertEquals(
83 'http://host.tld/picture-wall',
84 page_url(
85 array(
86 'HTTPS' => 'Off',
87 'SERVER_NAME' => 'host.tld',
88 'SERVER_PORT' => '80',
89 'SCRIPT_NAME' => '/index.php',
90 'REQUEST_URI' => '/picture-wall',
91 )
92 )
93 );
94
95 $this->assertEquals(
96 'http://host.tld/admin/picture-wall',
97 page_url(
98 array(
99 'HTTPS' => 'Off',
100 'SERVER_NAME' => 'host.tld',
101 'SERVER_PORT' => '80',
102 'SCRIPT_NAME' => '/admin/index.php',
103 'REQUEST_URI' => '/admin/picture-wall',
104 )
105 )
106 );
107 }
108
109 /**
110 * The route is stored in REQUEST_URI and subfolder
111 */
112 public function testPageUrlWithRouteUnderSubfolder()
113 {
114 $this->assertEquals(
115 'http://host.tld/subfolder/picture-wall',
116 page_url(
117 array(
118 'HTTPS' => 'Off',
119 'SERVER_NAME' => 'host.tld',
120 'SERVER_PORT' => '80',
121 'SCRIPT_NAME' => '/subfolder/index.php',
122 'REQUEST_URI' => '/subfolder/picture-wall',
123 )
124 )
125 );
126
127 $this->assertEquals(
128 'http://host.tld/subfolder/admin/picture-wall',
129 page_url(
130 array(
131 'HTTPS' => 'Off',
132 'SERVER_NAME' => 'host.tld',
133 'SERVER_PORT' => '80',
134 'SCRIPT_NAME' => '/subfolder/admin/index.php',
135 'REQUEST_URI' => '/subfolder/admin/picture-wall',
136 )
137 )
138 );
139 }
74} 140}
diff --git a/tests/http/HttpUtils/IndexUrlTestWithConstant.php b/tests/http/HttpUtils/IndexUrlTestWithConstant.php
new file mode 100644
index 00000000..ecaea724
--- /dev/null
+++ b/tests/http/HttpUtils/IndexUrlTestWithConstant.php
@@ -0,0 +1,51 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Http;
6
7use Shaarli\TestCase;
8
9/**
10 * Test index_url with SHAARLI_ROOT_URL defined to override automatic retrieval.
11 * This should stay in its dedicated class to make sure to not alter other tests of the suite.
12 */
13class IndexUrlTestWithConstant extends TestCase
14{
15 public static function setUpBeforeClass(): void
16 {
17 define('SHAARLI_ROOT_URL', 'http://other-host.tld/subfolder/');
18 }
19
20 /**
21 * The route is stored in REQUEST_URI and subfolder
22 */
23 public function testIndexUrlWithConstantDefined()
24 {
25 $this->assertEquals(
26 'http://other-host.tld/subfolder/',
27 index_url(
28 array(
29 'HTTPS' => 'Off',
30 'SERVER_NAME' => 'host.tld',
31 'SERVER_PORT' => '80',
32 'SCRIPT_NAME' => '/index.php',
33 'REQUEST_URI' => '/picture-wall',
34 )
35 )
36 );
37
38 $this->assertEquals(
39 'http://other-host.tld/subfolder/',
40 index_url(
41 array(
42 'HTTPS' => 'Off',
43 'SERVER_NAME' => 'host.tld',
44 'SERVER_PORT' => '80',
45 'SCRIPT_NAME' => '/admin/index.php',
46 'REQUEST_URI' => '/admin/picture-wall',
47 )
48 )
49 );
50 }
51}
diff --git a/tests/http/HttpUtils/IsHttpsTest.php b/tests/http/HttpUtils/IsHttpsTest.php
index 348956c6..8b3fd93d 100644
--- a/tests/http/HttpUtils/IsHttpsTest.php
+++ b/tests/http/HttpUtils/IsHttpsTest.php
@@ -9,7 +9,7 @@ require_once 'application/http/HttpUtils.php';
9 * 9 *
10 * Test class for is_https() function. 10 * Test class for is_https() function.
11 */ 11 */
12class IsHttpsTest extends \PHPUnit\Framework\TestCase 12class IsHttpsTest extends \Shaarli\TestCase
13{ 13{
14 14
15 /** 15 /**
diff --git a/tests/http/HttpUtils/PageUrlTest.php b/tests/http/HttpUtils/PageUrlTest.php
index f1991716..ebb3e617 100644
--- a/tests/http/HttpUtils/PageUrlTest.php
+++ b/tests/http/HttpUtils/PageUrlTest.php
@@ -10,7 +10,7 @@ require_once 'application/http/HttpUtils.php';
10/** 10/**
11 * Unitary tests for page_url() 11 * Unitary tests for page_url()
12 */ 12 */
13class PageUrlTest extends \PHPUnit\Framework\TestCase 13class PageUrlTest extends \Shaarli\TestCase
14{ 14{
15 /** 15 /**
16 * If on the main page, remove "index.php" from the URL resource 16 * If on the main page, remove "index.php" from the URL resource
diff --git a/tests/http/HttpUtils/ServerUrlTest.php b/tests/http/HttpUtils/ServerUrlTest.php
index 9caf1049..339664e1 100644
--- a/tests/http/HttpUtils/ServerUrlTest.php
+++ b/tests/http/HttpUtils/ServerUrlTest.php
@@ -10,7 +10,7 @@ require_once 'application/http/HttpUtils.php';
10/** 10/**
11 * Unitary tests for server_url() 11 * Unitary tests for server_url()
12 */ 12 */
13class ServerUrlTest extends \PHPUnit\Framework\TestCase 13class ServerUrlTest extends \Shaarli\TestCase
14{ 14{
15 /** 15 /**
16 * Detect if the server uses SSL 16 * Detect if the server uses SSL
diff --git a/tests/http/MetadataRetrieverTest.php b/tests/http/MetadataRetrieverTest.php
new file mode 100644
index 00000000..3c9eaa0e
--- /dev/null
+++ b/tests/http/MetadataRetrieverTest.php
@@ -0,0 +1,154 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Http;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9
10class MetadataRetrieverTest extends TestCase
11{
12 /** @var MetadataRetriever */
13 protected $retriever;
14
15 /** @var ConfigManager */
16 protected $conf;
17
18 /** @var HttpAccess */
19 protected $httpAccess;
20
21 public function setUp(): void
22 {
23 $this->conf = $this->createMock(ConfigManager::class);
24 $this->httpAccess = $this->createMock(HttpAccess::class);
25 $this->retriever = new MetadataRetriever($this->conf, $this->httpAccess);
26
27 $this->conf->method('get')->willReturnCallback(function (string $param, $default) {
28 return $default === null ? $param : $default;
29 });
30 }
31
32 /**
33 * Test metadata retrieve() with values returned
34 */
35 public function testFullRetrieval(): void
36 {
37 $url = 'https://domain.tld/link';
38 $remoteTitle = 'Remote Title ';
39 $remoteDesc = 'Sometimes the meta description is relevant.';
40 $remoteTags = 'abc def';
41 $remoteCharset = 'utf-8';
42
43 $expectedResult = [
44 'title' => $remoteTitle,
45 'description' => $remoteDesc,
46 'tags' => $remoteTags,
47 ];
48
49 $this->httpAccess
50 ->expects(static::once())
51 ->method('getCurlHeaderCallback')
52 ->willReturnCallback(
53 function (&$charset) use (
54 $remoteCharset
55 ): callable {
56 return function () use (
57 &$charset,
58 $remoteCharset
59 ): void {
60 $charset = $remoteCharset;
61 };
62 }
63 )
64 ;
65 $this->httpAccess
66 ->expects(static::once())
67 ->method('getCurlDownloadCallback')
68 ->willReturnCallback(
69 function (&$charset, &$title, &$description, &$tags) use (
70 $remoteCharset,
71 $remoteTitle,
72 $remoteDesc,
73 $remoteTags
74 ): callable {
75 return function () use (
76 &$charset,
77 &$title,
78 &$description,
79 &$tags,
80 $remoteCharset,
81 $remoteTitle,
82 $remoteDesc,
83 $remoteTags
84 ): void {
85 static::assertSame($remoteCharset, $charset);
86
87 $title = $remoteTitle;
88 $description = $remoteDesc;
89 $tags = $remoteTags;
90 };
91 }
92 )
93 ;
94 $this->httpAccess
95 ->expects(static::once())
96 ->method('getHttpResponse')
97 ->with($url, 30, 4194304)
98 ->willReturnCallback(function($url, $timeout, $maxBytes, $headerCallback, $dlCallback): void {
99 $headerCallback();
100 $dlCallback();
101 })
102 ;
103
104 $result = $this->retriever->retrieve($url);
105
106 static::assertSame($expectedResult, $result);
107 }
108
109 /**
110 * Test metadata retrieve() without any value
111 */
112 public function testEmptyRetrieval(): void
113 {
114 $url = 'https://domain.tld/link';
115
116 $expectedResult = [
117 'title' => null,
118 'description' => null,
119 'tags' => null,
120 ];
121
122 $this->httpAccess
123 ->expects(static::once())
124 ->method('getCurlDownloadCallback')
125 ->willReturnCallback(
126 function (): callable {
127 return function (): void {};
128 }
129 )
130 ;
131 $this->httpAccess
132 ->expects(static::once())
133 ->method('getCurlHeaderCallback')
134 ->willReturnCallback(
135 function (): callable {
136 return function (): void {};
137 }
138 )
139 ;
140 $this->httpAccess
141 ->expects(static::once())
142 ->method('getHttpResponse')
143 ->with($url, 30, 4194304)
144 ->willReturnCallback(function($url, $timeout, $maxBytes, $headerCallback, $dlCallback): void {
145 $headerCallback();
146 $dlCallback();
147 })
148 ;
149
150 $result = $this->retriever->retrieve($url);
151
152 static::assertSame($expectedResult, $result);
153 }
154}
diff --git a/tests/http/UrlTest.php b/tests/http/UrlTest.php
index ae92f73a..c6b39c29 100644
--- a/tests/http/UrlTest.php
+++ b/tests/http/UrlTest.php
@@ -8,7 +8,7 @@ namespace Shaarli\Http;
8/** 8/**
9 * Unitary tests for URL utilities 9 * Unitary tests for URL utilities
10 */ 10 */
11class UrlTest extends \PHPUnit\Framework\TestCase 11class UrlTest extends \Shaarli\TestCase
12{ 12{
13 // base URL for tests 13 // base URL for tests
14 protected static $baseUrl = 'http://domain.tld:3000'; 14 protected static $baseUrl = 'http://domain.tld:3000';
diff --git a/tests/http/UrlUtils/CleanupUrlTest.php b/tests/http/UrlUtils/CleanupUrlTest.php
index 6c4d124b..45690ecf 100644
--- a/tests/http/UrlUtils/CleanupUrlTest.php
+++ b/tests/http/UrlUtils/CleanupUrlTest.php
@@ -7,7 +7,7 @@ namespace Shaarli\Http;
7 7
8require_once 'application/http/UrlUtils.php'; 8require_once 'application/http/UrlUtils.php';
9 9
10class CleanupUrlTest extends \PHPUnit\Framework\TestCase 10class CleanupUrlTest extends \Shaarli\TestCase
11{ 11{
12 /** 12 /**
13 * @var string reference URL 13 * @var string reference URL
diff --git a/tests/http/UrlUtils/GetUrlSchemeTest.php b/tests/http/UrlUtils/GetUrlSchemeTest.php
index 2b97f7be..18a9a5e5 100644
--- a/tests/http/UrlUtils/GetUrlSchemeTest.php
+++ b/tests/http/UrlUtils/GetUrlSchemeTest.php
@@ -7,7 +7,7 @@ namespace Shaarli\Http;
7 7
8require_once 'application/http/UrlUtils.php'; 8require_once 'application/http/UrlUtils.php';
9 9
10class GetUrlSchemeTest extends \PHPUnit\Framework\TestCase 10class GetUrlSchemeTest extends \Shaarli\TestCase
11{ 11{
12 /** 12 /**
13 * Get empty scheme string for empty UrlUtils 13 * Get empty scheme string for empty UrlUtils
diff --git a/tests/http/UrlUtils/UnparseUrlTest.php b/tests/http/UrlUtils/UnparseUrlTest.php
index 040d8c54..5e6246cc 100644
--- a/tests/http/UrlUtils/UnparseUrlTest.php
+++ b/tests/http/UrlUtils/UnparseUrlTest.php
@@ -10,7 +10,7 @@ require_once 'application/http/UrlUtils.php';
10/** 10/**
11 * Unitary tests for unparse_url() 11 * Unitary tests for unparse_url()
12 */ 12 */
13class UnparseUrlTest extends \PHPUnit\Framework\TestCase 13class UnparseUrlTest extends \Shaarli\TestCase
14{ 14{
15 /** 15 /**
16 * Thanks for building nothing 16 * Thanks for building nothing
diff --git a/tests/http/UrlUtils/WhitelistProtocolsTest.php b/tests/http/UrlUtils/WhitelistProtocolsTest.php
index 69512dbd..b8a6baaa 100644
--- a/tests/http/UrlUtils/WhitelistProtocolsTest.php
+++ b/tests/http/UrlUtils/WhitelistProtocolsTest.php
@@ -9,7 +9,7 @@ require_once 'application/http/UrlUtils.php';
9 * 9 *
10 * Test whitelist_protocols() function of UrlUtils. 10 * Test whitelist_protocols() function of UrlUtils.
11 */ 11 */
12class WhitelistProtocolsTest extends \PHPUnit\Framework\TestCase 12class WhitelistProtocolsTest extends \Shaarli\TestCase
13{ 13{
14 /** 14 /**
15 * Test whitelist_protocols() on a note (relative URL). 15 * Test whitelist_protocols() on a note (relative URL).
diff --git a/tests/languages/fr/LanguagesFrTest.php b/tests/languages/fr/LanguagesFrTest.php
index b8b7ca3a..d84feed1 100644
--- a/tests/languages/fr/LanguagesFrTest.php
+++ b/tests/languages/fr/LanguagesFrTest.php
@@ -12,7 +12,7 @@ use Shaarli\Config\ConfigManager;
12 * 12 *
13 * @package Shaarli 13 * @package Shaarli
14 */ 14 */
15class LanguagesFrTest extends \PHPUnit\Framework\TestCase 15class LanguagesFrTest extends \Shaarli\TestCase
16{ 16{
17 /** 17 /**
18 * @var string Config file path (without extension). 18 * @var string Config file path (without extension).
@@ -27,7 +27,7 @@ class LanguagesFrTest extends \PHPUnit\Framework\TestCase
27 /** 27 /**
28 * Init: force French 28 * Init: force French
29 */ 29 */
30 public function setUp() 30 protected function setUp(): void
31 { 31 {
32 $this->conf = new ConfigManager(self::$configFile); 32 $this->conf = new ConfigManager(self::$configFile);
33 $this->conf->set('translation.language', 'fr'); 33 $this->conf->set('translation.language', 'fr');
@@ -36,7 +36,7 @@ class LanguagesFrTest extends \PHPUnit\Framework\TestCase
36 /** 36 /**
37 * Reset the locale since gettext seems to mess with it, making it too long 37 * Reset the locale since gettext seems to mess with it, making it too long
38 */ 38 */
39 public static function tearDownAfterClass() 39 public static function tearDownAfterClass(): void
40 { 40 {
41 if (! empty(getenv('UT_LOCALE'))) { 41 if (! empty(getenv('UT_LOCALE'))) {
42 setlocale(LC_ALL, getenv('UT_LOCALE')); 42 setlocale(LC_ALL, getenv('UT_LOCALE'));
diff --git a/tests/legacy/LegacyControllerTest.php b/tests/legacy/LegacyControllerTest.php
new file mode 100644
index 00000000..1a2549a3
--- /dev/null
+++ b/tests/legacy/LegacyControllerTest.php
@@ -0,0 +1,101 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Legacy;
6
7use Shaarli\Front\Controller\Visitor\FrontControllerMockHelper;
8use Shaarli\TestCase;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class LegacyControllerTest extends TestCase
13{
14 use FrontControllerMockHelper;
15
16 /** @var LegacyController */
17 protected $controller;
18
19 public function setUp(): void
20 {
21 $this->createContainer();
22
23 $this->controller = new LegacyController($this->container);
24 }
25
26 /**
27 * @dataProvider getProcessProvider
28 */
29 public function testProcess(string $legacyRoute, array $queryParameters, string $slimRoute, bool $isLoggedIn): void
30 {
31 $request = $this->createMock(Request::class);
32 $request->method('getQueryParams')->willReturn($queryParameters);
33 $request
34 ->method('getParam')
35 ->willReturnCallback(function (string $key) use ($queryParameters): ?string {
36 return $queryParameters[$key] ?? null;
37 })
38 ;
39 $response = new Response();
40
41 $this->container->loginManager->method('isLoggedIn')->willReturn($isLoggedIn);
42
43 $result = $this->controller->process($request, $response, $legacyRoute);
44
45 static::assertSame('/subfolder' . $slimRoute, $result->getHeader('location')[0]);
46 }
47
48 public function testProcessNotFound(): void
49 {
50 $request = $this->createMock(Request::class);
51 $response = new Response();
52
53 $this->expectException(UnknowLegacyRouteException::class);
54
55 $this->controller->process($request, $response, 'nope');
56 }
57
58 /**
59 * @return array[] Parameters:
60 * - string legacyRoute
61 * - array queryParameters
62 * - string slimRoute
63 * - bool isLoggedIn
64 */
65 public function getProcessProvider(): array
66 {
67 return [
68 ['post', [], '/admin/shaare', true],
69 ['post', [], '/login?returnurl=/subfolder/admin/shaare', false],
70 ['post', ['title' => 'test'], '/admin/shaare?title=test', true],
71 ['post', ['title' => 'test'], '/login?returnurl=/subfolder/admin/shaare?title=test', false],
72 ['addlink', [], '/admin/add-shaare', true],
73 ['addlink', [], '/login?returnurl=/subfolder/admin/add-shaare', false],
74 ['login', [], '/login', true],
75 ['login', [], '/login', false],
76 ['logout', [], '/admin/logout', true],
77 ['logout', [], '/admin/logout', false],
78 ['picwall', [], '/picture-wall', false],
79 ['picwall', [], '/picture-wall', true],
80 ['tagcloud', [], '/tags/cloud', false],
81 ['tagcloud', [], '/tags/cloud', true],
82 ['taglist', [], '/tags/list', false],
83 ['taglist', [], '/tags/list', true],
84 ['daily', [], '/daily', false],
85 ['daily', [], '/daily', true],
86 ['daily', ['day' => '123456789', 'discard' => '1'], '/daily?day=123456789', false],
87 ['rss', [], '/feed/rss', false],
88 ['rss', [], '/feed/rss', true],
89 ['rss', ['search' => 'filter123', 'other' => 'param'], '/feed/rss?search=filter123&other=param', false],
90 ['atom', [], '/feed/atom', false],
91 ['atom', [], '/feed/atom', true],
92 ['atom', ['search' => 'filter123', 'other' => 'param'], '/feed/atom?search=filter123&other=param', false],
93 ['opensearch', [], '/open-search', false],
94 ['opensearch', [], '/open-search', true],
95 ['dailyrss', [], '/daily-rss', false],
96 ['dailyrss', [], '/daily-rss', true],
97 ['configure', [], '/login?returnurl=/subfolder/admin/configure', false],
98 ['configure', [], '/admin/configure', true],
99 ];
100 }
101}
diff --git a/tests/legacy/LegacyLinkDBTest.php b/tests/legacy/LegacyLinkDBTest.php
index 17b2b0e6..5c3fd425 100644
--- a/tests/legacy/LegacyLinkDBTest.php
+++ b/tests/legacy/LegacyLinkDBTest.php
@@ -11,7 +11,6 @@ use ReflectionClass;
11use Shaarli; 11use Shaarli;
12use Shaarli\Bookmark\Bookmark; 12use Shaarli\Bookmark\Bookmark;
13 13
14require_once 'application/feed/Cache.php';
15require_once 'application/Utils.php'; 14require_once 'application/Utils.php';
16require_once 'tests/utils/ReferenceLinkDB.php'; 15require_once 'tests/utils/ReferenceLinkDB.php';
17 16
@@ -19,7 +18,7 @@ require_once 'tests/utils/ReferenceLinkDB.php';
19/** 18/**
20 * Unitary tests for LegacyLinkDBTest 19 * Unitary tests for LegacyLinkDBTest
21 */ 20 */
22class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase 21class LegacyLinkDBTest extends \Shaarli\TestCase
23{ 22{
24 // datastore to test write operations 23 // datastore to test write operations
25 protected static $testDatastore = 'sandbox/datastore.php'; 24 protected static $testDatastore = 'sandbox/datastore.php';
@@ -53,7 +52,7 @@ class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
53 * 52 *
54 * Resets test data for each test 53 * Resets test data for each test
55 */ 54 */
56 protected function setUp() 55 protected function setUp(): void
57 { 56 {
58 if (file_exists(self::$testDatastore)) { 57 if (file_exists(self::$testDatastore)) {
59 unlink(self::$testDatastore); 58 unlink(self::$testDatastore);
@@ -100,12 +99,12 @@ class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
100 99
101 /** 100 /**
102 * Attempt to instantiate a LinkDB whereas the datastore is not writable 101 * Attempt to instantiate a LinkDB whereas the datastore is not writable
103 *
104 * @expectedException Shaarli\Exceptions\IOException
105 * @expectedExceptionMessageRegExp /Error accessing "null"/
106 */ 102 */
107 public function testConstructDatastoreNotWriteable() 103 public function testConstructDatastoreNotWriteable()
108 { 104 {
105 $this->expectException(\Shaarli\Exceptions\IOException::class);
106 $this->expectExceptionMessageRegExp('/Error accessing "null"/');
107
109 new LegacyLinkDB('null/store.db', false, false); 108 new LegacyLinkDB('null/store.db', false, false);
110 } 109 }
111 110
@@ -258,7 +257,7 @@ class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
258 $link = self::$publicLinkDB->getLinkFromUrl('http://mediagoblin.org/'); 257 $link = self::$publicLinkDB->getLinkFromUrl('http://mediagoblin.org/');
259 258
260 $this->assertNotEquals(false, $link); 259 $this->assertNotEquals(false, $link);
261 $this->assertContains( 260 $this->assertContainsPolyfill(
262 'A free software media publishing platform', 261 'A free software media publishing platform',
263 $link['description'] 262 $link['description']
264 ); 263 );
@@ -297,6 +296,10 @@ class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
297 // They need to be grouped with the first case found - order by date DESC: `sTuff`. 296 // They need to be grouped with the first case found - order by date DESC: `sTuff`.
298 'sTuff' => 2, 297 'sTuff' => 2,
299 'ut' => 1, 298 'ut' => 1,
299 'assurance' => 1,
300 'coding-style' => 1,
301 'quality' => 1,
302 'standards' => 1,
300 ), 303 ),
301 self::$publicLinkDB->linksCountPerTag() 304 self::$publicLinkDB->linksCountPerTag()
302 ); 305 );
@@ -325,6 +328,10 @@ class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
325 'tag3' => 1, 328 'tag3' => 1,
326 'tag4' => 1, 329 'tag4' => 1,
327 'ut' => 1, 330 'ut' => 1,
331 'assurance' => 1,
332 'coding-style' => 1,
333 'quality' => 1,
334 'standards' => 1,
328 ), 335 ),
329 self::$privateLinkDB->linksCountPerTag() 336 self::$privateLinkDB->linksCountPerTag()
330 ); 337 );
@@ -421,22 +428,22 @@ class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
421 428
422 /** 429 /**
423 * Test filterHash() with an invalid smallhash. 430 * Test filterHash() with an invalid smallhash.
424 *
425 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
426 */ 431 */
427 public function testFilterHashInValid1() 432 public function testFilterHashInValid1()
428 { 433 {
434 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
435
429 $request = 'blabla'; 436 $request = 'blabla';
430 self::$publicLinkDB->filterHash($request); 437 self::$publicLinkDB->filterHash($request);
431 } 438 }
432 439
433 /** 440 /**
434 * Test filterHash() with an empty smallhash. 441 * Test filterHash() with an empty smallhash.
435 *
436 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
437 */ 442 */
438 public function testFilterHashInValid() 443 public function testFilterHashInValid()
439 { 444 {
445 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
446
440 self::$publicLinkDB->filterHash(''); 447 self::$publicLinkDB->filterHash('');
441 } 448 }
442 449
@@ -471,9 +478,9 @@ class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
471 478
472 $res = $linkDB->renameTag('cartoon', 'Taz'); 479 $res = $linkDB->renameTag('cartoon', 'Taz');
473 $this->assertEquals(3, count($res)); 480 $this->assertEquals(3, count($res));
474 $this->assertContains(' Taz ', $linkDB[4]['tags']); 481 $this->assertContainsPolyfill(' Taz ', $linkDB[4]['tags']);
475 $this->assertContains(' Taz ', $linkDB[1]['tags']); 482 $this->assertContainsPolyfill(' Taz ', $linkDB[1]['tags']);
476 $this->assertContains(' Taz ', $linkDB[0]['tags']); 483 $this->assertContainsPolyfill(' Taz ', $linkDB[0]['tags']);
477 } 484 }
478 485
479 /** 486 /**
@@ -513,7 +520,7 @@ class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
513 520
514 $res = $linkDB->renameTag('cartoon', null); 521 $res = $linkDB->renameTag('cartoon', null);
515 $this->assertEquals(3, count($res)); 522 $this->assertEquals(3, count($res));
516 $this->assertNotContains('cartoon', $linkDB[4]['tags']); 523 $this->assertNotContainsPolyfill('cartoon', $linkDB[4]['tags']);
517 } 524 }
518 525
519 /** 526 /**
@@ -545,6 +552,10 @@ class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
545 'tag4' => 1, 552 'tag4' => 1,
546 'ut' => 1, 553 'ut' => 1,
547 'w3c' => 1, 554 'w3c' => 1,
555 'assurance' => 1,
556 'coding-style' => 1,
557 'quality' => 1,
558 'standards' => 1,
548 ]; 559 ];
549 $tags = self::$privateLinkDB->linksCountPerTag(); 560 $tags = self::$privateLinkDB->linksCountPerTag();
550 561
diff --git a/tests/legacy/LegacyLinkFilterTest.php b/tests/legacy/LegacyLinkFilterTest.php
index ba9ec529..45d7754d 100644
--- a/tests/legacy/LegacyLinkFilterTest.php
+++ b/tests/legacy/LegacyLinkFilterTest.php
@@ -10,7 +10,7 @@ use Shaarli\Legacy\LegacyLinkFilter;
10/** 10/**
11 * Class LegacyLinkFilterTest. 11 * Class LegacyLinkFilterTest.
12 */ 12 */
13class LegacyLinkFilterTest extends \PHPUnit\Framework\TestCase 13class LegacyLinkFilterTest extends \Shaarli\TestCase
14{ 14{
15 /** 15 /**
16 * @var string Test datastore path. 16 * @var string Test datastore path.
@@ -34,7 +34,7 @@ class LegacyLinkFilterTest extends \PHPUnit\Framework\TestCase
34 /** 34 /**
35 * Instantiate linkFilter with ReferenceLinkDB data. 35 * Instantiate linkFilter with ReferenceLinkDB data.
36 */ 36 */
37 public static function setUpBeforeClass() 37 public static function setUpBeforeClass(): void
38 { 38 {
39 self::$refDB = new ReferenceLinkDB(true); 39 self::$refDB = new ReferenceLinkDB(true);
40 self::$refDB->write(self::$testDatastore); 40 self::$refDB->write(self::$testDatastore);
@@ -197,21 +197,23 @@ class LegacyLinkFilterTest extends \PHPUnit\Framework\TestCase
197 197
198 /** 198 /**
199 * Use an invalid date format 199 * Use an invalid date format
200 * @expectedException Exception
201 * @expectedExceptionMessageRegExp /Invalid date format/
202 */ 200 */
203 public function testFilterInvalidDayWithChars() 201 public function testFilterInvalidDayWithChars()
204 { 202 {
203 $this->expectException(\Exception::class);
204 $this->expectExceptionMessageRegExp('/Invalid date format/');
205
205 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, 'Rainy day, dream away'); 206 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, 'Rainy day, dream away');
206 } 207 }
207 208
208 /** 209 /**
209 * Use an invalid date format 210 * Use an invalid date format
210 * @expectedException Exception
211 * @expectedExceptionMessageRegExp /Invalid date format/
212 */ 211 */
213 public function testFilterInvalidDayDigits() 212 public function testFilterInvalidDayDigits()
214 { 213 {
214 $this->expectException(\Exception::class);
215 $this->expectExceptionMessageRegExp('/Invalid date format/');
216
215 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '20'); 217 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '20');
216 } 218 }
217 219
@@ -235,11 +237,11 @@ class LegacyLinkFilterTest extends \PHPUnit\Framework\TestCase
235 237
236 /** 238 /**
237 * No link for this hash 239 * No link for this hash
238 *
239 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
240 */ 240 */
241 public function testFilterUnknownSmallHash() 241 public function testFilterUnknownSmallHash()
242 { 242 {
243 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
244
243 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, 'Iblaah'); 245 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, 'Iblaah');
244 } 246 }
245 247
diff --git a/tests/legacy/LegacyUpdaterTest.php b/tests/legacy/LegacyUpdaterTest.php
index 7c429811..f7391b86 100644
--- a/tests/legacy/LegacyUpdaterTest.php
+++ b/tests/legacy/LegacyUpdaterTest.php
@@ -20,7 +20,7 @@ require_once 'inc/rain.tpl.class.php';
20 * Class UpdaterTest. 20 * Class UpdaterTest.
21 * Runs unit tests against the updater class. 21 * Runs unit tests against the updater class.
22 */ 22 */
23class LegacyUpdaterTest extends \PHPUnit\Framework\TestCase 23class LegacyUpdaterTest extends \Shaarli\TestCase
24{ 24{
25 /** 25 /**
26 * @var string Path to test datastore. 26 * @var string Path to test datastore.
@@ -40,7 +40,7 @@ class LegacyUpdaterTest extends \PHPUnit\Framework\TestCase
40 /** 40 /**
41 * Executed before each test. 41 * Executed before each test.
42 */ 42 */
43 public function setUp() 43 protected function setUp(): void
44 { 44 {
45 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); 45 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
46 $this->conf = new ConfigManager(self::$configFile); 46 $this->conf = new ConfigManager(self::$configFile);
@@ -80,23 +80,23 @@ class LegacyUpdaterTest extends \PHPUnit\Framework\TestCase
80 80
81 /** 81 /**
82 * Test errors in UpdaterUtils::write_updates_file(): empty updates file. 82 * Test errors in UpdaterUtils::write_updates_file(): empty updates file.
83 *
84 * @expectedException Exception
85 * @expectedExceptionMessageRegExp /Updates file path is not set(.*)/
86 */ 83 */
87 public function testWriteEmptyUpdatesFile() 84 public function testWriteEmptyUpdatesFile()
88 { 85 {
86 $this->expectException(\Exception::class);
87 $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/');
88
89 UpdaterUtils::write_updates_file('', array('test')); 89 UpdaterUtils::write_updates_file('', array('test'));
90 } 90 }
91 91
92 /** 92 /**
93 * Test errors in UpdaterUtils::write_updates_file(): not writable updates file. 93 * Test errors in UpdaterUtils::write_updates_file(): not writable updates file.
94 *
95 * @expectedException Exception
96 * @expectedExceptionMessageRegExp /Unable to write(.*)/
97 */ 94 */
98 public function testWriteUpdatesFileNotWritable() 95 public function testWriteUpdatesFileNotWritable()
99 { 96 {
97 $this->expectException(\Exception::class);
98 $this->expectExceptionMessageRegExp('/Unable to write(.*)/');
99
100 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 100 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
101 touch($updatesFile); 101 touch($updatesFile);
102 chmod($updatesFile, 0444); 102 chmod($updatesFile, 0444);
@@ -161,11 +161,11 @@ class LegacyUpdaterTest extends \PHPUnit\Framework\TestCase
161 161
162 /** 162 /**
163 * Test Update failed. 163 * Test Update failed.
164 *
165 * @expectedException \Exception
166 */ 164 */
167 public function testUpdateFailed() 165 public function testUpdateFailed()
168 { 166 {
167 $this->expectException(\Exception::class);
168
169 $updates = array( 169 $updates = array(
170 'updateMethodDummy1', 170 'updateMethodDummy1',
171 'updateMethodDummy2', 171 'updateMethodDummy2',
@@ -723,7 +723,7 @@ $GLOBALS[\'privateLinkByDefault\'] = true;';
723 $this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode')); 723 $this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode'));
724 $this->assertEquals(125, $this->conf->get('thumbnails.width')); 724 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
725 $this->assertEquals(90, $this->conf->get('thumbnails.height')); 725 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
726 $this->assertContains('You have enabled or changed thumbnails', $_SESSION['warnings'][0]); 726 $this->assertContainsPolyfill('You have enabled or changed thumbnails', $_SESSION['warnings'][0]);
727 } 727 }
728 728
729 /** 729 /**
@@ -754,7 +754,7 @@ $GLOBALS[\'privateLinkByDefault\'] = true;';
754 if (isset($_SESSION['warnings'])) { 754 if (isset($_SESSION['warnings'])) {
755 unset($_SESSION['warnings']); 755 unset($_SESSION['warnings']);
756 } 756 }
757 757
758 $updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION); 758 $updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
759 $this->assertTrue($updater->updateMethodWebThumbnailer()); 759 $this->assertTrue($updater->updateMethodWebThumbnailer());
760 $this->assertFalse($this->conf->exists('thumbnail')); 760 $this->assertFalse($this->conf->exists('thumbnail'));
diff --git a/tests/netscape/BookmarkExportTest.php b/tests/netscape/BookmarkExportTest.php
index 6c948bba..ad288f78 100644
--- a/tests/netscape/BookmarkExportTest.php
+++ b/tests/netscape/BookmarkExportTest.php
@@ -1,19 +1,21 @@
1<?php 1<?php
2
2namespace Shaarli\Netscape; 3namespace Shaarli\Netscape;
3 4
5use malkusch\lock\mutex\NoMutex;
4use Shaarli\Bookmark\BookmarkFileService; 6use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Bookmark\LinkDB;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
7use Shaarli\Formatter\FormatterFactory;
8use Shaarli\Formatter\BookmarkFormatter; 8use Shaarli\Formatter\BookmarkFormatter;
9use Shaarli\Formatter\FormatterFactory;
9use Shaarli\History; 10use Shaarli\History;
11use Shaarli\TestCase;
10 12
11require_once 'tests/utils/ReferenceLinkDB.php'; 13require_once 'tests/utils/ReferenceLinkDB.php';
12 14
13/** 15/**
14 * Netscape bookmark export 16 * Netscape bookmark export
15 */ 17 */
16class BookmarkExportTest extends \PHPUnit\Framework\TestCase 18class BookmarkExportTest extends TestCase
17{ 19{
18 /** 20 /**
19 * @var string datastore to test write operations 21 * @var string datastore to test write operations
@@ -21,6 +23,11 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
21 protected static $testDatastore = 'sandbox/datastore.php'; 23 protected static $testDatastore = 'sandbox/datastore.php';
22 24
23 /** 25 /**
26 * @var ConfigManager instance.
27 */
28 protected static $conf;
29
30 /**
24 * @var \ReferenceLinkDB instance. 31 * @var \ReferenceLinkDB instance.
25 */ 32 */
26 protected static $refDb = null; 33 protected static $refDb = null;
@@ -36,29 +43,49 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
36 protected static $formatter; 43 protected static $formatter;
37 44
38 /** 45 /**
46 * @var History instance
47 */
48 protected static $history;
49
50 /**
51 * @var NetscapeBookmarkUtils
52 */
53 protected $netscapeBookmarkUtils;
54
55 /**
39 * Instantiate reference data 56 * Instantiate reference data
40 */ 57 */
41 public static function setUpBeforeClass() 58 public static function setUpBeforeClass(): void
59 {
60 $mutex = new NoMutex();
61 static::$conf = new ConfigManager('tests/utils/config/configJson');
62 static::$conf->set('resource.datastore', static::$testDatastore);
63 static::$refDb = new \ReferenceLinkDB();
64 static::$refDb->write(static::$testDatastore);
65 static::$history = new History('sandbox/history.php');
66 static::$bookmarkService = new BookmarkFileService(static::$conf, static::$history, $mutex, true);
67 $factory = new FormatterFactory(static::$conf, true);
68 static::$formatter = $factory->getFormatter('raw');
69 }
70
71 public function setUp(): void
42 { 72 {
43 $conf = new ConfigManager('tests/utils/config/configJson'); 73 $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils(
44 $conf->set('resource.datastore', self::$testDatastore); 74 static::$bookmarkService,
45 self::$refDb = new \ReferenceLinkDB(); 75 static::$conf,
46 self::$refDb->write(self::$testDatastore); 76 static::$history
47 $history = new History('sandbox/history.php'); 77 );
48 self::$bookmarkService = new BookmarkFileService($conf, $history, true);
49 $factory = new FormatterFactory($conf, true);
50 self::$formatter = $factory->getFormatter('raw');
51 } 78 }
52 79
53 /** 80 /**
54 * Attempt to export an invalid link selection 81 * Attempt to export an invalid link selection
55 * @expectedException Exception
56 * @expectedExceptionMessageRegExp /Invalid export selection/
57 */ 82 */
58 public function testFilterAndFormatInvalid() 83 public function testFilterAndFormatInvalid()
59 { 84 {
60 NetscapeBookmarkUtils::filterAndFormat( 85 $this->expectException(\Exception::class);
61 self::$bookmarkService, 86 $this->expectExceptionMessageRegExp('/Invalid export selection/');
87
88 $this->netscapeBookmarkUtils->filterAndFormat(
62 self::$formatter, 89 self::$formatter,
63 'derp', 90 'derp',
64 false, 91 false,
@@ -71,8 +98,7 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
71 */ 98 */
72 public function testFilterAndFormatAll() 99 public function testFilterAndFormatAll()
73 { 100 {
74 $links = NetscapeBookmarkUtils::filterAndFormat( 101 $links = $this->netscapeBookmarkUtils->filterAndFormat(
75 self::$bookmarkService,
76 self::$formatter, 102 self::$formatter,
77 'all', 103 'all',
78 false, 104 false,
@@ -97,8 +123,7 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
97 */ 123 */
98 public function testFilterAndFormatPrivate() 124 public function testFilterAndFormatPrivate()
99 { 125 {
100 $links = NetscapeBookmarkUtils::filterAndFormat( 126 $links = $this->netscapeBookmarkUtils->filterAndFormat(
101 self::$bookmarkService,
102 self::$formatter, 127 self::$formatter,
103 'private', 128 'private',
104 false, 129 false,
@@ -123,8 +148,7 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
123 */ 148 */
124 public function testFilterAndFormatPublic() 149 public function testFilterAndFormatPublic()
125 { 150 {
126 $links = NetscapeBookmarkUtils::filterAndFormat( 151 $links = $this->netscapeBookmarkUtils->filterAndFormat(
127 self::$bookmarkService,
128 self::$formatter, 152 self::$formatter,
129 'public', 153 'public',
130 false, 154 false,
@@ -149,15 +173,14 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
149 */ 173 */
150 public function testFilterAndFormatDoNotPrependNoteUrl() 174 public function testFilterAndFormatDoNotPrependNoteUrl()
151 { 175 {
152 $links = NetscapeBookmarkUtils::filterAndFormat( 176 $links = $this->netscapeBookmarkUtils->filterAndFormat(
153 self::$bookmarkService,
154 self::$formatter, 177 self::$formatter,
155 'public', 178 'public',
156 false, 179 false,
157 '' 180 ''
158 ); 181 );
159 $this->assertEquals( 182 $this->assertEquals(
160 '?WDWyig', 183 '/shaare/WDWyig',
161 $links[2]['url'] 184 $links[2]['url']
162 ); 185 );
163 } 186 }
@@ -168,15 +191,14 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
168 public function testFilterAndFormatPrependNoteUrl() 191 public function testFilterAndFormatPrependNoteUrl()
169 { 192 {
170 $indexUrl = 'http://localhost:7469/shaarli/'; 193 $indexUrl = 'http://localhost:7469/shaarli/';
171 $links = NetscapeBookmarkUtils::filterAndFormat( 194 $links = $this->netscapeBookmarkUtils->filterAndFormat(
172 self::$bookmarkService,
173 self::$formatter, 195 self::$formatter,
174 'public', 196 'public',
175 true, 197 true,
176 $indexUrl 198 $indexUrl
177 ); 199 );
178 $this->assertEquals( 200 $this->assertEquals(
179 $indexUrl . '?WDWyig', 201 $indexUrl . 'shaare/WDWyig',
180 $links[2]['url'] 202 $links[2]['url']
181 ); 203 );
182 } 204 }
diff --git a/tests/netscape/BookmarkImportTest.php b/tests/netscape/BookmarkImportTest.php
index fef7f6d1..c526d5c8 100644
--- a/tests/netscape/BookmarkImportTest.php
+++ b/tests/netscape/BookmarkImportTest.php
@@ -1,29 +1,32 @@
1<?php 1<?php
2
2namespace Shaarli\Netscape; 3namespace Shaarli\Netscape;
3 4
4use DateTime; 5use DateTime;
6use malkusch\lock\mutex\NoMutex;
7use Psr\Http\Message\UploadedFileInterface;
5use Shaarli\Bookmark\Bookmark; 8use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkFilter;
7use Shaarli\Bookmark\BookmarkFileService; 9use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Bookmark\LinkDB; 10use Shaarli\Bookmark\BookmarkFilter;
9use Shaarli\Config\ConfigManager; 11use Shaarli\Config\ConfigManager;
10use Shaarli\History; 12use Shaarli\History;
13use Shaarli\TestCase;
14use Slim\Http\UploadedFile;
11 15
12/** 16/**
13 * Utility function to load a file's metadata in a $_FILES-like array 17 * Utility function to load a file's metadata in a $_FILES-like array
14 * 18 *
15 * @param string $filename Basename of the file 19 * @param string $filename Basename of the file
16 * 20 *
17 * @return array A $_FILES-like array 21 * @return UploadedFileInterface Upload file in PSR-7 compatible object
18 */ 22 */
19function file2array($filename) 23function file2array($filename)
20{ 24{
21 return array( 25 return new UploadedFile(
22 'filetoupload' => array( 26 __DIR__ . '/input/' . $filename,
23 'name' => $filename, 27 $filename,
24 'tmp_name' => __DIR__ . '/input/' . $filename, 28 null,
25 'size' => filesize(__DIR__ . '/input/' . $filename) 29 filesize(__DIR__ . '/input/' . $filename)
26 )
27 ); 30 );
28} 31}
29 32
@@ -31,7 +34,7 @@ function file2array($filename)
31/** 34/**
32 * Netscape bookmark import 35 * Netscape bookmark import
33 */ 36 */
34class BookmarkImportTest extends \PHPUnit\Framework\TestCase 37class BookmarkImportTest extends TestCase
35{ 38{
36 /** 39 /**
37 * @var string datastore to test write operations 40 * @var string datastore to test write operations
@@ -64,11 +67,16 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
64 protected $history; 67 protected $history;
65 68
66 /** 69 /**
70 * @var NetscapeBookmarkUtils
71 */
72 protected $netscapeBookmarkUtils;
73
74 /**
67 * @var string Save the current timezone. 75 * @var string Save the current timezone.
68 */ 76 */
69 protected static $defaultTimeZone; 77 protected static $defaultTimeZone;
70 78
71 public static function setUpBeforeClass() 79 public static function setUpBeforeClass(): void
72 { 80 {
73 self::$defaultTimeZone = date_default_timezone_get(); 81 self::$defaultTimeZone = date_default_timezone_get();
74 // Timezone without DST for test consistency 82 // Timezone without DST for test consistency
@@ -78,8 +86,9 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
78 /** 86 /**
79 * Resets test data before each test 87 * Resets test data before each test
80 */ 88 */
81 protected function setUp() 89 protected function setUp(): void
82 { 90 {
91 $mutex = new NoMutex();
83 if (file_exists(self::$testDatastore)) { 92 if (file_exists(self::$testDatastore)) {
84 unlink(self::$testDatastore); 93 unlink(self::$testDatastore);
85 } 94 }
@@ -90,18 +99,19 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
90 $this->conf->set('resource.page_cache', $this->pagecache); 99 $this->conf->set('resource.page_cache', $this->pagecache);
91 $this->conf->set('resource.datastore', self::$testDatastore); 100 $this->conf->set('resource.datastore', self::$testDatastore);
92 $this->history = new History(self::$historyFilePath); 101 $this->history = new History(self::$historyFilePath);
93 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true); 102 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true);
103 $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history);
94 } 104 }
95 105
96 /** 106 /**
97 * Delete history file. 107 * Delete history file.
98 */ 108 */
99 public function tearDown() 109 protected function tearDown(): void
100 { 110 {
101 @unlink(self::$historyFilePath); 111 @unlink(self::$historyFilePath);
102 } 112 }
103 113
104 public static function tearDownAfterClass() 114 public static function tearDownAfterClass(): void
105 { 115 {
106 date_default_timezone_set(self::$defaultTimeZone); 116 date_default_timezone_set(self::$defaultTimeZone);
107 } 117 }
@@ -115,7 +125,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
115 $this->assertEquals( 125 $this->assertEquals(
116 'File empty.htm (0 bytes) has an unknown file format.' 126 'File empty.htm (0 bytes) has an unknown file format.'
117 .' Nothing was imported.', 127 .' Nothing was imported.',
118 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) 128 $this->netscapeBookmarkUtils->import(null, $files)
119 ); 129 );
120 $this->assertEquals(0, $this->bookmarkService->count()); 130 $this->assertEquals(0, $this->bookmarkService->count());
121 } 131 }
@@ -128,7 +138,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
128 $files = file2array('no_doctype.htm'); 138 $files = file2array('no_doctype.htm');
129 $this->assertEquals( 139 $this->assertEquals(
130 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.', 140 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
131 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) 141 $this->netscapeBookmarkUtils->import(null, $files)
132 ); 142 );
133 $this->assertEquals(0, $this->bookmarkService->count()); 143 $this->assertEquals(0, $this->bookmarkService->count());
134 } 144 }
@@ -142,7 +152,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
142 $this->assertStringMatchesFormat( 152 $this->assertStringMatchesFormat(
143 'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:' 153 'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:'
144 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 154 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
145 NetscapeBookmarkUtils::import(null, $files, $this->bookmarkService, $this->conf, $this->history) 155 $this->netscapeBookmarkUtils->import(null, $files)
146 ); 156 );
147 $this->assertEquals(2, $this->bookmarkService->count()); 157 $this->assertEquals(2, $this->bookmarkService->count());
148 } 158 }
@@ -157,7 +167,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
157 $this->assertStringMatchesFormat( 167 $this->assertStringMatchesFormat(
158 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:' 168 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
159 .' 1 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 169 .' 1 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
160 NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history) 170 $this->netscapeBookmarkUtils->import([], $files)
161 ); 171 );
162 $this->assertEquals(1, $this->bookmarkService->count()); 172 $this->assertEquals(1, $this->bookmarkService->count());
163 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 173 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -185,7 +195,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
185 $this->assertStringMatchesFormat( 195 $this->assertStringMatchesFormat(
186 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:' 196 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
187 .' 8 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 197 .' 8 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
188 NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history) 198 $this->netscapeBookmarkUtils->import([], $files)
189 ); 199 );
190 $this->assertEquals(8, $this->bookmarkService->count()); 200 $this->assertEquals(8, $this->bookmarkService->count());
191 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 201 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -306,7 +316,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
306 $this->assertStringMatchesFormat( 316 $this->assertStringMatchesFormat(
307 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 317 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
308 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 318 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
309 NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history) 319 $this->netscapeBookmarkUtils->import([], $files)
310 ); 320 );
311 321
312 $this->assertEquals(2, $this->bookmarkService->count()); 322 $this->assertEquals(2, $this->bookmarkService->count());
@@ -349,7 +359,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
349 $this->assertStringMatchesFormat( 359 $this->assertStringMatchesFormat(
350 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 360 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
351 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 361 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
352 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 362 $this->netscapeBookmarkUtils->import($post, $files)
353 ); 363 );
354 364
355 $this->assertEquals(2, $this->bookmarkService->count()); 365 $this->assertEquals(2, $this->bookmarkService->count());
@@ -392,7 +402,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
392 $this->assertStringMatchesFormat( 402 $this->assertStringMatchesFormat(
393 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 403 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
394 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 404 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
395 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 405 $this->netscapeBookmarkUtils->import($post, $files)
396 ); 406 );
397 $this->assertEquals(2, $this->bookmarkService->count()); 407 $this->assertEquals(2, $this->bookmarkService->count());
398 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 408 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -410,7 +420,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
410 $this->assertStringMatchesFormat( 420 $this->assertStringMatchesFormat(
411 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 421 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
412 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 422 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
413 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 423 $this->netscapeBookmarkUtils->import($post, $files)
414 ); 424 );
415 $this->assertEquals(2, $this->bookmarkService->count()); 425 $this->assertEquals(2, $this->bookmarkService->count());
416 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 426 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -430,7 +440,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
430 $this->assertStringMatchesFormat( 440 $this->assertStringMatchesFormat(
431 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 441 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
432 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 442 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
433 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 443 $this->netscapeBookmarkUtils->import($post, $files)
434 ); 444 );
435 $this->assertEquals(2, $this->bookmarkService->count()); 445 $this->assertEquals(2, $this->bookmarkService->count());
436 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 446 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -445,7 +455,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
445 $this->assertStringMatchesFormat( 455 $this->assertStringMatchesFormat(
446 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 456 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
447 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.', 457 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
448 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 458 $this->netscapeBookmarkUtils->import($post, $files)
449 ); 459 );
450 $this->assertEquals(2, $this->bookmarkService->count()); 460 $this->assertEquals(2, $this->bookmarkService->count());
451 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 461 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -465,7 +475,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
465 $this->assertStringMatchesFormat( 475 $this->assertStringMatchesFormat(
466 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 476 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
467 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 477 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
468 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 478 $this->netscapeBookmarkUtils->import($post, $files)
469 ); 479 );
470 $this->assertEquals(2, $this->bookmarkService->count()); 480 $this->assertEquals(2, $this->bookmarkService->count());
471 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 481 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -480,7 +490,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
480 $this->assertStringMatchesFormat( 490 $this->assertStringMatchesFormat(
481 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 491 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
482 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.', 492 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
483 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 493 $this->netscapeBookmarkUtils->import($post, $files)
484 ); 494 );
485 $this->assertEquals(2, $this->bookmarkService->count()); 495 $this->assertEquals(2, $this->bookmarkService->count());
486 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 496 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -498,7 +508,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
498 $this->assertStringMatchesFormat( 508 $this->assertStringMatchesFormat(
499 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 509 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
500 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 510 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
501 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 511 $this->netscapeBookmarkUtils->import($post, $files)
502 ); 512 );
503 $this->assertEquals(2, $this->bookmarkService->count()); 513 $this->assertEquals(2, $this->bookmarkService->count());
504 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 514 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -508,7 +518,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
508 $this->assertStringMatchesFormat( 518 $this->assertStringMatchesFormat(
509 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 519 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
510 .' 0 bookmarks imported, 0 bookmarks overwritten, 2 bookmarks skipped.', 520 .' 0 bookmarks imported, 0 bookmarks overwritten, 2 bookmarks skipped.',
511 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 521 $this->netscapeBookmarkUtils->import($post, $files)
512 ); 522 );
513 $this->assertEquals(2, $this->bookmarkService->count()); 523 $this->assertEquals(2, $this->bookmarkService->count());
514 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 524 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -527,7 +537,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
527 $this->assertStringMatchesFormat( 537 $this->assertStringMatchesFormat(
528 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 538 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
529 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 539 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
530 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 540 $this->netscapeBookmarkUtils->import($post, $files)
531 ); 541 );
532 $this->assertEquals(2, $this->bookmarkService->count()); 542 $this->assertEquals(2, $this->bookmarkService->count());
533 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 543 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -548,7 +558,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
548 $this->assertStringMatchesFormat( 558 $this->assertStringMatchesFormat(
549 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 559 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
550 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 560 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
551 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history) 561 $this->netscapeBookmarkUtils->import($post, $files)
552 ); 562 );
553 $this->assertEquals(2, $this->bookmarkService->count()); 563 $this->assertEquals(2, $this->bookmarkService->count());
554 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 564 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -573,7 +583,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
573 $this->assertStringMatchesFormat( 583 $this->assertStringMatchesFormat(
574 'File same_date.htm (453 bytes) was successfully processed in %d seconds:' 584 'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
575 .' 3 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.', 585 .' 3 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
576 NetscapeBookmarkUtils::import(array(), $files, $this->bookmarkService, $this->conf, $this->history) 586 $this->netscapeBookmarkUtils->import(array(), $files)
577 ); 587 );
578 $this->assertEquals(3, $this->bookmarkService->count()); 588 $this->assertEquals(3, $this->bookmarkService->count());
579 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE)); 589 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
@@ -589,14 +599,14 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
589 'overwrite' => 'true', 599 'overwrite' => 'true',
590 ]; 600 ];
591 $files = file2array('netscape_basic.htm'); 601 $files = file2array('netscape_basic.htm');
592 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history); 602 $this->netscapeBookmarkUtils->import($post, $files);
593 $history = $this->history->getHistory(); 603 $history = $this->history->getHistory();
594 $this->assertEquals(1, count($history)); 604 $this->assertEquals(1, count($history));
595 $this->assertEquals(History::IMPORT, $history[0]['event']); 605 $this->assertEquals(History::IMPORT, $history[0]['event']);
596 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']); 606 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
597 607
598 // re-import as private, enable overwriting 608 // re-import as private, enable overwriting
599 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history); 609 $this->netscapeBookmarkUtils->import($post, $files);
600 $history = $this->history->getHistory(); 610 $history = $this->history->getHistory();
601 $this->assertEquals(2, count($history)); 611 $this->assertEquals(2, count($history));
602 $this->assertEquals(History::IMPORT, $history[0]['event']); 612 $this->assertEquals(History::IMPORT, $history[0]['event']);
diff --git a/tests/plugins/PluginAddlinkTest.php b/tests/plugins/PluginAddlinkTest.php
index d052f8b9..a3ec9fc9 100644
--- a/tests/plugins/PluginAddlinkTest.php
+++ b/tests/plugins/PluginAddlinkTest.php
@@ -2,19 +2,19 @@
2namespace Shaarli\Plugin\Addlink; 2namespace Shaarli\Plugin\Addlink;
3 3
4use Shaarli\Plugin\PluginManager; 4use Shaarli\Plugin\PluginManager;
5use Shaarli\Router; 5use Shaarli\Render\TemplatePage;
6 6
7require_once 'plugins/addlink_toolbar/addlink_toolbar.php'; 7require_once 'plugins/addlink_toolbar/addlink_toolbar.php';
8 8
9/** 9/**
10 * Unit test for the Addlink toolbar plugin 10 * Unit test for the Addlink toolbar plugin
11 */ 11 */
12class PluginAddlinkTest extends \PHPUnit\Framework\TestCase 12class PluginAddlinkTest extends \Shaarli\TestCase
13{ 13{
14 /** 14 /**
15 * Reset plugin path. 15 * Reset plugin path.
16 */ 16 */
17 public function setUp() 17 protected function setUp(): void
18 { 18 {
19 PluginManager::$PLUGINS_PATH = 'plugins'; 19 PluginManager::$PLUGINS_PATH = 'plugins';
20 } 20 }
@@ -26,8 +26,9 @@ class PluginAddlinkTest extends \PHPUnit\Framework\TestCase
26 { 26 {
27 $str = 'stuff'; 27 $str = 'stuff';
28 $data = array($str => $str); 28 $data = array($str => $str);
29 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 29 $data['_PAGE_'] = TemplatePage::LINKLIST;
30 $data['_LOGGEDIN_'] = true; 30 $data['_LOGGEDIN_'] = true;
31 $data['_BASE_PATH_'] = '/subfolder';
31 32
32 $data = hook_addlink_toolbar_render_header($data); 33 $data = hook_addlink_toolbar_render_header($data);
33 $this->assertEquals($str, $data[$str]); 34 $this->assertEquals($str, $data[$str]);
@@ -36,6 +37,8 @@ class PluginAddlinkTest extends \PHPUnit\Framework\TestCase
36 $data = array($str => $str); 37 $data = array($str => $str);
37 $data['_PAGE_'] = $str; 38 $data['_PAGE_'] = $str;
38 $data['_LOGGEDIN_'] = true; 39 $data['_LOGGEDIN_'] = true;
40 $data['_BASE_PATH_'] = '/subfolder';
41
39 $data = hook_addlink_toolbar_render_header($data); 42 $data = hook_addlink_toolbar_render_header($data);
40 $this->assertEquals($str, $data[$str]); 43 $this->assertEquals($str, $data[$str]);
41 $this->assertArrayNotHasKey('fields_toolbar', $data); 44 $this->assertArrayNotHasKey('fields_toolbar', $data);
@@ -48,8 +51,9 @@ class PluginAddlinkTest extends \PHPUnit\Framework\TestCase
48 { 51 {
49 $str = 'stuff'; 52 $str = 'stuff';
50 $data = array($str => $str); 53 $data = array($str => $str);
51 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 54 $data['_PAGE_'] = TemplatePage::LINKLIST;
52 $data['_LOGGEDIN_'] = false; 55 $data['_LOGGEDIN_'] = false;
56 $data['_BASE_PATH_'] = '/subfolder';
53 57
54 $data = hook_addlink_toolbar_render_header($data); 58 $data = hook_addlink_toolbar_render_header($data);
55 $this->assertEquals($str, $data[$str]); 59 $this->assertEquals($str, $data[$str]);
diff --git a/tests/plugins/PluginArchiveorgTest.php b/tests/plugins/PluginArchiveorgTest.php
index b9a67adb..467dc3d0 100644
--- a/tests/plugins/PluginArchiveorgTest.php
+++ b/tests/plugins/PluginArchiveorgTest.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2
2namespace Shaarli\Plugin\Archiveorg; 3namespace Shaarli\Plugin\Archiveorg;
3 4
4/** 5/**
@@ -6,6 +7,7 @@ namespace Shaarli\Plugin\Archiveorg;
6 */ 7 */
7 8
8use Shaarli\Plugin\PluginManager; 9use Shaarli\Plugin\PluginManager;
10use Shaarli\TestCase;
9 11
10require_once 'plugins/archiveorg/archiveorg.php'; 12require_once 'plugins/archiveorg/archiveorg.php';
11 13
@@ -13,20 +15,35 @@ require_once 'plugins/archiveorg/archiveorg.php';
13 * Class PluginArchiveorgTest 15 * Class PluginArchiveorgTest
14 * Unit test for the archiveorg plugin 16 * Unit test for the archiveorg plugin
15 */ 17 */
16class PluginArchiveorgTest extends \PHPUnit\Framework\TestCase 18class PluginArchiveorgTest extends TestCase
17{ 19{
20 protected $savedScriptName;
21
18 /** 22 /**
19 * Reset plugin path 23 * Reset plugin path
20 */ 24 */
21 public function setUp() 25 public function setUp(): void
22 { 26 {
23 PluginManager::$PLUGINS_PATH = 'plugins'; 27 PluginManager::$PLUGINS_PATH = 'plugins';
28
29 // plugins manipulate global vars
30 $_SERVER['SERVER_PORT'] = '80';
31 $_SERVER['SERVER_NAME'] = 'shaarli.shaarli';
32 $this->savedScriptName = $_SERVER['SCRIPT_NAME'] ?? null;
33 $_SERVER['SCRIPT_NAME'] = '/index.php';
34 }
35
36 public function tearDown(): void
37 {
38 unset($_SERVER['SERVER_PORT']);
39 unset($_SERVER['SERVER_NAME']);
40 $_SERVER['SCRIPT_NAME'] = $this->savedScriptName;
24 } 41 }
25 42
26 /** 43 /**
27 * Test render_linklist hook on external bookmarks. 44 * Test render_linklist hook on external bookmarks.
28 */ 45 */
29 public function testArchiveorgLinklistOnExternalLinks() 46 public function testArchiveorgLinklistOnExternalLinks(): void
30 { 47 {
31 $str = 'http://randomstr.com/test'; 48 $str = 'http://randomstr.com/test';
32 49
@@ -56,16 +73,16 @@ class PluginArchiveorgTest extends \PHPUnit\Framework\TestCase
56 /** 73 /**
57 * Test render_linklist hook on internal bookmarks. 74 * Test render_linklist hook on internal bookmarks.
58 */ 75 */
59 public function testArchiveorgLinklistOnInternalLinks() 76 public function testArchiveorgLinklistOnInternalLinks(): void
60 { 77 {
61 $internalLink1 = 'http://shaarli.shaarli/?qvMAqg'; 78 $internalLink1 = 'http://shaarli.shaarli/shaare/qvMAqg';
62 $internalLinkRealURL1 = '?qvMAqg'; 79 $internalLinkRealURL1 = '/shaare/qvMAqg';
63 80
64 $internalLink2 = 'http://shaarli.shaarli/?2_7zww'; 81 $internalLink2 = 'http://shaarli.shaarli/shaare/2_7zww';
65 $internalLinkRealURL2 = '?2_7zww'; 82 $internalLinkRealURL2 = '/shaare/2_7zww';
66 83
67 $internalLink3 = 'http://shaarli.shaarli/?z7u-_Q'; 84 $internalLink3 = 'http://shaarli.shaarli/shaare/z7u-_Q';
68 $internalLinkRealURL3 = '?z7u-_Q'; 85 $internalLinkRealURL3 = '/shaare/z7u-_Q';
69 86
70 $data = array( 87 $data = array(
71 'title' => $internalLink1, 88 'title' => $internalLink1,
diff --git a/tests/plugins/PluginDefaultColorsTest.php b/tests/plugins/PluginDefaultColorsTest.php
index b9951cca..cc844c60 100644
--- a/tests/plugins/PluginDefaultColorsTest.php
+++ b/tests/plugins/PluginDefaultColorsTest.php
@@ -2,11 +2,10 @@
2 2
3namespace Shaarli\Plugin\DefaultColors; 3namespace Shaarli\Plugin\DefaultColors;
4 4
5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Shaarli\Bookmark\LinkDB; 5use Shaarli\Bookmark\LinkDB;
8use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
9use Shaarli\Plugin\PluginManager; 7use Shaarli\Plugin\PluginManager;
8use Shaarli\TestCase;
10 9
11require_once 'plugins/default_colors/default_colors.php'; 10require_once 'plugins/default_colors/default_colors.php';
12 11
@@ -20,7 +19,7 @@ class PluginDefaultColorsTest extends TestCase
20 /** 19 /**
21 * Reset plugin path 20 * Reset plugin path
22 */ 21 */
23 public function setUp() 22 protected function setUp(): void
24 { 23 {
25 PluginManager::$PLUGINS_PATH = 'sandbox'; 24 PluginManager::$PLUGINS_PATH = 'sandbox';
26 mkdir(PluginManager::$PLUGINS_PATH . '/default_colors/'); 25 mkdir(PluginManager::$PLUGINS_PATH . '/default_colors/');
@@ -33,7 +32,7 @@ class PluginDefaultColorsTest extends TestCase
33 /** 32 /**
34 * Remove sandbox files and folder 33 * Remove sandbox files and folder
35 */ 34 */
36 public function tearDown() 35 protected function tearDown(): void
37 { 36 {
38 if (file_exists('sandbox/default_colors/default_colors.css.template')) { 37 if (file_exists('sandbox/default_colors/default_colors.css.template')) {
39 unlink('sandbox/default_colors/default_colors.css.template'); 38 unlink('sandbox/default_colors/default_colors.css.template');
@@ -57,6 +56,8 @@ class PluginDefaultColorsTest extends TestCase
57 $conf->set('plugins.DEFAULT_COLORS_BACKGROUND', 'value'); 56 $conf->set('plugins.DEFAULT_COLORS_BACKGROUND', 'value');
58 $errors = default_colors_init($conf); 57 $errors = default_colors_init($conf);
59 $this->assertEmpty($errors); 58 $this->assertEmpty($errors);
59
60 $this->assertFileExists($file = 'sandbox/default_colors/default_colors.css');
60 } 61 }
61 62
62 /** 63 /**
@@ -72,9 +73,9 @@ class PluginDefaultColorsTest extends TestCase
72 /** 73 /**
73 * Test the save plugin parameters hook with all colors specified. 74 * Test the save plugin parameters hook with all colors specified.
74 */ 75 */
75 public function testSavePluginParametersAll() 76 public function testGenerateCssFile()
76 { 77 {
77 $post = [ 78 $params = [
78 'other1' => true, 79 'other1' => true,
79 'DEFAULT_COLORS_MAIN' => 'blue', 80 'DEFAULT_COLORS_MAIN' => 'blue',
80 'DEFAULT_COLORS_BACKGROUND' => 'pink', 81 'DEFAULT_COLORS_BACKGROUND' => 'pink',
@@ -82,7 +83,7 @@ class PluginDefaultColorsTest extends TestCase
82 'DEFAULT_COLORS_DARK_MAIN' => 'green', 83 'DEFAULT_COLORS_DARK_MAIN' => 'green',
83 ]; 84 ];
84 85
85 hook_default_colors_save_plugin_parameters($post); 86 default_colors_generate_css_file($params);
86 $this->assertFileExists($file = 'sandbox/default_colors/default_colors.css'); 87 $this->assertFileExists($file = 'sandbox/default_colors/default_colors.css');
87 $content = file_get_contents($file); 88 $content = file_get_contents($file);
88 $expected = ':root { 89 $expected = ':root {
@@ -98,16 +99,16 @@ class PluginDefaultColorsTest extends TestCase
98 /** 99 /**
99 * Test the save plugin parameters hook with only one color specified. 100 * Test the save plugin parameters hook with only one color specified.
100 */ 101 */
101 public function testSavePluginParametersSingle() 102 public function testGenerateCssFileSingle()
102 { 103 {
103 $post = [ 104 $params = [
104 'other1' => true, 105 'other1' => true,
105 'DEFAULT_COLORS_BACKGROUND' => 'pink', 106 'DEFAULT_COLORS_BACKGROUND' => 'pink',
106 'other2' => ['yep'], 107 'other2' => ['yep'],
107 'DEFAULT_COLORS_DARK_MAIN' => '', 108 'DEFAULT_COLORS_DARK_MAIN' => '',
108 ]; 109 ];
109 110
110 hook_default_colors_save_plugin_parameters($post); 111 default_colors_generate_css_file($params);
111 $this->assertFileExists($file = 'sandbox/default_colors/default_colors.css'); 112 $this->assertFileExists($file = 'sandbox/default_colors/default_colors.css');
112 $content = file_get_contents($file); 113 $content = file_get_contents($file);
113 $expected = ':root { 114 $expected = ':root {
@@ -121,9 +122,9 @@ class PluginDefaultColorsTest extends TestCase
121 /** 122 /**
122 * Test the save plugin parameters hook with no color specified. 123 * Test the save plugin parameters hook with no color specified.
123 */ 124 */
124 public function testSavePluginParametersNone() 125 public function testGenerateCssFileNone()
125 { 126 {
126 hook_default_colors_save_plugin_parameters([]); 127 default_colors_generate_css_file([]);
127 $this->assertFileNotExists($file = 'sandbox/default_colors/default_colors.css'); 128 $this->assertFileNotExists($file = 'sandbox/default_colors/default_colors.css');
128 } 129 }
129 130
diff --git a/tests/plugins/PluginIssoTest.php b/tests/plugins/PluginIssoTest.php
index 99477205..16ecf357 100644
--- a/tests/plugins/PluginIssoTest.php
+++ b/tests/plugins/PluginIssoTest.php
@@ -5,6 +5,7 @@ use DateTime;
5use Shaarli\Bookmark\Bookmark; 5use Shaarli\Bookmark\Bookmark;
6use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Shaarli\Plugin\PluginManager; 7use Shaarli\Plugin\PluginManager;
8use Shaarli\TestCase;
8 9
9require_once 'plugins/isso/isso.php'; 10require_once 'plugins/isso/isso.php';
10 11
@@ -13,12 +14,12 @@ require_once 'plugins/isso/isso.php';
13 * 14 *
14 * Test the Isso plugin (comment system). 15 * Test the Isso plugin (comment system).
15 */ 16 */
16class PluginIssoTest extends \PHPUnit\Framework\TestCase 17class PluginIssoTest extends TestCase
17{ 18{
18 /** 19 /**
19 * Reset plugin path 20 * Reset plugin path
20 */ 21 */
21 public function setUp() 22 public function setUp(): void
22 { 23 {
23 PluginManager::$PLUGINS_PATH = 'plugins'; 24 PluginManager::$PLUGINS_PATH = 'plugins';
24 } 25 }
@@ -26,7 +27,7 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
26 /** 27 /**
27 * Test Isso init without errors. 28 * Test Isso init without errors.
28 */ 29 */
29 public function testIssoInitNoError() 30 public function testIssoInitNoError(): void
30 { 31 {
31 $conf = new ConfigManager(''); 32 $conf = new ConfigManager('');
32 $conf->set('plugins.ISSO_SERVER', 'value'); 33 $conf->set('plugins.ISSO_SERVER', 'value');
@@ -37,7 +38,7 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
37 /** 38 /**
38 * Test Isso init with errors. 39 * Test Isso init with errors.
39 */ 40 */
40 public function testIssoInitError() 41 public function testIssoInitError(): void
41 { 42 {
42 $conf = new ConfigManager(''); 43 $conf = new ConfigManager('');
43 $errors = isso_init($conf); 44 $errors = isso_init($conf);
@@ -47,7 +48,7 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
47 /** 48 /**
48 * Test render_linklist hook with valid settings to display the comment form. 49 * Test render_linklist hook with valid settings to display the comment form.
49 */ 50 */
50 public function testIssoDisplayed() 51 public function testIssoDisplayed(): void
51 { 52 {
52 $conf = new ConfigManager(''); 53 $conf = new ConfigManager('');
53 $conf->set('plugins.ISSO_SERVER', 'value'); 54 $conf->set('plugins.ISSO_SERVER', 'value');
@@ -87,7 +88,7 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
87 /** 88 /**
88 * Test isso plugin when multiple bookmarks are displayed (shouldn't be displayed). 89 * Test isso plugin when multiple bookmarks are displayed (shouldn't be displayed).
89 */ 90 */
90 public function testIssoMultipleLinks() 91 public function testIssoMultipleLinks(): void
91 { 92 {
92 $conf = new ConfigManager(''); 93 $conf = new ConfigManager('');
93 $conf->set('plugins.ISSO_SERVER', 'value'); 94 $conf->set('plugins.ISSO_SERVER', 'value');
@@ -115,14 +116,14 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
115 116
116 $processed = hook_isso_render_linklist($data, $conf); 117 $processed = hook_isso_render_linklist($data, $conf);
117 // link_plugin should be added for the icon 118 // link_plugin should be added for the icon
118 $this->assertContains('<a href="?'. $short1 .'#isso-thread">', $processed['links'][0]['link_plugin'][0]); 119 $this->assertContainsPolyfill('<a href="/shaare/'. $short1 .'#isso-thread">', $processed['links'][0]['link_plugin'][0]);
119 $this->assertContains('<a href="?'. $short2 .'#isso-thread">', $processed['links'][1]['link_plugin'][0]); 120 $this->assertContainsPolyfill('<a href="/shaare/'. $short2 .'#isso-thread">', $processed['links'][1]['link_plugin'][0]);
120 } 121 }
121 122
122 /** 123 /**
123 * Test isso plugin when using search (shouldn't be displayed). 124 * Test isso plugin when using search (shouldn't be displayed).
124 */ 125 */
125 public function testIssoNotDisplayedWhenSearch() 126 public function testIssoNotDisplayedWhenSearch(): void
126 { 127 {
127 $conf = new ConfigManager(''); 128 $conf = new ConfigManager('');
128 $conf->set('plugins.ISSO_SERVER', 'value'); 129 $conf->set('plugins.ISSO_SERVER', 'value');
@@ -145,13 +146,13 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
145 $processed = hook_isso_render_linklist($data, $conf); 146 $processed = hook_isso_render_linklist($data, $conf);
146 147
147 // link_plugin should be added for the icon 148 // link_plugin should be added for the icon
148 $this->assertContains('<a href="?'. $short1 .'#isso-thread">', $processed['links'][0]['link_plugin'][0]); 149 $this->assertContainsPolyfill('<a href="/shaare/'. $short1 .'#isso-thread">', $processed['links'][0]['link_plugin'][0]);
149 } 150 }
150 151
151 /** 152 /**
152 * Test isso plugin without server configuration (shouldn't be displayed). 153 * Test isso plugin without server configuration (shouldn't be displayed).
153 */ 154 */
154 public function testIssoWithoutConf() 155 public function testIssoWithoutConf(): void
155 { 156 {
156 $data = 'abc'; 157 $data = 'abc';
157 $conf = new ConfigManager(''); 158 $conf = new ConfigManager('');
diff --git a/tests/plugins/PluginPlayvideosTest.php b/tests/plugins/PluginPlayvideosTest.php
index 51472617..338d2e35 100644
--- a/tests/plugins/PluginPlayvideosTest.php
+++ b/tests/plugins/PluginPlayvideosTest.php
@@ -6,7 +6,7 @@ namespace Shaarli\Plugin\Playvideos;
6 */ 6 */
7 7
8use Shaarli\Plugin\PluginManager; 8use Shaarli\Plugin\PluginManager;
9use Shaarli\Router; 9use Shaarli\Render\TemplatePage;
10 10
11require_once 'plugins/playvideos/playvideos.php'; 11require_once 'plugins/playvideos/playvideos.php';
12 12
@@ -14,12 +14,12 @@ require_once 'plugins/playvideos/playvideos.php';
14 * Class PluginPlayvideosTest 14 * Class PluginPlayvideosTest
15 * Unit test for the PlayVideos plugin 15 * Unit test for the PlayVideos plugin
16 */ 16 */
17class PluginPlayvideosTest extends \PHPUnit\Framework\TestCase 17class PluginPlayvideosTest extends \Shaarli\TestCase
18{ 18{
19 /** 19 /**
20 * Reset plugin path 20 * Reset plugin path
21 */ 21 */
22 public function setUp() 22 protected function setUp(): void
23 { 23 {
24 PluginManager::$PLUGINS_PATH = 'plugins'; 24 PluginManager::$PLUGINS_PATH = 'plugins';
25 } 25 }
@@ -31,7 +31,7 @@ class PluginPlayvideosTest extends \PHPUnit\Framework\TestCase
31 { 31 {
32 $str = 'stuff'; 32 $str = 'stuff';
33 $data = array($str => $str); 33 $data = array($str => $str);
34 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 34 $data['_PAGE_'] = TemplatePage::LINKLIST;
35 35
36 $data = hook_playvideos_render_header($data); 36 $data = hook_playvideos_render_header($data);
37 $this->assertEquals($str, $data[$str]); 37 $this->assertEquals($str, $data[$str]);
@@ -50,7 +50,7 @@ class PluginPlayvideosTest extends \PHPUnit\Framework\TestCase
50 { 50 {
51 $str = 'stuff'; 51 $str = 'stuff';
52 $data = array($str => $str); 52 $data = array($str => $str);
53 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 53 $data['_PAGE_'] = TemplatePage::LINKLIST;
54 54
55 $data = hook_playvideos_render_footer($data); 55 $data = hook_playvideos_render_footer($data);
56 $this->assertEquals($str, $data[$str]); 56 $this->assertEquals($str, $data[$str]);
diff --git a/tests/plugins/PluginPubsubhubbubTest.php b/tests/plugins/PluginPubsubhubbubTest.php
index a7bd8fc9..d3f7b439 100644
--- a/tests/plugins/PluginPubsubhubbubTest.php
+++ b/tests/plugins/PluginPubsubhubbubTest.php
@@ -3,7 +3,7 @@ namespace Shaarli\Plugin\Pubsubhubbub;
3 3
4use Shaarli\Config\ConfigManager; 4use Shaarli\Config\ConfigManager;
5use Shaarli\Plugin\PluginManager; 5use Shaarli\Plugin\PluginManager;
6use Shaarli\Router; 6use Shaarli\Render\TemplatePage;
7 7
8require_once 'plugins/pubsubhubbub/pubsubhubbub.php'; 8require_once 'plugins/pubsubhubbub/pubsubhubbub.php';
9 9
@@ -11,7 +11,7 @@ require_once 'plugins/pubsubhubbub/pubsubhubbub.php';
11 * Class PluginPubsubhubbubTest 11 * Class PluginPubsubhubbubTest
12 * Unit test for the pubsubhubbub plugin 12 * Unit test for the pubsubhubbub plugin
13 */ 13 */
14class PluginPubsubhubbubTest extends \PHPUnit\Framework\TestCase 14class PluginPubsubhubbubTest extends \Shaarli\TestCase
15{ 15{
16 /** 16 /**
17 * @var string Config file path (without extension). 17 * @var string Config file path (without extension).
@@ -21,7 +21,7 @@ class PluginPubsubhubbubTest extends \PHPUnit\Framework\TestCase
21 /** 21 /**
22 * Reset plugin path 22 * Reset plugin path
23 */ 23 */
24 public function setUp() 24 protected function setUp(): void
25 { 25 {
26 PluginManager::$PLUGINS_PATH = 'plugins'; 26 PluginManager::$PLUGINS_PATH = 'plugins';
27 } 27 }
@@ -34,7 +34,7 @@ class PluginPubsubhubbubTest extends \PHPUnit\Framework\TestCase
34 $hub = 'http://domain.hub'; 34 $hub = 'http://domain.hub';
35 $conf = new ConfigManager(self::$configFile); 35 $conf = new ConfigManager(self::$configFile);
36 $conf->set('plugins.PUBSUBHUB_URL', $hub); 36 $conf->set('plugins.PUBSUBHUB_URL', $hub);
37 $data['_PAGE_'] = Router::$PAGE_FEED_RSS; 37 $data['_PAGE_'] = TemplatePage::FEED_RSS;
38 38
39 $data = hook_pubsubhubbub_render_feed($data, $conf); 39 $data = hook_pubsubhubbub_render_feed($data, $conf);
40 $expected = '<atom:link rel="hub" href="'. $hub .'" />'; 40 $expected = '<atom:link rel="hub" href="'. $hub .'" />';
@@ -49,7 +49,7 @@ class PluginPubsubhubbubTest extends \PHPUnit\Framework\TestCase
49 $hub = 'http://domain.hub'; 49 $hub = 'http://domain.hub';
50 $conf = new ConfigManager(self::$configFile); 50 $conf = new ConfigManager(self::$configFile);
51 $conf->set('plugins.PUBSUBHUB_URL', $hub); 51 $conf->set('plugins.PUBSUBHUB_URL', $hub);
52 $data['_PAGE_'] = Router::$PAGE_FEED_ATOM; 52 $data['_PAGE_'] = TemplatePage::FEED_ATOM;
53 53
54 $data = hook_pubsubhubbub_render_feed($data, $conf); 54 $data = hook_pubsubhubbub_render_feed($data, $conf);
55 $expected = '<link rel="hub" href="'. $hub .'" />'; 55 $expected = '<link rel="hub" href="'. $hub .'" />';
diff --git a/tests/plugins/PluginQrcodeTest.php b/tests/plugins/PluginQrcodeTest.php
index 0c61e14a..1d85fba6 100644
--- a/tests/plugins/PluginQrcodeTest.php
+++ b/tests/plugins/PluginQrcodeTest.php
@@ -6,7 +6,7 @@ namespace Shaarli\Plugin\Qrcode;
6 */ 6 */
7 7
8use Shaarli\Plugin\PluginManager; 8use Shaarli\Plugin\PluginManager;
9use Shaarli\Router; 9use Shaarli\Render\TemplatePage;
10 10
11require_once 'plugins/qrcode/qrcode.php'; 11require_once 'plugins/qrcode/qrcode.php';
12 12
@@ -14,12 +14,12 @@ require_once 'plugins/qrcode/qrcode.php';
14 * Class PluginQrcodeTest 14 * Class PluginQrcodeTest
15 * Unit test for the QR-Code plugin 15 * Unit test for the QR-Code plugin
16 */ 16 */
17class PluginQrcodeTest extends \PHPUnit\Framework\TestCase 17class PluginQrcodeTest extends \Shaarli\TestCase
18{ 18{
19 /** 19 /**
20 * Reset plugin path 20 * Reset plugin path
21 */ 21 */
22 public function setUp() 22 protected function setUp(): void
23 { 23 {
24 PluginManager::$PLUGINS_PATH = 'plugins'; 24 PluginManager::$PLUGINS_PATH = 'plugins';
25 } 25 }
@@ -57,7 +57,7 @@ class PluginQrcodeTest extends \PHPUnit\Framework\TestCase
57 { 57 {
58 $str = 'stuff'; 58 $str = 'stuff';
59 $data = array($str => $str); 59 $data = array($str => $str);
60 $data['_PAGE_'] = Router::$PAGE_LINKLIST; 60 $data['_PAGE_'] = TemplatePage::LINKLIST;
61 61
62 $data = hook_qrcode_render_footer($data); 62 $data = hook_qrcode_render_footer($data);
63 $this->assertEquals($str, $data[$str]); 63 $this->assertEquals($str, $data[$str]);
diff --git a/tests/plugins/PluginWallabagTest.php b/tests/plugins/PluginWallabagTest.php
index 79751921..36317215 100644
--- a/tests/plugins/PluginWallabagTest.php
+++ b/tests/plugins/PluginWallabagTest.php
@@ -10,12 +10,12 @@ require_once 'plugins/wallabag/wallabag.php';
10 * Class PluginWallabagTest 10 * Class PluginWallabagTest
11 * Unit test for the Wallabag plugin 11 * Unit test for the Wallabag plugin
12 */ 12 */
13class PluginWallabagTest extends \PHPUnit\Framework\TestCase 13class PluginWallabagTest extends \Shaarli\TestCase
14{ 14{
15 /** 15 /**
16 * Reset plugin path 16 * Reset plugin path
17 */ 17 */
18 public function setUp() 18 protected function setUp(): void
19 { 19 {
20 PluginManager::$PLUGINS_PATH = 'plugins'; 20 PluginManager::$PLUGINS_PATH = 'plugins';
21 } 21 }
diff --git a/tests/plugins/WallabagInstanceTest.php b/tests/plugins/WallabagInstanceTest.php
index a3cd9076..5ef3de1a 100644
--- a/tests/plugins/WallabagInstanceTest.php
+++ b/tests/plugins/WallabagInstanceTest.php
@@ -4,7 +4,7 @@ namespace Shaarli\Plugin\Wallabag;
4/** 4/**
5 * Class WallabagInstanceTest 5 * Class WallabagInstanceTest
6 */ 6 */
7class WallabagInstanceTest extends \PHPUnit\Framework\TestCase 7class WallabagInstanceTest extends \Shaarli\TestCase
8{ 8{
9 /** 9 /**
10 * @var string wallabag url. 10 * @var string wallabag url.
@@ -14,7 +14,7 @@ class WallabagInstanceTest extends \PHPUnit\Framework\TestCase
14 /** 14 /**
15 * Reset plugin path 15 * Reset plugin path
16 */ 16 */
17 public function setUp() 17 protected function setUp(): void
18 { 18 {
19 $this->instance = 'http://some.url'; 19 $this->instance = 'http://some.url';
20 } 20 }
diff --git a/tests/plugins/resources/hashtags.md b/tests/plugins/resources/hashtags.md
deleted file mode 100644
index 46326de3..00000000
--- a/tests/plugins/resources/hashtags.md
+++ /dev/null
@@ -1,10 +0,0 @@
1[#lol](?addtag=lol)
2
3 #test
4
5`#test2`
6
7```
8bla #bli blo
9#bla
10```
diff --git a/tests/plugins/resources/hashtags.raw b/tests/plugins/resources/hashtags.raw
deleted file mode 100644
index 9d2dc98a..00000000
--- a/tests/plugins/resources/hashtags.raw
+++ /dev/null
@@ -1,10 +0,0 @@
1#lol
2
3 #test
4
5`#test2`
6
7```
8bla #bli blo
9#bla
10```
diff --git a/tests/plugins/resources/markdown.html b/tests/plugins/resources/markdown.html
deleted file mode 100644
index c3460bf7..00000000
--- a/tests/plugins/resources/markdown.html
+++ /dev/null
@@ -1,33 +0,0 @@
1<div class="markdown"><ul>
2<li>test:
3<ul>
4<li><a href="http://link.tld">zero</a></li>
5<li><a href="http://link.tld">two</a></li>
6<li><a href="http://link.tld">three</a></li>
7</ul></li>
8</ul>
9<ol>
10<li><a href="http://link.tld">zero</a>
11<ol start="2">
12<li><a href="http://link.tld">two</a></li>
13<li><a href="http://link.tld">three</a></li>
14<li><a href="http://link.tld">four</a></li>
15<li>foo <a href="?addtag=foobar">#foobar</a></li>
16</ol></li>
17</ol>
18<p><a href="?addtag=foobar">#foobar</a> foo <code>lol #foo</code> <a href="?addtag=bar">#bar</a></p>
19<p>fsdfs <a href="http://link.tld">http://link.tld</a> <a href="?addtag=foobar">#foobar</a> <code>http://link.tld</code></p>
20<pre><code>http://link.tld #foobar
21next #foo</code></pre>
22<p>Block:</p>
23<pre><code>lorem ipsum #foobar http://link.tld
24#foobar http://link.tld</code></pre>
25<p><a href="?123456">link</a><br />
26<img src="/img/train.png" alt="link" /><br />
27<a href="http://test.tld/path/?query=value#hash">link</a><br />
28<a href="http://test.tld/path/?query=value#hash">link</a><br />
29<a href="https://test.tld/path/?query=value#hash">link</a><br />
30<a href="ftp://test.tld/path/?query=value#hash">link</a><br />
31<a href="magnet:test.tld/path/?query=value#hash">link</a><br />
32<a href="http://alert(&#039;xss&#039;)">link</a><br />
33<a href="http://test.tld/path/?query=value#hash">link</a></p></div>
diff --git a/tests/plugins/resources/markdown.md b/tests/plugins/resources/markdown.md
deleted file mode 100644
index 9350a8c7..00000000
--- a/tests/plugins/resources/markdown.md
+++ /dev/null
@@ -1,34 +0,0 @@
1* test:
2 * [zero](http://link.tld)
3 + [two](http://link.tld)
4 - [three](http://link.tld)
5
61. [zero](http://link.tld)
7 2. [two](http://link.tld)
8 3. [three](http://link.tld)
9 4. [four](http://link.tld)
10 5. foo #foobar
11
12#foobar foo `lol #foo` #bar
13
14fsdfs http://link.tld #foobar `http://link.tld`
15
16 http://link.tld #foobar
17 next #foo
18
19Block:
20
21```
22lorem ipsum #foobar http://link.tld
23#foobar http://link.tld
24```
25
26[link](?123456)
27![link](/img/train.png)
28[link](test.tld/path/?query=value#hash)
29[link](http://test.tld/path/?query=value#hash)
30[link](https://test.tld/path/?query=value#hash)
31[link](ftp://test.tld/path/?query=value#hash)
32[link](magnet:test.tld/path/?query=value#hash)
33[link](javascript:alert('xss'))
34[link](other://test.tld/path/?query=value#hash)
diff --git a/tests/plugins/test/test.php b/tests/plugins/test/test.php
index 2aaf5122..03be4f4e 100644
--- a/tests/plugins/test/test.php
+++ b/tests/plugins/test/test.php
@@ -13,9 +13,17 @@ function hook_test_random($data)
13 $data[1] = 'page test'; 13 $data[1] = 'page test';
14 } elseif (isset($data['_LOGGEDIN_']) && $data['_LOGGEDIN_'] === true) { 14 } elseif (isset($data['_LOGGEDIN_']) && $data['_LOGGEDIN_'] === true) {
15 $data[1] = 'loggedin'; 15 $data[1] = 'loggedin';
16 } elseif (array_key_exists('_LOGGEDIN_', $data)) {
17 $data[1] = 'loggedin';
18 $data[2] = $data['_LOGGEDIN_'];
16 } else { 19 } else {
17 $data[1] = $data[0]; 20 $data[1] = $data[0];
18 } 21 }
19 22
20 return $data; 23 return $data;
21} 24}
25
26function hook_test_error()
27{
28 new Unknown();
29}
diff --git a/tests/feed/CacheTest.php b/tests/render/PageCacheManagerTest.php
index c0a9f26f..08d4e5ea 100644
--- a/tests/feed/CacheTest.php
+++ b/tests/render/PageCacheManagerTest.php
@@ -1,18 +1,18 @@
1<?php 1<?php
2
2/** 3/**
3 * Cache tests 4 * Cache tests
4 */ 5 */
5namespace Shaarli\Feed;
6 6
7// required to access $_SESSION array 7namespace Shaarli\Render;
8session_start();
9 8
10require_once 'application/feed/Cache.php'; 9use Shaarli\Security\SessionManager;
10use Shaarli\TestCase;
11 11
12/** 12/**
13 * Unitary tests for cached pages 13 * Unitary tests for cached pages
14 */ 14 */
15class CacheTest extends \PHPUnit\Framework\TestCase 15class PageCacheManagerTest extends TestCase
16{ 16{
17 // test cache directory 17 // test cache directory
18 protected static $testCacheDir = 'sandbox/dummycache'; 18 protected static $testCacheDir = 'sandbox/dummycache';
@@ -20,12 +20,19 @@ class CacheTest extends \PHPUnit\Framework\TestCase
20 // dummy cached file names / content 20 // dummy cached file names / content
21 protected static $pages = array('a', 'toto', 'd7b59c'); 21 protected static $pages = array('a', 'toto', 'd7b59c');
22 22
23 /** @var PageCacheManager */
24 protected $cacheManager;
25
26 /** @var SessionManager */
27 protected $sessionManager;
23 28
24 /** 29 /**
25 * Populate the cache with dummy files 30 * Populate the cache with dummy files
26 */ 31 */
27 public function setUp() 32 protected function setUp(): void
28 { 33 {
34 $this->cacheManager = new PageCacheManager(static::$testCacheDir, true);
35
29 if (!is_dir(self::$testCacheDir)) { 36 if (!is_dir(self::$testCacheDir)) {
30 mkdir(self::$testCacheDir); 37 mkdir(self::$testCacheDir);
31 } else { 38 } else {
@@ -41,7 +48,7 @@ class CacheTest extends \PHPUnit\Framework\TestCase
41 /** 48 /**
42 * Remove dummycache folder after each tests. 49 * Remove dummycache folder after each tests.
43 */ 50 */
44 public function tearDown() 51 protected function tearDown(): void
45 { 52 {
46 array_map('unlink', glob(self::$testCacheDir . '/*')); 53 array_map('unlink', glob(self::$testCacheDir . '/*'));
47 rmdir(self::$testCacheDir); 54 rmdir(self::$testCacheDir);
@@ -52,7 +59,7 @@ class CacheTest extends \PHPUnit\Framework\TestCase
52 */ 59 */
53 public function testPurgeCachedPages() 60 public function testPurgeCachedPages()
54 { 61 {
55 purgeCachedPages(self::$testCacheDir); 62 $this->cacheManager->purgeCachedPages();
56 foreach (self::$pages as $page) { 63 foreach (self::$pages as $page) {
57 $this->assertFileNotExists(self::$testCacheDir . '/' . $page . '.cache'); 64 $this->assertFileNotExists(self::$testCacheDir . '/' . $page . '.cache');
58 } 65 }
@@ -65,28 +72,14 @@ class CacheTest extends \PHPUnit\Framework\TestCase
65 */ 72 */
66 public function testPurgeCachedPagesMissingDir() 73 public function testPurgeCachedPagesMissingDir()
67 { 74 {
75 $this->cacheManager = new PageCacheManager(self::$testCacheDir . '_missing', true);
76
68 $oldlog = ini_get('error_log'); 77 $oldlog = ini_get('error_log');
69 ini_set('error_log', '/dev/null'); 78 ini_set('error_log', '/dev/null');
70 $this->assertEquals( 79 $this->assertEquals(
71 'Cannot purge sandbox/dummycache_missing: no directory', 80 'Cannot purge sandbox/dummycache_missing: no directory',
72 purgeCachedPages(self::$testCacheDir . '_missing') 81 $this->cacheManager->purgeCachedPages()
73 ); 82 );
74 ini_set('error_log', $oldlog); 83 ini_set('error_log', $oldlog);
75 } 84 }
76
77 /**
78 * Purge cached pages and session cache
79 */
80 public function testInvalidateCaches()
81 {
82 $this->assertArrayNotHasKey('tags', $_SESSION);
83 $_SESSION['tags'] = array('goodbye', 'cruel', 'world');
84
85 invalidateCaches(self::$testCacheDir);
86 foreach (self::$pages as $page) {
87 $this->assertFileNotExists(self::$testCacheDir . '/' . $page . '.cache');
88 }
89
90 $this->assertArrayNotHasKey('tags', $_SESSION);
91 }
92} 85}
diff --git a/tests/render/ThemeUtilsTest.php b/tests/render/ThemeUtilsTest.php
index 58e3426b..7d841e4d 100644
--- a/tests/render/ThemeUtilsTest.php
+++ b/tests/render/ThemeUtilsTest.php
@@ -7,7 +7,7 @@ namespace Shaarli\Render;
7 * 7 *
8 * @package Shaarli 8 * @package Shaarli
9 */ 9 */
10class ThemeUtilsTest extends \PHPUnit\Framework\TestCase 10class ThemeUtilsTest extends \Shaarli\TestCase
11{ 11{
12 /** 12 /**
13 * Test getThemes() with existing theme directories. 13 * Test getThemes() with existing theme directories.
diff --git a/tests/security/BanManagerTest.php b/tests/security/BanManagerTest.php
index bba7c8ad..29d2791b 100644
--- a/tests/security/BanManagerTest.php
+++ b/tests/security/BanManagerTest.php
@@ -3,8 +3,9 @@
3 3
4namespace Shaarli\Security; 4namespace Shaarli\Security;
5 5
6use PHPUnit\Framework\TestCase; 6use Psr\Log\LoggerInterface;
7use Shaarli\FileUtils; 7use Shaarli\Helper\FileUtils;
8use Shaarli\TestCase;
8 9
9/** 10/**
10 * Test coverage for BanManager 11 * Test coverage for BanManager
@@ -32,7 +33,7 @@ class BanManagerTest extends TestCase
32 /** 33 /**
33 * Prepare or reset test resources 34 * Prepare or reset test resources
34 */ 35 */
35 public function setUp() 36 protected function setUp(): void
36 { 37 {
37 if (file_exists($this->banFile)) { 38 if (file_exists($this->banFile)) {
38 unlink($this->banFile); 39 unlink($this->banFile);
@@ -387,7 +388,7 @@ class BanManagerTest extends TestCase
387 3, 388 3,
388 1800, 389 1800,
389 $this->banFile, 390 $this->banFile,
390 $this->logFile 391 $this->createMock(LoggerInterface::class)
391 ); 392 );
392 } 393 }
393} 394}
diff --git a/tests/security/LoginManagerTest.php b/tests/security/LoginManagerTest.php
index 8fd1698c..f7609fc6 100644
--- a/tests/security/LoginManagerTest.php
+++ b/tests/security/LoginManagerTest.php
@@ -1,16 +1,17 @@
1<?php 1<?php
2namespace Shaarli\Security;
3 2
4require_once 'tests/utils/FakeConfigManager.php'; 3namespace Shaarli\Security;
5 4
6use PHPUnit\Framework\TestCase; 5use Psr\Log\LoggerInterface;
6use Shaarli\FakeConfigManager;
7use Shaarli\TestCase;
7 8
8/** 9/**
9 * Test coverage for LoginManager 10 * Test coverage for LoginManager
10 */ 11 */
11class LoginManagerTest extends TestCase 12class LoginManagerTest extends TestCase
12{ 13{
13 /** @var \FakeConfigManager Configuration Manager instance */ 14 /** @var FakeConfigManager Configuration Manager instance */
14 protected $configManager = null; 15 protected $configManager = null;
15 16
16 /** @var LoginManager Login Manager instance */ 17 /** @var LoginManager Login Manager instance */
@@ -58,10 +59,16 @@ class LoginManagerTest extends TestCase
58 /** @var string Salt used by hash functions */ 59 /** @var string Salt used by hash functions */
59 protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2'; 60 protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
60 61
62 /** @var CookieManager */
63 protected $cookieManager;
64
65 /** @var BanManager */
66 protected $banManager;
67
61 /** 68 /**
62 * Prepare or reset test resources 69 * Prepare or reset test resources
63 */ 70 */
64 public function setUp() 71 protected function setUp(): void
65 { 72 {
66 if (file_exists($this->banFile)) { 73 if (file_exists($this->banFile)) {
67 unlink($this->banFile); 74 unlink($this->banFile);
@@ -69,7 +76,7 @@ class LoginManagerTest extends TestCase
69 76
70 $this->passwordHash = sha1($this->password . $this->login . $this->salt); 77 $this->passwordHash = sha1($this->password . $this->login . $this->salt);
71 78
72 $this->configManager = new \FakeConfigManager([ 79 $this->configManager = new FakeConfigManager([
73 'credentials.login' => $this->login, 80 'credentials.login' => $this->login,
74 'credentials.hash' => $this->passwordHash, 81 'credentials.hash' => $this->passwordHash,
75 'credentials.salt' => $this->salt, 82 'credentials.salt' => $this->salt,
@@ -84,19 +91,34 @@ class LoginManagerTest extends TestCase
84 $this->cookie = []; 91 $this->cookie = [];
85 $this->session = []; 92 $this->session = [];
86 93
87 $this->sessionManager = new SessionManager($this->session, $this->configManager); 94 $this->cookieManager = $this->createMock(CookieManager::class);
88 $this->loginManager = new LoginManager($this->configManager, $this->sessionManager); 95 $this->cookieManager->method('getCookieParameter')->willReturnCallback(function (string $key) {
96 return $this->cookie[$key] ?? null;
97 });
98 $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path');
99 $this->banManager = $this->createMock(BanManager::class);
100 $this->loginManager = new LoginManager(
101 $this->configManager,
102 $this->sessionManager,
103 $this->cookieManager,
104 $this->banManager,
105 $this->createMock(LoggerInterface::class)
106 );
89 $this->server['REMOTE_ADDR'] = $this->ipAddr; 107 $this->server['REMOTE_ADDR'] = $this->ipAddr;
90 } 108 }
91 109
92 /** 110 /**
93 * Record a failed login attempt 111 * Record a failed login attempt
94 */ 112 */
95 public function testHandleFailedLogin() 113 public function testHandleFailedLogin(): void
96 { 114 {
115 $this->banManager->expects(static::exactly(2))->method('handleFailedAttempt');
116 $this->banManager->method('isBanned')->willReturn(true);
117
97 $this->loginManager->handleFailedLogin($this->server); 118 $this->loginManager->handleFailedLogin($this->server);
98 $this->loginManager->handleFailedLogin($this->server); 119 $this->loginManager->handleFailedLogin($this->server);
99 $this->assertFalse($this->loginManager->canLogin($this->server)); 120
121 static::assertFalse($this->loginManager->canLogin($this->server));
100 } 122 }
101 123
102 /** 124 /**
@@ -108,8 +130,13 @@ class LoginManagerTest extends TestCase
108 'REMOTE_ADDR' => $this->trustedProxy, 130 'REMOTE_ADDR' => $this->trustedProxy,
109 'HTTP_X_FORWARDED_FOR' => $this->ipAddr, 131 'HTTP_X_FORWARDED_FOR' => $this->ipAddr,
110 ]; 132 ];
133
134 $this->banManager->expects(static::exactly(2))->method('handleFailedAttempt');
135 $this->banManager->method('isBanned')->willReturn(true);
136
111 $this->loginManager->handleFailedLogin($server); 137 $this->loginManager->handleFailedLogin($server);
112 $this->loginManager->handleFailedLogin($server); 138 $this->loginManager->handleFailedLogin($server);
139
113 $this->assertFalse($this->loginManager->canLogin($server)); 140 $this->assertFalse($this->loginManager->canLogin($server));
114 } 141 }
115 142
@@ -190,11 +217,17 @@ class LoginManagerTest extends TestCase
190 */ 217 */
191 public function testCheckLoginStateNotConfigured() 218 public function testCheckLoginStateNotConfigured()
192 { 219 {
193 $configManager = new \FakeConfigManager([ 220 $configManager = new FakeConfigManager([
194 'resource.ban_file' => $this->banFile, 221 'resource.ban_file' => $this->banFile,
195 ]); 222 ]);
196 $loginManager = new LoginManager($configManager, null); 223 $loginManager = new LoginManager(
197 $loginManager->checkLoginState([], ''); 224 $configManager,
225 $this->sessionManager,
226 $this->cookieManager,
227 $this->banManager,
228 $this->createMock(LoggerInterface::class)
229 );
230 $loginManager->checkLoginState('');
198 231
199 $this->assertFalse($loginManager->isLoggedIn()); 232 $this->assertFalse($loginManager->isLoggedIn());
200 } 233 }
@@ -210,9 +243,9 @@ class LoginManagerTest extends TestCase
210 'expires_on' => time() + 100, 243 'expires_on' => time() + 100,
211 ]; 244 ];
212 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 245 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
213 $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope'; 246 $this->cookie[CookieManager::STAY_SIGNED_IN] = 'nope';
214 247
215 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); 248 $this->loginManager->checkLoginState($this->clientIpAddress);
216 249
217 $this->assertTrue($this->loginManager->isLoggedIn()); 250 $this->assertTrue($this->loginManager->isLoggedIn());
218 $this->assertTrue(empty($this->session['username'])); 251 $this->assertTrue(empty($this->session['username']));
@@ -224,9 +257,9 @@ class LoginManagerTest extends TestCase
224 public function testCheckLoginStateStaySignedInWithValidToken() 257 public function testCheckLoginStateStaySignedInWithValidToken()
225 { 258 {
226 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 259 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
227 $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken(); 260 $this->cookie[CookieManager::STAY_SIGNED_IN] = $this->loginManager->getStaySignedInToken();
228 261
229 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); 262 $this->loginManager->checkLoginState($this->clientIpAddress);
230 263
231 $this->assertTrue($this->loginManager->isLoggedIn()); 264 $this->assertTrue($this->loginManager->isLoggedIn());
232 $this->assertEquals($this->login, $this->session['username']); 265 $this->assertEquals($this->login, $this->session['username']);
@@ -241,7 +274,7 @@ class LoginManagerTest extends TestCase
241 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 274 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
242 $this->session['expires_on'] = time() - 100; 275 $this->session['expires_on'] = time() - 100;
243 276
244 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); 277 $this->loginManager->checkLoginState($this->clientIpAddress);
245 278
246 $this->assertFalse($this->loginManager->isLoggedIn()); 279 $this->assertFalse($this->loginManager->isLoggedIn());
247 } 280 }
@@ -253,7 +286,7 @@ class LoginManagerTest extends TestCase
253 { 286 {
254 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 287 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
255 288
256 $this->loginManager->checkLoginState($this->cookie, '10.7.157.98'); 289 $this->loginManager->checkLoginState('10.7.157.98');
257 290
258 $this->assertFalse($this->loginManager->isLoggedIn()); 291 $this->assertFalse($this->loginManager->isLoggedIn());
259 } 292 }
@@ -264,7 +297,7 @@ class LoginManagerTest extends TestCase
264 public function testCheckCredentialsWrongLogin() 297 public function testCheckCredentialsWrongLogin()
265 { 298 {
266 $this->assertFalse( 299 $this->assertFalse(
267 $this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password) 300 $this->loginManager->checkCredentials('', 'b4dl0g1n', $this->password)
268 ); 301 );
269 } 302 }
270 303
@@ -274,7 +307,7 @@ class LoginManagerTest extends TestCase
274 public function testCheckCredentialsWrongPassword() 307 public function testCheckCredentialsWrongPassword()
275 { 308 {
276 $this->assertFalse( 309 $this->assertFalse(
277 $this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd') 310 $this->loginManager->checkCredentials('', $this->login, 'b4dp455wd')
278 ); 311 );
279 } 312 }
280 313
@@ -284,7 +317,7 @@ class LoginManagerTest extends TestCase
284 public function testCheckCredentialsWrongLoginAndPassword() 317 public function testCheckCredentialsWrongLoginAndPassword()
285 { 318 {
286 $this->assertFalse( 319 $this->assertFalse(
287 $this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd') 320 $this->loginManager->checkCredentials('', 'b4dl0g1n', 'b4dp455wd')
288 ); 321 );
289 } 322 }
290 323
@@ -294,7 +327,7 @@ class LoginManagerTest extends TestCase
294 public function testCheckCredentialsGoodLoginAndPassword() 327 public function testCheckCredentialsGoodLoginAndPassword()
295 { 328 {
296 $this->assertTrue( 329 $this->assertTrue(
297 $this->loginManager->checkCredentials('', '', $this->login, $this->password) 330 $this->loginManager->checkCredentials('', $this->login, $this->password)
298 ); 331 );
299 } 332 }
300 333
@@ -305,7 +338,7 @@ class LoginManagerTest extends TestCase
305 { 338 {
306 $this->configManager->set('ldap.host', 'dummy'); 339 $this->configManager->set('ldap.host', 'dummy');
307 $this->assertFalse( 340 $this->assertFalse(
308 $this->loginManager->checkCredentials('', '', $this->login, $this->password) 341 $this->loginManager->checkCredentials('', $this->login, $this->password)
309 ); 342 );
310 } 343 }
311 344
diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php
index f264505e..6830d714 100644
--- a/tests/security/SessionManagerTest.php
+++ b/tests/security/SessionManagerTest.php
@@ -1,12 +1,9 @@
1<?php 1<?php
2require_once 'tests/utils/FakeConfigManager.php';
3 2
4// Initialize reference data _before_ PHPUnit starts a session 3namespace Shaarli\Security;
5require_once 'tests/utils/ReferenceSessionIdHashes.php';
6ReferenceSessionIdHashes::genAllHashes();
7 4
8use PHPUnit\Framework\TestCase; 5use Shaarli\FakeConfigManager;
9use Shaarli\Security\SessionManager; 6use Shaarli\TestCase;
10 7
11/** 8/**
12 * Test coverage for SessionManager 9 * Test coverage for SessionManager
@@ -16,7 +13,7 @@ class SessionManagerTest extends TestCase
16 /** @var array Session ID hashes */ 13 /** @var array Session ID hashes */
17 protected static $sidHashes = null; 14 protected static $sidHashes = null;
18 15
19 /** @var \FakeConfigManager ConfigManager substitute for testing */ 16 /** @var FakeConfigManager ConfigManager substitute for testing */
20 protected $conf = null; 17 protected $conf = null;
21 18
22 /** @var array $_SESSION array for testing */ 19 /** @var array $_SESSION array for testing */
@@ -28,15 +25,15 @@ class SessionManagerTest extends TestCase
28 /** 25 /**
29 * Assign reference data 26 * Assign reference data
30 */ 27 */
31 public static function setUpBeforeClass() 28 public static function setUpBeforeClass(): void
32 { 29 {
33 self::$sidHashes = ReferenceSessionIdHashes::getHashes(); 30 self::$sidHashes = \ReferenceSessionIdHashes::getHashes();
34 } 31 }
35 32
36 /** 33 /**
37 * Initialize or reset test resources 34 * Initialize or reset test resources
38 */ 35 */
39 public function setUp() 36 protected function setUp(): void
40 { 37 {
41 $this->conf = new FakeConfigManager([ 38 $this->conf = new FakeConfigManager([
42 'credentials.login' => 'johndoe', 39 'credentials.login' => 'johndoe',
@@ -44,7 +41,7 @@ class SessionManagerTest extends TestCase
44 'security.session_protection_disabled' => false, 41 'security.session_protection_disabled' => false,
45 ]); 42 ]);
46 $this->session = []; 43 $this->session = [];
47 $this->sessionManager = new SessionManager($this->session, $this->conf); 44 $this->sessionManager = new SessionManager($this->session, $this->conf, 'session_path');
48 } 45 }
49 46
50 /** 47 /**
@@ -69,7 +66,7 @@ class SessionManagerTest extends TestCase
69 $token => 1, 66 $token => 1,
70 ], 67 ],
71 ]; 68 ];
72 $sessionManager = new SessionManager($session, $this->conf); 69 $sessionManager = new SessionManager($session, $this->conf, 'session_path');
73 70
74 // check and destroy the token 71 // check and destroy the token
75 $this->assertTrue($sessionManager->checkToken($token)); 72 $this->assertTrue($sessionManager->checkToken($token));
@@ -211,15 +208,16 @@ class SessionManagerTest extends TestCase
211 'expires_on' => time() + 1000, 208 'expires_on' => time() + 1000,
212 'username' => 'johndoe', 209 'username' => 'johndoe',
213 'visibility' => 'public', 210 'visibility' => 'public',
214 'untaggedonly' => false, 211 'untaggedonly' => true,
215 ]; 212 ];
216 $this->sessionManager->logout(); 213 $this->sessionManager->logout();
217 214
218 $this->assertFalse(isset($this->session['ip'])); 215 $this->assertArrayNotHasKey('ip', $this->session);
219 $this->assertFalse(isset($this->session['expires_on'])); 216 $this->assertArrayNotHasKey('expires_on', $this->session);
220 $this->assertFalse(isset($this->session['username'])); 217 $this->assertArrayNotHasKey('username', $this->session);
221 $this->assertFalse(isset($this->session['visibility'])); 218 $this->assertArrayNotHasKey('visibility', $this->session);
222 $this->assertFalse(isset($this->session['untaggedonly'])); 219 $this->assertArrayHasKey('untaggedonly', $this->session);
220 $this->assertTrue($this->session['untaggedonly']);
223 } 221 }
224 222
225 /** 223 /**
@@ -269,4 +267,61 @@ class SessionManagerTest extends TestCase
269 $this->session['ip'] = 'ip_id_one'; 267 $this->session['ip'] = 'ip_id_one';
270 $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two')); 268 $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two'));
271 } 269 }
270
271 /**
272 * Test creating an entry in the session array
273 */
274 public function testSetSessionParameterCreate(): void
275 {
276 $this->sessionManager->setSessionParameter('abc', 'def');
277
278 static::assertSame('def', $this->session['abc']);
279 }
280
281 /**
282 * Test updating an entry in the session array
283 */
284 public function testSetSessionParameterUpdate(): void
285 {
286 $this->session['abc'] = 'ghi';
287
288 $this->sessionManager->setSessionParameter('abc', 'def');
289
290 static::assertSame('def', $this->session['abc']);
291 }
292
293 /**
294 * Test updating an entry in the session array with null value
295 */
296 public function testSetSessionParameterUpdateNull(): void
297 {
298 $this->session['abc'] = 'ghi';
299
300 $this->sessionManager->setSessionParameter('abc', null);
301
302 static::assertArrayHasKey('abc', $this->session);
303 static::assertNull($this->session['abc']);
304 }
305
306 /**
307 * Test deleting an existing entry in the session array
308 */
309 public function testDeleteSessionParameter(): void
310 {
311 $this->session['abc'] = 'def';
312
313 $this->sessionManager->deleteSessionParameter('abc');
314
315 static::assertArrayNotHasKey('abc', $this->session);
316 }
317
318 /**
319 * Test deleting a non existent entry in the session array
320 */
321 public function testDeleteSessionParameterNotExisting(): void
322 {
323 $this->sessionManager->deleteSessionParameter('abc');
324
325 static::assertArrayNotHasKey('abc', $this->session);
326 }
272} 327}
diff --git a/tests/updater/DummyUpdater.php b/tests/updater/DummyUpdater.php
index 07c7f5c4..3403233f 100644
--- a/tests/updater/DummyUpdater.php
+++ b/tests/updater/DummyUpdater.php
@@ -37,7 +37,7 @@ class DummyUpdater extends Updater
37 * 37 *
38 * @return bool true. 38 * @return bool true.
39 */ 39 */
40 final private function updateMethodDummy1() 40 final protected function updateMethodDummy1()
41 { 41 {
42 return true; 42 return true;
43 } 43 }
@@ -47,7 +47,7 @@ class DummyUpdater extends Updater
47 * 47 *
48 * @return bool true. 48 * @return bool true.
49 */ 49 */
50 final private function updateMethodDummy2() 50 final protected function updateMethodDummy2()
51 { 51 {
52 return true; 52 return true;
53 } 53 }
@@ -57,7 +57,7 @@ class DummyUpdater extends Updater
57 * 57 *
58 * @return bool true. 58 * @return bool true.
59 */ 59 */
60 final private function updateMethodDummy3() 60 final protected function updateMethodDummy3()
61 { 61 {
62 return true; 62 return true;
63 } 63 }
@@ -67,7 +67,7 @@ class DummyUpdater extends Updater
67 * 67 *
68 * @throws Exception error. 68 * @throws Exception error.
69 */ 69 */
70 final private function updateMethodException() 70 final protected function updateMethodException()
71 { 71 {
72 throw new Exception('whatever'); 72 throw new Exception('whatever');
73 } 73 }
diff --git a/tests/updater/UpdaterTest.php b/tests/updater/UpdaterTest.php
index c689982b..47332544 100644
--- a/tests/updater/UpdaterTest.php
+++ b/tests/updater/UpdaterTest.php
@@ -2,17 +2,19 @@
2namespace Shaarli\Updater; 2namespace Shaarli\Updater;
3 3
4use Exception; 4use Exception;
5use malkusch\lock\mutex\NoMutex;
6use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\BookmarkServiceInterface;
5use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\History;
10use Shaarli\TestCase;
6 11
7require_once 'tests/updater/DummyUpdater.php';
8require_once 'tests/utils/ReferenceLinkDB.php';
9require_once 'inc/rain.tpl.class.php';
10 12
11/** 13/**
12 * Class UpdaterTest. 14 * Class UpdaterTest.
13 * Runs unit tests against the updater class. 15 * Runs unit tests against the updater class.
14 */ 16 */
15class UpdaterTest extends \PHPUnit\Framework\TestCase 17class UpdaterTest extends TestCase
16{ 18{
17 /** 19 /**
18 * @var string Path to test datastore. 20 * @var string Path to test datastore.
@@ -29,13 +31,28 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
29 */ 31 */
30 protected $conf; 32 protected $conf;
31 33
34 /** @var BookmarkServiceInterface */
35 protected $bookmarkService;
36
37 /** @var \ReferenceLinkDB */
38 protected $refDB;
39
40 /** @var Updater */
41 protected $updater;
42
32 /** 43 /**
33 * Executed before each test. 44 * Executed before each test.
34 */ 45 */
35 public function setUp() 46 protected function setUp(): void
36 { 47 {
48 $mutex = new NoMutex();
49 $this->refDB = new \ReferenceLinkDB();
50 $this->refDB->write(self::$testDatastore);
51
37 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); 52 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
38 $this->conf = new ConfigManager(self::$configFile); 53 $this->conf = new ConfigManager(self::$configFile);
54 $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), $mutex, true);
55 $this->updater = new Updater([], $this->bookmarkService, $this->conf, true);
39 } 56 }
40 57
41 /** 58 /**
@@ -72,23 +89,23 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
72 89
73 /** 90 /**
74 * Test errors in UpdaterUtils::write_updates_file(): empty updates file. 91 * Test errors in UpdaterUtils::write_updates_file(): empty updates file.
75 *
76 * @expectedException Exception
77 * @expectedExceptionMessageRegExp /Updates file path is not set(.*)/
78 */ 92 */
79 public function testWriteEmptyUpdatesFile() 93 public function testWriteEmptyUpdatesFile()
80 { 94 {
95 $this->expectException(\Exception::class);
96 $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/');
97
81 UpdaterUtils::write_updates_file('', array('test')); 98 UpdaterUtils::write_updates_file('', array('test'));
82 } 99 }
83 100
84 /** 101 /**
85 * Test errors in UpdaterUtils::write_updates_file(): not writable updates file. 102 * Test errors in UpdaterUtils::write_updates_file(): not writable updates file.
86 *
87 * @expectedException Exception
88 * @expectedExceptionMessageRegExp /Unable to write(.*)/
89 */ 103 */
90 public function testWriteUpdatesFileNotWritable() 104 public function testWriteUpdatesFileNotWritable()
91 { 105 {
106 $this->expectException(\Exception::class);
107 $this->expectExceptionMessageRegExp('/Unable to write(.*)/');
108
92 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 109 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
93 touch($updatesFile); 110 touch($updatesFile);
94 chmod($updatesFile, 0444); 111 chmod($updatesFile, 0444);
@@ -153,11 +170,11 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
153 170
154 /** 171 /**
155 * Test Update failed. 172 * Test Update failed.
156 *
157 * @expectedException \Exception
158 */ 173 */
159 public function testUpdateFailed() 174 public function testUpdateFailed()
160 { 175 {
176 $this->expectException(\Exception::class);
177
161 $updates = array( 178 $updates = array(
162 'updateMethodDummy1', 179 'updateMethodDummy1',
163 'updateMethodDummy2', 180 'updateMethodDummy2',
@@ -167,4 +184,40 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
167 $updater = new DummyUpdater($updates, array(), $this->conf, true); 184 $updater = new DummyUpdater($updates, array(), $this->conf, true);
168 $updater->update(); 185 $updater->update();
169 } 186 }
187
188 public function testUpdateMethodRelativeHomeLinkRename(): void
189 {
190 $this->updater->setBasePath('/subfolder');
191 $this->conf->set('general.header_link', '?');
192
193 $this->updater->updateMethodRelativeHomeLink();
194
195 static::assertSame('/subfolder/', $this->conf->get('general.header_link'));
196 }
197
198 public function testUpdateMethodRelativeHomeLinkDoNotRename(): void
199 {
200 $this->conf->set('general.header_link', '~/my-blog');
201
202 $this->updater->updateMethodRelativeHomeLink();
203
204 static::assertSame('~/my-blog', $this->conf->get('general.header_link'));
205 }
206
207 public function testUpdateMethodMigrateExistingNotesUrl(): void
208 {
209 $this->updater->updateMethodMigrateExistingNotesUrl();
210
211 static::assertSame($this->refDB->getLinks()[0]->getUrl(), $this->bookmarkService->get(0)->getUrl());
212 static::assertSame($this->refDB->getLinks()[1]->getUrl(), $this->bookmarkService->get(1)->getUrl());
213 static::assertSame($this->refDB->getLinks()[4]->getUrl(), $this->bookmarkService->get(4)->getUrl());
214 static::assertSame($this->refDB->getLinks()[6]->getUrl(), $this->bookmarkService->get(6)->getUrl());
215 static::assertSame($this->refDB->getLinks()[7]->getUrl(), $this->bookmarkService->get(7)->getUrl());
216 static::assertSame($this->refDB->getLinks()[8]->getUrl(), $this->bookmarkService->get(8)->getUrl());
217 static::assertSame($this->refDB->getLinks()[9]->getUrl(), $this->bookmarkService->get(9)->getUrl());
218 static::assertSame('/shaare/WDWyig', $this->bookmarkService->get(42)->getUrl());
219 static::assertSame('/shaare/WDWyig', $this->bookmarkService->get(41)->getUrl());
220 static::assertSame('/shaare/0gCTjQ', $this->bookmarkService->get(10)->getUrl());
221 static::assertSame('/shaare/PCRizQ', $this->bookmarkService->get(11)->getUrl());
222 }
170} 223}
diff --git a/tests/utils/FakeApplicationUtils.php b/tests/utils/FakeApplicationUtils.php
index de83d598..d5289ede 100644
--- a/tests/utils/FakeApplicationUtils.php
+++ b/tests/utils/FakeApplicationUtils.php
@@ -2,6 +2,8 @@
2 2
3namespace Shaarli; 3namespace Shaarli;
4 4
5use Shaarli\Helper\ApplicationUtils;
6
5/** 7/**
6 * Fake ApplicationUtils class to avoid HTTP requests 8 * Fake ApplicationUtils class to avoid HTTP requests
7 */ 9 */
diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php
index 360b34a9..014c2af0 100644
--- a/tests/utils/FakeConfigManager.php
+++ b/tests/utils/FakeConfigManager.php
@@ -1,9 +1,13 @@
1<?php 1<?php
2 2
3namespace Shaarli;
4
5use Shaarli\Config\ConfigManager;
6
3/** 7/**
4 * Fake ConfigManager 8 * Fake ConfigManager
5 */ 9 */
6class FakeConfigManager 10class FakeConfigManager extends ConfigManager
7{ 11{
8 protected $values = []; 12 protected $values = [];
9 13
@@ -23,7 +27,7 @@ class FakeConfigManager
23 * @param string $key Key of the value to set 27 * @param string $key Key of the value to set
24 * @param mixed $value Value to set 28 * @param mixed $value Value to set
25 */ 29 */
26 public function set($key, $value) 30 public function set($key, $value, $write = false, $isLoggedIn = false)
27 { 31 {
28 $this->values[$key] = $value; 32 $this->values[$key] = $value;
29 } 33 }
@@ -35,7 +39,7 @@ class FakeConfigManager
35 * 39 *
36 * @return mixed The value if set, else the name of the key 40 * @return mixed The value if set, else the name of the key
37 */ 41 */
38 public function get($key) 42 public function get($key, $default = '')
39 { 43 {
40 if (isset($this->values[$key])) { 44 if (isset($this->values[$key])) {
41 return $this->values[$key]; 45 return $this->values[$key];
diff --git a/tests/utils/ReferenceHistory.php b/tests/utils/ReferenceHistory.php
index 516c9f51..aed5d2cf 100644
--- a/tests/utils/ReferenceHistory.php
+++ b/tests/utils/ReferenceHistory.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2 2
3use Shaarli\FileUtils; 3use Shaarli\Helper\FileUtils;
4use Shaarli\History; 4use Shaarli\History;
5 5
6/** 6/**
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index 0095f5a1..1f53dc3c 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -30,7 +30,7 @@ class ReferenceLinkDB
30 $this->addLink( 30 $this->addLink(
31 11, 31 11,
32 'Pined older', 32 'Pined older',
33 '?PCRizQ', 33 '/shaare/PCRizQ',
34 'This is an older pinned link', 34 'This is an older pinned link',
35 0, 35 0,
36 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100309_101010'), 36 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100309_101010'),
@@ -43,7 +43,7 @@ class ReferenceLinkDB
43 $this->addLink( 43 $this->addLink(
44 10, 44 10,
45 'Pined', 45 'Pined',
46 '?0gCTjQ', 46 '/shaare/0gCTjQ',
47 'This is a pinned link', 47 'This is a pinned link',
48 0, 48 0,
49 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121207_152312'), 49 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121207_152312'),
@@ -56,7 +56,7 @@ class ReferenceLinkDB
56 $this->addLink( 56 $this->addLink(
57 41, 57 41,
58 'Link title: @website', 58 'Link title: @website',
59 '?WDWyig', 59 '/shaare/WDWyig',
60 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag', 60 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag',
61 0, 61 0,
62 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), 62 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'),
@@ -68,7 +68,7 @@ class ReferenceLinkDB
68 $this->addLink( 68 $this->addLink(
69 42, 69 42,
70 'Note: I have a big ID but an old date', 70 'Note: I have a big ID but an old date',
71 '?WDWyig', 71 '/shaare/WDWyig',
72 'Used to test bookmarks reordering.', 72 'Used to test bookmarks reordering.',
73 0, 73 0,
74 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'), 74 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
@@ -82,7 +82,7 @@ class ReferenceLinkDB
82 'This guide extends and expands on PSR-1, the basic coding standard.', 82 'This guide extends and expands on PSR-1, the basic coding standard.',
83 0, 83 0,
84 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'), 84 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'),
85 '' 85 'coding-style standards quality assurance'
86 ); 86 );
87 87
88 $this->addLink( 88 $this->addLink(
diff --git a/tpl/default/404.html b/tpl/default/404.html
index 09737b4b..7b696e4c 100644
--- a/tpl/default/404.html
+++ b/tpl/default/404.html
@@ -8,7 +8,7 @@
8 {include="page.header"} 8 {include="page.header"}
9<div id="pageError" class="page-error-container center"> 9<div id="pageError" class="page-error-container center">
10 <h2>{'Sorry, nothing to see here.'|t}</h2> 10 <h2>{'Sorry, nothing to see here.'|t}</h2>
11 <img src="img/sad_star.png" alt=""> 11 <img src="{$asset_path}/img/sad_star.png#" alt="">
12 <p>{$error_message}</p> 12 <p>{$error_message}</p>
13</div> 13</div>
14{include="page.footer"} 14{include="page.footer"}
diff --git a/tpl/default/addlink.html b/tpl/default/addlink.html
index b4b4a0ec..4aac7ff1 100644
--- a/tpl/default/addlink.html
+++ b/tpl/default/addlink.html
@@ -9,7 +9,7 @@
9 <div class="pure-u-lg-1-3 pure-u-1-24"></div> 9 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> 10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
11 <h2 class="window-title">{"Shaare a new link"|t}</h2> 11 <h2 class="window-title">{"Shaare a new link"|t}</h2>
12 <form method="GET" action="#" name="addform" class="addform"> 12 <form method="GET" action="{$base_path}/admin/shaare" name="addform" class="addform">
13 <div> 13 <div>
14 <label for="shaare">{'URL or leave empty to post a note'|t}</label> 14 <label for="shaare">{'URL or leave empty to post a note'|t}</label>
15 <input type="text" name="post" id="shaare" class="autofocus"> 15 <input type="text" name="post" id="shaare" class="autofocus">
@@ -20,6 +20,62 @@
20 </form> 20 </form>
21 </div> 21 </div>
22</div> 22</div>
23
24<div class="pure-g addlink-batch-show-more-block pure-u-0">
25 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
26 <div class="pure-u-lg-1-3 pure-u-22-24 addlink-batch-show-more">
27 <a href="#">{'BULK CREATION'|t}&nbsp;<i class="fa fa-plus-circle" aria-hidden="true"></i></a>
28 </div>
29</div>
30
31<div class="addlink-batch-form-block">
32 {if="empty($async_metadata)"}
33 <div class="pure-g pure-alert pure-alert-warning pure-alert-closable">
34 <div class="pure-u-2-24"></div>
35 <div class="pure-u-20-24">
36 <p>
37 {'Metadata asynchronous retrieval is disabled.'|t}
38 {'We recommend that you enable the setting <em>general > enable_async_metadata</em> in your configuration file to use bulk link creation.'|t}
39 </p>
40 </div>
41 <div class="pure-u-2-24">
42 <i class="fa fa-times pure-alert-close"></i>
43 </div>
44 </div>
45 {/if}
46
47 <div class="pure-g">
48 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
49 <div id="batch-addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
50 <h2 class="window-title">{"Shaare multiple new links"|t}</h2>
51 <form method="POST" action="{$base_path}/admin/shaare-batch" name="batch-addform" class="batch-addform">
52 <div>
53 <label for="urls">{'Add one URL per line to create multiple bookmarks.'|t}</label>
54 <textarea name="urls" id="urls"></textarea>
55
56 <div>
57 <label for="tags">{'Tags'|t}</label>
58 </div>
59 <div>
60 <input type="text" name="tags" id="tags" class="lf_input"
61 data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off">
62 </div>
63
64 <div>
65 <input type="hidden" name="private" value="0">
66 <input type="checkbox" name="private" {if="$default_private_links"} checked="checked"{/if}>
67 &nbsp; <label for="lf_private">{'Private'|t}</label>
68 </div>
69 </div>
70 <div>
71 <input type="hidden" name="token" value="{$token}">
72 <input type="submit" value="{'Add links'|t}">
73 </div>
74 </form>
75 </div>
76 </div>
77</div>
78
23{include="page.footer"} 79{include="page.footer"}
24</body> 80</body>
25</html> 81</html>
diff --git a/tpl/default/changepassword.html b/tpl/default/changepassword.html
index ab579433..736774f3 100644
--- a/tpl/default/changepassword.html
+++ b/tpl/default/changepassword.html
@@ -9,7 +9,7 @@
9 <div class="pure-u-lg-1-3 pure-u-1-24"></div> 9 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> 10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
11 <h2 class="window-title">{"Change password"|t}</h2> 11 <h2 class="window-title">{"Change password"|t}</h2>
12 <form method="POST" action="#" name="changepasswordform" id="changepasswordform"> 12 <form method="POST" action="{$base_path}/admin/password" name="changepasswordform" id="changepasswordform">
13 <div> 13 <div>
14 <input type="password" name="oldpassword" aria-label="{'Current password'|t}" placeholder="{'Current password'|t}" class="autofocus"> 14 <input type="password" name="oldpassword" aria-label="{'Current password'|t}" placeholder="{'Current password'|t}" class="autofocus">
15 </div> 15 </div>
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html
index ec6e0b46..89d08e2c 100644
--- a/tpl/default/changetag.html
+++ b/tpl/default/changetag.html
@@ -9,7 +9,7 @@
9 <div class="pure-u-lg-1-3 pure-u-1-24"></div> 9 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24"> 10 <div id="addlink-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
11 <h2 class="window-title">{"Manage tags"|t}</h2> 11 <h2 class="window-title">{"Manage tags"|t}</h2>
12 <form method="POST" action="#" name="changetag" id="changetag"> 12 <form method="POST" action="{$base_path}/admin/tags" name="changetag" id="changetag">
13 <div> 13 <div>
14 <input type="text" name="fromtag" aria-label="{'Tag'|t}" placeholder="{'Tag'|t}" value="{$fromtag}" 14 <input type="text" name="fromtag" aria-label="{'Tag'|t}" placeholder="{'Tag'|t}" value="{$fromtag}"
15 list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1"> 15 list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1">
@@ -27,12 +27,12 @@
27 <div><i class="fa fa-info-circle" aria-hidden="true"></i> {'Case sensitive'|t}</div> 27 <div><i class="fa fa-info-circle" aria-hidden="true"></i> {'Case sensitive'|t}</div>
28 <input type="hidden" name="token" value="{$token}"> 28 <input type="hidden" name="token" value="{$token}">
29 <div> 29 <div>
30 <input type="submit" value="{'Rename'|t}" name="renametag"> 30 <input type="submit" value="{'Rename tag'|t}" name="renametag">
31 <input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete"> 31 <input type="submit" value="{'Delete tag'|t}" name="deletetag" class="button button-red confirm-delete">
32 </div> 32 </div>
33 </form> 33 </form>
34 34
35 <p>{'You can also edit tags in the'|t} <a href="?do=taglist&sort=usage">{'tag list'|t}</a>.</p> 35 <p>{'You can also edit tags in the'|t} <a href="{$base_path}/tags/list?sort=usage">{'tag list'|t}</a>.</p>
36 </div> 36 </div>
37</div> 37</div>
38{include="page.footer"} 38{include="page.footer"}
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index 8b75900d..bb2564af 100644
--- a/tpl/default/configure.html
+++ b/tpl/default/configure.html
@@ -11,7 +11,7 @@
11{$ratioInput='7-12'} 11{$ratioInput='7-12'}
12{$ratioInputMobile='1-8'} 12{$ratioInputMobile='1-8'}
13 13
14<form method="POST" action="#" name="configform" id="configform"> 14<form method="POST" action="{$base_path}/admin/configure" name="configform" id="configform">
15 <div class="pure-g"> 15 <div class="pure-g">
16 <div class="pure-u-lg-1-8 pure-u-1-24"></div> 16 <div class="pure-u-lg-1-8 pure-u-1-24"></div>
17 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete"> 17 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete">
@@ -35,7 +35,7 @@
35 <div class="form-label"> 35 <div class="form-label">
36 <label for="titleLink"> 36 <label for="titleLink">
37 <span class="label-name">{'Home link'|t}</span><br> 37 <span class="label-name">{'Home link'|t}</span><br>
38 <span class="label-desc">{'Default value'|t}: ?</span> 38 <span class="label-desc">{'Default value'|t}: {$base_path}/</span>
39 </label> 39 </label>
40 </div> 40 </div>
41 </div> 41 </div>
@@ -289,7 +289,7 @@
289 {if="! $gd_enabled"} 289 {if="! $gd_enabled"}
290 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t} 290 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
291 {elseif="$thumbnails_enabled"} 291 {elseif="$thumbnails_enabled"}
292 <a href="?do=thumbs_update">{'Synchronize thumbnails'|t}</a> 292 <a href="{$base_path}/admin/thumbnails">{'Synchronize thumbnails'|t}</a>
293 {/if} 293 {/if}
294 </span> 294 </span>
295 </label> 295 </label>
diff --git a/tpl/default/daily.html b/tpl/default/daily.html
index 6b5103a4..5e038c39 100644
--- a/tpl/default/daily.html
+++ b/tpl/default/daily.html
@@ -7,11 +7,24 @@
7{include="page.header"} 7{include="page.header"}
8 8
9<div class="pure-g"> 9<div class="pure-g">
10 <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
11 <a href="{$base_path}/daily?day">{'Daily'|t}</a>
12 <a href="{$base_path}/daily?week">{'Weekly'|t}</a>
13 <a href="{$base_path}/daily?month">{'Monthly'|t}</a>
14 </div>
15</div>
16
17
18<div class="pure-g">
10 <div class="pure-u-lg-1-6 pure-u-1-24"></div> 19 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
11 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily"> 20 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily">
12 <h2 class="window-title"> 21 <h2 class="window-title">
13 {'The Daily Shaarli'|t} 22 {$localizedType} Shaarli
14 <a href="?do=dailyrss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a> 23 <a href="{$base_path}/daily-rss?{$type}"
24 title="{function="t('1 RSS entry per :type', '', 1, 'shaarli', [':type' => t($type)])"}"
25 >
26 <i class="fa fa-rss"></i>
27 </a>
15 </h2> 28 </h2>
16 29
17 <div id="plugin_zone_start_daily" class="plugin_zone"> 30 <div id="plugin_zone_start_daily" class="plugin_zone">
@@ -25,19 +38,19 @@
25 <div class="pure-g"> 38 <div class="pure-g">
26 <div class="pure-u-lg-1-3 pure-u-1 center"> 39 <div class="pure-u-lg-1-3 pure-u-1 center">
27 {if="$previousday"} 40 {if="$previousday"}
28 <a href="?do=daily&amp;day={$previousday}"> 41 <a href="{$base_path}/daily?{$type}={$previousday}">
29 <i class="fa fa-arrow-left"></i> 42 <i class="fa fa-arrow-left"></i>
30 {'Previous day'|t} 43 {function="t('Previous :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
31 </a> 44 </a>
32 {/if} 45 {/if}
33 </div> 46 </div>
34 <div class="daily-desc pure-u-lg-1-3 pure-u-1 center"> 47 <div class="daily-desc pure-u-lg-1-3 pure-u-1 center">
35 {'All links of one day in a single page.'|t} 48 {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}
36 </div> 49 </div>
37 <div class="pure-u-lg-1-3 pure-u-1 center"> 50 <div class="pure-u-lg-1-3 pure-u-1 center">
38 {if="$nextday"} 51 {if="$nextday"}
39 <a href="?do=daily&amp;day={$nextday}"> 52 <a href="{$base_path}/daily?{$type}={$nextday}">
40 {'Next day'|t} 53 {function="t('Next :type', '', 1, 'shaarli', [':type' => t($type)], true)"}
41 <i class="fa fa-arrow-right"></i> 54 <i class="fa fa-arrow-right"></i>
42 </a> 55 </a>
43 {/if} 56 {/if}
@@ -45,10 +58,7 @@
45 </div> 58 </div>
46 <div> 59 <div>
47 <h3 class="window-subtitle"> 60 <h3 class="window-subtitle">
48 {if="!empty($dayDesc)"} 61 {$dayDesc}
49 {$dayDesc} -
50 {/if}
51 {function="format_date($dayDate, false)"}
52 </h3> 62 </h3>
53 63
54 <div id="plugin_zone_about_daily" class="plugin_zone"> 64 <div id="plugin_zone_about_daily" class="plugin_zone">
@@ -69,14 +79,14 @@
69 {$link=$value} 79 {$link=$value}
70 <div class="daily-entry"> 80 <div class="daily-entry">
71 <div class="daily-entry-title center"> 81 <div class="daily-entry-title center">
72 <a href="?{$link.shorturl}" title="{'Permalink'|t}"> 82 <a href="{$base_path}/?{$link.shorturl}" title="{'Permalink'|t}">
73 <i class="fa fa-link"></i> 83 <i class="fa fa-link"></i>
74 </a> 84 </a>
75 <a href="{$link.real_url}">{$link.title}</a> 85 <a href="{$link.real_url}">{$link.title}</a>
76 </div> 86 </div>
77 {if="$thumbnails_enabled && !empty($link.thumbnail)"} 87 {if="$thumbnails_enabled && !empty($link.thumbnail)"}
78 <div class="daily-entry-thumbnail"> 88 <div class="daily-entry-thumbnail">
79 <img data-src="{$link.thumbnail}#" class="b-lazy" 89 <img data-src="{$root_path}/{$link.thumbnail}#" class="b-lazy"
80 src="" 90 src=""
81 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 91 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
82 </div> 92 </div>
@@ -85,7 +95,7 @@
85 {if="$link.tags"} 95 {if="$link.tags"}
86 <div class="daily-entry-tags center"> 96 <div class="daily-entry-tags center">
87 {loop="link.taglist"} 97 {loop="link.taglist"}
88 <span class="label label-tag" title="Add tag"> 98 <span class="label label-tag">
89 {$value} 99 {$value}
90 </span> 100 </span>
91 {/loop} 101 {/loop}
@@ -116,7 +126,7 @@
116 </div> 126 </div>
117</div> 127</div>
118{include="page.footer"} 128{include="page.footer"}
119<script src="js/thumbnails.min.js?v={$version_hash}"></script> 129<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
120</body> 130</body>
121</html> 131</html>
122 132
diff --git a/tpl/default/dailyrss.html b/tpl/default/dailyrss.html
index f589b06e..871a3ba7 100644
--- a/tpl/default/dailyrss.html
+++ b/tpl/default/dailyrss.html
@@ -1,16 +1,35 @@
1<item> 1<?xml version="1.0" encoding="UTF-8"?>
2 <title>{$title} - {function="strftime('%A %e %B %Y', $daydate)"}</title> 2<rss version="2.0">
3 <guid>{$absurl}</guid> 3 <channel>
4 <link>{$absurl}</link> 4 <title>{$localizedType} - {$title}</title>
5 <pubDate>{$rssdate}</pubDate> 5 <link>{$index_url}</link>
6 <description><![CDATA[ 6 <description>{function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}</description>
7 {loop="links"} 7 <language>{$language}</language>
8 <h3><a href="{$value.url}">{$value.title}</a></h3> 8 <copyright>{$index_url}</copyright>
9 <small>{if="!$hide_timestamps"}{function="strftime('%c', $value.timestamp)"} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> 9 <generator>Shaarli</generator>
10 {$value.url}</small><br> 10
11 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br> 11 {loop="$days"}
12 {if="$value.description"}{$value.formatedDescription}{/if} 12 <item>
13 <br><br><hr> 13 <title>{$value.date_human} - {$title}</title>
14 {/loop} 14 <guid>{$value.absolute_url}</guid>
15 ]]></description> 15 <link>{$value.absolute_url}</link>
16</item> 16 <pubDate>{$value.date_rss}</pubDate>
17 <description><![CDATA[
18 {loop="$value.links"}
19 <h3><a href="{$value.url}">{$value.title}</a></h3>
20 <small>
21 {if="!$hide_timestamps"}{$value.created|format_date} &#8212; {/if}
22 <a href="{$index_url}shaare/{$value.shorturl}">{'Permalink'|t}</a>
23 {if="$value.tags"} &#8212; {$value.tags}{/if}
24 <br>
25 {$value.url}
26 </small><br>
27 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
28 {if="$value.description"}{$value.description}{/if}
29 <br><hr>
30 {/loop}
31 ]]></description>
32 </item>
33 {/loop}
34 </channel>
35</rss><!-- Cached version of {$page_url} -->
diff --git a/tpl/default/editlink.batch.html b/tpl/default/editlink.batch.html
new file mode 100644
index 00000000..b1f8e5bd
--- /dev/null
+++ b/tpl/default/editlink.batch.html
@@ -0,0 +1,32 @@
1<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head>
4 {include="includes"}
5</head>
6<body>
7<div class="dark-layer">
8 <div class="screen-center">
9 <div><span class="progressbar-current"></span> / <span class="progressbar-max"></span></div>
10 <div class="progressbar">
11 <div></div>
12 </div>
13 </div>
14</div>
15
16{include="page.header"}
17
18<div class="center">
19 <input type="submit" name="save_edit_batch" class="pure-button-shaarli" value="{'Save all'|t}">
20</div>
21
22{loop="$links"}
23 {include="editlink"}
24{/loop}
25
26<div class="center">
27 <input type="submit" name="save_edit_batch" class="pure-button-shaarli" value="{'Save all'|t}">
28</div>
29
30{include="page.footer"}
31{if="$async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
32<script src="{$asset_path}/js/shaare_batch.min.js?v={$version_hash}#"></script>
diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html
index d16059a3..83e541fd 100644
--- a/tpl/default/editlink.html
+++ b/tpl/default/editlink.html
@@ -1,3 +1,4 @@
1{if="empty($batch_mode)"}
1<!DOCTYPE html> 2<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}> 3<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head> 4<head>
@@ -5,9 +6,19 @@
5</head> 6</head>
6<body> 7<body>
7 {include="page.header"} 8 {include="page.header"}
9{else}
10 {ignore}Lil hack: when included in a loop in batch mode, `$value` is assigned by RainTPL with template vars.{/ignore}
11 {function="extract($value) ? '' : ''"}
12{/if}
8 <div id="editlinkform" class="edit-link-container" class="pure-g"> 13 <div id="editlinkform" class="edit-link-container" class="pure-g">
9 <div class="pure-u-lg-1-5 pure-u-1-24"></div> 14 <div class="pure-u-lg-1-5 pure-u-1-24"></div>
10 <form method="post" name="linkform" class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"> 15 <form method="post"
16 name="linkform"
17 action="{$base_path}/admin/shaare"
18 class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"
19 >
20 {$asyncLoadClass=$link_is_new && $async_metadata && empty($link.title) ? 'loading-input' : ''}
21
11 <h2 class="window-title"> 22 <h2 class="window-title">
12 {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} 23 {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
13 </h2> 24 </h2>
@@ -24,26 +35,37 @@
24 <div> 35 <div>
25 <label for="lf_title">{'Title'|t}</label> 36 <label for="lf_title">{'Title'|t}</label>
26 </div> 37 </div>
27 <div> 38 <div class="{$asyncLoadClass}">
28 <input type="text" name="lf_title" id="lf_title" value="{$link.title}" class="lf_input autofocus"> 39 <input type="text" name="lf_title" id="lf_title" value="{$link.title}"
40 class="lf_input {if="!$async_metadata"}autofocus{/if}"
41 >
42 <div class="icon-container">
43 <i class="loader"></i>
44 </div>
29 </div> 45 </div>
30 <div> 46 <div>
31 <label for="lf_description">{'Description'|t}</label> 47 <label for="lf_description">{'Description'|t}</label>
32 </div> 48 </div>
33 <div> 49 <div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}">
34 <textarea name="lf_description" id="lf_description" class="autofocus">{$link.description}</textarea> 50 <textarea name="lf_description" id="lf_description" class="autofocus">{$link.description}</textarea>
51 <div class="icon-container">
52 <i class="loader"></i>
53 </div>
35 </div> 54 </div>
36 <div> 55 <div>
37 <label for="lf_tags">{'Tags'|t}</label> 56 <label for="lf_tags">{'Tags'|t}</label>
38 </div> 57 </div>
39 <div> 58 <div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}">
40 <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input autofocus" 59 <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input autofocus"
41 data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off" > 60 data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off" >
61 <div class="icon-container">
62 <i class="loader"></i>
63 </div>
42 </div> 64 </div>
43 65
44 <div> 66 <div>
45 <input type="checkbox" name="lf_private" id="lf_private" 67 <input type="checkbox" name="lf_private" id="lf_private"
46 {if="($link_is_new && $default_private_links || $link.private == true)"} 68 {if="$link.private === true"}
47 checked="checked" 69 checked="checked"
48 {/if}> 70 {/if}>
49 &nbsp;<label for="lf_private">{'Private'|t}</label> 71 &nbsp;<label for="lf_private">{'Private'|t}</label>
@@ -66,10 +88,17 @@
66 88
67 89
68 <div class="submit-buttons center"> 90 <div class="submit-buttons center">
91 {if="!empty($batch_mode)"}
92 <a href="#" class="button button-grey" name="cancel-batch-link"
93 title="{'Remove this bookmark from batch creation/modification.'}"
94 >
95 {'Cancel'|t}
96 </a>
97 {/if}
69 <input type="submit" name="save_edit" class="" id="button-save-edit" 98 <input type="submit" name="save_edit" class="" id="button-save-edit"
70 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}"> 99 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}">
71 {if="!$link_is_new"} 100 {if="!$link_is_new"}
72 <a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}" 101 <a href="{$base_path}/admin/shaare/delete?id={$link.id}&amp;token={$token}"
73 title="" name="delete_link" class="button button-red confirm-delete"> 102 title="" name="delete_link" class="button button-red confirm-delete">
74 {'Delete'|t} 103 {'Delete'|t}
75 </a> 104 </a>
@@ -77,11 +106,16 @@
77 </div> 106 </div>
78 107
79 <input type="hidden" name="token" value="{$token}"> 108 <input type="hidden" name="token" value="{$token}">
109 <input type="hidden" name="source" value="{$source}">
80 {if="$http_referer"} 110 {if="$http_referer"}
81 <input type="hidden" name="returnurl" value="{$http_referer}"> 111 <input type="hidden" name="returnurl" value="{$http_referer}">
82 {/if} 112 {/if}
83 </form> 113 </form>
84 </div> 114 </div>
115
116{if="empty($batch_mode)"}
85 {include="page.footer"} 117 {include="page.footer"}
118 {if="$link_is_new && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
86</body> 119</body>
87</html> 120</html>
121{/if}
diff --git a/tpl/default/error.html b/tpl/default/error.html
index ef1dfd73..c3e0c3c1 100644
--- a/tpl/default/error.html
+++ b/tpl/default/error.html
@@ -15,7 +15,7 @@
15 </pre> 15 </pre>
16 {/if} 16 {/if}
17 17
18 <img src="img/sad_star.png" alt=""> 18 <img src="{$asset_path}/img/sad_star.png#" alt="">
19</div> 19</div>
20{include="page.footer"} 20{include="page.footer"}
21</body> 21</body>
diff --git a/tpl/default/export.html b/tpl/default/export.html
index 99c01b11..c9c92943 100644
--- a/tpl/default/export.html
+++ b/tpl/default/export.html
@@ -6,14 +6,13 @@
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8 8
9<form method="GET" action="#" name="exportform" id="exportform"> 9<form method="POST" action="{$base_path}/admin/export" name="exportform" id="exportform">
10 <div class="pure-g"> 10 <div class="pure-g">
11 <div class="pure-u-lg-1-4 pure-u-1-24"></div> 11 <div class="pure-u-lg-1-4 pure-u-1-24"></div>
12 <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete"> 12 <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete">
13 <div> 13 <div>
14 <h2 class="window-title">{"Export Database"|t}</h2> 14 <h2 class="window-title">{"Export Database"|t}</h2>
15 </div> 15 </div>
16 <input type="hidden" name="do" value="export">
17 <input type="hidden" name="token" value="{$token}"> 16 <input type="hidden" name="token" value="{$token}">
18 17
19 <div class="pure-g"> 18 <div class="pure-g">
diff --git a/tpl/default/feed.atom.html b/tpl/default/feed.atom.html
index bcfa7012..dd58bd1e 100644
--- a/tpl/default/feed.atom.html
+++ b/tpl/default/feed.atom.html
@@ -6,6 +6,8 @@
6 <updated>{$last_update}</updated> 6 <updated>{$last_update}</updated>
7 {/if} 7 {/if}
8 <link rel="self" href="{$self_link}#" /> 8 <link rel="self" href="{$self_link}#" />
9 <link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
10 title="Shaarli search - {$shaarlititle}" />
9 {loop="$plugins_feed_header"} 11 {loop="$plugins_feed_header"}
10 {$value} 12 {$value}
11 {/loop} 13 {/loop}
diff --git a/tpl/default/feed.rss.html b/tpl/default/feed.rss.html
index 66d9a869..85cec7f3 100644
--- a/tpl/default/feed.rss.html
+++ b/tpl/default/feed.rss.html
@@ -7,7 +7,9 @@
7 <language>{$language}</language> 7 <language>{$language}</language>
8 <copyright>{$index_url}</copyright> 8 <copyright>{$index_url}</copyright>
9 <generator>Shaarli</generator> 9 <generator>Shaarli</generator>
10 <atom:link rel="self" href="{$self_link}" /> 10 <atom:link rel="self" href="{$self_link}" />
11 <atom:link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
12 title="Shaarli search - {$shaarlititle}" />
11 {loop="$plugins_feed_header"} 13 {loop="$plugins_feed_header"}
12 {$value} 14 {$value}
13 {/loop} 15 {/loop}
diff --git a/tpl/default/import.html b/tpl/default/import.html
index c41afcdb..156de71f 100644
--- a/tpl/default/import.html
+++ b/tpl/default/import.html
@@ -6,7 +6,7 @@
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8 8
9<form method="POST" action="?do=import" enctype="multipart/form-data" name="uploadform" id="uploadform"> 9<form method="POST" action="{$base_path}/admin/import" enctype="multipart/form-data" name="uploadform" id="uploadform">
10 <div class="pure-g"> 10 <div class="pure-g">
11 <div class="pure-u-lg-1-4 pure-u-1-24"></div> 11 <div class="pure-u-lg-1-4 pure-u-1-24"></div>
12 <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete"> 12 <div class="pure-u-lg-1-2 pure-u-22-24 page-form page-form-complete">
diff --git a/tpl/default/includes.html b/tpl/default/includes.html
index 3820a4f7..3e3fb664 100644
--- a/tpl/default/includes.html
+++ b/tpl/default/includes.html
@@ -3,21 +3,22 @@
3<meta name="format-detection" content="telephone=no" /> 3<meta name="format-detection" content="telephone=no" />
4<meta name="viewport" content="width=device-width, initial-scale=1"> 4<meta name="viewport" content="width=device-width, initial-scale=1">
5<meta name="referrer" content="same-origin"> 5<meta name="referrer" content="same-origin">
6<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" /> 6<link rel="alternate" type="application/atom+xml" href="{$feedurl}feed/atom?{$searchcrits}#" title="ATOM Feed" />
7<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" /> 7<link rel="alternate" type="application/rss+xml" href="{$feedurl}feed/rss?{$searchcrits}#" title="RSS Feed" />
8<link href="img/favicon.png" rel="shortcut icon" type="image/png" /> 8<link href="{$asset_path}/img/favicon.png#" rel="shortcut icon" type="image/png" />
9<link href="img/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" /> 9<link href="{$asset_path}/img/apple-touch-icon.png#" rel="apple-touch-icon" sizes="180x180" />
10<link type="text/css" rel="stylesheet" href="css/shaarli.min.css?v={$version_hash}" /> 10<link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css?v={$version_hash}#" />
11{if="$formatter==='markdown'"} 11{if="strpos($formatter, 'markdown') !== false"}
12 <link type="text/css" rel="stylesheet" href="css/markdown.min.css?v={$version_hash}" /> 12 <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" />
13{/if} 13{/if}
14{loop="$plugins_includes.css_files"} 14{loop="$plugins_includes.css_files"}
15 <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/> 15 <link type="text/css" rel="stylesheet" href="{$root_path}/{$value}?v={$version_hash}#"/>
16{/loop} 16{/loop}
17{if="is_file('data/user.css')"} 17{if="is_file('data/user.css')"}
18 <link type="text/css" rel="stylesheet" href="data/user.css#" /> 18 <link type="text/css" rel="stylesheet" href="{$root_path}/data/user.css#" />
19{/if} 19{/if}
20<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/> 20<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
21 title="Shaarli search - {$shaarlititle}" />
21{if="! empty($links) && count($links) === 1"} 22{if="! empty($links) && count($links) === 1"}
22 {$link=reset($links)} 23 {$link=reset($links)}
23 <meta property="og:title" content="{$link.title}" /> 24 <meta property="og:title" content="{$link.title}" />
diff --git a/tpl/default/install.html b/tpl/default/install.html
index c6f501f0..4f98d49d 100644
--- a/tpl/default/install.html
+++ b/tpl/default/install.html
@@ -10,7 +10,7 @@
10{$ratioLabelMobile='7-8'} 10{$ratioLabelMobile='7-8'}
11{$ratioInputMobile='1-8'} 11{$ratioInputMobile='1-8'}
12 12
13<form method="POST" action="#" name="installform" id="installform"> 13<form method="POST" action="{$base_path}/install" name="installform" id="installform">
14<div class="pure-g"> 14<div class="pure-g">
15 <div class="pure-u-lg-1-6 pure-u-1-24"></div> 15 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
16 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete"> 16 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">
@@ -163,6 +163,16 @@
163 </div> 163 </div>
164</div> 164</div>
165</form> 165</form>
166
167<div class="pure-g">
168 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
169 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">
170 <h2 class="window-title">{'Server requirements'|t}</h2>
171
172 {include="server.requirements"}
173 </div>
174</div>
175
166{include="page.footer"} 176{include="page.footer"}
167</body> 177</body>
168</html> 178</html>
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index ffc236c7..e1115d49 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -94,7 +94,9 @@
94 {'tagged'|t} 94 {'tagged'|t}
95 {loop="$exploded_tags"} 95 {loop="$exploded_tags"}
96 <span class="label label-tag" title="{'Remove tag'|t}"> 96 <span class="label label-tag" title="{'Remove tag'|t}">
97 <a href="?removetag={function="urlencode($value)"}" aria-label="{'Remove tag'|t}">{$value}<span class="remove"><i class="fa fa-times" aria-hidden="true"></i></span></a> 97 <a href="{$base_path}/remove-tag/{function="$search_tags_url.$key1"}" aria-label="{'Remove tag'|t}">
98 {$value}<span class="remove"><i class="fa fa-times" aria-hidden="true"></i></span>
99 </a>
98 </span> 100 </span>
99 {/loop} 101 {/loop}
100 {/if} 102 {/if}
@@ -127,18 +129,23 @@
127 {$strAddTag=t('Add tag')} 129 {$strAddTag=t('Add tag')}
128 {$strToggleSticky=t('Toggle sticky')} 130 {$strToggleSticky=t('Toggle sticky')}
129 {$strSticky=t('Sticky')} 131 {$strSticky=t('Sticky')}
132 {$strShaarePrivate=t('Share a private link')}
130 {ignore}End of translations{/ignore} 133 {ignore}End of translations{/ignore}
131 {loop="links"} 134 {loop="links"}
132 <div class="anchor" id="{$value.shorturl}"></div> 135 <div class="anchor" id="{$value.shorturl}"></div>
133 136
134 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> 137 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
135 <div class="linklist-item-title"> 138 <div class="linklist-item-title">
136 {if="$thumbnails_enabled && !empty($value.thumbnail)"} 139 {if="$thumbnails_enabled && $value.thumbnail !== false"}
137 <div class="linklist-item-thumbnail" style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;"> 140 <div
141 class="linklist-item-thumbnail {if="$value.thumbnail === null"}hidden{/if}"
142 style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;"
143 {if="$value.thumbnail === null"}data-async-thumbnail="1"{/if}
144 >
138 <div class="thumbnail"> 145 <div class="thumbnail">
139 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} 146 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
140 <a href="{$value.real_url}" aria-hidden="true" tabindex="-1"> 147 <a href="{$value.real_url}" aria-hidden="true" tabindex="-1">
141 <img data-src="{$value.thumbnail}#" class="b-lazy" 148 <img data-src="{$root_path}/{$value.thumbnail}#" class="b-lazy"
142 src="" 149 src=""
143 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 150 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
144 </a> 151 </a>
@@ -156,14 +163,14 @@
156 </div> 163 </div>
157 164
158 <h2> 165 <h2>
159 <a href="{$value.real_url}"> 166 <a href="{$value.real_url}" class="linklist-real-url">
160 {if="strpos($value.url, $value.shorturl) === false"} 167 {if="strpos($value.url, $value.shorturl) === false"}
161 <i class="fa fa-external-link" aria-hidden="true"></i> 168 <i class="fa fa-external-link" aria-hidden="true"></i>
162 {else} 169 {else}
163 <i class="fa fa-sticky-note" aria-hidden="true"></i> 170 <i class="fa fa-sticky-note" aria-hidden="true"></i>
164 {/if} 171 {/if}
165 172
166 <span class="linklist-link">{$value.title}</span> 173 <span class="linklist-link">{$value.title_html}</span>
167 </a> 174 </a>
168 </h2> 175 </h2>
169 </div> 176 </div>
@@ -181,7 +188,7 @@
181 {$tag_counter=count($value.taglist)} 188 {$tag_counter=count($value.taglist)}
182 {loop="value.taglist"} 189 {loop="value.taglist"}
183 <span class="label label-tag" title="{$strAddTag}"> 190 <span class="label label-tag" title="{$strAddTag}">
184 <a href="?addtag={$value|urlencode}">{$value}</a> 191 <a href="{$base_path}/add-tag/{$value1.taglist_urlencoded.$key2}">{$value1.taglist_html.$key2}</a>
185 </span> 192 </span>
186 {if="$tag_counter - 1 != $counter"}&middot;{/if} 193 {if="$tag_counter - 1 != $counter"}&middot;{/if}
187 {/loop} 194 {/loop}
@@ -196,16 +203,16 @@
196 <input type="checkbox" class="link-checkbox" value="{$value.id}"> 203 <input type="checkbox" class="link-checkbox" value="{$value.id}">
197 </span> 204 </span>
198 <span class="linklist-item-infos-controls-item ctrl-edit"> 205 <span class="linklist-item-infos-controls-item ctrl-edit">
199 <a href="?edit_link={$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a> 206 <a href="{$base_path}/admin/shaare/{$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a>
200 </span> 207 </span>
201 <span class="linklist-item-infos-controls-item ctrl-delete"> 208 <span class="linklist-item-infos-controls-item ctrl-delete">
202 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" aria-label="{$strDelete}" 209 <a href="{$base_path}/admin/shaare/delete?id={$value.id}&amp;token={$token}" aria-label="{$strDelete}"
203 title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete"> 210 title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
204 <i class="fa fa-trash" aria-hidden="true"></i> 211 <i class="fa fa-trash" aria-hidden="true"></i>
205 </a> 212 </a>
206 </span> 213 </span>
207 <span class="linklist-item-infos-controls-item ctrl-pin"> 214 <span class="linklist-item-infos-controls-item ctrl-pin">
208 <a href="?do=pin&amp;id={$value.id}&amp;token={$token}" 215 <a href="{$base_path}/admin/shaare/{$value.id}/pin?token={$token}"
209 title="{$strToggleSticky}" aria-label="{$strToggleSticky}" class="pin-link {if="$value.sticky"}pinned-link{/if} pure-u-0 pure-u-lg-visible"> 216 title="{$strToggleSticky}" aria-label="{$strToggleSticky}" class="pin-link {if="$value.sticky"}pinned-link{/if} pure-u-0 pure-u-lg-visible">
210 <i class="fa fa-thumb-tack" aria-hidden="true"></i> 217 <i class="fa fa-thumb-tack" aria-hidden="true"></i>
211 </a> 218 </a>
@@ -222,7 +229,7 @@
222 </div> 229 </div>
223 {/if} 230 {/if}
224 {/if} 231 {/if}
225 <a href="?{$value.shorturl}" title="{$strPermalink}"> 232 <a href="{$base_path}/shaare/{$value.shorturl}" title="{$strPermalink}">
226 {if="!$hide_timestamps || $is_logged_in"} 233 {if="!$hide_timestamps || $is_logged_in"}
227 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink} 234 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
228 <span class="linkdate" title="{$updated}"> 235 <span class="linkdate" title="{$updated}">
@@ -235,6 +242,12 @@
235 {$strPermalinkLc} 242 {$strPermalinkLc}
236 </a> 243 </a>
237 244
245 {if="$is_logged_in && $value.private"}
246 <a href="{$base_path}/admin/shaare/private/{$value.shorturl}?token={$token}" title="{$strShaarePrivate}">
247 <i class="fa fa-share-alt"></i>
248 </a>
249 {/if}
250
238 <div class="pure-u-0 pure-u-lg-visible"> 251 <div class="pure-u-0 pure-u-lg-visible">
239 {if="isset($value.link_plugin)"} 252 {if="isset($value.link_plugin)"}
240 &middot; 253 &middot;
@@ -249,7 +262,7 @@
249 {ignore}do not add space or line break between these div - Firefox issue{/ignore} 262 {ignore}do not add space or line break between these div - Firefox issue{/ignore}
250 class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1"> 263 class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1">
251 <a href="{$value.real_url}" aria-label="{$value.title}" title="{$value.title}"> 264 <a href="{$value.real_url}" aria-label="{$value.title}" title="{$value.title}">
252 <i class="fa fa-link" aria-hidden="true"></i> {$value.url} 265 <i class="fa fa-link" aria-hidden="true"></i> {$value.url_html}
253 </a> 266 </a>
254 <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible"> 267 <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible">
255 <a href="#" aria-label="{$strFold}" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up" aria-hidden="true"></i></a> 268 <a href="#" aria-label="{$strFold}" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up" aria-hidden="true"></i></a>
@@ -265,12 +278,22 @@
265 {/if} 278 {/if}
266 {if="$is_logged_in"} 279 {if="$is_logged_in"}
267 &middot; 280 &middot;
268 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" aria-label="{$strDelete}" 281 <a href="{$base_path}/admin/shaare/delete?id={$value.id}&amp;token={$token}" aria-label="{$strDelete}"
269 title="{$strDelete}" class="delete-link confirm-delete"> 282 title="{$strDelete}" class="delete-link confirm-delete">
270 <i class="fa fa-trash" aria-hidden="true"></i> 283 <i class="fa fa-trash" aria-hidden="true"></i>
271 </a> 284 </a>
272 &middot; 285 &middot;
273 <a href="?edit_link={$value.id}" aria-label="{$strEdit}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i></a> 286 <a href="{$base_path}/admin/shaare/{$value.id}" aria-label="{$strEdit}" title="{$strEdit}">
287 <i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i>
288 </a>
289 &middot;
290 <a href="{$base_path}/admin/shaare/{$value.id}/pin?token={$token}"
291 aria-label="{$strToggleSticky}"
292 title="{$strToggleSticky}"
293 class="pin-link {if="$value.sticky"}pinned-link{/if}"
294 >
295 <i class="fa fa-thumb-tack" aria-hidden="true"></i>
296 </a>
274 {/if} 297 {/if}
275 </div> 298 </div>
276 </div> 299 </div>
@@ -295,6 +318,7 @@
295</div> 318</div>
296 319
297{include="page.footer"} 320{include="page.footer"}
298<script src="js/thumbnails.min.js?v={$version_hash}"></script> 321<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
322{if="$is_logged_in && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
299</body> 323</body>
300</html> 324</html>
diff --git a/tpl/default/linklist.paging.html b/tpl/default/linklist.paging.html
index 68947f92..aa637868 100644
--- a/tpl/default/linklist.paging.html
+++ b/tpl/default/linklist.paging.html
@@ -1,27 +1,29 @@
1<div class="linklist-paging"> 1<div class="linklist-paging">
2 <div class="paging pure-g"> 2 <div class="paging pure-g">
3 <div class="linklist-filters pure-u-1-3"> 3 <div class="linklist-filters pure-u-1-3">
4 {if="$is_logged_in or !empty($action_plugin)"} 4 <span class="linklist-filters-text pure-u-0 pure-u-lg-visible">
5 <span class="linklist-filters-text pure-u-0 pure-u-lg-visible"> 5 {'Filters'|t}
6 {'Filters'|t} 6 </span>
7 </span> 7 {if="$is_logged_in"}
8 {if="$is_logged_in"} 8 <a href="{$base_path}/admin/visibility/private" aria-label="{'Only display private links'|t}" title="{'Only display private links'|t}"
9 <a href="?visibility=private" aria-label="{'Only display private links'|t}" title="{'Only display private links'|t}" 9 class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}"
10 class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}" 10 ><i class="fa fa-user-secret" aria-hidden="true"></i></a>
11 ><i class="fa fa-user-secret" aria-hidden="true"></i></a> 11 <a href="{$base_path}/admin/visibility/public" aria-label="{'Only display public links'|t}" title="{'Only display public links'|t}"
12 <a href="?visibility=public" aria-label="{'Only display public links'|t}" title="{'Only display public links'|t}" 12 class="{if="$visibility==='public'"}filter-on{else}filter-off{/if}"
13 class="{if="$visibility==='public'"}filter-on{else}filter-off{/if}" 13 ><i class="fa fa-globe" aria-hidden="true"></i></a>
14 ><i class="fa fa-globe" aria-hidden="true"></i></a> 14 {/if}
15 {/if} 15 <a href="{$base_path}/untagged-only" aria-label="{'Filter untagged links'|t}" title="{'Filter untagged links'|t}"
16 <a href="?untaggedonly" aria-label="{'Filter untagged links'|t}" title="{'Filter untagged links'|t}" 16 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
17 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if} 17 ><i class="fa fa-tag" aria-hidden="true"></i></a>
18 ><i class="fa fa-tag" aria-hidden="true"></i></a> 18 {if="$is_logged_in"}
19 <a href="#" aria-label="{'Select all'|t}" title="{'Select all'|t}" 19 <a href="#" aria-label="{'Select all'|t}" title="{'Select all'|t}"
20 class="filter-off select-all-button pure-u-0 pure-u-lg-visible" 20 class="filter-off select-all-button pure-u-0 pure-u-lg-visible"
21 ><i class="fa fa-check-square-o" aria-hidden="true"></i></a> 21 ><i class="fa fa-check-square-o" aria-hidden="true"></i></a>
22 <a href="#" class="filter-off fold-all pure-u-lg-0" aria-label="{'Fold all'|t}" title="{'Fold all'|t}"> 22 {/if}
23 <i class="fa fa-chevron-up" aria-hidden="true"></i> 23 <a href="#" class="filter-off fold-all pure-u-lg-0" aria-label="{'Fold all'|t}" title="{'Fold all'|t}">
24 </a> 24 <i class="fa fa-chevron-up" aria-hidden="true"></i>
25 </a>
26 {if="!empty($action_plugin)"}
25 {loop="$action_plugin"} 27 {loop="$action_plugin"}
26 {$value.attr.class=isset($value.attr.class) ? $value.attr.class : ''} 28 {$value.attr.class=isset($value.attr.class) ? $value.attr.class : ''}
27 {$value.attr.class=!empty($value.on) ? $value.attr.class .' filter-on' : $value.attr.class .' filter-off'} 29 {$value.attr.class=!empty($value.on) ? $value.attr.class .' filter-on' : $value.attr.class .' filter-off'}
@@ -53,11 +55,16 @@
53 55
54 <div class="linksperpage pure-u-1-3"> 56 <div class="linksperpage pure-u-1-3">
55 <div class="pure-u-0 pure-u-lg-visible">{'Links per page'|t}</div> 57 <div class="pure-u-0 pure-u-lg-visible">{'Links per page'|t}</div>
56 <a href="?linksperpage=20">20</a> 58 <a href="{$base_path}/links-per-page?nb=20"
57 <a href="?linksperpage=50">50</a> 59 {if="$links_per_page == 20"}class="selected"{/if}>20</a>
58 <a href="?linksperpage=100">100</a> 60 <a href="{$base_path}/links-per-page?nb=50"
59 <form method="GET" class="pure-u-0 pure-u-lg-visible"> 61 {if="$links_per_page == 50"}class="selected"{/if}>50</a>
60 <input type="text" name="linksperpage" placeholder="133"> 62 <a href="{$base_path}/links-per-page?nb=100"
63 {if="$links_per_page == 100"}class="selected"{/if}>100</a>
64 <form method="GET" class="pure-u-0 pure-u-lg-visible" action="{$base_path}/links-per-page">
65 <input type="text" name="nb" placeholder="133"
66 {if="$links_per_page != 20 && $links_per_page != 50 && $links_per_page != 100"}
67 value="{$links_per_page}"{/if}>
61 </form> 68 </form>
62 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" aria-label="{'Fold all'|t}" title="{'Fold all'|t}"> 69 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" aria-label="{'Fold all'|t}" title="{'Fold all'|t}">
63 <i class="fa fa-chevron-up" aria-hidden="true"></i> 70 <i class="fa fa-chevron-up" aria-hidden="true"></i>
diff --git a/tpl/default/opensearch.html b/tpl/default/opensearch.html
index 3fcc30b7..1c7f279b 100644
--- a/tpl/default/opensearch.html
+++ b/tpl/default/opensearch.html
@@ -3,8 +3,8 @@
3 <ShortName>Shaarli search - {$pagetitle}</ShortName> 3 <ShortName>Shaarli search - {$pagetitle}</ShortName>
4 <Description>Shaarli search - {$pagetitle}</Description> 4 <Description>Shaarli search - {$pagetitle}</Description>
5 <Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" /> 5 <Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" />
6 <Url type="application/atom+xml" template="{$serverurl}?do=atom&amp;searchterm={searchTerms}"/> 6 <Url type="application/atom+xml" template="{$serverurl}feed/atom?searchterm={searchTerms}"/>
7 <Url type="application/rss+xml" template="{$serverurl}?do=rss&amp;searchterm={searchTerms}"/> 7 <Url type="application/rss+xml" template="{$serverurl}feed/rss?searchterm={searchTerms}"/>
8 <InputEncoding>UTF-8</InputEncoding> 8 <InputEncoding>UTF-8</InputEncoding>
9 <Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer> 9 <Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer>
10 <Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE 10 <Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html
index 0899826b..c153def0 100644
--- a/tpl/default/page.footer.html
+++ b/tpl/default/page.footer.html
@@ -10,7 +10,7 @@
10 {/if} 10 {/if}
11 &middot; 11 &middot;
12 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} &middot; 12 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} &middot;
13 <a href="doc/html/index.html" rel="nofollow">{'Documentation'|t}</a> 13 <a href="{$root_path}/doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
14 {loop="$plugins_footer.text"} 14 {loop="$plugins_footer.text"}
15 {$value} 15 {$value}
16 {/loop} 16 {/loop}
@@ -25,7 +25,7 @@
25{/loop} 25{/loop}
26 26
27{loop="$plugins_footer.js_files"} 27{loop="$plugins_footer.js_files"}
28 <script src="{$value}#"></script> 28 <script src="{$root_path}/{$value}#"></script>
29{/loop} 29{/loop}
30 30
31<div id="js-translations" class="hidden"> 31<div id="js-translations" class="hidden">
@@ -33,10 +33,11 @@
33 <span id="translation-fold-all">{'Fold all'|t}</span> 33 <span id="translation-fold-all">{'Fold all'|t}</span>
34 <span id="translation-expand">{'Expand'|t}</span> 34 <span id="translation-expand">{'Expand'|t}</span>
35 <span id="translation-expand-all">{'Expand all'|t}</span> 35 <span id="translation-expand-all">{'Expand all'|t}</span>
36 <span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span> 36 <span id="translation-delete-link">{'Are you sure you want to delete this tag?'|t}</span>
37 <span id="translation-shaarli-desc"> 37 <span id="translation-shaarli-desc">
38 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} 38 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t}
39 </span> 39 </span>
40</div> 40</div>
41 41
42<script src="js/shaarli.min.js?v={$version_hash}"></script> 42<input type="hidden" name="js_base_path" value="{$base_path}" />
43<script src="{$asset_path}/js/shaarli.min.js?v={$version_hash}#"></script>
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html
index 82f8ebf1..a71464c7 100644
--- a/tpl/default/page.header.html
+++ b/tpl/default/page.header.html
@@ -21,24 +21,24 @@
21 </li> 21 </li>
22 {if="$is_logged_in || $openshaarli"} 22 {if="$is_logged_in || $openshaarli"}
23 <li class="pure-menu-item"> 23 <li class="pure-menu-item">
24 <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare"> 24 <a href="{$base_path}/admin/add-shaare" class="pure-menu-link" id="shaarli-menu-shaare">
25 <i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t} 25 <i class="fa fa-plus" aria-hidden="true"></i> {'Shaare'|t}
26 </a> 26 </a>
27 </li> 27 </li>
28 <li class="pure-menu-item" id="shaarli-menu-tools"> 28 <li class="pure-menu-item" id="shaarli-menu-tools">
29 <a href="?do=tools" class="pure-menu-link">{'Tools'|t}</a> 29 <a href="{$base_path}/admin/tools" class="pure-menu-link">{'Tools'|t}</a>
30 </li> 30 </li>
31 {/if} 31 {/if}
32 <li class="pure-menu-item" id="shaarli-menu-tags"> 32 <li class="pure-menu-item" id="shaarli-menu-tags">
33 <a href="?do=tagcloud" class="pure-menu-link">{'Tag cloud'|t}</a> 33 <a href="{$base_path}/tags/cloud" class="pure-menu-link">{'Tag cloud'|t}</a>
34 </li> 34 </li>
35 {if="$thumbnails_enabled"} 35 {if="$thumbnails_enabled"}
36 <li class="pure-menu-item" id="shaarli-menu-picwall"> 36 <li class="pure-menu-item" id="shaarli-menu-picwall">
37 <a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a> 37 <a href="{$base_path}/picture-wall?{function="ltrim($searchcrits, '&')"}" class="pure-menu-link">{'Picture wall'|t}</a>
38 </li> 38 </li>
39 {/if} 39 {/if}
40 <li class="pure-menu-item" id="shaarli-menu-daily"> 40 <li class="pure-menu-item" id="shaarli-menu-daily">
41 <a href="?do=daily" class="pure-menu-link">{'Daily'|t}</a> 41 <a href="{$base_path}/daily" class="pure-menu-link">{'Daily'|t}</a>
42 </li> 42 </li>
43 {loop="$plugins_header.buttons_toolbar"} 43 {loop="$plugins_header.buttons_toolbar"}
44 <li class="pure-menu-item shaarli-menu-plugin"> 44 <li class="pure-menu-item shaarli-menu-plugin">
@@ -52,15 +52,15 @@
52 </li> 52 </li>
53 {/loop} 53 {/loop}
54 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss"> 54 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss">
55 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a> 55 <a href="{$base_path}/feed/{$feed_type}?{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
56 </li> 56 </li>
57 {if="$is_logged_in"} 57 {if="$is_logged_in"}
58 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout"> 58 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout">
59 <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a> 59 <a href="{$base_path}/admin/logout" class="pure-menu-link">{'Logout'|t}</a>
60 </li> 60 </li>
61 {else} 61 {else}
62 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login"> 62 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login">
63 <a href="/login" class="pure-menu-link">{'Login'|t}</a> 63 <a href="{$base_path}/login" class="pure-menu-link">{'Login'|t}</a>
64 </li> 64 </li>
65 {/if} 65 {/if}
66 </ul> 66 </ul>
@@ -74,13 +74,13 @@
74 </a> 74 </a>
75 </li> 75 </li>
76 <li class="pure-menu-item" id="shaarli-menu-desktop-rss"> 76 <li class="pure-menu-item" id="shaarli-menu-desktop-rss">
77 <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link" title="{'RSS Feed'|t}" aria-label="{'RSS Feed'|t}"> 77 <a href="{$base_path}/feed/{$feed_type}?{$searchcrits}" class="pure-menu-link" title="{'RSS Feed'|t}" aria-label="{'RSS Feed'|t}">
78 <i class="fa fa-rss" aria-hidden="true"></i> 78 <i class="fa fa-rss" aria-hidden="true"></i>
79 </a> 79 </a>
80 </li> 80 </li>
81 {if="!$is_logged_in"} 81 {if="!$is_logged_in"}
82 <li class="pure-menu-item" id="shaarli-menu-desktop-login"> 82 <li class="pure-menu-item" id="shaarli-menu-desktop-login">
83 <a href="/login" class="pure-menu-link" 83 <a href="{$base_path}/login" class="pure-menu-link"
84 data-open-id="header-login-form" 84 data-open-id="header-login-form"
85 id="login-button" aria-label="{'Login'|t}" title="{'Login'|t}"> 85 id="login-button" aria-label="{'Login'|t}" title="{'Login'|t}">
86 <i class="fa fa-user" aria-hidden="true"></i> 86 <i class="fa fa-user" aria-hidden="true"></i>
@@ -88,7 +88,7 @@
88 </li> 88 </li>
89 {else} 89 {else}
90 <li class="pure-menu-item" id="shaarli-menu-desktop-logout"> 90 <li class="pure-menu-item" id="shaarli-menu-desktop-logout">
91 <a href="?do=logout" class="pure-menu-link" aria-label="{'Logout'|t}" title="{'Logout'|t}"> 91 <a href="{$base_path}/admin/logout" class="pure-menu-link" aria-label="{'Logout'|t}" title="{'Logout'|t}">
92 <i class="fa fa-sign-out" aria-hidden="true"></i> 92 <i class="fa fa-sign-out" aria-hidden="true"></i>
93 </a> 93 </a>
94 </li> 94 </li>
@@ -101,7 +101,7 @@
101 101
102<main id="content" class="container" role="main"> 102<main id="content" class="container" role="main">
103 <div id="search" class="subheader-form searchform-block header-search"> 103 <div id="search" class="subheader-form searchform-block header-search">
104 <form method="GET" class="pure-form searchform" name="searchform"> 104 <form method="GET" class="pure-form searchform" name="searchform" action="{$base_path}/">
105 <input type="text" id="searchform_value" name="searchterm" aria-label="{'Search text'|t}" placeholder="{'Search text'|t}" 105 <input type="text" id="searchform_value" name="searchterm" aria-label="{'Search text'|t}" placeholder="{'Search text'|t}"
106 {if="!empty($search_term)"} 106 {if="!empty($search_term)"}
107 value="{$search_term}" 107 value="{$search_term}"
@@ -184,8 +184,22 @@
184 </div> 184 </div>
185{/if} 185{/if}
186 186
187{if="!empty($global_warnings) && $is_logged_in"} 187{if="!empty($global_errors)"}
188 <div class="pure-g pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert"> 188 <div class="pure-g header-alert-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
189 <div class="pure-u-2-24"></div>
190 <div class="pure-u-20-24">
191 {loop="$global_errors"}
192 <p>{$value}</p>
193 {/loop}
194 </div>
195 <div class="pure-u-2-24">
196 <i class="fa fa-times pure-alert-close"></i>
197 </div>
198 </div>
199{/if}
200
201{if="!empty($global_warnings)"}
202 <div class="pure-g header-alert-message pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
189 <div class="pure-u-2-24"></div> 203 <div class="pure-u-2-24"></div>
190 <div class="pure-u-20-24"> 204 <div class="pure-u-20-24">
191 {loop="global_warnings"} 205 {loop="global_warnings"}
@@ -198,4 +212,18 @@
198 </div> 212 </div>
199{/if} 213{/if}
200 214
215{if="!empty($global_successes)"}
216 <div class="pure-g header-alert-message new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert">
217 <div class="pure-u-2-24"></div>
218 <div class="pure-u-20-24">
219 {loop="$global_successes"}
220 <p>{$value}</p>
221 {/loop}
222 </div>
223 <div class="pure-u-2-24">
224 <i class="fa fa-times pure-alert-close"></i>
225 </div>
226 </div>
227{/if}
228
201 <div class="clear"></div> 229 <div class="clear"></div>
diff --git a/tpl/default/picwall.html b/tpl/default/picwall.html
index 73359949..ac613b35 100644
--- a/tpl/default/picwall.html
+++ b/tpl/default/picwall.html
@@ -5,61 +5,55 @@
5</head> 5</head>
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8{if="!$thumbnails_enabled"} 8
9<div class="pure-g pure-alert pure-alert-warning page-single-alert"> 9{if="count($linksToDisplay)===0 && $is_logged_in"}
10 <div class="pure-u-1 center"> 10 <div class="pure-g pure-alert pure-alert-warning page-single-alert">
11 {'Picture wall unavailable (thumbnails are disabled).'|t} 11 <div class="pure-u-1 center">
12 </div> 12 {'There is no cached thumbnail.'|t}
13</div> 13 <a href="{$base_path}/admin/thumbnails">{'Try to synchronize them.'|t}</a>
14{else}
15 {if="count($linksToDisplay)===0 && $is_logged_in"}
16 <div class="pure-g pure-alert pure-alert-warning page-single-alert">
17 <div class="pure-u-1 center">
18 {'There is no cached thumbnail. Try to <a href="?do=thumbs_update">synchronize them</a>.'|t}
19 </div>
20 </div> 14 </div>
21 {/if} 15 </div>
16{/if}
22 17
23 <div class="pure-g"> 18<div class="pure-g">
24 <div class="pure-u-lg-1-6 pure-u-1-24"></div> 19 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
25 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor"> 20 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
26 {$countPics=count($linksToDisplay)} 21 {$countPics=count($linksToDisplay)}
27 <h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2> 22 <h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
28 23
29 <div id="plugin_zone_start_picwall" class="plugin_zone"> 24 <div id="plugin_zone_start_picwall" class="plugin_zone">
30 {loop="$plugin_start_zone"} 25 {loop="$plugin_start_zone"}
31 {$value} 26 {$value}
32 {/loop} 27 {/loop}
33 </div> 28 </div>
34 29
35 <div id="picwall-container" class="picwall-container" role="list"> 30 <div id="picwall-container" class="picwall-container" role="list">
36 {loop="$linksToDisplay"} 31 {loop="$linksToDisplay"}
37 <div class="picwall-pictureframe" role="listitem"> 32 <div class="picwall-pictureframe" role="listitem">
38 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} 33 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
39 <img data-src="{$value.thumbnail}#" class="b-lazy" 34 <img data-src="{$root_path}/{$value.thumbnail}#" class="b-lazy"
40 src="" 35 src=""
41 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 36 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
42 <a href="{$value.real_url}"><span class="info">{$value.title}</span></a> 37 <a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
43 {loop="$value.picwall_plugin"} 38 {loop="$value.picwall_plugin"}
44 {$value} 39 {$value}
45 {/loop} 40 {/loop}
46 </div> 41 </div>
47 {/loop} 42 {/loop}
48 <div class="clear"></div> 43 <div class="clear"></div>
49 </div> 44 </div>
50 45
51 <div id="plugin_zone_end_picwall" class="plugin_zone"> 46 <div id="plugin_zone_end_picwall" class="plugin_zone">
52 {loop="$plugin_end_zone"} 47 {loop="$plugin_end_zone"}
53 {$value} 48 {$value}
54 {/loop} 49 {/loop}
55 </div>
56 </div> 50 </div>
57 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
58 </div> 51 </div>
59{/if} 52 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
53</div>
60 54
61{include="page.footer"} 55{include="page.footer"}
62<script src="js/thumbnails.min.js?v={$version_hash}"></script> 56<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
63</body> 57</body>
64</html> 58</html>
65 59
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html
index 4bfaa934..5c073da6 100644
--- a/tpl/default/pluginsadmin.html
+++ b/tpl/default/pluginsadmin.html
@@ -16,7 +16,7 @@
16 <div class="clear"></div> 16 <div class="clear"></div>
17</noscript> 17</noscript>
18 18
19<form method="POST" action="?do=save_pluginadmin" name="pluginform" id="pluginform" class="pluginform-container"> 19<form method="POST" action="{$base_path}/admin/plugins" name="pluginform" id="pluginform" class="pluginform-container">
20 <div class="pure-g"> 20 <div class="pure-g">
21 <div class="pure-u-lg-1-8 pure-u-1-24"></div> 21 <div class="pure-u-lg-1-8 pure-u-1-24"></div>
22 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete"> 22 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-complete">
@@ -117,7 +117,7 @@
117 117
118 <div class="center more"> 118 <div class="center more">
119 {"More plugins available"|t} 119 {"More plugins available"|t}
120 <a href="doc/html/Community-&-Related-software/#third-party-plugins">{"in the documentation"|t}</a>. 120 <a href="{$root_path}/doc/html/Community-&-Related-software/#third-party-plugins">{"in the documentation"|t}</a>.
121 </div> 121 </div>
122 <div class="center"> 122 <div class="center">
123 <input type="submit" value="{'Save'|t}" name="save"> 123 <input type="submit" value="{'Save'|t}" name="save">
@@ -127,7 +127,7 @@
127 <input type="hidden" name="token" value="{$token}"> 127 <input type="hidden" name="token" value="{$token}">
128</form> 128</form>
129 129
130<form action="?do=save_pluginadmin" method="POST"> 130<form action="{$base_path}/admin/plugins" method="POST">
131 <div class="pure-g"> 131 <div class="pure-g">
132 <div class="pure-u-lg-1-8 pure-u-1-24"></div> 132 <div class="pure-u-lg-1-8 pure-u-1-24"></div>
133 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-light"> 133 <div class="pure-u-lg-3-4 pure-u-22-24 page-form page-form-light">
@@ -173,10 +173,11 @@
173 </section> 173 </section>
174 </div> 174 </div>
175 </div> 175 </div>
176 <input type="hidden" name="token" value="{$token}">
176</form> 177</form>
177 178
178{include="page.footer"} 179{include="page.footer"}
179<script src="js/pluginsadmin.min.js?v={$version_hash}"></script> 180<script src="{$asset_path}/js/pluginsadmin.min.js?v={$version_hash}#"></script>
180 181
181</body> 182</body>
182</html> 183</html>
diff --git a/tpl/default/server.html b/tpl/default/server.html
new file mode 100644
index 00000000..de1c8b53
--- /dev/null
+++ b/tpl/default/server.html
@@ -0,0 +1,129 @@
1<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head>
4 {include="includes"}
5</head>
6<body>
7{include="page.header"}
8
9<div class="pure-g">
10 <div class="pure-u-lg-1-4 pure-u-1-24"></div>
11 <div class="pure-u-lg-1-2 pure-u-22-24 page-form server-tables-page">
12 <h2 class="window-title">{'Server administration'|t}</h2>
13
14 <h3 class="window-subtitle">{'General'|t}</h3>
15
16 <div class="pure-g server-row">
17 <div class="pure-u-lg-1-2 pure-u-1 server-label">
18 <p>{'Index URL'|t}</p>
19 </div>
20 <div class="pure-u-lg-1-2 pure-u-1">
21 <p><a href="{$index_url}" title="{$pagetitle}">{$index_url}</a></p>
22 </div>
23 </div>
24 <div class="pure-g server-row">
25 <div class="pure-u-lg-1-2 pure-u-1 server-label">
26 <p>{'Base path'|t}</p>
27 </div>
28 <div class="pure-u-lg-1-2 pure-u-1">
29 <p>{$base_path}</p>
30 </div>
31 </div>
32 <div class="pure-g server-row">
33 <div class="pure-u-lg-1-2 pure-u-1 server-label">
34 <p>{'Client IP'|t}</p>
35 </div>
36 <div class="pure-u-lg-1-2 pure-u-1">
37 <p>{$client_ip}</p>
38 </div>
39 </div>
40 <div class="pure-g server-row">
41 <div class="pure-u-lg-1-2 pure-u-1 server-label">
42 <p>{'Trusted reverse proxies'|t}</p>
43 </div>
44 <div class="pure-u-lg-1-2 pure-u-1">
45 {if="count($trusted_proxies) > 0"}
46 <p>
47 {loop="$trusted_proxies"}
48 {$value}<br>
49 {/loop}
50 </p>
51 {else}
52 <p>{'N/A'|t}</p>
53 {/if}
54 </div>
55 </div>
56
57 {include="server.requirements"}
58
59 <h3 class="window-subtitle">Version</h3>
60
61 <div class="pure-g server-row">
62 <div class="pure-u-lg-1-2 pure-u-1 server-label">
63 <p>Current version</p>
64 </div>
65 <div class="pure-u-lg-1-2 pure-u-1">
66 <p>{$current_version}</p>
67 </div>
68 </div>
69
70 <div class="pure-g server-row">
71 <div class="pure-u-lg-1-2 pure-u-1 server-label">
72 <p>Latest release</p>
73 </div>
74 <div class="pure-u-lg-1-2 pure-u-1">
75 <p>
76 <a href="{$release_url}" title="{'Visit releases page on Github'|t}">
77 {$latest_version}
78 </a>
79 </p>
80 </div>
81 </div>
82
83 <h3 class="window-subtitle">Thumbnails</h3>
84
85 <div class="pure-g server-row">
86 <div class="pure-u-lg-1-2 pure-u-1 server-label">
87 <p>Thumbnails status</p>
88 </div>
89 <div class="pure-u-lg-1-2 pure-u-1">
90 <p>
91 {if="$thumbnails_mode==='all'"}
92 {'All'|t}
93 {elseif="$thumbnails_mode==='common'"}
94 {'Only common media hosts'|t}
95 {else}
96 {'None'|t}
97 {/if}
98 </p>
99 </div>
100 </div>
101
102 {if="$thumbnails_mode!=='none'"}
103 <div class="center tools-item">
104 <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
105 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
106 </a>
107 </div>
108 {/if}
109
110 <h3 class="window-subtitle">Cache</h3>
111
112 <div class="center tools-item">
113 <a href="{$base_path}/admin/clear-cache?type=main">
114 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear main cache</span>
115 </a>
116 </div>
117
118 <div class="center tools-item">
119 <a href="{$base_path}/admin/clear-cache?type=thumbnails">
120 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear thumbnails cache</span>
121 </a>
122 </div>
123 </div>
124</div>
125
126{include="page.footer"}
127
128</body>
129</html>
diff --git a/tpl/default/server.requirements.html b/tpl/default/server.requirements.html
new file mode 100644
index 00000000..85def9b7
--- /dev/null
+++ b/tpl/default/server.requirements.html
@@ -0,0 +1,68 @@
1<div class="server-tables">
2 <h3 class="window-subtitle">{'Permissions'|t}</h3>
3
4 {if="count($permissions) > 0"}
5 <p class="center">
6 <i class="fa fa-close fa-color-red" aria-hidden="true"></i>
7 {'There are permissions that need to be fixed.'|t}
8 </p>
9
10 <p>
11 {loop="$permissions"}
12 <div class="center">{$value}</div>
13 {/loop}
14 </p>
15 {else}
16 <p class="center">
17 <i class="fa fa-check fa-color-green" aria-hidden="true"></i>
18 {'All read/write permissions are properly set.'|t}
19 </p>
20 {/if}
21
22 <h3 class="window-subtitle">PHP</h3>
23
24 <p class="center">
25 <strong>{'Running PHP'|t} {$php_version}</strong>
26 {if="$php_has_reached_eol"}
27 <i class="fa fa-circle fa-color-orange" aria-label="hidden"></i><br>
28 {'End of life: '|t} {$php_eol}
29 {else}
30 <i class="fa fa-circle fa-color-green" aria-label="hidden"></i><br>
31 {/if}
32 </p>
33
34 <table class="center">
35 <thead>
36 <tr>
37 <th>{'Extension'|t}</th>
38 <th>{'Usage'|t}</th>
39 <th>{'Status'|t}</th>
40 <th>{'Loaded'|t}</th>
41 </tr>
42 </thead>
43 <tbody>
44 {loop="$php_extensions"}
45 <tr>
46 <td>{$value.name}</td>
47 <td>{$value.desc}</td>
48 <td>{$value.required ? t('Required') : t('Optional')}</td>
49 <td>
50 {if="$value.loaded"}
51 {$classLoaded="fa-color-green"}
52 {$strLoaded=t('Loaded')}
53 {else}
54 {$strLoaded=t('Not loaded')}
55 {if="$value.required"}
56 {$classLoaded="fa-color-red"}
57 {else}
58 {$classLoaded="fa-color-orange"}
59 {/if}
60 {/if}
61
62 <i class="fa fa-circle {$classLoaded}" aria-label="{$strLoaded}" title="{$strLoaded}"></i>
63 </td>
64 </tr>
65 {/loop}
66 </tbody>
67 </table>
68</div>
diff --git a/tpl/default/tag.cloud.html b/tpl/default/tag.cloud.html
index 7839fcca..c067e1d4 100644
--- a/tpl/default/tag.cloud.html
+++ b/tpl/default/tag.cloud.html
@@ -15,7 +15,7 @@
15 <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2> 15 <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
16 {if="!empty($search_tags)"} 16 {if="!empty($search_tags)"}
17 <p class="center"> 17 <p class="center">
18 <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli"> 18 <a href="{$base_path}/?searchtags={$search_tags_url}" class="pure-button pure-button-shaarli">
19 {'List all links with those tags'|t} 19 {'List all links with those tags'|t}
20 </a> 20 </a>
21 </p> 21 </p>
@@ -48,8 +48,8 @@
48 48
49 <div id="cloudtag" class="cloudtag-container"> 49 <div id="cloudtag" class="cloudtag-container">
50 {loop="tags"} 50 {loop="tags"}
51 <a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" style="font-size:{$value.size}em;">{$key}</a 51 <a href="{$base_path}/?searchtags={$tags_url.$key1} {$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a
52 ><a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a> 52 ><a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
53 {loop="$value.tag_plugin"} 53 {loop="$value.tag_plugin"}
54 {$value} 54 {$value}
55 {/loop} 55 {/loop}
diff --git a/tpl/default/tag.list.html b/tpl/default/tag.list.html
index d5777465..96e7fbe0 100644
--- a/tpl/default/tag.list.html
+++ b/tpl/default/tag.list.html
@@ -15,7 +15,7 @@
15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2> 15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
16 {if="!empty($search_tags)"} 16 {if="!empty($search_tags)"}
17 <p class="center"> 17 <p class="center">
18 <a href="?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli"> 18 <a href="{$base_path}/?searchtags={$search_tags_url}" class="pure-button pure-button-shaarli">
19 {'List all links with those tags'|t} 19 {'List all links with those tags'|t}
20 </a> 20 </a>
21 </p> 21 </p>
@@ -47,17 +47,17 @@
47 47
48 <div id="taglist" class="taglist-container"> 48 <div id="taglist" class="taglist-container">
49 {loop="tags"} 49 {loop="tags"}
50 <div class="tag-list-item pure-g" data-tag="{$key}"> 50 <div class="tag-list-item pure-g" data-tag="{$key}" data-tag-url="{$tags_url.$key1}">
51 <div class="pure-u-1"> 51 <div class="pure-u-1">
52 {if="$is_logged_in===true"} 52 {if="$is_logged_in===true"}
53 <a href="#" class="delete-tag" aria-label="{'Delete'|t}"><i class="fa fa-trash" aria-hidden="true"></i></a>&nbsp;&nbsp; 53 <a href="#" class="delete-tag" aria-label="{'Delete'|t}"><i class="fa fa-trash" aria-hidden="true"></i></a>&nbsp;&nbsp;
54 <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag" aria-label="{'Rename tag'|t}"> 54 <a href="{$base_path}/admin/tags?fromtag={$tags_url.$key1}" class="rename-tag" aria-label="{'Rename tag'|t}">
55 <i class="fa fa-pencil-square-o {$key}" aria-hidden="true"></i> 55 <i class="fa fa-pencil-square-o {$key}" aria-hidden="true"></i>
56 </a> 56 </a>
57 {/if} 57 {/if}
58 58
59 <a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a> 59 <a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value}</a>
60 <a href="?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a> 60 <a href="{$base_path}/?searchtags={$tags_url.$key1} {$search_tags_url}" class="tag-link">{$key}</a>
61 61
62 {loop="$value.tag_plugin"} 62 {loop="$value.tag_plugin"}
63 {$value} 63 {$value}
diff --git a/tpl/default/tag.sort.html b/tpl/default/tag.sort.html
index d24c9f64..b3764e29 100644
--- a/tpl/default/tag.sort.html
+++ b/tpl/default/tag.sort.html
@@ -1,8 +1,8 @@
1<div class="pure-g"> 1<div class="pure-g">
2 <div class="pure-u-1 pure-alert pure-alert-success tag-sort"> 2 <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
3 {'Sort by:'|t} 3 {'Sort by:'|t}
4 <a href="?do=tagcloud">{'Cloud'|t}</a> &middot; 4 <a href="{$base_path}/tags/cloud">{'Cloud'|t}</a>
5 <a href="?do=taglist&sort=usage">{'Most used'|t}</a> &middot; 5 <a href="{$base_path}/tags/list?sort=usage">{'Most used'|t}</a>
6 <a href="?do=taglist&sort=alpha">{'Alphabetical'|t}</a> 6 <a href="{$base_path}/tags/list?sort=alpha">{'Alphabetical'|t}</a>
7 </div> 7 </div>
8</div> \ No newline at end of file 8</div>
diff --git a/tpl/default/thumbnails.html b/tpl/default/thumbnails.html
index 5f9bef08..504644ca 100644
--- a/tpl/default/thumbnails.html
+++ b/tpl/default/thumbnails.html
@@ -43,6 +43,6 @@
43</div> 43</div>
44 44
45{include="page.footer"} 45{include="page.footer"}
46<script src="js/thumbnails_update.min.js?v={$version_hash}"></script> 46<script src="{$asset_path}/js/thumbnails_update.min.js?v={$version_hash}#"></script>
47</body> 47</body>
48</html> 48</html>
diff --git a/tpl/default/tools.html b/tpl/default/tools.html
index 20d0c893..2df73598 100644
--- a/tpl/default/tools.html
+++ b/tpl/default/tools.html
@@ -11,48 +11,46 @@
11 <div class="pure-u-lg-1-3 pure-u-22-24 page-form page-form-light"> 11 <div class="pure-u-lg-1-3 pure-u-22-24 page-form page-form-light">
12 <h2 class="window-title">{'Settings'|t}</h2> 12 <h2 class="window-title">{'Settings'|t}</h2>
13 <div class="tools-item"> 13 <div class="tools-item">
14 <a href="?do=configure" title="{'Change Shaarli settings: title, timezone, etc.'|t}"> 14 <a href="{$base_path}/admin/configure" title="{'Change Shaarli settings: title, timezone, etc.'|t}">
15 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Configure your Shaarli'|t}</span> 15 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Configure your Shaarli'|t}</span>
16 </a> 16 </a>
17 </div> 17 </div>
18 <div class="tools-item"> 18 <div class="tools-item">
19 <a href="?do=pluginadmin" title="{'Enable, disable and configure plugins'|t}"> 19 <a href="{$base_path}/admin/plugins" title="{'Enable, disable and configure plugins'|t}">
20 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span> 20 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span>
21 </a> 21 </a>
22 </div> 22 </div>
23 <div class="tools-item">
24 <a href="{$base_path}/admin/server"
25 title="{'Check instance\'s server configuration'|t}">
26 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Server administration'|t}</span>
27 </a>
28 </div>
23 {if="!$openshaarli"} 29 {if="!$openshaarli"}
24 <div class="tools-item"> 30 <div class="tools-item">
25 <a href="?do=changepasswd" title="{'Change your password'|t}"> 31 <a href="{$base_path}/admin/password" title="{'Change your password'|t}">
26 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Change password'|t}</span> 32 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Change password'|t}</span>
27 </a> 33 </a>
28 </div> 34 </div>
29 {/if} 35 {/if}
30 <div class="tools-item"> 36 <div class="tools-item">
31 <a href="?do=changetag" title="{'Rename or delete a tag in all links'|t}"> 37 <a href="{$base_path}/admin/tags" title="{'Rename or delete a tag in all links'|t}">
32 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Manage tags'|t}</span> 38 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Manage tags'|t}</span>
33 </a> 39 </a>
34 </div> 40 </div>
35 <div class="tools-item"> 41 <div class="tools-item">
36 <a href="?do=import" 42 <a href="{$base_path}/admin/import"
37 title="{'Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, delicious...)'|t}"> 43 title="{'Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, delicious...)'|t}">
38 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Import links'|t}</span> 44 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Import links'|t}</span>
39 </a> 45 </a>
40 </div> 46 </div>
41 <div class="tools-item"> 47 <div class="tools-item">
42 <a href="?do=export" 48 <a href="{$base_path}/admin/export"
43 title="{'Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)'|t}"> 49 title="{'Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)'|t}">
44 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Export database'|t}</span> 50 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Export database'|t}</span>
45 </a> 51 </a>
46 </div> 52 </div>
47 53
48 {if="$thumbnails_enabled"}
49 <div class="tools-item">
50 <a href="?do=thumbs_update" title="{'Synchronize all link thumbnails'|t}">
51 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
52 </a>
53 </div>
54 {/if}
55
56 {loop="$tools_plugin"} 54 {loop="$tools_plugin"}
57 <div class="tools-item"> 55 <div class="tools-item">
58 {$value} 56 {$value}
@@ -86,7 +84,7 @@
86 alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}'); 84 alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}');
87 } 85 }
88 window.open( 86 window.open(
89 '{$pageabsaddr}?post='%20+%20encodeURIComponent(url)+ 87 '{$pageabsaddr}admin/shaare?post='%20+%20encodeURIComponent(url)+
90 '&amp;title='%20+%20encodeURIComponent(title)+ 88 '&amp;title='%20+%20encodeURIComponent(title)+
91 '&amp;description='%20+%20encodeURIComponent(desc)+ 89 '&amp;description='%20+%20encodeURIComponent(desc)+
92 '&amp;source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1' 90 '&amp;source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1'
diff --git a/tpl/vintage/404.html b/tpl/vintage/404.html
index 53e98e2e..0fef0f08 100644
--- a/tpl/vintage/404.html
+++ b/tpl/vintage/404.html
@@ -10,7 +10,7 @@
10<div class="error-container"> 10<div class="error-container">
11 <h1>404 Not found <small>Oh crap!</small></h1> 11 <h1>404 Not found <small>Oh crap!</small></h1>
12 <p>{$error_message}</p> 12 <p>{$error_message}</p>
13 <p>Would you mind <a href="?">clicking here</a>?</p> 13 <p>Would you mind <a href="{$base_path}/">clicking here</a>?</p>
14</div> 14</div>
15{include="page.footer"} 15{include="page.footer"}
16</body> 16</body>
diff --git a/tpl/vintage/addlink.html b/tpl/vintage/addlink.html
index da50f45e..ade08c7c 100644
--- a/tpl/vintage/addlink.html
+++ b/tpl/vintage/addlink.html
@@ -5,7 +5,7 @@
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="headerform"> 7 <div id="headerform">
8 <form method="GET" action="" name="addform" class="addform"> 8 <form method="GET" action="{$base_path}/admin/shaare" name="addform" class="addform">
9 <input type="text" name="post" class="linkurl"> 9 <input type="text" name="post" class="linkurl">
10 <input type="submit" value="Add link" class="bigbutton"> 10 <input type="submit" value="Add link" class="bigbutton">
11 </form> 11 </form>
diff --git a/tpl/vintage/changepassword.html b/tpl/vintage/changepassword.html
index c40daf9d..7e37b9a3 100644
--- a/tpl/vintage/changepassword.html
+++ b/tpl/vintage/changepassword.html
@@ -4,7 +4,7 @@
4<body onload="document.changepasswordform.oldpassword.focus();"> 4<body onload="document.changepasswordform.oldpassword.focus();">
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <form method="POST" action="#" name="changepasswordform" id="changepasswordform"> 7 <form method="POST" action="{$base_path}/admin/password" name="changepasswordform" id="changepasswordform">
8 Old password: <input type="password" name="oldpassword">&nbsp; &nbsp; 8 Old password: <input type="password" name="oldpassword">&nbsp; &nbsp;
9 New password: <input type="password" name="setpassword"> 9 New password: <input type="password" name="setpassword">
10 <input type="hidden" name="token" value="{$token}"> 10 <input type="hidden" name="token" value="{$token}">
@@ -12,4 +12,4 @@
12</div> 12</div>
13{include="page.footer"} 13{include="page.footer"}
14</body> 14</body>
15</html> \ No newline at end of file 15</html>
diff --git a/tpl/vintage/changetag.html b/tpl/vintage/changetag.html
index 670a8dd7..6ef60252 100644
--- a/tpl/vintage/changetag.html
+++ b/tpl/vintage/changetag.html
@@ -5,7 +5,7 @@
5<body onload="document.changetag.fromtag.focus();"> 5<body onload="document.changetag.fromtag.focus();">
6<div id="pageheader"> 6<div id="pageheader">
7 {include="page.header"} 7 {include="page.header"}
8 <form method="POST" action="" name="changetag" id="changetag"> 8 <form method="POST" action="{$base_path}/admin/tags" name="changetag" id="changetag">
9 <input type="hidden" name="token" value="{$token}"> 9 <input type="hidden" name="token" value="{$token}">
10 <div> 10 <div>
11 <label for="fromtag">Tag:</label> 11 <label for="fromtag">Tag:</label>
diff --git a/tpl/vintage/configure.html b/tpl/vintage/configure.html
index 53b0cad2..ba4f3f71 100644
--- a/tpl/vintage/configure.html
+++ b/tpl/vintage/configure.html
@@ -4,7 +4,7 @@
4<body onload="document.configform.title.focus();"> 4<body onload="document.configform.title.focus();">
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <form method="POST" action="#" name="configform" id="configform"> 7 <form method="POST" action="{$base_path}/admin/configure" name="configform" id="configform">
8 <input type="hidden" name="token" value="{$token}"> 8 <input type="hidden" name="token" value="{$token}">
9 <table id="configuration_table"> 9 <table id="configuration_table">
10 10
@@ -16,7 +16,7 @@
16 <tr> 16 <tr>
17 <td><b>Home link:</b></td> 17 <td><b>Home link:</b></td>
18 <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label 18 <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label
19 for="titleLink">(default value is: ?)</label></td> 19 for="titleLink">(default value is: {$base_path}/)</label></td>
20 </tr> 20 </tr>
21 21
22 <tr> 22 <tr>
@@ -159,7 +159,7 @@
159 {if="! $gd_enabled"} 159 {if="! $gd_enabled"}
160 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t} 160 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
161 {elseif="$thumbnails_enabled"} 161 {elseif="$thumbnails_enabled"}
162 <a href="?do=thumbs_update">{'Synchonize thumbnails'|t}</a> 162 <a href="{$base_path}/admin/thumbnails">{'Synchonize thumbnails'|t}</a>
163 {/if} 163 {/if}
164 </label> 164 </label>
165 </td> 165 </td>
diff --git a/tpl/vintage/daily.html b/tpl/vintage/daily.html
index 00f18e26..74f6cdc7 100644
--- a/tpl/vintage/daily.html
+++ b/tpl/vintage/daily.html
@@ -14,9 +14,9 @@
14 14
15 <div class="dailyAbout"> 15 <div class="dailyAbout">
16 All links of one day<br>in a single page.<br> 16 All links of one day<br>in a single page.<br>
17 {if="$previousday"} <a href="?do=daily&amp;day={$previousday}"><b>&lt;</b>Previous day</a>{else}<b>&lt;</b>Previous day{/if} 17 {if="$previousday"} <a href="{$base_path}/daily&amp;day={$previousday}"><b>&lt;</b>Previous day</a>{else}<b>&lt;</b>Previous day{/if}
18 - 18 -
19 {if="$nextday"}<a href="?do=daily&amp;day={$nextday}">Next day<b>&gt;</b></a>{else}Next day<b>&gt;</b>{/if} 19 {if="$nextday"}<a href="{$base_path}/daily&amp;day={$nextday}">Next day<b>&gt;</b></a>{else}Next day<b>&gt;</b>{/if}
20 <br> 20 <br>
21 21
22 {loop="$daily_about_plugin"} 22 {loop="$daily_about_plugin"}
@@ -24,13 +24,13 @@
24 {/loop} 24 {/loop}
25 25
26 <br> 26 <br>
27 <a href="?do=dailyrss" title="1 RSS entry per day"><img src="img/feed-icon-14x14.png" alt="rss_feed">Daily RSS Feed</a> 27 <a href="{$base_path}/daily-rss" title="1 RSS entry per day"><img src="{$asset_path}/img/feed-icon-14x14.png#" alt="rss_feed">Daily RSS Feed</a>
28 </div> 28 </div>
29 29
30 <div class="dailyTitle"> 30 <div class="dailyTitle">
31 <img src="img/floral_left.png" width="51" height="50" class="nomobile" alt="floral_left"> 31 <img src="{$asset_path}/img/floral_left.png#" width="51" height="50" class="nomobile" alt="floral_left">
32 The Daily Shaarli 32 The Daily Shaarli
33 <img src="img/floral_right.png" width="51" height="50" class="nomobile" alt="floral_right"> 33 <img src="{$asset_path}/img/floral_right.png#" width="51" height="50" class="nomobile" alt="floral_right">
34 </div> 34 </div>
35 35
36 <div class="dailyDate"> 36 <div class="dailyDate">
@@ -52,13 +52,13 @@
52 {$link=$value} 52 {$link=$value}
53 <div class="dailyEntry"> 53 <div class="dailyEntry">
54 <div class="dailyEntryPermalink"> 54 <div class="dailyEntryPermalink">
55 <a href="?{$value.shorturl}"> 55 <a href="{$base_path}/?{$value.shorturl}">
56 <img src="img/squiggle.png" width="25" height="26" title="permalink" alt="permalink"> 56 <img src="{$asset_path}/img/squiggle.png#" width="25" height="26" title="permalink" alt="permalink">
57 </a> 57 </a>
58 </div> 58 </div>
59 {if="!$hide_timestamps || $is_logged_in"} 59 {if="!$hide_timestamps || $is_logged_in"}
60 <div class="dailyEntryLinkdate"> 60 <div class="dailyEntryLinkdate">
61 <a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a> 61 <a href="{$base_path}/?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
62 </div> 62 </div>
63 {/if} 63 {/if}
64 {if="$link.tags"} 64 {if="$link.tags"}
@@ -101,9 +101,9 @@
101 {$value} 101 {$value}
102 {/loop} 102 {/loop}
103 </div> 103 </div>
104 <div id="closing"><img src="img/squiggle_closing.png" width="66" height="61" alt="-"></div> 104 <div id="closing"><img src="{$asset_path}/img/squiggle_closing.png#" width="66" height="61" alt="-"></div>
105</div> 105</div>
106{include="page.footer"} 106{include="page.footer"}
107<script src="js/thumbnails.min.js?v={$version_hash}"></script> 107<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
108</body> 108</body>
109</html> 109</html>
diff --git a/tpl/vintage/dailyrss.html b/tpl/vintage/dailyrss.html
index f589b06e..ff19bbfb 100644
--- a/tpl/vintage/dailyrss.html
+++ b/tpl/vintage/dailyrss.html
@@ -1,16 +1,32 @@
1<item> 1<?xml version="1.0" encoding="UTF-8"?>
2 <title>{$title} - {function="strftime('%A %e %B %Y', $daydate)"}</title> 2<rss version="2.0">
3 <guid>{$absurl}</guid> 3 <channel>
4 <link>{$absurl}</link> 4 <title>Daily - {$title}</title>
5 <pubDate>{$rssdate}</pubDate> 5 <link>{$index_url}</link>
6 <description><![CDATA[ 6 <description>Daily shaared bookmarks</description>
7 {loop="links"} 7 <language>{$language}</language>
8 <h3><a href="{$value.url}">{$value.title}</a></h3> 8 <copyright>{$index_url}</copyright>
9 <small>{if="!$hide_timestamps"}{function="strftime('%c', $value.timestamp)"} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> 9 <generator>Shaarli</generator>
10 {$value.url}</small><br> 10
11 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br> 11 {loop="$days"}
12 {if="$value.description"}{$value.formatedDescription}{/if} 12 <item>
13 <br><br><hr> 13 <title>{$value.date_human} - {$title}</title>
14 <guid>{$value.absolute_url}</guid>
15 <link>{$value.absolute_url}</link>
16 <pubDate>{$value.date_rss}</pubDate>
17 <description><![CDATA[
18 {loop="$value.links"}
19 <h3><a href="{$value.url}">{$value.title}</a></h3>
20 <small>
21 {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}<br>
22 {$value.url}
23 </small><br>
24 {if="$value.thumbnail"}<img src="{$index_url}{$value.thumbnail}#" alt="thumbnail" />{/if}<br>
25 {if="$value.description"}{$value.description}{/if}
26 <br><br><hr>
14 {/loop} 27 {/loop}
15 ]]></description> 28 ]]></description>
16</item> 29 </item>
30 {/loop}
31 </channel>
32</rss><!-- Cached version of {$page_url} -->
diff --git a/tpl/vintage/editlink.html b/tpl/vintage/editlink.html
index 6f7a330f..eb8807b5 100644
--- a/tpl/vintage/editlink.html
+++ b/tpl/vintage/editlink.html
@@ -1,21 +1,16 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3<head>{include="includes"} 3<head>{include="includes"}
4 <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
5</head> 4</head>
6<body 5<body
7{if="$link.title==''"}onload="document.linkform.lf_title.focus();" 6{if="$link.title==''"}onload="document.linkform.lf_title.focus();"
8{elseif="$link.description==''"}onload="document.linkform.lf_description.focus();" 7{elseif="$link.description==''"}onload="document.linkform.lf_description.focus();"
9{else}onload="document.linkform.lf_tags.focus();"{/if} > 8{else}onload="document.linkform.lf_tags.focus();"{/if} >
10<div id="pageheader"> 9<div id="pageheader">
11 {if="$source !== 'firefoxsocialapi'"}
12 {include="page.header"} 10 {include="page.header"}
13 {else}
14 <div id="shaarli_title"><a href="{$titleLink}">{$shaarlititle}</a></div> 11 <div id="shaarli_title"><a href="{$titleLink}">{$shaarlititle}</a></div>
15 {/if}
16 <div id="editlinkform"> 12 <div id="editlinkform">
17 <form method="post" name="linkform"> 13 <form method="post" name="linkform" action="{$base_path}/admin/shaare">
18 <input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
19 {if="isset($link.id)"} 14 {if="isset($link.id)"}
20 <input type="hidden" name="lf_id" value="{$link.id}"> 15 <input type="hidden" name="lf_id" value="{$link.id}">
21 {/if} 16 {/if}
@@ -48,19 +43,18 @@
48 {/if} 43 {/if}
49 <input type="submit" value="Save" name="save_edit" class="bigbutton"> 44 <input type="submit" value="Save" name="save_edit" class="bigbutton">
50 {if="!$link_is_new && isset($link.id)"} 45 {if="!$link_is_new && isset($link.id)"}
51 <a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}" 46 <a href="{$base_path}/admin/shaare/delete?id={$link.id}&amp;token={$token}"
52 name="delete_link" class="bigbutton" 47 name="delete_link" class="bigbutton"
53 onClick="return confirmDeleteLink();"> 48 onClick="return confirmDeleteLink();">
54 {'Delete'|t} 49 {'Delete'|t}
55 </a> 50 </a>
56 {/if} 51 {/if}
57 <input type="hidden" name="token" value="{$token}"> 52 <input type="hidden" name="token" value="{$token}">
53 <input type="hidden" name="source" value="{$source}">
58 {if="$http_referer"}<input type="hidden" name="returnurl" value="{$http_referer}">{/if} 54 {if="$http_referer"}<input type="hidden" name="returnurl" value="{$http_referer}">{/if}
59 </form> 55 </form>
60 </div> 56 </div>
61</div> 57</div>
62{if="$source !== 'firefoxsocialapi'"}
63{include="page.footer"} 58{include="page.footer"}
64{/if}
65</body> 59</body>
66</html> 60</html>
diff --git a/tpl/vintage/error.html b/tpl/vintage/error.html
index b6e62be0..64f54cd2 100644
--- a/tpl/vintage/error.html
+++ b/tpl/vintage/error.html
@@ -18,7 +18,7 @@
18 </pre> 18 </pre>
19 {/if} 19 {/if}
20 20
21 <p>Would you mind <a href="?">clicking here</a>?</p> 21 <p>Would you mind <a href="{$base_path}/">clicking here</a>?</p>
22</div> 22</div>
23{include="page.footer"} 23{include="page.footer"}
24</body> 24</body>
diff --git a/tpl/vintage/export.html b/tpl/vintage/export.html
index 67c3d05f..c30e3b0a 100644
--- a/tpl/vintage/export.html
+++ b/tpl/vintage/export.html
@@ -5,12 +5,13 @@
5 <div id="pageheader"> 5 <div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="toolsdiv"> 7 <div id="toolsdiv">
8 <form method="GET"> 8 <form method="POST" action="{$base_path}/admin/export">
9 <input type="hidden" name="do" value="export">
10 Selection:<br> 9 Selection:<br>
11 <input type="radio" name="selection" value="all" checked="true"> All<br> 10 <input type="radio" name="selection" value="all" checked="true"> All<br>
12 <input type="radio" name="selection" value="private"> Private<br> 11 <input type="radio" name="selection" value="private"> Private<br>
13 <input type="radio" name="selection" value="public"> Public<br> 12 <input type="radio" name="selection" value="public"> Public<br>
13 <input type="hidden" name="token" value="{$token}">
14
14 <br> 15 <br>
15 <input type="checkbox" name="prepend_note_url" id="prepend_note_url"> 16 <input type="checkbox" name="prepend_note_url" id="prepend_note_url">
16 <label for="prepend_note_url"> 17 <label for="prepend_note_url">
diff --git a/tpl/vintage/feed.atom.html b/tpl/vintage/feed.atom.html
index 0621cb9e..5919bb49 100644
--- a/tpl/vintage/feed.atom.html
+++ b/tpl/vintage/feed.atom.html
@@ -6,8 +6,8 @@
6 <updated>{$last_update}</updated> 6 <updated>{$last_update}</updated>
7 {/if} 7 {/if}
8 <link rel="self" href="{$self_link}#" /> 8 <link rel="self" href="{$self_link}#" />
9 <link rel="search" type="application/opensearchdescription+xml" href="{$index_url}?do=opensearch#" 9 <link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
10 title="Shaarli search - {$shaarlititle}" /> 10 title="Shaarli search - {$shaarlititle}" />
11 {loop="$feed_plugins_header"} 11 {loop="$feed_plugins_header"}
12 {$value} 12 {$value}
13 {/loop} 13 {/loop}
diff --git a/tpl/vintage/feed.rss.html b/tpl/vintage/feed.rss.html
index ee3fef88..4be8202f 100644
--- a/tpl/vintage/feed.rss.html
+++ b/tpl/vintage/feed.rss.html
@@ -8,7 +8,7 @@
8 <copyright>{$index_url}</copyright> 8 <copyright>{$index_url}</copyright>
9 <generator>Shaarli</generator> 9 <generator>Shaarli</generator>
10 <atom:link rel="self" href="{$self_link}" /> 10 <atom:link rel="self" href="{$self_link}" />
11 <atom:link rel="search" type="application/opensearchdescription+xml" href="{$index_url}?do=opensearch#" 11 <atom:link rel="search" type="application/opensearchdescription+xml" href="{$index_url}open-search#"
12 title="Shaarli search - {$shaarlititle}" /> 12 title="Shaarli search - {$shaarlititle}" />
13 {loop="$feed_plugins_header"} 13 {loop="$feed_plugins_header"}
14 {$value} 14 {$value}
diff --git a/tpl/vintage/import.html b/tpl/vintage/import.html
index bb9e4a56..7d6eac76 100644
--- a/tpl/vintage/import.html
+++ b/tpl/vintage/import.html
@@ -6,7 +6,7 @@
6 {include="page.header"} 6 {include="page.header"}
7 <div id="uploaddiv"> 7 <div id="uploaddiv">
8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize}). 8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize}).
9 <form method="POST" action="?do=import" enctype="multipart/form-data" 9 <form method="POST" action="{$base_path}/admin/import" enctype="multipart/form-data"
10 name="uploadform" id="uploadform"> 10 name="uploadform" id="uploadform">
11 <input type="hidden" name="token" value="{$token}"> 11 <input type="hidden" name="token" value="{$token}">
12 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 12 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
diff --git a/tpl/vintage/includes.html b/tpl/vintage/includes.html
index 8d273c44..eac05701 100644
--- a/tpl/vintage/includes.html
+++ b/tpl/vintage/includes.html
@@ -3,18 +3,19 @@
3<meta name="format-detection" content="telephone=no" /> 3<meta name="format-detection" content="telephone=no" />
4<meta name="viewport" content="width=device-width,initial-scale=1.0" /> 4<meta name="viewport" content="width=device-width,initial-scale=1.0" />
5<meta name="referrer" content="same-origin"> 5<meta name="referrer" content="same-origin">
6<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" /> 6<link rel="alternate" type="application/rss+xml" href="{$feedurl}feed/rss?{$searchcrits}#" title="RSS Feed" />
7<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" /> 7<link rel="alternate" type="application/atom+xml" href="{$feedurl}feed/atom?{$searchcrits}#" title="ATOM Feed" />
8<link href="img/favicon.ico" rel="shortcut icon" type="image/x-icon" /> 8<link href="img/favicon.ico" rel="shortcut icon" type="image/x-icon" />
9<link type="text/css" rel="stylesheet" href="css/shaarli.min.css" /> 9<link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css#" />
10{if="$formatter==='markdown'"} 10{if="$formatter==='markdown'"}
11 <link type="text/css" rel="stylesheet" href="css/markdown.min.css?v={$version_hash}" /> 11 <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" />
12{/if} 12{/if}
13{loop="$plugins_includes.css_files"} 13{loop="$plugins_includes.css_files"}
14<link type="text/css" rel="stylesheet" href="{$value}#"/> 14<link type="text/css" rel="stylesheet" href="{$base_path}/{$value}#"/>
15{/loop} 15{/loop}
16{if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="data/user.css#" />{/if} 16{if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />{/if}
17<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle|htmlspecialchars}"/> 17<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
18 title="Shaarli search - {$shaarlititle|htmlspecialchars}" />
18{if="! empty($links) && count($links) === 1"} 19{if="! empty($links) && count($links) === 1"}
19 {$link=reset($links)} 20 {$link=reset($links)}
20 <meta property="og:title" content="{$link.title}" /> 21 <meta property="og:title" content="{$link.title}" />
diff --git a/tpl/vintage/install.html b/tpl/vintage/install.html
index aca890d6..8c10b2cb 100644
--- a/tpl/vintage/install.html
+++ b/tpl/vintage/install.html
@@ -5,7 +5,7 @@
5<div id="install"> 5<div id="install">
6 <h1>Shaarli</h1> 6 <h1>Shaarli</h1>
7 It looks like it's the first time you run Shaarli. Please configure it:<br> 7 It looks like it's the first time you run Shaarli. Please configure it:<br>
8 <form method="POST" action="#" name="installform" id="installform"> 8 <form method="POST" action="{$base_path}/install" name="installform" id="installform">
9 <table> 9 <table>
10 <tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr> 10 <tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr>
11 <tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr> 11 <tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr>
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html
index dcb14e90..90f5cf8f 100644
--- a/tpl/vintage/linklist.html
+++ b/tpl/vintage/linklist.html
@@ -1,7 +1,6 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3<head> 3<head>
4 <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
5 {include="includes"} 4 {include="includes"}
6</head> 5</head>
7<body> 6<body>
@@ -66,25 +65,25 @@
66 tagged 65 tagged
67 {loop="$exploded_tags"} 66 {loop="$exploded_tags"}
68 <span class="linktag" title="Remove tag"> 67 <span class="linktag" title="Remove tag">
69 <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a> 68 <a href="{$base_path}/remove-tag/{function="urlencode($value)"}">{$value} <span class="remove">x</span></a>
70 </span> 69 </span>
71 {/loop} 70 {/loop}
72 {elseif="$search_tags === false"} 71 {elseif="$search_tags === false"}
73 <span class="linktag" title="Remove tag"> 72 <span class="linktag" title="Remove tag">
74 <a href="?">untagged <span class="remove">x</span></a> 73 <a href="{$base_path}/">untagged <span class="remove">x</span></a>
75 </span> 74 </span>
76 {/if} 75 {/if}
77 </div> 76 </div>
78 {/if} 77 {/if}
79 <ul> 78 <ul>
80 {loop="$links"} 79 {loop="$links"}
81 <li{if="$value.class"} class="{$value.class}"{/if}> 80 <li{if="$value.class"} class="{$value.class}"{/if} data-id="{$value.id}">
82 <a id="{$value.shorturl}"></a> 81 <a id="{$value.shorturl}"></a>
83 {if="$thumbnails_enabled && !empty($value.thumbnail)"} 82 {if="$thumbnails_enabled && $value.thumbnail !== false"}
84 <div class="thumbnail"> 83 <div class="thumbnail" {if="$value.thumbnail === null"}data-async-thumbnail="1"{/if}>
85 <a href="{$value.real_url}"> 84 <a href="{$value.real_url}">
86 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} 85 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
87 <img data-src="{$value.thumbnail}#" class="b-lazy" 86 <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy"
88 src="" 87 src=""
89 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 88 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
90 </a> 89 </a>
@@ -93,17 +92,16 @@
93 <div class="linkcontainer"> 92 <div class="linkcontainer">
94 {if="$is_logged_in"} 93 {if="$is_logged_in"}
95 <div class="linkeditbuttons"> 94 <div class="linkeditbuttons">
96 <form method="GET" class="buttoneditform"> 95 <a href="{$base_path}/admin/shaare/{$value.id}" title="Edit" class="button_edit">
97 <input type="hidden" name="edit_link" value="{$value.id}"> 96 <img src="{$asset_path}/img/edit_icon.png#">
98 <input type="image" alt="Edit" src="img/edit_icon.png" title="Edit" class="button_edit"> 97 </a>
99 </form><br> 98 <br>
100 <form method="GET" class="buttoneditform"> 99 <a href="{$base_path}/admin/shaare/delete?id={$value.id}&amp;token={$token}" label="Delete"
101 <input type="hidden" name="lf_linkdate" value="{$value.id}"> 100 onClick="return confirmDeleteLink();"
102 <input type="hidden" name="token" value="{$token}"> 101 class="button_delete"
103 <input type="hidden" name="delete_link"> 102 >
104 <input type="image" alt="Delete" src="img/delete_icon.png" title="Delete" 103 <img src="{$asset_path}/img/delete_icon.png#">
105 class="button_delete" onClick="return confirmDeleteLink();"> 104 </a>
106 </form>
107 </div> 105 </div>
108 {/if} 106 {/if}
109 <span class="linktitle"> 107 <span class="linktitle">
@@ -114,7 +112,7 @@
114 {if="!$hide_timestamps || $is_logged_in"} 112 {if="!$hide_timestamps || $is_logged_in"}
115 {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} 113 {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
116 <span class="linkdate" title="Permalink"> 114 <span class="linkdate" title="Permalink">
117 <a href="?{$value.shorturl}"> 115 <a href="{$base_path}/shaare/{$value.shorturl}">
118 <span title="{$updated}"> 116 <span title="{$updated}">
119 {$value.created|format_date} 117 {$value.created|format_date}
120 {if="$value.updated_timestamp"}*{/if} 118 {if="$value.updated_timestamp"}*{/if}
@@ -123,7 +121,7 @@
123 </a> - 121 </a> -
124 </span> 122 </span>
125 {else} 123 {else}
126 <span class="linkdate" title="Short link here"><a href="?{$value.shorturl}">permalink</a> - </span> 124 <span class="linkdate" title="Short link here"><a href="{$base_path}/shaare/{$value.shorturl}">permalink</a> - </span>
127 {/if} 125 {/if}
128 126
129 {loop="$value.link_plugin"} 127 {loop="$value.link_plugin"}
@@ -133,7 +131,7 @@
133 <a href="{$value.real_url}"><span class="linkurl" title="Short link">{$value.url}</span></a><br> 131 <a href="{$value.real_url}"><span class="linkurl" title="Short link">{$value.url}</span></a><br>
134 {if="$value.tags"} 132 {if="$value.tags"}
135 <div class="linktaglist"> 133 <div class="linktaglist">
136 {loop="$value.taglist"}<span class="linktag" title="Add tag"><a href="?addtag={$value|urlencode}">{$value}</a></span> {/loop} 134 {loop="$value.taglist"}<span class="linktag" title="Add tag"><a href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a></span> {/loop}
137 </div> 135 </div>
138 {/if} 136 {/if}
139 137
@@ -154,7 +152,8 @@
154</div> 152</div>
155 153
156 {include="page.footer"} 154 {include="page.footer"}
157<script src="js/thumbnails.min.js"></script> 155<script src="{$asset_path}/js/thumbnails.min.js#"></script>
156{if="$is_logged_in && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
158 157
159</body> 158</body>
160</html> 159</html>
diff --git a/tpl/vintage/linklist.paging.html b/tpl/vintage/linklist.paging.html
index 35149a6b..79daf16c 100644
--- a/tpl/vintage/linklist.paging.html
+++ b/tpl/vintage/linklist.paging.html
@@ -1,11 +1,11 @@
1<div class="paging"> 1<div class="paging">
2{if="$is_logged_in"} 2{if="$is_logged_in"}
3 <div class="paging_privatelinks"> 3 <div class="paging_privatelinks">
4 <a href="?visibility=private"> 4 <a href="{$base_path}/admin/isibility/private">
5 {if="$visibility=='private'"} 5 {if="$visibility=='private'"}
6 <img src="img/private_16x16_active.png" width="16" height="16" title="Click to see all links" alt="Click to see all links"> 6 <img src="{$asset_path}/img/private_16x16_active.png#" width="16" height="16" title="Click to see all links" alt="Click to see all links">
7 {else} 7 {else}
8 <img src="img/private_16x16.png" width="16" height="16" title="Click to see only private links" alt="Click to see only private links"> 8 <img src="{$asset_path}/img/private_16x16.png#" width="16" height="16" title="Click to see only private links" alt="Click to see only private links">
9 {/if} 9 {/if}
10 </a> 10 </a>
11 11
@@ -23,10 +23,15 @@
23 </div> 23 </div>
24 {/loop} 24 {/loop}
25 <div class="paging_linksperpage"> 25 <div class="paging_linksperpage">
26 Links per page: <a href="?linksperpage=20">20</a> <a href="?linksperpage=50">50</a> <a href="?linksperpage=100">100</a> 26 Links per page:
27 <form method="GET" class="linksperpage"><input type="text" name="linksperpage" size="2"></form> 27 <a href="{$base_path}/links-per-page?nb=20">20</a>
28 <a href="{$base_path}/links-per-page?nb=50">50</a>
29 <a href="{$base_path}/links-per-page?nb=100">100</a>
30 <form method="GET" class="linksperpage" action="{$base_path}/links-per-page">
31 <input type="text" name="nb" size="2">
32 </form>
28 </div> 33 </div>
29 {if="$previous_page_url"} <a href="{$previous_page_url}" class="paging_older">&#x25C4;Older</a> {/if} 34 {if="$previous_page_url"} <a href="{$previous_page_url}" class="paging_older">&#x25C4;Older</a> {/if}
30 <div class="paging_current">page {$page_current} / {$page_max} </div> 35 {if="$page_max>1"}<div class="paging_current">page {$page_current} / {$page_max} </div>{/if}
31 {if="$next_page_url"} <a href="{$next_page_url}" class="paging_newer">Newer&#x25BA;</a> {/if} 36 {if="$next_page_url"} <a href="{$next_page_url}" class="paging_newer">Newer&#x25BA;</a> {/if}
32</div> 37</div>
diff --git a/tpl/vintage/loginform.html b/tpl/vintage/loginform.html
index a3792066..6aa20ab1 100644
--- a/tpl/vintage/loginform.html
+++ b/tpl/vintage/loginform.html
@@ -11,7 +11,7 @@
11 {include="page.header"} 11 {include="page.header"}
12 12
13 <div id="headerform"> 13 <div id="headerform">
14 <form method="post" name="loginform"> 14 <form method="post" name="loginform" action="{$base_path}/login">
15 <label for="login">Login: <input type="text" id="login" name="login" tabindex="1" 15 <label for="login">Login: <input type="text" id="login" name="login" tabindex="1"
16 {if="!empty($username)"}value="{$username}"{/if}> 16 {if="!empty($username)"}value="{$username}"{/if}>
17 </label> 17 </label>
diff --git a/tpl/vintage/opensearch.html b/tpl/vintage/opensearch.html
index 3fcc30b7..1c7f279b 100644
--- a/tpl/vintage/opensearch.html
+++ b/tpl/vintage/opensearch.html
@@ -3,8 +3,8 @@
3 <ShortName>Shaarli search - {$pagetitle}</ShortName> 3 <ShortName>Shaarli search - {$pagetitle}</ShortName>
4 <Description>Shaarli search - {$pagetitle}</Description> 4 <Description>Shaarli search - {$pagetitle}</Description>
5 <Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" /> 5 <Url type="text/html" template="{$serverurl}?searchterm={searchTerms}" />
6 <Url type="application/atom+xml" template="{$serverurl}?do=atom&amp;searchterm={searchTerms}"/> 6 <Url type="application/atom+xml" template="{$serverurl}feed/atom?searchterm={searchTerms}"/>
7 <Url type="application/rss+xml" template="{$serverurl}?do=rss&amp;searchterm={searchTerms}"/> 7 <Url type="application/rss+xml" template="{$serverurl}feed/rss?searchterm={searchTerms}"/>
8 <InputEncoding>UTF-8</InputEncoding> 8 <InputEncoding>UTF-8</InputEncoding>
9 <Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer> 9 <Developer>Shaarli Community - https://github.com/shaarli/Shaarli/</Developer>
10 <Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE 10 <Image width="16" height="16">data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAHRklE
diff --git a/tpl/vintage/page.footer.html b/tpl/vintage/page.footer.html
index a3380841..0fe4c736 100644
--- a/tpl/vintage/page.footer.html
+++ b/tpl/vintage/page.footer.html
@@ -23,12 +23,14 @@
23</div> 23</div>
24{/if} 24{/if}
25 25
26<script src="js/shaarli.min.js"></script> 26<script src="{$asset_path}/js/shaarli.min.js#"></script>
27 27
28{if="$is_logged_in"} 28{if="$is_logged_in"}
29<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script> 29<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
30{/if} 30{/if}
31 31
32{loop="$plugins_footer.js_files"} 32{loop="$plugins_footer.js_files"}
33 <script src="{$value}#"></script> 33 <script src="{$base_path}/{$value}#"></script>
34{/loop} 34{/loop}
35
36<input type="hidden" name="js_base_path" value="{$base_path}" />
diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html
index a37926d2..0a33523b 100644
--- a/tpl/vintage/page.header.html
+++ b/tpl/vintage/page.header.html
@@ -18,22 +18,22 @@
18{else} 18{else}
19<li><a href="{$titleLink}" class="nomobile">Home</a></li> 19<li><a href="{$titleLink}" class="nomobile">Home</a></li>
20 {if="$is_logged_in"} 20 {if="$is_logged_in"}
21 <li><a href="?do=logout">Logout</a></li> 21 <li><a href="{$base_path}/admin/logout">Logout</a></li>
22 <li><a href="?do=tools">Tools</a></li> 22 <li><a href="{$base_path}/admin/tools">Tools</a></li>
23 <li><a href="?do=addlink">Add link</a></li> 23 <li><a href="{$base_path}/admin/add-shaare">Add link</a></li>
24 {elseif="$openshaarli"} 24 {elseif="$openshaarli"}
25 <li><a href="?do=tools">Tools</a></li> 25 <li><a href="{$base_path}/admin/tools">Tools</a></li>
26 <li><a href="?do=addlink">Add link</a></li> 26 <li><a href="{$base_path}/admin/add-shaare">Add link</a></li>
27 {else} 27 {else}
28 <li><a href="/login">Login</a></li> 28 <li><a href="{$base_path}/login">Login</a></li>
29 {/if} 29 {/if}
30 <li><a href="{$feedurl}?do=rss{$searchcrits}" class="nomobile">RSS Feed</a></li> 30 <li><a href="{$feedurl}/feed/rss?{$searchcrits}" class="nomobile">RSS Feed</a></li>
31 {if="$showatom"} 31 {if="$showatom"}
32 <li><a href="{$feedurl}?do=atom{$searchcrits}" class="nomobile">ATOM Feed</a></li> 32 <li><a href="{$feedurl}/feed/atom?{$searchcrits}" class="nomobile">ATOM Feed</a></li>
33 {/if} 33 {/if}
34 <li><a href="?do=tagcloud">Tag cloud</a></li> 34 <li><a href="{$base_path}/tags/cloud">Tag cloud</a></li>
35 <li><a href="?do=picwall{$searchcrits}">Picture wall</a></li> 35 <li><a href="{$base_path}/picture-wall{function="ltrim($searchcrits, '&')"}">Picture wall</a></li>
36 <li><a href="?do=daily">Daily</a></li> 36 <li><a href="{$base_path}/daily">Daily</a></li>
37 {loop="$plugins_header.buttons_toolbar"} 37 {loop="$plugins_header.buttons_toolbar"}
38 <li><a 38 <li><a
39 {loop="$value.attr"} 39 {loop="$value.attr"}
diff --git a/tpl/vintage/picwall.html b/tpl/vintage/picwall.html
index b3a16791..da3aa36c 100644
--- a/tpl/vintage/picwall.html
+++ b/tpl/vintage/picwall.html
@@ -38,6 +38,6 @@
38 38
39{include="page.footer"} 39{include="page.footer"}
40 40
41<script src="js/thumbnails.min.js"></script> 41<script src="{$asset_path}/js/thumbnails.min.js#"></script>
42</body> 42</body>
43</html> 43</html>
diff --git a/tpl/vintage/pluginsadmin.html b/tpl/vintage/pluginsadmin.html
index 63b45cac..d0972cd1 100644
--- a/tpl/vintage/pluginsadmin.html
+++ b/tpl/vintage/pluginsadmin.html
@@ -16,7 +16,7 @@
16</noscript> 16</noscript>
17 17
18<div id="pluginsadmin"> 18<div id="pluginsadmin">
19 <form action="?do=save_pluginadmin" method="POST"> 19 <form action="{$base_path}/admin/plugins" method="POST">
20 <section id="enabled_plugins"> 20 <section id="enabled_plugins">
21 <h1>Enabled Plugins</h1> 21 <h1>Enabled Plugins</h1>
22 22
@@ -86,9 +86,10 @@
86 <input type="submit" value="Save"/> 86 <input type="submit" value="Save"/>
87 </div> 87 </div>
88 </section> 88 </section>
89 <input type="hidden" name="token" value="{$token}">
89 </form> 90 </form>
90 91
91 <form action="?do=save_pluginadmin" method="POST"> 92 <form action="{$base_path}/admin/plugins" method="POST">
92 <section id="plugin_parameters"> 93 <section id="plugin_parameters">
93 <h1>Enabled Plugin Parameters</h1> 94 <h1>Enabled Plugin Parameters</h1>
94 95
@@ -124,6 +125,7 @@
124 </div> 125 </div>
125 </div> 126 </div>
126 </section> 127 </section>
128 <input type="hidden" name="token" value="{$token}">
127 </form> 129 </form>
128 130
129</div> 131</div>
diff --git a/tpl/vintage/tag.cloud.html b/tpl/vintage/tag.cloud.html
index d93bf4f9..5d21f239 100644
--- a/tpl/vintage/tag.cloud.html
+++ b/tpl/vintage/tag.cloud.html
@@ -12,8 +12,8 @@
12 12
13 <div id="cloudtag"> 13 <div id="cloudtag">
14 {loop="$tags"} 14 {loop="$tags"}
15 <a href="?addtag={$key|urlencode}" class="count">{$value.count}</a><a 15 <a href="{$base_path}/add-tag/{$key|urlencode}" class="count">{$value.count}</a><a
16 href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a> 16 href="{$base_path}/?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a>
17 {loop="$value.tag_plugin"} 17 {loop="$value.tag_plugin"}
18 {$value} 18 {$value}
19 {/loop} 19 {/loop}
diff --git a/tpl/vintage/thumbnails.html b/tpl/vintage/thumbnails.html
index 5cad845b..18f296f7 100644
--- a/tpl/vintage/thumbnails.html
+++ b/tpl/vintage/thumbnails.html
@@ -23,6 +23,6 @@
23<input type="hidden" name="ids" value="{function="implode(',', $ids)"}" /> 23<input type="hidden" name="ids" value="{function="implode(',', $ids)"}" />
24 24
25{include="page.footer"} 25{include="page.footer"}
26<script src="js/thumbnails_update.min.js?v={$version_hash}"></script> 26<script src="{$asset_path}/js/thumbnails_update.min.js?v={$version_hash}#"></script>
27</body> 27</body>
28</html> 28</html>
diff --git a/tpl/vintage/tools.html b/tpl/vintage/tools.html
index 1cef726e..1125bba9 100644
--- a/tpl/vintage/tools.html
+++ b/tpl/vintage/tools.html
@@ -5,17 +5,17 @@
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="toolsdiv"> 7 <div id="toolsdiv">
8 <a href="?do=configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a> 8 <a href="{$base_path}/admin/configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
9 <br><br> 9 <br><br>
10 <a href="?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a> 10 <a href="{$base_path}/admin/plugins"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
11 <br><br> 11 <br><br>
12 {if="!$openshaarli"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a> 12 {if="!$openshaarli"}<a href="{$base_path}/admin/password"><b>Change password</b><span>: Change your password.</span></a>
13 <br><br>{/if} 13 <br><br>{/if}
14 <a href="?do=changetag"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a> 14 <a href="{$base_path}/admin/tags"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a>
15 <br><br> 15 <br><br>
16 <a href="?do=import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a> 16 <a href="{$base_path}/admin/import"><b>Import</b><span>: Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)</span></a>
17 <br><br> 17 <br><br>
18 <a href="?do=export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a> 18 <a href="{$base_path}/admin/export"><b>Export</b><span>: Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)</span></a>
19 <br><br> 19 <br><br>
20 <a class="smallbutton" 20 <a class="smallbutton"
21 onclick="return alertBookmarklet();" 21 onclick="return alertBookmarklet();"
@@ -24,7 +24,7 @@
24 var%20url%20=%20location.href; 24 var%20url%20=%20location.href;
25 var%20title%20=%20document.title%20||%20url; 25 var%20title%20=%20document.title%20||%20url;
26 window.open( 26 window.open(
27 '{$pageabsaddr}?post='%20+%20encodeURIComponent(url)+ 27 '{$pageabsaddr}admin/shaare?post='%20+%20encodeURIComponent(url)+
28 '&amp;title='%20+%20encodeURIComponent(title)+ 28 '&amp;title='%20+%20encodeURIComponent(title)+
29 '&amp;description='%20+%20encodeURIComponent(document.getSelection())+ 29 '&amp;description='%20+%20encodeURIComponent(document.getSelection())+
30 '&amp;source=bookmarklet','_blank','menubar=no,height=390,width=600,toolbar=no,scrollbars=no,status=no,dialog=1' 30 '&amp;source=bookmarklet','_blank','menubar=no,height=390,width=600,toolbar=no,scrollbars=no,status=no,dialog=1'
diff --git a/webpack.config.js b/webpack.config.js
index 602147e5..a4aa633e 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -2,29 +2,26 @@ const path = require('path');
2const glob = require('glob'); 2const glob = require('glob');
3 3
4// Minify JS 4// Minify JS
5const MinifyPlugin = require('babel-minify-webpack-plugin'); 5const TerserPlugin = require('terser-webpack-plugin');
6 6
7// This plugin extracts the CSS into its own file instead of tying it with the JS. 7// This plugin extracts the CSS into its own file instead of tying it with the JS.
8// It prevents: 8// It prevents:
9// - not having styles due to a JS error 9// - not having styles due to a JS error
10// - the flash page without styles during JS loading 10// - the flash page without styles during JS loading
11const ExtractTextPlugin = require("extract-text-webpack-plugin"); 11const MiniCssExtractPlugin = require("mini-css-extract-plugin");
12 12
13const extractCssDefault = new ExtractTextPlugin({ 13const extractCss = new MiniCssExtractPlugin({
14 filename: "../css/[name].min.css", 14 filename: "../css/[name].min.css",
15 publicPath: 'tpl/default/css/',
16});
17
18const extractCssVintage = new ExtractTextPlugin({
19 filename: "../css/[name].min.css",
20 publicPath: 'tpl/vintage/css/',
21}); 15});
22 16
23module.exports = [ 17module.exports = [
24 { 18 {
19 mode: 'production',
25 entry: { 20 entry: {
21 shaare_batch: './assets/common/js/shaare-batch.js',
26 thumbnails: './assets/common/js/thumbnails.js', 22 thumbnails: './assets/common/js/thumbnails.js',
27 thumbnails_update: './assets/common/js/thumbnails-update.js', 23 thumbnails_update: './assets/common/js/thumbnails-update.js',
24 metadata: './assets/common/js/metadata.js',
28 pluginsadmin: './assets/default/js/plugins-admin.js', 25 pluginsadmin: './assets/default/js/plugins-admin.js',
29 shaarli: [ 26 shaarli: [
30 './assets/default/js/base.js', 27 './assets/default/js/base.js',
@@ -45,23 +42,23 @@ module.exports = [
45 loader: 'babel-loader', 42 loader: 'babel-loader',
46 options: { 43 options: {
47 presets: [ 44 presets: [
48 'babel-preset-env', 45 '@babel/preset-env',
49 ] 46 ]
50 } 47 }
51 } 48 }
52 }, 49 },
53 { 50 {
54 test: /\.s?css/, 51 test: /\.s?css/,
55 use: extractCssDefault.extract({ 52 use: [
56 use: [{ 53 {
57 loader: "css-loader", 54 loader: MiniCssExtractPlugin.loader,
58 options: { 55 options: {
59 minimize: true, 56 publicPath: 'tpl/default/css/',
60 } 57 },
61 }, { 58 },
62 loader: "sass-loader" 59 'css-loader',
63 }], 60 'sass-loader',
64 }) 61 ],
65 }, 62 },
66 { 63 {
67 test: /\.(gif|png|jpe?g|svg|ico)$/i, 64 test: /\.(gif|png|jpe?g|svg|ico)$/i,
@@ -81,17 +78,21 @@ module.exports = [
81 options: { 78 options: {
82 name: '../fonts/[name].[ext]', 79 name: '../fonts/[name].[ext]',
83 // do not add a publicPath here because it's already handled by CSS's publicPath 80 // do not add a publicPath here because it's already handled by CSS's publicPath
84 publicPath: '', 81 publicPath: '../default/',
85 } 82 }
86 }, 83 },
87 ], 84 ],
88 }, 85 },
86 optimization: {
87 minimize: true,
88 minimizer: [new TerserPlugin()],
89 },
89 plugins: [ 90 plugins: [
90 new MinifyPlugin(), 91 extractCss,
91 extractCssDefault,
92 ], 92 ],
93 }, 93 },
94 { 94 {
95 mode: 'production',
95 entry: { 96 entry: {
96 shaarli: [ 97 shaarli: [
97 './assets/vintage/js/base.js', 98 './assets/vintage/js/base.js',
@@ -100,6 +101,7 @@ module.exports = [
100 ].concat(glob.sync('./assets/vintage/img/*')), 101 ].concat(glob.sync('./assets/vintage/img/*')),
101 markdown: './assets/common/css/markdown.css', 102 markdown: './assets/common/css/markdown.css',
102 thumbnails: './assets/common/js/thumbnails.js', 103 thumbnails: './assets/common/js/thumbnails.js',
104 metadata: './assets/common/js/metadata.js',
103 thumbnails_update: './assets/common/js/thumbnails-update.js', 105 thumbnails_update: './assets/common/js/thumbnails-update.js',
104 }, 106 },
105 output: { 107 output: {
@@ -115,21 +117,23 @@ module.exports = [
115 loader: 'babel-loader', 117 loader: 'babel-loader',
116 options: { 118 options: {
117 presets: [ 119 presets: [
118 'babel-preset-env', 120 '@babel/preset-env',
119 ] 121 ]
120 } 122 }
121 } 123 }
122 }, 124 },
123 { 125 {
124 test: /\.css$/, 126 test: /\.css$/,
125 use: extractCssVintage.extract({ 127 use: [
126 use: [{ 128 {
127 loader: "css-loader", 129 loader: MiniCssExtractPlugin.loader,
128 options: { 130 options: {
129 minimize: true, 131 publicPath: 'tpl/vintage/css/',
130 } 132 },
131 }], 133 },
132 }) 134 'css-loader',
135 'sass-loader',
136 ],
133 }, 137 },
134 { 138 {
135 test: /\.(gif|png|jpe?g|svg|ico)$/i, 139 test: /\.(gif|png|jpe?g|svg|ico)$/i,
@@ -145,9 +149,12 @@ module.exports = [
145 }, 149 },
146 ], 150 ],
147 }, 151 },
152 optimization: {
153 minimize: true,
154 minimizer: [new TerserPlugin()],
155 },
148 plugins: [ 156 plugins: [
149 new MinifyPlugin(), 157 extractCss,
150 extractCssVintage,
151 ], 158 ],
152 }, 159 },
153]; 160];
diff --git a/yarn.lock b/yarn.lock
index 96f854c1..55bd9827 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,1015 +2,1317 @@
2# yarn lockfile v1 2# yarn lockfile v1
3 3
4 4
5abbrev@1: 5"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
6 version "1.1.1" 6 version "7.10.4"
7 resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 7 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
8 integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== 8 integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
9 dependencies:
10 "@babel/highlight" "^7.10.4"
11
12"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0":
13 version "7.11.0"
14 resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c"
15 integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==
16 dependencies:
17 browserslist "^4.12.0"
18 invariant "^2.2.4"
19 semver "^5.5.0"
20
21"@babel/core@>=7.9.0", "@babel/core@^7.11.6":
22 version "7.11.6"
23 resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651"
24 integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==
25 dependencies:
26 "@babel/code-frame" "^7.10.4"
27 "@babel/generator" "^7.11.6"
28 "@babel/helper-module-transforms" "^7.11.0"
29 "@babel/helpers" "^7.10.4"
30 "@babel/parser" "^7.11.5"
31 "@babel/template" "^7.10.4"
32 "@babel/traverse" "^7.11.5"
33 "@babel/types" "^7.11.5"
34 convert-source-map "^1.7.0"
35 debug "^4.1.0"
36 gensync "^1.0.0-beta.1"
37 json5 "^2.1.2"
38 lodash "^4.17.19"
39 resolve "^1.3.2"
40 semver "^5.4.1"
41 source-map "^0.5.0"
42
43"@babel/generator@^7.11.5", "@babel/generator@^7.11.6":
44 version "7.11.6"
45 resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620"
46 integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==
47 dependencies:
48 "@babel/types" "^7.11.5"
49 jsesc "^2.5.1"
50 source-map "^0.5.0"
51
52"@babel/helper-annotate-as-pure@^7.10.4":
53 version "7.10.4"
54 resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3"
55 integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==
56 dependencies:
57 "@babel/types" "^7.10.4"
58
59"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4":
60 version "7.10.4"
61 resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3"
62 integrity sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==
63 dependencies:
64 "@babel/helper-explode-assignable-expression" "^7.10.4"
65 "@babel/types" "^7.10.4"
66
67"@babel/helper-compilation-targets@^7.10.4":
68 version "7.10.4"
69 resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2"
70 integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==
71 dependencies:
72 "@babel/compat-data" "^7.10.4"
73 browserslist "^4.12.0"
74 invariant "^2.2.4"
75 levenary "^1.1.1"
76 semver "^5.5.0"
77
78"@babel/helper-create-class-features-plugin@^7.10.4":
79 version "7.10.5"
80 resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d"
81 integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==
82 dependencies:
83 "@babel/helper-function-name" "^7.10.4"
84 "@babel/helper-member-expression-to-functions" "^7.10.5"
85 "@babel/helper-optimise-call-expression" "^7.10.4"
86 "@babel/helper-plugin-utils" "^7.10.4"
87 "@babel/helper-replace-supers" "^7.10.4"
88 "@babel/helper-split-export-declaration" "^7.10.4"
89
90"@babel/helper-create-regexp-features-plugin@^7.10.4":
91 version "7.10.4"
92 resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8"
93 integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==
94 dependencies:
95 "@babel/helper-annotate-as-pure" "^7.10.4"
96 "@babel/helper-regex" "^7.10.4"
97 regexpu-core "^4.7.0"
98
99"@babel/helper-define-map@^7.10.4":
100 version "7.10.5"
101 resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30"
102 integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==
103 dependencies:
104 "@babel/helper-function-name" "^7.10.4"
105 "@babel/types" "^7.10.5"
106 lodash "^4.17.19"
107
108"@babel/helper-explode-assignable-expression@^7.10.4":
109 version "7.11.4"
110 resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz#2d8e3470252cc17aba917ede7803d4a7a276a41b"
111 integrity sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==
112 dependencies:
113 "@babel/types" "^7.10.4"
114
115"@babel/helper-function-name@^7.10.4":
116 version "7.10.4"
117 resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
118 integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
119 dependencies:
120 "@babel/helper-get-function-arity" "^7.10.4"
121 "@babel/template" "^7.10.4"
122 "@babel/types" "^7.10.4"
123
124"@babel/helper-get-function-arity@^7.10.4":
125 version "7.10.4"
126 resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
127 integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
128 dependencies:
129 "@babel/types" "^7.10.4"
130
131"@babel/helper-hoist-variables@^7.10.4":
132 version "7.10.4"
133 resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e"
134 integrity sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==
135 dependencies:
136 "@babel/types" "^7.10.4"
137
138"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5":
139 version "7.11.0"
140 resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df"
141 integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==
142 dependencies:
143 "@babel/types" "^7.11.0"
144
145"@babel/helper-module-imports@^7.10.4":
146 version "7.10.4"
147 resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620"
148 integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==
149 dependencies:
150 "@babel/types" "^7.10.4"
151
152"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0":
153 version "7.11.0"
154 resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359"
155 integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==
156 dependencies:
157 "@babel/helper-module-imports" "^7.10.4"
158 "@babel/helper-replace-supers" "^7.10.4"
159 "@babel/helper-simple-access" "^7.10.4"
160 "@babel/helper-split-export-declaration" "^7.11.0"
161 "@babel/template" "^7.10.4"
162 "@babel/types" "^7.11.0"
163 lodash "^4.17.19"
164
165"@babel/helper-optimise-call-expression@^7.10.4":
166 version "7.10.4"
167 resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673"
168 integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==
169 dependencies:
170 "@babel/types" "^7.10.4"
171
172"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
173 version "7.10.4"
174 resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
175 integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
176
177"@babel/helper-regex@^7.10.4":
178 version "7.10.5"
179 resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0"
180 integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==
181 dependencies:
182 lodash "^4.17.19"
183
184"@babel/helper-remap-async-to-generator@^7.10.4":
185 version "7.11.4"
186 resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz#4474ea9f7438f18575e30b0cac784045b402a12d"
187 integrity sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==
188 dependencies:
189 "@babel/helper-annotate-as-pure" "^7.10.4"
190 "@babel/helper-wrap-function" "^7.10.4"
191 "@babel/template" "^7.10.4"
192 "@babel/types" "^7.10.4"
193
194"@babel/helper-replace-supers@^7.10.4":
195 version "7.10.4"
196 resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf"
197 integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==
198 dependencies:
199 "@babel/helper-member-expression-to-functions" "^7.10.4"
200 "@babel/helper-optimise-call-expression" "^7.10.4"
201 "@babel/traverse" "^7.10.4"
202 "@babel/types" "^7.10.4"
203
204"@babel/helper-simple-access@^7.10.4":
205 version "7.10.4"
206 resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461"
207 integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==
208 dependencies:
209 "@babel/template" "^7.10.4"
210 "@babel/types" "^7.10.4"
211
212"@babel/helper-skip-transparent-expression-wrappers@^7.11.0":
213 version "7.11.0"
214 resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729"
215 integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==
216 dependencies:
217 "@babel/types" "^7.11.0"
218
219"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0":
220 version "7.11.0"
221 resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f"
222 integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==
223 dependencies:
224 "@babel/types" "^7.11.0"
225
226"@babel/helper-validator-identifier@^7.10.4":
227 version "7.10.4"
228 resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
229 integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
230
231"@babel/helper-wrap-function@^7.10.4":
232 version "7.10.4"
233 resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87"
234 integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==
235 dependencies:
236 "@babel/helper-function-name" "^7.10.4"
237 "@babel/template" "^7.10.4"
238 "@babel/traverse" "^7.10.4"
239 "@babel/types" "^7.10.4"
240
241"@babel/helpers@^7.10.4":
242 version "7.10.4"
243 resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044"
244 integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==
245 dependencies:
246 "@babel/template" "^7.10.4"
247 "@babel/traverse" "^7.10.4"
248 "@babel/types" "^7.10.4"
249
250"@babel/highlight@^7.10.4":
251 version "7.10.4"
252 resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143"
253 integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==
254 dependencies:
255 "@babel/helper-validator-identifier" "^7.10.4"
256 chalk "^2.0.0"
257 js-tokens "^4.0.0"
258
259"@babel/parser@^7.10.4", "@babel/parser@^7.11.5":
260 version "7.11.5"
261 resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
262 integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
9 263
10acorn-dynamic-import@^2.0.0: 264"@babel/plugin-proposal-async-generator-functions@^7.10.4":
11 version "2.0.2" 265 version "7.10.5"
12 resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" 266 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558"
13 integrity sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ= 267 integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==
268 dependencies:
269 "@babel/helper-plugin-utils" "^7.10.4"
270 "@babel/helper-remap-async-to-generator" "^7.10.4"
271 "@babel/plugin-syntax-async-generators" "^7.8.0"
272
273"@babel/plugin-proposal-class-properties@^7.10.4":
274 version "7.10.4"
275 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807"
276 integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==
14 dependencies: 277 dependencies:
15 acorn "^4.0.3" 278 "@babel/helper-create-class-features-plugin" "^7.10.4"
16 279 "@babel/helper-plugin-utils" "^7.10.4"
17acorn-jsx@^3.0.0: 280
18 version "3.0.1" 281"@babel/plugin-proposal-dynamic-import@^7.10.4":
19 resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" 282 version "7.10.4"
20 integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s= 283 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e"
284 integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==
285 dependencies:
286 "@babel/helper-plugin-utils" "^7.10.4"
287 "@babel/plugin-syntax-dynamic-import" "^7.8.0"
288
289"@babel/plugin-proposal-export-namespace-from@^7.10.4":
290 version "7.10.4"
291 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54"
292 integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==
21 dependencies: 293 dependencies:
22 acorn "^3.0.4" 294 "@babel/helper-plugin-utils" "^7.10.4"
295 "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
23 296
24acorn@^3.0.4: 297"@babel/plugin-proposal-json-strings@^7.10.4":
25 version "3.3.0" 298 version "7.10.4"
26 resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" 299 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db"
27 integrity sha1-ReN/s56No/JbruP/U2niu18iAXo= 300 integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==
301 dependencies:
302 "@babel/helper-plugin-utils" "^7.10.4"
303 "@babel/plugin-syntax-json-strings" "^7.8.0"
28 304
29acorn@^4.0.3: 305"@babel/plugin-proposal-logical-assignment-operators@^7.11.0":
30 version "4.0.13" 306 version "7.11.0"
31 resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" 307 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8"
32 integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= 308 integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q==
309 dependencies:
310 "@babel/helper-plugin-utils" "^7.10.4"
311 "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
33 312
34acorn@^5.0.0, acorn@^5.5.0: 313"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4":
35 version "5.7.3" 314 version "7.10.4"
36 resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" 315 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a"
37 integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== 316 integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==
317 dependencies:
318 "@babel/helper-plugin-utils" "^7.10.4"
319 "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
38 320
39ajv-keywords@^1.0.0: 321"@babel/plugin-proposal-numeric-separator@^7.10.4":
40 version "1.5.1" 322 version "7.10.4"
41 resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" 323 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06"
42 integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw= 324 integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==
325 dependencies:
326 "@babel/helper-plugin-utils" "^7.10.4"
327 "@babel/plugin-syntax-numeric-separator" "^7.10.4"
43 328
44ajv-keywords@^2.1.0: 329"@babel/plugin-proposal-object-rest-spread@^7.11.0":
45 version "2.1.1" 330 version "7.11.0"
46 resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" 331 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af"
47 integrity sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I= 332 integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==
333 dependencies:
334 "@babel/helper-plugin-utils" "^7.10.4"
335 "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
336 "@babel/plugin-transform-parameters" "^7.10.4"
48 337
49ajv-keywords@^3.1.0: 338"@babel/plugin-proposal-optional-catch-binding@^7.10.4":
50 version "3.4.0" 339 version "7.10.4"
51 resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d" 340 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd"
52 integrity sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw== 341 integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==
342 dependencies:
343 "@babel/helper-plugin-utils" "^7.10.4"
344 "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
53 345
54ajv@^4.7.0: 346"@babel/plugin-proposal-optional-chaining@^7.11.0":
55 version "4.11.8" 347 version "7.11.0"
56 resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" 348 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076"
57 integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY= 349 integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==
58 dependencies: 350 dependencies:
59 co "^4.6.0" 351 "@babel/helper-plugin-utils" "^7.10.4"
60 json-stable-stringify "^1.0.1" 352 "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0"
353 "@babel/plugin-syntax-optional-chaining" "^7.8.0"
61 354
62ajv@^5.0.0, ajv@^5.2.3, ajv@^5.3.0: 355"@babel/plugin-proposal-private-methods@^7.10.4":
63 version "5.5.2" 356 version "7.10.4"
64 resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" 357 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909"
65 integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= 358 integrity sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==
66 dependencies: 359 dependencies:
67 co "^4.6.0" 360 "@babel/helper-create-class-features-plugin" "^7.10.4"
68 fast-deep-equal "^1.0.0" 361 "@babel/helper-plugin-utils" "^7.10.4"
69 fast-json-stable-stringify "^2.0.0"
70 json-schema-traverse "^0.3.0"
71 362
72ajv@^6.1.0, ajv@^6.5.5: 363"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4":
73 version "6.10.0" 364 version "7.10.4"
74 resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" 365 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d"
75 integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== 366 integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==
76 dependencies: 367 dependencies:
77 fast-deep-equal "^2.0.1" 368 "@babel/helper-create-regexp-features-plugin" "^7.10.4"
78 fast-json-stable-stringify "^2.0.0" 369 "@babel/helper-plugin-utils" "^7.10.4"
79 json-schema-traverse "^0.4.1"
80 uri-js "^4.2.2"
81 370
82align-text@^0.1.1, align-text@^0.1.3: 371"@babel/plugin-syntax-async-generators@^7.8.0":
83 version "0.1.4" 372 version "7.8.4"
84 resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" 373 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
85 integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= 374 integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
86 dependencies: 375 dependencies:
87 kind-of "^3.0.2" 376 "@babel/helper-plugin-utils" "^7.8.0"
88 longest "^1.0.1"
89 repeat-string "^1.5.2"
90 377
91alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: 378"@babel/plugin-syntax-class-properties@^7.10.4":
92 version "1.0.2" 379 version "7.10.4"
93 resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" 380 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c"
94 integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= 381 integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==
382 dependencies:
383 "@babel/helper-plugin-utils" "^7.10.4"
95 384
96amdefine@>=0.0.4: 385"@babel/plugin-syntax-dynamic-import@^7.8.0":
97 version "1.0.1" 386 version "7.8.3"
98 resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" 387 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
99 integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= 388 integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
389 dependencies:
390 "@babel/helper-plugin-utils" "^7.8.0"
100 391
101ansi-escapes@^1.1.0: 392"@babel/plugin-syntax-export-namespace-from@^7.8.3":
102 version "1.4.0" 393 version "7.8.3"
103 resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" 394 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a"
104 integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= 395 integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==
396 dependencies:
397 "@babel/helper-plugin-utils" "^7.8.3"
105 398
106ansi-escapes@^3.0.0: 399"@babel/plugin-syntax-json-strings@^7.8.0":
107 version "3.2.0" 400 version "7.8.3"
108 resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" 401 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
109 integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== 402 integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
403 dependencies:
404 "@babel/helper-plugin-utils" "^7.8.0"
110 405
111ansi-regex@^2.0.0: 406"@babel/plugin-syntax-logical-assignment-operators@^7.10.4":
112 version "2.1.1" 407 version "7.10.4"
113 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 408 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
114 integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= 409 integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==
410 dependencies:
411 "@babel/helper-plugin-utils" "^7.10.4"
115 412
116ansi-regex@^3.0.0: 413"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
117 version "3.0.0" 414 version "7.8.3"
118 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 415 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
119 integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= 416 integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
417 dependencies:
418 "@babel/helper-plugin-utils" "^7.8.0"
120 419
121ansi-styles@^2.2.1: 420"@babel/plugin-syntax-numeric-separator@^7.10.4":
122 version "2.2.1" 421 version "7.10.4"
123 resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 422 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97"
124 integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= 423 integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==
424 dependencies:
425 "@babel/helper-plugin-utils" "^7.10.4"
125 426
126ansi-styles@^3.2.1: 427"@babel/plugin-syntax-object-rest-spread@^7.8.0":
127 version "3.2.1" 428 version "7.8.3"
128 resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 429 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
129 integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 430 integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
130 dependencies: 431 dependencies:
131 color-convert "^1.9.0" 432 "@babel/helper-plugin-utils" "^7.8.0"
132 433
133anymatch@^2.0.0: 434"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
134 version "2.0.0" 435 version "7.8.3"
135 resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" 436 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
136 integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== 437 integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
137 dependencies: 438 dependencies:
138 micromatch "^3.1.4" 439 "@babel/helper-plugin-utils" "^7.8.0"
139 normalize-path "^2.1.1"
140 440
141aproba@^1.0.3: 441"@babel/plugin-syntax-optional-chaining@^7.8.0":
142 version "1.2.0" 442 version "7.8.3"
143 resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" 443 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
144 integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== 444 integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
445 dependencies:
446 "@babel/helper-plugin-utils" "^7.8.0"
145 447
146are-we-there-yet@~1.1.2: 448"@babel/plugin-syntax-top-level-await@^7.10.4":
147 version "1.1.5" 449 version "7.10.4"
148 resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" 450 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d"
149 integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== 451 integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==
150 dependencies: 452 dependencies:
151 delegates "^1.0.0" 453 "@babel/helper-plugin-utils" "^7.10.4"
152 readable-stream "^2.0.6"
153 454
154argparse@^1.0.7: 455"@babel/plugin-transform-arrow-functions@^7.10.4":
155 version "1.0.10" 456 version "7.10.4"
156 resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" 457 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd"
157 integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== 458 integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==
158 dependencies: 459 dependencies:
159 sprintf-js "~1.0.2" 460 "@babel/helper-plugin-utils" "^7.10.4"
160 461
161arr-diff@^4.0.0: 462"@babel/plugin-transform-async-to-generator@^7.10.4":
162 version "4.0.0" 463 version "7.10.4"
163 resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" 464 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37"
164 integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= 465 integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==
466 dependencies:
467 "@babel/helper-module-imports" "^7.10.4"
468 "@babel/helper-plugin-utils" "^7.10.4"
469 "@babel/helper-remap-async-to-generator" "^7.10.4"
165 470
166arr-flatten@^1.1.0: 471"@babel/plugin-transform-block-scoped-functions@^7.10.4":
167 version "1.1.0" 472 version "7.10.4"
168 resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" 473 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8"
169 integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== 474 integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==
475 dependencies:
476 "@babel/helper-plugin-utils" "^7.10.4"
170 477
171arr-union@^3.1.0: 478"@babel/plugin-transform-block-scoping@^7.10.4":
172 version "3.1.0" 479 version "7.11.1"
173 resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" 480 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215"
174 integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= 481 integrity sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==
482 dependencies:
483 "@babel/helper-plugin-utils" "^7.10.4"
175 484
176array-find-index@^1.0.1: 485"@babel/plugin-transform-classes@^7.10.4":
177 version "1.0.2" 486 version "7.10.4"
178 resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" 487 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7"
179 integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= 488 integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==
489 dependencies:
490 "@babel/helper-annotate-as-pure" "^7.10.4"
491 "@babel/helper-define-map" "^7.10.4"
492 "@babel/helper-function-name" "^7.10.4"
493 "@babel/helper-optimise-call-expression" "^7.10.4"
494 "@babel/helper-plugin-utils" "^7.10.4"
495 "@babel/helper-replace-supers" "^7.10.4"
496 "@babel/helper-split-export-declaration" "^7.10.4"
497 globals "^11.1.0"
180 498
181array-includes@^3.0.3: 499"@babel/plugin-transform-computed-properties@^7.10.4":
182 version "3.0.3" 500 version "7.10.4"
183 resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" 501 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb"
184 integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= 502 integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==
185 dependencies: 503 dependencies:
186 define-properties "^1.1.2" 504 "@babel/helper-plugin-utils" "^7.10.4"
187 es-abstract "^1.7.0"
188 505
189array-unique@^0.3.2: 506"@babel/plugin-transform-destructuring@^7.10.4":
190 version "0.3.2" 507 version "7.10.4"
191 resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" 508 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5"
192 integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= 509 integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==
510 dependencies:
511 "@babel/helper-plugin-utils" "^7.10.4"
193 512
194asn1.js@^4.0.0: 513"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4":
195 version "4.10.1" 514 version "7.10.4"
196 resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" 515 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee"
197 integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== 516 integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==
198 dependencies: 517 dependencies:
199 bn.js "^4.0.0" 518 "@babel/helper-create-regexp-features-plugin" "^7.10.4"
200 inherits "^2.0.1" 519 "@babel/helper-plugin-utils" "^7.10.4"
201 minimalistic-assert "^1.0.0"
202 520
203asn1@~0.2.3: 521"@babel/plugin-transform-duplicate-keys@^7.10.4":
204 version "0.2.4" 522 version "7.10.4"
205 resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" 523 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47"
206 integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== 524 integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==
207 dependencies: 525 dependencies:
208 safer-buffer "~2.1.0" 526 "@babel/helper-plugin-utils" "^7.10.4"
209 527
210assert-plus@1.0.0, assert-plus@^1.0.0: 528"@babel/plugin-transform-exponentiation-operator@^7.10.4":
211 version "1.0.0" 529 version "7.10.4"
212 resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 530 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e"
213 integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= 531 integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==
532 dependencies:
533 "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4"
534 "@babel/helper-plugin-utils" "^7.10.4"
214 535
215assert@^1.1.1: 536"@babel/plugin-transform-for-of@^7.10.4":
216 version "1.5.0" 537 version "7.10.4"
217 resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" 538 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9"
218 integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== 539 integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==
219 dependencies: 540 dependencies:
220 object-assign "^4.1.1" 541 "@babel/helper-plugin-utils" "^7.10.4"
221 util "0.10.3"
222 542
223assign-symbols@^1.0.0: 543"@babel/plugin-transform-function-name@^7.10.4":
224 version "1.0.0" 544 version "7.10.4"
225 resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" 545 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7"
226 integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= 546 integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==
547 dependencies:
548 "@babel/helper-function-name" "^7.10.4"
549 "@babel/helper-plugin-utils" "^7.10.4"
227 550
228async-each@^1.0.1: 551"@babel/plugin-transform-literals@^7.10.4":
229 version "1.0.3" 552 version "7.10.4"
230 resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" 553 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c"
231 integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== 554 integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==
555 dependencies:
556 "@babel/helper-plugin-utils" "^7.10.4"
232 557
233async-foreach@^0.1.3: 558"@babel/plugin-transform-member-expression-literals@^7.10.4":
234 version "0.1.3" 559 version "7.10.4"
235 resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" 560 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7"
236 integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= 561 integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==
562 dependencies:
563 "@babel/helper-plugin-utils" "^7.10.4"
237 564
238async@^2.1.2, async@^2.4.1: 565"@babel/plugin-transform-modules-amd@^7.10.4":
239 version "2.6.2" 566 version "7.10.5"
240 resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" 567 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1"
241 integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== 568 integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==
242 dependencies: 569 dependencies:
243 lodash "^4.17.11" 570 "@babel/helper-module-transforms" "^7.10.5"
571 "@babel/helper-plugin-utils" "^7.10.4"
572 babel-plugin-dynamic-import-node "^2.3.3"
244 573
245asynckit@^0.4.0: 574"@babel/plugin-transform-modules-commonjs@^7.10.4":
246 version "0.4.0" 575 version "7.10.4"
247 resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 576 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0"
248 integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 577 integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==
578 dependencies:
579 "@babel/helper-module-transforms" "^7.10.4"
580 "@babel/helper-plugin-utils" "^7.10.4"
581 "@babel/helper-simple-access" "^7.10.4"
582 babel-plugin-dynamic-import-node "^2.3.3"
249 583
250atob@^2.1.1: 584"@babel/plugin-transform-modules-systemjs@^7.10.4":
251 version "2.1.2" 585 version "7.10.5"
252 resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" 586 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85"
253 integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== 587 integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==
588 dependencies:
589 "@babel/helper-hoist-variables" "^7.10.4"
590 "@babel/helper-module-transforms" "^7.10.5"
591 "@babel/helper-plugin-utils" "^7.10.4"
592 babel-plugin-dynamic-import-node "^2.3.3"
254 593
255autoprefixer@^6.3.1: 594"@babel/plugin-transform-modules-umd@^7.10.4":
256 version "6.7.7" 595 version "7.10.4"
257 resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" 596 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e"
258 integrity sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ= 597 integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==
259 dependencies: 598 dependencies:
260 browserslist "^1.7.6" 599 "@babel/helper-module-transforms" "^7.10.4"
261 caniuse-db "^1.0.30000634" 600 "@babel/helper-plugin-utils" "^7.10.4"
262 normalize-range "^0.1.2"
263 num2fraction "^1.2.2"
264 postcss "^5.2.16"
265 postcss-value-parser "^3.2.3"
266 601
267awesomplete@^1.1.2: 602"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4":
268 version "1.1.4" 603 version "7.10.4"
269 resolved "https://registry.yarnpkg.com/awesomplete/-/awesomplete-1.1.4.tgz#cdfcbbb2391857ff3a3340b5b1ebde7701b355e6" 604 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6"
270 integrity sha512-AgYrODNlVD3ZJ6Em54YesLnOSusuVCjoRAt0l5bi3L1Oiv5r5dkPdxVPJaG3/wnPlxRUmGcpGnK02VK7N02kCg== 605 integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==
606 dependencies:
607 "@babel/helper-create-regexp-features-plugin" "^7.10.4"
271 608
272aws-sign2@~0.7.0: 609"@babel/plugin-transform-new-target@^7.10.4":
273 version "0.7.0" 610 version "7.10.4"
274 resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 611 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888"
275 integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= 612 integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==
613 dependencies:
614 "@babel/helper-plugin-utils" "^7.10.4"
276 615
277aws4@^1.8.0: 616"@babel/plugin-transform-object-super@^7.10.4":
278 version "1.8.0" 617 version "7.10.4"
279 resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" 618 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894"
280 integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== 619 integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==
620 dependencies:
621 "@babel/helper-plugin-utils" "^7.10.4"
622 "@babel/helper-replace-supers" "^7.10.4"
623
624"@babel/plugin-transform-parameters@^7.10.4":
625 version "7.10.5"
626 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a"
627 integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==
628 dependencies:
629 "@babel/helper-get-function-arity" "^7.10.4"
630 "@babel/helper-plugin-utils" "^7.10.4"
631
632"@babel/plugin-transform-property-literals@^7.10.4":
633 version "7.10.4"
634 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0"
635 integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==
636 dependencies:
637 "@babel/helper-plugin-utils" "^7.10.4"
638
639"@babel/plugin-transform-regenerator@^7.10.4":
640 version "7.10.4"
641 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63"
642 integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==
643 dependencies:
644 regenerator-transform "^0.14.2"
645
646"@babel/plugin-transform-reserved-words@^7.10.4":
647 version "7.10.4"
648 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd"
649 integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==
650 dependencies:
651 "@babel/helper-plugin-utils" "^7.10.4"
652
653"@babel/plugin-transform-shorthand-properties@^7.10.4":
654 version "7.10.4"
655 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6"
656 integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==
657 dependencies:
658 "@babel/helper-plugin-utils" "^7.10.4"
659
660"@babel/plugin-transform-spread@^7.11.0":
661 version "7.11.0"
662 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc"
663 integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==
664 dependencies:
665 "@babel/helper-plugin-utils" "^7.10.4"
666 "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0"
667
668"@babel/plugin-transform-sticky-regex@^7.10.4":
669 version "7.10.4"
670 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d"
671 integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==
672 dependencies:
673 "@babel/helper-plugin-utils" "^7.10.4"
674 "@babel/helper-regex" "^7.10.4"
675
676"@babel/plugin-transform-template-literals@^7.10.4":
677 version "7.10.5"
678 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c"
679 integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==
680 dependencies:
681 "@babel/helper-annotate-as-pure" "^7.10.4"
682 "@babel/helper-plugin-utils" "^7.10.4"
683
684"@babel/plugin-transform-typeof-symbol@^7.10.4":
685 version "7.10.4"
686 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc"
687 integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==
688 dependencies:
689 "@babel/helper-plugin-utils" "^7.10.4"
690
691"@babel/plugin-transform-unicode-escapes@^7.10.4":
692 version "7.10.4"
693 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007"
694 integrity sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==
695 dependencies:
696 "@babel/helper-plugin-utils" "^7.10.4"
697
698"@babel/plugin-transform-unicode-regex@^7.10.4":
699 version "7.10.4"
700 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8"
701 integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==
702 dependencies:
703 "@babel/helper-create-regexp-features-plugin" "^7.10.4"
704 "@babel/helper-plugin-utils" "^7.10.4"
705
706"@babel/preset-env@^7.11.5":
707 version "7.11.5"
708 resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.5.tgz#18cb4b9379e3e92ffea92c07471a99a2914e4272"
709 integrity sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA==
710 dependencies:
711 "@babel/compat-data" "^7.11.0"
712 "@babel/helper-compilation-targets" "^7.10.4"
713 "@babel/helper-module-imports" "^7.10.4"
714 "@babel/helper-plugin-utils" "^7.10.4"
715 "@babel/plugin-proposal-async-generator-functions" "^7.10.4"
716 "@babel/plugin-proposal-class-properties" "^7.10.4"
717 "@babel/plugin-proposal-dynamic-import" "^7.10.4"
718 "@babel/plugin-proposal-export-namespace-from" "^7.10.4"
719 "@babel/plugin-proposal-json-strings" "^7.10.4"
720 "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0"
721 "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4"
722 "@babel/plugin-proposal-numeric-separator" "^7.10.4"
723 "@babel/plugin-proposal-object-rest-spread" "^7.11.0"
724 "@babel/plugin-proposal-optional-catch-binding" "^7.10.4"
725 "@babel/plugin-proposal-optional-chaining" "^7.11.0"
726 "@babel/plugin-proposal-private-methods" "^7.10.4"
727 "@babel/plugin-proposal-unicode-property-regex" "^7.10.4"
728 "@babel/plugin-syntax-async-generators" "^7.8.0"
729 "@babel/plugin-syntax-class-properties" "^7.10.4"
730 "@babel/plugin-syntax-dynamic-import" "^7.8.0"
731 "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
732 "@babel/plugin-syntax-json-strings" "^7.8.0"
733 "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
734 "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
735 "@babel/plugin-syntax-numeric-separator" "^7.10.4"
736 "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
737 "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
738 "@babel/plugin-syntax-optional-chaining" "^7.8.0"
739 "@babel/plugin-syntax-top-level-await" "^7.10.4"
740 "@babel/plugin-transform-arrow-functions" "^7.10.4"
741 "@babel/plugin-transform-async-to-generator" "^7.10.4"
742 "@babel/plugin-transform-block-scoped-functions" "^7.10.4"
743 "@babel/plugin-transform-block-scoping" "^7.10.4"
744 "@babel/plugin-transform-classes" "^7.10.4"
745 "@babel/plugin-transform-computed-properties" "^7.10.4"
746 "@babel/plugin-transform-destructuring" "^7.10.4"
747 "@babel/plugin-transform-dotall-regex" "^7.10.4"
748 "@babel/plugin-transform-duplicate-keys" "^7.10.4"
749 "@babel/plugin-transform-exponentiation-operator" "^7.10.4"
750 "@babel/plugin-transform-for-of" "^7.10.4"
751 "@babel/plugin-transform-function-name" "^7.10.4"
752 "@babel/plugin-transform-literals" "^7.10.4"
753 "@babel/plugin-transform-member-expression-literals" "^7.10.4"
754 "@babel/plugin-transform-modules-amd" "^7.10.4"
755 "@babel/plugin-transform-modules-commonjs" "^7.10.4"
756 "@babel/plugin-transform-modules-systemjs" "^7.10.4"
757 "@babel/plugin-transform-modules-umd" "^7.10.4"
758 "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4"
759 "@babel/plugin-transform-new-target" "^7.10.4"
760 "@babel/plugin-transform-object-super" "^7.10.4"
761 "@babel/plugin-transform-parameters" "^7.10.4"
762 "@babel/plugin-transform-property-literals" "^7.10.4"
763 "@babel/plugin-transform-regenerator" "^7.10.4"
764 "@babel/plugin-transform-reserved-words" "^7.10.4"
765 "@babel/plugin-transform-shorthand-properties" "^7.10.4"
766 "@babel/plugin-transform-spread" "^7.11.0"
767 "@babel/plugin-transform-sticky-regex" "^7.10.4"
768 "@babel/plugin-transform-template-literals" "^7.10.4"
769 "@babel/plugin-transform-typeof-symbol" "^7.10.4"
770 "@babel/plugin-transform-unicode-escapes" "^7.10.4"
771 "@babel/plugin-transform-unicode-regex" "^7.10.4"
772 "@babel/preset-modules" "^0.1.3"
773 "@babel/types" "^7.11.5"
774 browserslist "^4.12.0"
775 core-js-compat "^3.6.2"
776 invariant "^2.2.2"
777 levenary "^1.1.1"
778 semver "^5.5.0"
281 779
282babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: 780"@babel/preset-modules@^0.1.3":
283 version "6.26.0" 781 version "0.1.4"
284 resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" 782 resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e"
285 integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= 783 integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==
286 dependencies: 784 dependencies:
287 chalk "^1.1.3" 785 "@babel/helper-plugin-utils" "^7.0.0"
786 "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
787 "@babel/plugin-transform-dotall-regex" "^7.4.4"
788 "@babel/types" "^7.4.4"
288 esutils "^2.0.2" 789 esutils "^2.0.2"
289 js-tokens "^3.0.2" 790
290 791"@babel/runtime@^7.8.4":
291babel-core@^6.24.1, babel-core@^6.26.0: 792 version "7.11.2"
292 version "6.26.3" 793 resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
293 resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" 794 integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
294 integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== 795 dependencies:
295 dependencies: 796 regenerator-runtime "^0.13.4"
296 babel-code-frame "^6.26.0" 797
297 babel-generator "^6.26.0" 798"@babel/template@^7.10.4":
298 babel-helpers "^6.24.1" 799 version "7.10.4"
299 babel-messages "^6.23.0" 800 resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
300 babel-register "^6.26.0" 801 integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==
301 babel-runtime "^6.26.0" 802 dependencies:
302 babel-template "^6.26.0" 803 "@babel/code-frame" "^7.10.4"
303 babel-traverse "^6.26.0" 804 "@babel/parser" "^7.10.4"
304 babel-types "^6.26.0" 805 "@babel/types" "^7.10.4"
305 babylon "^6.18.0" 806
306 convert-source-map "^1.5.1" 807"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5":
307 debug "^2.6.9" 808 version "7.11.5"
308 json5 "^0.5.1" 809 resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3"
309 lodash "^4.17.4" 810 integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==
811 dependencies:
812 "@babel/code-frame" "^7.10.4"
813 "@babel/generator" "^7.11.5"
814 "@babel/helper-function-name" "^7.10.4"
815 "@babel/helper-split-export-declaration" "^7.11.0"
816 "@babel/parser" "^7.11.5"
817 "@babel/types" "^7.11.5"
818 debug "^4.1.0"
819 globals "^11.1.0"
820 lodash "^4.17.19"
821
822"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.4.4":
823 version "7.11.5"
824 resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
825 integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
826 dependencies:
827 "@babel/helper-validator-identifier" "^7.10.4"
828 lodash "^4.17.19"
829 to-fast-properties "^2.0.0"
830
831"@eslint/eslintrc@^0.1.3":
832 version "0.1.3"
833 resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085"
834 integrity sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==
835 dependencies:
836 ajv "^6.12.4"
837 debug "^4.1.1"
838 espree "^7.3.0"
839 globals "^12.1.0"
840 ignore "^4.0.6"
841 import-fresh "^3.2.1"
842 js-yaml "^3.13.1"
843 lodash "^4.17.19"
310 minimatch "^3.0.4" 844 minimatch "^3.0.4"
311 path-is-absolute "^1.0.1" 845 strip-json-comments "^3.1.1"
312 private "^0.1.8"
313 slash "^1.0.0"
314 source-map "^0.5.7"
315
316babel-generator@^6.26.0:
317 version "6.26.1"
318 resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
319 integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==
320 dependencies:
321 babel-messages "^6.23.0"
322 babel-runtime "^6.26.0"
323 babel-types "^6.26.0"
324 detect-indent "^4.0.0"
325 jsesc "^1.3.0"
326 lodash "^4.17.4"
327 source-map "^0.5.7"
328 trim-right "^1.0.1"
329
330babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
331 version "6.24.1"
332 resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
333 integrity sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=
334 dependencies:
335 babel-helper-explode-assignable-expression "^6.24.1"
336 babel-runtime "^6.22.0"
337 babel-types "^6.24.1"
338
339babel-helper-call-delegate@^6.24.1:
340 version "6.24.1"
341 resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
342 integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=
343 dependencies:
344 babel-helper-hoist-variables "^6.24.1"
345 babel-runtime "^6.22.0"
346 babel-traverse "^6.24.1"
347 babel-types "^6.24.1"
348
349babel-helper-define-map@^6.24.1:
350 version "6.26.0"
351 resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
352 integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=
353 dependencies:
354 babel-helper-function-name "^6.24.1"
355 babel-runtime "^6.26.0"
356 babel-types "^6.26.0"
357 lodash "^4.17.4"
358
359babel-helper-evaluate-path@^0.2.0:
360 version "0.2.0"
361 resolved "https://registry.yarnpkg.com/babel-helper-evaluate-path/-/babel-helper-evaluate-path-0.2.0.tgz#0bb2eb01996c0cef53c5e8405e999fe4a0244c08"
362 integrity sha512-0EK9TUKMxHL549hWDPkQoS7R0Ozg1CDLheVBHYds2B2qoAvmr9ejY3zOXFsrICK73TN7bPhU14PBeKc8jcBTwg==
363 846
364babel-helper-explode-assignable-expression@^6.24.1: 847"@nodelib/fs.scandir@2.1.3":
365 version "6.24.1" 848 version "2.1.3"
366 resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" 849 resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
367 integrity sha1-8luCz33BBDPFX3BZLVdGQArCLKo= 850 integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==
368 dependencies: 851 dependencies:
369 babel-runtime "^6.22.0" 852 "@nodelib/fs.stat" "2.0.3"
370 babel-traverse "^6.24.1" 853 run-parallel "^1.1.9"
371 babel-types "^6.24.1"
372 854
373babel-helper-flip-expressions@^0.2.0: 855"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
374 version "0.2.0" 856 version "2.0.3"
375 resolved "https://registry.yarnpkg.com/babel-helper-flip-expressions/-/babel-helper-flip-expressions-0.2.0.tgz#160d2090a3d9f9c64a750905321a0bc218f884ec" 857 resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3"
376 integrity sha512-rAsPA1pWBc7e2E6HepkP2e1sXugT+Oq/VCqhyuHJ8aJ2d/ifwnJfd4Qxjm21qlW43AN8tqaeByagKK6wECFMSw== 858 integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==
377 859
378babel-helper-function-name@^6.24.1: 860"@nodelib/fs.walk@^1.2.3":
379 version "6.24.1" 861 version "1.2.4"
380 resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" 862 resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976"
381 integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= 863 integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==
382 dependencies: 864 dependencies:
383 babel-helper-get-function-arity "^6.24.1" 865 "@nodelib/fs.scandir" "2.1.3"
384 babel-runtime "^6.22.0" 866 fastq "^1.6.0"
385 babel-template "^6.24.1"
386 babel-traverse "^6.24.1"
387 babel-types "^6.24.1"
388 867
389babel-helper-get-function-arity@^6.24.1: 868"@npmcli/move-file@^1.0.1":
390 version "6.24.1" 869 version "1.0.1"
391 resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" 870 resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464"
392 integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0= 871 integrity sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==
393 dependencies: 872 dependencies:
394 babel-runtime "^6.22.0" 873 mkdirp "^1.0.4"
395 babel-types "^6.24.1"
396 874
397babel-helper-hoist-variables@^6.24.1: 875"@stylelint/postcss-css-in-js@^0.37.2":
398 version "6.24.1" 876 version "0.37.2"
399 resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" 877 resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2"
400 integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY= 878 integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==
401 dependencies: 879 dependencies:
402 babel-runtime "^6.22.0" 880 "@babel/core" ">=7.9.0"
403 babel-types "^6.24.1"
404 881
405babel-helper-is-nodes-equiv@^0.0.1: 882"@stylelint/postcss-markdown@^0.36.1":
406 version "0.0.1" 883 version "0.36.1"
407 resolved "https://registry.yarnpkg.com/babel-helper-is-nodes-equiv/-/babel-helper-is-nodes-equiv-0.0.1.tgz#34e9b300b1479ddd98ec77ea0bbe9342dfe39684" 884 resolved "https://registry.yarnpkg.com/@stylelint/postcss-markdown/-/postcss-markdown-0.36.1.tgz#829b87e6c0f108014533d9d7b987dc9efb6632e8"
408 integrity sha1-NOmzALFHnd2Y7HfqC76TQt/jloQ= 885 integrity sha512-iDxMBWk9nB2BPi1VFQ+Dc5+XpvODBHw2n3tYpaBZuEAFQlbtF9If0Qh5LTTwSi/XwdbJ2jt+0dis3i8omyggpw==
886 dependencies:
887 remark "^12.0.0"
888 unist-util-find-all-after "^3.0.1"
409 889
410babel-helper-is-void-0@^0.2.0: 890"@types/color-name@^1.1.1":
411 version "0.2.0" 891 version "1.1.1"
412 resolved "https://registry.yarnpkg.com/babel-helper-is-void-0/-/babel-helper-is-void-0-0.2.0.tgz#6ed0ada8a9b1c5b6e88af6b47c1b3b5c080860eb" 892 resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
413 integrity sha512-Axj1AYuD0E3Dl7nT3KxROP7VekEofz3XtEljzURf3fABalLpr8PamtgLFt+zuxtaCxRf9iuZmbAMMYWri5Bazw== 893 integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
414 894
415babel-helper-mark-eval-scopes@^0.2.0: 895"@types/json-schema@^7.0.5":
416 version "0.2.0" 896 version "7.0.6"
417 resolved "https://registry.yarnpkg.com/babel-helper-mark-eval-scopes/-/babel-helper-mark-eval-scopes-0.2.0.tgz#7648aaf2ec92aae9b09a20ad91e8df5e1fcc94b2" 897 resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
418 integrity sha512-KJuwrOUcHbvbh6he4xRXZFLaivK9DF9o3CrvpWnK1Wp0B+1ANYABXBMgwrnNFIDK/AvicxQ9CNr8wsgivlp4Aw== 898 integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
419
420babel-helper-optimise-call-expression@^6.24.1:
421 version "6.24.1"
422 resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
423 integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=
424 dependencies:
425 babel-runtime "^6.22.0"
426 babel-types "^6.24.1"
427
428babel-helper-regex@^6.24.1:
429 version "6.26.0"
430 resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
431 integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=
432 dependencies:
433 babel-runtime "^6.26.0"
434 babel-types "^6.26.0"
435 lodash "^4.17.4"
436
437babel-helper-remap-async-to-generator@^6.24.1:
438 version "6.24.1"
439 resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
440 integrity sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=
441 dependencies:
442 babel-helper-function-name "^6.24.1"
443 babel-runtime "^6.22.0"
444 babel-template "^6.24.1"
445 babel-traverse "^6.24.1"
446 babel-types "^6.24.1"
447
448babel-helper-remove-or-void@^0.2.0:
449 version "0.2.0"
450 resolved "https://registry.yarnpkg.com/babel-helper-remove-or-void/-/babel-helper-remove-or-void-0.2.0.tgz#8e46ad5b30560d57d7510b3fd93f332ee7c67386"
451 integrity sha512-1Z41upf/XR+PwY7Nd+F15Jo5BiQi5205ZXUuKed3yoyQgDkMyoM7vAdjEJS/T+M6jy32sXjskMUgms4zeiVtRA==
452
453babel-helper-replace-supers@^6.24.1:
454 version "6.24.1"
455 resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
456 integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo=
457 dependencies:
458 babel-helper-optimise-call-expression "^6.24.1"
459 babel-messages "^6.23.0"
460 babel-runtime "^6.22.0"
461 babel-template "^6.24.1"
462 babel-traverse "^6.24.1"
463 babel-types "^6.24.1"
464
465babel-helper-to-multiple-sequence-expressions@^0.2.0:
466 version "0.2.0"
467 resolved "https://registry.yarnpkg.com/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.2.0.tgz#d1a419634c6cb301f27858c659167cfee0a9d318"
468 integrity sha512-ij9lpfdP3+Zc/7kNwa+NXbTrUlsYEWPwt/ugmQO0qflzLrveTIkbfOqQztvitk81aG5NblYDQXDlRohzu3oa8Q==
469 899
470babel-helpers@^6.24.1: 900"@types/json5@^0.0.29":
471 version "6.24.1" 901 version "0.0.29"
472 resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" 902 resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
473 integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= 903 integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
474 dependencies:
475 babel-runtime "^6.22.0"
476 babel-template "^6.24.1"
477 904
478babel-loader@^7.1.2: 905"@types/minimist@^1.2.0":
479 version "7.1.5" 906 version "1.2.0"
480 resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68" 907 resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
481 integrity sha512-iCHfbieL5d1LfOQeeVJEUyD9rTwBcP/fcEbRCfempxTDuqrKpu0AZjLAQHEQa3Yqyj9ORKe2iHfoj4rHLf7xpw== 908 integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
482 dependencies:
483 find-cache-dir "^1.0.0"
484 loader-utils "^1.0.2"
485 mkdirp "^0.5.1"
486 909
487babel-messages@^6.23.0: 910"@types/node@*":
488 version "6.23.0" 911 version "14.11.2"
489 resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" 912 resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
490 integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= 913 integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==
491 dependencies:
492 babel-runtime "^6.22.0"
493 914
494babel-minify-webpack-plugin@^0.2.0: 915"@types/normalize-package-data@^2.4.0":
495 version "0.2.0" 916 version "2.4.0"
496 resolved "https://registry.yarnpkg.com/babel-minify-webpack-plugin/-/babel-minify-webpack-plugin-0.2.0.tgz#ef9694d11a1b8ab8f3204d89f5c9278dd28fc2a9" 917 resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
497 integrity sha512-+5G5Qqm+DIVl7gY4rkHqlFRkaf1FZtz0imzu/Dy9+88AfOIuy7D5MQjkNgQr5gU6/YSZ+rImgxDqFcWkvvrjkQ== 918 integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
498 dependencies:
499 babel-core "^6.24.1"
500 babel-preset-minify "^0.2.0"
501 webpack-sources "^1.0.1"
502 919
503babel-plugin-check-es2015-constants@^6.22.0: 920"@types/parse-json@^4.0.0":
504 version "6.22.0" 921 version "4.0.0"
505 resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" 922 resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
506 integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o= 923 integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
924
925"@types/unist@^2.0.0", "@types/unist@^2.0.2":
926 version "2.0.3"
927 resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
928 integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
929
930"@webassemblyjs/ast@1.9.0":
931 version "1.9.0"
932 resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
933 integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==
934 dependencies:
935 "@webassemblyjs/helper-module-context" "1.9.0"
936 "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
937 "@webassemblyjs/wast-parser" "1.9.0"
938
939"@webassemblyjs/floating-point-hex-parser@1.9.0":
940 version "1.9.0"
941 resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4"
942 integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==
943
944"@webassemblyjs/helper-api-error@1.9.0":
945 version "1.9.0"
946 resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2"
947 integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==
948
949"@webassemblyjs/helper-buffer@1.9.0":
950 version "1.9.0"
951 resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00"
952 integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==
953
954"@webassemblyjs/helper-code-frame@1.9.0":
955 version "1.9.0"
956 resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27"
957 integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==
958 dependencies:
959 "@webassemblyjs/wast-printer" "1.9.0"
960
961"@webassemblyjs/helper-fsm@1.9.0":
962 version "1.9.0"
963 resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8"
964 integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==
965
966"@webassemblyjs/helper-module-context@1.9.0":
967 version "1.9.0"
968 resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07"
969 integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==
970 dependencies:
971 "@webassemblyjs/ast" "1.9.0"
972
973"@webassemblyjs/helper-wasm-bytecode@1.9.0":
974 version "1.9.0"
975 resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790"
976 integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==
977
978"@webassemblyjs/helper-wasm-section@1.9.0":
979 version "1.9.0"
980 resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346"
981 integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==
982 dependencies:
983 "@webassemblyjs/ast" "1.9.0"
984 "@webassemblyjs/helper-buffer" "1.9.0"
985 "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
986 "@webassemblyjs/wasm-gen" "1.9.0"
987
988"@webassemblyjs/ieee754@1.9.0":
989 version "1.9.0"
990 resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4"
991 integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==
992 dependencies:
993 "@xtuc/ieee754" "^1.2.0"
994
995"@webassemblyjs/leb128@1.9.0":
996 version "1.9.0"
997 resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95"
998 integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==
999 dependencies:
1000 "@xtuc/long" "4.2.2"
1001
1002"@webassemblyjs/utf8@1.9.0":
1003 version "1.9.0"
1004 resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab"
1005 integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==
1006
1007"@webassemblyjs/wasm-edit@1.9.0":
1008 version "1.9.0"
1009 resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf"
1010 integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==
1011 dependencies:
1012 "@webassemblyjs/ast" "1.9.0"
1013 "@webassemblyjs/helper-buffer" "1.9.0"
1014 "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
1015 "@webassemblyjs/helper-wasm-section" "1.9.0"
1016 "@webassemblyjs/wasm-gen" "1.9.0"
1017 "@webassemblyjs/wasm-opt" "1.9.0"
1018 "@webassemblyjs/wasm-parser" "1.9.0"
1019 "@webassemblyjs/wast-printer" "1.9.0"
1020
1021"@webassemblyjs/wasm-gen@1.9.0":
1022 version "1.9.0"
1023 resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c"
1024 integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==
1025 dependencies:
1026 "@webassemblyjs/ast" "1.9.0"
1027 "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
1028 "@webassemblyjs/ieee754" "1.9.0"
1029 "@webassemblyjs/leb128" "1.9.0"
1030 "@webassemblyjs/utf8" "1.9.0"
1031
1032"@webassemblyjs/wasm-opt@1.9.0":
1033 version "1.9.0"
1034 resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61"
1035 integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==
1036 dependencies:
1037 "@webassemblyjs/ast" "1.9.0"
1038 "@webassemblyjs/helper-buffer" "1.9.0"
1039 "@webassemblyjs/wasm-gen" "1.9.0"
1040 "@webassemblyjs/wasm-parser" "1.9.0"
1041
1042"@webassemblyjs/wasm-parser@1.9.0":
1043 version "1.9.0"
1044 resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e"
1045 integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==
1046 dependencies:
1047 "@webassemblyjs/ast" "1.9.0"
1048 "@webassemblyjs/helper-api-error" "1.9.0"
1049 "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
1050 "@webassemblyjs/ieee754" "1.9.0"
1051 "@webassemblyjs/leb128" "1.9.0"
1052 "@webassemblyjs/utf8" "1.9.0"
1053
1054"@webassemblyjs/wast-parser@1.9.0":
1055 version "1.9.0"
1056 resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914"
1057 integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==
1058 dependencies:
1059 "@webassemblyjs/ast" "1.9.0"
1060 "@webassemblyjs/floating-point-hex-parser" "1.9.0"
1061 "@webassemblyjs/helper-api-error" "1.9.0"
1062 "@webassemblyjs/helper-code-frame" "1.9.0"
1063 "@webassemblyjs/helper-fsm" "1.9.0"
1064 "@xtuc/long" "4.2.2"
1065
1066"@webassemblyjs/wast-printer@1.9.0":
1067 version "1.9.0"
1068 resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899"
1069 integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==
1070 dependencies:
1071 "@webassemblyjs/ast" "1.9.0"
1072 "@webassemblyjs/wast-parser" "1.9.0"
1073 "@xtuc/long" "4.2.2"
1074
1075"@xtuc/ieee754@^1.2.0":
1076 version "1.2.0"
1077 resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
1078 integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
1079
1080"@xtuc/long@4.2.2":
1081 version "4.2.2"
1082 resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
1083 integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
1084
1085acorn-jsx@^5.2.0:
1086 version "5.3.1"
1087 resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
1088 integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
1089
1090acorn@^6.4.1:
1091 version "6.4.1"
1092 resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
1093 integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
1094
1095acorn@^7.4.0:
1096 version "7.4.0"
1097 resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c"
1098 integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==
1099
1100aggregate-error@^3.0.0:
1101 version "3.1.0"
1102 resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
1103 integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
507 dependencies: 1104 dependencies:
508 babel-runtime "^6.22.0" 1105 clean-stack "^2.0.0"
1106 indent-string "^4.0.0"
509 1107
510babel-plugin-minify-builtins@^0.2.0: 1108ajv-errors@^1.0.0:
511 version "0.2.0" 1109 version "1.0.1"
512 resolved "https://registry.yarnpkg.com/babel-plugin-minify-builtins/-/babel-plugin-minify-builtins-0.2.0.tgz#317f824b0907210b6348671bb040ca072e2e0c82" 1110 resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
513 integrity sha512-4i+8ntaS8gwVUcOz5y+zE+55OVOl2nTbmHV51D4wAIiKcRI8U5K//ip1GHfhsgk/NJrrHK7h97Oy5jpqt0Iixg== 1111 integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
1112
1113ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
1114 version "3.5.2"
1115 resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
1116 integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
1117
1118ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4:
1119 version "6.12.5"
1120 resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
1121 integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
514 dependencies: 1122 dependencies:
515 babel-helper-evaluate-path "^0.2.0" 1123 fast-deep-equal "^3.1.1"
1124 fast-json-stable-stringify "^2.0.0"
1125 json-schema-traverse "^0.4.1"
1126 uri-js "^4.2.2"
516 1127
517babel-plugin-minify-constant-folding@^0.2.0: 1128ansi-colors@^4.1.1:
518 version "0.2.0" 1129 version "4.1.1"
519 resolved "https://registry.yarnpkg.com/babel-plugin-minify-constant-folding/-/babel-plugin-minify-constant-folding-0.2.0.tgz#8c70b528b2eb7c13e94d95c8789077d4cdbc3970" 1130 resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
520 integrity sha512-B3ffQBEUQ8ydlIkYv2MkZtTCbV7FAkWAV7NkyhcXlGpD10PaCxNGQ/B9oguXGowR1m16Q5nGhvNn8Pkn1MO6Hw== 1131 integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
1132
1133ansi-regex@^4.1.0:
1134 version "4.1.0"
1135 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
1136 integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
1137
1138ansi-regex@^5.0.0:
1139 version "5.0.0"
1140 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
1141 integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
1142
1143ansi-styles@^3.2.0, ansi-styles@^3.2.1:
1144 version "3.2.1"
1145 resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
1146 integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
521 dependencies: 1147 dependencies:
522 babel-helper-evaluate-path "^0.2.0" 1148 color-convert "^1.9.0"
523 1149
524babel-plugin-minify-dead-code-elimination@^0.2.0: 1150ansi-styles@^4.0.0, ansi-styles@^4.1.0:
525 version "0.2.0" 1151 version "4.2.1"
526 resolved "https://registry.yarnpkg.com/babel-plugin-minify-dead-code-elimination/-/babel-plugin-minify-dead-code-elimination-0.2.0.tgz#e8025ee10a1e5e4f202633a6928ce892c33747e3" 1152 resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
527 integrity sha512-zE7y3pRyzA4zK5nBou0kTcwUTSQ/AiFrynt1cIEYN7vcO2gS9ZFZoI0aO9JYLUdct5fsC1vfB35408yrzTyVfg== 1153 integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
528 dependencies: 1154 dependencies:
529 babel-helper-evaluate-path "^0.2.0" 1155 "@types/color-name" "^1.1.1"
530 babel-helper-mark-eval-scopes "^0.2.0" 1156 color-convert "^2.0.1"
531 babel-helper-remove-or-void "^0.2.0"
532 lodash.some "^4.6.0"
533 1157
534babel-plugin-minify-flip-comparisons@^0.2.0: 1158anymatch@^2.0.0:
535 version "0.2.0" 1159 version "2.0.0"
536 resolved "https://registry.yarnpkg.com/babel-plugin-minify-flip-comparisons/-/babel-plugin-minify-flip-comparisons-0.2.0.tgz#0c9c8e93155c8f09dedad8118b634c259f709ef5" 1160 resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
537 integrity sha512-QOqXSEmD/LhT3LpM1WCyzAGcQZYYKJF7oOHvS6QbpomHenydrV53DMdPX2mK01icBExKZcJAHF209wvDBa+CSg== 1161 integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
538 dependencies: 1162 dependencies:
539 babel-helper-is-void-0 "^0.2.0" 1163 micromatch "^3.1.4"
1164 normalize-path "^2.1.1"
540 1165
541babel-plugin-minify-guarded-expressions@^0.2.0: 1166anymatch@~3.1.1:
542 version "0.2.0" 1167 version "3.1.1"
543 resolved "https://registry.yarnpkg.com/babel-plugin-minify-guarded-expressions/-/babel-plugin-minify-guarded-expressions-0.2.0.tgz#8a8c950040fce3e258a12e6eb21eab94ad7235ab" 1168 resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
544 integrity sha512-5+NSPdRQ9mnrHaA+zFj+D5OzmSiv90EX5zGH6cWQgR/OUqmCHSDqgTRPFvOctgpo8MJyO7Rt7ajs2UfLnlAwYg== 1169 integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
545 dependencies: 1170 dependencies:
546 babel-helper-flip-expressions "^0.2.0" 1171 normalize-path "^3.0.0"
1172 picomatch "^2.0.4"
547 1173
548babel-plugin-minify-infinity@^0.2.0: 1174aproba@^1.1.1:
549 version "0.2.0" 1175 version "1.2.0"
550 resolved "https://registry.yarnpkg.com/babel-plugin-minify-infinity/-/babel-plugin-minify-infinity-0.2.0.tgz#30960c615ddbc657c045bb00a1d8eb4af257cf03" 1176 resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
551 integrity sha512-U694vrla1lN6vDHWGrR832t3a/A2eh+kyl019LxEE2+sS4VTydyOPRsAOIYAdJegWRA4cMX1lm9azAN0cLIr8g== 1177 integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
552 1178
553babel-plugin-minify-mangle-names@^0.2.0: 1179argparse@^1.0.7:
554 version "0.2.0" 1180 version "1.0.10"
555 resolved "https://registry.yarnpkg.com/babel-plugin-minify-mangle-names/-/babel-plugin-minify-mangle-names-0.2.0.tgz#719892297ff0106a6ec1a4b0fc062f1f8b6a8529" 1181 resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
556 integrity sha512-Gixuak1/CO7VCdjn15/8Bxe/QsAtDG4zPbnsNoe1mIJGCIH/kcmSjFhMlGJtXDQZd6EKzeMfA5WmX9+jvGRefw== 1182 integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
557 dependencies: 1183 dependencies:
558 babel-helper-mark-eval-scopes "^0.2.0" 1184 sprintf-js "~1.0.2"
559 1185
560babel-plugin-minify-numeric-literals@^0.2.0: 1186arr-diff@^4.0.0:
561 version "0.2.0" 1187 version "4.0.0"
562 resolved "https://registry.yarnpkg.com/babel-plugin-minify-numeric-literals/-/babel-plugin-minify-numeric-literals-0.2.0.tgz#5746e851700167a380c05e93f289a7070459a0d1" 1188 resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
563 integrity sha512-VcLpb+r1YS7+RIOXdRsFVLLqoh22177USpHf+JM/g1nZbzdqENmfd5v534MLAbRErhbz6SyK+NQViVzVtBxu8g== 1189 integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
564 1190
565babel-plugin-minify-replace@^0.2.0: 1191arr-flatten@^1.1.0:
566 version "0.2.0" 1192 version "1.1.0"
567 resolved "https://registry.yarnpkg.com/babel-plugin-minify-replace/-/babel-plugin-minify-replace-0.2.0.tgz#3c1f06bc4e6d3e301eacb763edc1be611efc39b0" 1193 resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
568 integrity sha512-SEW6zoSVxh3OH6E1LCgyhhTWMnCv+JIRu5h5IlJDA11tU4ZeSF7uPQcO4vN/o52+FssRB26dmzJ/8D+z0QPg5Q== 1194 integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
569 1195
570babel-plugin-minify-simplify@^0.2.0: 1196arr-union@^3.1.0:
571 version "0.2.0" 1197 version "3.1.0"
572 resolved "https://registry.yarnpkg.com/babel-plugin-minify-simplify/-/babel-plugin-minify-simplify-0.2.0.tgz#21ceec4857100c5476d7cef121f351156e5c9bc0" 1198 resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
573 integrity sha512-Mj3Mwy2zVosMfXDWXZrQH5/uMAyfJdmDQ1NVqit+ArbHC3LlXVzptuyC1JxTyai/wgFvjLaichm/7vSUshkWqw== 1199 integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
1200
1201array-includes@^3.1.1:
1202 version "3.1.1"
1203 resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
1204 integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==
574 dependencies: 1205 dependencies:
575 babel-helper-flip-expressions "^0.2.0" 1206 define-properties "^1.1.3"
576 babel-helper-is-nodes-equiv "^0.0.1" 1207 es-abstract "^1.17.0"
577 babel-helper-to-multiple-sequence-expressions "^0.2.0" 1208 is-string "^1.0.5"
578 1209
579babel-plugin-minify-type-constructors@^0.2.0: 1210array-union@^2.1.0:
580 version "0.2.0" 1211 version "2.1.0"
581 resolved "https://registry.yarnpkg.com/babel-plugin-minify-type-constructors/-/babel-plugin-minify-type-constructors-0.2.0.tgz#7f3b6458be0863cfd59e9985bed6d134aa7a2e17" 1212 resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
582 integrity sha512-NiOvvA9Pq6bki6nP4BayXwT5GZadw7DJFDDzHmkpnOQpENWe8RtHtKZM44MG1R6EQ5XxgbLdsdhswIzTkFlO5g== 1213 integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
583 dependencies:
584 babel-helper-is-void-0 "^0.2.0"
585
586babel-plugin-syntax-async-functions@^6.8.0:
587 version "6.13.0"
588 resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
589 integrity sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=
590
591babel-plugin-syntax-exponentiation-operator@^6.8.0:
592 version "6.13.0"
593 resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
594 integrity sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=
595
596babel-plugin-syntax-trailing-function-commas@^6.22.0:
597 version "6.22.0"
598 resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
599 integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=
600
601babel-plugin-transform-async-to-generator@^6.22.0:
602 version "6.24.1"
603 resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
604 integrity sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=
605 dependencies:
606 babel-helper-remap-async-to-generator "^6.24.1"
607 babel-plugin-syntax-async-functions "^6.8.0"
608 babel-runtime "^6.22.0"
609
610babel-plugin-transform-es2015-arrow-functions@^6.22.0:
611 version "6.22.0"
612 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
613 integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=
614 dependencies:
615 babel-runtime "^6.22.0"
616
617babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
618 version "6.22.0"
619 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
620 integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE=
621 dependencies:
622 babel-runtime "^6.22.0"
623
624babel-plugin-transform-es2015-block-scoping@^6.23.0:
625 version "6.26.0"
626 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
627 integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=
628 dependencies:
629 babel-runtime "^6.26.0"
630 babel-template "^6.26.0"
631 babel-traverse "^6.26.0"
632 babel-types "^6.26.0"
633 lodash "^4.17.4"
634
635babel-plugin-transform-es2015-classes@^6.23.0:
636 version "6.24.1"
637 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
638 integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=
639 dependencies:
640 babel-helper-define-map "^6.24.1"
641 babel-helper-function-name "^6.24.1"
642 babel-helper-optimise-call-expression "^6.24.1"
643 babel-helper-replace-supers "^6.24.1"
644 babel-messages "^6.23.0"
645 babel-runtime "^6.22.0"
646 babel-template "^6.24.1"
647 babel-traverse "^6.24.1"
648 babel-types "^6.24.1"
649
650babel-plugin-transform-es2015-computed-properties@^6.22.0:
651 version "6.24.1"
652 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
653 integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=
654 dependencies:
655 babel-runtime "^6.22.0"
656 babel-template "^6.24.1"
657
658babel-plugin-transform-es2015-destructuring@^6.23.0:
659 version "6.23.0"
660 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
661 integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=
662 dependencies:
663 babel-runtime "^6.22.0"
664
665babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
666 version "6.24.1"
667 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
668 integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4=
669 dependencies:
670 babel-runtime "^6.22.0"
671 babel-types "^6.24.1"
672
673babel-plugin-transform-es2015-for-of@^6.23.0:
674 version "6.23.0"
675 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
676 integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=
677 dependencies:
678 babel-runtime "^6.22.0"
679
680babel-plugin-transform-es2015-function-name@^6.22.0:
681 version "6.24.1"
682 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
683 integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=
684 dependencies:
685 babel-helper-function-name "^6.24.1"
686 babel-runtime "^6.22.0"
687 babel-types "^6.24.1"
688
689babel-plugin-transform-es2015-literals@^6.22.0:
690 version "6.22.0"
691 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
692 integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=
693 dependencies:
694 babel-runtime "^6.22.0"
695
696babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
697 version "6.24.1"
698 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
699 integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=
700 dependencies:
701 babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
702 babel-runtime "^6.22.0"
703 babel-template "^6.24.1"
704
705babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
706 version "6.26.2"
707 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
708 integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==
709 dependencies:
710 babel-plugin-transform-strict-mode "^6.24.1"
711 babel-runtime "^6.26.0"
712 babel-template "^6.26.0"
713 babel-types "^6.26.0"
714
715babel-plugin-transform-es2015-modules-systemjs@^6.23.0:
716 version "6.24.1"
717 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
718 integrity sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=
719 dependencies:
720 babel-helper-hoist-variables "^6.24.1"
721 babel-runtime "^6.22.0"
722 babel-template "^6.24.1"
723
724babel-plugin-transform-es2015-modules-umd@^6.23.0:
725 version "6.24.1"
726 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
727 integrity sha1-rJl+YoXNGO1hdq22B9YCNErThGg=
728 dependencies:
729 babel-plugin-transform-es2015-modules-amd "^6.24.1"
730 babel-runtime "^6.22.0"
731 babel-template "^6.24.1"
732
733babel-plugin-transform-es2015-object-super@^6.22.0:
734 version "6.24.1"
735 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
736 integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40=
737 dependencies:
738 babel-helper-replace-supers "^6.24.1"
739 babel-runtime "^6.22.0"
740
741babel-plugin-transform-es2015-parameters@^6.23.0:
742 version "6.24.1"
743 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
744 integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=
745 dependencies:
746 babel-helper-call-delegate "^6.24.1"
747 babel-helper-get-function-arity "^6.24.1"
748 babel-runtime "^6.22.0"
749 babel-template "^6.24.1"
750 babel-traverse "^6.24.1"
751 babel-types "^6.24.1"
752
753babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
754 version "6.24.1"
755 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
756 integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=
757 dependencies:
758 babel-runtime "^6.22.0"
759 babel-types "^6.24.1"
760
761babel-plugin-transform-es2015-spread@^6.22.0:
762 version "6.22.0"
763 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
764 integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE=
765 dependencies:
766 babel-runtime "^6.22.0"
767
768babel-plugin-transform-es2015-sticky-regex@^6.22.0:
769 version "6.24.1"
770 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
771 integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw=
772 dependencies:
773 babel-helper-regex "^6.24.1"
774 babel-runtime "^6.22.0"
775 babel-types "^6.24.1"
776
777babel-plugin-transform-es2015-template-literals@^6.22.0:
778 version "6.22.0"
779 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
780 integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=
781 dependencies:
782 babel-runtime "^6.22.0"
783
784babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
785 version "6.23.0"
786 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
787 integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=
788 dependencies:
789 babel-runtime "^6.22.0"
790
791babel-plugin-transform-es2015-unicode-regex@^6.22.0:
792 version "6.24.1"
793 resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
794 integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek=
795 dependencies:
796 babel-helper-regex "^6.24.1"
797 babel-runtime "^6.22.0"
798 regexpu-core "^2.0.0"
799
800babel-plugin-transform-exponentiation-operator@^6.22.0:
801 version "6.24.1"
802 resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
803 integrity sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=
804 dependencies:
805 babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
806 babel-plugin-syntax-exponentiation-operator "^6.8.0"
807 babel-runtime "^6.22.0"
808
809babel-plugin-transform-inline-consecutive-adds@^0.2.0:
810 version "0.2.0"
811 resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.2.0.tgz#15dae78921057f4004f8eafd79e15ddc5f12f426"
812 integrity sha512-GlhOuLOQ28ua9prg0hT33HslCrEmz9xWXy9ZNZSACppCyRxxRW+haYtRgm7uYXCcd0q8ggCWD2pfWEJp5iiZfQ==
813 1214
814babel-plugin-transform-member-expression-literals@^6.8.5: 1215array-unique@^0.3.2:
815 version "6.9.4" 1216 version "0.3.2"
816 resolved "https://registry.yarnpkg.com/babel-plugin-transform-member-expression-literals/-/babel-plugin-transform-member-expression-literals-6.9.4.tgz#37039c9a0c3313a39495faac2ff3a6b5b9d038bf" 1217 resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
817 integrity sha1-NwOcmgwzE6OUlfqsL/OmtbnQOL8= 1218 integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
818 1219
819babel-plugin-transform-merge-sibling-variables@^6.8.6: 1220array.prototype.flat@^1.2.3:
820 version "6.9.4" 1221 version "1.2.3"
821 resolved "https://registry.yarnpkg.com/babel-plugin-transform-merge-sibling-variables/-/babel-plugin-transform-merge-sibling-variables-6.9.4.tgz#85b422fc3377b449c9d1cde44087203532401dae" 1222 resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
822 integrity sha1-hbQi/DN3tEnJ0c3kQIcgNTJAHa4= 1223 integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
1224 dependencies:
1225 define-properties "^1.1.3"
1226 es-abstract "^1.17.0-next.1"
823 1227
824babel-plugin-transform-minify-booleans@^6.8.3: 1228arrify@^1.0.1:
825 version "6.9.4" 1229 version "1.0.1"
826 resolved "https://registry.yarnpkg.com/babel-plugin-transform-minify-booleans/-/babel-plugin-transform-minify-booleans-6.9.4.tgz#acbb3e56a3555dd23928e4b582d285162dd2b198" 1230 resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
827 integrity sha1-rLs+VqNVXdI5KOS1gtKFFi3SsZg= 1231 integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
828 1232
829babel-plugin-transform-property-literals@^6.8.5: 1233asn1.js@^5.2.0:
830 version "6.9.4" 1234 version "5.4.1"
831 resolved "https://registry.yarnpkg.com/babel-plugin-transform-property-literals/-/babel-plugin-transform-property-literals-6.9.4.tgz#98c1d21e255736573f93ece54459f6ce24985d39" 1235 resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
832 integrity sha1-mMHSHiVXNlc/k+zlRFn2ziSYXTk= 1236 integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
833 dependencies: 1237 dependencies:
834 esutils "^2.0.2" 1238 bn.js "^4.0.0"
1239 inherits "^2.0.1"
1240 minimalistic-assert "^1.0.0"
1241 safer-buffer "^2.1.0"
835 1242
836babel-plugin-transform-regenerator@^6.22.0: 1243assert@^1.1.1:
837 version "6.26.0" 1244 version "1.5.0"
838 resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" 1245 resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
839 integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8= 1246 integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==
840 dependencies: 1247 dependencies:
841 regenerator-transform "^0.10.0" 1248 object-assign "^4.1.1"
1249 util "0.10.3"
842 1250
843babel-plugin-transform-regexp-constructors@^0.2.0: 1251assign-symbols@^1.0.0:
844 version "0.2.0" 1252 version "1.0.0"
845 resolved "https://registry.yarnpkg.com/babel-plugin-transform-regexp-constructors/-/babel-plugin-transform-regexp-constructors-0.2.0.tgz#6aa5dd0acc515db4be929bbcec4ed4c946c534a3" 1253 resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
846 integrity sha512-7IsQ6aQx6LAaOqy97/PthTf+5Nx9grZww3r6E62IdWe76Yr8KsuwVjxzqSPQvESJqTE3EMADQ9S0RtwWDGNG9Q== 1254 integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
847 1255
848babel-plugin-transform-remove-console@^6.8.5: 1256astral-regex@^1.0.0:
849 version "6.9.4" 1257 version "1.0.0"
850 resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz#b980360c067384e24b357a588d807d3c83527780" 1258 resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
851 integrity sha1-uYA2DAZzhOJLNXpYjYB9PINSd4A= 1259 integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
852 1260
853babel-plugin-transform-remove-debugger@^6.8.5: 1261astral-regex@^2.0.0:
854 version "6.9.4" 1262 version "2.0.0"
855 resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-debugger/-/babel-plugin-transform-remove-debugger-6.9.4.tgz#42b727631c97978e1eb2d199a7aec84a18339ef2" 1263 resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
856 integrity sha1-QrcnYxyXl44estGZp67IShgznvI= 1264 integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
857 1265
858babel-plugin-transform-remove-undefined@^0.2.0: 1266async-each@^1.0.1:
859 version "0.2.0" 1267 version "1.0.3"
860 resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-undefined/-/babel-plugin-transform-remove-undefined-0.2.0.tgz#94f052062054c707e8d094acefe79416b63452b1" 1268 resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
861 integrity sha512-O8v57tPMHkp89kA4ZfQEYds/pzgvz/QYerBJjIuL5/Jc7RnvMVRA5gJY9zFKP7WayW8WOSBV4vh8Y8FJRio+ow== 1269 integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
862 dependencies:
863 babel-helper-evaluate-path "^0.2.0"
864 1270
865babel-plugin-transform-simplify-comparison-operators@^6.8.5: 1271atob@^2.1.2:
866 version "6.9.4" 1272 version "2.1.2"
867 resolved "https://registry.yarnpkg.com/babel-plugin-transform-simplify-comparison-operators/-/babel-plugin-transform-simplify-comparison-operators-6.9.4.tgz#f62afe096cab0e1f68a2d753fdf283888471ceb9" 1273 resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
868 integrity sha1-9ir+CWyrDh9ootdT/fKDiIRxzrk= 1274 integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
869 1275
870babel-plugin-transform-strict-mode@^6.24.1: 1276autoprefixer@^9.8.6:
871 version "6.24.1" 1277 version "9.8.6"
872 resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" 1278 resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
873 integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= 1279 integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==
874 dependencies: 1280 dependencies:
875 babel-runtime "^6.22.0" 1281 browserslist "^4.12.0"
876 babel-types "^6.24.1" 1282 caniuse-lite "^1.0.30001109"
877 1283 colorette "^1.2.1"
878babel-plugin-transform-undefined-to-void@^6.8.3: 1284 normalize-range "^0.1.2"
879 version "6.9.4" 1285 num2fraction "^1.2.2"
880 resolved "https://registry.yarnpkg.com/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz#be241ca81404030678b748717322b89d0c8fe280" 1286 postcss "^7.0.32"
881 integrity sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA= 1287 postcss-value-parser "^4.1.0"
882
883babel-preset-env@^1.6.1:
884 version "1.7.0"
885 resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a"
886 integrity sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==
887 dependencies:
888 babel-plugin-check-es2015-constants "^6.22.0"
889 babel-plugin-syntax-trailing-function-commas "^6.22.0"
890 babel-plugin-transform-async-to-generator "^6.22.0"
891 babel-plugin-transform-es2015-arrow-functions "^6.22.0"
892 babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
893 babel-plugin-transform-es2015-block-scoping "^6.23.0"
894 babel-plugin-transform-es2015-classes "^6.23.0"
895 babel-plugin-transform-es2015-computed-properties "^6.22.0"
896 babel-plugin-transform-es2015-destructuring "^6.23.0"
897 babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
898 babel-plugin-transform-es2015-for-of "^6.23.0"
899 babel-plugin-transform-es2015-function-name "^6.22.0"
900 babel-plugin-transform-es2015-literals "^6.22.0"
901 babel-plugin-transform-es2015-modules-amd "^6.22.0"
902 babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
903 babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
904 babel-plugin-transform-es2015-modules-umd "^6.23.0"
905 babel-plugin-transform-es2015-object-super "^6.22.0"
906 babel-plugin-transform-es2015-parameters "^6.23.0"
907 babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
908 babel-plugin-transform-es2015-spread "^6.22.0"
909 babel-plugin-transform-es2015-sticky-regex "^6.22.0"
910 babel-plugin-transform-es2015-template-literals "^6.22.0"
911 babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
912 babel-plugin-transform-es2015-unicode-regex "^6.22.0"
913 babel-plugin-transform-exponentiation-operator "^6.22.0"
914 babel-plugin-transform-regenerator "^6.22.0"
915 browserslist "^3.2.6"
916 invariant "^2.2.2"
917 semver "^5.3.0"
918 1288
919babel-preset-minify@^0.2.0: 1289awesomplete@^1.1.2:
920 version "0.2.0" 1290 version "1.1.5"
921 resolved "https://registry.yarnpkg.com/babel-preset-minify/-/babel-preset-minify-0.2.0.tgz#006566552d9b83834472273f306c0131062a0acc" 1291 resolved "https://registry.yarnpkg.com/awesomplete/-/awesomplete-1.1.5.tgz#1b2b5dd106d3955595619c03da472a1dc0faf0af"
922 integrity sha512-mR8Q44RmMzm18bM2Lqd9uiPopzk5GDCtVuquNbLFmX6lOKnqWoenaNBxnWW0UhBFC75lEHTIgNGCbnsRI0pJVw== 1292 integrity sha512-UFw1mPW8NaSECDSTC36HbAOTpF9JK2wBUJcNn4MSvlNtK7SZ9N72gB+ajHtA6D1abYXRcszZnBA4nHBwvFwzHw==
923 dependencies:
924 babel-plugin-minify-builtins "^0.2.0"
925 babel-plugin-minify-constant-folding "^0.2.0"
926 babel-plugin-minify-dead-code-elimination "^0.2.0"
927 babel-plugin-minify-flip-comparisons "^0.2.0"
928 babel-plugin-minify-guarded-expressions "^0.2.0"
929 babel-plugin-minify-infinity "^0.2.0"
930 babel-plugin-minify-mangle-names "^0.2.0"
931 babel-plugin-minify-numeric-literals "^0.2.0"
932 babel-plugin-minify-replace "^0.2.0"
933 babel-plugin-minify-simplify "^0.2.0"
934 babel-plugin-minify-type-constructors "^0.2.0"
935 babel-plugin-transform-inline-consecutive-adds "^0.2.0"
936 babel-plugin-transform-member-expression-literals "^6.8.5"
937 babel-plugin-transform-merge-sibling-variables "^6.8.6"
938 babel-plugin-transform-minify-booleans "^6.8.3"
939 babel-plugin-transform-property-literals "^6.8.5"
940 babel-plugin-transform-regexp-constructors "^0.2.0"
941 babel-plugin-transform-remove-console "^6.8.5"
942 babel-plugin-transform-remove-debugger "^6.8.5"
943 babel-plugin-transform-remove-undefined "^0.2.0"
944 babel-plugin-transform-simplify-comparison-operators "^6.8.5"
945 babel-plugin-transform-undefined-to-void "^6.8.3"
946 lodash.isplainobject "^4.0.6"
947
948babel-register@^6.26.0:
949 version "6.26.0"
950 resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
951 integrity sha1-btAhFz4vy0htestFxgCahW9kcHE=
952 dependencies:
953 babel-core "^6.26.0"
954 babel-runtime "^6.26.0"
955 core-js "^2.5.0"
956 home-or-tmp "^2.0.0"
957 lodash "^4.17.4"
958 mkdirp "^0.5.1"
959 source-map-support "^0.4.15"
960
961babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
962 version "6.26.0"
963 resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
964 integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
965 dependencies:
966 core-js "^2.4.0"
967 regenerator-runtime "^0.11.0"
968
969babel-template@^6.24.1, babel-template@^6.26.0:
970 version "6.26.0"
971 resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
972 integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=
973 dependencies:
974 babel-runtime "^6.26.0"
975 babel-traverse "^6.26.0"
976 babel-types "^6.26.0"
977 babylon "^6.18.0"
978 lodash "^4.17.4"
979
980babel-traverse@^6.24.1, babel-traverse@^6.26.0:
981 version "6.26.0"
982 resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
983 integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=
984 dependencies:
985 babel-code-frame "^6.26.0"
986 babel-messages "^6.23.0"
987 babel-runtime "^6.26.0"
988 babel-types "^6.26.0"
989 babylon "^6.18.0"
990 debug "^2.6.8"
991 globals "^9.18.0"
992 invariant "^2.2.2"
993 lodash "^4.17.4"
994 1293
995babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: 1294babel-loader@^8.1.0:
996 version "6.26.0" 1295 version "8.1.0"
997 resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" 1296 resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3"
998 integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= 1297 integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==
999 dependencies: 1298 dependencies:
1000 babel-runtime "^6.26.0" 1299 find-cache-dir "^2.1.0"
1001 esutils "^2.0.2" 1300 loader-utils "^1.4.0"
1002 lodash "^4.17.4" 1301 mkdirp "^0.5.3"
1003 to-fast-properties "^1.0.3" 1302 pify "^4.0.1"
1303 schema-utils "^2.6.5"
1004 1304
1005babylon@^6.18.0: 1305babel-plugin-dynamic-import-node@^2.3.3:
1006 version "6.18.0" 1306 version "2.3.3"
1007 resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" 1307 resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
1008 integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== 1308 integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==
1309 dependencies:
1310 object.assign "^4.1.0"
1009 1311
1010balanced-match@^0.4.2: 1312bail@^1.0.0:
1011 version "0.4.2" 1313 version "1.0.5"
1012 resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" 1314 resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
1013 integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg= 1315 integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
1014 1316
1015balanced-match@^1.0.0: 1317balanced-match@^1.0.0:
1016 version "1.0.0" 1318 version "1.0.0"
@@ -1018,9 +1320,9 @@ balanced-match@^1.0.0:
1018 integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 1320 integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
1019 1321
1020base64-js@^1.0.2: 1322base64-js@^1.0.2:
1021 version "1.3.0" 1323 version "1.3.1"
1022 resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" 1324 resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
1023 integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw== 1325 integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
1024 1326
1025base@^0.11.1: 1327base@^0.11.1:
1026 version "0.11.2" 1328 version "0.11.2"
@@ -1035,13 +1337,6 @@ base@^0.11.1:
1035 mixin-deep "^1.2.0" 1337 mixin-deep "^1.2.0"
1036 pascalcase "^0.1.1" 1338 pascalcase "^0.1.1"
1037 1339
1038bcrypt-pbkdf@^1.0.0:
1039 version "1.0.2"
1040 resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
1041 integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
1042 dependencies:
1043 tweetnacl "^0.14.3"
1044
1045big.js@^5.2.2: 1340big.js@^5.2.2:
1046 version "5.2.2" 1341 version "5.2.2"
1047 resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" 1342 resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
@@ -1052,22 +1347,37 @@ binary-extensions@^1.0.0:
1052 resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" 1347 resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
1053 integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== 1348 integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
1054 1349
1350binary-extensions@^2.0.0:
1351 version "2.1.0"
1352 resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9"
1353 integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==
1354
1355bindings@^1.5.0:
1356 version "1.5.0"
1357 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
1358 integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
1359 dependencies:
1360 file-uri-to-path "1.0.0"
1361
1055blazy@^1.8.2: 1362blazy@^1.8.2:
1056 version "1.8.2" 1363 version "1.8.2"
1057 resolved "https://registry.yarnpkg.com/blazy/-/blazy-1.8.2.tgz#50dfd638baaf9003efd6eb3a836aca54184ab6da" 1364 resolved "https://registry.yarnpkg.com/blazy/-/blazy-1.8.2.tgz#50dfd638baaf9003efd6eb3a836aca54184ab6da"
1058 integrity sha1-UN/WOLqvkAPv1us6g2rKVBhKtto= 1365 integrity sha1-UN/WOLqvkAPv1us6g2rKVBhKtto=
1059 1366
1060block-stream@*: 1367bluebird@^3.5.5:
1061 version "0.0.9" 1368 version "3.7.2"
1062 resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" 1369 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
1063 integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= 1370 integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
1064 dependencies: 1371
1065 inherits "~2.0.0" 1372bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
1373 version "4.11.9"
1374 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
1375 integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
1066 1376
1067bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: 1377bn.js@^5.1.1:
1068 version "4.11.8" 1378 version "5.1.3"
1069 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" 1379 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
1070 integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== 1380 integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
1071 1381
1072brace-expansion@^1.1.7: 1382brace-expansion@^1.1.7:
1073 version "1.1.11" 1383 version "1.1.11"
@@ -1093,6 +1403,13 @@ braces@^2.3.1, braces@^2.3.2:
1093 split-string "^3.0.2" 1403 split-string "^3.0.2"
1094 to-regex "^3.0.1" 1404 to-regex "^3.0.1"
1095 1405
1406braces@^3.0.1, braces@~3.0.2:
1407 version "3.0.2"
1408 resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
1409 integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
1410 dependencies:
1411 fill-range "^7.0.1"
1412
1096brorand@^1.0.1: 1413brorand@^1.0.1:
1097 version "1.1.0" 1414 version "1.1.0"
1098 resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" 1415 resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -1129,7 +1446,7 @@ browserify-des@^1.0.0:
1129 inherits "^2.0.1" 1446 inherits "^2.0.1"
1130 safe-buffer "^5.1.2" 1447 safe-buffer "^5.1.2"
1131 1448
1132browserify-rsa@^4.0.0: 1449browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
1133 version "4.0.1" 1450 version "4.0.1"
1134 resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" 1451 resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
1135 integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= 1452 integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
@@ -1138,17 +1455,19 @@ browserify-rsa@^4.0.0:
1138 randombytes "^2.0.1" 1455 randombytes "^2.0.1"
1139 1456
1140browserify-sign@^4.0.0: 1457browserify-sign@^4.0.0:
1141 version "4.0.4" 1458 version "4.2.1"
1142 resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" 1459 resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3"
1143 integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg= 1460 integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==
1144 dependencies: 1461 dependencies:
1145 bn.js "^4.1.1" 1462 bn.js "^5.1.1"
1146 browserify-rsa "^4.0.0" 1463 browserify-rsa "^4.0.1"
1147 create-hash "^1.1.0" 1464 create-hash "^1.2.0"
1148 create-hmac "^1.1.2" 1465 create-hmac "^1.1.7"
1149 elliptic "^6.0.0" 1466 elliptic "^6.5.3"
1150 inherits "^2.0.1" 1467 inherits "^2.0.4"
1151 parse-asn1 "^5.0.0" 1468 parse-asn1 "^5.1.5"
1469 readable-stream "^3.6.0"
1470 safe-buffer "^5.2.0"
1152 1471
1153browserify-zlib@^0.2.0: 1472browserify-zlib@^0.2.0:
1154 version "0.2.0" 1473 version "0.2.0"
@@ -1157,21 +1476,15 @@ browserify-zlib@^0.2.0:
1157 dependencies: 1476 dependencies:
1158 pako "~1.0.5" 1477 pako "~1.0.5"
1159 1478
1160browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: 1479browserslist@^4.12.0, browserslist@^4.8.5:
1161 version "1.7.7" 1480 version "4.14.3"
1162 resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" 1481 resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.3.tgz#381f9e7f13794b2eb17e1761b4f118e8ae665a53"
1163 integrity sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk= 1482 integrity sha512-GcZPC5+YqyPO4SFnz48/B0YaCwS47Q9iPChRGi6t7HhflKBcINzFrJvRfC+jp30sRMKxF+d4EHGs27Z0XP1NaQ==
1164 dependencies: 1483 dependencies:
1165 caniuse-db "^1.0.30000639" 1484 caniuse-lite "^1.0.30001131"
1166 electron-to-chromium "^1.2.7" 1485 electron-to-chromium "^1.3.570"
1167 1486 escalade "^3.1.0"
1168browserslist@^3.2.6: 1487 node-releases "^1.1.61"
1169 version "3.2.8"
1170 resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6"
1171 integrity sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==
1172 dependencies:
1173 caniuse-lite "^1.0.30000844"
1174 electron-to-chromium "^1.3.47"
1175 1488
1176buffer-from@^1.0.0: 1489buffer-from@^1.0.0:
1177 version "1.1.1" 1490 version "1.1.1"
@@ -1184,9 +1497,9 @@ buffer-xor@^1.0.3:
1184 integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= 1497 integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
1185 1498
1186buffer@^4.3.0: 1499buffer@^4.3.0:
1187 version "4.9.1" 1500 version "4.9.2"
1188 resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" 1501 resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
1189 integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= 1502 integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
1190 dependencies: 1503 dependencies:
1191 base64-js "^1.0.2" 1504 base64-js "^1.0.2"
1192 ieee754 "^1.1.4" 1505 ieee754 "^1.1.4"
@@ -1197,6 +1510,50 @@ builtin-status-codes@^3.0.0:
1197 resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" 1510 resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
1198 integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= 1511 integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
1199 1512
1513cacache@^12.0.2:
1514 version "12.0.4"
1515 resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c"
1516 integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==
1517 dependencies:
1518 bluebird "^3.5.5"
1519 chownr "^1.1.1"
1520 figgy-pudding "^3.5.1"
1521 glob "^7.1.4"
1522 graceful-fs "^4.1.15"
1523 infer-owner "^1.0.3"
1524 lru-cache "^5.1.1"
1525 mississippi "^3.0.0"
1526 mkdirp "^0.5.1"
1527 move-concurrently "^1.0.1"
1528 promise-inflight "^1.0.1"
1529 rimraf "^2.6.3"
1530 ssri "^6.0.1"
1531 unique-filename "^1.1.1"
1532 y18n "^4.0.0"
1533
1534cacache@^15.0.5:
1535 version "15.0.5"
1536 resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0"
1537 integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==
1538 dependencies:
1539 "@npmcli/move-file" "^1.0.1"
1540 chownr "^2.0.0"
1541 fs-minipass "^2.0.0"
1542 glob "^7.1.4"
1543 infer-owner "^1.0.4"
1544 lru-cache "^6.0.0"
1545 minipass "^3.1.1"
1546 minipass-collect "^1.0.2"
1547 minipass-flush "^1.0.5"
1548 minipass-pipeline "^1.2.2"
1549 mkdirp "^1.0.3"
1550 p-map "^4.0.0"
1551 promise-inflight "^1.0.1"
1552 rimraf "^3.0.2"
1553 ssri "^8.0.0"
1554 tar "^6.0.2"
1555 unique-filename "^1.1.1"
1556
1200cache-base@^1.0.1: 1557cache-base@^1.0.1:
1201 version "1.0.1" 1558 version "1.0.1"
1202 resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" 1559 resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -1212,91 +1569,41 @@ cache-base@^1.0.1:
1212 union-value "^1.0.0" 1569 union-value "^1.0.0"
1213 unset-value "^1.0.0" 1570 unset-value "^1.0.0"
1214 1571
1215caller-path@^0.1.0: 1572callsites@^3.0.0:
1216 version "0.1.0" 1573 version "3.1.0"
1217 resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" 1574 resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
1218 integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8= 1575 integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
1219 dependencies:
1220 callsites "^0.2.0"
1221
1222callsites@^0.2.0:
1223 version "0.2.0"
1224 resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
1225 integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=
1226 1576
1227camelcase-keys@^2.0.0: 1577camelcase-keys@^6.2.2:
1228 version "2.1.0" 1578 version "6.2.2"
1229 resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" 1579 resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
1230 integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= 1580 integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
1231 dependencies: 1581 dependencies:
1232 camelcase "^2.0.0" 1582 camelcase "^5.3.1"
1233 map-obj "^1.0.0" 1583 map-obj "^4.0.0"
1234 1584 quick-lru "^4.0.1"
1235camelcase@^1.0.2:
1236 version "1.2.1"
1237 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
1238 integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=
1239 1585
1240camelcase@^2.0.0: 1586camelcase@^5.0.0, camelcase@^5.3.1:
1241 version "2.1.1" 1587 version "5.3.1"
1242 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" 1588 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
1243 integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= 1589 integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
1244 1590
1245camelcase@^3.0.0: 1591camelcase@^6.0.0:
1246 version "3.0.0" 1592 version "6.0.0"
1247 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" 1593 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e"
1248 integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= 1594 integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
1249 1595
1250camelcase@^4.1.0: 1596caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001131:
1251 version "4.1.0" 1597 version "1.0.30001135"
1252 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" 1598 resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001135.tgz#995b1eb94404a3c9a0d7600c113c9bb27f2cd8aa"
1253 integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= 1599 integrity sha512-ziNcheTGTHlu9g34EVoHQdIu5g4foc8EsxMGC7Xkokmvw0dqNtX8BS8RgCgFBaAiSp2IdjvBxNdh0ssib28eVQ==
1254 1600
1255caniuse-api@^1.5.2: 1601ccount@^1.0.0:
1256 version "1.6.1" 1602 version "1.0.5"
1257 resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" 1603 resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17"
1258 integrity sha1-tTTnxzTE+B7F++isoq0kNUuWLGw= 1604 integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==
1259 dependencies:
1260 browserslist "^1.3.6"
1261 caniuse-db "^1.0.30000529"
1262 lodash.memoize "^4.1.2"
1263 lodash.uniq "^4.5.0"
1264
1265caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
1266 version "1.0.30000969"
1267 resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000969.tgz#e6aeca9b1bac88865990913a0b041f587180cd59"
1268 integrity sha512-ttrmwpIXvEL/kg0JSg6Q+xEbMxAEcjZOOgZMGPcMe5JMYgi20Nvs9bqMRGfyIOQtd1jYa6yRWODIR6apj3xPQw==
1269
1270caniuse-lite@^1.0.30000844:
1271 version "1.0.30000969"
1272 resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000969.tgz#7664f571f2072657bde70b00a1fc1ba41f1942a9"
1273 integrity sha512-Kus0yxkoAJgVc0bax7S4gLSlFifCa7MnSZL9p9VuS/HIKEL4seaqh28KIQAAO50cD/rJ5CiJkJFapkdDAlhFxQ==
1274
1275caseless@~0.12.0:
1276 version "0.12.0"
1277 resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
1278 integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
1279
1280center-align@^0.1.1:
1281 version "0.1.3"
1282 resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
1283 integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60=
1284 dependencies:
1285 align-text "^0.1.3"
1286 lazy-cache "^1.0.3"
1287
1288chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
1289 version "1.1.3"
1290 resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
1291 integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
1292 dependencies:
1293 ansi-styles "^2.2.1"
1294 escape-string-regexp "^1.0.2"
1295 has-ansi "^2.0.0"
1296 strip-ansi "^3.0.0"
1297 supports-color "^2.0.0"
1298 1605
1299chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1: 1606chalk@^2.0.0, chalk@^2.4.2:
1300 version "2.4.2" 1607 version "2.4.2"
1301 resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 1608 resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
1302 integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 1609 integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -1305,15 +1612,53 @@ chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1:
1305 escape-string-regexp "^1.0.5" 1612 escape-string-regexp "^1.0.5"
1306 supports-color "^5.3.0" 1613 supports-color "^5.3.0"
1307 1614
1308chardet@^0.4.0: 1615chalk@^4.0.0, chalk@^4.1.0:
1309 version "0.4.2" 1616 version "4.1.0"
1310 resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" 1617 resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
1311 integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I= 1618 integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
1619 dependencies:
1620 ansi-styles "^4.1.0"
1621 supports-color "^7.1.0"
1622
1623character-entities-html4@^1.0.0:
1624 version "1.1.4"
1625 resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125"
1626 integrity sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==
1627
1628character-entities-legacy@^1.0.0:
1629 version "1.1.4"
1630 resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1"
1631 integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==
1312 1632
1313chokidar@^2.0.2: 1633character-entities@^1.0.0:
1314 version "2.1.6" 1634 version "1.2.4"
1315 resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" 1635 resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
1316 integrity sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g== 1636 integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
1637
1638character-reference-invalid@^1.0.0:
1639 version "1.1.4"
1640 resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
1641 integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
1642
1643"chokidar@>=2.0.0 <4.0.0", chokidar@^3.4.1:
1644 version "3.4.2"
1645 resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d"
1646 integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==
1647 dependencies:
1648 anymatch "~3.1.1"
1649 braces "~3.0.2"
1650 glob-parent "~5.1.0"
1651 is-binary-path "~2.1.0"
1652 is-glob "~4.0.1"
1653 normalize-path "~3.0.0"
1654 readdirp "~3.4.0"
1655 optionalDependencies:
1656 fsevents "~2.1.2"
1657
1658chokidar@^2.1.8:
1659 version "2.1.8"
1660 resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
1661 integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
1317 dependencies: 1662 dependencies:
1318 anymatch "^2.0.0" 1663 anymatch "^2.0.0"
1319 async-each "^1.0.1" 1664 async-each "^1.0.1"
@@ -1330,9 +1675,21 @@ chokidar@^2.0.2:
1330 fsevents "^1.2.7" 1675 fsevents "^1.2.7"
1331 1676
1332chownr@^1.1.1: 1677chownr@^1.1.1:
1333 version "1.1.1" 1678 version "1.1.4"
1334 resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" 1679 resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
1335 integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== 1680 integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
1681
1682chownr@^2.0.0:
1683 version "2.0.0"
1684 resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
1685 integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
1686
1687chrome-trace-event@^1.0.2:
1688 version "1.0.2"
1689 resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
1690 integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==
1691 dependencies:
1692 tslib "^1.9.0"
1336 1693
1337cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: 1694cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
1338 version "1.0.4" 1695 version "1.0.4"
@@ -1342,18 +1699,6 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
1342 inherits "^2.0.1" 1699 inherits "^2.0.1"
1343 safe-buffer "^5.0.1" 1700 safe-buffer "^5.0.1"
1344 1701
1345circular-json@^0.3.1:
1346 version "0.3.3"
1347 resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
1348 integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
1349
1350clap@^1.0.9:
1351 version "1.2.3"
1352 resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51"
1353 integrity sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==
1354 dependencies:
1355 chalk "^1.1.3"
1356
1357class-utils@^0.3.5: 1702class-utils@^0.3.5:
1358 version "0.3.6" 1703 version "0.3.6"
1359 resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" 1704 resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
@@ -1364,74 +1709,31 @@ class-utils@^0.3.5:
1364 isobject "^3.0.0" 1709 isobject "^3.0.0"
1365 static-extend "^0.1.1" 1710 static-extend "^0.1.1"
1366 1711
1367cli-cursor@^1.0.1: 1712clean-stack@^2.0.0:
1368 version "1.0.2"
1369 resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
1370 integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=
1371 dependencies:
1372 restore-cursor "^1.0.1"
1373
1374cli-cursor@^2.1.0:
1375 version "2.1.0"
1376 resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
1377 integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
1378 dependencies:
1379 restore-cursor "^2.0.0"
1380
1381cli-width@^2.0.0:
1382 version "2.2.0" 1713 version "2.2.0"
1383 resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" 1714 resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
1384 integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= 1715 integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
1385
1386cliui@^2.1.0:
1387 version "2.1.0"
1388 resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
1389 integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=
1390 dependencies:
1391 center-align "^0.1.1"
1392 right-align "^0.1.1"
1393 wordwrap "0.0.2"
1394
1395cliui@^3.2.0:
1396 version "3.2.0"
1397 resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
1398 integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=
1399 dependencies:
1400 string-width "^1.0.1"
1401 strip-ansi "^3.0.1"
1402 wrap-ansi "^2.0.0"
1403 1716
1404clone-deep@^2.0.1: 1717cliui@^5.0.0:
1405 version "2.0.2" 1718 version "5.0.0"
1406 resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" 1719 resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
1407 integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ== 1720 integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
1408 dependencies: 1721 dependencies:
1409 for-own "^1.0.0" 1722 string-width "^3.1.0"
1410 is-plain-object "^2.0.4" 1723 strip-ansi "^5.2.0"
1411 kind-of "^6.0.0" 1724 wrap-ansi "^5.1.0"
1412 shallow-clone "^1.0.0"
1413
1414clone@^1.0.2:
1415 version "1.0.4"
1416 resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
1417 integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
1418
1419co@^4.6.0:
1420 version "4.6.0"
1421 resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
1422 integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
1423 1725
1424coa@~1.0.1: 1726clone-regexp@^2.1.0:
1425 version "1.0.4" 1727 version "2.2.0"
1426 resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" 1728 resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
1427 integrity sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0= 1729 integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==
1428 dependencies: 1730 dependencies:
1429 q "^1.1.2" 1731 is-regexp "^2.0.0"
1430 1732
1431code-point-at@^1.0.0: 1733collapse-white-space@^1.0.2:
1432 version "1.1.0" 1734 version "1.0.6"
1433 resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 1735 resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287"
1434 integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= 1736 integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==
1435 1737
1436collection-visit@^1.0.0: 1738collection-visit@^1.0.0:
1437 version "1.0.0" 1739 version "1.0.0"
@@ -1441,64 +1743,39 @@ collection-visit@^1.0.0:
1441 map-visit "^1.0.0" 1743 map-visit "^1.0.0"
1442 object-visit "^1.0.0" 1744 object-visit "^1.0.0"
1443 1745
1444color-convert@^1.3.0, color-convert@^1.9.0: 1746color-convert@^1.9.0:
1445 version "1.9.3" 1747 version "1.9.3"
1446 resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 1748 resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
1447 integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 1749 integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
1448 dependencies: 1750 dependencies:
1449 color-name "1.1.3" 1751 color-name "1.1.3"
1450 1752
1753color-convert@^2.0.1:
1754 version "2.0.1"
1755 resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
1756 integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
1757 dependencies:
1758 color-name "~1.1.4"
1759
1451color-name@1.1.3: 1760color-name@1.1.3:
1452 version "1.1.3" 1761 version "1.1.3"
1453 resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 1762 resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
1454 integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 1763 integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
1455 1764
1456color-name@^1.0.0: 1765color-name@~1.1.4:
1457 version "1.1.4" 1766 version "1.1.4"
1458 resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 1767 resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
1459 integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 1768 integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
1460 1769
1461color-string@^0.3.0: 1770colorette@^1.2.1:
1462 version "0.3.0" 1771 version "1.2.1"
1463 resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" 1772 resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
1464 integrity sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE= 1773 integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
1465 dependencies:
1466 color-name "^1.0.0"
1467
1468color@^0.11.0:
1469 version "0.11.4"
1470 resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764"
1471 integrity sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=
1472 dependencies:
1473 clone "^1.0.2"
1474 color-convert "^1.3.0"
1475 color-string "^0.3.0"
1476
1477colormin@^1.0.5:
1478 version "1.1.2"
1479 resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133"
1480 integrity sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=
1481 dependencies:
1482 color "^0.11.0"
1483 css-color-names "0.0.4"
1484 has "^1.0.1"
1485
1486colors@~1.1.2:
1487 version "1.1.2"
1488 resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
1489 integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
1490
1491combined-stream@^1.0.6, combined-stream@~1.0.6:
1492 version "1.0.8"
1493 resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
1494 integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
1495 dependencies:
1496 delayed-stream "~1.0.0"
1497 1774
1498commander@^2.8.1: 1775commander@^2.20.0:
1499 version "2.20.0" 1776 version "2.20.3"
1500 resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" 1777 resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
1501 integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== 1778 integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
1502 1779
1503commondir@^1.0.1: 1780commondir@^1.0.1:
1504 version "1.0.1" 1781 version "1.0.1"
@@ -1515,7 +1792,7 @@ concat-map@0.0.1:
1515 resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 1792 resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
1516 integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 1793 integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
1517 1794
1518concat-stream@^1.4.6, concat-stream@^1.6.0: 1795concat-stream@^1.5.0:
1519 version "1.6.2" 1796 version "1.6.2"
1520 resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" 1797 resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
1521 integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== 1798 integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@@ -1525,17 +1802,15 @@ concat-stream@^1.4.6, concat-stream@^1.6.0:
1525 readable-stream "^2.2.2" 1802 readable-stream "^2.2.2"
1526 typedarray "^0.0.6" 1803 typedarray "^0.0.6"
1527 1804
1528console-browserify@^1.1.0: 1805confusing-browser-globals@^1.0.9:
1529 version "1.1.0" 1806 version "1.0.9"
1530 resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" 1807 resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz#72bc13b483c0276801681871d4898516f8f54fdd"
1531 integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= 1808 integrity sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==
1532 dependencies:
1533 date-now "^0.1.4"
1534 1809
1535console-control-strings@^1.0.0, console-control-strings@~1.1.0: 1810console-browserify@^1.1.0:
1536 version "1.1.0" 1811 version "1.2.0"
1537 resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" 1812 resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
1538 integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= 1813 integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
1539 1814
1540constants-browserify@^1.0.0: 1815constants-browserify@^1.0.0:
1541 version "1.0.0" 1816 version "1.0.0"
@@ -1547,37 +1822,63 @@ contains-path@^0.1.0:
1547 resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" 1822 resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
1548 integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= 1823 integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
1549 1824
1550convert-source-map@^1.5.1: 1825convert-source-map@^1.7.0:
1551 version "1.6.0" 1826 version "1.7.0"
1552 resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" 1827 resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
1553 integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== 1828 integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
1554 dependencies: 1829 dependencies:
1555 safe-buffer "~5.1.1" 1830 safe-buffer "~5.1.1"
1556 1831
1832copy-concurrently@^1.0.0:
1833 version "1.0.5"
1834 resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
1835 integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==
1836 dependencies:
1837 aproba "^1.1.1"
1838 fs-write-stream-atomic "^1.0.8"
1839 iferr "^0.1.5"
1840 mkdirp "^0.5.1"
1841 rimraf "^2.5.4"
1842 run-queue "^1.0.0"
1843
1557copy-descriptor@^0.1.0: 1844copy-descriptor@^0.1.0:
1558 version "0.1.1" 1845 version "0.1.1"
1559 resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" 1846 resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
1560 integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= 1847 integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
1561 1848
1562core-js@^2.4.0, core-js@^2.5.0: 1849core-js-compat@^3.6.2:
1563 version "2.6.5" 1850 version "3.6.5"
1564 resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" 1851 resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c"
1565 integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== 1852 integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==
1853 dependencies:
1854 browserslist "^4.8.5"
1855 semver "7.0.0"
1566 1856
1567core-util-is@1.0.2, core-util-is@~1.0.0: 1857core-util-is@~1.0.0:
1568 version "1.0.2" 1858 version "1.0.2"
1569 resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 1859 resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
1570 integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= 1860 integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
1571 1861
1862cosmiconfig@^7.0.0:
1863 version "7.0.0"
1864 resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3"
1865 integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==
1866 dependencies:
1867 "@types/parse-json" "^4.0.0"
1868 import-fresh "^3.2.1"
1869 parse-json "^5.0.0"
1870 path-type "^4.0.0"
1871 yaml "^1.10.0"
1872
1572create-ecdh@^4.0.0: 1873create-ecdh@^4.0.0:
1573 version "4.0.3" 1874 version "4.0.4"
1574 resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" 1875 resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
1575 integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw== 1876 integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==
1576 dependencies: 1877 dependencies:
1577 bn.js "^4.1.0" 1878 bn.js "^4.1.0"
1578 elliptic "^6.0.0" 1879 elliptic "^6.5.3"
1579 1880
1580create-hash@^1.1.0, create-hash@^1.1.2: 1881create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
1581 version "1.2.0" 1882 version "1.2.0"
1582 resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" 1883 resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
1583 integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== 1884 integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
@@ -1588,7 +1889,7 @@ create-hash@^1.1.0, create-hash@^1.1.2:
1588 ripemd160 "^2.0.1" 1889 ripemd160 "^2.0.1"
1589 sha.js "^2.4.0" 1890 sha.js "^2.4.0"
1590 1891
1591create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: 1892create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
1592 version "1.1.7" 1893 version "1.1.7"
1593 resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" 1894 resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
1594 integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== 1895 integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
@@ -1600,22 +1901,25 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
1600 safe-buffer "^5.0.1" 1901 safe-buffer "^5.0.1"
1601 sha.js "^2.4.8" 1902 sha.js "^2.4.8"
1602 1903
1603cross-spawn@^3.0.0: 1904cross-spawn@^6.0.5:
1604 version "3.0.1" 1905 version "6.0.5"
1605 resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" 1906 resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
1606 integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= 1907 integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
1607 dependencies: 1908 dependencies:
1608 lru-cache "^4.0.1" 1909 nice-try "^1.0.4"
1910 path-key "^2.0.1"
1911 semver "^5.5.0"
1912 shebang-command "^1.2.0"
1609 which "^1.2.9" 1913 which "^1.2.9"
1610 1914
1611cross-spawn@^5.0.1, cross-spawn@^5.1.0: 1915cross-spawn@^7.0.2:
1612 version "5.1.0" 1916 version "7.0.3"
1613 resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" 1917 resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
1614 integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= 1918 integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
1615 dependencies: 1919 dependencies:
1616 lru-cache "^4.0.1" 1920 path-key "^3.1.0"
1617 shebang-command "^1.2.0" 1921 shebang-command "^2.0.0"
1618 which "^1.2.9" 1922 which "^2.0.1"
1619 1923
1620crypto-browserify@^3.11.0: 1924crypto-browserify@^3.11.0:
1621 version "3.12.0" 1925 version "3.12.0"
@@ -1634,132 +1938,57 @@ crypto-browserify@^3.11.0:
1634 randombytes "^2.0.0" 1938 randombytes "^2.0.0"
1635 randomfill "^1.0.3" 1939 randomfill "^1.0.3"
1636 1940
1637css-color-names@0.0.4: 1941css-loader@^4.3.0:
1638 version "0.0.4" 1942 version "4.3.0"
1639 resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" 1943 resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.3.0.tgz#c888af64b2a5b2e85462c72c0f4a85c7e2e0821e"
1640 integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= 1944 integrity sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg==
1641 1945 dependencies:
1642css-loader@^0.28.9: 1946 camelcase "^6.0.0"
1643 version "0.28.11" 1947 cssesc "^3.0.0"
1644 resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7" 1948 icss-utils "^4.1.1"
1645 integrity sha512-wovHgjAx8ZIMGSL8pTys7edA1ClmzxHeY6n/d97gg5odgsxEgKjULPR0viqyC+FWMCL9sfqoC/QCUBo62tLvPg== 1949 loader-utils "^2.0.0"
1646 dependencies: 1950 postcss "^7.0.32"
1647 babel-code-frame "^6.26.0" 1951 postcss-modules-extract-imports "^2.0.0"
1648 css-selector-tokenizer "^0.7.0" 1952 postcss-modules-local-by-default "^3.0.3"
1649 cssnano "^3.10.0" 1953 postcss-modules-scope "^2.2.0"
1650 icss-utils "^2.1.0" 1954 postcss-modules-values "^3.0.0"
1651 loader-utils "^1.0.2" 1955 postcss-value-parser "^4.1.0"
1652 lodash.camelcase "^4.3.0" 1956 schema-utils "^2.7.1"
1653 object-assign "^4.1.1" 1957 semver "^7.3.2"
1654 postcss "^5.0.6" 1958
1655 postcss-modules-extract-imports "^1.2.0" 1959cssesc@^3.0.0:
1656 postcss-modules-local-by-default "^1.2.0" 1960 version "3.0.0"
1657 postcss-modules-scope "^1.1.0" 1961 resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
1658 postcss-modules-values "^1.3.0" 1962 integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
1659 postcss-value-parser "^3.3.0"
1660 source-list-map "^2.0.0"
1661
1662css-selector-tokenizer@^0.7.0:
1663 version "0.7.1"
1664 resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz#a177271a8bca5019172f4f891fc6eed9cbf68d5d"
1665 integrity sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==
1666 dependencies:
1667 cssesc "^0.1.0"
1668 fastparse "^1.1.1"
1669 regexpu-core "^1.0.0"
1670
1671cssesc@^0.1.0:
1672 version "0.1.0"
1673 resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
1674 integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=
1675
1676cssnano@^3.10.0:
1677 version "3.10.0"
1678 resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38"
1679 integrity sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=
1680 dependencies:
1681 autoprefixer "^6.3.1"
1682 decamelize "^1.1.2"
1683 defined "^1.0.0"
1684 has "^1.0.1"
1685 object-assign "^4.0.1"
1686 postcss "^5.0.14"
1687 postcss-calc "^5.2.0"
1688 postcss-colormin "^2.1.8"
1689 postcss-convert-values "^2.3.4"
1690 postcss-discard-comments "^2.0.4"
1691 postcss-discard-duplicates "^2.0.1"
1692 postcss-discard-empty "^2.0.1"
1693 postcss-discard-overridden "^0.1.1"
1694 postcss-discard-unused "^2.2.1"
1695 postcss-filter-plugins "^2.0.0"
1696 postcss-merge-idents "^2.1.5"
1697 postcss-merge-longhand "^2.0.1"
1698 postcss-merge-rules "^2.0.3"
1699 postcss-minify-font-values "^1.0.2"
1700 postcss-minify-gradients "^1.0.1"
1701 postcss-minify-params "^1.0.4"
1702 postcss-minify-selectors "^2.0.4"
1703 postcss-normalize-charset "^1.1.0"
1704 postcss-normalize-url "^3.0.7"
1705 postcss-ordered-values "^2.1.0"
1706 postcss-reduce-idents "^2.2.2"
1707 postcss-reduce-initial "^1.0.0"
1708 postcss-reduce-transforms "^1.0.3"
1709 postcss-svgo "^2.1.1"
1710 postcss-unique-selectors "^2.0.2"
1711 postcss-value-parser "^3.2.3"
1712 postcss-zindex "^2.0.1"
1713
1714csso@~2.3.1:
1715 version "2.3.2"
1716 resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85"
1717 integrity sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=
1718 dependencies:
1719 clap "^1.0.9"
1720 source-map "^0.5.3"
1721
1722currently-unhandled@^0.4.1:
1723 version "0.4.1"
1724 resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
1725 integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
1726 dependencies:
1727 array-find-index "^1.0.1"
1728
1729d@1:
1730 version "1.0.0"
1731 resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
1732 integrity sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=
1733 dependencies:
1734 es5-ext "^0.10.9"
1735
1736dashdash@^1.12.0:
1737 version "1.14.1"
1738 resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
1739 integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
1740 dependencies:
1741 assert-plus "^1.0.0"
1742 1963
1743date-now@^0.1.4: 1964cyclist@^1.0.1:
1744 version "0.1.4" 1965 version "1.0.1"
1745 resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" 1966 resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
1746 integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= 1967 integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
1747 1968
1748debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: 1969debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
1749 version "2.6.9" 1970 version "2.6.9"
1750 resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 1971 resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
1751 integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 1972 integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
1752 dependencies: 1973 dependencies:
1753 ms "2.0.0" 1974 ms "2.0.0"
1754 1975
1755debug@^3.1.0, debug@^3.2.6: 1976debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
1756 version "3.2.6" 1977 version "4.2.0"
1757 resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" 1978 resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
1758 integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== 1979 integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
1980 dependencies:
1981 ms "2.1.2"
1982
1983decamelize-keys@^1.1.0:
1984 version "1.1.0"
1985 resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
1986 integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
1759 dependencies: 1987 dependencies:
1760 ms "^2.1.1" 1988 decamelize "^1.1.0"
1989 map-obj "^1.0.0"
1761 1990
1762decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: 1991decamelize@^1.1.0, decamelize@^1.2.0:
1763 version "1.2.0" 1992 version "1.2.0"
1764 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 1993 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
1765 integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= 1994 integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -1769,17 +1998,12 @@ decode-uri-component@^0.2.0:
1769 resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" 1998 resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
1770 integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= 1999 integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
1771 2000
1772deep-extend@^0.6.0: 2001deep-is@^0.1.3:
1773 version "0.6.0"
1774 resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
1775 integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
1776
1777deep-is@~0.1.3:
1778 version "0.1.3" 2002 version "0.1.3"
1779 resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" 2003 resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
1780 integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= 2004 integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
1781 2005
1782define-properties@^1.1.2: 2006define-properties@^1.1.3:
1783 version "1.1.3" 2007 version "1.1.3"
1784 resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" 2008 resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
1785 integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== 2009 integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -1808,40 +2032,18 @@ define-property@^2.0.2:
1808 is-descriptor "^1.0.2" 2032 is-descriptor "^1.0.2"
1809 isobject "^3.0.1" 2033 isobject "^3.0.1"
1810 2034
1811defined@^1.0.0:
1812 version "1.0.0"
1813 resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
1814 integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
1815
1816delayed-stream@~1.0.0:
1817 version "1.0.0"
1818 resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
1819 integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
1820
1821delegates@^1.0.0:
1822 version "1.0.0"
1823 resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
1824 integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
1825
1826des.js@^1.0.0: 2035des.js@^1.0.0:
1827 version "1.0.0" 2036 version "1.0.1"
1828 resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" 2037 resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
1829 integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw= 2038 integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==
1830 dependencies: 2039 dependencies:
1831 inherits "^2.0.1" 2040 inherits "^2.0.1"
1832 minimalistic-assert "^1.0.0" 2041 minimalistic-assert "^1.0.0"
1833 2042
1834detect-indent@^4.0.0: 2043detect-file@^1.0.0:
1835 version "4.0.0" 2044 version "1.0.0"
1836 resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" 2045 resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
1837 integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= 2046 integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
1838 dependencies:
1839 repeating "^2.0.0"
1840
1841detect-libc@^1.0.2:
1842 version "1.0.3"
1843 resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
1844 integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
1845 2047
1846diffie-hellman@^5.0.0: 2048diffie-hellman@^5.0.0:
1847 version "5.0.3" 2049 version "5.0.3"
@@ -1852,7 +2054,14 @@ diffie-hellman@^5.0.0:
1852 miller-rabin "^4.0.0" 2054 miller-rabin "^4.0.0"
1853 randombytes "^2.0.0" 2055 randombytes "^2.0.0"
1854 2056
1855doctrine@1.5.0, doctrine@^1.2.2: 2057dir-glob@^3.0.1:
2058 version "3.0.1"
2059 resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
2060 integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
2061 dependencies:
2062 path-type "^4.0.0"
2063
2064doctrine@1.5.0:
1856 version "1.5.0" 2065 version "1.5.0"
1857 resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" 2066 resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
1858 integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= 2067 integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=
@@ -1860,35 +2069,70 @@ doctrine@1.5.0, doctrine@^1.2.2:
1860 esutils "^2.0.2" 2069 esutils "^2.0.2"
1861 isarray "^1.0.0" 2070 isarray "^1.0.0"
1862 2071
1863doctrine@^2.1.0: 2072doctrine@^3.0.0:
1864 version "2.1.0" 2073 version "3.0.0"
1865 resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" 2074 resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
1866 integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== 2075 integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
1867 dependencies: 2076 dependencies:
1868 esutils "^2.0.2" 2077 esutils "^2.0.2"
1869 2078
2079dom-serializer@0:
2080 version "0.2.2"
2081 resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
2082 integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
2083 dependencies:
2084 domelementtype "^2.0.1"
2085 entities "^2.0.0"
2086
1870domain-browser@^1.1.1: 2087domain-browser@^1.1.1:
1871 version "1.2.0" 2088 version "1.2.0"
1872 resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" 2089 resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
1873 integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== 2090 integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
1874 2091
1875ecc-jsbn@~0.1.1: 2092domelementtype@1, domelementtype@^1.3.1:
1876 version "0.1.2" 2093 version "1.3.1"
1877 resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" 2094 resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
1878 integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= 2095 integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
2096
2097domelementtype@^2.0.1:
2098 version "2.0.2"
2099 resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971"
2100 integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==
2101
2102domhandler@^2.3.0:
2103 version "2.4.2"
2104 resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
2105 integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
1879 dependencies: 2106 dependencies:
1880 jsbn "~0.1.0" 2107 domelementtype "1"
1881 safer-buffer "^2.1.0" 2108
2109domutils@^1.5.1:
2110 version "1.7.0"
2111 resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
2112 integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
2113 dependencies:
2114 dom-serializer "0"
2115 domelementtype "1"
2116
2117duplexify@^3.4.2, duplexify@^3.6.0:
2118 version "3.7.1"
2119 resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
2120 integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==
2121 dependencies:
2122 end-of-stream "^1.0.0"
2123 inherits "^2.0.1"
2124 readable-stream "^2.0.0"
2125 stream-shift "^1.0.0"
1882 2126
1883electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.47: 2127electron-to-chromium@^1.3.570:
1884 version "1.3.135" 2128 version "1.3.570"
1885 resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.135.tgz#f5799b95f2bcd8de17cde47d63392d83a4477041" 2129 resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.570.tgz#3f5141cc39b4e3892a276b4889980dabf1d29c7f"
1886 integrity sha512-xXLNstRdVsisPF3pL3H9TVZo2XkMILfqtD6RiWIUmDK2sFX1Bjwqmd8LBp0Kuo2FgKO63JXPoEVGm8WyYdwP0Q== 2130 integrity sha512-Y6OCoVQgFQBP5py6A/06+yWxUZHDlNr/gNDGatjH8AZqXl8X0tE4LfjLJsXGz/JmWJz8a6K7bR1k+QzZ+k//fg==
1887 2131
1888elliptic@^6.0.0: 2132elliptic@^6.5.3:
1889 version "6.4.1" 2133 version "6.5.3"
1890 resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" 2134 resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
1891 integrity sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ== 2135 integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
1892 dependencies: 2136 dependencies:
1893 bn.js "^4.4.0" 2137 bn.js "^4.4.0"
1894 brorand "^1.0.1" 2138 brorand "^1.0.1"
@@ -1898,325 +2142,284 @@ elliptic@^6.0.0:
1898 minimalistic-assert "^1.0.0" 2142 minimalistic-assert "^1.0.0"
1899 minimalistic-crypto-utils "^1.0.0" 2143 minimalistic-crypto-utils "^1.0.0"
1900 2144
1901emojis-list@^2.0.0: 2145emoji-regex@^7.0.1:
1902 version "2.1.0" 2146 version "7.0.3"
1903 resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" 2147 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
1904 integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= 2148 integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
1905 2149
1906enhanced-resolve@^3.4.0: 2150emoji-regex@^8.0.0:
1907 version "3.4.1" 2151 version "8.0.0"
1908 resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" 2152 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
1909 integrity sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24= 2153 integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
2154
2155emojis-list@^3.0.0:
2156 version "3.0.0"
2157 resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
2158 integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
2159
2160end-of-stream@^1.0.0, end-of-stream@^1.1.0:
2161 version "1.4.4"
2162 resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
2163 integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
2164 dependencies:
2165 once "^1.4.0"
2166
2167enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0:
2168 version "4.3.0"
2169 resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126"
2170 integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==
1910 dependencies: 2171 dependencies:
1911 graceful-fs "^4.1.2" 2172 graceful-fs "^4.1.2"
1912 memory-fs "^0.4.0" 2173 memory-fs "^0.5.0"
1913 object-assign "^4.0.1" 2174 tapable "^1.0.0"
1914 tapable "^0.2.7"
1915 2175
1916errno@^0.1.3: 2176enquirer@^2.3.5:
2177 version "2.3.6"
2178 resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
2179 integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
2180 dependencies:
2181 ansi-colors "^4.1.1"
2182
2183entities@^1.1.1:
2184 version "1.1.2"
2185 resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
2186 integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
2187
2188entities@^2.0.0:
2189 version "2.0.3"
2190 resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
2191 integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
2192
2193errno@^0.1.3, errno@~0.1.7:
1917 version "0.1.7" 2194 version "0.1.7"
1918 resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" 2195 resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
1919 integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== 2196 integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
1920 dependencies: 2197 dependencies:
1921 prr "~1.0.1" 2198 prr "~1.0.1"
1922 2199
1923error-ex@^1.2.0: 2200error-ex@^1.2.0, error-ex@^1.3.1:
1924 version "1.3.2" 2201 version "1.3.2"
1925 resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" 2202 resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
1926 integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== 2203 integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
1927 dependencies: 2204 dependencies:
1928 is-arrayish "^0.2.1" 2205 is-arrayish "^0.2.1"
1929 2206
1930es-abstract@^1.7.0: 2207es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5:
1931 version "1.13.0" 2208 version "1.17.6"
1932 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" 2209 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
1933 integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== 2210 integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
1934 dependencies: 2211 dependencies:
1935 es-to-primitive "^1.2.0" 2212 es-to-primitive "^1.2.1"
1936 function-bind "^1.1.1" 2213 function-bind "^1.1.1"
1937 has "^1.0.3" 2214 has "^1.0.3"
1938 is-callable "^1.1.4" 2215 has-symbols "^1.0.1"
1939 is-regex "^1.0.4" 2216 is-callable "^1.2.0"
1940 object-keys "^1.0.12" 2217 is-regex "^1.1.0"
1941 2218 object-inspect "^1.7.0"
1942es-to-primitive@^1.2.0: 2219 object-keys "^1.1.1"
1943 version "1.2.0" 2220 object.assign "^4.1.0"
1944 resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" 2221 string.prototype.trimend "^1.0.1"
1945 integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== 2222 string.prototype.trimstart "^1.0.1"
2223
2224es-abstract@^1.18.0-next.0:
2225 version "1.18.0-next.0"
2226 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc"
2227 integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==
2228 dependencies:
2229 es-to-primitive "^1.2.1"
2230 function-bind "^1.1.1"
2231 has "^1.0.3"
2232 has-symbols "^1.0.1"
2233 is-callable "^1.2.0"
2234 is-negative-zero "^2.0.0"
2235 is-regex "^1.1.1"
2236 object-inspect "^1.8.0"
2237 object-keys "^1.1.1"
2238 object.assign "^4.1.0"
2239 string.prototype.trimend "^1.0.1"
2240 string.prototype.trimstart "^1.0.1"
2241
2242es-to-primitive@^1.2.1:
2243 version "1.2.1"
2244 resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
2245 integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
1946 dependencies: 2246 dependencies:
1947 is-callable "^1.1.4" 2247 is-callable "^1.1.4"
1948 is-date-object "^1.0.1" 2248 is-date-object "^1.0.1"
1949 is-symbol "^1.0.2" 2249 is-symbol "^1.0.2"
1950 2250
1951es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: 2251escalade@^3.1.0:
1952 version "0.10.50" 2252 version "3.1.0"
1953 resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778" 2253 resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e"
1954 integrity sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw== 2254 integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==
1955 dependencies:
1956 es6-iterator "~2.0.3"
1957 es6-symbol "~3.1.1"
1958 next-tick "^1.0.0"
1959
1960es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3:
1961 version "2.0.3"
1962 resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
1963 integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
1964 dependencies:
1965 d "1"
1966 es5-ext "^0.10.35"
1967 es6-symbol "^3.1.1"
1968
1969es6-map@^0.1.3:
1970 version "0.1.5"
1971 resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
1972 integrity sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=
1973 dependencies:
1974 d "1"
1975 es5-ext "~0.10.14"
1976 es6-iterator "~2.0.1"
1977 es6-set "~0.1.5"
1978 es6-symbol "~3.1.1"
1979 event-emitter "~0.3.5"
1980
1981es6-set@~0.1.5:
1982 version "0.1.5"
1983 resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
1984 integrity sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=
1985 dependencies:
1986 d "1"
1987 es5-ext "~0.10.14"
1988 es6-iterator "~2.0.1"
1989 es6-symbol "3.1.1"
1990 event-emitter "~0.3.5"
1991
1992es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1:
1993 version "3.1.1"
1994 resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
1995 integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=
1996 dependencies:
1997 d "1"
1998 es5-ext "~0.10.14"
1999
2000es6-weak-map@^2.0.1:
2001 version "2.0.2"
2002 resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
2003 integrity sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=
2004 dependencies:
2005 d "1"
2006 es5-ext "^0.10.14"
2007 es6-iterator "^2.0.1"
2008 es6-symbol "^3.1.1"
2009 2255
2010escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: 2256escape-string-regexp@^1.0.5:
2011 version "1.0.5" 2257 version "1.0.5"
2012 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 2258 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
2013 integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 2259 integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
2014 2260
2015escope@^3.6.0: 2261eslint-config-airbnb-base@^14.2.0:
2016 version "3.6.0" 2262 version "14.2.0"
2017 resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" 2263 resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.0.tgz#fe89c24b3f9dc8008c9c0d0d88c28f95ed65e9c4"
2018 integrity sha1-4Bl16BJ4GhY6ba392AOY3GTIicM= 2264 integrity sha512-Snswd5oC6nJaevs3nZoLSTvGJBvzTfnBqOIArkf3cbyTyq9UD79wOk8s+RiL6bhca0p/eRO6veczhf6A/7Jy8Q==
2019 dependencies:
2020 es6-map "^0.1.3"
2021 es6-weak-map "^2.0.1"
2022 esrecurse "^4.1.0"
2023 estraverse "^4.1.1"
2024
2025eslint-config-airbnb-base@^12.1.0:
2026 version "12.1.0"
2027 resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz#386441e54a12ccd957b0a92564a4bafebd747944"
2028 integrity sha512-/vjm0Px5ZCpmJqnjIzcFb9TKZrKWz0gnuG/7Gfkt0Db1ELJR51xkZth+t14rYdqWgX836XbuxtArbIHlVhbLBA==
2029 dependencies: 2265 dependencies:
2030 eslint-restricted-globals "^0.1.1" 2266 confusing-browser-globals "^1.0.9"
2267 object.assign "^4.1.0"
2268 object.entries "^1.1.2"
2031 2269
2032eslint-import-resolver-node@^0.3.2: 2270eslint-import-resolver-node@^0.3.3:
2033 version "0.3.2" 2271 version "0.3.4"
2034 resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" 2272 resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
2035 integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== 2273 integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==
2036 dependencies: 2274 dependencies:
2037 debug "^2.6.9" 2275 debug "^2.6.9"
2038 resolve "^1.5.0" 2276 resolve "^1.13.1"
2039 2277
2040eslint-module-utils@^2.4.0: 2278eslint-module-utils@^2.6.0:
2041 version "2.4.0" 2279 version "2.6.0"
2042 resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz#8b93499e9b00eab80ccb6614e69f03678e84e09a" 2280 resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6"
2043 integrity sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw== 2281 integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==
2044 dependencies: 2282 dependencies:
2045 debug "^2.6.8" 2283 debug "^2.6.9"
2046 pkg-dir "^2.0.0" 2284 pkg-dir "^2.0.0"
2047 2285
2048eslint-plugin-import@^2.8.0: 2286eslint-plugin-import@^2.22.0:
2049 version "2.17.2" 2287 version "2.22.0"
2050 resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz#d227d5c6dc67eca71eb590d2bb62fb38d86e9fcb" 2288 resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e"
2051 integrity sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g== 2289 integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==
2052 dependencies: 2290 dependencies:
2053 array-includes "^3.0.3" 2291 array-includes "^3.1.1"
2292 array.prototype.flat "^1.2.3"
2054 contains-path "^0.1.0" 2293 contains-path "^0.1.0"
2055 debug "^2.6.9" 2294 debug "^2.6.9"
2056 doctrine "1.5.0" 2295 doctrine "1.5.0"
2057 eslint-import-resolver-node "^0.3.2" 2296 eslint-import-resolver-node "^0.3.3"
2058 eslint-module-utils "^2.4.0" 2297 eslint-module-utils "^2.6.0"
2059 has "^1.0.3" 2298 has "^1.0.3"
2060 lodash "^4.17.11"
2061 minimatch "^3.0.4" 2299 minimatch "^3.0.4"
2300 object.values "^1.1.1"
2062 read-pkg-up "^2.0.0" 2301 read-pkg-up "^2.0.0"
2063 resolve "^1.10.0" 2302 resolve "^1.17.0"
2064 2303 tsconfig-paths "^3.9.0"
2065eslint-restricted-globals@^0.1.1:
2066 version "0.1.1"
2067 resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
2068 integrity sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=
2069 2304
2070eslint-scope@^3.7.1: 2305eslint-scope@^4.0.3:
2071 version "3.7.3" 2306 version "4.0.3"
2072 resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535" 2307 resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
2073 integrity sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA== 2308 integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
2074 dependencies: 2309 dependencies:
2075 esrecurse "^4.1.0" 2310 esrecurse "^4.1.0"
2076 estraverse "^4.1.1" 2311 estraverse "^4.1.1"
2077 2312
2078eslint-visitor-keys@^1.0.0: 2313eslint-scope@^5.1.0:
2079 version "1.0.0" 2314 version "5.1.1"
2080 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" 2315 resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
2081 integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== 2316 integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
2082 2317 dependencies:
2083eslint@^2.7.0: 2318 esrecurse "^4.3.0"
2084 version "2.13.1" 2319 estraverse "^4.1.1"
2085 resolved "https://registry.yarnpkg.com/eslint/-/eslint-2.13.1.tgz#e4cc8fa0f009fb829aaae23855a29360be1f6c11" 2320
2086 integrity sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE= 2321eslint-utils@^2.1.0:
2087 dependencies: 2322 version "2.1.0"
2088 chalk "^1.1.3" 2323 resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
2089 concat-stream "^1.4.6" 2324 integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
2090 debug "^2.1.1" 2325 dependencies:
2091 doctrine "^1.2.2" 2326 eslint-visitor-keys "^1.1.0"
2092 es6-map "^0.1.3" 2327
2093 escope "^3.6.0" 2328eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0:
2094 espree "^3.1.6" 2329 version "1.3.0"
2095 estraverse "^4.2.0" 2330 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
2096 esutils "^2.0.2" 2331 integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
2097 file-entry-cache "^1.1.1" 2332
2098 glob "^7.0.3" 2333eslint@^7.9.0:
2099 globals "^9.2.0" 2334 version "7.9.0"
2100 ignore "^3.1.2" 2335 resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.9.0.tgz#522aeccc5c3a19017cf0cb46ebfd660a79acf337"
2101 imurmurhash "^0.1.4" 2336 integrity sha512-V6QyhX21+uXp4T+3nrNfI3hQNBDa/P8ga7LoQOenwrlEFXrEnUEE+ok1dMtaS3b6rmLXhT1TkTIsG75HMLbknA==
2102 inquirer "^0.12.0" 2337 dependencies:
2103 is-my-json-valid "^2.10.0" 2338 "@babel/code-frame" "^7.0.0"
2104 is-resolvable "^1.0.0" 2339 "@eslint/eslintrc" "^0.1.3"
2105 js-yaml "^3.5.1" 2340 ajv "^6.10.0"
2106 json-stable-stringify "^1.0.0" 2341 chalk "^4.0.0"
2107 levn "^0.3.0" 2342 cross-spawn "^7.0.2"
2108 lodash "^4.0.0" 2343 debug "^4.0.1"
2109 mkdirp "^0.5.0" 2344 doctrine "^3.0.0"
2110 optionator "^0.8.1" 2345 enquirer "^2.3.5"
2111 path-is-absolute "^1.0.0" 2346 eslint-scope "^5.1.0"
2112 path-is-inside "^1.0.1" 2347 eslint-utils "^2.1.0"
2113 pluralize "^1.2.1" 2348 eslint-visitor-keys "^1.3.0"
2114 progress "^1.1.8" 2349 espree "^7.3.0"
2115 require-uncached "^1.0.2" 2350 esquery "^1.2.0"
2116 shelljs "^0.6.0"
2117 strip-json-comments "~1.0.1"
2118 table "^3.7.8"
2119 text-table "~0.2.0"
2120 user-home "^2.0.0"
2121
2122eslint@^4.16.0:
2123 version "4.19.1"
2124 resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300"
2125 integrity sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==
2126 dependencies:
2127 ajv "^5.3.0"
2128 babel-code-frame "^6.22.0"
2129 chalk "^2.1.0"
2130 concat-stream "^1.6.0"
2131 cross-spawn "^5.1.0"
2132 debug "^3.1.0"
2133 doctrine "^2.1.0"
2134 eslint-scope "^3.7.1"
2135 eslint-visitor-keys "^1.0.0"
2136 espree "^3.5.4"
2137 esquery "^1.0.0"
2138 esutils "^2.0.2" 2351 esutils "^2.0.2"
2139 file-entry-cache "^2.0.0" 2352 file-entry-cache "^5.0.1"
2140 functional-red-black-tree "^1.0.1" 2353 functional-red-black-tree "^1.0.1"
2141 glob "^7.1.2" 2354 glob-parent "^5.0.0"
2142 globals "^11.0.1" 2355 globals "^12.1.0"
2143 ignore "^3.3.3" 2356 ignore "^4.0.6"
2357 import-fresh "^3.0.0"
2144 imurmurhash "^0.1.4" 2358 imurmurhash "^0.1.4"
2145 inquirer "^3.0.6" 2359 is-glob "^4.0.0"
2146 is-resolvable "^1.0.0" 2360 js-yaml "^3.13.1"
2147 js-yaml "^3.9.1"
2148 json-stable-stringify-without-jsonify "^1.0.1" 2361 json-stable-stringify-without-jsonify "^1.0.1"
2149 levn "^0.3.0" 2362 levn "^0.4.1"
2150 lodash "^4.17.4" 2363 lodash "^4.17.19"
2151 minimatch "^3.0.2" 2364 minimatch "^3.0.4"
2152 mkdirp "^0.5.1"
2153 natural-compare "^1.4.0" 2365 natural-compare "^1.4.0"
2154 optionator "^0.8.2" 2366 optionator "^0.9.1"
2155 path-is-inside "^1.0.2"
2156 pluralize "^7.0.0"
2157 progress "^2.0.0" 2367 progress "^2.0.0"
2158 regexpp "^1.0.1" 2368 regexpp "^3.1.0"
2159 require-uncached "^1.0.3" 2369 semver "^7.2.1"
2160 semver "^5.3.0" 2370 strip-ansi "^6.0.0"
2161 strip-ansi "^4.0.0" 2371 strip-json-comments "^3.1.0"
2162 strip-json-comments "~2.0.1" 2372 table "^5.2.3"
2163 table "4.0.2" 2373 text-table "^0.2.0"
2164 text-table "~0.2.0" 2374 v8-compile-cache "^2.0.3"
2165 2375
2166espree@^3.1.6, espree@^3.5.4: 2376espree@^7.3.0:
2167 version "3.5.4" 2377 version "7.3.0"
2168 resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" 2378 resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348"
2169 integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A== 2379 integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==
2170 dependencies: 2380 dependencies:
2171 acorn "^5.5.0" 2381 acorn "^7.4.0"
2172 acorn-jsx "^3.0.0" 2382 acorn-jsx "^5.2.0"
2173 2383 eslint-visitor-keys "^1.3.0"
2174esprima@^2.6.0:
2175 version "2.7.3"
2176 resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
2177 integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=
2178 2384
2179esprima@^4.0.0: 2385esprima@^4.0.0:
2180 version "4.0.1" 2386 version "4.0.1"
2181 resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" 2387 resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
2182 integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 2388 integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
2183 2389
2184esquery@^1.0.0: 2390esquery@^1.2.0:
2185 version "1.0.1" 2391 version "1.3.1"
2186 resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" 2392 resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
2187 integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== 2393 integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
2188 dependencies: 2394 dependencies:
2189 estraverse "^4.0.0" 2395 estraverse "^5.1.0"
2190 2396
2191esrecurse@^4.1.0: 2397esrecurse@^4.1.0, esrecurse@^4.3.0:
2192 version "4.2.1" 2398 version "4.3.0"
2193 resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" 2399 resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
2194 integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== 2400 integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
2195 dependencies: 2401 dependencies:
2196 estraverse "^4.1.0" 2402 estraverse "^5.2.0"
2197 2403
2198estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: 2404estraverse@^4.1.1:
2199 version "4.2.0" 2405 version "4.3.0"
2200 resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" 2406 resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
2201 integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= 2407 integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
2202 2408
2203esutils@^2.0.2: 2409estraverse@^5.1.0, estraverse@^5.2.0:
2204 version "2.0.2" 2410 version "5.2.0"
2205 resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" 2411 resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
2206 integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= 2412 integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
2207 2413
2208event-emitter@~0.3.5: 2414esutils@^2.0.2:
2209 version "0.3.5" 2415 version "2.0.3"
2210 resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" 2416 resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
2211 integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= 2417 integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
2212 dependencies:
2213 d "1"
2214 es5-ext "~0.10.14"
2215 2418
2216events@^3.0.0: 2419events@^3.0.0:
2217 version "3.0.0" 2420 version "3.2.0"
2218 resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" 2421 resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
2219 integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== 2422 integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
2220 2423
2221evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: 2424evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
2222 version "1.0.3" 2425 version "1.0.3"
@@ -2226,23 +2429,12 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
2226 md5.js "^1.3.4" 2429 md5.js "^1.3.4"
2227 safe-buffer "^5.1.1" 2430 safe-buffer "^5.1.1"
2228 2431
2229execa@^0.7.0: 2432execall@^2.0.0:
2230 version "0.7.0" 2433 version "2.0.0"
2231 resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" 2434 resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45"
2232 integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= 2435 integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==
2233 dependencies: 2436 dependencies:
2234 cross-spawn "^5.0.1" 2437 clone-regexp "^2.1.0"
2235 get-stream "^3.0.0"
2236 is-stream "^1.1.0"
2237 npm-run-path "^2.0.0"
2238 p-finally "^1.0.0"
2239 signal-exit "^3.0.0"
2240 strip-eof "^1.0.0"
2241
2242exit-hook@^1.0.0:
2243 version "1.1.1"
2244 resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
2245 integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=
2246 2438
2247expand-brackets@^2.1.4: 2439expand-brackets@^2.1.4:
2248 version "2.1.4" 2440 version "2.1.4"
@@ -2257,6 +2449,13 @@ expand-brackets@^2.1.4:
2257 snapdragon "^0.8.1" 2449 snapdragon "^0.8.1"
2258 to-regex "^3.0.1" 2450 to-regex "^3.0.1"
2259 2451
2452expand-tilde@^2.0.0, expand-tilde@^2.0.2:
2453 version "2.0.2"
2454 resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
2455 integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
2456 dependencies:
2457 homedir-polyfill "^1.0.1"
2458
2260extend-shallow@^2.0.1: 2459extend-shallow@^2.0.1:
2261 version "2.0.1" 2460 version "2.0.1"
2262 resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" 2461 resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -2272,20 +2471,11 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
2272 assign-symbols "^1.0.0" 2471 assign-symbols "^1.0.0"
2273 is-extendable "^1.0.1" 2472 is-extendable "^1.0.1"
2274 2473
2275extend@~3.0.2: 2474extend@^3.0.0:
2276 version "3.0.2" 2475 version "3.0.2"
2277 resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 2476 resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
2278 integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 2477 integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
2279 2478
2280external-editor@^2.0.4:
2281 version "2.2.0"
2282 resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
2283 integrity sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==
2284 dependencies:
2285 chardet "^0.4.0"
2286 iconv-lite "^0.4.17"
2287 tmp "^0.0.33"
2288
2289extglob@^2.0.4: 2479extglob@^2.0.4:
2290 version "2.0.4" 2480 version "2.0.4"
2291 resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" 2481 resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
@@ -2300,81 +2490,56 @@ extglob@^2.0.4:
2300 snapdragon "^0.8.1" 2490 snapdragon "^0.8.1"
2301 to-regex "^3.0.1" 2491 to-regex "^3.0.1"
2302 2492
2303extract-text-webpack-plugin@^3.0.2: 2493fast-deep-equal@^3.1.1:
2304 version "3.0.2" 2494 version "3.1.3"
2305 resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz#5f043eaa02f9750a9258b78c0a6e0dc1408fb2f7" 2495 resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
2306 integrity sha512-bt/LZ4m5Rqt/Crl2HiKuAl/oqg0psx1tsTLkvWbJen1CtD+fftkZhMaQ9HOtY2gWsl2Wq+sABmMVi9z3DhKWQQ== 2496 integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
2307 dependencies:
2308 async "^2.4.1"
2309 loader-utils "^1.1.0"
2310 schema-utils "^0.3.0"
2311 webpack-sources "^1.0.1"
2312
2313extsprintf@1.3.0:
2314 version "1.3.0"
2315 resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
2316 integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
2317
2318extsprintf@^1.2.0:
2319 version "1.4.0"
2320 resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
2321 integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
2322 2497
2323fast-deep-equal@^1.0.0: 2498fast-glob@^3.1.1, fast-glob@^3.2.4:
2324 version "1.1.0" 2499 version "3.2.4"
2325 resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" 2500 resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
2326 integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= 2501 integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==
2327 2502 dependencies:
2328fast-deep-equal@^2.0.1: 2503 "@nodelib/fs.stat" "^2.0.2"
2329 version "2.0.1" 2504 "@nodelib/fs.walk" "^1.2.3"
2330 resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" 2505 glob-parent "^5.1.0"
2331 integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= 2506 merge2 "^1.3.0"
2507 micromatch "^4.0.2"
2508 picomatch "^2.2.1"
2332 2509
2333fast-json-stable-stringify@^2.0.0: 2510fast-json-stable-stringify@^2.0.0:
2334 version "2.0.0" 2511 version "2.1.0"
2335 resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" 2512 resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
2336 integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= 2513 integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
2337 2514
2338fast-levenshtein@~2.0.4: 2515fast-levenshtein@^2.0.6:
2339 version "2.0.6" 2516 version "2.0.6"
2340 resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 2517 resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
2341 integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= 2518 integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
2342 2519
2343fastparse@^1.1.1: 2520fastest-levenshtein@^1.0.12:
2344 version "1.1.2" 2521 version "1.0.12"
2345 resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" 2522 resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
2346 integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== 2523 integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
2347
2348figures@^1.3.5:
2349 version "1.7.0"
2350 resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
2351 integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=
2352 dependencies:
2353 escape-string-regexp "^1.0.5"
2354 object-assign "^4.1.0"
2355 2524
2356figures@^2.0.0: 2525fastq@^1.6.0:
2357 version "2.0.0" 2526 version "1.8.0"
2358 resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" 2527 resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481"
2359 integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= 2528 integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==
2360 dependencies: 2529 dependencies:
2361 escape-string-regexp "^1.0.5" 2530 reusify "^1.0.4"
2362 2531
2363file-entry-cache@^1.1.1: 2532figgy-pudding@^3.5.1:
2364 version "1.3.1" 2533 version "3.5.2"
2365 resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-1.3.1.tgz#44c61ea607ae4be9c1402f41f44270cbfe334ff8" 2534 resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
2366 integrity sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g= 2535 integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
2367 dependencies:
2368 flat-cache "^1.2.1"
2369 object-assign "^4.0.1"
2370 2536
2371file-entry-cache@^2.0.0: 2537file-entry-cache@^5.0.1:
2372 version "2.0.0" 2538 version "5.0.1"
2373 resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" 2539 resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
2374 integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E= 2540 integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
2375 dependencies: 2541 dependencies:
2376 flat-cache "^1.2.1" 2542 flat-cache "^2.0.1"
2377 object-assign "^4.0.1"
2378 2543
2379file-loader@^1.1.6: 2544file-loader@^1.1.6:
2380 version "1.1.11" 2545 version "1.1.11"
@@ -2384,6 +2549,11 @@ file-loader@^1.1.6:
2384 loader-utils "^1.0.2" 2549 loader-utils "^1.0.2"
2385 schema-utils "^0.4.5" 2550 schema-utils "^0.4.5"
2386 2551
2552file-uri-to-path@1.0.0:
2553 version "1.0.0"
2554 resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
2555 integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
2556
2387fill-range@^4.0.0: 2557fill-range@^4.0.0:
2388 version "4.0.0" 2558 version "4.0.0"
2389 resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" 2559 resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
@@ -2394,22 +2564,30 @@ fill-range@^4.0.0:
2394 repeat-string "^1.6.1" 2564 repeat-string "^1.6.1"
2395 to-regex-range "^2.1.0" 2565 to-regex-range "^2.1.0"
2396 2566
2397find-cache-dir@^1.0.0: 2567fill-range@^7.0.1:
2398 version "1.0.0" 2568 version "7.0.1"
2399 resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" 2569 resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
2400 integrity sha1-kojj6ePMN0hxfTnq3hfPcfww7m8= 2570 integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
2571 dependencies:
2572 to-regex-range "^5.0.1"
2573
2574find-cache-dir@^2.1.0:
2575 version "2.1.0"
2576 resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
2577 integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==
2401 dependencies: 2578 dependencies:
2402 commondir "^1.0.1" 2579 commondir "^1.0.1"
2403 make-dir "^1.0.0" 2580 make-dir "^2.0.0"
2404 pkg-dir "^2.0.0" 2581 pkg-dir "^3.0.0"
2405 2582
2406find-up@^1.0.0: 2583find-cache-dir@^3.3.1:
2407 version "1.1.2" 2584 version "3.3.1"
2408 resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" 2585 resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880"
2409 integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= 2586 integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==
2410 dependencies: 2587 dependencies:
2411 path-exists "^2.0.0" 2588 commondir "^1.0.1"
2412 pinkie-promise "^2.0.0" 2589 make-dir "^3.0.2"
2590 pkg-dir "^4.1.0"
2413 2591
2414find-up@^2.0.0, find-up@^2.1.0: 2592find-up@^2.0.0, find-up@^2.1.0:
2415 version "2.1.0" 2593 version "2.1.0"
@@ -2418,57 +2596,63 @@ find-up@^2.0.0, find-up@^2.1.0:
2418 dependencies: 2596 dependencies:
2419 locate-path "^2.0.0" 2597 locate-path "^2.0.0"
2420 2598
2421flat-cache@^1.2.1: 2599find-up@^3.0.0:
2422 version "1.3.4" 2600 version "3.0.0"
2423 resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f" 2601 resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
2424 integrity sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg== 2602 integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
2425 dependencies: 2603 dependencies:
2426 circular-json "^0.3.1" 2604 locate-path "^3.0.0"
2427 graceful-fs "^4.1.2"
2428 rimraf "~2.6.2"
2429 write "^0.2.1"
2430 2605
2431flatten@^1.0.2: 2606find-up@^4.0.0, find-up@^4.1.0:
2432 version "1.0.2" 2607 version "4.1.0"
2433 resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" 2608 resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
2434 integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I= 2609 integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
2610 dependencies:
2611 locate-path "^5.0.0"
2612 path-exists "^4.0.0"
2435 2613
2436for-in@^0.1.3: 2614findup-sync@^3.0.0:
2437 version "0.1.8" 2615 version "3.0.0"
2438 resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" 2616 resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1"
2439 integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= 2617 integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==
2618 dependencies:
2619 detect-file "^1.0.0"
2620 is-glob "^4.0.0"
2621 micromatch "^3.0.4"
2622 resolve-dir "^1.0.1"
2440 2623
2441for-in@^1.0.1, for-in@^1.0.2: 2624flat-cache@^2.0.1:
2442 version "1.0.2" 2625 version "2.0.1"
2443 resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" 2626 resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
2444 integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= 2627 integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
2628 dependencies:
2629 flatted "^2.0.0"
2630 rimraf "2.6.3"
2631 write "1.0.3"
2445 2632
2446for-own@^1.0.0: 2633flatted@^2.0.0:
2447 version "1.0.0" 2634 version "2.0.2"
2448 resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" 2635 resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
2449 integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= 2636 integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
2637
2638flush-write-stream@^1.0.0:
2639 version "1.1.1"
2640 resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
2641 integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==
2450 dependencies: 2642 dependencies:
2451 for-in "^1.0.1" 2643 inherits "^2.0.3"
2644 readable-stream "^2.3.6"
2452 2645
2453forever-agent@~0.6.1: 2646for-in@^1.0.2:
2454 version "0.6.1" 2647 version "1.0.2"
2455 resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 2648 resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
2456 integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= 2649 integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
2457 2650
2458fork-awesome@^1.1.7: 2651fork-awesome@^1.1.7:
2459 version "1.1.7" 2652 version "1.1.7"
2460 resolved "https://registry.yarnpkg.com/fork-awesome/-/fork-awesome-1.1.7.tgz#1427da1cac3d1713046ee88427e5fcecb9501d21" 2653 resolved "https://registry.yarnpkg.com/fork-awesome/-/fork-awesome-1.1.7.tgz#1427da1cac3d1713046ee88427e5fcecb9501d21"
2461 integrity sha512-IHI7XCSXrKfUIWslse8c/PaaVDT1oBaYge+ju40ihL2ooiQeBpTr4wvIXhgTd2NuhntlvX+M5jYHAPTzNlmv0g== 2654 integrity sha512-IHI7XCSXrKfUIWslse8c/PaaVDT1oBaYge+ju40ihL2ooiQeBpTr4wvIXhgTd2NuhntlvX+M5jYHAPTzNlmv0g==
2462 2655
2463form-data@~2.3.2:
2464 version "2.3.3"
2465 resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
2466 integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
2467 dependencies:
2468 asynckit "^0.4.0"
2469 combined-stream "^1.0.6"
2470 mime-types "^2.1.12"
2471
2472fragment-cache@^0.2.1: 2656fragment-cache@^0.2.1:
2473 version "0.2.1" 2657 version "0.2.1"
2474 resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" 2658 resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@@ -2476,28 +2660,30 @@ fragment-cache@^0.2.1:
2476 dependencies: 2660 dependencies:
2477 map-cache "^0.2.2" 2661 map-cache "^0.2.2"
2478 2662
2479front-matter@2.1.2: 2663from2@^2.1.0:
2480 version "2.1.2" 2664 version "2.3.0"
2481 resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-2.1.2.tgz#f75983b9f2f413be658c93dfd7bd8ce4078f5cdb" 2665 resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
2482 integrity sha1-91mDufL0E75ljJPf172M5AePXNs= 2666 integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
2483 dependencies: 2667 dependencies:
2484 js-yaml "^3.4.6" 2668 inherits "^2.0.1"
2669 readable-stream "^2.0.0"
2485 2670
2486fs-extra@^3.0.1: 2671fs-minipass@^2.0.0:
2487 version "3.0.1" 2672 version "2.1.0"
2488 resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" 2673 resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
2489 integrity sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE= 2674 integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
2490 dependencies: 2675 dependencies:
2491 graceful-fs "^4.1.2" 2676 minipass "^3.0.0"
2492 jsonfile "^3.0.0"
2493 universalify "^0.1.0"
2494 2677
2495fs-minipass@^1.2.5: 2678fs-write-stream-atomic@^1.0.8:
2496 version "1.2.6" 2679 version "1.0.10"
2497 resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" 2680 resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
2498 integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== 2681 integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=
2499 dependencies: 2682 dependencies:
2500 minipass "^2.2.1" 2683 graceful-fs "^4.1.2"
2684 iferr "^0.1.5"
2685 imurmurhash "^0.1.4"
2686 readable-stream "1 || 2"
2501 2687
2502fs.realpath@^1.0.0: 2688fs.realpath@^1.0.0:
2503 version "1.0.0" 2689 version "1.0.0"
@@ -2505,22 +2691,17 @@ fs.realpath@^1.0.0:
2505 integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 2691 integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
2506 2692
2507fsevents@^1.2.7: 2693fsevents@^1.2.7:
2508 version "1.2.9" 2694 version "1.2.13"
2509 resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" 2695 resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
2510 integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== 2696 integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
2511 dependencies: 2697 dependencies:
2698 bindings "^1.5.0"
2512 nan "^2.12.1" 2699 nan "^2.12.1"
2513 node-pre-gyp "^0.12.0"
2514 2700
2515fstream@^1.0.0, fstream@^1.0.12: 2701fsevents@~2.1.2:
2516 version "1.0.12" 2702 version "2.1.3"
2517 resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" 2703 resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
2518 integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== 2704 integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
2519 dependencies:
2520 graceful-fs "^4.1.2"
2521 inherits "~2.0.0"
2522 mkdirp ">=0.5 0"
2523 rimraf "2"
2524 2705
2525function-bind@^1.1.1: 2706function-bind@^1.1.1:
2526 version "1.1.1" 2707 version "1.1.1"
@@ -2532,68 +2713,26 @@ functional-red-black-tree@^1.0.1:
2532 resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" 2713 resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
2533 integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= 2714 integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
2534 2715
2535gauge@~2.7.3: 2716gensync@^1.0.0-beta.1:
2536 version "2.7.4" 2717 version "1.0.0-beta.1"
2537 resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" 2718 resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
2538 integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= 2719 integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
2539 dependencies:
2540 aproba "^1.0.3"
2541 console-control-strings "^1.0.0"
2542 has-unicode "^2.0.0"
2543 object-assign "^4.1.0"
2544 signal-exit "^3.0.0"
2545 string-width "^1.0.1"
2546 strip-ansi "^3.0.1"
2547 wide-align "^1.1.0"
2548
2549gaze@^1.0.0:
2550 version "1.1.3"
2551 resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
2552 integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==
2553 dependencies:
2554 globule "^1.0.0"
2555
2556generate-function@^2.0.0:
2557 version "2.3.1"
2558 resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
2559 integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
2560 dependencies:
2561 is-property "^1.0.2"
2562 2720
2563generate-object-property@^1.1.0: 2721get-caller-file@^2.0.1:
2564 version "1.2.0" 2722 version "2.0.5"
2565 resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" 2723 resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
2566 integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA= 2724 integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
2567 dependencies:
2568 is-property "^1.0.0"
2569 2725
2570get-caller-file@^1.0.1: 2726get-stdin@^8.0.0:
2571 version "1.0.3" 2727 version "8.0.0"
2572 resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" 2728 resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
2573 integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== 2729 integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
2574
2575get-stdin@^4.0.1:
2576 version "4.0.1"
2577 resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
2578 integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
2579
2580get-stream@^3.0.0:
2581 version "3.0.0"
2582 resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
2583 integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
2584 2730
2585get-value@^2.0.3, get-value@^2.0.6: 2731get-value@^2.0.3, get-value@^2.0.6:
2586 version "2.0.6" 2732 version "2.0.6"
2587 resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" 2733 resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
2588 integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= 2734 integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
2589 2735
2590getpass@^0.1.1:
2591 version "0.1.7"
2592 resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
2593 integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
2594 dependencies:
2595 assert-plus "^1.0.0"
2596
2597glob-parent@^3.1.0: 2736glob-parent@^3.1.0:
2598 version "3.1.0" 2737 version "3.1.0"
2599 resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" 2738 resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@@ -2602,10 +2741,17 @@ glob-parent@^3.1.0:
2602 is-glob "^3.1.0" 2741 is-glob "^3.1.0"
2603 path-dirname "^1.0.0" 2742 path-dirname "^1.0.0"
2604 2743
2605glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1: 2744glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0:
2606 version "7.1.4" 2745 version "5.1.1"
2607 resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" 2746 resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
2608 integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== 2747 integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
2748 dependencies:
2749 is-glob "^4.0.1"
2750
2751glob@^7.1.3, glob@^7.1.4:
2752 version "7.1.6"
2753 resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
2754 integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
2609 dependencies: 2755 dependencies:
2610 fs.realpath "^1.0.0" 2756 fs.realpath "^1.0.0"
2611 inflight "^1.0.4" 2757 inflight "^1.0.4"
@@ -2614,81 +2760,102 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1:
2614 once "^1.3.0" 2760 once "^1.3.0"
2615 path-is-absolute "^1.0.0" 2761 path-is-absolute "^1.0.0"
2616 2762
2617globals@^11.0.1: 2763global-modules@^1.0.0:
2618 version "11.12.0" 2764 version "1.0.0"
2619 resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" 2765 resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
2620 integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== 2766 integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
2767 dependencies:
2768 global-prefix "^1.0.1"
2769 is-windows "^1.0.1"
2770 resolve-dir "^1.0.0"
2621 2771
2622globals@^9.18.0, globals@^9.2.0: 2772global-modules@^2.0.0:
2623 version "9.18.0" 2773 version "2.0.0"
2624 resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" 2774 resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
2625 integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== 2775 integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
2776 dependencies:
2777 global-prefix "^3.0.0"
2626 2778
2627globule@^1.0.0: 2779global-prefix@^1.0.1:
2628 version "1.2.1" 2780 version "1.0.2"
2629 resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d" 2781 resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
2630 integrity sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ== 2782 integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
2631 dependencies: 2783 dependencies:
2632 glob "~7.1.1" 2784 expand-tilde "^2.0.2"
2633 lodash "~4.17.10" 2785 homedir-polyfill "^1.0.1"
2634 minimatch "~3.0.2" 2786 ini "^1.3.4"
2787 is-windows "^1.0.1"
2788 which "^1.2.14"
2635 2789
2636gonzales-pe-sl@^4.2.3: 2790global-prefix@^3.0.0:
2637 version "4.2.3" 2791 version "3.0.0"
2638 resolved "https://registry.yarnpkg.com/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz#6a868bc380645f141feeb042c6f97fcc71b59fe6" 2792 resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
2639 integrity sha1-aoaLw4BkXxQf7rBCxvl/zHG1n+Y= 2793 integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
2640 dependencies: 2794 dependencies:
2641 minimist "1.1.x" 2795 ini "^1.3.5"
2796 kind-of "^6.0.2"
2797 which "^1.3.1"
2642 2798
2643graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: 2799globals@^11.1.0:
2644 version "4.1.15" 2800 version "11.12.0"
2645 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" 2801 resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
2646 integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== 2802 integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
2647 2803
2648har-schema@^2.0.0: 2804globals@^12.1.0:
2649 version "2.0.0" 2805 version "12.4.0"
2650 resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 2806 resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
2651 integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= 2807 integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
2808 dependencies:
2809 type-fest "^0.8.1"
2652 2810
2653har-validator@~5.1.0: 2811globby@^11.0.1:
2654 version "5.1.3" 2812 version "11.0.1"
2655 resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" 2813 resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357"
2656 integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== 2814 integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==
2657 dependencies: 2815 dependencies:
2658 ajv "^6.5.5" 2816 array-union "^2.1.0"
2659 har-schema "^2.0.0" 2817 dir-glob "^3.0.1"
2818 fast-glob "^3.1.1"
2819 ignore "^5.1.4"
2820 merge2 "^1.3.0"
2821 slash "^3.0.0"
2660 2822
2661has-ansi@^2.0.0: 2823globjoin@^0.1.4:
2662 version "2.0.0" 2824 version "0.1.4"
2663 resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 2825 resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
2664 integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= 2826 integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=
2827
2828gonzales-pe@^4.3.0:
2829 version "4.3.0"
2830 resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3"
2831 integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==
2665 dependencies: 2832 dependencies:
2666 ansi-regex "^2.0.0" 2833 minimist "^1.2.5"
2667 2834
2668has-flag@^1.0.0: 2835graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
2669 version "1.0.0" 2836 version "4.2.4"
2670 resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" 2837 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
2671 integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= 2838 integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
2672 2839
2673has-flag@^2.0.0: 2840hard-rejection@^2.1.0:
2674 version "2.0.0" 2841 version "2.1.0"
2675 resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" 2842 resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
2676 integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= 2843 integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
2677 2844
2678has-flag@^3.0.0: 2845has-flag@^3.0.0:
2679 version "3.0.0" 2846 version "3.0.0"
2680 resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 2847 resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
2681 integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 2848 integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
2682 2849
2683has-symbols@^1.0.0: 2850has-flag@^4.0.0:
2684 version "1.0.0" 2851 version "4.0.0"
2685 resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" 2852 resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
2686 integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= 2853 integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
2687 2854
2688has-unicode@^2.0.0: 2855has-symbols@^1.0.1:
2689 version "2.0.1" 2856 version "1.0.1"
2690 resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 2857 resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
2691 integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= 2858 integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
2692 2859
2693has-value@^0.3.1: 2860has-value@^0.3.1:
2694 version "0.3.1" 2861 version "0.3.1"
@@ -2721,7 +2888,7 @@ has-values@^1.0.0:
2721 is-number "^3.0.0" 2888 is-number "^3.0.0"
2722 kind-of "^4.0.0" 2889 kind-of "^4.0.0"
2723 2890
2724has@^1.0.1, has@^1.0.3: 2891has@^1.0.3:
2725 version "1.0.3" 2892 version "1.0.3"
2726 resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 2893 resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
2727 integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 2894 integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
@@ -2729,12 +2896,13 @@ has@^1.0.1, has@^1.0.3:
2729 function-bind "^1.1.1" 2896 function-bind "^1.1.1"
2730 2897
2731hash-base@^3.0.0: 2898hash-base@^3.0.0:
2732 version "3.0.4" 2899 version "3.1.0"
2733 resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" 2900 resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
2734 integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= 2901 integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==
2735 dependencies: 2902 dependencies:
2736 inherits "^2.0.1" 2903 inherits "^2.0.4"
2737 safe-buffer "^5.0.1" 2904 readable-stream "^3.6.0"
2905 safe-buffer "^5.2.0"
2738 2906
2739hash.js@^1.0.0, hash.js@^1.0.3: 2907hash.js@^1.0.0, hash.js@^1.0.3:
2740 version "1.1.7" 2908 version "1.1.7"
@@ -2744,6 +2912,11 @@ hash.js@^1.0.0, hash.js@^1.0.3:
2744 inherits "^2.0.3" 2912 inherits "^2.0.3"
2745 minimalistic-assert "^1.0.1" 2913 minimalistic-assert "^1.0.1"
2746 2914
2915he@^1.2.0:
2916 version "1.2.0"
2917 resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
2918 integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
2919
2747hmac-drbg@^1.0.0: 2920hmac-drbg@^1.0.0:
2748 version "1.0.1" 2921 version "1.0.1"
2749 resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" 2922 resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -2753,100 +2926,107 @@ hmac-drbg@^1.0.0:
2753 minimalistic-assert "^1.0.0" 2926 minimalistic-assert "^1.0.0"
2754 minimalistic-crypto-utils "^1.0.1" 2927 minimalistic-crypto-utils "^1.0.1"
2755 2928
2756home-or-tmp@^2.0.0: 2929homedir-polyfill@^1.0.1:
2757 version "2.0.0" 2930 version "1.0.3"
2758 resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" 2931 resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
2759 integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= 2932 integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
2760 dependencies: 2933 dependencies:
2761 os-homedir "^1.0.0" 2934 parse-passwd "^1.0.0"
2762 os-tmpdir "^1.0.1"
2763 2935
2764hosted-git-info@^2.1.4: 2936hosted-git-info@^2.1.4:
2765 version "2.7.1" 2937 version "2.8.8"
2766 resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" 2938 resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
2767 integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== 2939 integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
2768 2940
2769html-comment-regex@^1.1.0: 2941html-tags@^3.1.0:
2770 version "1.1.2" 2942 version "3.1.0"
2771 resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" 2943 resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
2772 integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== 2944 integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
2773 2945
2774http-signature@~1.2.0: 2946htmlparser2@^3.10.0:
2775 version "1.2.0" 2947 version "3.10.1"
2776 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 2948 resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
2777 integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= 2949 integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
2778 dependencies: 2950 dependencies:
2779 assert-plus "^1.0.0" 2951 domelementtype "^1.3.1"
2780 jsprim "^1.2.2" 2952 domhandler "^2.3.0"
2781 sshpk "^1.7.0" 2953 domutils "^1.5.1"
2954 entities "^1.1.1"
2955 inherits "^2.0.1"
2956 readable-stream "^3.1.1"
2782 2957
2783https-browserify@^1.0.0: 2958https-browserify@^1.0.0:
2784 version "1.0.0" 2959 version "1.0.0"
2785 resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" 2960 resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
2786 integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= 2961 integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
2787 2962
2788iconv-lite@^0.4.17, iconv-lite@^0.4.4: 2963icss-utils@^4.0.0, icss-utils@^4.1.1:
2789 version "0.4.24" 2964 version "4.1.1"
2790 resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 2965 resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
2791 integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 2966 integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
2792 dependencies:
2793 safer-buffer ">= 2.1.2 < 3"
2794
2795icss-replace-symbols@^1.1.0:
2796 version "1.1.0"
2797 resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
2798 integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
2799
2800icss-utils@^2.1.0:
2801 version "2.1.0"
2802 resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962"
2803 integrity sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=
2804 dependencies: 2967 dependencies:
2805 postcss "^6.0.1" 2968 postcss "^7.0.14"
2806 2969
2807ieee754@^1.1.4: 2970ieee754@^1.1.4:
2808 version "1.1.13" 2971 version "1.1.13"
2809 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" 2972 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
2810 integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== 2973 integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
2811 2974
2812ignore-walk@^3.0.1: 2975iferr@^0.1.5:
2813 version "3.0.1" 2976 version "0.1.5"
2814 resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" 2977 resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
2815 integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== 2978 integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
2979
2980ignore@^4.0.6:
2981 version "4.0.6"
2982 resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
2983 integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
2984
2985ignore@^5.1.4, ignore@^5.1.8:
2986 version "5.1.8"
2987 resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
2988 integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
2989
2990import-fresh@^3.0.0, import-fresh@^3.2.1:
2991 version "3.2.1"
2992 resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
2993 integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
2816 dependencies: 2994 dependencies:
2817 minimatch "^3.0.4" 2995 parent-module "^1.0.0"
2996 resolve-from "^4.0.0"
2997
2998import-lazy@^4.0.0:
2999 version "4.0.0"
3000 resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153"
3001 integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==
2818 3002
2819ignore@^3.1.2, ignore@^3.3.3: 3003import-local@^2.0.0:
2820 version "3.3.10" 3004 version "2.0.0"
2821 resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" 3005 resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
2822 integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== 3006 integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
3007 dependencies:
3008 pkg-dir "^3.0.0"
3009 resolve-cwd "^2.0.0"
2823 3010
2824imurmurhash@^0.1.4: 3011imurmurhash@^0.1.4:
2825 version "0.1.4" 3012 version "0.1.4"
2826 resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 3013 resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
2827 integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= 3014 integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
2828 3015
2829in-publish@^2.0.0: 3016indent-string@^4.0.0:
2830 version "2.0.0" 3017 version "4.0.0"
2831 resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" 3018 resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
2832 integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= 3019 integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
2833
2834indent-string@^2.1.0:
2835 version "2.1.0"
2836 resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
2837 integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=
2838 dependencies:
2839 repeating "^2.0.0"
2840 3020
2841indexes-of@^1.0.1: 3021indexes-of@^1.0.1:
2842 version "1.0.1" 3022 version "1.0.1"
2843 resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" 3023 resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
2844 integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= 3024 integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
2845 3025
2846indexof@0.0.1: 3026infer-owner@^1.0.3, infer-owner@^1.0.4:
2847 version "0.0.1" 3027 version "1.0.4"
2848 resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 3028 resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
2849 integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= 3029 integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
2850 3030
2851inflight@^1.0.4: 3031inflight@^1.0.4:
2852 version "1.0.6" 3032 version "1.0.6"
@@ -2856,82 +3036,38 @@ inflight@^1.0.4:
2856 once "^1.3.0" 3036 once "^1.3.0"
2857 wrappy "1" 3037 wrappy "1"
2858 3038
2859inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: 3039inherits@2, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
2860 version "2.0.3" 3040 version "2.0.4"
2861 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 3041 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
2862 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 3042 integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
2863 3043
2864inherits@2.0.1: 3044inherits@2.0.1:
2865 version "2.0.1" 3045 version "2.0.1"
2866 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" 3046 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
2867 integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= 3047 integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
2868 3048
2869ini@~1.3.0: 3049inherits@2.0.3:
3050 version "2.0.3"
3051 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
3052 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
3053
3054ini@^1.3.4, ini@^1.3.5:
2870 version "1.3.5" 3055 version "1.3.5"
2871 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" 3056 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
2872 integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== 3057 integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
2873 3058
2874inquirer@^0.12.0: 3059interpret@^1.4.0:
2875 version "0.12.0" 3060 version "1.4.0"
2876 resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" 3061 resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
2877 integrity sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34= 3062 integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
2878 dependencies:
2879 ansi-escapes "^1.1.0"
2880 ansi-regex "^2.0.0"
2881 chalk "^1.0.0"
2882 cli-cursor "^1.0.1"
2883 cli-width "^2.0.0"
2884 figures "^1.3.5"
2885 lodash "^4.3.0"
2886 readline2 "^1.0.1"
2887 run-async "^0.1.0"
2888 rx-lite "^3.1.2"
2889 string-width "^1.0.1"
2890 strip-ansi "^3.0.0"
2891 through "^2.3.6"
2892
2893inquirer@^3.0.6:
2894 version "3.3.0"
2895 resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
2896 integrity sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==
2897 dependencies:
2898 ansi-escapes "^3.0.0"
2899 chalk "^2.0.0"
2900 cli-cursor "^2.1.0"
2901 cli-width "^2.0.0"
2902 external-editor "^2.0.4"
2903 figures "^2.0.0"
2904 lodash "^4.3.0"
2905 mute-stream "0.0.7"
2906 run-async "^2.2.0"
2907 rx-lite "^4.0.8"
2908 rx-lite-aggregates "^4.0.8"
2909 string-width "^2.1.0"
2910 strip-ansi "^4.0.0"
2911 through "^2.3.6"
2912
2913interpret@^1.0.0:
2914 version "1.2.0"
2915 resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
2916 integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
2917 3063
2918invariant@^2.2.2: 3064invariant@^2.2.2, invariant@^2.2.4:
2919 version "2.2.4" 3065 version "2.2.4"
2920 resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" 3066 resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
2921 integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== 3067 integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
2922 dependencies: 3068 dependencies:
2923 loose-envify "^1.0.0" 3069 loose-envify "^1.0.0"
2924 3070
2925invert-kv@^1.0.0:
2926 version "1.0.0"
2927 resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
2928 integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
2929
2930is-absolute-url@^2.0.0:
2931 version "2.1.0"
2932 resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
2933 integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
2934
2935is-accessor-descriptor@^0.1.6: 3071is-accessor-descriptor@^0.1.6:
2936 version "0.1.6" 3072 version "0.1.6"
2937 resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" 3073 resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
@@ -2946,6 +3082,24 @@ is-accessor-descriptor@^1.0.0:
2946 dependencies: 3082 dependencies:
2947 kind-of "^6.0.0" 3083 kind-of "^6.0.0"
2948 3084
3085is-alphabetical@^1.0.0:
3086 version "1.0.4"
3087 resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
3088 integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
3089
3090is-alphanumeric@^1.0.0:
3091 version "1.0.0"
3092 resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4"
3093 integrity sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=
3094
3095is-alphanumerical@^1.0.0:
3096 version "1.0.4"
3097 resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf"
3098 integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==
3099 dependencies:
3100 is-alphabetical "^1.0.0"
3101 is-decimal "^1.0.0"
3102
2949is-arrayish@^0.2.1: 3103is-arrayish@^0.2.1:
2950 version "0.2.1" 3104 version "0.2.1"
2951 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 3105 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -2958,15 +3112,27 @@ is-binary-path@^1.0.0:
2958 dependencies: 3112 dependencies:
2959 binary-extensions "^1.0.0" 3113 binary-extensions "^1.0.0"
2960 3114
3115is-binary-path@~2.1.0:
3116 version "2.1.0"
3117 resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
3118 integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
3119 dependencies:
3120 binary-extensions "^2.0.0"
3121
2961is-buffer@^1.1.5: 3122is-buffer@^1.1.5:
2962 version "1.1.6" 3123 version "1.1.6"
2963 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 3124 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
2964 integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== 3125 integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
2965 3126
2966is-callable@^1.1.4: 3127is-buffer@^2.0.0:
2967 version "1.1.4" 3128 version "2.0.4"
2968 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" 3129 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
2969 integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== 3130 integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
3131
3132is-callable@^1.1.4, is-callable@^1.2.0:
3133 version "1.2.2"
3134 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
3135 integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
2970 3136
2971is-data-descriptor@^0.1.4: 3137is-data-descriptor@^0.1.4:
2972 version "0.1.4" 3138 version "0.1.4"
@@ -2983,9 +3149,14 @@ is-data-descriptor@^1.0.0:
2983 kind-of "^6.0.0" 3149 kind-of "^6.0.0"
2984 3150
2985is-date-object@^1.0.1: 3151is-date-object@^1.0.1:
2986 version "1.0.1" 3152 version "1.0.2"
2987 resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" 3153 resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
2988 integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= 3154 integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
3155
3156is-decimal@^1.0.0, is-decimal@^1.0.2:
3157 version "1.0.4"
3158 resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
3159 integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
2989 3160
2990is-descriptor@^0.1.0: 3161is-descriptor@^0.1.0:
2991 version "0.1.6" 3162 version "0.1.6"
@@ -3022,25 +3193,16 @@ is-extglob@^2.1.0, is-extglob@^2.1.1:
3022 resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 3193 resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
3023 integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 3194 integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
3024 3195
3025is-finite@^1.0.0:
3026 version "1.0.2"
3027 resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
3028 integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=
3029 dependencies:
3030 number-is-nan "^1.0.0"
3031
3032is-fullwidth-code-point@^1.0.0:
3033 version "1.0.0"
3034 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
3035 integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
3036 dependencies:
3037 number-is-nan "^1.0.0"
3038
3039is-fullwidth-code-point@^2.0.0: 3196is-fullwidth-code-point@^2.0.0:
3040 version "2.0.0" 3197 version "2.0.0"
3041 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 3198 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
3042 integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 3199 integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
3043 3200
3201is-fullwidth-code-point@^3.0.0:
3202 version "3.0.0"
3203 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
3204 integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
3205
3044is-glob@^3.1.0: 3206is-glob@^3.1.0:
3045 version "3.1.0" 3207 version "3.1.0"
3046 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" 3208 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
@@ -3048,28 +3210,22 @@ is-glob@^3.1.0:
3048 dependencies: 3210 dependencies:
3049 is-extglob "^2.1.0" 3211 is-extglob "^2.1.0"
3050 3212
3051is-glob@^4.0.0: 3213is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
3052 version "4.0.1" 3214 version "4.0.1"
3053 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" 3215 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
3054 integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== 3216 integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
3055 dependencies: 3217 dependencies:
3056 is-extglob "^2.1.1" 3218 is-extglob "^2.1.1"
3057 3219
3058is-my-ip-valid@^1.0.0: 3220is-hexadecimal@^1.0.0:
3059 version "1.0.0" 3221 version "1.0.4"
3060 resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" 3222 resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
3061 integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== 3223 integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
3062 3224
3063is-my-json-valid@^2.10.0: 3225is-negative-zero@^2.0.0:
3064 version "2.20.0" 3226 version "2.0.0"
3065 resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.0.tgz#1345a6fca3e8daefc10d0fa77067f54cedafd59a" 3227 resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
3066 integrity sha512-XTHBZSIIxNsIsZXg7XB5l8z/OBFosl1Wao4tXLpeC7eKU4Vm/kdop2azkPqULwnfGQjmeDIyey9g7afMMtdWAA== 3228 integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
3067 dependencies:
3068 generate-function "^2.0.0"
3069 generate-object-property "^1.1.0"
3070 is-my-ip-valid "^1.0.0"
3071 jsonpointer "^4.0.0"
3072 xtend "^4.0.0"
3073 3229
3074is-number@^3.0.0: 3230is-number@^3.0.0:
3075 version "3.0.0" 3231 version "3.0.0"
@@ -3078,74 +3234,77 @@ is-number@^3.0.0:
3078 dependencies: 3234 dependencies:
3079 kind-of "^3.0.2" 3235 kind-of "^3.0.2"
3080 3236
3081is-plain-obj@^1.0.0: 3237is-number@^7.0.0:
3238 version "7.0.0"
3239 resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
3240 integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
3241
3242is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
3082 version "1.1.0" 3243 version "1.1.0"
3083 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" 3244 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
3084 integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= 3245 integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
3085 3246
3086is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: 3247is-plain-obj@^2.0.0:
3248 version "2.1.0"
3249 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
3250 integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
3251
3252is-plain-object@^2.0.3, is-plain-object@^2.0.4:
3087 version "2.0.4" 3253 version "2.0.4"
3088 resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" 3254 resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
3089 integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== 3255 integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
3090 dependencies: 3256 dependencies:
3091 isobject "^3.0.1" 3257 isobject "^3.0.1"
3092 3258
3093is-promise@^2.1.0: 3259is-regex@^1.1.0, is-regex@^1.1.1:
3094 version "2.1.0" 3260 version "1.1.1"
3095 resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" 3261 resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
3096 integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= 3262 integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
3097
3098is-property@^1.0.0, is-property@^1.0.2:
3099 version "1.0.2"
3100 resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
3101 integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=
3102
3103is-regex@^1.0.4:
3104 version "1.0.4"
3105 resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
3106 integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
3107 dependencies: 3263 dependencies:
3108 has "^1.0.1" 3264 has-symbols "^1.0.1"
3109
3110is-resolvable@^1.0.0:
3111 version "1.1.0"
3112 resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
3113 integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
3114 3265
3115is-stream@^1.1.0: 3266is-regexp@^2.0.0:
3116 version "1.1.0"
3117 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
3118 integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
3119
3120is-svg@^2.0.0:
3121 version "2.1.0" 3267 version "2.1.0"
3122 resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" 3268 resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
3123 integrity sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk= 3269 integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==
3124 dependencies: 3270
3125 html-comment-regex "^1.1.0" 3271is-string@^1.0.5:
3272 version "1.0.5"
3273 resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
3274 integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
3126 3275
3127is-symbol@^1.0.2: 3276is-symbol@^1.0.2:
3128 version "1.0.2" 3277 version "1.0.3"
3129 resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" 3278 resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
3130 integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== 3279 integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
3131 dependencies: 3280 dependencies:
3132 has-symbols "^1.0.0" 3281 has-symbols "^1.0.1"
3133 3282
3134is-typedarray@~1.0.0: 3283is-typedarray@^1.0.0:
3135 version "1.0.0" 3284 version "1.0.0"
3136 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 3285 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
3137 integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 3286 integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
3138 3287
3139is-utf8@^0.2.0: 3288is-whitespace-character@^1.0.0:
3140 version "0.2.1" 3289 version "1.0.4"
3141 resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" 3290 resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7"
3142 integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= 3291 integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==
3143 3292
3144is-windows@^1.0.2: 3293is-windows@^1.0.1, is-windows@^1.0.2:
3145 version "1.0.2" 3294 version "1.0.2"
3146 resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" 3295 resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
3147 integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== 3296 integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
3148 3297
3298is-word-character@^1.0.0:
3299 version "1.0.4"
3300 resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230"
3301 integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==
3302
3303is-wsl@^1.1.0:
3304 version "1.1.0"
3305 resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
3306 integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
3307
3149isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: 3308isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
3150 version "1.0.0" 3309 version "1.0.0"
3151 resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 3310 resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -3168,99 +3327,58 @@ isobject@^3.0.0, isobject@^3.0.1:
3168 resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" 3327 resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
3169 integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= 3328 integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
3170 3329
3171isstream@~0.1.2: 3330jest-worker@^26.3.0:
3172 version "0.1.2" 3331 version "26.3.0"
3173 resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 3332 resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f"
3174 integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= 3333 integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw==
3175 3334 dependencies:
3176js-base64@^2.1.8, js-base64@^2.1.9: 3335 "@types/node" "*"
3177 version "2.5.1" 3336 merge-stream "^2.0.0"
3178 resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" 3337 supports-color "^7.0.0"
3179 integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==
3180 3338
3181"js-tokens@^3.0.0 || ^4.0.0": 3339"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
3182 version "4.0.0" 3340 version "4.0.0"
3183 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 3341 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
3184 integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 3342 integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
3185 3343
3186js-tokens@^3.0.2: 3344js-yaml@^3.13.1:
3187 version "3.0.2" 3345 version "3.14.0"
3188 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" 3346 resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
3189 integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= 3347 integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
3190
3191js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.9.1:
3192 version "3.13.1"
3193 resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
3194 integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
3195 dependencies: 3348 dependencies:
3196 argparse "^1.0.7" 3349 argparse "^1.0.7"
3197 esprima "^4.0.0" 3350 esprima "^4.0.0"
3198 3351
3199js-yaml@~3.7.0: 3352jsesc@^2.5.1:
3200 version "3.7.0" 3353 version "2.5.2"
3201 resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" 3354 resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
3202 integrity sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A= 3355 integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
3203 dependencies:
3204 argparse "^1.0.7"
3205 esprima "^2.6.0"
3206
3207jsbn@~0.1.0:
3208 version "0.1.1"
3209 resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
3210 integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
3211
3212jsesc@^1.3.0:
3213 version "1.3.0"
3214 resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
3215 integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s=
3216 3356
3217jsesc@~0.5.0: 3357jsesc@~0.5.0:
3218 version "0.5.0" 3358 version "0.5.0"
3219 resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" 3359 resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
3220 integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= 3360 integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
3221 3361
3222json-loader@^0.5.4: 3362json-parse-better-errors@^1.0.2:
3223 version "0.5.7" 3363 version "1.0.2"
3224 resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" 3364 resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
3225 integrity sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w== 3365 integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
3226 3366
3227json-schema-traverse@^0.3.0: 3367json-parse-even-better-errors@^2.3.0:
3228 version "0.3.1" 3368 version "2.3.1"
3229 resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" 3369 resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
3230 integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= 3370 integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
3231 3371
3232json-schema-traverse@^0.4.1: 3372json-schema-traverse@^0.4.1:
3233 version "0.4.1" 3373 version "0.4.1"
3234 resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 3374 resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
3235 integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 3375 integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
3236 3376
3237json-schema@0.2.3:
3238 version "0.2.3"
3239 resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
3240 integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
3241
3242json-stable-stringify-without-jsonify@^1.0.1: 3377json-stable-stringify-without-jsonify@^1.0.1:
3243 version "1.0.1" 3378 version "1.0.1"
3244 resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" 3379 resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
3245 integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= 3380 integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
3246 3381
3247json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
3248 version "1.0.1"
3249 resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
3250 integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
3251 dependencies:
3252 jsonify "~0.0.0"
3253
3254json-stringify-safe@~5.0.1:
3255 version "5.0.1"
3256 resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
3257 integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
3258
3259json5@^0.5.1:
3260 version "0.5.1"
3261 resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
3262 integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
3263
3264json5@^1.0.1: 3382json5@^1.0.1:
3265 version "1.0.1" 3383 version "1.0.1"
3266 resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" 3384 resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
@@ -3268,32 +3386,12 @@ json5@^1.0.1:
3268 dependencies: 3386 dependencies:
3269 minimist "^1.2.0" 3387 minimist "^1.2.0"
3270 3388
3271jsonfile@^3.0.0: 3389json5@^2.1.2:
3272 version "3.0.1" 3390 version "2.1.3"
3273 resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" 3391 resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
3274 integrity sha1-pezG9l9T9mLEQVx2daAzHQmS7GY= 3392 integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
3275 optionalDependencies:
3276 graceful-fs "^4.1.6"
3277
3278jsonify@~0.0.0:
3279 version "0.0.0"
3280 resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
3281 integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
3282
3283jsonpointer@^4.0.0:
3284 version "4.0.1"
3285 resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
3286 integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk=
3287
3288jsprim@^1.2.2:
3289 version "1.4.1"
3290 resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
3291 integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
3292 dependencies: 3393 dependencies:
3293 assert-plus "1.0.0" 3394 minimist "^1.2.5"
3294 extsprintf "1.3.0"
3295 json-schema "0.2.3"
3296 verror "1.10.0"
3297 3395
3298kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: 3396kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
3299 version "3.2.2" 3397 version "3.2.2"
@@ -3314,46 +3412,45 @@ kind-of@^5.0.0:
3314 resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" 3412 resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
3315 integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== 3413 integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
3316 3414
3317kind-of@^6.0.0, kind-of@^6.0.2: 3415kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
3318 version "6.0.2" 3416 version "6.0.3"
3319 resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" 3417 resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
3320 integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== 3418 integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
3321 3419
3322known-css-properties@^0.3.0: 3420klona@^2.0.3:
3323 version "0.3.0" 3421 version "2.0.4"
3324 resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.3.0.tgz#a3d135bbfc60ee8c6eacf2f7e7e6f2d4755e49a4" 3422 resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
3325 integrity sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ== 3423 integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
3326 3424
3327lazy-cache@^1.0.3: 3425known-css-properties@^0.19.0:
3328 version "1.0.4" 3426 version "0.19.0"
3329 resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" 3427 resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.19.0.tgz#5d92b7fa16c72d971bda9b7fe295bdf61836ee5b"
3330 integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= 3428 integrity sha512-eYboRV94Vco725nKMlpkn3nV2+96p9c3gKXRsYqAJSswSENvBhN7n5L+uDhY58xQa0UukWsDMTGELzmD8Q+wTA==
3331 3429
3332lcid@^1.0.0: 3430leven@^3.1.0:
3333 version "1.0.0" 3431 version "3.1.0"
3334 resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" 3432 resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
3335 integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= 3433 integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
3336 dependencies:
3337 invert-kv "^1.0.0"
3338 3434
3339levn@^0.3.0, levn@~0.3.0: 3435levenary@^1.1.1:
3340 version "0.3.0" 3436 version "1.1.1"
3341 resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" 3437 resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77"
3342 integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= 3438 integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==
3343 dependencies: 3439 dependencies:
3344 prelude-ls "~1.1.2" 3440 leven "^3.1.0"
3345 type-check "~0.3.2"
3346 3441
3347load-json-file@^1.0.0: 3442levn@^0.4.1:
3348 version "1.1.0" 3443 version "0.4.1"
3349 resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" 3444 resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
3350 integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= 3445 integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
3351 dependencies: 3446 dependencies:
3352 graceful-fs "^4.1.2" 3447 prelude-ls "^1.2.1"
3353 parse-json "^2.2.0" 3448 type-check "~0.4.0"
3354 pify "^2.0.0" 3449
3355 pinkie-promise "^2.0.0" 3450lines-and-columns@^1.1.6:
3356 strip-bom "^2.0.0" 3451 version "1.1.6"
3452 resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
3453 integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
3357 3454
3358load-json-file@^2.0.0: 3455load-json-file@^2.0.0:
3359 version "2.0.0" 3456 version "2.0.0"
@@ -3365,20 +3462,29 @@ load-json-file@^2.0.0:
3365 pify "^2.0.0" 3462 pify "^2.0.0"
3366 strip-bom "^3.0.0" 3463 strip-bom "^3.0.0"
3367 3464
3368loader-runner@^2.3.0: 3465loader-runner@^2.4.0:
3369 version "2.4.0" 3466 version "2.4.0"
3370 resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" 3467 resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
3371 integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== 3468 integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
3372 3469
3373loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: 3470loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0:
3374 version "1.2.3" 3471 version "1.4.0"
3375 resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" 3472 resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
3376 integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== 3473 integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
3377 dependencies: 3474 dependencies:
3378 big.js "^5.2.2" 3475 big.js "^5.2.2"
3379 emojis-list "^2.0.0" 3476 emojis-list "^3.0.0"
3380 json5 "^1.0.1" 3477 json5 "^1.0.1"
3381 3478
3479loader-utils@^2.0.0:
3480 version "2.0.0"
3481 resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
3482 integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
3483 dependencies:
3484 big.js "^5.2.2"
3485 emojis-list "^3.0.0"
3486 json5 "^2.1.2"
3487
3382locate-path@^2.0.0: 3488locate-path@^2.0.0:
3383 version "2.0.0" 3489 version "2.0.0"
3384 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" 3490 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -3387,55 +3493,37 @@ locate-path@^2.0.0:
3387 p-locate "^2.0.0" 3493 p-locate "^2.0.0"
3388 path-exists "^3.0.0" 3494 path-exists "^3.0.0"
3389 3495
3390lodash.camelcase@^4.3.0: 3496locate-path@^3.0.0:
3391 version "4.3.0" 3497 version "3.0.0"
3392 resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" 3498 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
3393 integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= 3499 integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
3394 3500 dependencies:
3395lodash.capitalize@^4.1.0: 3501 p-locate "^3.0.0"
3396 version "4.2.1" 3502 path-exists "^3.0.0"
3397 resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9"
3398 integrity sha1-+CbJtOKoUR2E46yinbBeGk87cqk=
3399
3400lodash.isplainobject@^4.0.6:
3401 version "4.0.6"
3402 resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
3403 integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
3404
3405lodash.kebabcase@^4.0.0:
3406 version "4.1.1"
3407 resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
3408 integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY=
3409
3410lodash.memoize@^4.1.2:
3411 version "4.1.2"
3412 resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
3413 integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
3414
3415lodash.some@^4.6.0:
3416 version "4.6.0"
3417 resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
3418 integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=
3419 3503
3420lodash.tail@^4.1.1: 3504locate-path@^5.0.0:
3421 version "4.1.1" 3505 version "5.0.0"
3422 resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" 3506 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
3423 integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= 3507 integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
3508 dependencies:
3509 p-locate "^4.1.0"
3424 3510
3425lodash.uniq@^4.5.0: 3511lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
3426 version "4.5.0" 3512 version "4.17.20"
3427 resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" 3513 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
3428 integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= 3514 integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
3429 3515
3430lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.3.0, lodash@~4.17.10: 3516log-symbols@^4.0.0:
3431 version "4.17.15" 3517 version "4.0.0"
3432 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" 3518 resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
3433 integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== 3519 integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
3520 dependencies:
3521 chalk "^4.0.0"
3434 3522
3435longest@^1.0.1: 3523longest-streak@^2.0.1:
3436 version "1.0.1" 3524 version "2.0.4"
3437 resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" 3525 resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
3438 integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= 3526 integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
3439 3527
3440loose-envify@^1.0.0: 3528loose-envify@^1.0.0:
3441 version "1.4.0" 3529 version "1.4.0"
@@ -3444,39 +3532,50 @@ loose-envify@^1.0.0:
3444 dependencies: 3532 dependencies:
3445 js-tokens "^3.0.0 || ^4.0.0" 3533 js-tokens "^3.0.0 || ^4.0.0"
3446 3534
3447loud-rejection@^1.0.0: 3535lru-cache@^5.1.1:
3448 version "1.6.0" 3536 version "5.1.1"
3449 resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" 3537 resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
3450 integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= 3538 integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
3451 dependencies: 3539 dependencies:
3452 currently-unhandled "^0.4.1" 3540 yallist "^3.0.2"
3453 signal-exit "^3.0.0"
3454 3541
3455lru-cache@^4.0.1: 3542lru-cache@^6.0.0:
3456 version "4.1.5" 3543 version "6.0.0"
3457 resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" 3544 resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
3458 integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== 3545 integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
3459 dependencies: 3546 dependencies:
3460 pseudomap "^1.0.2" 3547 yallist "^4.0.0"
3461 yallist "^2.1.2"
3462 3548
3463make-dir@^1.0.0: 3549make-dir@^2.0.0:
3464 version "1.3.0" 3550 version "2.1.0"
3465 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" 3551 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
3466 integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== 3552 integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
3553 dependencies:
3554 pify "^4.0.1"
3555 semver "^5.6.0"
3556
3557make-dir@^3.0.2:
3558 version "3.1.0"
3559 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
3560 integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
3467 dependencies: 3561 dependencies:
3468 pify "^3.0.0" 3562 semver "^6.0.0"
3469 3563
3470map-cache@^0.2.2: 3564map-cache@^0.2.2:
3471 version "0.2.2" 3565 version "0.2.2"
3472 resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" 3566 resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
3473 integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= 3567 integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
3474 3568
3475map-obj@^1.0.0, map-obj@^1.0.1: 3569map-obj@^1.0.0:
3476 version "1.0.1" 3570 version "1.0.1"
3477 resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" 3571 resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
3478 integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= 3572 integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
3479 3573
3574map-obj@^4.0.0:
3575 version "4.1.0"
3576 resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5"
3577 integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==
3578
3480map-visit@^1.0.0: 3579map-visit@^1.0.0:
3481 version "1.0.0" 3580 version "1.0.0"
3482 resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" 3581 resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -3484,10 +3583,22 @@ map-visit@^1.0.0:
3484 dependencies: 3583 dependencies:
3485 object-visit "^1.0.0" 3584 object-visit "^1.0.0"
3486 3585
3487math-expression-evaluator@^1.2.14: 3586markdown-escapes@^1.0.0:
3488 version "1.2.17" 3587 version "1.0.4"
3489 resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" 3588 resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
3490 integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw= 3589 integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
3590
3591markdown-table@^2.0.0:
3592 version "2.0.0"
3593 resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b"
3594 integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==
3595 dependencies:
3596 repeat-string "^1.0.0"
3597
3598mathml-tag-names@^2.1.3:
3599 version "2.1.3"
3600 resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
3601 integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
3491 3602
3492md5.js@^1.3.4: 3603md5.js@^1.3.4:
3493 version "1.3.5" 3604 version "1.3.5"
@@ -3498,14 +3609,14 @@ md5.js@^1.3.4:
3498 inherits "^2.0.1" 3609 inherits "^2.0.1"
3499 safe-buffer "^5.1.2" 3610 safe-buffer "^5.1.2"
3500 3611
3501mem@^1.1.0: 3612mdast-util-compact@^2.0.0:
3502 version "1.1.0" 3613 version "2.0.1"
3503 resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" 3614 resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz#cabc69a2f43103628326f35b1acf735d55c99490"
3504 integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y= 3615 integrity sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA==
3505 dependencies: 3616 dependencies:
3506 mimic-fn "^1.0.0" 3617 unist-util-visit "^2.0.0"
3507 3618
3508memory-fs@^0.4.0, memory-fs@~0.4.1: 3619memory-fs@^0.4.1:
3509 version "0.4.1" 3620 version "0.4.1"
3510 resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" 3621 resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
3511 integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= 3622 integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
@@ -3513,28 +3624,42 @@ memory-fs@^0.4.0, memory-fs@~0.4.1:
3513 errno "^0.1.3" 3624 errno "^0.1.3"
3514 readable-stream "^2.0.1" 3625 readable-stream "^2.0.1"
3515 3626
3516meow@^3.7.0: 3627memory-fs@^0.5.0:
3517 version "3.7.0" 3628 version "0.5.0"
3518 resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" 3629 resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c"
3519 integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= 3630 integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==
3520 dependencies: 3631 dependencies:
3521 camelcase-keys "^2.0.0" 3632 errno "^0.1.3"
3522 decamelize "^1.1.2" 3633 readable-stream "^2.0.1"
3523 loud-rejection "^1.0.0"
3524 map-obj "^1.0.1"
3525 minimist "^1.1.3"
3526 normalize-package-data "^2.3.4"
3527 object-assign "^4.0.1"
3528 read-pkg-up "^1.0.1"
3529 redent "^1.0.0"
3530 trim-newlines "^1.0.0"
3531 3634
3532merge@^1.2.0: 3635meow@^7.1.1:
3533 version "1.2.1" 3636 version "7.1.1"
3534 resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" 3637 resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306"
3535 integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== 3638 integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==
3639 dependencies:
3640 "@types/minimist" "^1.2.0"
3641 camelcase-keys "^6.2.2"
3642 decamelize-keys "^1.1.0"
3643 hard-rejection "^2.1.0"
3644 minimist-options "4.1.0"
3645 normalize-package-data "^2.5.0"
3646 read-pkg-up "^7.0.1"
3647 redent "^3.0.0"
3648 trim-newlines "^3.0.0"
3649 type-fest "^0.13.1"
3650 yargs-parser "^18.1.3"
3651
3652merge-stream@^2.0.0:
3653 version "2.0.0"
3654 resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
3655 integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
3656
3657merge2@^1.3.0:
3658 version "1.4.1"
3659 resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
3660 integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
3536 3661
3537micromatch@^3.1.10, micromatch@^3.1.4: 3662micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
3538 version "3.1.10" 3663 version "3.1.10"
3539 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" 3664 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
3540 integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== 3665 integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
@@ -3553,6 +3678,14 @@ micromatch@^3.1.10, micromatch@^3.1.4:
3553 snapdragon "^0.8.1" 3678 snapdragon "^0.8.1"
3554 to-regex "^3.0.2" 3679 to-regex "^3.0.2"
3555 3680
3681micromatch@^4.0.2:
3682 version "4.0.2"
3683 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
3684 integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
3685 dependencies:
3686 braces "^3.0.1"
3687 picomatch "^2.0.5"
3688
3556miller-rabin@^4.0.0: 3689miller-rabin@^4.0.0:
3557 version "4.0.1" 3690 version "4.0.1"
3558 resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" 3691 resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -3561,27 +3694,20 @@ miller-rabin@^4.0.0:
3561 bn.js "^4.0.0" 3694 bn.js "^4.0.0"
3562 brorand "^1.0.1" 3695 brorand "^1.0.1"
3563 3696
3564mime-db@1.40.0: 3697min-indent@^1.0.0:
3565 version "1.40.0" 3698 version "1.0.1"
3566 resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" 3699 resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
3567 integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== 3700 integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
3568 3701
3569mime-types@^2.1.12, mime-types@~2.1.19: 3702mini-css-extract-plugin@^0.11.2:
3570 version "2.1.24" 3703 version "0.11.2"
3571 resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" 3704 resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.2.tgz#e3af4d5e04fbcaaf11838ab230510073060b37bf"
3572 integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== 3705 integrity sha512-h2LknfX4U1kScXxH8xE9LCOqT5B+068EAj36qicMb8l4dqdJoyHcmWmpd+ueyZfgu/POvIn+teoUnTtei2ikug==
3573 dependencies: 3706 dependencies:
3574 mime-db "1.40.0" 3707 loader-utils "^1.1.0"
3575 3708 normalize-url "1.9.1"
3576mime@^1.4.1: 3709 schema-utils "^1.0.0"
3577 version "1.6.0" 3710 webpack-sources "^1.1.0"
3578 resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
3579 integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
3580
3581mimic-fn@^1.0.0:
3582 version "1.2.0"
3583 resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
3584 integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
3585 3711
3586minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: 3712minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
3587 version "1.0.1" 3713 version "1.0.1"
@@ -3593,42 +3719,78 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
3593 resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" 3719 resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
3594 integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= 3720 integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
3595 3721
3596minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: 3722minimatch@^3.0.4:
3597 version "3.0.4" 3723 version "3.0.4"
3598 resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 3724 resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
3599 integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 3725 integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
3600 dependencies: 3726 dependencies:
3601 brace-expansion "^1.1.7" 3727 brace-expansion "^1.1.7"
3602 3728
3603minimist@0.0.8: 3729minimist-options@4.1.0:
3604 version "0.0.8" 3730 version "4.1.0"
3605 resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 3731 resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
3606 integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 3732 integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
3733 dependencies:
3734 arrify "^1.0.1"
3735 is-plain-obj "^1.1.0"
3736 kind-of "^6.0.3"
3607 3737
3608minimist@1.1.x: 3738minimist@^1.2.0, minimist@^1.2.5:
3609 version "1.1.3" 3739 version "1.2.5"
3610 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" 3740 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
3611 integrity sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag= 3741 integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
3612 3742
3613minimist@^1.1.3, minimist@^1.2.0: 3743minipass-collect@^1.0.2:
3614 version "1.2.0" 3744 version "1.0.2"
3615 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 3745 resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
3616 integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= 3746 integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
3747 dependencies:
3748 minipass "^3.0.0"
3749
3750minipass-flush@^1.0.5:
3751 version "1.0.5"
3752 resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
3753 integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
3754 dependencies:
3755 minipass "^3.0.0"
3617 3756
3618minipass@^2.2.1, minipass@^2.3.4: 3757minipass-pipeline@^1.2.2:
3619 version "2.3.5" 3758 version "1.2.4"
3620 resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" 3759 resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
3621 integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== 3760 integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
3622 dependencies: 3761 dependencies:
3623 safe-buffer "^5.1.2" 3762 minipass "^3.0.0"
3624 yallist "^3.0.0"
3625 3763
3626minizlib@^1.1.1: 3764minipass@^3.0.0, minipass@^3.1.1:
3627 version "1.2.1" 3765 version "3.1.3"
3628 resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" 3766 resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
3629 integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== 3767 integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
3768 dependencies:
3769 yallist "^4.0.0"
3770
3771minizlib@^2.1.1:
3772 version "2.1.2"
3773 resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
3774 integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
3630 dependencies: 3775 dependencies:
3631 minipass "^2.2.1" 3776 minipass "^3.0.0"
3777 yallist "^4.0.0"
3778
3779mississippi@^3.0.0:
3780 version "3.0.0"
3781 resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
3782 integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
3783 dependencies:
3784 concat-stream "^1.5.0"
3785 duplexify "^3.4.2"
3786 end-of-stream "^1.1.0"
3787 flush-write-stream "^1.0.0"
3788 from2 "^2.1.0"
3789 parallel-transform "^1.1.0"
3790 pump "^3.0.0"
3791 pumpify "^1.3.3"
3792 stream-each "^1.1.0"
3793 through2 "^2.0.0"
3632 3794
3633mixin-deep@^1.2.0: 3795mixin-deep@^1.2.0:
3634 version "1.3.2" 3796 version "1.3.2"
@@ -3638,45 +3800,44 @@ mixin-deep@^1.2.0:
3638 for-in "^1.0.2" 3800 for-in "^1.0.2"
3639 is-extendable "^1.0.1" 3801 is-extendable "^1.0.1"
3640 3802
3641mixin-object@^2.0.1: 3803mkdirp@^0.5.1, mkdirp@^0.5.3:
3642 version "2.0.1" 3804 version "0.5.5"
3643 resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" 3805 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
3644 integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= 3806 integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
3645 dependencies: 3807 dependencies:
3646 for-in "^0.1.3" 3808 minimist "^1.2.5"
3647 is-extendable "^0.1.1"
3648 3809
3649"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: 3810mkdirp@^1.0.3, mkdirp@^1.0.4:
3650 version "0.5.1" 3811 version "1.0.4"
3651 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 3812 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
3652 integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= 3813 integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
3814
3815move-concurrently@^1.0.1:
3816 version "1.0.1"
3817 resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
3818 integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
3653 dependencies: 3819 dependencies:
3654 minimist "0.0.8" 3820 aproba "^1.1.1"
3821 copy-concurrently "^1.0.0"
3822 fs-write-stream-atomic "^1.0.8"
3823 mkdirp "^0.5.1"
3824 rimraf "^2.5.4"
3825 run-queue "^1.0.3"
3655 3826
3656ms@2.0.0: 3827ms@2.0.0:
3657 version "2.0.0" 3828 version "2.0.0"
3658 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 3829 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
3659 integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 3830 integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
3660 3831
3661ms@^2.1.1: 3832ms@2.1.2:
3662 version "2.1.1" 3833 version "2.1.2"
3663 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 3834 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
3664 integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 3835 integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
3665
3666mute-stream@0.0.5:
3667 version "0.0.5"
3668 resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
3669 integrity sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=
3670
3671mute-stream@0.0.7:
3672 version "0.0.7"
3673 resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
3674 integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
3675 3836
3676nan@^2.12.1, nan@^2.13.2: 3837nan@^2.12.1:
3677 version "2.14.0" 3838 version "2.14.1"
3678 resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" 3839 resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
3679 integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== 3840 integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
3680 3841
3681nanomatch@^1.2.9: 3842nanomatch@^1.2.9:
3682 version "1.2.13" 3843 version "1.2.13"
@@ -3700,47 +3861,20 @@ natural-compare@^1.4.0:
3700 resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" 3861 resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
3701 integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= 3862 integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
3702 3863
3703needle@^2.2.1: 3864neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2:
3704 version "2.4.0" 3865 version "2.6.2"
3705 resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" 3866 resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
3706 integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== 3867 integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
3707 dependencies:
3708 debug "^3.2.6"
3709 iconv-lite "^0.4.4"
3710 sax "^1.2.4"
3711
3712neo-async@^2.5.0:
3713 version "2.6.1"
3714 resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
3715 integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
3716 3868
3717next-tick@^1.0.0: 3869nice-try@^1.0.4:
3718 version "1.0.0" 3870 version "1.0.5"
3719 resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" 3871 resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
3720 integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= 3872 integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
3721 3873
3722node-gyp@^3.8.0: 3874node-libs-browser@^2.2.1:
3723 version "3.8.0" 3875 version "2.2.1"
3724 resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" 3876 resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
3725 integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== 3877 integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==
3726 dependencies:
3727 fstream "^1.0.0"
3728 glob "^7.0.3"
3729 graceful-fs "^4.1.2"
3730 mkdirp "^0.5.0"
3731 nopt "2 || 3"
3732 npmlog "0 || 1 || 2 || 3 || 4"
3733 osenv "0"
3734 request "^2.87.0"
3735 rimraf "2"
3736 semver "~5.3.0"
3737 tar "^2.0.0"
3738 which "1"
3739
3740node-libs-browser@^2.0.0:
3741 version "2.2.0"
3742 resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.0.tgz#c72f60d9d46de08a940dedbb25f3ffa2f9bbaa77"
3743 integrity sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==
3744 dependencies: 3878 dependencies:
3745 assert "^1.1.1" 3879 assert "^1.1.1"
3746 browserify-zlib "^0.2.0" 3880 browserify-zlib "^0.2.0"
@@ -3752,7 +3886,7 @@ node-libs-browser@^2.0.0:
3752 events "^3.0.0" 3886 events "^3.0.0"
3753 https-browserify "^1.0.0" 3887 https-browserify "^1.0.0"
3754 os-browserify "^0.3.0" 3888 os-browserify "^0.3.0"
3755 path-browserify "0.0.0" 3889 path-browserify "0.0.1"
3756 process "^0.11.10" 3890 process "^0.11.10"
3757 punycode "^1.2.4" 3891 punycode "^1.2.4"
3758 querystring-es3 "^0.2.0" 3892 querystring-es3 "^0.2.0"
@@ -3764,63 +3898,14 @@ node-libs-browser@^2.0.0:
3764 tty-browserify "0.0.0" 3898 tty-browserify "0.0.0"
3765 url "^0.11.0" 3899 url "^0.11.0"
3766 util "^0.11.0" 3900 util "^0.11.0"
3767 vm-browserify "0.0.4" 3901 vm-browserify "^1.0.1"
3768 3902
3769node-pre-gyp@^0.12.0: 3903node-releases@^1.1.61:
3770 version "0.12.0" 3904 version "1.1.61"
3771 resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" 3905 resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e"
3772 integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== 3906 integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==
3773 dependencies:
3774 detect-libc "^1.0.2"
3775 mkdirp "^0.5.1"
3776 needle "^2.2.1"
3777 nopt "^4.0.1"
3778 npm-packlist "^1.1.6"
3779 npmlog "^4.0.2"
3780 rc "^1.2.7"
3781 rimraf "^2.6.1"
3782 semver "^5.3.0"
3783 tar "^4"
3784
3785node-sass@^4.12.0:
3786 version "4.12.0"
3787 resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017"
3788 integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==
3789 dependencies:
3790 async-foreach "^0.1.3"
3791 chalk "^1.1.1"
3792 cross-spawn "^3.0.0"
3793 gaze "^1.0.0"
3794 get-stdin "^4.0.1"
3795 glob "^7.0.3"
3796 in-publish "^2.0.0"
3797 lodash "^4.17.11"
3798 meow "^3.7.0"
3799 mkdirp "^0.5.1"
3800 nan "^2.13.2"
3801 node-gyp "^3.8.0"
3802 npmlog "^4.0.0"
3803 request "^2.88.0"
3804 sass-graph "^2.2.4"
3805 stdout-stream "^1.4.0"
3806 "true-case-path" "^1.0.2"
3807
3808"nopt@2 || 3":
3809 version "3.0.6"
3810 resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
3811 integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k=
3812 dependencies:
3813 abbrev "1"
3814 3907
3815nopt@^4.0.1: 3908normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
3816 version "4.0.1"
3817 resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
3818 integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
3819 dependencies:
3820 abbrev "1"
3821 osenv "^0.1.4"
3822
3823normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
3824 version "2.5.0" 3909 version "2.5.0"
3825 resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" 3910 resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
3826 integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== 3911 integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
@@ -3837,7 +3922,7 @@ normalize-path@^2.1.1:
3837 dependencies: 3922 dependencies:
3838 remove-trailing-separator "^1.0.1" 3923 remove-trailing-separator "^1.0.1"
3839 3924
3840normalize-path@^3.0.0: 3925normalize-path@^3.0.0, normalize-path@~3.0.0:
3841 version "3.0.0" 3926 version "3.0.0"
3842 resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 3927 resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
3843 integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 3928 integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@@ -3847,7 +3932,12 @@ normalize-range@^0.1.2:
3847 resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" 3932 resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
3848 integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= 3933 integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
3849 3934
3850normalize-url@^1.4.0: 3935normalize-selector@^0.2.0:
3936 version "0.2.0"
3937 resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03"
3938 integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=
3939
3940normalize-url@1.9.1:
3851 version "1.9.1" 3941 version "1.9.1"
3852 resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" 3942 resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
3853 integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= 3943 integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=
@@ -3857,51 +3947,11 @@ normalize-url@^1.4.0:
3857 query-string "^4.1.0" 3947 query-string "^4.1.0"
3858 sort-keys "^1.0.0" 3948 sort-keys "^1.0.0"
3859 3949
3860npm-bundled@^1.0.1:
3861 version "1.0.6"
3862 resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd"
3863 integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==
3864
3865npm-packlist@^1.1.6:
3866 version "1.4.1"
3867 resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc"
3868 integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==
3869 dependencies:
3870 ignore-walk "^3.0.1"
3871 npm-bundled "^1.0.1"
3872
3873npm-run-path@^2.0.0:
3874 version "2.0.2"
3875 resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
3876 integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
3877 dependencies:
3878 path-key "^2.0.0"
3879
3880"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2:
3881 version "4.1.2"
3882 resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
3883 integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
3884 dependencies:
3885 are-we-there-yet "~1.1.2"
3886 console-control-strings "~1.1.0"
3887 gauge "~2.7.3"
3888 set-blocking "~2.0.0"
3889
3890num2fraction@^1.2.2: 3950num2fraction@^1.2.2:
3891 version "1.2.2" 3951 version "1.2.2"
3892 resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" 3952 resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
3893 integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= 3953 integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
3894 3954
3895number-is-nan@^1.0.0:
3896 version "1.0.1"
3897 resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
3898 integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
3899
3900oauth-sign@~0.9.0:
3901 version "0.9.0"
3902 resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
3903 integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
3904
3905object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: 3955object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
3906 version "4.1.1" 3956 version "4.1.1"
3907 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 3957 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -3916,7 +3966,12 @@ object-copy@^0.1.0:
3916 define-property "^0.2.5" 3966 define-property "^0.2.5"
3917 kind-of "^3.0.3" 3967 kind-of "^3.0.3"
3918 3968
3919object-keys@^1.0.12: 3969object-inspect@^1.7.0, object-inspect@^1.8.0:
3970 version "1.8.0"
3971 resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
3972 integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
3973
3974object-keys@^1.0.12, object-keys@^1.1.1:
3920 version "1.1.1" 3975 version "1.1.1"
3921 resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" 3976 resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
3922 integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== 3977 integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@@ -3928,6 +3983,25 @@ object-visit@^1.0.0:
3928 dependencies: 3983 dependencies:
3929 isobject "^3.0.0" 3984 isobject "^3.0.0"
3930 3985
3986object.assign@^4.1.0:
3987 version "4.1.1"
3988 resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd"
3989 integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==
3990 dependencies:
3991 define-properties "^1.1.3"
3992 es-abstract "^1.18.0-next.0"
3993 has-symbols "^1.0.1"
3994 object-keys "^1.1.1"
3995
3996object.entries@^1.1.2:
3997 version "1.1.2"
3998 resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add"
3999 integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==
4000 dependencies:
4001 define-properties "^1.1.3"
4002 es-abstract "^1.17.5"
4003 has "^1.0.3"
4004
3931object.pick@^1.3.0: 4005object.pick@^1.3.0:
3932 version "1.3.0" 4006 version "1.3.0"
3933 resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" 4007 resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
@@ -3935,81 +4009,40 @@ object.pick@^1.3.0:
3935 dependencies: 4009 dependencies:
3936 isobject "^3.0.1" 4010 isobject "^3.0.1"
3937 4011
3938once@^1.3.0: 4012object.values@^1.1.1:
4013 version "1.1.1"
4014 resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
4015 integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
4016 dependencies:
4017 define-properties "^1.1.3"
4018 es-abstract "^1.17.0-next.1"
4019 function-bind "^1.1.1"
4020 has "^1.0.3"
4021
4022once@^1.3.0, once@^1.3.1, once@^1.4.0:
3939 version "1.4.0" 4023 version "1.4.0"
3940 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 4024 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
3941 integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 4025 integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
3942 dependencies: 4026 dependencies:
3943 wrappy "1" 4027 wrappy "1"
3944 4028
3945onetime@^1.0.0: 4029optionator@^0.9.1:
3946 version "1.1.0" 4030 version "0.9.1"
3947 resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" 4031 resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
3948 integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= 4032 integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
3949
3950onetime@^2.0.0:
3951 version "2.0.1"
3952 resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
3953 integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
3954 dependencies: 4033 dependencies:
3955 mimic-fn "^1.0.0" 4034 deep-is "^0.1.3"
3956 4035 fast-levenshtein "^2.0.6"
3957optionator@^0.8.1, optionator@^0.8.2: 4036 levn "^0.4.1"
3958 version "0.8.2" 4037 prelude-ls "^1.2.1"
3959 resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" 4038 type-check "^0.4.0"
3960 integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= 4039 word-wrap "^1.2.3"
3961 dependencies:
3962 deep-is "~0.1.3"
3963 fast-levenshtein "~2.0.4"
3964 levn "~0.3.0"
3965 prelude-ls "~1.1.2"
3966 type-check "~0.3.2"
3967 wordwrap "~1.0.0"
3968 4040
3969os-browserify@^0.3.0: 4041os-browserify@^0.3.0:
3970 version "0.3.0" 4042 version "0.3.0"
3971 resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" 4043 resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
3972 integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= 4044 integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
3973 4045
3974os-homedir@^1.0.0:
3975 version "1.0.2"
3976 resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
3977 integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
3978
3979os-locale@^1.4.0:
3980 version "1.4.0"
3981 resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
3982 integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=
3983 dependencies:
3984 lcid "^1.0.0"
3985
3986os-locale@^2.0.0:
3987 version "2.1.0"
3988 resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
3989 integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==
3990 dependencies:
3991 execa "^0.7.0"
3992 lcid "^1.0.0"
3993 mem "^1.1.0"
3994
3995os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
3996 version "1.0.2"
3997 resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
3998 integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
3999
4000osenv@0, osenv@^0.1.4:
4001 version "0.1.5"
4002 resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
4003 integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
4004 dependencies:
4005 os-homedir "^1.0.0"
4006 os-tmpdir "^1.0.0"
4007
4008p-finally@^1.0.0:
4009 version "1.0.0"
4010 resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
4011 integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
4012
4013p-limit@^1.1.0: 4046p-limit@^1.1.0:
4014 version "1.3.0" 4047 version "1.3.0"
4015 resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" 4048 resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@@ -4017,6 +4050,20 @@ p-limit@^1.1.0:
4017 dependencies: 4050 dependencies:
4018 p-try "^1.0.0" 4051 p-try "^1.0.0"
4019 4052
4053p-limit@^2.0.0, p-limit@^2.2.0:
4054 version "2.3.0"
4055 resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
4056 integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
4057 dependencies:
4058 p-try "^2.0.0"
4059
4060p-limit@^3.0.2:
4061 version "3.0.2"
4062 resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe"
4063 integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==
4064 dependencies:
4065 p-try "^2.0.0"
4066
4020p-locate@^2.0.0: 4067p-locate@^2.0.0:
4021 version "2.0.0" 4068 version "2.0.0"
4022 resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" 4069 resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
@@ -4024,28 +4071,81 @@ p-locate@^2.0.0:
4024 dependencies: 4071 dependencies:
4025 p-limit "^1.1.0" 4072 p-limit "^1.1.0"
4026 4073
4074p-locate@^3.0.0:
4075 version "3.0.0"
4076 resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
4077 integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
4078 dependencies:
4079 p-limit "^2.0.0"
4080
4081p-locate@^4.1.0:
4082 version "4.1.0"
4083 resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
4084 integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
4085 dependencies:
4086 p-limit "^2.2.0"
4087
4088p-map@^4.0.0:
4089 version "4.0.0"
4090 resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
4091 integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
4092 dependencies:
4093 aggregate-error "^3.0.0"
4094
4027p-try@^1.0.0: 4095p-try@^1.0.0:
4028 version "1.0.0" 4096 version "1.0.0"
4029 resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" 4097 resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
4030 integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= 4098 integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
4031 4099
4100p-try@^2.0.0:
4101 version "2.2.0"
4102 resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
4103 integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
4104
4032pako@~1.0.5: 4105pako@~1.0.5:
4033 version "1.0.10" 4106 version "1.0.11"
4034 resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" 4107 resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
4035 integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== 4108 integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
4036 4109
4037parse-asn1@^5.0.0: 4110parallel-transform@^1.1.0:
4038 version "5.1.4" 4111 version "1.2.0"
4039 resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" 4112 resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
4040 integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== 4113 integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==
4114 dependencies:
4115 cyclist "^1.0.1"
4116 inherits "^2.0.3"
4117 readable-stream "^2.1.5"
4118
4119parent-module@^1.0.0:
4120 version "1.0.1"
4121 resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
4122 integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
4123 dependencies:
4124 callsites "^3.0.0"
4125
4126parse-asn1@^5.0.0, parse-asn1@^5.1.5:
4127 version "5.1.6"
4128 resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
4129 integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==
4041 dependencies: 4130 dependencies:
4042 asn1.js "^4.0.0" 4131 asn1.js "^5.2.0"
4043 browserify-aes "^1.0.0" 4132 browserify-aes "^1.0.0"
4044 create-hash "^1.1.0"
4045 evp_bytestokey "^1.0.0" 4133 evp_bytestokey "^1.0.0"
4046 pbkdf2 "^3.0.3" 4134 pbkdf2 "^3.0.3"
4047 safe-buffer "^5.1.1" 4135 safe-buffer "^5.1.1"
4048 4136
4137parse-entities@^2.0.0:
4138 version "2.0.0"
4139 resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
4140 integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
4141 dependencies:
4142 character-entities "^1.0.0"
4143 character-entities-legacy "^1.0.0"
4144 character-reference-invalid "^1.0.0"
4145 is-alphanumerical "^1.0.0"
4146 is-decimal "^1.0.0"
4147 is-hexadecimal "^1.0.0"
4148
4049parse-json@^2.2.0: 4149parse-json@^2.2.0:
4050 version "2.2.0" 4150 version "2.2.0"
4051 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" 4151 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
@@ -4053,62 +4153,66 @@ parse-json@^2.2.0:
4053 dependencies: 4153 dependencies:
4054 error-ex "^1.2.0" 4154 error-ex "^1.2.0"
4055 4155
4156parse-json@^5.0.0:
4157 version "5.1.0"
4158 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646"
4159 integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==
4160 dependencies:
4161 "@babel/code-frame" "^7.0.0"
4162 error-ex "^1.3.1"
4163 json-parse-even-better-errors "^2.3.0"
4164 lines-and-columns "^1.1.6"
4165
4166parse-passwd@^1.0.0:
4167 version "1.0.0"
4168 resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
4169 integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
4170
4056pascalcase@^0.1.1: 4171pascalcase@^0.1.1:
4057 version "0.1.1" 4172 version "0.1.1"
4058 resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" 4173 resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
4059 integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= 4174 integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
4060 4175
4061path-browserify@0.0.0: 4176path-browserify@0.0.1:
4062 version "0.0.0" 4177 version "0.0.1"
4063 resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" 4178 resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
4064 integrity sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo= 4179 integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==
4065 4180
4066path-dirname@^1.0.0: 4181path-dirname@^1.0.0:
4067 version "1.0.2" 4182 version "1.0.2"
4068 resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" 4183 resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
4069 integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= 4184 integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
4070 4185
4071path-exists@^2.0.0:
4072 version "2.1.0"
4073 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
4074 integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
4075 dependencies:
4076 pinkie-promise "^2.0.0"
4077
4078path-exists@^3.0.0: 4186path-exists@^3.0.0:
4079 version "3.0.0" 4187 version "3.0.0"
4080 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 4188 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
4081 integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= 4189 integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
4082 4190
4083path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: 4191path-exists@^4.0.0:
4192 version "4.0.0"
4193 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
4194 integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
4195
4196path-is-absolute@^1.0.0:
4084 version "1.0.1" 4197 version "1.0.1"
4085 resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 4198 resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
4086 integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 4199 integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
4087 4200
4088path-is-inside@^1.0.1, path-is-inside@^1.0.2: 4201path-key@^2.0.1:
4089 version "1.0.2"
4090 resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
4091 integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
4092
4093path-key@^2.0.0:
4094 version "2.0.1" 4202 version "2.0.1"
4095 resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 4203 resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
4096 integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= 4204 integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
4097 4205
4206path-key@^3.1.0:
4207 version "3.1.1"
4208 resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
4209 integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
4210
4098path-parse@^1.0.6: 4211path-parse@^1.0.6:
4099 version "1.0.6" 4212 version "1.0.6"
4100 resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 4213 resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
4101 integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 4214 integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
4102 4215
4103path-type@^1.0.0:
4104 version "1.1.0"
4105 resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
4106 integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
4107 dependencies:
4108 graceful-fs "^4.1.2"
4109 pify "^2.0.0"
4110 pinkie-promise "^2.0.0"
4111
4112path-type@^2.0.0: 4216path-type@^2.0.0:
4113 version "2.0.0" 4217 version "2.0.0"
4114 resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" 4218 resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@@ -4116,10 +4220,15 @@ path-type@^2.0.0:
4116 dependencies: 4220 dependencies:
4117 pify "^2.0.0" 4221 pify "^2.0.0"
4118 4222
4223path-type@^4.0.0:
4224 version "4.0.0"
4225 resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
4226 integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
4227
4119pbkdf2@^3.0.3: 4228pbkdf2@^3.0.3:
4120 version "3.0.17" 4229 version "3.1.1"
4121 resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" 4230 resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94"
4122 integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== 4231 integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==
4123 dependencies: 4232 dependencies:
4124 create-hash "^1.1.2" 4233 create-hash "^1.1.2"
4125 create-hmac "^1.1.4" 4234 create-hmac "^1.1.4"
@@ -4127,32 +4236,20 @@ pbkdf2@^3.0.3:
4127 safe-buffer "^5.0.1" 4236 safe-buffer "^5.0.1"
4128 sha.js "^2.4.8" 4237 sha.js "^2.4.8"
4129 4238
4130performance-now@^2.1.0: 4239picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
4131 version "2.1.0" 4240 version "2.2.2"
4132 resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 4241 resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
4133 integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= 4242 integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
4134 4243
4135pify@^2.0.0: 4244pify@^2.0.0:
4136 version "2.3.0" 4245 version "2.3.0"
4137 resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 4246 resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
4138 integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= 4247 integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
4139 4248
4140pify@^3.0.0: 4249pify@^4.0.1:
4141 version "3.0.0" 4250 version "4.0.1"
4142 resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" 4251 resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
4143 integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= 4252 integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
4144
4145pinkie-promise@^2.0.0:
4146 version "2.0.1"
4147 resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
4148 integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
4149 dependencies:
4150 pinkie "^2.0.0"
4151
4152pinkie@^2.0.0:
4153 version "2.0.4"
4154 resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
4155 integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
4156 4253
4157pkg-dir@^2.0.0: 4254pkg-dir@^2.0.0:
4158 version "2.0.0" 4255 version "2.0.0"
@@ -4161,350 +4258,168 @@ pkg-dir@^2.0.0:
4161 dependencies: 4258 dependencies:
4162 find-up "^2.1.0" 4259 find-up "^2.1.0"
4163 4260
4164pluralize@^1.2.1: 4261pkg-dir@^3.0.0:
4165 version "1.2.1" 4262 version "3.0.0"
4166 resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" 4263 resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
4167 integrity sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU= 4264 integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
4265 dependencies:
4266 find-up "^3.0.0"
4168 4267
4169pluralize@^7.0.0: 4268pkg-dir@^4.1.0:
4170 version "7.0.0" 4269 version "4.2.0"
4171 resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" 4270 resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
4172 integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== 4271 integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
4272 dependencies:
4273 find-up "^4.0.0"
4173 4274
4174posix-character-classes@^0.1.0: 4275posix-character-classes@^0.1.0:
4175 version "0.1.1" 4276 version "0.1.1"
4176 resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" 4277 resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
4177 integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= 4278 integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
4178 4279
4179postcss-calc@^5.2.0: 4280postcss-html@^0.36.0:
4180 version "5.3.1" 4281 version "0.36.0"
4181 resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" 4282 resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.36.0.tgz#b40913f94eaacc2453fd30a1327ad6ee1f88b204"
4182 integrity sha1-d7rnypKK2FcW4v2kLyYb98HWW14= 4283 integrity sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==
4183 dependencies:
4184 postcss "^5.0.2"
4185 postcss-message-helpers "^2.0.0"
4186 reduce-css-calc "^1.2.6"
4187
4188postcss-colormin@^2.1.8:
4189 version "2.2.2"
4190 resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b"
4191 integrity sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=
4192 dependencies:
4193 colormin "^1.0.5"
4194 postcss "^5.0.13"
4195 postcss-value-parser "^3.2.3"
4196
4197postcss-convert-values@^2.3.4:
4198 version "2.6.1"
4199 resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d"
4200 integrity sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=
4201 dependencies: 4284 dependencies:
4202 postcss "^5.0.11" 4285 htmlparser2 "^3.10.0"
4203 postcss-value-parser "^3.1.2"
4204 4286
4205postcss-discard-comments@^2.0.4: 4287postcss-less@^3.1.4:
4206 version "2.0.4" 4288 version "3.1.4"
4207 resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" 4289 resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.1.4.tgz#369f58642b5928ef898ffbc1a6e93c958304c5ad"
4208 integrity sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0= 4290 integrity sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==
4209 dependencies: 4291 dependencies:
4210 postcss "^5.0.14" 4292 postcss "^7.0.14"
4211 4293
4212postcss-discard-duplicates@^2.0.1: 4294postcss-media-query-parser@^0.2.3:
4213 version "2.1.0" 4295 version "0.2.3"
4214 resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" 4296 resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
4215 integrity sha1-uavye4isGIFYpesSq8riAmO5GTI= 4297 integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=
4216 dependencies:
4217 postcss "^5.0.4"
4218
4219postcss-discard-empty@^2.0.1:
4220 version "2.1.0"
4221 resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5"
4222 integrity sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=
4223 dependencies:
4224 postcss "^5.0.14"
4225
4226postcss-discard-overridden@^0.1.1:
4227 version "0.1.1"
4228 resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58"
4229 integrity sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=
4230 dependencies:
4231 postcss "^5.0.16"
4232
4233postcss-discard-unused@^2.2.1:
4234 version "2.2.3"
4235 resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433"
4236 integrity sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=
4237 dependencies:
4238 postcss "^5.0.14"
4239 uniqs "^2.0.0"
4240
4241postcss-filter-plugins@^2.0.0:
4242 version "2.0.3"
4243 resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz#82245fdf82337041645e477114d8e593aa18b8ec"
4244 integrity sha512-T53GVFsdinJhgwm7rg1BzbeBRomOg9y5MBVhGcsV0CxurUdVj1UlPdKtn7aqYA/c/QVkzKMjq2bSV5dKG5+AwQ==
4245 dependencies:
4246 postcss "^5.0.4"
4247
4248postcss-merge-idents@^2.1.5:
4249 version "2.1.7"
4250 resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270"
4251 integrity sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=
4252 dependencies:
4253 has "^1.0.1"
4254 postcss "^5.0.10"
4255 postcss-value-parser "^3.1.1"
4256
4257postcss-merge-longhand@^2.0.1:
4258 version "2.0.2"
4259 resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658"
4260 integrity sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=
4261 dependencies:
4262 postcss "^5.0.4"
4263
4264postcss-merge-rules@^2.0.3:
4265 version "2.1.2"
4266 resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721"
4267 integrity sha1-0d9d+qexrMO+VT8OnhDofGG19yE=
4268 dependencies:
4269 browserslist "^1.5.2"
4270 caniuse-api "^1.5.2"
4271 postcss "^5.0.4"
4272 postcss-selector-parser "^2.2.2"
4273 vendors "^1.0.0"
4274 4298
4275postcss-message-helpers@^2.0.0: 4299postcss-modules-extract-imports@^2.0.0:
4276 version "2.0.0" 4300 version "2.0.0"
4277 resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" 4301 resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
4278 integrity sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4= 4302 integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
4279
4280postcss-minify-font-values@^1.0.2:
4281 version "1.0.5"
4282 resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69"
4283 integrity sha1-S1jttWZB66fIR0qzUmyv17vey2k=
4284 dependencies:
4285 object-assign "^4.0.1"
4286 postcss "^5.0.4"
4287 postcss-value-parser "^3.0.2"
4288
4289postcss-minify-gradients@^1.0.1:
4290 version "1.0.5"
4291 resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1"
4292 integrity sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=
4293 dependencies:
4294 postcss "^5.0.12"
4295 postcss-value-parser "^3.3.0"
4296
4297postcss-minify-params@^1.0.4:
4298 version "1.2.2"
4299 resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3"
4300 integrity sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=
4301 dependencies:
4302 alphanum-sort "^1.0.1"
4303 postcss "^5.0.2"
4304 postcss-value-parser "^3.0.2"
4305 uniqs "^2.0.0"
4306
4307postcss-minify-selectors@^2.0.4:
4308 version "2.1.1"
4309 resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf"
4310 integrity sha1-ssapjAByz5G5MtGkllCBFDEXNb8=
4311 dependencies:
4312 alphanum-sort "^1.0.2"
4313 has "^1.0.1"
4314 postcss "^5.0.14"
4315 postcss-selector-parser "^2.0.0"
4316
4317postcss-modules-extract-imports@^1.2.0:
4318 version "1.2.1"
4319 resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz#dc87e34148ec7eab5f791f7cd5849833375b741a"
4320 integrity sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==
4321 dependencies: 4303 dependencies:
4322 postcss "^6.0.1" 4304 postcss "^7.0.5"
4323 4305
4324postcss-modules-local-by-default@^1.2.0: 4306postcss-modules-local-by-default@^3.0.3:
4325 version "1.2.0" 4307 version "3.0.3"
4326 resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" 4308 resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0"
4327 integrity sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk= 4309 integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==
4328 dependencies:
4329 css-selector-tokenizer "^0.7.0"
4330 postcss "^6.0.1"
4331
4332postcss-modules-scope@^1.1.0:
4333 version "1.1.0"
4334 resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
4335 integrity sha1-1upkmUx5+XtipytCb75gVqGUu5A=
4336 dependencies:
4337 css-selector-tokenizer "^0.7.0"
4338 postcss "^6.0.1"
4339
4340postcss-modules-values@^1.3.0:
4341 version "1.3.0"
4342 resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
4343 integrity sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=
4344 dependencies: 4310 dependencies:
4345 icss-replace-symbols "^1.1.0" 4311 icss-utils "^4.1.1"
4346 postcss "^6.0.1" 4312 postcss "^7.0.32"
4313 postcss-selector-parser "^6.0.2"
4314 postcss-value-parser "^4.1.0"
4347 4315
4348postcss-normalize-charset@^1.1.0: 4316postcss-modules-scope@^2.2.0:
4349 version "1.1.1" 4317 version "2.2.0"
4350 resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" 4318 resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
4351 integrity sha1-757nEhLX/nWceO0WL2HtYrXLk/E= 4319 integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
4352 dependencies: 4320 dependencies:
4353 postcss "^5.0.5" 4321 postcss "^7.0.6"
4322 postcss-selector-parser "^6.0.0"
4354 4323
4355postcss-normalize-url@^3.0.7: 4324postcss-modules-values@^3.0.0:
4356 version "3.0.8" 4325 version "3.0.0"
4357 resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" 4326 resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
4358 integrity sha1-EI90s/L82viRov+j6kWSJ5/HgiI= 4327 integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
4359 dependencies: 4328 dependencies:
4360 is-absolute-url "^2.0.0" 4329 icss-utils "^4.0.0"
4361 normalize-url "^1.4.0" 4330 postcss "^7.0.6"
4362 postcss "^5.0.14"
4363 postcss-value-parser "^3.2.3"
4364 4331
4365postcss-ordered-values@^2.1.0: 4332postcss-resolve-nested-selector@^0.1.1:
4366 version "2.2.3" 4333 version "0.1.1"
4367 resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" 4334 resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e"
4368 integrity sha1-7sbCpntsQSqNsgQud/6NpD+VwR0= 4335 integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=
4369 dependencies:
4370 postcss "^5.0.4"
4371 postcss-value-parser "^3.0.1"
4372 4336
4373postcss-reduce-idents@^2.2.2: 4337postcss-safe-parser@^4.0.2:
4374 version "2.4.0" 4338 version "4.0.2"
4375 resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" 4339 resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96"
4376 integrity sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM= 4340 integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==
4377 dependencies: 4341 dependencies:
4378 postcss "^5.0.4" 4342 postcss "^7.0.26"
4379 postcss-value-parser "^3.0.2"
4380 4343
4381postcss-reduce-initial@^1.0.0: 4344postcss-sass@^0.4.4:
4382 version "1.0.1" 4345 version "0.4.4"
4383 resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" 4346 resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.4.4.tgz#91f0f3447b45ce373227a98b61f8d8f0785285a3"
4384 integrity sha1-aPgGlfBF0IJjqHmtJA343WT2ROo= 4347 integrity sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==
4385 dependencies: 4348 dependencies:
4386 postcss "^5.0.4" 4349 gonzales-pe "^4.3.0"
4350 postcss "^7.0.21"
4387 4351
4388postcss-reduce-transforms@^1.0.3: 4352postcss-scss@^2.1.1:
4389 version "1.0.4" 4353 version "2.1.1"
4390 resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" 4354 resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383"
4391 integrity sha1-/3b02CEkN7McKYpC0uFEQCV3GuE= 4355 integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==
4392 dependencies: 4356 dependencies:
4393 has "^1.0.1" 4357 postcss "^7.0.6"
4394 postcss "^5.0.8"
4395 postcss-value-parser "^3.0.1"
4396 4358
4397postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: 4359postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
4398 version "2.2.3" 4360 version "6.0.3"
4399 resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" 4361 resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.3.tgz#766d77728728817cc140fa1ac6da5e77f9fada98"
4400 integrity sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A= 4362 integrity sha512-0ClFaY4X1ra21LRqbW6y3rUbWcxnSVkDFG57R7Nxus9J9myPFlv+jYDMohzpkBx0RrjjiqjtycpchQ+PLGmZ9w==
4401 dependencies: 4363 dependencies:
4402 flatten "^1.0.2" 4364 cssesc "^3.0.0"
4403 indexes-of "^1.0.1" 4365 indexes-of "^1.0.1"
4404 uniq "^1.0.1" 4366 uniq "^1.0.1"
4367 util-deprecate "^1.0.2"
4405 4368
4406postcss-svgo@^2.1.1: 4369postcss-syntax@^0.36.2:
4407 version "2.1.6" 4370 version "0.36.2"
4408 resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" 4371 resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c"
4409 integrity sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0= 4372 integrity sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==
4410 dependencies:
4411 is-svg "^2.0.0"
4412 postcss "^5.0.14"
4413 postcss-value-parser "^3.2.3"
4414 svgo "^0.7.0"
4415
4416postcss-unique-selectors@^2.0.2:
4417 version "2.0.2"
4418 resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d"
4419 integrity sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=
4420 dependencies:
4421 alphanum-sort "^1.0.1"
4422 postcss "^5.0.4"
4423 uniqs "^2.0.0"
4424 4373
4425postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: 4374postcss-value-parser@^4.1.0:
4426 version "3.3.1" 4375 version "4.1.0"
4427 resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" 4376 resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
4428 integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== 4377 integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
4429
4430postcss-zindex@^2.0.1:
4431 version "2.2.0"
4432 resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22"
4433 integrity sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=
4434 dependencies:
4435 has "^1.0.1"
4436 postcss "^5.0.4"
4437 uniqs "^2.0.0"
4438
4439postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16:
4440 version "5.2.18"
4441 resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5"
4442 integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==
4443 dependencies:
4444 chalk "^1.1.3"
4445 js-base64 "^2.1.9"
4446 source-map "^0.5.6"
4447 supports-color "^3.2.3"
4448 4378
4449postcss@^6.0.1: 4379postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
4450 version "6.0.23" 4380 version "7.0.34"
4451 resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" 4381 resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.34.tgz#f2baf57c36010df7de4009940f21532c16d65c20"
4452 integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== 4382 integrity sha512-H/7V2VeNScX9KE83GDrDZNiGT1m2H+UTnlinIzhjlLX9hfMUn1mHNnGeX81a1c8JSBdBvqk7c2ZOG6ZPn5itGw==
4453 dependencies: 4383 dependencies:
4454 chalk "^2.4.1" 4384 chalk "^2.4.2"
4455 source-map "^0.6.1" 4385 source-map "^0.6.1"
4456 supports-color "^5.4.0" 4386 supports-color "^6.1.0"
4457 4387
4458prelude-ls@~1.1.2: 4388prelude-ls@^1.2.1:
4459 version "1.1.2" 4389 version "1.2.1"
4460 resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" 4390 resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
4461 integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= 4391 integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
4462 4392
4463prepend-http@^1.0.0: 4393prepend-http@^1.0.0:
4464 version "1.0.4" 4394 version "1.0.4"
4465 resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" 4395 resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
4466 integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= 4396 integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
4467 4397
4468private@^0.1.6, private@^0.1.8:
4469 version "0.1.8"
4470 resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
4471 integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
4472
4473process-nextick-args@~2.0.0: 4398process-nextick-args@~2.0.0:
4474 version "2.0.0" 4399 version "2.0.1"
4475 resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" 4400 resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
4476 integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== 4401 integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
4477 4402
4478process@^0.11.10: 4403process@^0.11.10:
4479 version "0.11.10" 4404 version "0.11.10"
4480 resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 4405 resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
4481 integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= 4406 integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
4482 4407
4483progress@^1.1.8:
4484 version "1.1.8"
4485 resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
4486 integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=
4487
4488progress@^2.0.0: 4408progress@^2.0.0:
4489 version "2.0.3" 4409 version "2.0.3"
4490 resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" 4410 resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
4491 integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== 4411 integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
4492 4412
4413promise-inflight@^1.0.1:
4414 version "1.0.1"
4415 resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
4416 integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
4417
4493prr@~1.0.1: 4418prr@~1.0.1:
4494 version "1.0.1" 4419 version "1.0.1"
4495 resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" 4420 resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
4496 integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= 4421 integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
4497 4422
4498pseudomap@^1.0.2:
4499 version "1.0.2"
4500 resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
4501 integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
4502
4503psl@^1.1.24:
4504 version "1.1.31"
4505 resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
4506 integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
4507
4508public-encrypt@^4.0.0: 4423public-encrypt@^4.0.0:
4509 version "4.0.3" 4424 version "4.0.3"
4510 resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" 4425 resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@@ -4517,12 +4432,37 @@ public-encrypt@^4.0.0:
4517 randombytes "^2.0.1" 4432 randombytes "^2.0.1"
4518 safe-buffer "^5.1.2" 4433 safe-buffer "^5.1.2"
4519 4434
4435pump@^2.0.0:
4436 version "2.0.1"
4437 resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
4438 integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
4439 dependencies:
4440 end-of-stream "^1.1.0"
4441 once "^1.3.1"
4442
4443pump@^3.0.0:
4444 version "3.0.0"
4445 resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
4446 integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
4447 dependencies:
4448 end-of-stream "^1.1.0"
4449 once "^1.3.1"
4450
4451pumpify@^1.3.3:
4452 version "1.5.1"
4453 resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
4454 integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
4455 dependencies:
4456 duplexify "^3.6.0"
4457 inherits "^2.0.3"
4458 pump "^2.0.0"
4459
4520punycode@1.3.2: 4460punycode@1.3.2:
4521 version "1.3.2" 4461 version "1.3.2"
4522 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" 4462 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
4523 integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= 4463 integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
4524 4464
4525punycode@^1.2.4, punycode@^1.4.1: 4465punycode@^1.2.4:
4526 version "1.4.1" 4466 version "1.4.1"
4527 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 4467 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
4528 integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= 4468 integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
@@ -4538,19 +4478,9 @@ pure-extras@^1.0.0:
4538 integrity sha1-N+PMNZDLqFCYFFTNpdso4npjhxo= 4478 integrity sha1-N+PMNZDLqFCYFFTNpdso4npjhxo=
4539 4479
4540purecss@^1.0.0: 4480purecss@^1.0.0:
4541 version "1.0.0" 4481 version "1.0.1"
4542 resolved "https://registry.yarnpkg.com/purecss/-/purecss-1.0.0.tgz#3dbcd9e2a7592448a69acb705cce16311bf4b785" 4482 resolved "https://registry.yarnpkg.com/purecss/-/purecss-1.0.1.tgz#c83d84326a10beb5c3b36d20c0254e946e5568a7"
4543 integrity sha512-gfC78WCOWNnfkzulx9aoWwcl+0JflhwKeJ+k9s/ZyIawfYNA4bqBmt0DtfgtQK9iuYMtGfbdE8R2AQMjSWR2VQ== 4483 integrity sha512-mTUc5ZzpzafswEhCmTDfSRMMyRFdLYdd+KywMwnBC/MuA/Th7jug2z0Xso4WkxvtxoU/BS9aRb7WnBNyuA7YJQ==
4544
4545q@^1.1.2:
4546 version "1.5.1"
4547 resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
4548 integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
4549
4550qs@~6.5.2:
4551 version "6.5.2"
4552 resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
4553 integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
4554 4484
4555query-string@^4.1.0: 4485query-string@^4.1.0:
4556 version "4.3.4" 4486 version "4.3.4"
@@ -4570,7 +4500,12 @@ querystring@0.2.0:
4570 resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" 4500 resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
4571 integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= 4501 integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
4572 4502
4573randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: 4503quick-lru@^4.0.1:
4504 version "4.0.1"
4505 resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
4506 integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
4507
4508randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
4574 version "2.1.0" 4509 version "2.1.0"
4575 resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" 4510 resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
4576 integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 4511 integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
@@ -4585,24 +4520,6 @@ randomfill@^1.0.3:
4585 randombytes "^2.0.5" 4520 randombytes "^2.0.5"
4586 safe-buffer "^5.1.0" 4521 safe-buffer "^5.1.0"
4587 4522
4588rc@^1.2.7:
4589 version "1.2.8"
4590 resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
4591 integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
4592 dependencies:
4593 deep-extend "^0.6.0"
4594 ini "~1.3.0"
4595 minimist "^1.2.0"
4596 strip-json-comments "~2.0.1"
4597
4598read-pkg-up@^1.0.1:
4599 version "1.0.1"
4600 resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
4601 integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
4602 dependencies:
4603 find-up "^1.0.0"
4604 read-pkg "^1.0.0"
4605
4606read-pkg-up@^2.0.0: 4523read-pkg-up@^2.0.0:
4607 version "2.0.0" 4524 version "2.0.0"
4608 resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" 4525 resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
@@ -4611,14 +4528,14 @@ read-pkg-up@^2.0.0:
4611 find-up "^2.0.0" 4528 find-up "^2.0.0"
4612 read-pkg "^2.0.0" 4529 read-pkg "^2.0.0"
4613 4530
4614read-pkg@^1.0.0: 4531read-pkg-up@^7.0.1:
4615 version "1.1.0" 4532 version "7.0.1"
4616 resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" 4533 resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
4617 integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= 4534 integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
4618 dependencies: 4535 dependencies:
4619 load-json-file "^1.0.0" 4536 find-up "^4.1.0"
4620 normalize-package-data "^2.3.2" 4537 read-pkg "^5.2.0"
4621 path-type "^1.0.0" 4538 type-fest "^0.8.1"
4622 4539
4623read-pkg@^2.0.0: 4540read-pkg@^2.0.0:
4624 version "2.0.0" 4541 version "2.0.0"
@@ -4629,10 +4546,20 @@ read-pkg@^2.0.0:
4629 normalize-package-data "^2.3.2" 4546 normalize-package-data "^2.3.2"
4630 path-type "^2.0.0" 4547 path-type "^2.0.0"
4631 4548
4632readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6: 4549read-pkg@^5.2.0:
4633 version "2.3.6" 4550 version "5.2.0"
4634 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 4551 resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
4635 integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== 4552 integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
4553 dependencies:
4554 "@types/normalize-package-data" "^2.4.0"
4555 normalize-package-data "^2.5.0"
4556 parse-json "^5.0.0"
4557 type-fest "^0.6.0"
4558
4559"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
4560 version "2.3.7"
4561 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
4562 integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
4636 dependencies: 4563 dependencies:
4637 core-util-is "~1.0.0" 4564 core-util-is "~1.0.0"
4638 inherits "~2.0.3" 4565 inherits "~2.0.3"
@@ -4642,6 +4569,15 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable
4642 string_decoder "~1.1.1" 4569 string_decoder "~1.1.1"
4643 util-deprecate "~1.0.1" 4570 util-deprecate "~1.0.1"
4644 4571
4572readable-stream@^3.1.1, readable-stream@^3.6.0:
4573 version "3.6.0"
4574 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
4575 integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
4576 dependencies:
4577 inherits "^2.0.3"
4578 string_decoder "^1.1.1"
4579 util-deprecate "^1.0.1"
4580
4645readdirp@^2.2.1: 4581readdirp@^2.2.1:
4646 version "2.2.1" 4582 version "2.2.1"
4647 resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" 4583 resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@@ -4651,57 +4587,44 @@ readdirp@^2.2.1:
4651 micromatch "^3.1.10" 4587 micromatch "^3.1.10"
4652 readable-stream "^2.0.2" 4588 readable-stream "^2.0.2"
4653 4589
4654readline2@^1.0.1: 4590readdirp@~3.4.0:
4655 version "1.0.1" 4591 version "3.4.0"
4656 resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" 4592 resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
4657 integrity sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU= 4593 integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==
4658 dependencies:
4659 code-point-at "^1.0.0"
4660 is-fullwidth-code-point "^1.0.0"
4661 mute-stream "0.0.5"
4662
4663redent@^1.0.0:
4664 version "1.0.0"
4665 resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
4666 integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=
4667 dependencies: 4594 dependencies:
4668 indent-string "^2.1.0" 4595 picomatch "^2.2.1"
4669 strip-indent "^1.0.1"
4670 4596
4671reduce-css-calc@^1.2.6: 4597redent@^3.0.0:
4672 version "1.3.0" 4598 version "3.0.0"
4673 resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" 4599 resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
4674 integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY= 4600 integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
4675 dependencies: 4601 dependencies:
4676 balanced-match "^0.4.2" 4602 indent-string "^4.0.0"
4677 math-expression-evaluator "^1.2.14" 4603 strip-indent "^3.0.0"
4678 reduce-function-call "^1.0.1"
4679 4604
4680reduce-function-call@^1.0.1: 4605regenerate-unicode-properties@^8.2.0:
4681 version "1.0.2" 4606 version "8.2.0"
4682 resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" 4607 resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
4683 integrity sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk= 4608 integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
4684 dependencies: 4609 dependencies:
4685 balanced-match "^0.4.2" 4610 regenerate "^1.4.0"
4686 4611
4687regenerate@^1.2.1: 4612regenerate@^1.4.0:
4688 version "1.4.0" 4613 version "1.4.1"
4689 resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" 4614 resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f"
4690 integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== 4615 integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==
4691 4616
4692regenerator-runtime@^0.11.0: 4617regenerator-runtime@^0.13.4:
4693 version "0.11.1" 4618 version "0.13.7"
4694 resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" 4619 resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
4695 integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== 4620 integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
4696 4621
4697regenerator-transform@^0.10.0: 4622regenerator-transform@^0.14.2:
4698 version "0.10.1" 4623 version "0.14.5"
4699 resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" 4624 resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
4700 integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q== 4625 integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==
4701 dependencies: 4626 dependencies:
4702 babel-runtime "^6.18.0" 4627 "@babel/runtime" "^7.8.4"
4703 babel-types "^6.19.0"
4704 private "^0.1.6"
4705 4628
4706regex-not@^1.0.0, regex-not@^1.0.2: 4629regex-not@^1.0.0, regex-not@^1.0.2:
4707 version "1.0.2" 4630 version "1.0.2"
@@ -4711,41 +4634,86 @@ regex-not@^1.0.0, regex-not@^1.0.2:
4711 extend-shallow "^3.0.2" 4634 extend-shallow "^3.0.2"
4712 safe-regex "^1.1.0" 4635 safe-regex "^1.1.0"
4713 4636
4714regexpp@^1.0.1: 4637regexpp@^3.1.0:
4715 version "1.1.0" 4638 version "3.1.0"
4716 resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" 4639 resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
4717 integrity sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw== 4640 integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
4718 4641
4719regexpu-core@^1.0.0: 4642regexpu-core@^4.7.0:
4720 version "1.0.0" 4643 version "4.7.1"
4721 resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" 4644 resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
4722 integrity sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs= 4645 integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
4723 dependencies: 4646 dependencies:
4724 regenerate "^1.2.1" 4647 regenerate "^1.4.0"
4725 regjsgen "^0.2.0" 4648 regenerate-unicode-properties "^8.2.0"
4726 regjsparser "^0.1.4" 4649 regjsgen "^0.5.1"
4727 4650 regjsparser "^0.6.4"
4728regexpu-core@^2.0.0: 4651 unicode-match-property-ecmascript "^1.0.4"
4729 version "2.0.0" 4652 unicode-match-property-value-ecmascript "^1.2.0"
4730 resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" 4653
4731 integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA= 4654regjsgen@^0.5.1:
4732 dependencies: 4655 version "0.5.2"
4733 regenerate "^1.2.1" 4656 resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
4734 regjsgen "^0.2.0" 4657 integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
4735 regjsparser "^0.1.4"
4736
4737regjsgen@^0.2.0:
4738 version "0.2.0"
4739 resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
4740 integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=
4741 4658
4742regjsparser@^0.1.4: 4659regjsparser@^0.6.4:
4743 version "0.1.5" 4660 version "0.6.4"
4744 resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" 4661 resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
4745 integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw= 4662 integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==
4746 dependencies: 4663 dependencies:
4747 jsesc "~0.5.0" 4664 jsesc "~0.5.0"
4748 4665
4666remark-parse@^8.0.0:
4667 version "8.0.3"
4668 resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1"
4669 integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==
4670 dependencies:
4671 ccount "^1.0.0"
4672 collapse-white-space "^1.0.2"
4673 is-alphabetical "^1.0.0"
4674 is-decimal "^1.0.0"
4675 is-whitespace-character "^1.0.0"
4676 is-word-character "^1.0.0"
4677 markdown-escapes "^1.0.0"
4678 parse-entities "^2.0.0"
4679 repeat-string "^1.5.4"
4680 state-toggle "^1.0.0"
4681 trim "0.0.1"
4682 trim-trailing-lines "^1.0.0"
4683 unherit "^1.0.4"
4684 unist-util-remove-position "^2.0.0"
4685 vfile-location "^3.0.0"
4686 xtend "^4.0.1"
4687
4688remark-stringify@^8.0.0:
4689 version "8.1.1"
4690 resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-8.1.1.tgz#e2a9dc7a7bf44e46a155ec78996db896780d8ce5"
4691 integrity sha512-q4EyPZT3PcA3Eq7vPpT6bIdokXzFGp9i85igjmhRyXWmPs0Y6/d2FYwUNotKAWyLch7g0ASZJn/KHHcHZQ163A==
4692 dependencies:
4693 ccount "^1.0.0"
4694 is-alphanumeric "^1.0.0"
4695 is-decimal "^1.0.0"
4696 is-whitespace-character "^1.0.0"
4697 longest-streak "^2.0.1"
4698 markdown-escapes "^1.0.0"
4699 markdown-table "^2.0.0"
4700 mdast-util-compact "^2.0.0"
4701 parse-entities "^2.0.0"
4702 repeat-string "^1.5.4"
4703 state-toggle "^1.0.0"
4704 stringify-entities "^3.0.0"
4705 unherit "^1.0.4"
4706 xtend "^4.0.1"
4707
4708remark@^12.0.0:
4709 version "12.0.1"
4710 resolved "https://registry.yarnpkg.com/remark/-/remark-12.0.1.tgz#f1ddf68db7be71ca2bad0a33cd3678b86b9c709f"
4711 integrity sha512-gS7HDonkdIaHmmP/+shCPejCEEW+liMp/t/QwmF0Xt47Rpuhl32lLtDV1uKWvGoq+kxr5jSgg5oAIpGuyULjUw==
4712 dependencies:
4713 remark-parse "^8.0.0"
4714 remark-stringify "^8.0.0"
4715 unified "^9.0.0"
4716
4749remove-trailing-separator@^1.0.1: 4717remove-trailing-separator@^1.0.1:
4750 version "1.1.0" 4718 version "1.1.0"
4751 resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" 4719 resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -4756,114 +4724,99 @@ repeat-element@^1.1.2:
4756 resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" 4724 resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
4757 integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== 4725 integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
4758 4726
4759repeat-string@^1.5.2, repeat-string@^1.6.1: 4727repeat-string@^1.0.0, repeat-string@^1.5.4, repeat-string@^1.6.1:
4760 version "1.6.1" 4728 version "1.6.1"
4761 resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" 4729 resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
4762 integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= 4730 integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
4763 4731
4764repeating@^2.0.0: 4732replace-ext@1.0.0:
4765 version "2.0.1" 4733 version "1.0.0"
4766 resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" 4734 resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
4767 integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= 4735 integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
4768 dependencies:
4769 is-finite "^1.0.0"
4770
4771request@^2.87.0, request@^2.88.0:
4772 version "2.88.0"
4773 resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
4774 integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
4775 dependencies:
4776 aws-sign2 "~0.7.0"
4777 aws4 "^1.8.0"
4778 caseless "~0.12.0"
4779 combined-stream "~1.0.6"
4780 extend "~3.0.2"
4781 forever-agent "~0.6.1"
4782 form-data "~2.3.2"
4783 har-validator "~5.1.0"
4784 http-signature "~1.2.0"
4785 is-typedarray "~1.0.0"
4786 isstream "~0.1.2"
4787 json-stringify-safe "~5.0.1"
4788 mime-types "~2.1.19"
4789 oauth-sign "~0.9.0"
4790 performance-now "^2.1.0"
4791 qs "~6.5.2"
4792 safe-buffer "^5.1.2"
4793 tough-cookie "~2.4.3"
4794 tunnel-agent "^0.6.0"
4795 uuid "^3.3.2"
4796 4736
4797require-directory@^2.1.1: 4737require-directory@^2.1.1:
4798 version "2.1.1" 4738 version "2.1.1"
4799 resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 4739 resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
4800 integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 4740 integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
4801 4741
4802require-main-filename@^1.0.1: 4742require-main-filename@^2.0.0:
4803 version "1.0.1" 4743 version "2.0.0"
4804 resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" 4744 resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
4805 integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= 4745 integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
4806 4746
4807require-uncached@^1.0.2, require-uncached@^1.0.3: 4747resolve-cwd@^2.0.0:
4808 version "1.0.3" 4748 version "2.0.0"
4809 resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" 4749 resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
4810 integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= 4750 integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
4811 dependencies: 4751 dependencies:
4812 caller-path "^0.1.0" 4752 resolve-from "^3.0.0"
4813 resolve-from "^1.0.0"
4814 4753
4815resolve-from@^1.0.0: 4754resolve-dir@^1.0.0, resolve-dir@^1.0.1:
4816 version "1.0.1" 4755 version "1.0.1"
4817 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" 4756 resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
4818 integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= 4757 integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
4758 dependencies:
4759 expand-tilde "^2.0.0"
4760 global-modules "^1.0.0"
4761
4762resolve-from@^3.0.0:
4763 version "3.0.0"
4764 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
4765 integrity sha1-six699nWiBvItuZTM17rywoYh0g=
4766
4767resolve-from@^4.0.0:
4768 version "4.0.0"
4769 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
4770 integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
4771
4772resolve-from@^5.0.0:
4773 version "5.0.0"
4774 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
4775 integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
4819 4776
4820resolve-url@^0.2.1: 4777resolve-url@^0.2.1:
4821 version "0.2.1" 4778 version "0.2.1"
4822 resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" 4779 resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
4823 integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= 4780 integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
4824 4781
4825resolve@^1.10.0, resolve@^1.5.0: 4782resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2:
4826 version "1.11.0" 4783 version "1.17.0"
4827 resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232" 4784 resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
4828 integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw== 4785 integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
4829 dependencies: 4786 dependencies:
4830 path-parse "^1.0.6" 4787 path-parse "^1.0.6"
4831 4788
4832restore-cursor@^1.0.1:
4833 version "1.0.1"
4834 resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
4835 integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=
4836 dependencies:
4837 exit-hook "^1.0.0"
4838 onetime "^1.0.0"
4839
4840restore-cursor@^2.0.0:
4841 version "2.0.0"
4842 resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
4843 integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
4844 dependencies:
4845 onetime "^2.0.0"
4846 signal-exit "^3.0.2"
4847
4848ret@~0.1.10: 4789ret@~0.1.10:
4849 version "0.1.15" 4790 version "0.1.15"
4850 resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" 4791 resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
4851 integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== 4792 integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
4852 4793
4853right-align@^0.1.1: 4794reusify@^1.0.4:
4854 version "0.1.3" 4795 version "1.0.4"
4855 resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" 4796 resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
4856 integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= 4797 integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
4857 dependencies:
4858 align-text "^0.1.1"
4859 4798
4860rimraf@2, rimraf@^2.6.1, rimraf@~2.6.2: 4799rimraf@2.6.3:
4861 version "2.6.3" 4800 version "2.6.3"
4862 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" 4801 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
4863 integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== 4802 integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
4864 dependencies: 4803 dependencies:
4865 glob "^7.1.3" 4804 glob "^7.1.3"
4866 4805
4806rimraf@^2.5.4, rimraf@^2.6.3:
4807 version "2.7.1"
4808 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
4809 integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
4810 dependencies:
4811 glob "^7.1.3"
4812
4813rimraf@^3.0.2:
4814 version "3.0.2"
4815 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
4816 integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
4817 dependencies:
4818 glob "^7.1.3"
4819
4867ripemd160@^2.0.0, ripemd160@^2.0.1: 4820ripemd160@^2.0.0, ripemd160@^2.0.1:
4868 version "2.0.2" 4821 version "2.0.2"
4869 resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" 4822 resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -4872,38 +4825,24 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
4872 hash-base "^3.0.0" 4825 hash-base "^3.0.0"
4873 inherits "^2.0.1" 4826 inherits "^2.0.1"
4874 4827
4875run-async@^0.1.0: 4828run-parallel@^1.1.9:
4876 version "0.1.0" 4829 version "1.1.9"
4877 resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" 4830 resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
4878 integrity sha1-yK1KXhEGYeQCp9IbUw4AnyX444k= 4831 integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
4879 dependencies:
4880 once "^1.3.0"
4881
4882run-async@^2.2.0:
4883 version "2.3.0"
4884 resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
4885 integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
4886 dependencies:
4887 is-promise "^2.1.0"
4888 4832
4889rx-lite-aggregates@^4.0.8: 4833run-queue@^1.0.0, run-queue@^1.0.3:
4890 version "4.0.8" 4834 version "1.0.3"
4891 resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" 4835 resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
4892 integrity sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74= 4836 integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
4893 dependencies: 4837 dependencies:
4894 rx-lite "*" 4838 aproba "^1.1.1"
4895
4896rx-lite@*, rx-lite@^4.0.8:
4897 version "4.0.8"
4898 resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
4899 integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=
4900 4839
4901rx-lite@^3.1.2: 4840safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
4902 version "3.1.2" 4841 version "5.2.1"
4903 resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" 4842 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
4904 integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= 4843 integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
4905 4844
4906safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 4845safe-buffer@~5.1.0, safe-buffer@~5.1.1:
4907 version "5.1.2" 4846 version "5.1.2"
4908 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 4847 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
4909 integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 4848 integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
@@ -4915,63 +4854,28 @@ safe-regex@^1.1.0:
4915 dependencies: 4854 dependencies:
4916 ret "~0.1.10" 4855 ret "~0.1.10"
4917 4856
4918"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: 4857safer-buffer@^2.1.0:
4919 version "2.1.2" 4858 version "2.1.2"
4920 resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 4859 resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
4921 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 4860 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
4922 4861
4923sass-graph@^2.2.4: 4862sass-loader@^10.0.2:
4924 version "2.2.4" 4863 version "10.0.2"
4925 resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" 4864 resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.2.tgz#c7b73010848b264792dd45372eea0b87cba4401e"
4926 integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= 4865 integrity sha512-wV6NDUVB8/iEYMalV/+139+vl2LaRFlZGEd5/xmdcdzQcgmis+npyco6NsDTVOlNA3y2NV9Gcz+vHyFMIT+ffg==
4927 dependencies:
4928 glob "^7.0.0"
4929 lodash "^4.0.0"
4930 scss-tokenizer "^0.2.3"
4931 yargs "^7.0.0"
4932
4933sass-lint@^1.12.1:
4934 version "1.13.1"
4935 resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.13.1.tgz#5fd2b2792e9215272335eb0f0dc607f61e8acc8f"
4936 integrity sha512-DSyah8/MyjzW2BWYmQWekYEKir44BpLqrCFsgs9iaWiVTcwZfwXHF586hh3D1n+/9ihUNMfd8iHAyb9KkGgs7Q==
4937 dependencies:
4938 commander "^2.8.1"
4939 eslint "^2.7.0"
4940 front-matter "2.1.2"
4941 fs-extra "^3.0.1"
4942 glob "^7.0.0"
4943 globule "^1.0.0"
4944 gonzales-pe-sl "^4.2.3"
4945 js-yaml "^3.5.4"
4946 known-css-properties "^0.3.0"
4947 lodash.capitalize "^4.1.0"
4948 lodash.kebabcase "^4.0.0"
4949 merge "^1.2.0"
4950 path-is-absolute "^1.0.0"
4951 util "^0.10.3"
4952
4953sass-loader@^6.0.6:
4954 version "6.0.7"
4955 resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.7.tgz#dd2fdb3e7eeff4a53f35ba6ac408715488353d00"
4956 integrity sha512-JoiyD00Yo1o61OJsoP2s2kb19L1/Y2p3QFcCdWdF6oomBGKVYuZyqHWemRBfQ2uGYsk+CH3eCguXNfpjzlcpaA==
4957 dependencies: 4866 dependencies:
4958 clone-deep "^2.0.1" 4867 klona "^2.0.3"
4959 loader-utils "^1.0.1" 4868 loader-utils "^2.0.0"
4960 lodash.tail "^4.1.1" 4869 neo-async "^2.6.2"
4961 neo-async "^2.5.0" 4870 schema-utils "^2.7.1"
4962 pify "^3.0.0" 4871 semver "^7.3.2"
4963
4964sax@^1.2.4, sax@~1.2.1:
4965 version "1.2.4"
4966 resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
4967 integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
4968 4872
4969schema-utils@^0.3.0: 4873sass@^1.26.11:
4970 version "0.3.0" 4874 version "1.26.11"
4971 resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" 4875 resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.11.tgz#0f22cc4ab2ba27dad1d4ca30837beb350b709847"
4972 integrity sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8= 4876 integrity sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw==
4973 dependencies: 4877 dependencies:
4974 ajv "^5.0.0" 4878 chokidar ">=2.0.0 <4.0.0"
4975 4879
4976schema-utils@^0.4.5: 4880schema-utils@^0.4.5:
4977 version "0.4.7" 4881 version "0.4.7"
@@ -4981,43 +4885,67 @@ schema-utils@^0.4.5:
4981 ajv "^6.1.0" 4885 ajv "^6.1.0"
4982 ajv-keywords "^3.1.0" 4886 ajv-keywords "^3.1.0"
4983 4887
4984scss-tokenizer@^0.2.3: 4888schema-utils@^1.0.0:
4985 version "0.2.3" 4889 version "1.0.0"
4986 resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" 4890 resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
4987 integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= 4891 integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==
4988 dependencies: 4892 dependencies:
4989 js-base64 "^2.1.8" 4893 ajv "^6.1.0"
4990 source-map "^0.4.2" 4894 ajv-errors "^1.0.0"
4895 ajv-keywords "^3.1.0"
4991 4896
4992"semver@2 || 3 || 4 || 5", semver@^5.3.0: 4897schema-utils@^2.6.5, schema-utils@^2.7.1:
4993 version "5.7.0" 4898 version "2.7.1"
4994 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" 4899 resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
4995 integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== 4900 integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
4901 dependencies:
4902 "@types/json-schema" "^7.0.5"
4903 ajv "^6.12.4"
4904 ajv-keywords "^3.5.2"
4996 4905
4997semver@~5.3.0: 4906"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
4998 version "5.3.0" 4907 version "5.7.1"
4999 resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" 4908 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
5000 integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= 4909 integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
5001 4910
5002set-blocking@^2.0.0, set-blocking@~2.0.0: 4911semver@7.0.0:
5003 version "2.0.0" 4912 version "7.0.0"
5004 resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 4913 resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
5005 integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= 4914 integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
4915
4916semver@^6.0.0:
4917 version "6.3.0"
4918 resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
4919 integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
5006 4920
5007set-value@^0.4.3: 4921semver@^7.2.1, semver@^7.3.2:
5008 version "0.4.3" 4922 version "7.3.2"
5009 resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" 4923 resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
5010 integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= 4924 integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
4925
4926serialize-javascript@^4.0.0:
4927 version "4.0.0"
4928 resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
4929 integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
5011 dependencies: 4930 dependencies:
5012 extend-shallow "^2.0.1" 4931 randombytes "^2.1.0"
5013 is-extendable "^0.1.1"
5014 is-plain-object "^2.0.1"
5015 to-object-path "^0.3.0"
5016 4932
5017set-value@^2.0.0: 4933serialize-javascript@^5.0.1:
4934 version "5.0.1"
4935 resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
4936 integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
4937 dependencies:
4938 randombytes "^2.1.0"
4939
4940set-blocking@^2.0.0:
5018 version "2.0.0" 4941 version "2.0.0"
5019 resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" 4942 resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
5020 integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg== 4943 integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
4944
4945set-value@^2.0.0, set-value@^2.0.1:
4946 version "2.0.1"
4947 resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
4948 integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
5021 dependencies: 4949 dependencies:
5022 extend-shallow "^2.0.1" 4950 extend-shallow "^2.0.1"
5023 is-extendable "^0.1.1" 4951 is-extendable "^0.1.1"
@@ -5037,15 +4965,6 @@ sha.js@^2.4.0, sha.js@^2.4.8:
5037 inherits "^2.0.1" 4965 inherits "^2.0.1"
5038 safe-buffer "^5.0.1" 4966 safe-buffer "^5.0.1"
5039 4967
5040shallow-clone@^1.0.0:
5041 version "1.0.0"
5042 resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571"
5043 integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==
5044 dependencies:
5045 is-extendable "^0.1.1"
5046 kind-of "^5.0.0"
5047 mixin-object "^2.0.1"
5048
5049shebang-command@^1.2.0: 4968shebang-command@^1.2.0:
5050 version "1.2.0" 4969 version "1.2.0"
5051 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 4970 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -5053,38 +4972,51 @@ shebang-command@^1.2.0:
5053 dependencies: 4972 dependencies:
5054 shebang-regex "^1.0.0" 4973 shebang-regex "^1.0.0"
5055 4974
4975shebang-command@^2.0.0:
4976 version "2.0.0"
4977 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
4978 integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
4979 dependencies:
4980 shebang-regex "^3.0.0"
4981
5056shebang-regex@^1.0.0: 4982shebang-regex@^1.0.0:
5057 version "1.0.0" 4983 version "1.0.0"
5058 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 4984 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
5059 integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= 4985 integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
5060 4986
5061shelljs@^0.6.0: 4987shebang-regex@^3.0.0:
5062 version "0.6.1" 4988 version "3.0.0"
5063 resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" 4989 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
5064 integrity sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg= 4990 integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
5065
5066signal-exit@^3.0.0, signal-exit@^3.0.2:
5067 version "3.0.2"
5068 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
5069 integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
5070 4991
5071slash@^1.0.0: 4992signal-exit@^3.0.2:
5072 version "1.0.0" 4993 version "3.0.3"
5073 resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 4994 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
5074 integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= 4995 integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
5075 4996
5076slice-ansi@0.0.4: 4997slash@^3.0.0:
5077 version "0.0.4" 4998 version "3.0.0"
5078 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" 4999 resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
5079 integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= 5000 integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
5080 5001
5081slice-ansi@1.0.0: 5002slice-ansi@^2.1.0:
5082 version "1.0.0" 5003 version "2.1.0"
5083 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" 5004 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
5084 integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg== 5005 integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
5085 dependencies: 5006 dependencies:
5007 ansi-styles "^3.2.0"
5008 astral-regex "^1.0.0"
5086 is-fullwidth-code-point "^2.0.0" 5009 is-fullwidth-code-point "^2.0.0"
5087 5010
5011slice-ansi@^4.0.0:
5012 version "4.0.0"
5013 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
5014 integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
5015 dependencies:
5016 ansi-styles "^4.0.0"
5017 astral-regex "^2.0.0"
5018 is-fullwidth-code-point "^3.0.0"
5019
5088snapdragon-node@^2.0.1: 5020snapdragon-node@^2.0.1:
5089 version "2.1.1" 5021 version "2.1.1"
5090 resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" 5022 resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -5128,70 +5060,69 @@ source-list-map@^2.0.0:
5128 integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== 5060 integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
5129 5061
5130source-map-resolve@^0.5.0: 5062source-map-resolve@^0.5.0:
5131 version "0.5.2" 5063 version "0.5.3"
5132 resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" 5064 resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
5133 integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== 5065 integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
5134 dependencies: 5066 dependencies:
5135 atob "^2.1.1" 5067 atob "^2.1.2"
5136 decode-uri-component "^0.2.0" 5068 decode-uri-component "^0.2.0"
5137 resolve-url "^0.2.1" 5069 resolve-url "^0.2.1"
5138 source-map-url "^0.4.0" 5070 source-map-url "^0.4.0"
5139 urix "^0.1.0" 5071 urix "^0.1.0"
5140 5072
5141source-map-support@^0.4.15: 5073source-map-support@~0.5.12:
5142 version "0.4.18" 5074 version "0.5.19"
5143 resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" 5075 resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
5144 integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== 5076 integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
5145 dependencies: 5077 dependencies:
5146 source-map "^0.5.6" 5078 buffer-from "^1.0.0"
5079 source-map "^0.6.0"
5147 5080
5148source-map-url@^0.4.0: 5081source-map-url@^0.4.0:
5149 version "0.4.0" 5082 version "0.4.0"
5150 resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" 5083 resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
5151 integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= 5084 integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
5152 5085
5153source-map@^0.4.2: 5086source-map@^0.5.0, source-map@^0.5.6:
5154 version "0.4.4"
5155 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
5156 integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
5157 dependencies:
5158 amdefine ">=0.0.4"
5159
5160source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1:
5161 version "0.5.7" 5087 version "0.5.7"
5162 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" 5088 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
5163 integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= 5089 integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
5164 5090
5165source-map@^0.6.1, source-map@~0.6.1: 5091source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
5166 version "0.6.1" 5092 version "0.6.1"
5167 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 5093 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
5168 integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 5094 integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
5169 5095
5170spdx-correct@^3.0.0: 5096spdx-correct@^3.0.0:
5171 version "3.1.0" 5097 version "3.1.1"
5172 resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" 5098 resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
5173 integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== 5099 integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
5174 dependencies: 5100 dependencies:
5175 spdx-expression-parse "^3.0.0" 5101 spdx-expression-parse "^3.0.0"
5176 spdx-license-ids "^3.0.0" 5102 spdx-license-ids "^3.0.0"
5177 5103
5178spdx-exceptions@^2.1.0: 5104spdx-exceptions@^2.1.0:
5179 version "2.2.0" 5105 version "2.3.0"
5180 resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" 5106 resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
5181 integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== 5107 integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
5182 5108
5183spdx-expression-parse@^3.0.0: 5109spdx-expression-parse@^3.0.0:
5184 version "3.0.0" 5110 version "3.0.1"
5185 resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" 5111 resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
5186 integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== 5112 integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
5187 dependencies: 5113 dependencies:
5188 spdx-exceptions "^2.1.0" 5114 spdx-exceptions "^2.1.0"
5189 spdx-license-ids "^3.0.0" 5115 spdx-license-ids "^3.0.0"
5190 5116
5191spdx-license-ids@^3.0.0: 5117spdx-license-ids@^3.0.0:
5192 version "3.0.4" 5118 version "3.0.6"
5193 resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1" 5119 resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
5194 integrity sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA== 5120 integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==
5121
5122specificity@^0.4.1:
5123 version "0.4.1"
5124 resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019"
5125 integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==
5195 5126
5196split-string@^3.0.1, split-string@^3.0.2: 5127split-string@^3.0.1, split-string@^3.0.2:
5197 version "3.1.0" 5128 version "3.1.0"
@@ -5205,20 +5136,24 @@ sprintf-js@~1.0.2:
5205 resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 5136 resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
5206 integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= 5137 integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
5207 5138
5208sshpk@^1.7.0: 5139ssri@^6.0.1:
5209 version "1.16.1" 5140 version "6.0.1"
5210 resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" 5141 resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
5211 integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== 5142 integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
5212 dependencies: 5143 dependencies:
5213 asn1 "~0.2.3" 5144 figgy-pudding "^3.5.1"
5214 assert-plus "^1.0.0" 5145
5215 bcrypt-pbkdf "^1.0.0" 5146ssri@^8.0.0:
5216 dashdash "^1.12.0" 5147 version "8.0.0"
5217 ecc-jsbn "~0.1.1" 5148 resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808"
5218 getpass "^0.1.1" 5149 integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==
5219 jsbn "~0.1.0" 5150 dependencies:
5220 safer-buffer "^2.0.2" 5151 minipass "^3.1.1"
5221 tweetnacl "~0.14.0" 5152
5153state-toggle@^1.0.0:
5154 version "1.0.3"
5155 resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"
5156 integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==
5222 5157
5223static-extend@^0.1.1: 5158static-extend@^0.1.1:
5224 version "0.1.2" 5159 version "0.1.2"
@@ -5228,13 +5163,6 @@ static-extend@^0.1.1:
5228 define-property "^0.2.5" 5163 define-property "^0.2.5"
5229 object-copy "^0.1.0" 5164 object-copy "^0.1.0"
5230 5165
5231stdout-stream@^1.4.0:
5232 version "1.4.1"
5233 resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de"
5234 integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==
5235 dependencies:
5236 readable-stream "^2.0.1"
5237
5238stream-browserify@^2.0.1: 5166stream-browserify@^2.0.1:
5239 version "2.0.2" 5167 version "2.0.2"
5240 resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" 5168 resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
@@ -5243,6 +5171,14 @@ stream-browserify@^2.0.1:
5243 inherits "~2.0.1" 5171 inherits "~2.0.1"
5244 readable-stream "^2.0.2" 5172 readable-stream "^2.0.2"
5245 5173
5174stream-each@^1.1.0:
5175 version "1.2.3"
5176 resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
5177 integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==
5178 dependencies:
5179 end-of-stream "^1.1.0"
5180 stream-shift "^1.0.0"
5181
5246stream-http@^2.7.2: 5182stream-http@^2.7.2:
5247 version "2.8.3" 5183 version "2.8.3"
5248 resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" 5184 resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
@@ -5254,34 +5190,56 @@ stream-http@^2.7.2:
5254 to-arraybuffer "^1.0.0" 5190 to-arraybuffer "^1.0.0"
5255 xtend "^4.0.0" 5191 xtend "^4.0.0"
5256 5192
5193stream-shift@^1.0.0:
5194 version "1.0.1"
5195 resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
5196 integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
5197
5257strict-uri-encode@^1.0.0: 5198strict-uri-encode@^1.0.0:
5258 version "1.1.0" 5199 version "1.1.0"
5259 resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" 5200 resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
5260 integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= 5201 integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
5261 5202
5262string-width@^1.0.1, string-width@^1.0.2: 5203string-width@^3.0.0, string-width@^3.1.0:
5263 version "1.0.2" 5204 version "3.1.0"
5264 resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 5205 resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
5265 integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= 5206 integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
5266 dependencies: 5207 dependencies:
5267 code-point-at "^1.0.0" 5208 emoji-regex "^7.0.1"
5268 is-fullwidth-code-point "^1.0.0" 5209 is-fullwidth-code-point "^2.0.0"
5269 strip-ansi "^3.0.0" 5210 strip-ansi "^5.1.0"
5270 5211
5271"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: 5212string-width@^4.2.0:
5272 version "2.1.1" 5213 version "4.2.0"
5273 resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 5214 resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
5274 integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 5215 integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
5275 dependencies: 5216 dependencies:
5276 is-fullwidth-code-point "^2.0.0" 5217 emoji-regex "^8.0.0"
5277 strip-ansi "^4.0.0" 5218 is-fullwidth-code-point "^3.0.0"
5219 strip-ansi "^6.0.0"
5278 5220
5279string_decoder@^1.0.0: 5221string.prototype.trimend@^1.0.1:
5280 version "1.2.0" 5222 version "1.0.1"
5281 resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" 5223 resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
5282 integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== 5224 integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
5283 dependencies: 5225 dependencies:
5284 safe-buffer "~5.1.0" 5226 define-properties "^1.1.3"
5227 es-abstract "^1.17.5"
5228
5229string.prototype.trimstart@^1.0.1:
5230 version "1.0.1"
5231 resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
5232 integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
5233 dependencies:
5234 define-properties "^1.1.3"
5235 es-abstract "^1.17.5"
5236
5237string_decoder@^1.0.0, string_decoder@^1.1.1:
5238 version "1.3.0"
5239 resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
5240 integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
5241 dependencies:
5242 safe-buffer "~5.2.0"
5285 5243
5286string_decoder@~1.1.1: 5244string_decoder@~1.1.1:
5287 version "1.1.1" 5245 version "1.1.1"
@@ -5290,185 +5248,277 @@ string_decoder@~1.1.1:
5290 dependencies: 5248 dependencies:
5291 safe-buffer "~5.1.0" 5249 safe-buffer "~5.1.0"
5292 5250
5293strip-ansi@^3.0.0, strip-ansi@^3.0.1: 5251stringify-entities@^3.0.0:
5294 version "3.0.1" 5252 version "3.0.1"
5295 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 5253 resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-3.0.1.tgz#32154b91286ab0869ab2c07696223bd23b6dbfc0"
5296 integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= 5254 integrity sha512-Lsk3ISA2++eJYqBMPKcr/8eby1I6L0gP0NlxF8Zja6c05yr/yCYyb2c9PwXjd08Ib3If1vn1rbs1H5ZtVuOfvQ==
5297 dependencies: 5255 dependencies:
5298 ansi-regex "^2.0.0" 5256 character-entities-html4 "^1.0.0"
5257 character-entities-legacy "^1.0.0"
5258 is-alphanumerical "^1.0.0"
5259 is-decimal "^1.0.2"
5260 is-hexadecimal "^1.0.0"
5299 5261
5300strip-ansi@^4.0.0: 5262strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
5301 version "4.0.0" 5263 version "5.2.0"
5302 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 5264 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
5303 integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= 5265 integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
5304 dependencies: 5266 dependencies:
5305 ansi-regex "^3.0.0" 5267 ansi-regex "^4.1.0"
5306 5268
5307strip-bom@^2.0.0: 5269strip-ansi@^6.0.0:
5308 version "2.0.0" 5270 version "6.0.0"
5309 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" 5271 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
5310 integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= 5272 integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
5311 dependencies: 5273 dependencies:
5312 is-utf8 "^0.2.0" 5274 ansi-regex "^5.0.0"
5313 5275
5314strip-bom@^3.0.0: 5276strip-bom@^3.0.0:
5315 version "3.0.0" 5277 version "3.0.0"
5316 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" 5278 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
5317 integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= 5279 integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
5318 5280
5319strip-eof@^1.0.0: 5281strip-indent@^3.0.0:
5320 version "1.0.0" 5282 version "3.0.0"
5321 resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" 5283 resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
5322 integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= 5284 integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
5323
5324strip-indent@^1.0.1:
5325 version "1.0.1"
5326 resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
5327 integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=
5328 dependencies: 5285 dependencies:
5329 get-stdin "^4.0.1" 5286 min-indent "^1.0.0"
5330
5331strip-json-comments@~1.0.1:
5332 version "1.0.4"
5333 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
5334 integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=
5335 5287
5336strip-json-comments@~2.0.1: 5288strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
5337 version "2.0.1" 5289 version "3.1.1"
5338 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 5290 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
5339 integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= 5291 integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
5340 5292
5341style-loader@^0.19.1: 5293style-search@^0.1.0:
5342 version "0.19.1" 5294 version "0.1.0"
5343 resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.19.1.tgz#591ffc80bcefe268b77c5d9ebc0505d772619f85" 5295 resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
5344 integrity sha512-IRE+ijgojrygQi3rsqT0U4dd+UcPCqcVvauZpCnQrGAlEe+FUIyrK93bUDScamesjP08JlQNsFJU+KmPedP5Og== 5296 integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=
5345 dependencies:
5346 loader-utils "^1.0.2"
5347 schema-utils "^0.3.0"
5348 5297
5349supports-color@^2.0.0: 5298stylelint-config-recommended@^3.0.0:
5299 version "3.0.0"
5300 resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz#e0e547434016c5539fe2650afd58049a2fd1d657"
5301 integrity sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ==
5302
5303stylelint-config-standard@^20.0.0:
5304 version "20.0.0"
5305 resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-20.0.0.tgz#06135090c9e064befee3d594289f50e295b5e20d"
5306 integrity sha512-IB2iFdzOTA/zS4jSVav6z+wGtin08qfj+YyExHB3LF9lnouQht//YyB0KZq9gGz5HNPkddHOzcY8HsUey6ZUlA==
5307 dependencies:
5308 stylelint-config-recommended "^3.0.0"
5309
5310stylelint-scss@^3.18.0:
5311 version "3.18.0"
5312 resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.18.0.tgz#8f06371c223909bf3f62e839548af1badeed31e9"
5313 integrity sha512-LD7+hv/6/ApNGt7+nR/50ft7cezKP2HM5rI8avIdGaUWre3xlHfV4jKO/DRZhscfuN+Ewy9FMhcTq0CcS0C/SA==
5314 dependencies:
5315 lodash "^4.17.15"
5316 postcss-media-query-parser "^0.2.3"
5317 postcss-resolve-nested-selector "^0.1.1"
5318 postcss-selector-parser "^6.0.2"
5319 postcss-value-parser "^4.1.0"
5320
5321stylelint@^13.7.1:
5322 version "13.7.1"
5323 resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.7.1.tgz#bee97ee78d778a3f1dbe3f7397b76414973e263e"
5324 integrity sha512-qzqazcyRxrSRdmFuO0/SZOJ+LyCxYy0pwcvaOBBnl8/2VfHSMrtNIE+AnyJoyq6uKb+mt+hlgmVrvVi6G6XHfQ==
5325 dependencies:
5326 "@stylelint/postcss-css-in-js" "^0.37.2"
5327 "@stylelint/postcss-markdown" "^0.36.1"
5328 autoprefixer "^9.8.6"
5329 balanced-match "^1.0.0"
5330 chalk "^4.1.0"
5331 cosmiconfig "^7.0.0"
5332 debug "^4.1.1"
5333 execall "^2.0.0"
5334 fast-glob "^3.2.4"
5335 fastest-levenshtein "^1.0.12"
5336 file-entry-cache "^5.0.1"
5337 get-stdin "^8.0.0"
5338 global-modules "^2.0.0"
5339 globby "^11.0.1"
5340 globjoin "^0.1.4"
5341 html-tags "^3.1.0"
5342 ignore "^5.1.8"
5343 import-lazy "^4.0.0"
5344 imurmurhash "^0.1.4"
5345 known-css-properties "^0.19.0"
5346 lodash "^4.17.20"
5347 log-symbols "^4.0.0"
5348 mathml-tag-names "^2.1.3"
5349 meow "^7.1.1"
5350 micromatch "^4.0.2"
5351 normalize-selector "^0.2.0"
5352 postcss "^7.0.32"
5353 postcss-html "^0.36.0"
5354 postcss-less "^3.1.4"
5355 postcss-media-query-parser "^0.2.3"
5356 postcss-resolve-nested-selector "^0.1.1"
5357 postcss-safe-parser "^4.0.2"
5358 postcss-sass "^0.4.4"
5359 postcss-scss "^2.1.1"
5360 postcss-selector-parser "^6.0.2"
5361 postcss-syntax "^0.36.2"
5362 postcss-value-parser "^4.1.0"
5363 resolve-from "^5.0.0"
5364 slash "^3.0.0"
5365 specificity "^0.4.1"
5366 string-width "^4.2.0"
5367 strip-ansi "^6.0.0"
5368 style-search "^0.1.0"
5369 sugarss "^2.0.0"
5370 svg-tags "^1.0.0"
5371 table "^6.0.1"
5372 v8-compile-cache "^2.1.1"
5373 write-file-atomic "^3.0.3"
5374
5375sugarss@^2.0.0:
5350 version "2.0.0" 5376 version "2.0.0"
5351 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 5377 resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d"
5352 integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= 5378 integrity sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==
5353
5354supports-color@^3.2.3:
5355 version "3.2.3"
5356 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
5357 integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=
5358 dependencies: 5379 dependencies:
5359 has-flag "^1.0.0" 5380 postcss "^7.0.2"
5360 5381
5361supports-color@^4.2.1: 5382supports-color@^5.3.0:
5362 version "4.5.0"
5363 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
5364 integrity sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=
5365 dependencies:
5366 has-flag "^2.0.0"
5367
5368supports-color@^5.3.0, supports-color@^5.4.0:
5369 version "5.5.0" 5383 version "5.5.0"
5370 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 5384 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
5371 integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 5385 integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
5372 dependencies: 5386 dependencies:
5373 has-flag "^3.0.0" 5387 has-flag "^3.0.0"
5374 5388
5375svgo@^0.7.0: 5389supports-color@^6.1.0:
5376 version "0.7.2" 5390 version "6.1.0"
5377 resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" 5391 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
5378 integrity sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U= 5392 integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
5379 dependencies: 5393 dependencies:
5380 coa "~1.0.1" 5394 has-flag "^3.0.0"
5381 colors "~1.1.2"
5382 csso "~2.3.1"
5383 js-yaml "~3.7.0"
5384 mkdirp "~0.5.1"
5385 sax "~1.2.1"
5386 whet.extend "~0.9.9"
5387 5395
5388table@4.0.2: 5396supports-color@^7.0.0, supports-color@^7.1.0:
5389 version "4.0.2" 5397 version "7.2.0"
5390 resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" 5398 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
5391 integrity sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA== 5399 integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
5392 dependencies:
5393 ajv "^5.2.3"
5394 ajv-keywords "^2.1.0"
5395 chalk "^2.1.0"
5396 lodash "^4.17.4"
5397 slice-ansi "1.0.0"
5398 string-width "^2.1.1"
5399
5400table@^3.7.8:
5401 version "3.8.3"
5402 resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
5403 integrity sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=
5404 dependencies:
5405 ajv "^4.7.0"
5406 ajv-keywords "^1.0.0"
5407 chalk "^1.1.1"
5408 lodash "^4.0.0"
5409 slice-ansi "0.0.4"
5410 string-width "^2.0.0"
5411
5412tapable@^0.2.7:
5413 version "0.2.9"
5414 resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.9.tgz#af2d8bbc9b04f74ee17af2b4d9048f807acd18a8"
5415 integrity sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==
5416
5417tar@^2.0.0:
5418 version "2.2.2"
5419 resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40"
5420 integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==
5421 dependencies: 5400 dependencies:
5422 block-stream "*" 5401 has-flag "^4.0.0"
5423 fstream "^1.0.12"
5424 inherits "2"
5425 5402
5426tar@^4: 5403svg-tags@^1.0.0:
5427 version "4.4.8" 5404 version "1.0.0"
5428 resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" 5405 resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
5429 integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== 5406 integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
5407
5408table@^5.2.3:
5409 version "5.4.6"
5410 resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
5411 integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
5412 dependencies:
5413 ajv "^6.10.2"
5414 lodash "^4.17.14"
5415 slice-ansi "^2.1.0"
5416 string-width "^3.0.0"
5417
5418table@^6.0.1:
5419 version "6.0.3"
5420 resolved "https://registry.yarnpkg.com/table/-/table-6.0.3.tgz#e5b8a834e37e27ad06de2e0fda42b55cfd8a0123"
5421 integrity sha512-8321ZMcf1B9HvVX/btKv8mMZahCjn2aYrDlpqHaBFCfnox64edeH9kEid0vTLTRR8gWR2A20aDgeuTTea4sVtw==
5422 dependencies:
5423 ajv "^6.12.4"
5424 lodash "^4.17.20"
5425 slice-ansi "^4.0.0"
5426 string-width "^4.2.0"
5427
5428tapable@^1.0.0, tapable@^1.1.3:
5429 version "1.1.3"
5430 resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
5431 integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
5432
5433tar@^6.0.2:
5434 version "6.0.5"
5435 resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
5436 integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
5437 dependencies:
5438 chownr "^2.0.0"
5439 fs-minipass "^2.0.0"
5440 minipass "^3.0.0"
5441 minizlib "^2.1.1"
5442 mkdirp "^1.0.3"
5443 yallist "^4.0.0"
5444
5445terser-webpack-plugin@^1.4.3:
5446 version "1.4.5"
5447 resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b"
5448 integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==
5449 dependencies:
5450 cacache "^12.0.2"
5451 find-cache-dir "^2.1.0"
5452 is-wsl "^1.1.0"
5453 schema-utils "^1.0.0"
5454 serialize-javascript "^4.0.0"
5455 source-map "^0.6.1"
5456 terser "^4.1.2"
5457 webpack-sources "^1.4.0"
5458 worker-farm "^1.7.0"
5459
5460terser-webpack-plugin@^4.2.2:
5461 version "4.2.2"
5462 resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.2.2.tgz#d86200c700053bba637913fe4310ba1bdeb5568e"
5463 integrity sha512-3qAQpykRTD5DReLu5/cwpsg7EZFzP3Q0Hp2XUWJUw2mpq2jfgOKTZr8IZKKnNieRVVo1UauROTdhbQJZveGKtQ==
5464 dependencies:
5465 cacache "^15.0.5"
5466 find-cache-dir "^3.3.1"
5467 jest-worker "^26.3.0"
5468 p-limit "^3.0.2"
5469 schema-utils "^2.7.1"
5470 serialize-javascript "^5.0.1"
5471 source-map "^0.6.1"
5472 terser "^5.3.2"
5473 webpack-sources "^1.4.3"
5474
5475terser@^4.1.2:
5476 version "4.8.0"
5477 resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
5478 integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
5430 dependencies: 5479 dependencies:
5431 chownr "^1.1.1" 5480 commander "^2.20.0"
5432 fs-minipass "^1.2.5" 5481 source-map "~0.6.1"
5433 minipass "^2.3.4" 5482 source-map-support "~0.5.12"
5434 minizlib "^1.1.1"
5435 mkdirp "^0.5.0"
5436 safe-buffer "^5.1.2"
5437 yallist "^3.0.2"
5438 5483
5439text-table@~0.2.0: 5484terser@^5.3.2:
5485 version "5.3.2"
5486 resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.2.tgz#f4bea90eb92945b2a028ceef79181b9bb586e7af"
5487 integrity sha512-H67sydwBz5jCUA32ZRL319ULu+Su1cAoZnnc+lXnenGRYWyLE3Scgkt8mNoAsMx0h5kdo758zdoS0LG9rYZXDQ==
5488 dependencies:
5489 commander "^2.20.0"
5490 source-map "~0.6.1"
5491 source-map-support "~0.5.12"
5492
5493text-table@^0.2.0:
5440 version "0.2.0" 5494 version "0.2.0"
5441 resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" 5495 resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
5442 integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= 5496 integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
5443 5497
5444through@^2.3.6: 5498through2@^2.0.0:
5445 version "2.3.8" 5499 version "2.0.5"
5446 resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 5500 resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
5447 integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= 5501 integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
5502 dependencies:
5503 readable-stream "~2.3.6"
5504 xtend "~4.0.1"
5448 5505
5449timers-browserify@^2.0.4: 5506timers-browserify@^2.0.4:
5450 version "2.0.10" 5507 version "2.0.11"
5451 resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" 5508 resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"
5452 integrity sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg== 5509 integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==
5453 dependencies: 5510 dependencies:
5454 setimmediate "^1.0.4" 5511 setimmediate "^1.0.4"
5455 5512
5456tmp@^0.0.33:
5457 version "0.0.33"
5458 resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
5459 integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
5460 dependencies:
5461 os-tmpdir "~1.0.2"
5462
5463to-arraybuffer@^1.0.0: 5513to-arraybuffer@^1.0.0:
5464 version "1.0.1" 5514 version "1.0.1"
5465 resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" 5515 resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
5466 integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= 5516 integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
5467 5517
5468to-fast-properties@^1.0.3: 5518to-fast-properties@^2.0.0:
5469 version "1.0.3" 5519 version "2.0.0"
5470 resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" 5520 resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
5471 integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= 5521 integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
5472 5522
5473to-object-path@^0.3.0: 5523to-object-path@^0.3.0:
5474 version "0.3.0" 5524 version "0.3.0"
@@ -5485,6 +5535,13 @@ to-regex-range@^2.1.0:
5485 is-number "^3.0.0" 5535 is-number "^3.0.0"
5486 repeat-string "^1.6.1" 5536 repeat-string "^1.6.1"
5487 5537
5538to-regex-range@^5.0.1:
5539 version "5.0.1"
5540 resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
5541 integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
5542 dependencies:
5543 is-number "^7.0.0"
5544
5488to-regex@^3.0.1, to-regex@^3.0.2: 5545to-regex@^3.0.1, to-regex@^3.0.2:
5489 version "3.0.2" 5546 version "3.0.2"
5490 resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" 5547 resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -5495,108 +5552,194 @@ to-regex@^3.0.1, to-regex@^3.0.2:
5495 regex-not "^1.0.2" 5552 regex-not "^1.0.2"
5496 safe-regex "^1.1.0" 5553 safe-regex "^1.1.0"
5497 5554
5498tough-cookie@~2.4.3: 5555trim-newlines@^3.0.0:
5499 version "2.4.3" 5556 version "3.0.0"
5500 resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" 5557 resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30"
5501 integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== 5558 integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==
5502 dependencies:
5503 psl "^1.1.24"
5504 punycode "^1.4.1"
5505 5559
5506trim-newlines@^1.0.0: 5560trim-trailing-lines@^1.0.0:
5507 version "1.0.0" 5561 version "1.1.3"
5508 resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" 5562 resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz#7f0739881ff76657b7776e10874128004b625a94"
5509 integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= 5563 integrity sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==
5510 5564
5511trim-right@^1.0.1: 5565trim@0.0.1:
5512 version "1.0.1" 5566 version "0.0.1"
5513 resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" 5567 resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
5514 integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= 5568 integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0=
5515 5569
5516"true-case-path@^1.0.2": 5570trough@^1.0.0:
5517 version "1.0.3" 5571 version "1.0.5"
5518 resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" 5572 resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
5519 integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== 5573 integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
5574
5575tsconfig-paths@^3.9.0:
5576 version "3.9.0"
5577 resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
5578 integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==
5520 dependencies: 5579 dependencies:
5521 glob "^7.1.2" 5580 "@types/json5" "^0.0.29"
5581 json5 "^1.0.1"
5582 minimist "^1.2.0"
5583 strip-bom "^3.0.0"
5584
5585tslib@^1.9.0:
5586 version "1.13.0"
5587 resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
5588 integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
5522 5589
5523tty-browserify@0.0.0: 5590tty-browserify@0.0.0:
5524 version "0.0.0" 5591 version "0.0.0"
5525 resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" 5592 resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
5526 integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= 5593 integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
5527 5594
5528tunnel-agent@^0.6.0: 5595type-check@^0.4.0, type-check@~0.4.0:
5529 version "0.6.0" 5596 version "0.4.0"
5530 resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 5597 resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
5531 integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= 5598 integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
5532 dependencies: 5599 dependencies:
5533 safe-buffer "^5.0.1" 5600 prelude-ls "^1.2.1"
5534 5601
5535tweetnacl@^0.14.3, tweetnacl@~0.14.0: 5602type-fest@^0.13.1:
5536 version "0.14.5" 5603 version "0.13.1"
5537 resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 5604 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
5538 integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= 5605 integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
5539 5606
5540type-check@~0.3.2: 5607type-fest@^0.6.0:
5541 version "0.3.2" 5608 version "0.6.0"
5542 resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" 5609 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
5543 integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= 5610 integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
5611
5612type-fest@^0.8.1:
5613 version "0.8.1"
5614 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
5615 integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
5616
5617typedarray-to-buffer@^3.1.5:
5618 version "3.1.5"
5619 resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
5620 integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
5544 dependencies: 5621 dependencies:
5545 prelude-ls "~1.1.2" 5622 is-typedarray "^1.0.0"
5546 5623
5547typedarray@^0.0.6: 5624typedarray@^0.0.6:
5548 version "0.0.6" 5625 version "0.0.6"
5549 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 5626 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
5550 integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= 5627 integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
5551 5628
5552uglify-js@^2.8.29: 5629unherit@^1.0.4:
5553 version "2.8.29" 5630 version "1.1.3"
5554 resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" 5631 resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22"
5555 integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= 5632 integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==
5556 dependencies: 5633 dependencies:
5557 source-map "~0.5.1" 5634 inherits "^2.0.0"
5558 yargs "~3.10.0" 5635 xtend "^4.0.0"
5559 optionalDependencies:
5560 uglify-to-browserify "~1.0.0"
5561 5636
5562uglify-to-browserify@~1.0.0: 5637unicode-canonical-property-names-ecmascript@^1.0.4:
5563 version "1.0.2" 5638 version "1.0.4"
5564 resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" 5639 resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
5565 integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= 5640 integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
5566 5641
5567uglifyjs-webpack-plugin@^0.4.6: 5642unicode-match-property-ecmascript@^1.0.4:
5568 version "0.4.6" 5643 version "1.0.4"
5569 resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" 5644 resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
5570 integrity sha1-uVH0q7a9YX5m9j64kUmOORdj4wk= 5645 integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
5571 dependencies: 5646 dependencies:
5572 source-map "^0.5.6" 5647 unicode-canonical-property-names-ecmascript "^1.0.4"
5573 uglify-js "^2.8.29" 5648 unicode-property-aliases-ecmascript "^1.0.4"
5574 webpack-sources "^1.0.1" 5649
5650unicode-match-property-value-ecmascript@^1.2.0:
5651 version "1.2.0"
5652 resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
5653 integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
5654
5655unicode-property-aliases-ecmascript@^1.0.4:
5656 version "1.1.0"
5657 resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
5658 integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
5659
5660unified@^9.0.0:
5661 version "9.2.0"
5662 resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8"
5663 integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==
5664 dependencies:
5665 bail "^1.0.0"
5666 extend "^3.0.0"
5667 is-buffer "^2.0.0"
5668 is-plain-obj "^2.0.0"
5669 trough "^1.0.0"
5670 vfile "^4.0.0"
5575 5671
5576union-value@^1.0.0: 5672union-value@^1.0.0:
5577 version "1.0.0" 5673 version "1.0.1"
5578 resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" 5674 resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
5579 integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ= 5675 integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
5580 dependencies: 5676 dependencies:
5581 arr-union "^3.1.0" 5677 arr-union "^3.1.0"
5582 get-value "^2.0.6" 5678 get-value "^2.0.6"
5583 is-extendable "^0.1.1" 5679 is-extendable "^0.1.1"
5584 set-value "^0.4.3" 5680 set-value "^2.0.1"
5585 5681
5586uniq@^1.0.1: 5682uniq@^1.0.1:
5587 version "1.0.1" 5683 version "1.0.1"
5588 resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" 5684 resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
5589 integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= 5685 integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
5590 5686
5591uniqs@^2.0.0: 5687unique-filename@^1.1.1:
5592 version "2.0.0" 5688 version "1.1.1"
5593 resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" 5689 resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
5594 integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= 5690 integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
5691 dependencies:
5692 unique-slug "^2.0.0"
5595 5693
5596universalify@^0.1.0: 5694unique-slug@^2.0.0:
5597 version "0.1.2" 5695 version "2.0.2"
5598 resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" 5696 resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
5599 integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== 5697 integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
5698 dependencies:
5699 imurmurhash "^0.1.4"
5700
5701unist-util-find-all-after@^3.0.1:
5702 version "3.0.1"
5703 resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-3.0.1.tgz#95cc62f48812d879b4685a0512bf1b838da50e9a"
5704 integrity sha512-0GICgc++sRJesLwEYDjFVJPJttBpVQaTNgc6Jw0Jhzvfs+jtKePEMu+uD+PqkRUrAvGQqwhpDwLGWo1PK8PDEw==
5705 dependencies:
5706 unist-util-is "^4.0.0"
5707
5708unist-util-is@^4.0.0:
5709 version "4.0.2"
5710 resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.2.tgz#c7d1341188aa9ce5b3cff538958de9895f14a5de"
5711 integrity sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ==
5712
5713unist-util-remove-position@^2.0.0:
5714 version "2.0.1"
5715 resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc"
5716 integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==
5717 dependencies:
5718 unist-util-visit "^2.0.0"
5719
5720unist-util-stringify-position@^2.0.0:
5721 version "2.0.3"
5722 resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da"
5723 integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==
5724 dependencies:
5725 "@types/unist" "^2.0.2"
5726
5727unist-util-visit-parents@^3.0.0:
5728 version "3.1.0"
5729 resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.0.tgz#4dd262fb9dcfe44f297d53e882fc6ff3421173d5"
5730 integrity sha512-0g4wbluTF93npyPrp/ymd3tCDTMnP0yo2akFD2FIBAYXq/Sga3lwaU1D8OYKbtpioaI6CkDcQ6fsMnmtzt7htw==
5731 dependencies:
5732 "@types/unist" "^2.0.0"
5733 unist-util-is "^4.0.0"
5734
5735unist-util-visit@^2.0.0:
5736 version "2.0.3"
5737 resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
5738 integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==
5739 dependencies:
5740 "@types/unist" "^2.0.0"
5741 unist-util-is "^4.0.0"
5742 unist-util-visit-parents "^3.0.0"
5600 5743
5601unset-value@^1.0.0: 5744unset-value@^1.0.0:
5602 version "1.0.0" 5745 version "1.0.0"
@@ -5607,14 +5750,14 @@ unset-value@^1.0.0:
5607 isobject "^3.0.0" 5750 isobject "^3.0.0"
5608 5751
5609upath@^1.1.1: 5752upath@^1.1.1:
5610 version "1.1.2" 5753 version "1.2.0"
5611 resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" 5754 resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
5612 integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== 5755 integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
5613 5756
5614uri-js@^4.2.2: 5757uri-js@^4.2.2:
5615 version "4.2.2" 5758 version "4.4.0"
5616 resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" 5759 resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602"
5617 integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== 5760 integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==
5618 dependencies: 5761 dependencies:
5619 punycode "^2.1.0" 5762 punycode "^2.1.0"
5620 5763
@@ -5623,15 +5766,6 @@ urix@^0.1.0:
5623 resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" 5766 resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
5624 integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= 5767 integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
5625 5768
5626url-loader@^0.6.2:
5627 version "0.6.2"
5628 resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
5629 integrity sha512-h3qf9TNn53BpuXTTcpC+UehiRrl0Cv45Yr/xWayApjw6G8Bg2dGke7rIwDQ39piciWCWrC+WiqLjOh3SUp9n0Q==
5630 dependencies:
5631 loader-utils "^1.0.2"
5632 mime "^1.4.1"
5633 schema-utils "^0.3.0"
5634
5635url@^0.11.0: 5769url@^0.11.0:
5636 version "0.11.0" 5770 version "0.11.0"
5637 resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" 5771 resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
@@ -5645,14 +5779,7 @@ use@^3.1.0:
5645 resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" 5779 resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
5646 integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== 5780 integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
5647 5781
5648user-home@^2.0.0: 5782util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
5649 version "2.0.0"
5650 resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
5651 integrity sha1-nHC/2Babwdy/SGBODwS4tJzenp8=
5652 dependencies:
5653 os-homedir "^1.0.0"
5654
5655util-deprecate@~1.0.1:
5656 version "1.0.2" 5783 version "1.0.2"
5657 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 5784 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
5658 integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 5785 integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
@@ -5664,13 +5791,6 @@ util@0.10.3:
5664 dependencies: 5791 dependencies:
5665 inherits "2.0.1" 5792 inherits "2.0.1"
5666 5793
5667util@^0.10.3:
5668 version "0.10.4"
5669 resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
5670 integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
5671 dependencies:
5672 inherits "2.0.3"
5673
5674util@^0.11.0: 5794util@^0.11.0:
5675 version "0.11.1" 5795 version "0.11.1"
5676 resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" 5796 resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
@@ -5678,10 +5798,10 @@ util@^0.11.0:
5678 dependencies: 5798 dependencies:
5679 inherits "2.0.3" 5799 inherits "2.0.3"
5680 5800
5681uuid@^3.3.2: 5801v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1:
5682 version "3.3.2" 5802 version "2.1.1"
5683 resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" 5803 resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
5684 integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== 5804 integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
5685 5805
5686validate-npm-package-license@^3.0.1: 5806validate-npm-package-license@^3.0.1:
5687 version "3.0.4" 5807 version "3.0.4"
@@ -5691,214 +5811,222 @@ validate-npm-package-license@^3.0.1:
5691 spdx-correct "^3.0.0" 5811 spdx-correct "^3.0.0"
5692 spdx-expression-parse "^3.0.0" 5812 spdx-expression-parse "^3.0.0"
5693 5813
5694vendors@^1.0.0: 5814vfile-location@^3.0.0:
5695 version "1.0.3" 5815 version "3.1.0"
5696 resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.3.tgz#a6467781abd366217c050f8202e7e50cc9eef8c0" 5816 resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.1.0.tgz#81cd8a04b0ac935185f4fce16f270503fc2f692f"
5697 integrity sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw== 5817 integrity sha512-FCZ4AN9xMcjFIG1oGmZKo61PjwJHRVA+0/tPUP2ul4uIwjGGndIxavEMRpWn5p4xwm/ZsdXp9YNygf1ZyE4x8g==
5698 5818
5699verror@1.10.0: 5819vfile-message@^2.0.0:
5700 version "1.10.0" 5820 version "2.0.4"
5701 resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 5821 resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a"
5702 integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= 5822 integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==
5703 dependencies: 5823 dependencies:
5704 assert-plus "^1.0.0" 5824 "@types/unist" "^2.0.0"
5705 core-util-is "1.0.2" 5825 unist-util-stringify-position "^2.0.0"
5706 extsprintf "^1.2.0" 5826
5827vfile@^4.0.0:
5828 version "4.2.0"
5829 resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.0.tgz#26c78ac92eb70816b01d4565e003b7e65a2a0e01"
5830 integrity sha512-a/alcwCvtuc8OX92rqqo7PflxiCgXRFjdyoGVuYV+qbgCb0GgZJRvIgCD4+U/Kl1yhaRsaTwksF88xbPyGsgpw==
5831 dependencies:
5832 "@types/unist" "^2.0.0"
5833 is-buffer "^2.0.0"
5834 replace-ext "1.0.0"
5835 unist-util-stringify-position "^2.0.0"
5836 vfile-message "^2.0.0"
5837
5838vm-browserify@^1.0.1:
5839 version "1.1.2"
5840 resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
5841 integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
5707 5842
5708vm-browserify@0.0.4: 5843watchpack-chokidar2@^2.0.0:
5709 version "0.0.4" 5844 version "2.0.0"
5710 resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" 5845 resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
5711 integrity sha1-XX6kW7755Kb/ZflUOOCofDV9WnM= 5846 integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==
5712 dependencies: 5847 dependencies:
5713 indexof "0.0.1" 5848 chokidar "^2.1.8"
5714 5849
5715watchpack@^1.4.0: 5850watchpack@^1.7.4:
5716 version "1.6.0" 5851 version "1.7.4"
5717 resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" 5852 resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b"
5718 integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== 5853 integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==
5719 dependencies: 5854 dependencies:
5720 chokidar "^2.0.2"
5721 graceful-fs "^4.1.2" 5855 graceful-fs "^4.1.2"
5722 neo-async "^2.5.0" 5856 neo-async "^2.5.0"
5723 5857 optionalDependencies:
5724webpack-sources@^1.0.1: 5858 chokidar "^3.4.1"
5725 version "1.3.0" 5859 watchpack-chokidar2 "^2.0.0"
5726 resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" 5860
5727 integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== 5861webpack-cli@^3.3.12:
5862 version "3.3.12"
5863 resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a"
5864 integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==
5865 dependencies:
5866 chalk "^2.4.2"
5867 cross-spawn "^6.0.5"
5868 enhanced-resolve "^4.1.1"
5869 findup-sync "^3.0.0"
5870 global-modules "^2.0.0"
5871 import-local "^2.0.0"
5872 interpret "^1.4.0"
5873 loader-utils "^1.4.0"
5874 supports-color "^6.1.0"
5875 v8-compile-cache "^2.1.1"
5876 yargs "^13.3.2"
5877
5878webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
5879 version "1.4.3"
5880 resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
5881 integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
5728 dependencies: 5882 dependencies:
5729 source-list-map "^2.0.0" 5883 source-list-map "^2.0.0"
5730 source-map "~0.6.1" 5884 source-map "~0.6.1"
5731 5885
5732webpack@^3.10.0: 5886webpack@^4.44.2:
5733 version "3.12.0" 5887 version "4.44.2"
5734 resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.12.0.tgz#3f9e34360370602fcf639e97939db486f4ec0d74" 5888 resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72"
5735 integrity sha512-Sw7MdIIOv/nkzPzee4o0EdvCuPmxT98+vVpIvwtcwcF1Q4SDSNp92vwcKc4REe7NItH9f1S4ra9FuQ7yuYZ8bQ== 5889 integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==
5736 dependencies: 5890 dependencies:
5737 acorn "^5.0.0" 5891 "@webassemblyjs/ast" "1.9.0"
5738 acorn-dynamic-import "^2.0.0" 5892 "@webassemblyjs/helper-module-context" "1.9.0"
5739 ajv "^6.1.0" 5893 "@webassemblyjs/wasm-edit" "1.9.0"
5740 ajv-keywords "^3.1.0" 5894 "@webassemblyjs/wasm-parser" "1.9.0"
5741 async "^2.1.2" 5895 acorn "^6.4.1"
5742 enhanced-resolve "^3.4.0" 5896 ajv "^6.10.2"
5743 escope "^3.6.0" 5897 ajv-keywords "^3.4.1"
5744 interpret "^1.0.0" 5898 chrome-trace-event "^1.0.2"
5745 json-loader "^0.5.4" 5899 enhanced-resolve "^4.3.0"
5746 json5 "^0.5.1" 5900 eslint-scope "^4.0.3"
5747 loader-runner "^2.3.0" 5901 json-parse-better-errors "^1.0.2"
5748 loader-utils "^1.1.0" 5902 loader-runner "^2.4.0"
5749 memory-fs "~0.4.1" 5903 loader-utils "^1.2.3"
5750 mkdirp "~0.5.0" 5904 memory-fs "^0.4.1"
5751 node-libs-browser "^2.0.0" 5905 micromatch "^3.1.10"
5752 source-map "^0.5.3" 5906 mkdirp "^0.5.3"
5753 supports-color "^4.2.1" 5907 neo-async "^2.6.1"
5754 tapable "^0.2.7" 5908 node-libs-browser "^2.2.1"
5755 uglifyjs-webpack-plugin "^0.4.6" 5909 schema-utils "^1.0.0"
5756 watchpack "^1.4.0" 5910 tapable "^1.1.3"
5757 webpack-sources "^1.0.1" 5911 terser-webpack-plugin "^1.4.3"
5758 yargs "^8.0.2" 5912 watchpack "^1.7.4"
5759 5913 webpack-sources "^1.4.1"
5760whet.extend@~0.9.9:
5761 version "0.9.9"
5762 resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
5763 integrity sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=
5764
5765which-module@^1.0.0:
5766 version "1.0.0"
5767 resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
5768 integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=
5769 5914
5770which-module@^2.0.0: 5915which-module@^2.0.0:
5771 version "2.0.0" 5916 version "2.0.0"
5772 resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 5917 resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
5773 integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= 5918 integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
5774 5919
5775which@1, which@^1.2.9: 5920which@^1.2.14, which@^1.2.9, which@^1.3.1:
5776 version "1.3.1" 5921 version "1.3.1"
5777 resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 5922 resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
5778 integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== 5923 integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
5779 dependencies: 5924 dependencies:
5780 isexe "^2.0.0" 5925 isexe "^2.0.0"
5781 5926
5782wide-align@^1.1.0: 5927which@^2.0.1:
5783 version "1.1.3" 5928 version "2.0.2"
5784 resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" 5929 resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
5785 integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== 5930 integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
5786 dependencies: 5931 dependencies:
5787 string-width "^1.0.2 || 2" 5932 isexe "^2.0.0"
5788
5789window-size@0.1.0:
5790 version "0.1.0"
5791 resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
5792 integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=
5793 5933
5794wordwrap@0.0.2: 5934word-wrap@^1.2.3:
5795 version "0.0.2" 5935 version "1.2.3"
5796 resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" 5936 resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
5797 integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= 5937 integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
5798 5938
5799wordwrap@~1.0.0: 5939worker-farm@^1.7.0:
5800 version "1.0.0" 5940 version "1.7.0"
5801 resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" 5941 resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
5802 integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= 5942 integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==
5943 dependencies:
5944 errno "~0.1.7"
5803 5945
5804wrap-ansi@^2.0.0: 5946wrap-ansi@^5.1.0:
5805 version "2.1.0" 5947 version "5.1.0"
5806 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" 5948 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
5807 integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= 5949 integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
5808 dependencies: 5950 dependencies:
5809 string-width "^1.0.1" 5951 ansi-styles "^3.2.0"
5810 strip-ansi "^3.0.1" 5952 string-width "^3.0.0"
5953 strip-ansi "^5.0.0"
5811 5954
5812wrappy@1: 5955wrappy@1:
5813 version "1.0.2" 5956 version "1.0.2"
5814 resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 5957 resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
5815 integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 5958 integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
5816 5959
5817write@^0.2.1: 5960write-file-atomic@^3.0.3:
5818 version "0.2.1" 5961 version "3.0.3"
5819 resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" 5962 resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
5820 integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= 5963 integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
5964 dependencies:
5965 imurmurhash "^0.1.4"
5966 is-typedarray "^1.0.0"
5967 signal-exit "^3.0.2"
5968 typedarray-to-buffer "^3.1.5"
5969
5970write@1.0.3:
5971 version "1.0.3"
5972 resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
5973 integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
5821 dependencies: 5974 dependencies:
5822 mkdirp "^0.5.1" 5975 mkdirp "^0.5.1"
5823 5976
5824xtend@^4.0.0: 5977xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
5825 version "4.0.1" 5978 version "4.0.2"
5826 resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 5979 resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
5827 integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= 5980 integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
5828 5981
5829y18n@^3.2.1: 5982y18n@^4.0.0:
5830 version "3.2.1" 5983 version "4.0.0"
5831 resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" 5984 resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
5832 integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= 5985 integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
5833 5986
5834yallist@^2.1.2: 5987yallist@^3.0.2:
5835 version "2.1.2" 5988 version "3.1.1"
5836 resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 5989 resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
5837 integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= 5990 integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
5838 5991
5839yallist@^3.0.0, yallist@^3.0.2: 5992yallist@^4.0.0:
5840 version "3.0.3" 5993 version "4.0.0"
5841 resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" 5994 resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
5842 integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== 5995 integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
5843 5996
5844yargs-parser@^5.0.0: 5997yaml@^1.10.0:
5845 version "5.0.0" 5998 version "1.10.0"
5846 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" 5999 resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
5847 integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= 6000 integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
6001
6002yargs-parser@^13.1.2:
6003 version "13.1.2"
6004 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
6005 integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
5848 dependencies: 6006 dependencies:
5849 camelcase "^3.0.0" 6007 camelcase "^5.0.0"
6008 decamelize "^1.2.0"
5850 6009
5851yargs-parser@^7.0.0: 6010yargs-parser@^18.1.3:
5852 version "7.0.0" 6011 version "18.1.3"
5853 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" 6012 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
5854 integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k= 6013 integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
5855 dependencies: 6014 dependencies:
5856 camelcase "^4.1.0" 6015 camelcase "^5.0.0"
5857 6016 decamelize "^1.2.0"
5858yargs@^7.0.0: 6017
5859 version "7.1.0" 6018yargs@^13.3.2:
5860 resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" 6019 version "13.3.2"
5861 integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= 6020 resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
5862 dependencies: 6021 integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
5863 camelcase "^3.0.0" 6022 dependencies:
5864 cliui "^3.2.0" 6023 cliui "^5.0.0"
5865 decamelize "^1.1.1" 6024 find-up "^3.0.0"
5866 get-caller-file "^1.0.1" 6025 get-caller-file "^2.0.1"
5867 os-locale "^1.4.0"
5868 read-pkg-up "^1.0.1"
5869 require-directory "^2.1.1"
5870 require-main-filename "^1.0.1"
5871 set-blocking "^2.0.0"
5872 string-width "^1.0.2"
5873 which-module "^1.0.0"
5874 y18n "^3.2.1"
5875 yargs-parser "^5.0.0"
5876
5877yargs@^8.0.2:
5878 version "8.0.2"
5879 resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
5880 integrity sha1-YpmpBVsc78lp/355wdkY3Osiw2A=
5881 dependencies:
5882 camelcase "^4.1.0"
5883 cliui "^3.2.0"
5884 decamelize "^1.1.1"
5885 get-caller-file "^1.0.1"
5886 os-locale "^2.0.0"
5887 read-pkg-up "^2.0.0"
5888 require-directory "^2.1.1" 6026 require-directory "^2.1.1"
5889 require-main-filename "^1.0.1" 6027 require-main-filename "^2.0.0"
5890 set-blocking "^2.0.0" 6028 set-blocking "^2.0.0"
5891 string-width "^2.0.0" 6029 string-width "^3.0.0"
5892 which-module "^2.0.0" 6030 which-module "^2.0.0"
5893 y18n "^3.2.1" 6031 y18n "^4.0.0"
5894 yargs-parser "^7.0.0" 6032 yargs-parser "^13.1.2"
5895
5896yargs@~3.10.0:
5897 version "3.10.0"
5898 resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
5899 integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=
5900 dependencies:
5901 camelcase "^1.0.2"
5902 cliui "^2.1.0"
5903 decamelize "^1.0.0"
5904 window-size "0.1.0"