aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-13 12:05:08 +0200
committerArthurHoaro <arthur@hoa.ro>2020-10-13 12:05:08 +0200
commitb6f678a5a1d15acf284ebcec16c905e976671ce1 (patch)
tree33c7da831482ed79c44896ef19c73c72ada84f2e
parentb14687036b9b800681197f51fdc47e62f0c88e2e (diff)
parent1c1520b6b98ab20201bfe15577782a52320339df (diff)
downloadShaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.tar.gz
Shaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.tar.zst
Shaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.zip
Merge branch 'v0.12' into latest
-rw-r--r--.dev/.sasslintrc17
-rw-r--r--.dev/.stylelintrc.js15
-rw-r--r--.editorconfig2
-rw-r--r--.github/mailmap7
-rw-r--r--.htaccess23
-rw-r--r--.travis.yml31
-rw-r--r--AUTHORS31
-rw-r--r--CHANGELOG.md72
-rw-r--r--Dockerfile1
-rw-r--r--Dockerfile.armhf2
-rw-r--r--Makefile15
-rw-r--r--README.md2
-rw-r--r--application/ApplicationUtils.php3
-rw-r--r--application/History.php17
-rw-r--r--application/Languages.php3
-rw-r--r--application/Router.php184
-rw-r--r--application/Thumbnailer.php4
-rw-r--r--application/Utils.php22
-rw-r--r--application/api/ApiMiddleware.php30
-rw-r--r--application/api/ApiUtils.php83
-rw-r--r--application/api/controllers/ApiController.php8
-rw-r--r--application/api/controllers/HistoryController.php2
-rw-r--r--application/api/controllers/Info.php5
-rw-r--r--application/api/controllers/Links.php79
-rw-r--r--application/api/controllers/Tags.php44
-rw-r--r--application/bookmark/Bookmark.php462
-rw-r--r--application/bookmark/BookmarkArray.php260
-rw-r--r--application/bookmark/BookmarkFileService.php407
-rw-r--r--application/bookmark/BookmarkFilter.php473
-rw-r--r--application/bookmark/BookmarkIO.php108
-rw-r--r--application/bookmark/BookmarkInitializer.php110
-rw-r--r--application/bookmark/BookmarkServiceInterface.php186
-rw-r--r--application/bookmark/LinkUtils.php137
-rw-r--r--application/bookmark/exception/BookmarkNotFoundException.php (renamed from application/bookmark/exception/LinkNotFoundException.php)2
-rw-r--r--application/bookmark/exception/DatastoreNotInitializedException.php10
-rw-r--r--application/bookmark/exception/EmptyDataStoreException.php7
-rw-r--r--application/bookmark/exception/InvalidBookmarkException.php30
-rw-r--r--application/bookmark/exception/NotWritableDataStoreException.php19
-rw-r--r--application/config/ConfigJson.php2
-rw-r--r--application/config/ConfigManager.php6
-rw-r--r--application/config/ConfigPlugin.php17
-rw-r--r--application/container/ContainerBuilder.php165
-rw-r--r--application/container/ShaarliContainer.php51
-rw-r--r--application/feed/Cache.php38
-rw-r--r--application/feed/FeedBuilder.php189
-rw-r--r--application/formatter/BookmarkDefaultFormatter.php87
-rw-r--r--application/formatter/BookmarkFormatter.php313
-rw-r--r--application/formatter/BookmarkMarkdownFormatter.php206
-rw-r--r--application/formatter/BookmarkRawFormatter.php13
-rw-r--r--application/formatter/FormatterFactory.php51
-rw-r--r--application/front/ShaarliAdminMiddleware.php27
-rw-r--r--application/front/ShaarliMiddleware.php114
-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/ManageShaareController.php371
-rw-r--r--application/front/controller/admin/ManageTagController.php88
-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/SessionFilterController.php50
-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.php241
-rw-r--r--application/front/controller/visitor/DailyController.php192
-rw-r--r--application/front/controller/visitor/ErrorController.php45
-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.php165
-rw-r--r--application/front/controller/visitor/LoginController.php154
-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.php180
-rw-r--r--application/front/controller/visitor/TagCloudController.php121
-rw-r--r--application/front/controller/visitor/TagController.php118
-rw-r--r--application/front/exceptions/AlreadyInstalledException.php15
-rw-r--r--application/front/exceptions/CantLoginException.php10
-rw-r--r--application/front/exceptions/LoginBannedException.php15
-rw-r--r--application/front/exceptions/OpenShaarliPasswordException.php18
-rw-r--r--application/front/exceptions/ResourcePermissionException.php13
-rw-r--r--application/front/exceptions/ShaarliFrontException.php23
-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/http/HttpAccess.php39
-rw-r--r--application/http/HttpUtils.php125
-rw-r--r--application/http/UrlUtils.php2
-rw-r--r--application/legacy/LegacyController.php162
-rw-r--r--application/legacy/LegacyLinkDB.php (renamed from application/bookmark/LinkDB.php)75
-rw-r--r--application/legacy/LegacyLinkFilter.php (renamed from application/bookmark/LinkFilter.php)20
-rw-r--r--application/legacy/LegacyRouter.php63
-rw-r--r--application/legacy/LegacyUpdater.php618
-rw-r--r--application/legacy/UnknowLegacyRouteException.php9
-rw-r--r--application/netscape/NetscapeBookmarkUtils.php212
-rw-r--r--application/plugin/PluginManager.php30
-rw-r--r--application/render/PageBuilder.php94
-rw-r--r--application/render/PageCacheManager.php60
-rw-r--r--application/render/TemplatePage.php33
-rw-r--r--application/security/CookieManager.php33
-rw-r--r--application/security/LoginManager.php95
-rw-r--r--application/security/SessionManager.php114
-rw-r--r--application/updater/Updater.php485
-rw-r--r--application/updater/UpdaterUtils.php65
-rw-r--r--assets/common/css/markdown.css (renamed from plugins/markdown/markdown.css)4
-rw-r--r--assets/common/js/thumbnails-update.js14
-rw-r--r--assets/default/js/base.js73
-rw-r--r--assets/default/scss/shaarli.scss66
-rw-r--r--assets/vintage/css/shaarli.css2
-rw-r--r--composer.json14
-rw-r--r--composer.lock1601
-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)66
-rw-r--r--doc/md/Continuous-integration-tools.md29
-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.md644
-rw-r--r--doc/md/Server-security.md76
-rw-r--r--doc/md/Shaarli-configuration.md222
-rw-r--r--doc/md/Sharing-content.md71
-rw-r--r--doc/md/Static-analysis.md13
-rw-r--r--doc/md/Troubleshooting.md205
-rw-r--r--doc/md/Unit-tests-Docker.md56
-rw-r--r--doc/md/Unit-tests.md157
-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)169
-rw-r--r--doc/md/dev/Release-Shaarli.md145
-rw-r--r--doc/md/dev/Theming.md (renamed from doc/md/Theming.md)5
-rw-r--r--doc/md/dev/Translations.md (renamed from doc/md/Translations.md)99
-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.md124
-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.md33
-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.md120
-rw-r--r--docker-compose.yml2
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po1219
-rw-r--r--inc/languages/jp/LC_MESSAGES/shaarli.po1293
-rw-r--r--index.php1981
-rw-r--r--init.php85
-rw-r--r--mkdocs.yml47
-rw-r--r--package.json31
-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.php33
-rw-r--r--plugins/isso/isso.php6
-rw-r--r--plugins/isso/isso_button.html5
-rw-r--r--plugins/markdown/README.md102
-rw-r--r--plugins/markdown/help.html5
-rw-r--r--plugins/markdown/markdown.meta4
-rw-r--r--plugins/markdown/markdown.php365
-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--shaarli_version.php2
-rw-r--r--tests/ApplicationUtilsTest.php31
-rw-r--r--tests/FileUtilsTest.php22
-rw-r--r--tests/HistoryTest.php255
-rw-r--r--tests/LanguagesTest.php4
-rw-r--r--tests/PluginManagerTest.php79
-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.php12
-rw-r--r--tests/api/ApiMiddlewareTest.php73
-rw-r--r--tests/api/ApiUtilsTest.php145
-rw-r--r--tests/api/controllers/history/HistoryTest.php10
-rw-r--r--tests/api/controllers/info/InfoTest.php24
-rw-r--r--tests/api/controllers/links/DeleteLinkTest.php32
-rw-r--r--tests/api/controllers/links/GetLinkIdTest.php25
-rw-r--r--tests/api/controllers/links/GetLinksTest.php23
-rw-r--r--tests/api/controllers/links/PostLinkTest.php40
-rw-r--r--tests/api/controllers/links/PutLinkTest.php40
-rw-r--r--tests/api/controllers/tags/DeleteTagTest.php41
-rw-r--r--tests/api/controllers/tags/GetTagNameTest.php20
-rw-r--r--tests/api/controllers/tags/GetTagsTest.php29
-rw-r--r--tests/api/controllers/tags/PutTagTest.php43
-rw-r--r--tests/bookmark/BookmarkArrayTest.php236
-rw-r--r--tests/bookmark/BookmarkFileServiceTest.php1111
-rw-r--r--tests/bookmark/BookmarkFilterTest.php526
-rw-r--r--tests/bookmark/BookmarkInitializerTest.php150
-rw-r--r--tests/bookmark/BookmarkTest.php388
-rw-r--r--tests/bookmark/LinkUtilsTest.php48
-rw-r--r--tests/bootstrap.php26
-rw-r--r--tests/config/ConfigJsonTest.php27
-rw-r--r--tests/config/ConfigManagerTest.php26
-rw-r--r--tests/config/ConfigPhpTest.php8
-rw-r--r--tests/config/ConfigPluginTest.php22
-rw-r--r--tests/container/ContainerBuilderTest.php88
-rw-r--r--tests/container/ShaarliTestContainer.php42
-rw-r--r--tests/feed/CachedPageTest.php13
-rw-r--r--tests/feed/FeedBuilderTest.php171
-rw-r--r--tests/formatter/BookmarkDefaultFormatterTest.php177
-rw-r--r--tests/formatter/BookmarkMarkdownFormatterTest.php160
-rw-r--r--tests/formatter/BookmarkRawFormatterTest.php97
-rw-r--r--tests/formatter/FormatterFactoryTest.php101
-rw-r--r--tests/front/ShaarliAdminMiddlewareTest.php100
-rw-r--r--tests/front/ShaarliMiddlewareTest.php221
-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/ManageShaareControllerTest/AddShaareTest.php47
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php418
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php376
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php317
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php155
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php145
-rw-r--r--tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php308
-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/SessionFilterControllerTest.php177
-rw-r--r--tests/front/controller/admin/ShaarliAdminControllerTest.php184
-rw-r--r--tests/front/controller/admin/ThumbnailsControllerTest.php154
-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.php448
-rw-r--r--tests/front/controller/visitor/DailyControllerTest.php478
-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.php295
-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/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/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/LegacyDummyUpdater.php74
-rw-r--r--tests/legacy/LegacyLinkDBTest.php (renamed from tests/bookmark/LinkDBTest.php)141
-rw-r--r--tests/legacy/LegacyLinkFilterTest.php (renamed from tests/bookmark/LinkFilterTest.php)142
-rw-r--r--tests/legacy/LegacyUpdaterTest.php886
-rw-r--r--tests/netscape/BookmarkExportTest.php110
-rw-r--r--tests/netscape/BookmarkImportTest.php608
-rw-r--r--tests/plugins/PluginAddlinkTest.php14
-rw-r--r--tests/plugins/PluginArchiveorgTest.php41
-rw-r--r--tests/plugins/PluginDefaultColorsTest.php25
-rw-r--r--tests/plugins/PluginIssoTest.php35
-rw-r--r--tests/plugins/PluginMarkdownTest.php306
-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.php4
-rw-r--r--tests/security/LoginManagerTest.php68
-rw-r--r--tests/security/SessionManagerTest.php90
-rw-r--r--tests/updater/DummyUpdater.php21
-rw-r--r--tests/updater/UpdaterTest.php727
-rw-r--r--tests/utils/FakeBookmarkService.php18
-rw-r--r--tests/utils/ReferenceHistory.php2
-rw-r--r--tests/utils/ReferenceLinkDB.php119
-rw-r--r--tests/utils/config/configJson.json.php6
-rw-r--r--tpl/default/404.html4
-rw-r--r--tpl/default/addlink.html2
-rw-r--r--tpl/default/changepassword.html2
-rw-r--r--tpl/default/changetag.html4
-rw-r--r--tpl/default/configure.html28
-rw-r--r--tpl/default/daily.html12
-rw-r--r--tpl/default/dailyrss.html48
-rw-r--r--tpl/default/editlink.html21
-rw-r--r--tpl/default/error.html22
-rw-r--r--tpl/default/export.html3
-rw-r--r--tpl/default/feed.atom.html4
-rw-r--r--tpl/default/feed.rss.html4
-rw-r--r--tpl/default/import.html2
-rw-r--r--tpl/default/includes.html24
-rw-r--r--tpl/default/install.html2
-rw-r--r--tpl/default/linklist.html32
-rw-r--r--tpl/default/linklist.paging.html59
-rw-r--r--tpl/default/loginform.html60
-rw-r--r--tpl/default/opensearch.html4
-rw-r--r--tpl/default/page.footer.html7
-rw-r--r--tpl/default/page.header.html56
-rw-r--r--tpl/default/picwall.html86
-rw-r--r--tpl/default/pluginsadmin.html7
-rw-r--r--tpl/default/tag.cloud.html7
-rw-r--r--tpl/default/tag.list.html10
-rw-r--r--tpl/default/tag.sort.html8
-rw-r--r--tpl/default/thumbnails.html4
-rw-r--r--tpl/default/tools.html16
-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.html19
-rw-r--r--tpl/vintage/daily.html20
-rw-r--r--tpl/vintage/dailyrss.html46
-rw-r--r--tpl/vintage/editlink.html24
-rw-r--r--tpl/vintage/error.html25
-rw-r--r--tpl/vintage/export.html5
-rw-r--r--tpl/vintage/feed.atom.html6
-rw-r--r--tpl/vintage/feed.rss.html2
-rw-r--r--tpl/vintage/import.html2
-rw-r--r--tpl/vintage/includes.html20
-rw-r--r--tpl/vintage/install.html2
-rw-r--r--tpl/vintage/linklist.html36
-rw-r--r--tpl/vintage/linklist.paging.html17
-rw-r--r--tpl/vintage/loginform.html44
-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.html4
-rw-r--r--tpl/vintage/tools.html14
-rw-r--r--webpack.config.js72
-rw-r--r--yarn.lock7575
375 files changed, 30892 insertions, 14736 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/.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 d1415423..d7460947 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,31 +1,40 @@
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
14 - language: php
15 php: 7.4
6 - language: php 16 - language: php
7 php: 7.3 17 php: 7.3
8 - language: php 18 - language: php
9 php: 7.2 19 php: 7.2
10 - language: php 20 - language: php
11 php: 7.1 21 php: 7.1
22 # jobs for frontend builds
12 - language: node_js 23 - language: node_js
13 node_js: 8 24 node_js: 10
14 cache: 25 cache:
15 yarn: true 26 yarn: true
16 directories: 27 directories:
17 - $HOME/.cache/yarn 28 - $HOME/.cache/yarn
18
19 install: 29 install:
20 - yarn install 30 - yarn install
21
22 before_script: 31 before_script:
23 - PATH=${PATH//:\.\/node_modules\/\.bin/} 32 - PATH=${PATH//:\.\/node_modules\/\.bin/}
24
25 script: 33 script:
26 - yarn run build # Just to be sure that the build isn't broken 34 - yarn run build # verify successful frontend builds
27 - make eslint 35 - make eslint # javascript static analysis
28 - make sasslint 36 - make sasslint # linter for SASS syntax
37 # jobs for documentation builds
29 - language: python 38 - language: python
30 python: 3.6 39 python: 3.6
31 cache: 40 cache:
@@ -41,7 +50,9 @@ cache:
41 - $HOME/.composer/cache 50 - $HOME/.composer/cache
42 51
43install: 52install:
44 - 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
45 56
46before_script: 57before_script:
47 - 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..e2ff71fd 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
diff --git a/Dockerfile.armhf b/Dockerfile.armhf
index b75663bb..5bbf6680 100644
--- a/Dockerfile.armhf
+++ b/Dockerfile.armhf
@@ -12,7 +12,7 @@ RUN apk --update --no-cache add py2-pip \
12# - Resolve PHP dependencies with Composer 12# - Resolve PHP dependencies with Composer
13FROM arm32v6/alpine:3.8 as composer 13FROM arm32v6/alpine:3.8 as composer
14COPY --from=docs /usr/src/app/shaarli /app/shaarli 14COPY --from=docs /usr/src/app/shaarli /app/shaarli
15RUN apk --update --no-cache add php7-curl php7-mbstring composer \ 15RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \
16 && cd /app/shaarli \ 16 && cd /app/shaarli \
17 && composer --prefer-dist --no-dev install 17 && composer --prefer-dist --no-dev install
18 18
diff --git a/Makefile b/Makefile
index 286d2c90..0ff6bd3f 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
@@ -80,10 +80,15 @@ locale_test_%:
80 --testsuite language-$(firstword $(subst _, ,$*)) 80 --testsuite language-$(firstword $(subst _, ,$*))
81 81
82all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR 82all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR
83 @$(BIN)/phpcov merge --html coverage coverage 83 @# --The current version is not compatible with PHP 7.2
84 @#$(BIN)/phpcov merge --html coverage coverage
84 @# --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)
85 @#$(BIN)/phpcov merge --text coverage/txt coverage 86 @#$(BIN)/phpcov merge --text coverage/txt coverage
86 87
88### download 3rd-party PHP libraries, including dev dependencies
89composer_dependencies_dev: clean
90 composer install --prefer-dist
91
87## 92##
88# Custom release archive generation 93# Custom release archive generation
89# 94#
@@ -122,7 +127,8 @@ release_tar: composer_dependencies htmldoc translate build_frontend
122### generate a release zip and include 3rd-party dependencies and translations 127### generate a release zip and include 3rd-party dependencies and translations
123release_zip: composer_dependencies htmldoc translate build_frontend 128release_zip: composer_dependencies htmldoc translate build_frontend
124 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD 129 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
125 mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor} 130 mkdir -p $(ARCHIVE_PREFIX)/doc
131 mkdir -p $(ARCHIVE_PREFIX)/vendor
126 rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/ 132 rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
127 zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)doc/ 133 zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)doc/
128 rsync -a vendor/ $(ARCHIVE_PREFIX)vendor/ 134 rsync -a vendor/ $(ARCHIVE_PREFIX)vendor/
@@ -154,6 +160,7 @@ phpdoc: clean
154htmldoc: 160htmldoc:
155 python3 -m venv venv/ 161 python3 -m venv venv/
156 bash -c 'source venv/bin/activate; \ 162 bash -c 'source venv/bin/activate; \
163 pip install wheel; \
157 pip install mkdocs; \ 164 pip install mkdocs; \
158 mkdocs build --clean' 165 mkdocs build --clean'
159 find doc/html/ -type f -exec chmod a-x '{}' \; 166 find doc/html/ -type f -exec chmod a-x '{}' \;
@@ -171,4 +178,4 @@ eslint:
171 178
172### Run CSSLint check against Shaarli's SCSS files 179### Run CSSLint check against Shaarli's SCSS files
173sasslint: 180sasslint:
174 @yarn run sass-lint -c .dev/.sasslintrc 'assets/default/scss/*.scss' -v -q 181 @yarn run stylelint --config .dev/.stylelintrc.js 'assets/default/scss/*.scss'
diff --git a/README.md b/README.md
index 6c841c63..4fb0bfe0 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._
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.10.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4)
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.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.0) 12[![](https://img.shields.io/badge/latest-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1)
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.11.x-blue.svg)](https://github.com/shaarli/Shaarli)
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php
index 7fe3cb32..3aa21829 100644
--- a/application/ApplicationUtils.php
+++ b/application/ApplicationUtils.php
@@ -150,6 +150,8 @@ class ApplicationUtils
150 * @param string $minVersion minimum PHP required version 150 * @param string $minVersion minimum PHP required version
151 * @param string $curVersion current PHP version (use PHP_VERSION) 151 * @param string $curVersion current PHP version (use PHP_VERSION)
152 * 152 *
153 * @return bool true on success
154 *
153 * @throws Exception the PHP version is not supported 155 * @throws Exception the PHP version is not supported
154 */ 156 */
155 public static function checkPHPVersion($minVersion, $curVersion) 157 public static function checkPHPVersion($minVersion, $curVersion)
@@ -163,6 +165,7 @@ class ApplicationUtils
163 ); 165 );
164 throw new Exception(sprintf($msg, $minVersion)); 166 throw new Exception(sprintf($msg, $minVersion));
165 } 167 }
168 return true;
166 } 169 }
167 170
168 /** 171 /**
diff --git a/application/History.php b/application/History.php
index a5846652..4fd2f294 100644
--- a/application/History.php
+++ b/application/History.php
@@ -3,6 +3,7 @@ namespace Shaarli;
3 3
4use DateTime; 4use DateTime;
5use Exception; 5use Exception;
6use Shaarli\Bookmark\Bookmark;
6 7
7/** 8/**
8 * Class History 9 * Class History
@@ -20,7 +21,7 @@ use Exception;
20 * - UPDATED: link updated 21 * - UPDATED: link updated
21 * - DELETED: link deleted 22 * - DELETED: link deleted
22 * - SETTINGS: the settings have been updated through the UI. 23 * - SETTINGS: the settings have been updated through the UI.
23 * - IMPORT: bulk links import 24 * - IMPORT: bulk bookmarks import
24 * 25 *
25 * Note: new events are put at the beginning of the file and history array. 26 * Note: new events are put at the beginning of the file and history array.
26 */ 27 */
@@ -96,31 +97,31 @@ class History
96 /** 97 /**
97 * Add Event: new link. 98 * Add Event: new link.
98 * 99 *
99 * @param array $link Link data. 100 * @param Bookmark $link Link data.
100 */ 101 */
101 public function addLink($link) 102 public function addLink($link)
102 { 103 {
103 $this->addEvent(self::CREATED, $link['id']); 104 $this->addEvent(self::CREATED, $link->getId());
104 } 105 }
105 106
106 /** 107 /**
107 * Add Event: update existing link. 108 * Add Event: update existing link.
108 * 109 *
109 * @param array $link Link data. 110 * @param Bookmark $link Link data.
110 */ 111 */
111 public function updateLink($link) 112 public function updateLink($link)
112 { 113 {
113 $this->addEvent(self::UPDATED, $link['id']); 114 $this->addEvent(self::UPDATED, $link->getId());
114 } 115 }
115 116
116 /** 117 /**
117 * Add Event: delete existing link. 118 * Add Event: delete existing link.
118 * 119 *
119 * @param array $link Link data. 120 * @param Bookmark $link Link data.
120 */ 121 */
121 public function deleteLink($link) 122 public function deleteLink($link)
122 { 123 {
123 $this->addEvent(self::DELETED, $link['id']); 124 $this->addEvent(self::DELETED, $link->getId());
124 } 125 }
125 126
126 /** 127 /**
@@ -134,7 +135,7 @@ class History
134 /** 135 /**
135 * Add Event: bulk import. 136 * Add Event: bulk import.
136 * 137 *
137 * Note: we don't store links add/update one by one since it can have a huge impact on performances. 138 * Note: we don't store bookmarks add/update one by one since it can have a huge impact on performances.
138 */ 139 */
139 public function importLinks() 140 public function importLinks()
140 { 141 {
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 d5f5ac28..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/**
@@ -27,6 +26,7 @@ class Thumbnailer
27 'instagram.com', 26 'instagram.com',
28 'pinterest.com', 27 'pinterest.com',
29 'pinterest.fr', 28 'pinterest.fr',
29 'soundcloud.com',
30 'tumblr.com', 30 'tumblr.com',
31 'deviantart.com', 31 'deviantart.com',
32 ]; 32 ];
@@ -89,7 +89,7 @@ class Thumbnailer
89 89
90 try { 90 try {
91 return $this->wt->thumbnail($url); 91 return $this->wt->thumbnail($url);
92 } catch (WebThumbnailerException $e) { 92 } catch (\Throwable $e) {
93 // Exceptions are only thrown in debug mode. 93 // Exceptions are only thrown in debug mode.
94 error_log(get_class($e) . ': ' . $e->getMessage()); 94 error_log(get_class($e) . ': ' . $e->getMessage());
95 } 95 }
diff --git a/application/Utils.php b/application/Utils.php
index 925e1a22..bcfda65c 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -87,18 +87,22 @@ function endsWith($haystack, $needle, $case = true)
87 * 87 *
88 * @param mixed $input Data to escape: a single string or an array of strings. 88 * @param mixed $input Data to escape: a single string or an array of strings.
89 * 89 *
90 * @return string escaped. 90 * @return string|array escaped.
91 */ 91 */
92function escape($input) 92function escape($input)
93{ 93{
94 if (is_bool($input)) { 94 if (null === $input) {
95 return null;
96 }
97
98 if (is_bool($input) || is_int($input) || is_float($input) || $input instanceof DateTimeInterface) {
95 return $input; 99 return $input;
96 } 100 }
97 101
98 if (is_array($input)) { 102 if (is_array($input)) {
99 $out = array(); 103 $out = array();
100 foreach ($input as $key => $value) { 104 foreach ($input as $key => $value) {
101 $out[$key] = escape($value); 105 $out[escape($key)] = escape($value);
102 } 106 }
103 return $out; 107 return $out;
104 } 108 }
@@ -159,10 +163,10 @@ function checkDateFormat($format, $string)
159 */ 163 */
160function generateLocation($referer, $host, $loopTerms = array()) 164function generateLocation($referer, $host, $loopTerms = array())
161{ 165{
162 $finalReferer = '?'; 166 $finalReferer = './?';
163 167
164 // No referer if it contains any value in $loopCriteria. 168 // No referer if it contains any value in $loopCriteria.
165 foreach ($loopTerms as $value) { 169 foreach (array_filter($loopTerms) as $value) {
166 if (strpos($referer, $value) !== false) { 170 if (strpos($referer, $value) !== false) {
167 return $finalReferer; 171 return $finalReferer;
168 } 172 }
@@ -294,15 +298,15 @@ function normalize_spaces($string)
294 * Requires php-intl to display international datetimes, 298 * Requires php-intl to display international datetimes,
295 * otherwise default format '%c' will be returned. 299 * otherwise default format '%c' will be returned.
296 * 300 *
297 * @param DateTime $date to format. 301 * @param DateTimeInterface $date to format.
298 * @param bool $time Displays time if true. 302 * @param bool $time Displays time if true.
299 * @param bool $intl Use international format if true. 303 * @param bool $intl Use international format if true.
300 * 304 *
301 * @return bool|string Formatted date, or false if the input is invalid. 305 * @return bool|string Formatted date, or false if the input is invalid.
302 */ 306 */
303function format_date($date, $time = true, $intl = true) 307function format_date($date, $time = true, $intl = true)
304{ 308{
305 if (! $date instanceof DateTime) { 309 if (! $date instanceof DateTimeInterface) {
306 return false; 310 return false;
307 } 311 }
308 312
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index 2d55bda6..f5b53b01 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -3,6 +3,7 @@ namespace Shaarli\Api;
3 3
4use Shaarli\Api\Exceptions\ApiAuthorizationException; 4use Shaarli\Api\Exceptions\ApiAuthorizationException;
5use Shaarli\Api\Exceptions\ApiException; 5use Shaarli\Api\Exceptions\ApiException;
6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
7use Slim\Container; 8use Slim\Container;
8use Slim\Http\Request; 9use Slim\Http\Request;
@@ -70,7 +71,14 @@ class ApiMiddleware
70 $response = $e->getApiResponse(); 71 $response = $e->getApiResponse();
71 } 72 }
72 73
73 return $response; 74 return $response
75 ->withHeader('Access-Control-Allow-Origin', '*')
76 ->withHeader(
77 'Access-Control-Allow-Headers',
78 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
79 )
80 ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
81 ;
74 } 82 }
75 83
76 /** 84 /**
@@ -99,7 +107,9 @@ class ApiMiddleware
99 */ 107 */
100 protected function checkToken($request) 108 protected function checkToken($request)
101 { 109 {
102 if (! $request->hasHeader('Authorization')) { 110 if (!$request->hasHeader('Authorization')
111 && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])
112 ) {
103 throw new ApiAuthorizationException('JWT token not provided'); 113 throw new ApiAuthorizationException('JWT token not provided');
104 } 114 }
105 115
@@ -107,7 +117,11 @@ class ApiMiddleware
107 throw new ApiAuthorizationException('Token secret must be set in Shaarli\'s administration'); 117 throw new ApiAuthorizationException('Token secret must be set in Shaarli\'s administration');
108 } 118 }
109 119
110 $authorization = $request->getHeaderLine('Authorization'); 120 if (isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])) {
121 $authorization = $this->container->environment['REDIRECT_HTTP_AUTHORIZATION'];
122 } else {
123 $authorization = $request->getHeaderLine('Authorization');
124 }
111 125
112 if (! preg_match('/^Bearer (.*)/i', $authorization, $matches)) { 126 if (! preg_match('/^Bearer (.*)/i', $authorization, $matches)) {
113 throw new ApiAuthorizationException('Invalid JWT header'); 127 throw new ApiAuthorizationException('Invalid JWT header');
@@ -117,7 +131,7 @@ class ApiMiddleware
117 } 131 }
118 132
119 /** 133 /**
120 * Instantiate a new LinkDB including private links, 134 * Instantiate a new LinkDB including private bookmarks,
121 * and load in the Slim container. 135 * and load in the Slim container.
122 * 136 *
123 * FIXME! LinkDB could use a refactoring to avoid this trick. 137 * FIXME! LinkDB could use a refactoring to avoid this trick.
@@ -126,10 +140,10 @@ class ApiMiddleware
126 */ 140 */
127 protected function setLinkDb($conf) 141 protected function setLinkDb($conf)
128 { 142 {
129 $linkDb = new \Shaarli\Bookmark\LinkDB( 143 $linkDb = new BookmarkFileService(
130 $conf->get('resource.datastore'), 144 $conf,
131 true, 145 $this->container->get('history'),
132 $conf->get('privacy.hide_public_links') 146 true
133 ); 147 );
134 $this->container['db'] = $linkDb; 148 $this->container['db'] = $linkDb;
135 } 149 }
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index 1e3ac02e..faebb8f5 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -2,6 +2,7 @@
2namespace Shaarli\Api; 2namespace Shaarli\Api;
3 3
4use Shaarli\Api\Exceptions\ApiAuthorizationException; 4use Shaarli\Api\Exceptions\ApiAuthorizationException;
5use Shaarli\Bookmark\Bookmark;
5use Shaarli\Http\Base64Url; 6use Shaarli\Http\Base64Url;
6 7
7/** 8/**
@@ -15,6 +16,8 @@ class ApiUtils
15 * @param string $token JWT token extracted from the headers. 16 * @param string $token JWT token extracted from the headers.
16 * @param string $secret API secret set in the settings. 17 * @param string $secret API secret set in the settings.
17 * 18 *
19 * @return bool true on success
20 *
18 * @throws ApiAuthorizationException the token is not valid. 21 * @throws ApiAuthorizationException the token is not valid.
19 */ 22 */
20 public static function validateJwtToken($token, $secret) 23 public static function validateJwtToken($token, $secret)
@@ -45,33 +48,35 @@ class ApiUtils
45 ) { 48 ) {
46 throw new ApiAuthorizationException('Invalid JWT issued time'); 49 throw new ApiAuthorizationException('Invalid JWT issued time');
47 } 50 }
51
52 return true;
48 } 53 }
49 54
50 /** 55 /**
51 * Format a Link for the REST API. 56 * Format a Link for the REST API.
52 * 57 *
53 * @param array $link Link data read from the datastore. 58 * @param Bookmark $bookmark Bookmark data read from the datastore.
54 * @param string $indexUrl Shaarli's index URL (used for relative URL). 59 * @param string $indexUrl Shaarli's index URL (used for relative URL).
55 * 60 *
56 * @return array Link data formatted for the REST API. 61 * @return array Link data formatted for the REST API.
57 */ 62 */
58 public static function formatLink($link, $indexUrl) 63 public static function formatLink($bookmark, $indexUrl)
59 { 64 {
60 $out['id'] = $link['id']; 65 $out['id'] = $bookmark->getId();
61 // Not an internal link 66 // Not an internal link
62 if (! is_note($link['url'])) { 67 if (! $bookmark->isNote()) {
63 $out['url'] = $link['url']; 68 $out['url'] = $bookmark->getUrl();
64 } else { 69 } else {
65 $out['url'] = $indexUrl . $link['url']; 70 $out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/');
66 } 71 }
67 $out['shorturl'] = $link['shorturl']; 72 $out['shorturl'] = $bookmark->getShortUrl();
68 $out['title'] = $link['title']; 73 $out['title'] = $bookmark->getTitle();
69 $out['description'] = $link['description']; 74 $out['description'] = $bookmark->getDescription();
70 $out['tags'] = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY); 75 $out['tags'] = $bookmark->getTags();
71 $out['private'] = $link['private'] == true; 76 $out['private'] = $bookmark->isPrivate();
72 $out['created'] = $link['created']->format(\DateTime::ATOM); 77 $out['created'] = $bookmark->getCreated()->format(\DateTime::ATOM);
73 if (! empty($link['updated'])) { 78 if (! empty($bookmark->getUpdated())) {
74 $out['updated'] = $link['updated']->format(\DateTime::ATOM); 79 $out['updated'] = $bookmark->getUpdated()->format(\DateTime::ATOM);
75 } else { 80 } else {
76 $out['updated'] = ''; 81 $out['updated'] = '';
77 } 82 }
@@ -79,7 +84,7 @@ class ApiUtils
79 } 84 }
80 85
81 /** 86 /**
82 * Convert a link given through a request, to a valid link for LinkDB. 87 * Convert a link given through a request, to a valid Bookmark for the datastore.
83 * 88 *
84 * 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.
85 * 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.
@@ -87,50 +92,42 @@ class ApiUtils
87 * @param array $input Request Link. 92 * @param array $input Request Link.
88 * @param bool $defaultPrivate Request Link. 93 * @param bool $defaultPrivate Request Link.
89 * 94 *
90 * @return array Formatted link. 95 * @return Bookmark instance.
91 */ 96 */
92 public static function buildLinkFromRequest($input, $defaultPrivate) 97 public static function buildLinkFromRequest($input, $defaultPrivate)
93 { 98 {
94 $input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : ''; 99 $bookmark = new Bookmark();
100 $url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
95 if (isset($input['private'])) { 101 if (isset($input['private'])) {
96 $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN); 102 $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
97 } else { 103 } else {
98 $private = $defaultPrivate; 104 $private = $defaultPrivate;
99 } 105 }
100 106
101 $link = [ 107 $bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
102 'title' => ! empty($input['title']) ? $input['title'] : $input['url'], 108 $bookmark->setUrl($url);
103 'url' => $input['url'], 109 $bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
104 'description' => ! empty($input['description']) ? $input['description'] : '', 110 $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
105 'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '', 111 $bookmark->setPrivate($private);
106 'private' => $private, 112
107 'created' => new \DateTime(), 113 return $bookmark;
108 ];
109 return $link;
110 } 114 }
111 115
112 /** 116 /**
113 * Update link fields using an updated link object. 117 * Update link fields using an updated link object.
114 * 118 *
115 * @param array $oldLink data 119 * @param Bookmark $oldLink data
116 * @param array $newLink data 120 * @param Bookmark $newLink data
117 * 121 *
118 * @return array $oldLink updated with $newLink values 122 * @return Bookmark $oldLink updated with $newLink values
119 */ 123 */
120 public static function updateLink($oldLink, $newLink) 124 public static function updateLink($oldLink, $newLink)
121 { 125 {
122 foreach (['title', 'url', 'description', 'tags', 'private'] as $field) { 126 $oldLink->setTitle($newLink->getTitle());
123 $oldLink[$field] = $newLink[$field]; 127 $oldLink->setUrl($newLink->getUrl());
124 } 128 $oldLink->setDescription($newLink->getDescription());
125 $oldLink['updated'] = new \DateTime(); 129 $oldLink->setTags($newLink->getTags());
126 130 $oldLink->setPrivate($newLink->isPrivate());
127 if (empty($oldLink['url'])) {
128 $oldLink['url'] = '?' . $oldLink['shorturl'];
129 }
130
131 if (empty($oldLink['title'])) {
132 $oldLink['title'] = $oldLink['url'];
133 }
134 131
135 return $oldLink; 132 return $oldLink;
136 } 133 }
@@ -139,7 +136,7 @@ class ApiUtils
139 * Format a Tag for the REST API. 136 * Format a Tag for the REST API.
140 * 137 *
141 * @param string $tag Tag name 138 * @param string $tag Tag name
142 * @param int $occurrences Number of links using this tag 139 * @param int $occurrences Number of bookmarks using this tag
143 * 140 *
144 * @return array Link data formatted for the REST API. 141 * @return array Link data formatted for the REST API.
145 */ 142 */
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php
index a6e7cbab..c4b3d0c3 100644
--- a/application/api/controllers/ApiController.php
+++ b/application/api/controllers/ApiController.php
@@ -2,7 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\LinkDB; 5use Shaarli\Bookmark\BookmarkServiceInterface;
6use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Slim\Container; 7use Slim\Container;
8 8
@@ -26,9 +26,9 @@ abstract class ApiController
26 protected $conf; 26 protected $conf;
27 27
28 /** 28 /**
29 * @var LinkDB 29 * @var BookmarkServiceInterface
30 */ 30 */
31 protected $linkDb; 31 protected $bookmarkService;
32 32
33 /** 33 /**
34 * @var HistoryController 34 * @var HistoryController
@@ -51,7 +51,7 @@ abstract class ApiController
51 { 51 {
52 $this->ci = $ci; 52 $this->ci = $ci;
53 $this->conf = $ci->get('conf'); 53 $this->conf = $ci->get('conf');
54 $this->linkDb = $ci->get('db'); 54 $this->bookmarkService = $ci->get('db');
55 $this->history = $ci->get('history'); 55 $this->history = $ci->get('history');
56 if ($this->conf->get('dev.debug', false)) { 56 if ($this->conf->get('dev.debug', false)) {
57 $this->jsonStyle = JSON_PRETTY_PRINT; 57 $this->jsonStyle = JSON_PRETTY_PRINT;
diff --git a/application/api/controllers/HistoryController.php b/application/api/controllers/HistoryController.php
index 9afcfa26..505647a9 100644
--- a/application/api/controllers/HistoryController.php
+++ b/application/api/controllers/HistoryController.php
@@ -41,7 +41,7 @@ class HistoryController extends ApiController
41 throw new ApiBadParametersException('Invalid offset'); 41 throw new ApiBadParametersException('Invalid offset');
42 } 42 }
43 43
44 // limit parameter is either a number of links or 'all' for everything. 44 // limit parameter is either a number of bookmarks or 'all' for everything.
45 $limit = $request->getParam('limit'); 45 $limit = $request->getParam('limit');
46 if (empty($limit)) { 46 if (empty($limit)) {
47 $limit = count($history); 47 $limit = count($history);
diff --git a/application/api/controllers/Info.php b/application/api/controllers/Info.php
index f37dcae5..12f6b2f0 100644
--- a/application/api/controllers/Info.php
+++ b/application/api/controllers/Info.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\BookmarkFilter;
5use Slim\Http\Request; 6use Slim\Http\Request;
6use Slim\Http\Response; 7use Slim\Http\Response;
7 8
@@ -26,8 +27,8 @@ class Info extends ApiController
26 public function getInfo($request, $response) 27 public function getInfo($request, $response)
27 { 28 {
28 $info = [ 29 $info = [
29 'global_counter' => count($this->linkDb), 30 'global_counter' => $this->bookmarkService->count(),
30 'private_counter' => count_private($this->linkDb), 31 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE),
31 'settings' => array( 32 'settings' => array(
32 'title' => $this->conf->get('general.title', 'Shaarli'), 33 'title' => $this->conf->get('general.title', 'Shaarli'),
33 'header_link' => $this->conf->get('general.header_link', '?'), 34 'header_link' => $this->conf->get('general.header_link', '?'),
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index ffcfd4c7..29247950 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -11,7 +11,7 @@ use Slim\Http\Response;
11/** 11/**
12 * Class Links 12 * Class Links
13 * 13 *
14 * REST API Controller: all services related to links collection. 14 * REST API Controller: all services related to bookmarks collection.
15 * 15 *
16 * @package Api\Controllers 16 * @package Api\Controllers
17 * @see http://shaarli.github.io/api-documentation/#links-links-collection 17 * @see http://shaarli.github.io/api-documentation/#links-links-collection
@@ -19,12 +19,12 @@ use Slim\Http\Response;
19class Links extends ApiController 19class Links extends ApiController
20{ 20{
21 /** 21 /**
22 * @var int Number of links returned if no limit is provided. 22 * @var int Number of bookmarks returned if no limit is provided.
23 */ 23 */
24 public static $DEFAULT_LIMIT = 20; 24 public static $DEFAULT_LIMIT = 20;
25 25
26 /** 26 /**
27 * Retrieve a list of links, allowing different filters. 27 * Retrieve a list of bookmarks, allowing different filters.
28 * 28 *
29 * @param Request $request Slim request. 29 * @param Request $request Slim request.
30 * @param Response $response Slim response. 30 * @param Response $response Slim response.
@@ -36,33 +36,32 @@ class Links extends ApiController
36 public function getLinks($request, $response) 36 public function getLinks($request, $response)
37 { 37 {
38 $private = $request->getParam('visibility'); 38 $private = $request->getParam('visibility');
39 $links = $this->linkDb->filterSearch( 39 $bookmarks = $this->bookmarkService->search(
40 [ 40 [
41 'searchtags' => $request->getParam('searchtags', ''), 41 'searchtags' => $request->getParam('searchtags', ''),
42 'searchterm' => $request->getParam('searchterm', ''), 42 'searchterm' => $request->getParam('searchterm', ''),
43 ], 43 ],
44 false,
45 $private 44 $private
46 ); 45 );
47 46
48 // Return links from the {offset}th link, starting from 0. 47 // Return bookmarks from the {offset}th link, starting from 0.
49 $offset = $request->getParam('offset'); 48 $offset = $request->getParam('offset');
50 if (! empty($offset) && ! ctype_digit($offset)) { 49 if (! empty($offset) && ! ctype_digit($offset)) {
51 throw new ApiBadParametersException('Invalid offset'); 50 throw new ApiBadParametersException('Invalid offset');
52 } 51 }
53 $offset = ! empty($offset) ? intval($offset) : 0; 52 $offset = ! empty($offset) ? intval($offset) : 0;
54 if ($offset > count($links)) { 53 if ($offset > count($bookmarks)) {
55 return $response->withJson([], 200, $this->jsonStyle); 54 return $response->withJson([], 200, $this->jsonStyle);
56 } 55 }
57 56
58 // limit parameter is either a number of links or 'all' for everything. 57 // limit parameter is either a number of bookmarks or 'all' for everything.
59 $limit = $request->getParam('limit'); 58 $limit = $request->getParam('limit');
60 if (empty($limit)) { 59 if (empty($limit)) {
61 $limit = self::$DEFAULT_LIMIT; 60 $limit = self::$DEFAULT_LIMIT;
62 } elseif (ctype_digit($limit)) { 61 } elseif (ctype_digit($limit)) {
63 $limit = intval($limit); 62 $limit = intval($limit);
64 } elseif ($limit === 'all') { 63 } elseif ($limit === 'all') {
65 $limit = count($links); 64 $limit = count($bookmarks);
66 } else { 65 } else {
67 throw new ApiBadParametersException('Invalid limit'); 66 throw new ApiBadParametersException('Invalid limit');
68 } 67 }
@@ -72,12 +71,12 @@ class Links extends ApiController
72 71
73 $out = []; 72 $out = [];
74 $index = 0; 73 $index = 0;
75 foreach ($links as $link) { 74 foreach ($bookmarks as $bookmark) {
76 if (count($out) >= $limit) { 75 if (count($out) >= $limit) {
77 break; 76 break;
78 } 77 }
79 if ($index++ >= $offset) { 78 if ($index++ >= $offset) {
80 $out[] = ApiUtils::formatLink($link, $indexUrl); 79 $out[] = ApiUtils::formatLink($bookmark, $indexUrl);
81 } 80 }
82 } 81 }
83 82
@@ -97,11 +96,11 @@ class Links extends ApiController
97 */ 96 */
98 public function getLink($request, $response, $args) 97 public function getLink($request, $response, $args)
99 { 98 {
100 if (!isset($this->linkDb[$args['id']])) { 99 if (!$this->bookmarkService->exists($args['id'])) {
101 throw new ApiLinkNotFoundException(); 100 throw new ApiLinkNotFoundException();
102 } 101 }
103 $index = index_url($this->ci['environment']); 102 $index = index_url($this->ci['environment']);
104 $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index); 103 $out = ApiUtils::formatLink($this->bookmarkService->get($args['id']), $index);
105 104
106 return $response->withJson($out, 200, $this->jsonStyle); 105 return $response->withJson($out, 200, $this->jsonStyle);
107 } 106 }
@@ -117,9 +116,11 @@ class Links extends ApiController
117 public function postLink($request, $response) 116 public function postLink($request, $response)
118 { 117 {
119 $data = $request->getParsedBody(); 118 $data = $request->getParsedBody();
120 $link = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); 119 $bookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
121 // duplicate by URL, return 409 Conflict 120 // duplicate by URL, return 409 Conflict
122 if (! empty($link['url']) && ! empty($dup = $this->linkDb->getLinkFromUrl($link['url']))) { 121 if (! empty($bookmark->getUrl())
122 && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl()))
123 ) {
123 return $response->withJson( 124 return $response->withJson(
124 ApiUtils::formatLink($dup, index_url($this->ci['environment'])), 125 ApiUtils::formatLink($dup, index_url($this->ci['environment'])),
125 409, 126 409,
@@ -127,23 +128,9 @@ class Links extends ApiController
127 ); 128 );
128 } 129 }
129 130
130 $link['id'] = $this->linkDb->getNextId(); 131 $this->bookmarkService->add($bookmark);
131 $link['shorturl'] = link_small_hash($link['created'], $link['id']); 132 $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
132 133 $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]);
133 // note: general relative URL
134 if (empty($link['url'])) {
135 $link['url'] = '?' . $link['shorturl'];
136 }
137
138 if (empty($link['title'])) {
139 $link['title'] = $link['url'];
140 }
141
142 $this->linkDb[$link['id']] = $link;
143 $this->linkDb->save($this->conf->get('resource.page_cache'));
144 $this->history->addLink($link);
145 $out = ApiUtils::formatLink($link, index_url($this->ci['environment']));
146 $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $link['id']]);
147 return $response->withAddedHeader('Location', $redirect) 134 return $response->withAddedHeader('Location', $redirect)
148 ->withJson($out, 201, $this->jsonStyle); 135 ->withJson($out, 201, $this->jsonStyle);
149 } 136 }
@@ -161,18 +148,18 @@ class Links extends ApiController
161 */ 148 */
162 public function putLink($request, $response, $args) 149 public function putLink($request, $response, $args)
163 { 150 {
164 if (! isset($this->linkDb[$args['id']])) { 151 if (! $this->bookmarkService->exists($args['id'])) {
165 throw new ApiLinkNotFoundException(); 152 throw new ApiLinkNotFoundException();
166 } 153 }
167 154
168 $index = index_url($this->ci['environment']); 155 $index = index_url($this->ci['environment']);
169 $data = $request->getParsedBody(); 156 $data = $request->getParsedBody();
170 157
171 $requestLink = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); 158 $requestBookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
172 // duplicate URL on a different link, return 409 Conflict 159 // duplicate URL on a different link, return 409 Conflict
173 if (! empty($requestLink['url']) 160 if (! empty($requestBookmark->getUrl())
174 && ! empty($dup = $this->linkDb->getLinkFromUrl($requestLink['url'])) 161 && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl()))
175 && $dup['id'] != $args['id'] 162 && $dup->getId() != $args['id']
176 ) { 163 ) {
177 return $response->withJson( 164 return $response->withJson(
178 ApiUtils::formatLink($dup, $index), 165 ApiUtils::formatLink($dup, $index),
@@ -181,13 +168,11 @@ class Links extends ApiController
181 ); 168 );
182 } 169 }
183 170
184 $responseLink = $this->linkDb[$args['id']]; 171 $responseBookmark = $this->bookmarkService->get($args['id']);
185 $responseLink = ApiUtils::updateLink($responseLink, $requestLink); 172 $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark);
186 $this->linkDb[$responseLink['id']] = $responseLink; 173 $this->bookmarkService->set($responseBookmark);
187 $this->linkDb->save($this->conf->get('resource.page_cache'));
188 $this->history->updateLink($responseLink);
189 174
190 $out = ApiUtils::formatLink($responseLink, $index); 175 $out = ApiUtils::formatLink($responseBookmark, $index);
191 return $response->withJson($out, 200, $this->jsonStyle); 176 return $response->withJson($out, 200, $this->jsonStyle);
192 } 177 }
193 178
@@ -204,13 +189,11 @@ class Links extends ApiController
204 */ 189 */
205 public function deleteLink($request, $response, $args) 190 public function deleteLink($request, $response, $args)
206 { 191 {
207 if (! isset($this->linkDb[$args['id']])) { 192 if (! $this->bookmarkService->exists($args['id'])) {
208 throw new ApiLinkNotFoundException(); 193 throw new ApiLinkNotFoundException();
209 } 194 }
210 $link = $this->linkDb[$args['id']]; 195 $bookmark = $this->bookmarkService->get($args['id']);
211 unset($this->linkDb[(int) $args['id']]); 196 $this->bookmarkService->remove($bookmark);
212 $this->linkDb->save($this->conf->get('resource.page_cache'));
213 $this->history->deleteLink($link);
214 197
215 return $response->withStatus(204); 198 return $response->withStatus(204);
216 } 199 }
diff --git a/application/api/controllers/Tags.php b/application/api/controllers/Tags.php
index 82f3ef74..e60e00a7 100644
--- a/application/api/controllers/Tags.php
+++ b/application/api/controllers/Tags.php
@@ -5,6 +5,7 @@ namespace Shaarli\Api\Controllers;
5use Shaarli\Api\ApiUtils; 5use Shaarli\Api\ApiUtils;
6use Shaarli\Api\Exceptions\ApiBadParametersException; 6use Shaarli\Api\Exceptions\ApiBadParametersException;
7use Shaarli\Api\Exceptions\ApiTagNotFoundException; 7use Shaarli\Api\Exceptions\ApiTagNotFoundException;
8use Shaarli\Bookmark\BookmarkFilter;
8use Slim\Http\Request; 9use Slim\Http\Request;
9use Slim\Http\Response; 10use Slim\Http\Response;
10 11
@@ -18,7 +19,7 @@ use Slim\Http\Response;
18class Tags extends ApiController 19class Tags extends ApiController
19{ 20{
20 /** 21 /**
21 * @var int Number of links returned if no limit is provided. 22 * @var int Number of bookmarks returned if no limit is provided.
22 */ 23 */
23 public static $DEFAULT_LIMIT = 'all'; 24 public static $DEFAULT_LIMIT = 'all';
24 25
@@ -35,7 +36,7 @@ class Tags extends ApiController
35 public function getTags($request, $response) 36 public function getTags($request, $response)
36 { 37 {
37 $visibility = $request->getParam('visibility'); 38 $visibility = $request->getParam('visibility');
38 $tags = $this->linkDb->linksCountPerTag([], $visibility); 39 $tags = $this->bookmarkService->bookmarksCountPerTag([], $visibility);
39 40
40 // Return tags from the {offset}th tag, starting from 0. 41 // Return tags from the {offset}th tag, starting from 0.
41 $offset = $request->getParam('offset'); 42 $offset = $request->getParam('offset');
@@ -47,7 +48,7 @@ class Tags extends ApiController
47 return $response->withJson([], 200, $this->jsonStyle); 48 return $response->withJson([], 200, $this->jsonStyle);
48 } 49 }
49 50
50 // limit parameter is either a number of links or 'all' for everything. 51 // limit parameter is either a number of bookmarks or 'all' for everything.
51 $limit = $request->getParam('limit'); 52 $limit = $request->getParam('limit');
52 if (empty($limit)) { 53 if (empty($limit)) {
53 $limit = self::$DEFAULT_LIMIT; 54 $limit = self::$DEFAULT_LIMIT;
@@ -87,7 +88,7 @@ class Tags extends ApiController
87 */ 88 */
88 public function getTag($request, $response, $args) 89 public function getTag($request, $response, $args)
89 { 90 {
90 $tags = $this->linkDb->linksCountPerTag(); 91 $tags = $this->bookmarkService->bookmarksCountPerTag();
91 if (!isset($tags[$args['tagName']])) { 92 if (!isset($tags[$args['tagName']])) {
92 throw new ApiTagNotFoundException(); 93 throw new ApiTagNotFoundException();
93 } 94 }
@@ -111,7 +112,7 @@ class Tags extends ApiController
111 */ 112 */
112 public function putTag($request, $response, $args) 113 public function putTag($request, $response, $args)
113 { 114 {
114 $tags = $this->linkDb->linksCountPerTag(); 115 $tags = $this->bookmarkService->bookmarksCountPerTag();
115 if (! isset($tags[$args['tagName']])) { 116 if (! isset($tags[$args['tagName']])) {
116 throw new ApiTagNotFoundException(); 117 throw new ApiTagNotFoundException();
117 } 118 }
@@ -121,13 +122,19 @@ class Tags extends ApiController
121 throw new ApiBadParametersException('New tag name is required in the request body'); 122 throw new ApiBadParametersException('New tag name is required in the request body');
122 } 123 }
123 124
124 $updated = $this->linkDb->renameTag($args['tagName'], $data['name']); 125 $bookmarks = $this->bookmarkService->search(
125 $this->linkDb->save($this->conf->get('resource.page_cache')); 126 ['searchtags' => $args['tagName']],
126 foreach ($updated as $link) { 127 BookmarkFilter::$ALL,
127 $this->history->updateLink($link); 128 true
129 );
130 foreach ($bookmarks as $bookmark) {
131 $bookmark->renameTag($args['tagName'], $data['name']);
132 $this->bookmarkService->set($bookmark, false);
133 $this->history->updateLink($bookmark);
128 } 134 }
135 $this->bookmarkService->save();
129 136
130 $tags = $this->linkDb->linksCountPerTag(); 137 $tags = $this->bookmarkService->bookmarksCountPerTag();
131 $out = ApiUtils::formatTag($data['name'], $tags[$data['name']]); 138 $out = ApiUtils::formatTag($data['name'], $tags[$data['name']]);
132 return $response->withJson($out, 200, $this->jsonStyle); 139 return $response->withJson($out, 200, $this->jsonStyle);
133 } 140 }
@@ -145,15 +152,22 @@ class Tags extends ApiController
145 */ 152 */
146 public function deleteTag($request, $response, $args) 153 public function deleteTag($request, $response, $args)
147 { 154 {
148 $tags = $this->linkDb->linksCountPerTag(); 155 $tags = $this->bookmarkService->bookmarksCountPerTag();
149 if (! isset($tags[$args['tagName']])) { 156 if (! isset($tags[$args['tagName']])) {
150 throw new ApiTagNotFoundException(); 157 throw new ApiTagNotFoundException();
151 } 158 }
152 $updated = $this->linkDb->renameTag($args['tagName'], null); 159
153 $this->linkDb->save($this->conf->get('resource.page_cache')); 160 $bookmarks = $this->bookmarkService->search(
154 foreach ($updated as $link) { 161 ['searchtags' => $args['tagName']],
155 $this->history->updateLink($link); 162 BookmarkFilter::$ALL,
163 true
164 );
165 foreach ($bookmarks as $bookmark) {
166 $bookmark->deleteTag($args['tagName']);
167 $this->bookmarkService->set($bookmark, false);
168 $this->history->updateLink($bookmark);
156 } 169 }
170 $this->bookmarkService->save();
157 171
158 return $response->withStatus(204); 172 return $response->withStatus(204);
159 } 173 }
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php
new file mode 100644
index 00000000..1beb8be2
--- /dev/null
+++ b/application/bookmark/Bookmark.php
@@ -0,0 +1,462 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use DateTime;
6use DateTimeInterface;
7use Shaarli\Bookmark\Exception\InvalidBookmarkException;
8
9/**
10 * Class Bookmark
11 *
12 * This class represent a single Bookmark with all its attributes.
13 * Every bookmark should manipulated using this, before being formatted.
14 *
15 * @package Shaarli\Bookmark
16 */
17class Bookmark
18{
19 /** @var string Date format used in string (former ID format) */
20 const LINK_DATE_FORMAT = 'Ymd_His';
21
22 /** @var int Bookmark ID */
23 protected $id;
24
25 /** @var string Permalink identifier */
26 protected $shortUrl;
27
28 /** @var string Bookmark's URL - $shortUrl prefixed with `?` for notes */
29 protected $url;
30
31 /** @var string Bookmark's title */
32 protected $title;
33
34 /** @var string Raw bookmark's description */
35 protected $description;
36
37 /** @var array List of bookmark's tags */
38 protected $tags;
39
40 /** @var string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */
41 protected $thumbnail;
42
43 /** @var bool Set to true if the bookmark is set as sticky */
44 protected $sticky;
45
46 /** @var DateTimeInterface Creation datetime */
47 protected $created;
48
49 /** @var DateTimeInterface datetime */
50 protected $updated;
51
52 /** @var bool True if the bookmark can only be seen while logged in */
53 protected $private;
54
55 /**
56 * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
57 *
58 * @param array $data
59 *
60 * @return $this
61 */
62 public function fromArray($data)
63 {
64 $this->id = $data['id'];
65 $this->shortUrl = $data['shorturl'];
66 $this->url = $data['url'];
67 $this->title = $data['title'];
68 $this->description = $data['description'];
69 $this->thumbnail = isset($data['thumbnail']) ? $data['thumbnail'] : null;
70 $this->sticky = isset($data['sticky']) ? $data['sticky'] : false;
71 $this->created = $data['created'];
72 if (is_array($data['tags'])) {
73 $this->tags = $data['tags'];
74 } else {
75 $this->tags = preg_split('/\s+/', $data['tags'], -1, PREG_SPLIT_NO_EMPTY);
76 }
77 if (! empty($data['updated'])) {
78 $this->updated = $data['updated'];
79 }
80 $this->private = $data['private'] ? true : false;
81
82 return $this;
83 }
84
85 /**
86 * Make sure that the current instance of Bookmark is valid and can be saved into the data store.
87 * A valid link requires:
88 * - an integer ID
89 * - a short URL (for permalinks)
90 * - a creation date
91 *
92 * This function also initialize optional empty fields:
93 * - the URL with the permalink
94 * - the title with the URL
95 *
96 * @throws InvalidBookmarkException
97 */
98 public function validate()
99 {
100 if ($this->id === null
101 || ! is_int($this->id)
102 || empty($this->shortUrl)
103 || empty($this->created)
104 || ! $this->created instanceof DateTimeInterface
105 ) {
106 throw new InvalidBookmarkException($this);
107 }
108 if (empty($this->url)) {
109 $this->url = '/shaare/'. $this->shortUrl;
110 }
111 if (empty($this->title)) {
112 $this->title = $this->url;
113 }
114 }
115
116 /**
117 * Set the Id.
118 * If they're not already initialized, this function also set:
119 * - created: with the current datetime
120 * - shortUrl: with a generated small hash from the date and the given ID
121 *
122 * @param int $id
123 *
124 * @return Bookmark
125 */
126 public function setId($id)
127 {
128 $this->id = $id;
129 if (empty($this->created)) {
130 $this->created = new DateTime();
131 }
132 if (empty($this->shortUrl)) {
133 $this->shortUrl = link_small_hash($this->created, $this->id);
134 }
135
136 return $this;
137 }
138
139 /**
140 * Get the Id.
141 *
142 * @return int
143 */
144 public function getId()
145 {
146 return $this->id;
147 }
148
149 /**
150 * Get the ShortUrl.
151 *
152 * @return string
153 */
154 public function getShortUrl()
155 {
156 return $this->shortUrl;
157 }
158
159 /**
160 * Get the Url.
161 *
162 * @return string
163 */
164 public function getUrl()
165 {
166 return $this->url;
167 }
168
169 /**
170 * Get the Title.
171 *
172 * @return string
173 */
174 public function getTitle()
175 {
176 return $this->title;
177 }
178
179 /**
180 * Get the Description.
181 *
182 * @return string
183 */
184 public function getDescription()
185 {
186 return ! empty($this->description) ? $this->description : '';
187 }
188
189 /**
190 * Get the Created.
191 *
192 * @return DateTimeInterface
193 */
194 public function getCreated()
195 {
196 return $this->created;
197 }
198
199 /**
200 * Get the Updated.
201 *
202 * @return DateTimeInterface
203 */
204 public function getUpdated()
205 {
206 return $this->updated;
207 }
208
209 /**
210 * Set the ShortUrl.
211 *
212 * @param string $shortUrl
213 *
214 * @return Bookmark
215 */
216 public function setShortUrl($shortUrl)
217 {
218 $this->shortUrl = $shortUrl;
219
220 return $this;
221 }
222
223 /**
224 * Set the Url.
225 *
226 * @param string $url
227 * @param array $allowedProtocols
228 *
229 * @return Bookmark
230 */
231 public function setUrl($url, $allowedProtocols = [])
232 {
233 $url = trim($url);
234 if (! empty($url)) {
235 $url = whitelist_protocols($url, $allowedProtocols);
236 }
237 $this->url = $url;
238
239 return $this;
240 }
241
242 /**
243 * Set the Title.
244 *
245 * @param string $title
246 *
247 * @return Bookmark
248 */
249 public function setTitle($title)
250 {
251 $this->title = trim($title);
252
253 return $this;
254 }
255
256 /**
257 * Set the Description.
258 *
259 * @param string $description
260 *
261 * @return Bookmark
262 */
263 public function setDescription($description)
264 {
265 $this->description = $description;
266
267 return $this;
268 }
269
270 /**
271 * Set the Created.
272 * Note: you shouldn't set this manually except for special cases (like bookmark import)
273 *
274 * @param DateTimeInterface $created
275 *
276 * @return Bookmark
277 */
278 public function setCreated($created)
279 {
280 $this->created = $created;
281
282 return $this;
283 }
284
285 /**
286 * Set the Updated.
287 *
288 * @param DateTimeInterface $updated
289 *
290 * @return Bookmark
291 */
292 public function setUpdated($updated)
293 {
294 $this->updated = $updated;
295
296 return $this;
297 }
298
299 /**
300 * Get the Private.
301 *
302 * @return bool
303 */
304 public function isPrivate()
305 {
306 return $this->private ? true : false;
307 }
308
309 /**
310 * Set the Private.
311 *
312 * @param bool $private
313 *
314 * @return Bookmark
315 */
316 public function setPrivate($private)
317 {
318 $this->private = $private ? true : false;
319
320 return $this;
321 }
322
323 /**
324 * Get the Tags.
325 *
326 * @return array
327 */
328 public function getTags()
329 {
330 return is_array($this->tags) ? $this->tags : [];
331 }
332
333 /**
334 * Set the Tags.
335 *
336 * @param array $tags
337 *
338 * @return Bookmark
339 */
340 public function setTags($tags)
341 {
342 $this->setTagsString(implode(' ', $tags));
343
344 return $this;
345 }
346
347 /**
348 * Get the Thumbnail.
349 *
350 * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found
351 */
352 public function getThumbnail()
353 {
354 return !$this->isNote() ? $this->thumbnail : false;
355 }
356
357 /**
358 * Set the Thumbnail.
359 *
360 * @param string|bool $thumbnail Thumbnail's URL - false if no thumbnail could be found
361 *
362 * @return Bookmark
363 */
364 public function setThumbnail($thumbnail)
365 {
366 $this->thumbnail = $thumbnail;
367
368 return $this;
369 }
370
371 /**
372 * Get the Sticky.
373 *
374 * @return bool
375 */
376 public function isSticky()
377 {
378 return $this->sticky ? true : false;
379 }
380
381 /**
382 * Set the Sticky.
383 *
384 * @param bool $sticky
385 *
386 * @return Bookmark
387 */
388 public function setSticky($sticky)
389 {
390 $this->sticky = $sticky ? true : false;
391
392 return $this;
393 }
394
395 /**
396 * @return string Bookmark's tags as a string, separated by a space
397 */
398 public function getTagsString()
399 {
400 return implode(' ', $this->getTags());
401 }
402
403 /**
404 * @return bool
405 */
406 public function isNote()
407 {
408 // We check empty value to get a valid result if the link has not been saved yet
409 return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
410 }
411
412 /**
413 * Set tags from a string.
414 * Note:
415 * - tags must be separated whether by a space or a comma
416 * - multiple spaces will be removed
417 * - trailing dash in tags will be removed
418 *
419 * @param string $tags
420 *
421 * @return $this
422 */
423 public function setTagsString($tags)
424 {
425 // Remove first '-' char in tags.
426 $tags = preg_replace('/(^| )\-/', '$1', $tags);
427 // Explode all tags separted by spaces or commas
428 $tags = preg_split('/[\s,]+/', $tags);
429 // Remove eventual empty values
430 $tags = array_values(array_filter($tags));
431
432 $this->tags = $tags;
433
434 return $this;
435 }
436
437 /**
438 * Rename a tag in tags list.
439 *
440 * @param string $fromTag
441 * @param string $toTag
442 */
443 public function renameTag($fromTag, $toTag)
444 {
445 if (($pos = array_search($fromTag, $this->tags)) !== false) {
446 $this->tags[$pos] = trim($toTag);
447 }
448 }
449
450 /**
451 * Delete a tag from tags list.
452 *
453 * @param string $tag
454 */
455 public function deleteTag($tag)
456 {
457 if (($pos = array_search($tag, $this->tags)) !== false) {
458 unset($this->tags[$pos]);
459 $this->tags = array_values($this->tags);
460 }
461 }
462}
diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php
new file mode 100644
index 00000000..3bd5eb20
--- /dev/null
+++ b/application/bookmark/BookmarkArray.php
@@ -0,0 +1,260 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Shaarli\Bookmark\Exception\InvalidBookmarkException;
6
7/**
8 * Class BookmarkArray
9 *
10 * Implementing ArrayAccess, this allows us to use the bookmark list
11 * as an array and iterate over it.
12 *
13 * @package Shaarli\Bookmark
14 */
15class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
16{
17 /**
18 * @var Bookmark[]
19 */
20 protected $bookmarks;
21
22 /**
23 * @var array List of all bookmarks IDS mapped with their array offset.
24 * Map: id->offset.
25 */
26 protected $ids;
27
28 /**
29 * @var int Position in the $this->keys array (for the Iterator interface)
30 */
31 protected $position;
32
33 /**
34 * @var array List of offset keys (for the Iterator interface implementation)
35 */
36 protected $keys;
37
38 /**
39 * @var array List of all recorded URLs (key=url, value=bookmark offset)
40 * for fast reserve search (url-->bookmark offset)
41 */
42 protected $urls;
43
44 public function __construct()
45 {
46 $this->ids = [];
47 $this->bookmarks = [];
48 $this->keys = [];
49 $this->urls = [];
50 $this->position = 0;
51 }
52
53 /**
54 * Countable - Counts elements of an object
55 *
56 * @return int Number of bookmarks
57 */
58 public function count()
59 {
60 return count($this->bookmarks);
61 }
62
63 /**
64 * ArrayAccess - Assigns a value to the specified offset
65 *
66 * @param int $offset Bookmark ID
67 * @param Bookmark $value instance
68 *
69 * @throws InvalidBookmarkException
70 */
71 public function offsetSet($offset, $value)
72 {
73 if (! $value instanceof Bookmark
74 || $value->getId() === null || empty($value->getUrl())
75 || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId())
76 || $offset !== null && $offset !== $value->getId()
77 ) {
78 throw new InvalidBookmarkException($value);
79 }
80
81 // If the bookmark exists, we reuse the real offset, otherwise new entry
82 if ($offset !== null) {
83 $existing = $this->getBookmarkOffset($offset);
84 } else {
85 $existing = $this->getBookmarkOffset($value->getId());
86 }
87
88 if ($existing !== null) {
89 $offset = $existing;
90 } else {
91 $offset = count($this->bookmarks);
92 }
93
94 $this->bookmarks[$offset] = $value;
95 $this->urls[$value->getUrl()] = $offset;
96 $this->ids[$value->getId()] = $offset;
97 }
98
99 /**
100 * ArrayAccess - Whether or not an offset exists
101 *
102 * @param int $offset Bookmark ID
103 *
104 * @return bool true if it exists, false otherwise
105 */
106 public function offsetExists($offset)
107 {
108 return array_key_exists($this->getBookmarkOffset($offset), $this->bookmarks);
109 }
110
111 /**
112 * ArrayAccess - Unsets an offset
113 *
114 * @param int $offset Bookmark ID
115 */
116 public function offsetUnset($offset)
117 {
118 $realOffset = $this->getBookmarkOffset($offset);
119 $url = $this->bookmarks[$realOffset]->getUrl();
120 unset($this->urls[$url]);
121 unset($this->ids[$offset]);
122 unset($this->bookmarks[$realOffset]);
123 }
124
125 /**
126 * ArrayAccess - Returns the value at specified offset
127 *
128 * @param int $offset Bookmark ID
129 *
130 * @return Bookmark|null The Bookmark if found, null otherwise
131 */
132 public function offsetGet($offset)
133 {
134 $realOffset = $this->getBookmarkOffset($offset);
135 return isset($this->bookmarks[$realOffset]) ? $this->bookmarks[$realOffset] : null;
136 }
137
138 /**
139 * Iterator - Returns the current element
140 *
141 * @return Bookmark corresponding to the current position
142 */
143 public function current()
144 {
145 return $this[$this->keys[$this->position]];
146 }
147
148 /**
149 * Iterator - Returns the key of the current element
150 *
151 * @return int Bookmark ID corresponding to the current position
152 */
153 public function key()
154 {
155 return $this->keys[$this->position];
156 }
157
158 /**
159 * Iterator - Moves forward to next element
160 */
161 public function next()
162 {
163 ++$this->position;
164 }
165
166 /**
167 * Iterator - Rewinds the Iterator to the first element
168 *
169 * Entries are sorted by date (latest first)
170 */
171 public function rewind()
172 {
173 $this->keys = array_keys($this->ids);
174 $this->position = 0;
175 }
176
177 /**
178 * Iterator - Checks if current position is valid
179 *
180 * @return bool true if the current Bookmark ID exists, false otherwise
181 */
182 public function valid()
183 {
184 return isset($this->keys[$this->position]);
185 }
186
187 /**
188 * Returns a bookmark offset in bookmarks array from its unique ID.
189 *
190 * @param int $id Persistent ID of a bookmark.
191 *
192 * @return int Real offset in local array, or null if doesn't exist.
193 */
194 protected function getBookmarkOffset($id)
195 {
196 if (isset($this->ids[$id])) {
197 return $this->ids[$id];
198 }
199 return null;
200 }
201
202 /**
203 * Return the next key for bookmark creation.
204 * E.g. If the last ID is 597, the next will be 598.
205 *
206 * @return int next ID.
207 */
208 public function getNextId()
209 {
210 if (!empty($this->ids)) {
211 return max(array_keys($this->ids)) + 1;
212 }
213 return 0;
214 }
215
216 /**
217 * @param $url
218 *
219 * @return Bookmark|null
220 */
221 public function getByUrl($url)
222 {
223 if (! empty($url)
224 && isset($this->urls[$url])
225 && isset($this->bookmarks[$this->urls[$url]])
226 ) {
227 return $this->bookmarks[$this->urls[$url]];
228 }
229 return null;
230 }
231
232 /**
233 * Reorder links by creation date (newest first).
234 *
235 * Also update the urls and ids mapping arrays.
236 *
237 * @param string $order ASC|DESC
238 * @param bool $ignoreSticky If set to true, sticky bookmarks won't be first
239 */
240 public function reorder(string $order = 'DESC', bool $ignoreSticky = false): void
241 {
242 $order = $order === 'ASC' ? -1 : 1;
243 // Reorder array by dates.
244 usort($this->bookmarks, function ($a, $b) use ($order, $ignoreSticky) {
245 /** @var $a Bookmark */
246 /** @var $b Bookmark */
247 if (false === $ignoreSticky && $a->isSticky() !== $b->isSticky()) {
248 return $a->isSticky() ? -1 : 1;
249 }
250 return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order;
251 });
252
253 $this->urls = [];
254 $this->ids = [];
255 foreach ($this->bookmarks as $key => $bookmark) {
256 $this->urls[$bookmark->getUrl()] = $key;
257 $this->ids[$bookmark->getId()] = $key;
258 }
259 }
260}
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
new file mode 100644
index 00000000..c9ec2609
--- /dev/null
+++ b/application/bookmark/BookmarkFileService.php
@@ -0,0 +1,407 @@
1<?php
2
3
4namespace Shaarli\Bookmark;
5
6
7use Exception;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
10use Shaarli\Bookmark\Exception\EmptyDataStoreException;
11use Shaarli\Config\ConfigManager;
12use Shaarli\Formatter\BookmarkMarkdownFormatter;
13use Shaarli\History;
14use Shaarli\Legacy\LegacyLinkDB;
15use Shaarli\Legacy\LegacyUpdater;
16use Shaarli\Render\PageCacheManager;
17use Shaarli\Updater\UpdaterUtils;
18
19/**
20 * Class BookmarksService
21 *
22 * This is the entry point to manipulate the bookmark DB.
23 * It manipulates loads links from a file data store containing all bookmarks.
24 *
25 * It also triggers the legacy format (bookmarks as arrays) migration.
26 */
27class BookmarkFileService implements BookmarkServiceInterface
28{
29 /** @var Bookmark[] instance */
30 protected $bookmarks;
31
32 /** @var BookmarkIO instance */
33 protected $bookmarksIO;
34
35 /** @var BookmarkFilter */
36 protected $bookmarkFilter;
37
38 /** @var ConfigManager instance */
39 protected $conf;
40
41 /** @var History instance */
42 protected $history;
43
44 /** @var PageCacheManager instance */
45 protected $pageCacheManager;
46
47 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
48 protected $isLoggedIn;
49
50 /**
51 * @inheritDoc
52 */
53 public function __construct(ConfigManager $conf, History $history, $isLoggedIn)
54 {
55 $this->conf = $conf;
56 $this->history = $history;
57 $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
58 $this->bookmarksIO = new BookmarkIO($this->conf);
59 $this->isLoggedIn = $isLoggedIn;
60
61 if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
62 $this->bookmarks = [];
63 } else {
64 try {
65 $this->bookmarks = $this->bookmarksIO->read();
66 } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
67 $this->bookmarks = new BookmarkArray();
68
69 if ($this->isLoggedIn) {
70 // Datastore file does not exists, we initialize it with default bookmarks.
71 if ($e instanceof DatastoreNotInitializedException) {
72 $this->initialize();
73 } else {
74 $this->save();
75 }
76 }
77 }
78
79 if (! $this->bookmarks instanceof BookmarkArray) {
80 $this->migrate();
81 exit(
82 'Your data store has been migrated, please reload the page.'. PHP_EOL .
83 'If this message keeps showing up, please delete data/updates.txt file.'
84 );
85 }
86 }
87
88 $this->bookmarkFilter = new BookmarkFilter($this->bookmarks);
89 }
90
91 /**
92 * @inheritDoc
93 */
94 public function findByHash($hash)
95 {
96 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
97 // PHP 7.3 introduced array_key_first() to avoid this hack
98 $first = reset($bookmark);
99 if (! $this->isLoggedIn && $first->isPrivate()) {
100 throw new Exception('Not authorized');
101 }
102
103 return $first;
104 }
105
106 /**
107 * @inheritDoc
108 */
109 public function findByUrl($url)
110 {
111 return $this->bookmarks->getByUrl($url);
112 }
113
114 /**
115 * @inheritDoc
116 */
117 public function search(
118 $request = [],
119 $visibility = null,
120 $caseSensitive = false,
121 $untaggedOnly = false,
122 bool $ignoreSticky = false
123 ) {
124 if ($visibility === null) {
125 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
126 }
127
128 // Filter bookmark database according to parameters.
129 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
130 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
131
132 if ($ignoreSticky) {
133 $this->bookmarks->reorder('DESC', true);
134 }
135
136 return $this->bookmarkFilter->filter(
137 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
138 [$searchtags, $searchterm],
139 $caseSensitive,
140 $visibility,
141 $untaggedOnly
142 );
143 }
144
145 /**
146 * @inheritDoc
147 */
148 public function get($id, $visibility = null)
149 {
150 if (! isset($this->bookmarks[$id])) {
151 throw new BookmarkNotFoundException();
152 }
153
154 if ($visibility === null) {
155 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
156 }
157
158 $bookmark = $this->bookmarks[$id];
159 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
160 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
161 ) {
162 throw new Exception('Unauthorized');
163 }
164
165 return $bookmark;
166 }
167
168 /**
169 * @inheritDoc
170 */
171 public function set($bookmark, $save = true)
172 {
173 if (true !== $this->isLoggedIn) {
174 throw new Exception(t('You\'re not authorized to alter the datastore'));
175 }
176 if (! $bookmark instanceof Bookmark) {
177 throw new Exception(t('Provided data is invalid'));
178 }
179 if (! isset($this->bookmarks[$bookmark->getId()])) {
180 throw new BookmarkNotFoundException();
181 }
182 $bookmark->validate();
183
184 $bookmark->setUpdated(new \DateTime());
185 $this->bookmarks[$bookmark->getId()] = $bookmark;
186 if ($save === true) {
187 $this->save();
188 $this->history->updateLink($bookmark);
189 }
190 return $this->bookmarks[$bookmark->getId()];
191 }
192
193 /**
194 * @inheritDoc
195 */
196 public function add($bookmark, $save = true)
197 {
198 if (true !== $this->isLoggedIn) {
199 throw new Exception(t('You\'re not authorized to alter the datastore'));
200 }
201 if (! $bookmark instanceof Bookmark) {
202 throw new Exception(t('Provided data is invalid'));
203 }
204 if (! empty($bookmark->getId())) {
205 throw new Exception(t('This bookmarks already exists'));
206 }
207 $bookmark->setId($this->bookmarks->getNextId());
208 $bookmark->validate();
209
210 $this->bookmarks[$bookmark->getId()] = $bookmark;
211 if ($save === true) {
212 $this->save();
213 $this->history->addLink($bookmark);
214 }
215 return $this->bookmarks[$bookmark->getId()];
216 }
217
218 /**
219 * @inheritDoc
220 */
221 public function addOrSet($bookmark, $save = true)
222 {
223 if (true !== $this->isLoggedIn) {
224 throw new Exception(t('You\'re not authorized to alter the datastore'));
225 }
226 if (! $bookmark instanceof Bookmark) {
227 throw new Exception('Provided data is invalid');
228 }
229 if ($bookmark->getId() === null) {
230 return $this->add($bookmark, $save);
231 }
232 return $this->set($bookmark, $save);
233 }
234
235 /**
236 * @inheritDoc
237 */
238 public function remove($bookmark, $save = true)
239 {
240 if (true !== $this->isLoggedIn) {
241 throw new Exception(t('You\'re not authorized to alter the datastore'));
242 }
243 if (! $bookmark instanceof Bookmark) {
244 throw new Exception(t('Provided data is invalid'));
245 }
246 if (! isset($this->bookmarks[$bookmark->getId()])) {
247 throw new BookmarkNotFoundException();
248 }
249
250 unset($this->bookmarks[$bookmark->getId()]);
251 if ($save === true) {
252 $this->save();
253 $this->history->deleteLink($bookmark);
254 }
255 }
256
257 /**
258 * @inheritDoc
259 */
260 public function exists($id, $visibility = null)
261 {
262 if (! isset($this->bookmarks[$id])) {
263 return false;
264 }
265
266 if ($visibility === null) {
267 $visibility = $this->isLoggedIn ? 'all' : 'public';
268 }
269
270 $bookmark = $this->bookmarks[$id];
271 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
272 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
273 ) {
274 return false;
275 }
276
277 return true;
278 }
279
280 /**
281 * @inheritDoc
282 */
283 public function count($visibility = null)
284 {
285 return count($this->search([], $visibility));
286 }
287
288 /**
289 * @inheritDoc
290 */
291 public function save()
292 {
293 if (true !== $this->isLoggedIn) {
294 // TODO: raise an Exception instead
295 die('You are not authorized to change the database.');
296 }
297
298 $this->bookmarks->reorder();
299 $this->bookmarksIO->write($this->bookmarks);
300 $this->pageCacheManager->invalidateCaches();
301 }
302
303 /**
304 * @inheritDoc
305 */
306 public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
307 {
308 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
309 $tags = [];
310 $caseMapping = [];
311 foreach ($bookmarks as $bookmark) {
312 foreach ($bookmark->getTags() as $tag) {
313 if (empty($tag)
314 || (! $this->isLoggedIn && startsWith($tag, '.'))
315 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
316 || in_array($tag, $filteringTags, true)
317 ) {
318 continue;
319 }
320
321 // The first case found will be displayed.
322 if (!isset($caseMapping[strtolower($tag)])) {
323 $caseMapping[strtolower($tag)] = $tag;
324 $tags[$caseMapping[strtolower($tag)]] = 0;
325 }
326 $tags[$caseMapping[strtolower($tag)]]++;
327 }
328 }
329
330 /*
331 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
332 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
333 *
334 * So we now use array_multisort() to sort tags by DESC occurrences,
335 * then ASC alphabetically for equal values.
336 *
337 * @see https://github.com/shaarli/Shaarli/issues/1142
338 */
339 $keys = array_keys($tags);
340 $tmpTags = array_combine($keys, $keys);
341 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
342 return $tags;
343 }
344
345 /**
346 * @inheritDoc
347 */
348 public function days()
349 {
350 $bookmarkDays = [];
351 foreach ($this->search() as $bookmark) {
352 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
353 }
354 $bookmarkDays = array_keys($bookmarkDays);
355 sort($bookmarkDays);
356
357 return $bookmarkDays;
358 }
359
360 /**
361 * @inheritDoc
362 */
363 public function filterDay($request)
364 {
365 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
366
367 return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility);
368 }
369
370 /**
371 * @inheritDoc
372 */
373 public function initialize()
374 {
375 $initializer = new BookmarkInitializer($this);
376 $initializer->initialize();
377
378 if (true === $this->isLoggedIn) {
379 $this->save();
380 }
381 }
382
383 /**
384 * Handles migration to the new database format (BookmarksArray).
385 */
386 protected function migrate()
387 {
388 $bookmarkDb = new LegacyLinkDB(
389 $this->conf->get('resource.datastore'),
390 true,
391 false
392 );
393 $updater = new LegacyUpdater(
394 UpdaterUtils::read_updates_file($this->conf->get('resource.updates')),
395 $bookmarkDb,
396 $this->conf,
397 true
398 );
399 $newUpdates = $updater->update();
400 if (! empty($newUpdates)) {
401 UpdaterUtils::write_updates_file(
402 $this->conf->get('resource.updates'),
403 $updater->getDoneUpdates()
404 );
405 }
406 }
407}
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php
new file mode 100644
index 00000000..6636bbfe
--- /dev/null
+++ b/application/bookmark/BookmarkFilter.php
@@ -0,0 +1,473 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Exception;
6use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7
8/**
9 * Class LinkFilter.
10 *
11 * Perform search and filter operation on link data list.
12 */
13class BookmarkFilter
14{
15 /**
16 * @var string permalinks.
17 */
18 public static $FILTER_HASH = 'permalink';
19
20 /**
21 * @var string text search.
22 */
23 public static $FILTER_TEXT = 'fulltext';
24
25 /**
26 * @var string tag filter.
27 */
28 public static $FILTER_TAG = 'tags';
29
30 /**
31 * @var string filter by day.
32 */
33 public static $FILTER_DAY = 'FILTER_DAY';
34
35 /**
36 * @var string filter by day.
37 */
38 public static $DEFAULT = 'NO_FILTER';
39
40 /** @var string Visibility: all */
41 public static $ALL = 'all';
42
43 /** @var string Visibility: public */
44 public static $PUBLIC = 'public';
45
46 /** @var string Visibility: private */
47 public static $PRIVATE = 'private';
48
49 /**
50 * @var string Allowed characters for hashtags (regex syntax).
51 */
52 public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
53
54 /**
55 * @var Bookmark[] all available bookmarks.
56 */
57 private $bookmarks;
58
59 /**
60 * @param Bookmark[] $bookmarks initialization.
61 */
62 public function __construct($bookmarks)
63 {
64 $this->bookmarks = $bookmarks;
65 }
66
67 /**
68 * Filter bookmarks according to parameters.
69 *
70 * @param string $type Type of filter (eg. tags, permalink, etc.).
71 * @param mixed $request Filter content.
72 * @param bool $casesensitive Optional: Perform case sensitive filter if true.
73 * @param string $visibility Optional: return only all/private/public bookmarks
74 * @param bool $untaggedonly Optional: return only untagged bookmarks. Applies only if $type includes FILTER_TAG
75 *
76 * @return Bookmark[] filtered bookmark list.
77 *
78 * @throws BookmarkNotFoundException
79 */
80 public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false)
81 {
82 if (!in_array($visibility, ['all', 'public', 'private'])) {
83 $visibility = 'all';
84 }
85
86 switch ($type) {
87 case self::$FILTER_HASH:
88 return $this->filterSmallHash($request);
89 case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext"
90 $noRequest = empty($request) || (empty($request[0]) && empty($request[1]));
91 if ($noRequest) {
92 if ($untaggedonly) {
93 return $this->filterUntagged($visibility);
94 }
95 return $this->noFilter($visibility);
96 }
97 if ($untaggedonly) {
98 $filtered = $this->filterUntagged($visibility);
99 } else {
100 $filtered = $this->bookmarks;
101 }
102 if (!empty($request[0])) {
103 $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
104 }
105 if (!empty($request[1])) {
106 $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility);
107 }
108 return $filtered;
109 case self::$FILTER_TEXT:
110 return $this->filterFulltext($request, $visibility);
111 case self::$FILTER_TAG:
112 if ($untaggedonly) {
113 return $this->filterUntagged($visibility);
114 } else {
115 return $this->filterTags($request, $casesensitive, $visibility);
116 }
117 case self::$FILTER_DAY:
118 return $this->filterDay($request, $visibility);
119 default:
120 return $this->noFilter($visibility);
121 }
122 }
123
124 /**
125 * Unknown filter, but handle private only.
126 *
127 * @param string $visibility Optional: return only all/private/public bookmarks
128 *
129 * @return Bookmark[] filtered bookmarks.
130 */
131 private function noFilter($visibility = 'all')
132 {
133 if ($visibility === 'all') {
134 return $this->bookmarks;
135 }
136
137 $out = array();
138 foreach ($this->bookmarks as $key => $value) {
139 if ($value->isPrivate() && $visibility === 'private') {
140 $out[$key] = $value;
141 } elseif (!$value->isPrivate() && $visibility === 'public') {
142 $out[$key] = $value;
143 }
144 }
145
146 return $out;
147 }
148
149 /**
150 * Returns the shaare corresponding to a smallHash.
151 *
152 * @param string $smallHash permalink hash.
153 *
154 * @return array $filtered array containing permalink data.
155 *
156 * @throws \Shaarli\Bookmark\Exception\BookmarkNotFoundException if the smallhash doesn't match any link.
157 */
158 private function filterSmallHash($smallHash)
159 {
160 foreach ($this->bookmarks as $key => $l) {
161 if ($smallHash == $l->getShortUrl()) {
162 // Yes, this is ugly and slow
163 return [$key => $l];
164 }
165 }
166
167 throw new BookmarkNotFoundException();
168 }
169
170 /**
171 * Returns the list of bookmarks corresponding to a full-text search
172 *
173 * Searches:
174 * - in the URLs, title and description;
175 * - are case-insensitive;
176 * - terms surrounded by quotes " are exact terms search.
177 * - terms starting with a dash - are excluded (except exact terms).
178 *
179 * Example:
180 * print_r($mydb->filterFulltext('hollandais'));
181 *
182 * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
183 * - allows to perform searches on Unicode text
184 * - see https://github.com/shaarli/Shaarli/issues/75 for examples
185 *
186 * @param string $searchterms search query.
187 * @param string $visibility Optional: return only all/private/public bookmarks.
188 *
189 * @return array search results.
190 */
191 private function filterFulltext($searchterms, $visibility = 'all')
192 {
193 if (empty($searchterms)) {
194 return $this->noFilter($visibility);
195 }
196
197 $filtered = array();
198 $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
199 $exactRegex = '/"([^"]+)"/';
200 // Retrieve exact search terms.
201 preg_match_all($exactRegex, $search, $exactSearch);
202 $exactSearch = array_values(array_filter($exactSearch[1]));
203
204 // Remove exact search terms to get AND terms search.
205 $explodedSearchAnd = explode(' ', trim(preg_replace($exactRegex, '', $search)));
206 $explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
207
208 // Filter excluding terms and update andSearch.
209 $excludeSearch = array();
210 $andSearch = array();
211 foreach ($explodedSearchAnd as $needle) {
212 if ($needle[0] == '-' && strlen($needle) > 1) {
213 $excludeSearch[] = substr($needle, 1);
214 } else {
215 $andSearch[] = $needle;
216 }
217 }
218
219 // Iterate over every stored link.
220 foreach ($this->bookmarks as $id => $link) {
221 // ignore non private bookmarks when 'privatonly' is on.
222 if ($visibility !== 'all') {
223 if (!$link->isPrivate() && $visibility === 'private') {
224 continue;
225 } elseif ($link->isPrivate() && $visibility === 'public') {
226 continue;
227 }
228 }
229
230 // Concatenate link fields to search across fields.
231 // Adds a '\' separator for exact search terms.
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
237 // Be optimistic
238 $found = true;
239
240 // First, we look for exact term search
241 for ($i = 0; $i < count($exactSearch) && $found; $i++) {
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.
247 for ($i = 0; $i < count($andSearch) && $found; $i++) {
248 $found = strpos($content, $andSearch[$i]) !== false;
249 }
250
251 // Exclude terms.
252 for ($i = 0; $i < count($excludeSearch) && $found; $i++) {
253 $found = strpos($content, $excludeSearch[$i]) === false;
254 }
255
256 if ($found) {
257 $filtered[$id] = $link;
258 }
259 }
260
261 return $filtered;
262 }
263
264 /**
265 * generate a regex fragment out of a tag
266 *
267 * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
268 *
269 * @return string generated regex fragment
270 */
271 private static function tag2regex($tag)
272 {
273 $len = strlen($tag);
274 if (!$len || $tag === "-" || $tag === "*") {
275 // nothing to search, return empty regex
276 return '';
277 }
278 if ($tag[0] === "-") {
279 // query is negated
280 $i = 1; // use offset to start after '-' character
281 $regex = '(?!'; // create negative lookahead
282 } else {
283 $i = 0; // start at first character
284 $regex = '(?='; // use positive lookahead
285 }
286 $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
287 // iterate over string, separating it into placeholder and content
288 for (; $i < $len; $i++) {
289 if ($tag[$i] === '*') {
290 // placeholder found
291 $regex .= '[^ ]*?';
292 } else {
293 // regular characters
294 $offset = strpos($tag, '*', $i);
295 if ($offset === false) {
296 // no placeholder found, set offset to end of string
297 $offset = $len;
298 }
299 // subtract one, as we want to get before the placeholder or end of string
300 $offset -= 1;
301 // we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
302 $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
303 // move $i on
304 $i = $offset;
305 }
306 }
307 $regex .= '(?:$| ))'; // after the tag may only be a space or the end
308 return $regex;
309 }
310
311 /**
312 * Returns the list of bookmarks associated with a given list of tags
313 *
314 * You can specify one or more tags, separated by space or a comma, e.g.
315 * print_r($mydb->filterTags('linux programming'));
316 *
317 * @param string $tags list of tags separated by commas or blank spaces.
318 * @param bool $casesensitive ignore case if false.
319 * @param string $visibility Optional: return only all/private/public bookmarks.
320 *
321 * @return array filtered bookmarks.
322 */
323 public function filterTags($tags, $casesensitive = false, $visibility = 'all')
324 {
325 // get single tags (we may get passed an array, even though the docs say different)
326 $inputTags = $tags;
327 if (!is_array($tags)) {
328 // we got an input string, split tags
329 $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
330 }
331
332 if (!count($inputTags)) {
333 // no input tags
334 return $this->noFilter($visibility);
335 }
336
337 // If we only have public visibility, we can't look for hidden tags
338 if ($visibility === self::$PUBLIC) {
339 $inputTags = array_values(array_filter($inputTags, function ($tag) {
340 return ! startsWith($tag, '.');
341 }));
342
343 if (empty($inputTags)) {
344 return [];
345 }
346 }
347
348 // build regex from all tags
349 $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
350 if (!$casesensitive) {
351 // make regex case insensitive
352 $re .= 'i';
353 }
354
355 // create resulting array
356 $filtered = [];
357
358 // iterate over each link
359 foreach ($this->bookmarks as $key => $link) {
360 // check level of visibility
361 // ignore non private bookmarks when 'privateonly' is on.
362 if ($visibility !== 'all') {
363 if (!$link->isPrivate() && $visibility === 'private') {
364 continue;
365 } elseif ($link->isPrivate() && $visibility === 'public') {
366 continue;
367 }
368 }
369 $search = $link->getTagsString(); // build search string, start with tags of current link
370 if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
371 // description given and at least one possible tag found
372 $descTags = array();
373 // find all tags in the form of #tag in the description
374 preg_match_all(
375 '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
376 $link->getDescription(),
377 $descTags
378 );
379 if (count($descTags[1])) {
380 // there were some tags in the description, add them to the search string
381 $search .= ' ' . implode(' ', $descTags[1]);
382 }
383 };
384 // match regular expression with search string
385 if (!preg_match($re, $search)) {
386 // this entry does _not_ match our regex
387 continue;
388 }
389 $filtered[$key] = $link;
390 }
391 return $filtered;
392 }
393
394 /**
395 * Return only bookmarks without any tag.
396 *
397 * @param string $visibility return only all/private/public bookmarks.
398 *
399 * @return array filtered bookmarks.
400 */
401 public function filterUntagged($visibility)
402 {
403 $filtered = [];
404 foreach ($this->bookmarks as $key => $link) {
405 if ($visibility !== 'all') {
406 if (!$link->isPrivate() && $visibility === 'private') {
407 continue;
408 } elseif ($link->isPrivate() && $visibility === 'public') {
409 continue;
410 }
411 }
412
413 if (empty(trim($link->getTagsString()))) {
414 $filtered[$key] = $link;
415 }
416 }
417
418 return $filtered;
419 }
420
421 /**
422 * Returns the list of articles for a given day, chronologically sorted
423 *
424 * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
425 * print_r($mydb->filterDay('20120125'));
426 *
427 * @param string $day day to filter.
428 * @param string $visibility return only all/private/public bookmarks.
429
430 * @return array all link matching given day.
431 *
432 * @throws Exception if date format is invalid.
433 */
434 public function filterDay($day, $visibility)
435 {
436 if (!checkDateFormat('Ymd', $day)) {
437 throw new Exception('Invalid date format');
438 }
439
440 $filtered = [];
441 foreach ($this->bookmarks as $key => $bookmark) {
442 if ($visibility === static::$PUBLIC && $bookmark->isPrivate()) {
443 continue;
444 }
445
446 if ($bookmark->getCreated()->format('Ymd') == $day) {
447 $filtered[$key] = $bookmark;
448 }
449 }
450
451 // sort by date ASC
452 return array_reverse($filtered, true);
453 }
454
455 /**
456 * Convert a list of tags (str) to an array. Also
457 * - handle case sensitivity.
458 * - accepts spaces commas as separator.
459 *
460 * @param string $tags string containing a list of tags.
461 * @param bool $casesensitive will convert everything to lowercase if false.
462 *
463 * @return array filtered tags string.
464 */
465 public static function tagsStrToArray($tags, $casesensitive)
466 {
467 // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
468 $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
469 $tagsOut = str_replace(',', ' ', $tagsOut);
470
471 return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
472 }
473}
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php
new file mode 100644
index 00000000..6bf7f365
--- /dev/null
+++ b/application/bookmark/BookmarkIO.php
@@ -0,0 +1,108 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
6use Shaarli\Bookmark\Exception\EmptyDataStoreException;
7use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
8use Shaarli\Config\ConfigManager;
9
10/**
11 * Class BookmarkIO
12 *
13 * This class performs read/write operation to the file data store.
14 * Used by BookmarkFileService.
15 *
16 * @package Shaarli\Bookmark
17 */
18class BookmarkIO
19{
20 /**
21 * @var string Datastore file path
22 */
23 protected $datastore;
24
25 /**
26 * @var ConfigManager instance
27 */
28 protected $conf;
29
30 /**
31 * string Datastore PHP prefix
32 */
33 protected static $phpPrefix = '<?php /* ';
34
35 /**
36 * string Datastore PHP suffix
37 */
38 protected static $phpSuffix = ' */ ?>';
39
40 /**
41 * LinksIO constructor.
42 *
43 * @param ConfigManager $conf instance
44 */
45 public function __construct($conf)
46 {
47 $this->conf = $conf;
48 $this->datastore = $conf->get('resource.datastore');
49 }
50
51 /**
52 * Reads database from disk to memory
53 *
54 * @return BookmarkArray instance
55 *
56 * @throws NotWritableDataStoreException Data couldn't be loaded
57 * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark
58 * @throws DatastoreNotInitializedException File does not exists
59 */
60 public function read()
61 {
62 if (! file_exists($this->datastore)) {
63 throw new DatastoreNotInitializedException();
64 }
65
66 if (!is_writable($this->datastore)) {
67 throw new NotWritableDataStoreException($this->datastore);
68 }
69
70 // Note that gzinflate is faster than gzuncompress.
71 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
72 $links = unserialize(gzinflate(base64_decode(
73 substr(file_get_contents($this->datastore),
74 strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
75
76 if (empty($links)) {
77 if (filesize($this->datastore) > 100) {
78 throw new NotWritableDataStoreException($this->datastore);
79 }
80 throw new EmptyDataStoreException();
81 }
82
83 return $links;
84 }
85
86 /**
87 * Saves the database from memory to disk
88 *
89 * @param BookmarkArray $links instance.
90 *
91 * @throws NotWritableDataStoreException the datastore is not writable
92 */
93 public function write($links)
94 {
95 if (is_file($this->datastore) && !is_writeable($this->datastore)) {
96 // The datastore exists but is not writeable
97 throw new NotWritableDataStoreException($this->datastore);
98 } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
99 // The datastore does not exist and its parent directory is not writeable
100 throw new NotWritableDataStoreException(dirname($this->datastore));
101 }
102
103 file_put_contents(
104 $this->datastore,
105 self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix
106 );
107 }
108}
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php
new file mode 100644
index 00000000..815047e3
--- /dev/null
+++ b/application/bookmark/BookmarkInitializer.php
@@ -0,0 +1,110 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5/**
6 * Class BookmarkInitializer
7 *
8 * This class is used to initialized default bookmarks after a fresh install of Shaarli.
9 * It should be only called if the datastore file does not exist(users might want to delete the default bookmarks).
10 *
11 * To prevent data corruption, it does not overwrite existing bookmarks,
12 * even though there should not be any.
13 *
14 * @package Shaarli\Bookmark
15 */
16class BookmarkInitializer
17{
18 /** @var BookmarkServiceInterface */
19 protected $bookmarkService;
20
21 /**
22 * BookmarkInitializer constructor.
23 *
24 * @param BookmarkServiceInterface $bookmarkService
25 */
26 public function __construct($bookmarkService)
27 {
28 $this->bookmarkService = $bookmarkService;
29 }
30
31 /**
32 * Initialize the data store with default bookmarks
33 */
34 public function initialize()
35 {
36 $bookmark = new Bookmark();
37 $bookmark->setTitle('quicksilver (loop) on Vimeo ' . t('(private bookmark with thumbnail demo)'));
38 $bookmark->setUrl('https://vimeo.com/153493904');
39 $bookmark->setDescription(t(
40'Shaarli will automatically pick up the thumbnail for links to a variety of websites.
41
42Explore your new Shaarli instance by trying out controls and menus.
43Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli.
44
45Now you can edit or delete the default shaares.
46'
47 ));
48 $bookmark->setTagsString('shaarli help thumbnail');
49 $bookmark->setPrivate(true);
50 $this->bookmarkService->add($bookmark, false);
51
52 $bookmark = new Bookmark();
53 $bookmark->setTitle(t('Note: Shaare descriptions'));
54 $bookmark->setDescription(t(
55'Adding a shaare without entering a URL creates a text-only "note" post such as this one.
56This note is private, so you are the only one able to see it while logged in.
57
58You can use this to keep notes, post articles, code snippets, and much more.
59
60The Markdown formatting setting allows you to format your notes and bookmark description:
61
62### Title headings
63
64#### Multiple headings levels
65 * bullet lists
66 * _italic_ text
67 * **bold** text
68 * ~~strike through~~ text
69 * `code` blocks
70 * images
71 * [links](https://en.wikipedia.org/wiki/Markdown)
72
73Markdown also supports tables:
74
75| Name | Type | Color | Qty |
76| ------- | --------- | ------ | ----- |
77| Orange | Fruit | Orange | 126 |
78| Apple | Fruit | Any | 62 |
79| Lemon | Fruit | Yellow | 30 |
80| Carrot | Vegetable | Red | 14 |
81'
82 ));
83 $bookmark->setTagsString('shaarli help');
84 $bookmark->setPrivate(true);
85 $this->bookmarkService->add($bookmark, false);
86
87 $bookmark = new Bookmark();
88 $bookmark->setTitle(
89 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service')
90 );
91 $bookmark->setDescription(t(
92'Welcome to Shaarli!
93
94Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately.
95You can add a description to your bookmarks, such as this one, and tag them.
96
97Create a new shaare by clicking the `+Shaare` button, or using any of the recommended tools (browser extension, mobile app, bookmarklet, REST API, etc.).
98
99You 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`).
100Hashtags such as #shaarli #help are also supported.
101You can also filter the available [RSS feed](/feed/atom) and picture wall by tag or plaintext search.
102
103We hope that you will enjoy using Shaarli, maintained with â¤ï¸ by the community!
104Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if you have a suggestion or encounter an issue.
105'
106 ));
107 $bookmark->setTagsString('shaarli help');
108 $this->bookmarkService->add($bookmark, false);
109 }
110}
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php
new file mode 100644
index 00000000..b9b483eb
--- /dev/null
+++ b/application/bookmark/BookmarkServiceInterface.php
@@ -0,0 +1,186 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5
6use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
8use Shaarli\Config\ConfigManager;
9use Shaarli\History;
10
11/**
12 * Class BookmarksService
13 *
14 * This is the entry point to manipulate the bookmark DB.
15 */
16interface BookmarkServiceInterface
17{
18 /**
19 * BookmarksService constructor.
20 *
21 * @param ConfigManager $conf instance
22 * @param History $history instance
23 * @param bool $isLoggedIn true if the current user is logged in
24 */
25 public function __construct(ConfigManager $conf, History $history, $isLoggedIn);
26
27 /**
28 * Find a bookmark by hash
29 *
30 * @param string $hash
31 *
32 * @return mixed
33 *
34 * @throws \Exception
35 */
36 public function findByHash($hash);
37
38 /**
39 * @param $url
40 *
41 * @return Bookmark|null
42 */
43 public function findByUrl($url);
44
45 /**
46 * Search bookmarks
47 *
48 * @param mixed $request
49 * @param string $visibility
50 * @param bool $caseSensitive
51 * @param bool $untaggedOnly
52 * @param bool $ignoreSticky
53 *
54 * @return Bookmark[]
55 */
56 public function search(
57 $request = [],
58 $visibility = null,
59 $caseSensitive = false,
60 $untaggedOnly = false,
61 bool $ignoreSticky = false
62 );
63
64 /**
65 * Get a single bookmark by its ID.
66 *
67 * @param int $id Bookmark ID
68 * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
69 * exception
70 *
71 * @return Bookmark
72 *
73 * @throws BookmarkNotFoundException
74 * @throws \Exception
75 */
76 public function get($id, $visibility = null);
77
78 /**
79 * Updates an existing bookmark (depending on its ID).
80 *
81 * @param Bookmark $bookmark
82 * @param bool $save Writes to the datastore if set to true
83 *
84 * @return Bookmark Updated bookmark
85 *
86 * @throws BookmarkNotFoundException
87 * @throws \Exception
88 */
89 public function set($bookmark, $save = true);
90
91 /**
92 * Adds a new bookmark (the ID must be empty).
93 *
94 * @param Bookmark $bookmark
95 * @param bool $save Writes to the datastore if set to true
96 *
97 * @return Bookmark new bookmark
98 *
99 * @throws \Exception
100 */
101 public function add($bookmark, $save = true);
102
103 /**
104 * Adds or updates a bookmark depending on its ID:
105 * - a Bookmark without ID will be added
106 * - a Bookmark with an existing ID will be updated
107 *
108 * @param Bookmark $bookmark
109 * @param bool $save
110 *
111 * @return Bookmark
112 *
113 * @throws \Exception
114 */
115 public function addOrSet($bookmark, $save = true);
116
117 /**
118 * Deletes a bookmark.
119 *
120 * @param Bookmark $bookmark
121 * @param bool $save
122 *
123 * @throws \Exception
124 */
125 public function remove($bookmark, $save = true);
126
127 /**
128 * Get a single bookmark by its ID.
129 *
130 * @param int $id Bookmark ID
131 * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
132 * exception
133 *
134 * @return bool
135 */
136 public function exists($id, $visibility = null);
137
138 /**
139 * Return the number of available bookmarks for given visibility.
140 *
141 * @param string $visibility public|private|all
142 *
143 * @return int Number of bookmarks
144 */
145 public function count($visibility = null);
146
147 /**
148 * Write the datastore.
149 *
150 * @throws NotWritableDataStoreException
151 */
152 public function save();
153
154 /**
155 * Returns the list tags appearing in the bookmarks with the given tags
156 *
157 * @param array $filteringTags tags selecting the bookmarks to consider
158 * @param string $visibility process only all/private/public bookmarks
159 *
160 * @return array tag => bookmarksCount
161 */
162 public function bookmarksCountPerTag($filteringTags = [], $visibility = 'all');
163
164 /**
165 * Returns the list of days containing articles (oldest first)
166 *
167 * @return array containing days (in format YYYYMMDD).
168 */
169 public function days();
170
171 /**
172 * Returns the list of articles for a given day.
173 *
174 * @param string $request day to filter. Format: YYYYMMDD.
175 *
176 * @return Bookmark[] list of shaare found.
177 *
178 * @throws BookmarkNotFoundException
179 */
180 public function filterDay($request);
181
182 /**
183 * Creates the default database after a fresh install.
184 */
185 public function initialize();
186}
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index 77eb2d95..e7af4d55 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -1,112 +1,6 @@
1<?php 1<?php
2 2
3use Shaarli\Bookmark\LinkDB; 3use Shaarli\Bookmark\Bookmark;
4
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 4
111/** 5/**
112 * Extract title from an HTML document. 6 * Extract title from an HTML document.
@@ -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 }
@@ -188,30 +82,11 @@ function html_extract_tag($tag, $html)
188} 82}
189 83
190/** 84/**
191 * Count private links in given linklist. 85 * In a string, converts URLs to clickable bookmarks.
192 *
193 * @param array|Countable $links Linklist.
194 *
195 * @return int Number of private links.
196 */
197function count_private($links)
198{
199 $cpt = 0;
200 foreach ($links as $link) {
201 if ($link['private']) {
202 $cpt += 1;
203 }
204 }
205
206 return $cpt;
207}
208
209/**
210 * In a string, converts URLs to clickable links.
211 * 86 *
212 * @param string $text input string. 87 * @param string $text input string.
213 * 88 *
214 * @return string returns $text with all links converted to HTML links. 89 * @return string returns $text with all bookmarks converted to HTML bookmarks.
215 * 90 *
216 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 91 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
217 */ 92 */
@@ -239,7 +114,7 @@ function hashtag_autolink($description, $indexUrl = '')
239 * \p{Mn} - any non marking space (accents, umlauts, etc) 114 * \p{Mn} - any non marking space (accents, umlauts, etc)
240 */ 115 */
241 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 116 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
242 $replacement = '$1<a href="'. $indexUrl .'?addtag=$2" title="Hashtag $2">#$2</a>'; 117 $replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>';
243 return preg_replace($regex, $replacement, $description); 118 return preg_replace($regex, $replacement, $description);
244} 119}
245 120
@@ -279,7 +154,7 @@ function format_description($description, $indexUrl = '')
279 */ 154 */
280function link_small_hash($date, $id) 155function link_small_hash($date, $id)
281{ 156{
282 return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id); 157 return smallHash($date->format(Bookmark::LINK_DATE_FORMAT) . $id);
283} 158}
284 159
285/** 160/**
diff --git a/application/bookmark/exception/LinkNotFoundException.php b/application/bookmark/exception/BookmarkNotFoundException.php
index f9414428..827a3d35 100644
--- a/application/bookmark/exception/LinkNotFoundException.php
+++ b/application/bookmark/exception/BookmarkNotFoundException.php
@@ -3,7 +3,7 @@ namespace Shaarli\Bookmark\Exception;
3 3
4use Exception; 4use Exception;
5 5
6class LinkNotFoundException extends Exception 6class BookmarkNotFoundException extends Exception
7{ 7{
8 /** 8 /**
9 * LinkNotFoundException constructor. 9 * LinkNotFoundException constructor.
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/bookmark/exception/EmptyDataStoreException.php b/application/bookmark/exception/EmptyDataStoreException.php
new file mode 100644
index 00000000..cd48c1e6
--- /dev/null
+++ b/application/bookmark/exception/EmptyDataStoreException.php
@@ -0,0 +1,7 @@
1<?php
2
3
4namespace Shaarli\Bookmark\Exception;
5
6
7class EmptyDataStoreException extends \Exception {}
diff --git a/application/bookmark/exception/InvalidBookmarkException.php b/application/bookmark/exception/InvalidBookmarkException.php
new file mode 100644
index 00000000..10c84a6d
--- /dev/null
+++ b/application/bookmark/exception/InvalidBookmarkException.php
@@ -0,0 +1,30 @@
1<?php
2
3namespace Shaarli\Bookmark\Exception;
4
5use Shaarli\Bookmark\Bookmark;
6
7class InvalidBookmarkException extends \Exception
8{
9 public function __construct($bookmark)
10 {
11 if ($bookmark instanceof Bookmark) {
12 if ($bookmark->getCreated() instanceof \DateTime) {
13 $created = $bookmark->getCreated()->format(\DateTime::ATOM);
14 } elseif (empty($bookmark->getCreated())) {
15 $created = '';
16 } else {
17 $created = 'Not a DateTime object';
18 }
19 $this->message = 'This bookmark is not valid'. PHP_EOL;
20 $this->message .= ' - ID: '. $bookmark->getId() . PHP_EOL;
21 $this->message .= ' - Title: '. $bookmark->getTitle() . PHP_EOL;
22 $this->message .= ' - Url: '. $bookmark->getUrl() . PHP_EOL;
23 $this->message .= ' - ShortUrl: '. $bookmark->getShortUrl() . PHP_EOL;
24 $this->message .= ' - Created: '. $created . PHP_EOL;
25 } else {
26 $this->message = 'The provided data is not a bookmark'. PHP_EOL;
27 $this->message .= var_export($bookmark, true);
28 }
29 }
30}
diff --git a/application/bookmark/exception/NotWritableDataStoreException.php b/application/bookmark/exception/NotWritableDataStoreException.php
new file mode 100644
index 00000000..95f34b50
--- /dev/null
+++ b/application/bookmark/exception/NotWritableDataStoreException.php
@@ -0,0 +1,19 @@
1<?php
2
3
4namespace Shaarli\Bookmark\Exception;
5
6
7class NotWritableDataStoreException extends \Exception
8{
9 /**
10 * NotReadableDataStore constructor.
11 *
12 * @param string $dataStore file path
13 */
14 public function __construct($dataStore)
15 {
16 $this->message = 'Couldn\'t load data from the data store file "'. $dataStore .'". '.
17 'Your data might be corrupted, or your file isn\'t readable.';
18 }
19}
diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php
index 4509357c..c0c0dab9 100644
--- a/application/config/ConfigJson.php
+++ b/application/config/ConfigJson.php
@@ -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. '.
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index c95e6800..4c98be30 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,7 +362,7 @@ 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: ');
@@ -381,6 +382,7 @@ class ConfigManager
381 // default state of the 'remember me' checkbox of the login form 382 // default state of the 'remember me' checkbox of the login form
382 $this->setEmpty('privacy.remember_user_default', true); 383 $this->setEmpty('privacy.remember_user_default', true);
383 384
385 $this->setEmpty('thumbnails.mode', Thumbnailer::MODE_ALL);
384 $this->setEmpty('thumbnails.width', '125'); 386 $this->setEmpty('thumbnails.width', '125');
385 $this->setEmpty('thumbnails.height', '90'); 387 $this->setEmpty('thumbnails.height', '90');
386 388
@@ -389,6 +391,8 @@ class ConfigManager
389 $this->setEmpty('translation.extensions', []); 391 $this->setEmpty('translation.extensions', []);
390 392
391 $this->setEmpty('plugins', array()); 393 $this->setEmpty('plugins', array());
394
395 $this->setEmpty('formatter', 'markdown');
392 } 396 }
393 397
394 /** 398 /**
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
new file mode 100644
index 00000000..55bb51b5
--- /dev/null
+++ b/application/container/ContainerBuilder.php
@@ -0,0 +1,165 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Container;
6
7use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Feed\FeedBuilder;
11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\Front\Controller\Visitor\ErrorController;
13use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
14use Shaarli\History;
15use Shaarli\Http\HttpAccess;
16use Shaarli\Netscape\NetscapeBookmarkUtils;
17use Shaarli\Plugin\PluginManager;
18use Shaarli\Render\PageBuilder;
19use Shaarli\Render\PageCacheManager;
20use Shaarli\Security\CookieManager;
21use Shaarli\Security\LoginManager;
22use Shaarli\Security\SessionManager;
23use Shaarli\Thumbnailer;
24use Shaarli\Updater\Updater;
25use Shaarli\Updater\UpdaterUtils;
26
27/**
28 * Class ContainerBuilder
29 *
30 * Helper used to build a Slim container instance with Shaarli's object dependencies.
31 * Note that most injected objects MUST be added as closures, to let the container instantiate
32 * only the objects it requires during the execution.
33 *
34 * @package Container
35 */
36class ContainerBuilder
37{
38 /** @var ConfigManager */
39 protected $conf;
40
41 /** @var SessionManager */
42 protected $session;
43
44 /** @var CookieManager */
45 protected $cookieManager;
46
47 /** @var LoginManager */
48 protected $login;
49
50 /** @var string|null */
51 protected $basePath = null;
52
53 public function __construct(
54 ConfigManager $conf,
55 SessionManager $session,
56 CookieManager $cookieManager,
57 LoginManager $login
58 ) {
59 $this->conf = $conf;
60 $this->session = $session;
61 $this->login = $login;
62 $this->cookieManager = $cookieManager;
63 }
64
65 public function build(): ShaarliContainer
66 {
67 $container = new ShaarliContainer();
68
69 $container['conf'] = $this->conf;
70 $container['sessionManager'] = $this->session;
71 $container['cookieManager'] = $this->cookieManager;
72 $container['loginManager'] = $this->login;
73 $container['basePath'] = $this->basePath;
74
75 $container['plugins'] = function (ShaarliContainer $container): PluginManager {
76 return new PluginManager($container->conf);
77 };
78
79 $container['history'] = function (ShaarliContainer $container): History {
80 return new History($container->conf->get('resource.history'));
81 };
82
83 $container['bookmarkService'] = function (ShaarliContainer $container): BookmarkServiceInterface {
84 return new BookmarkFileService(
85 $container->conf,
86 $container->history,
87 $container->loginManager->isLoggedIn()
88 );
89 };
90
91 $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
92 return new PageBuilder(
93 $container->conf,
94 $container->sessionManager->getSession(),
95 $container->bookmarkService,
96 $container->sessionManager->generateToken(),
97 $container->loginManager->isLoggedIn()
98 );
99 };
100
101 $container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
102 $pluginManager = new PluginManager($container->conf);
103
104 $pluginManager->load($container->conf->get('general.enabled_plugins'));
105
106 return $pluginManager;
107 };
108
109 $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
110 return new FormatterFactory(
111 $container->conf,
112 $container->loginManager->isLoggedIn()
113 );
114 };
115
116 $container['pageCacheManager'] = function (ShaarliContainer $container): PageCacheManager {
117 return new PageCacheManager(
118 $container->conf->get('resource.page_cache'),
119 $container->loginManager->isLoggedIn()
120 );
121 };
122
123 $container['feedBuilder'] = function (ShaarliContainer $container): FeedBuilder {
124 return new FeedBuilder(
125 $container->bookmarkService,
126 $container->formatterFactory->getFormatter(),
127 $container->environment,
128 $container->loginManager->isLoggedIn()
129 );
130 };
131
132 $container['thumbnailer'] = function (ShaarliContainer $container): Thumbnailer {
133 return new Thumbnailer($container->conf);
134 };
135
136 $container['httpAccess'] = function (): HttpAccess {
137 return new HttpAccess();
138 };
139
140 $container['netscapeBookmarkUtils'] = function (ShaarliContainer $container): NetscapeBookmarkUtils {
141 return new NetscapeBookmarkUtils($container->bookmarkService, $container->conf, $container->history);
142 };
143
144 $container['updater'] = function (ShaarliContainer $container): Updater {
145 return new Updater(
146 UpdaterUtils::read_updates_file($container->conf->get('resource.updates')),
147 $container->bookmarkService,
148 $container->conf,
149 $container->loginManager->isLoggedIn()
150 );
151 };
152
153 $container['notFoundHandler'] = function (ShaarliContainer $container): ErrorNotFoundController {
154 return new ErrorNotFoundController($container);
155 };
156 $container['errorHandler'] = function (ShaarliContainer $container): ErrorController {
157 return new ErrorController($container);
158 };
159 $container['phpErrorHandler'] = function (ShaarliContainer $container): ErrorController {
160 return new ErrorController($container);
161 };
162
163 return $container;
164 }
165}
diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php
new file mode 100644
index 00000000..66e669aa
--- /dev/null
+++ b/application/container/ShaarliContainer.php
@@ -0,0 +1,51 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Container;
6
7use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Feed\FeedBuilder;
10use Shaarli\Formatter\FormatterFactory;
11use Shaarli\History;
12use Shaarli\Http\HttpAccess;
13use Shaarli\Netscape\NetscapeBookmarkUtils;
14use Shaarli\Plugin\PluginManager;
15use Shaarli\Render\PageBuilder;
16use Shaarli\Render\PageCacheManager;
17use Shaarli\Security\CookieManager;
18use Shaarli\Security\LoginManager;
19use Shaarli\Security\SessionManager;
20use Shaarli\Thumbnailer;
21use Shaarli\Updater\Updater;
22use Slim\Container;
23
24/**
25 * Extension of Slim container to document the injected objects.
26 *
27 * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`)
28 * @property BookmarkServiceInterface $bookmarkService
29 * @property CookieManager $cookieManager
30 * @property ConfigManager $conf
31 * @property mixed[] $environment $_SERVER automatically injected by Slim
32 * @property callable $errorHandler Overrides default Slim exception display
33 * @property FeedBuilder $feedBuilder
34 * @property FormatterFactory $formatterFactory
35 * @property History $history
36 * @property HttpAccess $httpAccess
37 * @property LoginManager $loginManager
38 * @property NetscapeBookmarkUtils $netscapeBookmarkUtils
39 * @property callable $notFoundHandler Overrides default Slim exception display
40 * @property PageBuilder $pageBuilder
41 * @property PageCacheManager $pageCacheManager
42 * @property callable $phpErrorHandler Overrides default Slim PHP error display
43 * @property PluginManager $pluginManager
44 * @property SessionManager $sessionManager
45 * @property Thumbnailer $thumbnailer
46 * @property Updater $updater
47 */
48class ShaarliContainer extends Container
49{
50
51}
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 7c859474..f6def630 100644
--- a/application/feed/FeedBuilder.php
+++ b/application/feed/FeedBuilder.php
@@ -2,6 +2,9 @@
2namespace Shaarli\Feed; 2namespace Shaarli\Feed;
3 3
4use DateTime; 4use DateTime;
5use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkServiceInterface;
7use Shaarli\Formatter\BookmarkFormatter;
5 8
6/** 9/**
7 * FeedBuilder class. 10 * FeedBuilder class.
@@ -26,37 +29,30 @@ class FeedBuilder
26 public static $DEFAULT_LANGUAGE = 'en-en'; 29 public static $DEFAULT_LANGUAGE = 'en-en';
27 30
28 /** 31 /**
29 * @var int Number of links to display in a feed by default. 32 * @var int Number of bookmarks to display in a feed by default.
30 */ 33 */
31 public static $DEFAULT_NB_LINKS = 50; 34 public static $DEFAULT_NB_LINKS = 50;
32 35
33 /** 36 /**
34 * @var \Shaarli\Bookmark\LinkDB instance. 37 * @var BookmarkServiceInterface instance.
35 */ 38 */
36 protected $linkDB; 39 protected $linkDB;
37 40
38 /** 41 /**
39 * @var string RSS or ATOM feed. 42 * @var BookmarkFormatter instance.
40 */ 43 */
41 protected $feedType; 44 protected $formatter;
42 45
43 /** 46 /** @var mixed[] $_SERVER */
44 * @var array $_SERVER
45 */
46 protected $serverInfo; 47 protected $serverInfo;
47 48
48 /** 49 /**
49 * @var array $_GET
50 */
51 protected $userInput;
52
53 /**
54 * @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.
55 */ 51 */
56 protected $isLoggedIn; 52 protected $isLoggedIn;
57 53
58 /** 54 /**
59 * @var boolean Use permalinks instead of direct links if true. 55 * @var boolean Use permalinks instead of direct bookmarks if true.
60 */ 56 */
61 protected $usePermalinks; 57 protected $usePermalinks;
62 58
@@ -69,7 +65,6 @@ class FeedBuilder
69 * @var string server locale. 65 * @var string server locale.
70 */ 66 */
71 protected $locale; 67 protected $locale;
72
73 /** 68 /**
74 * @var DateTime Latest item date. 69 * @var DateTime Latest item date.
75 */ 70 */
@@ -78,38 +73,38 @@ class FeedBuilder
78 /** 73 /**
79 * Feed constructor. 74 * Feed constructor.
80 * 75 *
81 * @param \Shaarli\Bookmark\LinkDB $linkDB LinkDB instance. 76 * @param BookmarkServiceInterface $linkDB LinkDB instance.
82 * @param string $feedType Type of feed. 77 * @param BookmarkFormatter $formatter instance.
83 * @param array $serverInfo $_SERVER. 78 * @param array $serverInfo $_SERVER.
84 * @param array $userInput $_GET. 79 * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
85 * @param boolean $isLoggedIn True if the user is currently logged in,
86 * false otherwise.
87 */ 80 */
88 public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLoggedIn) 81 public function __construct($linkDB, $formatter, $serverInfo, $isLoggedIn)
89 { 82 {
90 $this->linkDB = $linkDB; 83 $this->linkDB = $linkDB;
91 $this->feedType = $feedType; 84 $this->formatter = $formatter;
92 $this->serverInfo = $serverInfo; 85 $this->serverInfo = $serverInfo;
93 $this->userInput = $userInput;
94 $this->isLoggedIn = $isLoggedIn; 86 $this->isLoggedIn = $isLoggedIn;
95 } 87 }
96 88
97 /** 89 /**
98 * Build data for feed templates. 90 * Build data for feed templates.
99 * 91 *
92 * @param string $feedType Type of feed (RSS/ATOM).
93 * @param array $userInput $_GET.
94 *
100 * @return array Formatted data for feeds templates. 95 * @return array Formatted data for feeds templates.
101 */ 96 */
102 public function buildData() 97 public function buildData(string $feedType, ?array $userInput)
103 { 98 {
104 // Search for untagged links 99 // Search for untagged bookmarks
105 if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { 100 if (isset($this->userInput['searchtags']) && empty($userInput['searchtags'])) {
106 $this->userInput['searchtags'] = false; 101 $userInput['searchtags'] = false;
107 } 102 }
108 103
109 // Optionally filter the results: 104 // Optionally filter the results:
110 $linksToDisplay = $this->linkDB->filterSearch($this->userInput); 105 $linksToDisplay = $this->linkDB->search($userInput, null, false, false, true);
111 106
112 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay)); 107 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
113 108
114 // 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.
115 $keys = array(); 110 $keys = array();
@@ -118,17 +113,18 @@ class FeedBuilder
118 } 113 }
119 114
120 $pageaddr = escape(index_url($this->serverInfo)); 115 $pageaddr = escape(index_url($this->serverInfo));
116 $this->formatter->addContextData('index_url', $pageaddr);
121 $linkDisplayed = array(); 117 $linkDisplayed = array();
122 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { 118 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
123 $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr); 119 $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr);
124 } 120 }
125 121
126 $data['language'] = $this->getTypeLanguage(); 122 $data['language'] = $this->getTypeLanguage($feedType);
127 $data['last_update'] = $this->getLatestDateFormatted(); 123 $data['last_update'] = $this->getLatestDateFormatted($feedType);
128 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn; 124 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
129 // Remove leading slash from REQUEST_URI. 125 // Remove leading path from REQUEST_URI (already contained in $pageaddr).
130 $data['self_link'] = escape(server_url($this->serverInfo)) 126 $requestUri = preg_replace('#(.*?/)(feed.*)#', '$2', escape($this->serverInfo['REQUEST_URI']));
131 . escape($this->serverInfo['REQUEST_URI']); 127 $data['self_link'] = $pageaddr . $requestUri;
132 $data['index_url'] = $pageaddr; 128 $data['index_url'] = $pageaddr;
133 $data['usepermalinks'] = $this->usePermalinks === true; 129 $data['usepermalinks'] = $this->usePermalinks === true;
134 $data['links'] = $linkDisplayed; 130 $data['links'] = $linkDisplayed;
@@ -137,56 +133,7 @@ class FeedBuilder
137 } 133 }
138 134
139 /** 135 /**
140 * Build a feed item (one per shaare). 136 * Set this to true to use permalinks instead of direct bookmarks.
141 *
142 * @param array $link Single link array extracted from LinkDB.
143 * @param string $pageaddr Index URL.
144 *
145 * @return array Link array with feed attributes.
146 */
147 protected function buildItem($link, $pageaddr)
148 {
149 $link['guid'] = $pageaddr . '?' . $link['shorturl'];
150 // Prepend the root URL for notes
151 if (is_note($link['url'])) {
152 $link['url'] = $pageaddr . $link['url'];
153 }
154 if ($this->usePermalinks === true) {
155 $permalink = '<a href="' . $link['url'] . '" title="' . t('Direct link') . '">' . t('Direct link') . '</a>';
156 } else {
157 $permalink = '<a href="' . $link['guid'] . '" title="' . t('Permalink') . '">' . t('Permalink') . '</a>';
158 }
159 $link['description'] = format_description($link['description'], $pageaddr);
160 $link['description'] .= PHP_EOL . '<br>&#8212; ' . $permalink;
161
162 $pubDate = $link['created'];
163 $link['pub_iso_date'] = $this->getIsoDate($pubDate);
164
165 // atom:entry elements MUST contain exactly one atom:updated element.
166 if (!empty($link['updated'])) {
167 $upDate = $link['updated'];
168 $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
169 } else {
170 $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);
171 }
172
173 // Save the more recent item.
174 if (empty($this->latestDate) || $this->latestDate < $pubDate) {
175 $this->latestDate = $pubDate;
176 }
177 if (!empty($upDate) && $this->latestDate < $upDate) {
178 $this->latestDate = $upDate;
179 }
180
181 $taglist = array_filter(explode(' ', $link['tags']), 'strlen');
182 uasort($taglist, 'strcasecmp');
183 $link['taglist'] = $taglist;
184
185 return $link;
186 }
187
188 /**
189 * Set this to true to use permalinks instead of direct links.
190 * 137 *
191 * @param boolean $usePermalinks true to force permalinks. 138 * @param boolean $usePermalinks true to force permalinks.
192 */ 139 */
@@ -216,21 +163,63 @@ class FeedBuilder
216 } 163 }
217 164
218 /** 165 /**
166 * Build a feed item (one per shaare).
167 *
168 * @param string $feedType Type of feed (RSS/ATOM).
169 * @param Bookmark $link Single link array extracted from LinkDB.
170 * @param string $pageaddr Index URL.
171 *
172 * @return array Link array with feed attributes.
173 */
174 protected function buildItem(string $feedType, $link, $pageaddr)
175 {
176 $data = $this->formatter->format($link);
177 $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl'];
178 if ($this->usePermalinks === true) {
179 $permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
180 } else {
181 $permalink = '<a href="'. $data['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
182 }
183 $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
184
185 $data['pub_iso_date'] = $this->getIsoDate($feedType, $data['created']);
186
187 // atom:entry elements MUST contain exactly one atom:updated element.
188 if (!empty($link->getUpdated())) {
189 $data['up_iso_date'] = $this->getIsoDate($feedType, $data['updated'], DateTime::ATOM);
190 } else {
191 $data['up_iso_date'] = $this->getIsoDate($feedType, $data['created'], DateTime::ATOM);
192 }
193
194 // Save the more recent item.
195 if (empty($this->latestDate) || $this->latestDate < $data['created']) {
196 $this->latestDate = $data['created'];
197 }
198 if (!empty($data['updated']) && $this->latestDate < $data['updated']) {
199 $this->latestDate = $data['updated'];
200 }
201
202 return $data;
203 }
204
205 /**
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);
@@ -273,23 +265,24 @@ class FeedBuilder
273 * Returns the number of link to display according to 'nb' user input parameter. 265 * Returns the number of link to display according to 'nb' user input parameter.
274 * 266 *
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 links (max parameter). 268 * If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
277 * 269 *
278 * @param int $max maximum number of links to display. 270 * @param int $max maximum number of bookmarks to display.
271 * @param array $userInput $_GET.
279 * 272 *
280 * @return int number of links 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
new file mode 100644
index 00000000..9d4a0fa0
--- /dev/null
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -0,0 +1,87 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5/**
6 * Class BookmarkDefaultFormatter
7 *
8 * Default bookmark formatter.
9 * Escape values for HTML display and automatically add link to URL and hashtags.
10 *
11 * @package Shaarli\Formatter
12 */
13class BookmarkDefaultFormatter extends BookmarkFormatter
14{
15 /**
16 * @inheritdoc
17 */
18 public function formatTitle($bookmark)
19 {
20 return escape($bookmark->getTitle());
21 }
22
23 /**
24 * @inheritdoc
25 */
26 public function formatDescription($bookmark)
27 {
28 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
29 return format_description(escape($bookmark->getDescription()), $indexUrl);
30 }
31
32 /**
33 * @inheritdoc
34 */
35 protected function formatTagList($bookmark)
36 {
37 return escape(parent::formatTagList($bookmark));
38 }
39
40 /**
41 * @inheritdoc
42 */
43 public function formatTagString($bookmark)
44 {
45 return implode(' ', $this->formatTagList($bookmark));
46 }
47
48 /**
49 * @inheritdoc
50 */
51 public function formatUrl($bookmark)
52 {
53 if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
54 return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
55 }
56
57 return escape($bookmark->getUrl());
58 }
59
60 /**
61 * @inheritdoc
62 */
63 protected function formatRealUrl($bookmark)
64 {
65 if ($bookmark->isNote()) {
66 if (isset($this->contextData['index_url'])) {
67 $prefix = rtrim($this->contextData['index_url'], '/') . '/';
68 }
69
70 if (isset($this->contextData['base_path'])) {
71 $prefix = rtrim($this->contextData['base_path'], '/') . '/';
72 }
73
74 return escape($prefix ?? '') . escape(ltrim($bookmark->getUrl(), '/'));
75 }
76
77 return escape($bookmark->getUrl());
78 }
79
80 /**
81 * @inheritdoc
82 */
83 protected function formatThumbnail($bookmark)
84 {
85 return escape($bookmark->getThumbnail());
86 }
87}
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php
new file mode 100644
index 00000000..0042dafe
--- /dev/null
+++ b/application/formatter/BookmarkFormatter.php
@@ -0,0 +1,313 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigManager;
8
9/**
10 * Class BookmarkFormatter
11 *
12 * Abstract class processing all bookmark attributes through methods designed to be overridden.
13 *
14 * @package Shaarli\Formatter
15 */
16abstract class BookmarkFormatter
17{
18 /**
19 * @var ConfigManager
20 */
21 protected $conf;
22
23 /** @var bool */
24 protected $isLoggedIn;
25
26 /**
27 * @var array Additional parameters than can be used for specific formatting
28 * e.g. index_url for Feed formatting
29 */
30 protected $contextData = [];
31
32 /**
33 * LinkDefaultFormatter constructor.
34 * @param ConfigManager $conf
35 */
36 public function __construct(ConfigManager $conf, bool $isLoggedIn)
37 {
38 $this->conf = $conf;
39 $this->isLoggedIn = $isLoggedIn;
40 }
41
42 /**
43 * Convert a Bookmark into an array usable by templates and plugins.
44 *
45 * All Bookmark attributes are formatted through a format method
46 * that can be overridden in a formatter extending this class.
47 *
48 * @param Bookmark $bookmark instance
49 *
50 * @return array formatted representation of a Bookmark
51 */
52 public function format($bookmark)
53 {
54 $out['id'] = $this->formatId($bookmark);
55 $out['shorturl'] = $this->formatShortUrl($bookmark);
56 $out['url'] = $this->formatUrl($bookmark);
57 $out['real_url'] = $this->formatRealUrl($bookmark);
58 $out['title'] = $this->formatTitle($bookmark);
59 $out['description'] = $this->formatDescription($bookmark);
60 $out['thumbnail'] = $this->formatThumbnail($bookmark);
61 $out['urlencoded_taglist'] = $this->formatUrlEncodedTagList($bookmark);
62 $out['taglist'] = $this->formatTagList($bookmark);
63 $out['urlencoded_tags'] = $this->formatUrlEncodedTagString($bookmark);
64 $out['tags'] = $this->formatTagString($bookmark);
65 $out['sticky'] = $bookmark->isSticky();
66 $out['private'] = $bookmark->isPrivate();
67 $out['class'] = $this->formatClass($bookmark);
68 $out['created'] = $this->formatCreated($bookmark);
69 $out['updated'] = $this->formatUpdated($bookmark);
70 $out['timestamp'] = $this->formatCreatedTimestamp($bookmark);
71 $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark);
72 return $out;
73 }
74
75 /**
76 * Add additional data available to formatters.
77 * This is used for example to add `index_url` in description's links.
78 *
79 * @param string $key Context data key
80 * @param string $value Context data value
81 */
82 public function addContextData($key, $value)
83 {
84 $this->contextData[$key] = $value;
85
86 return $this;
87 }
88
89 /**
90 * Format ID
91 *
92 * @param Bookmark $bookmark instance
93 *
94 * @return int formatted ID
95 */
96 protected function formatId($bookmark)
97 {
98 return $bookmark->getId();
99 }
100
101 /**
102 * Format ShortUrl
103 *
104 * @param Bookmark $bookmark instance
105 *
106 * @return string formatted ShortUrl
107 */
108 protected function formatShortUrl($bookmark)
109 {
110 return $bookmark->getShortUrl();
111 }
112
113 /**
114 * Format Url
115 *
116 * @param Bookmark $bookmark instance
117 *
118 * @return string formatted Url
119 */
120 protected function formatUrl($bookmark)
121 {
122 return $bookmark->getUrl();
123 }
124
125 /**
126 * Format RealUrl
127 * Legacy: identical to Url
128 *
129 * @param Bookmark $bookmark instance
130 *
131 * @return string formatted RealUrl
132 */
133 protected function formatRealUrl($bookmark)
134 {
135 return $this->formatUrl($bookmark);
136 }
137
138 /**
139 * Format Title
140 *
141 * @param Bookmark $bookmark instance
142 *
143 * @return string formatted Title
144 */
145 protected function formatTitle($bookmark)
146 {
147 return $bookmark->getTitle();
148 }
149
150 /**
151 * Format Description
152 *
153 * @param Bookmark $bookmark instance
154 *
155 * @return string formatted Description
156 */
157 protected function formatDescription($bookmark)
158 {
159 return $bookmark->getDescription();
160 }
161
162 /**
163 * Format Thumbnail
164 *
165 * @param Bookmark $bookmark instance
166 *
167 * @return string formatted Thumbnail
168 */
169 protected function formatThumbnail($bookmark)
170 {
171 return $bookmark->getThumbnail();
172 }
173
174 /**
175 * Format Tags
176 *
177 * @param Bookmark $bookmark instance
178 *
179 * @return array formatted Tags
180 */
181 protected function formatTagList($bookmark)
182 {
183 return $this->filterTagList($bookmark->getTags());
184 }
185
186 /**
187 * Format Url Encoded Tags
188 *
189 * @param Bookmark $bookmark instance
190 *
191 * @return array formatted Tags
192 */
193 protected function formatUrlEncodedTagList($bookmark)
194 {
195 return array_map('urlencode', $this->filterTagList($bookmark->getTags()));
196 }
197
198 /**
199 * Format TagString
200 *
201 * @param Bookmark $bookmark instance
202 *
203 * @return string formatted TagString
204 */
205 protected function formatTagString($bookmark)
206 {
207 return implode(' ', $this->formatTagList($bookmark));
208 }
209
210 /**
211 * Format TagString
212 *
213 * @param Bookmark $bookmark instance
214 *
215 * @return string formatted TagString
216 */
217 protected function formatUrlEncodedTagString($bookmark)
218 {
219 return implode(' ', $this->formatUrlEncodedTagList($bookmark));
220 }
221
222 /**
223 * Format Class
224 * Used to add specific CSS class for a link
225 *
226 * @param Bookmark $bookmark instance
227 *
228 * @return string formatted Class
229 */
230 protected function formatClass($bookmark)
231 {
232 return $bookmark->isPrivate() ? 'private' : '';
233 }
234
235 /**
236 * Format Created
237 *
238 * @param Bookmark $bookmark instance
239 *
240 * @return DateTime instance
241 */
242 protected function formatCreated(Bookmark $bookmark)
243 {
244 return $bookmark->getCreated();
245 }
246
247 /**
248 * Format Updated
249 *
250 * @param Bookmark $bookmark instance
251 *
252 * @return DateTime instance
253 */
254 protected function formatUpdated(Bookmark $bookmark)
255 {
256 return $bookmark->getUpdated();
257 }
258
259 /**
260 * Format CreatedTimestamp
261 *
262 * @param Bookmark $bookmark instance
263 *
264 * @return int formatted CreatedTimestamp
265 */
266 protected function formatCreatedTimestamp(Bookmark $bookmark)
267 {
268 if (! empty($bookmark->getCreated())) {
269 return $bookmark->getCreated()->getTimestamp();
270 }
271 return 0;
272 }
273
274 /**
275 * Format UpdatedTimestamp
276 *
277 * @param Bookmark $bookmark instance
278 *
279 * @return int formatted UpdatedTimestamp
280 */
281 protected function formatUpdatedTimestamp(Bookmark $bookmark)
282 {
283 if (! empty($bookmark->getUpdated())) {
284 return $bookmark->getUpdated()->getTimestamp();
285 }
286 return 0;
287 }
288
289 /**
290 * Format tag list, e.g. remove private tags if the user is not logged in.
291 *
292 * @param array $tags
293 *
294 * @return array
295 */
296 protected function filterTagList(array $tags): array
297 {
298 if ($this->isLoggedIn === true) {
299 return $tags;
300 }
301
302 $out = [];
303 foreach ($tags as $tag) {
304 if (strpos($tag, '.') === 0) {
305 continue;
306 }
307
308 $out[] = $tag;
309 }
310
311 return $out;
312 }
313}
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
new file mode 100644
index 00000000..5d244d4c
--- /dev/null
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -0,0 +1,206 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Class BookmarkMarkdownFormatter
9 *
10 * Format bookmark description into Markdown format.
11 *
12 * @package Shaarli\Formatter
13 */
14class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
15{
16 /**
17 * When this tag is present in a bookmark, its description should not be processed with Markdown
18 */
19 const NO_MD_TAG = 'nomarkdown';
20
21 /** @var \Parsedown instance */
22 protected $parsedown;
23
24 /** @var bool used to escape HTML in Markdown or not.
25 * It MUST be set to true for shared instance as HTML content can
26 * introduce XSS vulnerabilities.
27 */
28 protected $escape;
29
30 /**
31 * @var array List of allowed protocols for links inside bookmark's description.
32 */
33 protected $allowedProtocols;
34
35 /**
36 * LinkMarkdownFormatter constructor.
37 *
38 * @param ConfigManager $conf instance
39 * @param bool $isLoggedIn
40 */
41 public function __construct(ConfigManager $conf, bool $isLoggedIn)
42 {
43 parent::__construct($conf, $isLoggedIn);
44
45 $this->parsedown = new \Parsedown();
46 $this->escape = $conf->get('security.markdown_escape', true);
47 $this->allowedProtocols = $conf->get('security.allowed_protocols', []);
48 }
49
50 /**
51 * @inheritdoc
52 */
53 public function formatDescription($bookmark)
54 {
55 if (in_array(self::NO_MD_TAG, $bookmark->getTags())) {
56 return parent::formatDescription($bookmark);
57 }
58
59 $processedDescription = $bookmark->getDescription();
60 $processedDescription = $this->filterProtocols($processedDescription);
61 $processedDescription = $this->formatHashTags($processedDescription);
62 $processedDescription = $this->reverseEscapedHtml($processedDescription);
63 $processedDescription = $this->parsedown
64 ->setMarkupEscaped($this->escape)
65 ->setBreaksEnabled(true)
66 ->text($processedDescription);
67 $processedDescription = $this->sanitizeHtml($processedDescription);
68
69 if (!empty($processedDescription)) {
70 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
71 }
72
73 return $processedDescription;
74 }
75
76 /**
77 * Remove the NO markdown tag if it is present
78 *
79 * @inheritdoc
80 */
81 protected function formatTagList($bookmark)
82 {
83 $out = parent::formatTagList($bookmark);
84 if ($this->isLoggedIn === false && ($pos = array_search(self::NO_MD_TAG, $out)) !== false) {
85 unset($out[$pos]);
86 return array_values($out);
87 }
88 return $out;
89 }
90
91 /**
92 * Replace not whitelisted protocols with http:// in given description.
93 * Also adds `index_url` to relative links if it's specified
94 *
95 * @param string $description input description text.
96 *
97 * @return string $description without malicious link.
98 */
99 protected function filterProtocols($description)
100 {
101 $allowedProtocols = $this->allowedProtocols;
102 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
103
104 return preg_replace_callback(
105 '#]\((.*?)\)#is',
106 function ($match) use ($allowedProtocols, $indexUrl) {
107 $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : '';
108 $link .= whitelist_protocols($match[1], $allowedProtocols);
109 return ']('. $link.')';
110 },
111 $description
112 );
113 }
114
115 /**
116 * Replace hashtag in Markdown links format
117 * E.g. `#hashtag` becomes `[#hashtag](./add-tag/hashtag)`
118 * It includes the index URL if specified.
119 *
120 * @param string $description
121 *
122 * @return string
123 */
124 protected function formatHashTags($description)
125 {
126 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
127
128 /*
129 * To support unicode: http://stackoverflow.com/a/35498078/1484919
130 * \p{Pc} - to match underscore
131 * \p{N} - numeric character in any script
132 * \p{L} - letter from any language
133 * \p{Mn} - any non marking space (accents, umlauts, etc)
134 */
135 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
136 $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)';
137
138 $descriptionLines = explode(PHP_EOL, $description);
139 $descriptionOut = '';
140 $codeBlockOn = false;
141 $lineCount = 0;
142
143 foreach ($descriptionLines as $descriptionLine) {
144 // Detect line of code: starting with 4 spaces,
145 // except lists which can start with +/*/- or `2.` after spaces.
146 $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0;
147 // Detect and toggle block of code
148 if (!$codeBlockOn) {
149 $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0;
150 } elseif (preg_match('/^```/', $descriptionLine) > 0) {
151 $codeBlockOn = false;
152 }
153
154 if (!$codeBlockOn && !$codeLineOn) {
155 $descriptionLine = preg_replace($regex, $replacement, $descriptionLine);
156 }
157
158 $descriptionOut .= $descriptionLine;
159 if ($lineCount++ < count($descriptionLines) - 1) {
160 $descriptionOut .= PHP_EOL;
161 }
162 }
163
164 return $descriptionOut;
165 }
166
167 /**
168 * Remove dangerous HTML tags (tags, iframe, etc.).
169 * Doesn't affect <code> content (already escaped by Parsedown).
170 *
171 * @param string $description input description text.
172 *
173 * @return string given string escaped.
174 */
175 protected function sanitizeHtml($description)
176 {
177 $escapeTags = array(
178 'script',
179 'style',
180 'link',
181 'iframe',
182 'frameset',
183 'frame',
184 );
185 foreach ($escapeTags as $tag) {
186 $description = preg_replace_callback(
187 '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is',
188 function ($match) {
189 return escape($match[0]);
190 },
191 $description
192 );
193 }
194 $description = preg_replace(
195 '#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is',
196 '$1',
197 $description
198 );
199 return $description;
200 }
201
202 protected function reverseEscapedHtml($description)
203 {
204 return unescape($description);
205 }
206}
diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php
new file mode 100644
index 00000000..bc372273
--- /dev/null
+++ b/application/formatter/BookmarkRawFormatter.php
@@ -0,0 +1,13 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5/**
6 * Class BookmarkRawFormatter
7 *
8 * Used to retrieve bookmarks as array with raw values.
9 * Warning: Do NOT use this for HTML content as it can introduce XSS vulnerabilities.
10 *
11 * @package Shaarli\Formatter
12 */
13class BookmarkRawFormatter extends BookmarkFormatter {}
diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php
new file mode 100644
index 00000000..a029579f
--- /dev/null
+++ b/application/formatter/FormatterFactory.php
@@ -0,0 +1,51 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Class FormatterFactory
9 *
10 * Helper class used to instantiate the proper BookmarkFormatter.
11 *
12 * @package Shaarli\Formatter
13 */
14class FormatterFactory
15{
16 /** @var ConfigManager instance */
17 protected $conf;
18
19 /** @var bool */
20 protected $isLoggedIn;
21
22 /**
23 * FormatterFactory constructor.
24 *
25 * @param ConfigManager $conf
26 * @param bool $isLoggedIn
27 */
28 public function __construct(ConfigManager $conf, bool $isLoggedIn)
29 {
30 $this->conf = $conf;
31 $this->isLoggedIn = $isLoggedIn;
32 }
33
34 /**
35 * Instanciate a BookmarkFormatter depending on the configuration or provided formatter type.
36 *
37 * @param string|null $type force a specific type regardless of the configuration
38 *
39 * @return BookmarkFormatter instance.
40 */
41 public function getFormatter(string $type = null): BookmarkFormatter
42 {
43 $type = $type ? $type : $this->conf->get('formatter', 'default');
44 $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter';
45 if (!class_exists($className)) {
46 $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter';
47 }
48
49 return new $className($this->conf, $this->isLoggedIn);
50 }
51}
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
new file mode 100644
index 00000000..d1aa1399
--- /dev/null
+++ b/application/front/ShaarliMiddleware.php
@@ -0,0 +1,114 @@
1<?php
2
3namespace Shaarli\Front;
4
5use Shaarli\Container\ShaarliContainer;
6use Shaarli\Front\Exception\UnauthorizedException;
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Class ShaarliMiddleware
12 *
13 * This will be called before accessing any Shaarli controller.
14 */
15class ShaarliMiddleware
16{
17 /** @var ShaarliContainer contains all Shaarli DI */
18 protected $container;
19
20 public function __construct(ShaarliContainer $container)
21 {
22 $this->container = $container;
23 }
24
25 /**
26 * Middleware execution:
27 * - run updates
28 * - if not logged in open shaarli, redirect to login
29 * - execute the controller
30 * - return the response
31 *
32 * In case of error, the error template will be displayed with the exception message.
33 *
34 * @param Request $request Slim request
35 * @param Response $response Slim response
36 * @param callable $next Next action
37 *
38 * @return Response response.
39 */
40 public function __invoke(Request $request, Response $response, callable $next): Response
41 {
42 $this->initBasePath($request);
43
44 try {
45 if (!is_file($this->container->conf->getConfigFileExt())
46 && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
47 ) {
48 return $response->withRedirect($this->container->basePath . '/install');
49 }
50
51 $this->runUpdates();
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();
100 }
101
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 }
113 }
114}
diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php
new file mode 100644
index 00000000..e675fcca
--- /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']);
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/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php
new file mode 100644
index 00000000..bb083486
--- /dev/null
+++ b/application/front/controller/admin/ManageShaareController.php
@@ -0,0 +1,371 @@
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\BookmarkMarkdownFormatter;
10use Shaarli\Render\TemplatePage;
11use Shaarli\Thumbnailer;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15/**
16 * Class PostBookmarkController
17 *
18 * Slim controller used to handle Shaarli create or edit bookmarks.
19 */
20class ManageShaareController extends ShaarliAdminController
21{
22 /**
23 * GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL
24 */
25 public function addShaare(Request $request, Response $response): Response
26 {
27 $this->assignView(
28 'pagetitle',
29 t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli')
30 );
31
32 return $response->write($this->render(TemplatePage::ADDLINK));
33 }
34
35 /**
36 * GET /admin/shaare - Displays the bookmark form for creation.
37 * Note that if the URL is found in existing bookmarks, then it will be in edit mode.
38 */
39 public function displayCreateForm(Request $request, Response $response): Response
40 {
41 $url = cleanup_url($request->getParam('post'));
42
43 $linkIsNew = false;
44 // Check if URL is not already in database (in this case, we will edit the existing link)
45 $bookmark = $this->container->bookmarkService->findByUrl($url);
46 if (null === $bookmark) {
47 $linkIsNew = true;
48 // Get shaare data if it was provided in URL (e.g.: by the bookmarklet).
49 $title = $request->getParam('title');
50 $description = $request->getParam('description');
51 $tags = $request->getParam('tags');
52 $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN);
53
54 // If this is an HTTP(S) link, we try go get the page to extract
55 // the title (otherwise we will to straight to the edit form.)
56 if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
57 $retrieveDescription = $this->container->conf->get('general.retrieve_description');
58 // Short timeout to keep the application responsive
59 // The callback will fill $charset and $title with data from the downloaded page.
60 $this->container->httpAccess->getHttpResponse(
61 $url,
62 $this->container->conf->get('general.download_timeout', 30),
63 $this->container->conf->get('general.download_max_size', 4194304),
64 $this->container->httpAccess->getCurlDownloadCallback(
65 $charset,
66 $title,
67 $description,
68 $tags,
69 $retrieveDescription
70 )
71 );
72 if (! empty($title) && strtolower($charset) !== 'utf-8' && mb_check_encoding($charset)) {
73 $title = mb_convert_encoding($title, 'utf-8', $charset);
74 }
75 }
76
77 if (empty($url) && empty($title)) {
78 $title = $this->container->conf->get('general.default_note_title', t('Note: '));
79 }
80
81 $link = [
82 'title' => $title,
83 'url' => $url ?? '',
84 'description' => $description ?? '',
85 'tags' => $tags ?? '',
86 'private' => $private,
87 ];
88 } else {
89 $formatter = $this->container->formatterFactory->getFormatter('raw');
90 $link = $formatter->format($bookmark);
91 }
92
93 return $this->displayForm($link, $linkIsNew, $request, $response);
94 }
95
96 /**
97 * GET /admin/shaare/{id} - Displays the bookmark form in edition mode.
98 */
99 public function displayEditForm(Request $request, Response $response, array $args): Response
100 {
101 $id = $args['id'] ?? '';
102 try {
103 if (false === ctype_digit($id)) {
104 throw new BookmarkNotFoundException();
105 }
106 $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
107 } catch (BookmarkNotFoundException $e) {
108 $this->saveErrorMessage(sprintf(
109 t('Bookmark with identifier %s could not be found.'),
110 $id
111 ));
112
113 return $this->redirect($response, '/');
114 }
115
116 $formatter = $this->container->formatterFactory->getFormatter('raw');
117 $link = $formatter->format($bookmark);
118
119 return $this->displayForm($link, false, $request, $response);
120 }
121
122 /**
123 * POST /admin/shaare
124 */
125 public function save(Request $request, Response $response): Response
126 {
127 $this->checkToken($request);
128
129 // lf_id should only be present if the link exists.
130 $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null;
131 if (null !== $id && true === $this->container->bookmarkService->exists($id)) {
132 // Edit
133 $bookmark = $this->container->bookmarkService->get($id);
134 } else {
135 // New link
136 $bookmark = new Bookmark();
137 }
138
139 $bookmark->setTitle($request->getParam('lf_title'));
140 $bookmark->setDescription($request->getParam('lf_description'));
141 $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', []));
142 $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN));
143 $bookmark->setTagsString($request->getParam('lf_tags'));
144
145 if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
146 && false === $bookmark->isNote()
147 ) {
148 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
149 }
150 $this->container->bookmarkService->addOrSet($bookmark, false);
151
152 // To preserve backward compatibility with 3rd parties, plugins still use arrays
153 $formatter = $this->container->formatterFactory->getFormatter('raw');
154 $data = $formatter->format($bookmark);
155 $this->executePageHooks('save_link', $data);
156
157 $bookmark->fromArray($data);
158 $this->container->bookmarkService->set($bookmark);
159
160 // If we are called from the bookmarklet, we must close the popup:
161 if ($request->getParam('source') === 'bookmarklet') {
162 return $response->write('<script>self.close();</script>');
163 }
164
165 if (!empty($request->getParam('returnurl'))) {
166 $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl'));
167 }
168
169 return $this->redirectFromReferer(
170 $request,
171 $response,
172 ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'],
173 $bookmark->getShortUrl()
174 );
175 }
176
177 /**
178 * GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter).
179 */
180 public function deleteBookmark(Request $request, Response $response): Response
181 {
182 $this->checkToken($request);
183
184 $ids = escape(trim($request->getParam('id') ?? ''));
185 if (empty($ids) || strpos($ids, ' ') !== false) {
186 // multiple, space-separated ids provided
187 $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
188 } else {
189 $ids = [$ids];
190 }
191
192 // assert at least one id is given
193 if (0 === count($ids)) {
194 $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
195
196 return $this->redirectFromReferer($request, $response, [], ['delete-shaare']);
197 }
198
199 $formatter = $this->container->formatterFactory->getFormatter('raw');
200 $count = 0;
201 foreach ($ids as $id) {
202 try {
203 $bookmark = $this->container->bookmarkService->get((int) $id);
204 } catch (BookmarkNotFoundException $e) {
205 $this->saveErrorMessage(sprintf(
206 t('Bookmark with identifier %s could not be found.'),
207 $id
208 ));
209
210 continue;
211 }
212
213 $data = $formatter->format($bookmark);
214 $this->executePageHooks('delete_link', $data);
215 $this->container->bookmarkService->remove($bookmark, false);
216 ++ $count;
217 }
218
219 if ($count > 0) {
220 $this->container->bookmarkService->save();
221 }
222
223 // If we are called from the bookmarklet, we must close the popup:
224 if ($request->getParam('source') === 'bookmarklet') {
225 return $response->write('<script>self.close();</script>');
226 }
227
228 // Don't redirect to where we were previously because the datastore has changed.
229 return $this->redirect($response, '/');
230 }
231
232 /**
233 * GET /admin/shaare/visibility
234 *
235 * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter).
236 */
237 public function changeVisibility(Request $request, Response $response): Response
238 {
239 $this->checkToken($request);
240
241 $ids = trim(escape($request->getParam('id') ?? ''));
242 if (empty($ids) || strpos($ids, ' ') !== false) {
243 // multiple, space-separated ids provided
244 $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
245 } else {
246 // only a single id provided
247 $ids = [$ids];
248 }
249
250 // assert at least one id is given
251 if (0 === count($ids)) {
252 $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
253
254 return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
255 }
256
257 // assert that the visibility is valid
258 $visibility = $request->getParam('newVisibility');
259 if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) {
260 $this->saveErrorMessage(t('Invalid visibility provided.'));
261
262 return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
263 } else {
264 $isPrivate = $visibility === 'private';
265 }
266
267 $formatter = $this->container->formatterFactory->getFormatter('raw');
268 $count = 0;
269
270 foreach ($ids as $id) {
271 try {
272 $bookmark = $this->container->bookmarkService->get((int) $id);
273 } catch (BookmarkNotFoundException $e) {
274 $this->saveErrorMessage(sprintf(
275 t('Bookmark with identifier %s could not be found.'),
276 $id
277 ));
278
279 continue;
280 }
281
282 $bookmark->setPrivate($isPrivate);
283
284 // To preserve backward compatibility with 3rd parties, plugins still use arrays
285 $data = $formatter->format($bookmark);
286 $this->executePageHooks('save_link', $data);
287 $bookmark->fromArray($data);
288
289 $this->container->bookmarkService->set($bookmark, false);
290 ++$count;
291 }
292
293 if ($count > 0) {
294 $this->container->bookmarkService->save();
295 }
296
297 return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']);
298 }
299
300 /**
301 * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark.
302 */
303 public function pinBookmark(Request $request, Response $response, array $args): Response
304 {
305 $this->checkToken($request);
306
307 $id = $args['id'] ?? '';
308 try {
309 if (false === ctype_digit($id)) {
310 throw new BookmarkNotFoundException();
311 }
312 $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
313 } catch (BookmarkNotFoundException $e) {
314 $this->saveErrorMessage(sprintf(
315 t('Bookmark with identifier %s could not be found.'),
316 $id
317 ));
318
319 return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
320 }
321
322 $formatter = $this->container->formatterFactory->getFormatter('raw');
323
324 $bookmark->setSticky(!$bookmark->isSticky());
325
326 // To preserve backward compatibility with 3rd parties, plugins still use arrays
327 $data = $formatter->format($bookmark);
328 $this->executePageHooks('save_link', $data);
329 $bookmark->fromArray($data);
330
331 $this->container->bookmarkService->set($bookmark);
332
333 return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
334 }
335
336 /**
337 * Helper function used to display the shaare form whether it's a new or existing bookmark.
338 *
339 * @param array $link data used in template, either from parameters or from the data store
340 */
341 protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response
342 {
343 $tags = $this->container->bookmarkService->bookmarksCountPerTag();
344 if ($this->container->conf->get('formatter') === 'markdown') {
345 $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
346 }
347
348 $data = escape([
349 'link' => $link,
350 'link_is_new' => $isNew,
351 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
352 'source' => $request->getParam('source') ?? '',
353 'tags' => $tags,
354 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
355 ]);
356
357 $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
358
359 foreach ($data as $key => $value) {
360 $this->assignView($key, $value);
361 }
362
363 $editLabel = false === $isNew ? t('Edit') .' ' : '';
364 $this->assignView(
365 'pagetitle',
366 $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli')
367 );
368
369 return $response->write($this->render(TemplatePage::EDIT_LINK));
370 }
371}
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/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/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/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..81c87ed0
--- /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($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..18368751
--- /dev/null
+++ b/application/front/controller/visitor/BookmarkListController.php
@@ -0,0 +1,241 @@
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 try {
141 $bookmark = $this->container->bookmarkService->findByHash($args['hash']);
142 } catch (BookmarkNotFoundException $e) {
143 $this->assignView('error_message', $e->getMessage());
144
145 return $response->write($this->render(TemplatePage::ERROR_404));
146 }
147
148 $this->updateThumbnail($bookmark);
149
150 $formatter = $this->container->formatterFactory->getFormatter();
151 $formatter->addContextData('base_path', $this->container->basePath);
152
153 $data = array_merge(
154 $this->initializeTemplateVars(),
155 [
156 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'),
157 'links' => [$formatter->format($bookmark)],
158 ]
159 );
160
161 $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
162 $this->assignAllView($data);
163
164 return $response->write($this->render(TemplatePage::LINKLIST));
165 }
166
167 /**
168 * Update the thumbnail of a single bookmark if necessary.
169 */
170 protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
171 {
172 // Logged in, thumbnails enabled, not a note, is HTTP
173 // and (never retrieved yet or no valid cache file)
174 if ($this->container->loginManager->isLoggedIn()
175 && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
176 && false !== $bookmark->getThumbnail()
177 && !$bookmark->isNote()
178 && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail()))
179 && startsWith(strtolower($bookmark->getUrl()), 'http')
180 ) {
181 $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
182 $this->container->bookmarkService->set($bookmark, $writeDatastore);
183
184 return true;
185 }
186
187 return false;
188 }
189
190 /**
191 * @return string[] Default template variables without values.
192 */
193 protected function initializeTemplateVars(): array
194 {
195 return [
196 'previous_page_url' => '',
197 'next_page_url' => '',
198 'page_max' => '',
199 'search_tags' => '',
200 'result_count' => '',
201 ];
202 }
203
204 /**
205 * Process legacy routes if necessary. They used query parameters.
206 * If no legacy routes is passed, return null.
207 */
208 protected function processLegacyController(Request $request, Response $response): ?Response
209 {
210 // Legacy smallhash filter
211 $queryString = $this->container->environment['QUERY_STRING'] ?? null;
212 if (null !== $queryString && 1 === preg_match('/^([a-zA-Z0-9-_@]{6})($|&|#)/', $queryString, $match)) {
213 return $this->redirect($response, '/shaare/' . $match[1]);
214 }
215
216 // Legacy controllers (mostly used for redirections)
217 if (null !== $request->getQueryParam('do')) {
218 $legacyController = new LegacyController($this->container);
219
220 try {
221 return $legacyController->process($request, $response, $request->getQueryParam('do'));
222 } catch (UnknowLegacyRouteException $e) {
223 // We ignore legacy 404
224 return null;
225 }
226 }
227
228 // Legacy GET admin routes
229 $legacyGetRoutes = array_intersect(
230 LegacyController::LEGACY_GET_ROUTES,
231 array_keys($request->getQueryParams() ?? [])
232 );
233 if (1 === count($legacyGetRoutes)) {
234 $legacyController = new LegacyController($this->container);
235
236 return $legacyController->process($request, $response, $legacyGetRoutes[0]);
237 }
238
239 return null;
240 }
241}
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php
new file mode 100644
index 00000000..07617cf1
--- /dev/null
+++ b/application/front/controller/visitor/DailyController.php
@@ -0,0 +1,192 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use DateTime;
8use DateTimeImmutable;
9use Shaarli\Bookmark\Bookmark;
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 $day = $request->getQueryParam('day') ?? date('Ymd');
30
31 $availableDates = $this->container->bookmarkService->days();
32 $nbAvailableDates = count($availableDates);
33 $index = array_search($day, $availableDates);
34
35 if ($index === false) {
36 // no bookmarks for day, but at least one day with bookmarks
37 $day = $availableDates[$nbAvailableDates - 1] ?? $day;
38 $previousDay = $availableDates[$nbAvailableDates - 2] ?? '';
39 } else {
40 $previousDay = $availableDates[$index - 1] ?? '';
41 $nextDay = $availableDates[$index + 1] ?? '';
42 }
43
44 if ($day === date('Ymd')) {
45 $this->assignView('dayDesc', t('Today'));
46 } elseif ($day === date('Ymd', strtotime('-1 days'))) {
47 $this->assignView('dayDesc', t('Yesterday'));
48 }
49
50 try {
51 $linksToDisplay = $this->container->bookmarkService->filterDay($day);
52 } catch (\Exception $exc) {
53 $linksToDisplay = [];
54 }
55
56 $formatter = $this->container->formatterFactory->getFormatter();
57 $formatter->addContextData('base_path', $this->container->basePath);
58 // We pre-format some fields for proper output.
59 foreach ($linksToDisplay as $key => $bookmark) {
60 $linksToDisplay[$key] = $formatter->format($bookmark);
61 // This page is a bit specific, we need raw description to calculate the length
62 $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
63 $linksToDisplay[$key]['description'] = $bookmark->getDescription();
64 }
65
66 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
67 $data = [
68 'linksToDisplay' => $linksToDisplay,
69 'day' => $dayDate->getTimestamp(),
70 'dayDate' => $dayDate,
71 'previousday' => $previousDay ?? '',
72 'nextday' => $nextDay ?? '',
73 ];
74
75 // Hooks are called before column construction so that plugins don't have to deal with columns.
76 $this->executePageHooks('render_daily', $data, TemplatePage::DAILY);
77
78 $data['cols'] = $this->calculateColumns($data['linksToDisplay']);
79
80 $this->assignAllView($data);
81
82 $mainTitle = $this->container->conf->get('general.title', 'Shaarli');
83 $this->assignView(
84 'pagetitle',
85 t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle
86 );
87
88 return $response->write($this->render(TemplatePage::DAILY));
89 }
90
91 /**
92 * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
93 * Gives the last 7 days (which have bookmarks).
94 * This RSS feed cannot be filtered and does not trigger plugins yet.
95 */
96 public function rss(Request $request, Response $response): Response
97 {
98 $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
99
100 $pageUrl = page_url($this->container->environment);
101 $cache = $this->container->pageCacheManager->getCachePage($pageUrl);
102
103 $cached = $cache->cachedVersion();
104 if (!empty($cached)) {
105 return $response->write($cached);
106 }
107
108 $days = [];
109 foreach ($this->container->bookmarkService->search() as $bookmark) {
110 $day = $bookmark->getCreated()->format('Ymd');
111
112 // Stop iterating after DAILY_RSS_NB_DAYS entries
113 if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) {
114 break;
115 }
116
117 $days[$day][] = $bookmark;
118 }
119
120 // Build the RSS feed.
121 $indexUrl = escape(index_url($this->container->environment));
122
123 $formatter = $this->container->formatterFactory->getFormatter();
124 $formatter->addContextData('index_url', $indexUrl);
125
126 $dataPerDay = [];
127
128 /** @var Bookmark[] $bookmarks */
129 foreach ($days as $day => $bookmarks) {
130 $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
131 $dataPerDay[$day] = [
132 'date' => $dayDatetime,
133 'date_rss' => $dayDatetime->format(DateTime::RSS),
134 'date_human' => format_date($dayDatetime, false, true),
135 'absolute_url' => $indexUrl . 'daily?day=' . $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'] = $indexUrl . $bookmark->getUrl();
145 }
146 }
147 }
148
149 $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli'));
150 $this->assignView('index_url', $indexUrl);
151 $this->assignView('page_url', $pageUrl);
152 $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false));
153 $this->assignView('days', $dataPerDay);
154
155 $rssContent = $this->render(TemplatePage::DAILY_RSS);
156
157 $cache->cache($rssContent);
158
159 return $response->write($rssContent);
160 }
161
162 /**
163 * We need to spread the articles on 3 columns.
164 * did not want to use a JavaScript lib like http://masonry.desandro.com/
165 * so I manually spread entries with a simple method: I roughly evaluate the
166 * height of a div according to title and description length.
167 */
168 protected function calculateColumns(array $links): array
169 {
170 // Entries to display, for each column.
171 $columns = [[], [], []];
172 // Rough estimate of columns fill.
173 $fill = [0, 0, 0];
174 foreach ($links as $link) {
175 // Roughly estimate length of entry (by counting characters)
176 // Title: 30 chars = 1 line. 1 line is 30 pixels height.
177 // Description: 836 characters gives roughly 342 pixel height.
178 // This is not perfect, but it's usually OK.
179 $length = strlen($link['title'] ?? '') + (342 * strlen($link['description'] ?? '')) / 836;
180 if (! empty($link['thumbnail'])) {
181 $length += 100; // 1 thumbnails roughly takes 100 pixels height.
182 }
183 // Then put in column which is the less filled:
184 $smallest = min($fill); // find smallest value in array.
185 $index = array_search($smallest, $fill); // find index of this smallest value.
186 array_push($columns[$index], $link); // Put entry in this column.
187 $fill[$index] += $length;
188 }
189
190 return $columns;
191 }
192}
diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php
new file mode 100644
index 00000000..10aa84c8
--- /dev/null
+++ b/application/front/controller/visitor/ErrorController.php
@@ -0,0 +1,45 @@
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(
32 'stacktrace',
33 nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString())
34 );
35 } else {
36 $this->assignView('message', t('An unexpected error occurred.'));
37 }
38
39 $response = $response->withStatus(500);
40 }
41
42
43 return $response->write($this->render('error'));
44 }
45}
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..7cb32777
--- /dev/null
+++ b/application/front/controller/visitor/InstallController.php
@@ -0,0 +1,165 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\ApplicationUtils;
8use Shaarli\Container\ShaarliContainer;
9use Shaarli\Front\Exception\AlreadyInstalledException;
10use Shaarli\Front\Exception\ResourcePermissionException;
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 return $response->write($this->render('install'));
57 }
58
59 /**
60 * Route checking that the session parameter has been properly saved between two distinct requests.
61 * If the session parameter is preserved, redirect to install template page, otherwise displays error.
62 */
63 public function sessionTest(Request $request, Response $response): Response
64 {
65 // This part makes sure sessions works correctly.
66 // (Because on some hosts, session.save_path may not be set correctly,
67 // or we may not have write access to it.)
68 if (static::SESSION_TEST_VALUE
69 !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
70 ) {
71 // Step 2: Check if data in session is correct.
72 $msg = t(
73 '<pre>Sessions do not seem to work correctly on your server.<br>'.
74 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
75 'and that you have write access to it.<br>'.
76 'It currently points to %s.<br>'.
77 'On some browsers, accessing your server via a hostname like \'localhost\' '.
78 'or any custom hostname without a dot causes cookie storage to fail. '.
79 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
80 );
81 $msg = sprintf($msg, $this->container->sessionManager->getSavePath());
82
83 $this->assignView('message', $msg);
84
85 return $response->write($this->render('error'));
86 }
87
88 return $this->redirect($response, '/install');
89 }
90
91 /**
92 * Save installation form and initialize config file and datastore if necessary.
93 */
94 public function save(Request $request, Response $response): Response
95 {
96 $timezone = 'UTC';
97 if (!empty($request->getParam('continent'))
98 && !empty($request->getParam('city'))
99 && isTimeZoneValid($request->getParam('continent'), $request->getParam('city'))
100 ) {
101 $timezone = $request->getParam('continent') . '/' . $request->getParam('city');
102 }
103 $this->container->conf->set('general.timezone', $timezone);
104
105 $login = $request->getParam('setlogin');
106 $this->container->conf->set('credentials.login', $login);
107 $salt = sha1(uniqid('', true) .'_'. mt_rand());
108 $this->container->conf->set('credentials.salt', $salt);
109 $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt));
110
111 if (!empty($request->getParam('title'))) {
112 $this->container->conf->set('general.title', escape($request->getParam('title')));
113 } else {
114 $this->container->conf->set(
115 'general.title',
116 'Shared bookmarks on '.escape(index_url($this->container->environment))
117 );
118 }
119
120 $this->container->conf->set('translation.language', escape($request->getParam('language')));
121 $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
122 $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
123 $this->container->conf->set(
124 'api.secret',
125 generate_api_secret(
126 $this->container->conf->get('credentials.login'),
127 $this->container->conf->get('credentials.salt')
128 )
129 );
130 $this->container->conf->set('general.header_link', $this->container->basePath . '/');
131
132 try {
133 // Everything is ok, let's create config file.
134 $this->container->conf->write($this->container->loginManager->isLoggedIn());
135 } catch (\Exception $e) {
136 $this->assignView('message', t('Error while writing config file after configuration update.'));
137 $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString());
138
139 return $response->write($this->render('error'));
140 }
141
142 $this->container->sessionManager->setSessionParameter(
143 SessionManager::KEY_SUCCESS_MESSAGES,
144 [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')]
145 );
146
147 return $this->redirect($response, '/login');
148 }
149
150 protected function checkPermissions(): bool
151 {
152 // Ensure Shaarli has proper access to its resources
153 $errors = ApplicationUtils::checkResourcePermissions($this->container->conf);
154 if (empty($errors)) {
155 return true;
156 }
157
158 $message = t('Insufficient permissions:') . PHP_EOL;
159 foreach ($errors as $error) {
160 $message .= PHP_EOL . $error;
161 }
162
163 throw new ResourcePermissionException($message);
164 }
165}
diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php
new file mode 100644
index 00000000..121ba40b
--- /dev/null
+++ b/application/front/controller/visitor/LoginController.php
@@ -0,0 +1,154 @@
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 $this->container->environment['REMOTE_ADDR'],
69 client_ip_id($this->container->environment),
70 $request->getParam('login'),
71 $request->getParam('password')
72 )
73 ) {
74 $this->container->loginManager->handleFailedLogin($this->container->environment);
75
76 $this->container->sessionManager->setSessionParameter(
77 SessionManager::KEY_ERROR_MESSAGES,
78 [t('Wrong login/password.')]
79 );
80
81 // Call controller directly instead of unnecessary redirection
82 return $this->index($request, $response);
83 }
84
85 $this->container->loginManager->handleSuccessfulLogin($this->container->environment);
86
87 $cookiePath = $this->container->basePath . '/';
88 $expirationTime = $this->saveLongLastingSession($request, $cookiePath);
89 $this->renewUserSession($cookiePath, $expirationTime);
90
91 // Force referer from given return URL
92 $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
93
94 return $this->redirectFromReferer($request, $response, ['login', 'install']);
95 }
96
97 /**
98 * Make sure that the user is allowed to login and/or displaying the login page:
99 * - not already logged in
100 * - not open shaarli
101 * - not banned
102 */
103 protected function checkLoginState(): bool
104 {
105 if ($this->container->loginManager->isLoggedIn()
106 || $this->container->conf->get('security.open_shaarli', false)
107 ) {
108 throw new CantLoginException();
109 }
110
111 if (true !== $this->container->loginManager->canLogin($this->container->environment)) {
112 throw new LoginBannedException();
113 }
114
115 return true;
116 }
117
118 /**
119 * @return int Session duration in seconds
120 */
121 protected function saveLongLastingSession(Request $request, string $cookiePath): int
122 {
123 if (empty($request->getParam('longlastingsession'))) {
124 // Standard session expiration (=when browser closes)
125 $expirationTime = 0;
126 } else {
127 // Keep the session cookie even after the browser closes
128 $this->container->sessionManager->setStaySignedIn(true);
129 $expirationTime = $this->container->sessionManager->extendSession();
130 }
131
132 $this->container->cookieManager->setCookieParameter(
133 CookieManager::STAY_SIGNED_IN,
134 $this->container->loginManager->getStaySignedInToken(),
135 $expirationTime,
136 $cookiePath
137 );
138
139 return $expirationTime;
140 }
141
142 protected function renewUserSession(string $cookiePath, int $expirationTime): void
143 {
144 // Send cookie with the new expiration date to the browser
145 $this->container->sessionManager->destroy();
146 $this->container->sessionManager->cookieParameters(
147 $expirationTime,
148 $cookiePath,
149 $this->container->environment['SERVER_NAME']
150 );
151 $this->container->sessionManager->start();
152 $this->container->sessionManager->regenerateId(true);
153 }
154}
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..55c075a2
--- /dev/null
+++ b/application/front/controller/visitor/ShaarliVisitorController.php
@@ -0,0 +1,180 @@
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 'bookmarkService' => $this->container->bookmarkService
110 ];
111 }
112
113 /**
114 * Simple helper which prepend the base path to redirect path.
115 *
116 * @param Response $response
117 * @param string $path Absolute path, e.g.: `/`, or `/admin/shaare/123` regardless of install directory
118 *
119 * @return Response updated
120 */
121 protected function redirect(Response $response, string $path): Response
122 {
123 return $response->withRedirect($this->container->basePath . $path);
124 }
125
126 /**
127 * Generates a redirection to the previous page, based on the HTTP_REFERER.
128 * It fails back to the home page.
129 *
130 * @param array $loopTerms Terms to remove from path and query string to prevent direction loop.
131 * @param array $clearParams List of parameter to remove from the query string of the referrer.
132 */
133 protected function redirectFromReferer(
134 Request $request,
135 Response $response,
136 array $loopTerms = [],
137 array $clearParams = [],
138 string $anchor = null
139 ): Response {
140 $defaultPath = $this->container->basePath . '/';
141 $referer = $this->container->environment['HTTP_REFERER'] ?? null;
142
143 if (null !== $referer) {
144 $currentUrl = parse_url($referer);
145 // If the referer is not related to Shaarli instance, redirect to default
146 if (isset($currentUrl['host'])
147 && strpos(index_url($this->container->environment), $currentUrl['host']) === false
148 ) {
149 return $response->withRedirect($defaultPath);
150 }
151
152 parse_str($currentUrl['query'] ?? '', $params);
153 $path = $currentUrl['path'] ?? $defaultPath;
154 } else {
155 $params = [];
156 $path = $defaultPath;
157 }
158
159 // Prevent redirection loop
160 if (isset($currentUrl)) {
161 foreach ($clearParams as $value) {
162 unset($params[$value]);
163 }
164
165 $checkQuery = implode('', array_keys($params));
166 foreach ($loopTerms as $value) {
167 if (strpos($path . $checkQuery, $value) !== false) {
168 $params = [];
169 $path = $defaultPath;
170 break;
171 }
172 }
173 }
174
175 $queryString = count($params) > 0 ? '?'. http_build_query($params) : '';
176 $anchor = $anchor ? '#' . $anchor : '';
177
178 return $response->withRedirect($path . $queryString . $anchor);
179 }
180}
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/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
new file mode 100644
index 00000000..79d0ea15
--- /dev/null
+++ b/application/front/exceptions/LoginBannedException.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class LoginBannedException extends ShaarliFrontException
8{
9 public function __construct()
10 {
11 $message = t('You have been banned after too many failed login attempts. Try again later.');
12
13 parent::__construct($message, 401);
14 }
15}
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/ShaarliFrontException.php b/application/front/exceptions/ShaarliFrontException.php
new file mode 100644
index 00000000..73847e6d
--- /dev/null
+++ b/application/front/exceptions/ShaarliFrontException.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7use Throwable;
8
9/**
10 * Class ShaarliException
11 *
12 * Exception class used to defined any custom exception thrown during front rendering.
13 *
14 * @package Front\Exception
15 */
16class ShaarliFrontException extends \Exception
17{
18 /** Override parent constructor to force $message and $httpCode parameters to be set. */
19 public function __construct(string $message, int $httpCode, Throwable $previous = null)
20 {
21 parent::__construct($message, $httpCode, $previous);
22 }
23}
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/http/HttpAccess.php b/application/http/HttpAccess.php
new file mode 100644
index 00000000..81d9e076
--- /dev/null
+++ b/application/http/HttpAccess.php
@@ -0,0 +1,39 @@
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($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
18 {
19 return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction);
20 }
21
22 public function getCurlDownloadCallback(
23 &$charset,
24 &$title,
25 &$description,
26 &$keywords,
27 $retrieveDescription,
28 $curlGetInfo = 'curl_getinfo'
29 ) {
30 return get_curl_download_callback(
31 $charset,
32 $title,
33 $description,
34 $keywords,
35 $retrieveDescription,
36 $curlGetInfo
37 );
38 }
39}
diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php
index 2ea9195d..9f414073 100644
--- a/application/http/HttpUtils.php
+++ b/application/http/HttpUtils.php
@@ -369,7 +369,11 @@ function server_url($server)
369 */ 369 */
370function index_url($server) 370function index_url($server)
371{ 371{
372 $scriptname = $server['SCRIPT_NAME']; 372 if (defined('SHAARLI_ROOT_URL') && null !== SHAARLI_ROOT_URL) {
373 return rtrim(SHAARLI_ROOT_URL, '/') . '/';
374 }
375
376 $scriptname = !empty($server['SCRIPT_NAME']) ? $server['SCRIPT_NAME'] : '/';
373 if (endsWith($scriptname, 'index.php')) { 377 if (endsWith($scriptname, 'index.php')) {
374 $scriptname = substr($scriptname, 0, -9); 378 $scriptname = substr($scriptname, 0, -9);
375 } 379 }
@@ -377,7 +381,7 @@ function index_url($server)
377} 381}
378 382
379/** 383/**
380 * Returns the absolute URL of the current script, with the query 384 * Returns the absolute URL of the current script, with current route and query
381 * 385 *
382 * If the resource is "index.php", then it is removed (for better-looking URLs) 386 * If the resource is "index.php", then it is removed (for better-looking URLs)
383 * 387 *
@@ -387,10 +391,17 @@ function index_url($server)
387 */ 391 */
388function page_url($server) 392function page_url($server)
389{ 393{
394 $scriptname = $server['SCRIPT_NAME'] ?? '';
395 if (endsWith($scriptname, 'index.php')) {
396 $scriptname = substr($scriptname, 0, -9);
397 }
398
399 $route = preg_replace('@^' . $scriptname . '@', '', $server['REQUEST_URI'] ?? '');
390 if (! empty($server['QUERY_STRING'])) { 400 if (! empty($server['QUERY_STRING'])) {
391 return index_url($server).'?'.$server['QUERY_STRING']; 401 return index_url($server) . $route . '?' . $server['QUERY_STRING'];
392 } 402 }
393 return index_url($server); 403
404 return index_url($server) . $route;
394} 405}
395 406
396/** 407/**
@@ -477,3 +488,109 @@ function is_https($server)
477 488
478 return ! empty($server['HTTPS']); 489 return ! empty($server['HTTPS']);
479} 490}
491
492/**
493 * Get cURL callback function for CURLOPT_WRITEFUNCTION
494 *
495 * @param string $charset to extract from the downloaded page (reference)
496 * @param string $title to extract from the downloaded page (reference)
497 * @param string $description to extract from the downloaded page (reference)
498 * @param string $keywords to extract from the downloaded page (reference)
499 * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
500 * @param string $curlGetInfo Optionally overrides curl_getinfo function
501 *
502 * @return Closure
503 */
504function get_curl_download_callback(
505 &$charset,
506 &$title,
507 &$description,
508 &$keywords,
509 $retrieveDescription,
510 $curlGetInfo = 'curl_getinfo'
511) {
512 $isRedirected = false;
513 $currentChunk = 0;
514 $foundChunk = null;
515
516 /**
517 * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
518 *
519 * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
520 * Then we extract the title and the charset and stop the download when it's done.
521 *
522 * @param resource $ch cURL resource
523 * @param string $data chunk of data being downloaded
524 *
525 * @return int|bool length of $data or false if we need to stop the download
526 */
527 return function (&$ch, $data) use (
528 $retrieveDescription,
529 $curlGetInfo,
530 &$charset,
531 &$title,
532 &$description,
533 &$keywords,
534 &$isRedirected,
535 &$currentChunk,
536 &$foundChunk
537 ) {
538 $currentChunk++;
539 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
540 if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
541 $isRedirected = true;
542 return strlen($data);
543 }
544 if (!empty($responseCode) && $responseCode !== 200) {
545 return false;
546 }
547 // After a redirection, the content type will keep the previous request value
548 // until it finds the next content-type header.
549 if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
550 $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
551 }
552 if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
553 return false;
554 }
555 if (!empty($contentType) && empty($charset)) {
556 $charset = header_extract_charset($contentType);
557 }
558 if (empty($charset)) {
559 $charset = html_extract_charset($data);
560 }
561 if (empty($title)) {
562 $title = html_extract_title($data);
563 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
564 }
565 if ($retrieveDescription && empty($description)) {
566 $description = html_extract_tag('description', $data);
567 $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
568 }
569 if ($retrieveDescription && empty($keywords)) {
570 $keywords = html_extract_tag('keywords', $data);
571 if (! empty($keywords)) {
572 $foundChunk = $currentChunk;
573 // Keywords use the format tag1, tag2 multiple words, tag
574 // So we format them to match Shaarli's separator and glue multiple words with '-'
575 $keywords = implode(' ', array_map(function($keyword) {
576 return implode('-', preg_split('/\s+/', trim($keyword)));
577 }, explode(',', $keywords)));
578 }
579 }
580
581 // We got everything we want, stop the download.
582 // If we already found either the title, description or keywords,
583 // it's highly unlikely that we'll found the other metas further than
584 // in the same chunk of data or the next one. So we also stop the download after that.
585 if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
586 && (! $retrieveDescription
587 || $foundChunk < $currentChunk
588 || (!empty($title) && !empty($description) && !empty($keywords))
589 )
590 ) {
591 return false;
592 }
593
594 return strlen($data);
595 };
596}
diff --git a/application/http/UrlUtils.php b/application/http/UrlUtils.php
index 4bc84b82..e8d1a283 100644
--- a/application/http/UrlUtils.php
+++ b/application/http/UrlUtils.php
@@ -73,7 +73,7 @@ function add_trailing_slash($url)
73 */ 73 */
74function whitelist_protocols($url, $protocols) 74function whitelist_protocols($url, $protocols)
75{ 75{
76 if (startsWith($url, '?') || startsWith($url, '/')) { 76 if (startsWith($url, '?') || startsWith($url, '/') || startsWith($url, '#')) {
77 return $url; 77 return $url;
78 } 78 }
79 $protocols = array_merge(['http', 'https'], $protocols); 79 $protocols = array_merge(['http', 'https'], $protocols);
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/bookmark/LinkDB.php b/application/legacy/LegacyLinkDB.php
index 76ba95f0..7bf76fd4 100644
--- a/application/bookmark/LinkDB.php
+++ b/application/legacy/LegacyLinkDB.php
@@ -1,17 +1,18 @@
1<?php 1<?php
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Legacy;
4 4
5use ArrayAccess; 5use ArrayAccess;
6use Countable; 6use Countable;
7use DateTime; 7use DateTime;
8use Iterator; 8use Iterator;
9use Shaarli\Bookmark\Exception\LinkNotFoundException; 9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Exceptions\IOException; 10use Shaarli\Exceptions\IOException;
11use Shaarli\FileUtils; 11use Shaarli\FileUtils;
12use Shaarli\Render\PageCacheManager;
12 13
13/** 14/**
14 * Data storage for links. 15 * Data storage for bookmarks.
15 * 16 *
16 * This object behaves like an associative array. 17 * This object behaves like an associative array.
17 * 18 *
@@ -29,8 +30,8 @@ use Shaarli\FileUtils;
29 * - private: Is this link private? 0=no, other value=yes 30 * - private: Is this link private? 0=no, other value=yes
30 * - tags: tags attached to this entry (separated by spaces) 31 * - tags: tags attached to this entry (separated by spaces)
31 * - title Title of the link 32 * - title Title of the link
32 * - url URL of the link. Used for displayable links. 33 * - url URL of the link. Used for displayable bookmarks.
33 * Can be absolute or relative in the database but the relative links 34 * Can be absolute or relative in the database but the relative bookmarks
34 * will be converted to absolute ones in templates. 35 * will be converted to absolute ones in templates.
35 * - real_url Raw URL in stored in the DB (absolute or relative). 36 * - real_url Raw URL in stored in the DB (absolute or relative).
36 * - shorturl Permalink smallhash 37 * - shorturl Permalink smallhash
@@ -49,11 +50,13 @@ use Shaarli\FileUtils;
49 * Example: 50 * Example:
50 * - DB: link #1 (2010-01-01) link #2 (2016-01-01) 51 * - DB: link #1 (2010-01-01) link #2 (2016-01-01)
51 * - Order: #2 #1 52 * - Order: #2 #1
52 * - Import links containing: link #3 (2013-01-01) 53 * - Import bookmarks containing: link #3 (2013-01-01)
53 * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01) 54 * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01)
54 * - Real order: #2 #3 #1 55 * - Real order: #2 #3 #1
56 *
57 * @deprecated
55 */ 58 */
56class LinkDB implements Iterator, Countable, ArrayAccess 59class LegacyLinkDB implements Iterator, Countable, ArrayAccess
57{ 60{
58 // Links are stored as a PHP serialized string 61 // Links are stored as a PHP serialized string
59 private $datastore; 62 private $datastore;
@@ -61,7 +64,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
61 // Link date storage format 64 // Link date storage format
62 const LINK_DATE_FORMAT = 'Ymd_His'; 65 const LINK_DATE_FORMAT = 'Ymd_His';
63 66
64 // List of links (associative array) 67 // List of bookmarks (associative array)
65 // - key: link date (e.g. "20110823_124546"), 68 // - key: link date (e.g. "20110823_124546"),
66 // - value: associative array (keys: title, description...) 69 // - value: associative array (keys: title, description...)
67 private $links; 70 private $links;
@@ -71,7 +74,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
71 private $urls; 74 private $urls;
72 75
73 /** 76 /**
74 * @var array List of all links IDS mapped with their array offset. 77 * @var array List of all bookmarks IDS mapped with their array offset.
75 * Map: id->offset. 78 * Map: id->offset.
76 */ 79 */
77 protected $ids; 80 protected $ids;
@@ -82,10 +85,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess
82 // Position in the $this->keys array (for the Iterator interface) 85 // Position in the $this->keys array (for the Iterator interface)
83 private $position; 86 private $position;
84 87
85 // Is the user logged in? (used to filter private links) 88 // Is the user logged in? (used to filter private bookmarks)
86 private $loggedIn; 89 private $loggedIn;
87 90
88 // Hide public links 91 // Hide public bookmarks
89 private $hidePublicLinks; 92 private $hidePublicLinks;
90 93
91 /** 94 /**
@@ -95,7 +98,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
95 * 98 *
96 * @param string $datastore datastore file path. 99 * @param string $datastore datastore file path.
97 * @param boolean $isLoggedIn is the user logged in? 100 * @param boolean $isLoggedIn is the user logged in?
98 * @param boolean $hidePublicLinks if true all links are private. 101 * @param boolean $hidePublicLinks if true all bookmarks are private.
99 */ 102 */
100 public function __construct( 103 public function __construct(
101 $datastore, 104 $datastore,
@@ -280,7 +283,7 @@ You use the community supported version of the original Shaarli project, by Seba
280 */ 283 */
281 private function read() 284 private function read()
282 { 285 {
283 // Public links are hidden and user not logged in => nothing to show 286 // Public bookmarks are hidden and user not logged in => nothing to show
284 if ($this->hidePublicLinks && !$this->loggedIn) { 287 if ($this->hidePublicLinks && !$this->loggedIn) {
285 $this->links = array(); 288 $this->links = array();
286 return; 289 return;
@@ -310,7 +313,7 @@ You use the community supported version of the original Shaarli project, by Seba
310 313
311 $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false; 314 $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
312 315
313 // To be able to load links before running the update, and prepare the update 316 // To be able to load bookmarks before running the update, and prepare the update
314 if (!isset($link['created'])) { 317 if (!isset($link['created'])) {
315 $link['id'] = $link['linkdate']; 318 $link['id'] = $link['linkdate'];
316 $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']); 319 $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
@@ -350,7 +353,8 @@ You use the community supported version of the original Shaarli project, by Seba
350 353
351 $this->write(); 354 $this->write();
352 355
353 invalidateCaches($pageCacheDir); 356 $pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn);
357 $pageCacheManager->invalidateCaches();
354 } 358 }
355 359
356 /** 360 /**
@@ -375,13 +379,13 @@ You use the community supported version of the original Shaarli project, by Seba
375 * 379 *
376 * @return array $filtered array containing permalink data. 380 * @return array $filtered array containing permalink data.
377 * 381 *
378 * @throws LinkNotFoundException if the smallhash is malformed or doesn't match any link. 382 * @throws BookmarkNotFoundException if the smallhash is malformed or doesn't match any link.
379 */ 383 */
380 public function filterHash($request) 384 public function filterHash($request)
381 { 385 {
382 $request = substr($request, 0, 6); 386 $request = substr($request, 0, 6);
383 $linkFilter = new LinkFilter($this->links); 387 $linkFilter = new LegacyLinkFilter($this->links);
384 return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request); 388 return $linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, $request);
385 } 389 }
386 390
387 /** 391 /**
@@ -393,21 +397,21 @@ You use the community supported version of the original Shaarli project, by Seba
393 */ 397 */
394 public function filterDay($request) 398 public function filterDay($request)
395 { 399 {
396 $linkFilter = new LinkFilter($this->links); 400 $linkFilter = new LegacyLinkFilter($this->links);
397 return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request); 401 return $linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, $request);
398 } 402 }
399 403
400 /** 404 /**
401 * Filter links according to search parameters. 405 * Filter bookmarks according to search parameters.
402 * 406 *
403 * @param array $filterRequest Search request content. Supported keys: 407 * @param array $filterRequest Search request content. Supported keys:
404 * - searchtags: list of tags 408 * - searchtags: list of tags
405 * - searchterm: term search 409 * - searchterm: term search
406 * @param bool $casesensitive Optional: Perform case sensitive filter 410 * @param bool $casesensitive Optional: Perform case sensitive filter
407 * @param string $visibility return only all/private/public links 411 * @param string $visibility return only all/private/public bookmarks
408 * @param bool $untaggedonly return only untagged links 412 * @param bool $untaggedonly return only untagged bookmarks
409 * 413 *
410 * @return array filtered links, all links if no suitable filter was provided. 414 * @return array filtered bookmarks, all bookmarks if no suitable filter was provided.
411 */ 415 */
412 public function filterSearch( 416 public function filterSearch(
413 $filterRequest = array(), 417 $filterRequest = array(),
@@ -420,19 +424,19 @@ You use the community supported version of the original Shaarli project, by Seba
420 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; 424 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
421 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; 425 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
422 426
423 // Search tags + fullsearch - blank string parameter will return all links. 427 // Search tags + fullsearch - blank string parameter will return all bookmarks.
424 $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; // == "vuotext" 428 $type = LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT; // == "vuotext"
425 $request = [$searchtags, $searchterm]; 429 $request = [$searchtags, $searchterm];
426 430
427 $linkFilter = new LinkFilter($this); 431 $linkFilter = new LegacyLinkFilter($this);
428 return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly); 432 return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly);
429 } 433 }
430 434
431 /** 435 /**
432 * Returns the list tags appearing in the links with the given tags 436 * Returns the list tags appearing in the bookmarks with the given tags
433 * 437 *
434 * @param array $filteringTags tags selecting the links to consider 438 * @param array $filteringTags tags selecting the bookmarks to consider
435 * @param string $visibility process only all/private/public links 439 * @param string $visibility process only all/private/public bookmarks
436 * 440 *
437 * @return array tag => linksCount 441 * @return array tag => linksCount
438 */ 442 */
@@ -471,12 +475,12 @@ You use the community supported version of the original Shaarli project, by Seba
471 } 475 }
472 476
473 /** 477 /**
474 * Rename or delete a tag across all links. 478 * Rename or delete a tag across all bookmarks.
475 * 479 *
476 * @param string $from Tag to rename 480 * @param string $from Tag to rename
477 * @param string $to New tag. If none is provided, the from tag will be deleted 481 * @param string $to New tag. If none is provided, the from tag will be deleted
478 * 482 *
479 * @return array|bool List of altered links or false on error 483 * @return array|bool List of altered bookmarks or false on error
480 */ 484 */
481 public function renameTag($from, $to) 485 public function renameTag($from, $to)
482 { 486 {
@@ -519,7 +523,7 @@ You use the community supported version of the original Shaarli project, by Seba
519 } 523 }
520 524
521 /** 525 /**
522 * Reorder links by creation date (newest first). 526 * Reorder bookmarks by creation date (newest first).
523 * 527 *
524 * Also update the urls and ids mapping arrays. 528 * Also update the urls and ids mapping arrays.
525 * 529 *
@@ -533,6 +537,9 @@ You use the community supported version of the original Shaarli project, by Seba
533 if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) { 537 if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
534 return $a['sticky'] ? -1 : 1; 538 return $a['sticky'] ? -1 : 1;
535 } 539 }
540 if ($a['created'] == $b['created']) {
541 return $a['id'] < $b['id'] ? 1 * $order : -1 * $order;
542 }
536 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; 543 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
537 }); 544 });
538 545
@@ -559,7 +566,7 @@ You use the community supported version of the original Shaarli project, by Seba
559 } 566 }
560 567
561 /** 568 /**
562 * Returns a link offset in links array from its unique ID. 569 * Returns a link offset in bookmarks array from its unique ID.
563 * 570 *
564 * @param int $id Persistent ID of a link. 571 * @param int $id Persistent ID of a link.
565 * 572 *
diff --git a/application/bookmark/LinkFilter.php b/application/legacy/LegacyLinkFilter.php
index 9b966307..7cf93d60 100644
--- a/application/bookmark/LinkFilter.php
+++ b/application/legacy/LegacyLinkFilter.php
@@ -1,16 +1,18 @@
1<?php 1<?php
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Legacy;
4 4
5use Exception; 5use Exception;
6use Shaarli\Bookmark\Exception\LinkNotFoundException; 6use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7 7
8/** 8/**
9 * Class LinkFilter. 9 * Class LinkFilter.
10 * 10 *
11 * Perform search and filter operation on link data list. 11 * Perform search and filter operation on link data list.
12 *
13 * @deprecated
12 */ 14 */
13class LinkFilter 15class LegacyLinkFilter
14{ 16{
15 /** 17 /**
16 * @var string permalinks. 18 * @var string permalinks.
@@ -38,12 +40,12 @@ class LinkFilter
38 public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}'; 40 public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
39 41
40 /** 42 /**
41 * @var LinkDB all available links. 43 * @var LegacyLinkDB all available links.
42 */ 44 */
43 private $links; 45 private $links;
44 46
45 /** 47 /**
46 * @param LinkDB $links initialization. 48 * @param LegacyLinkDB $links initialization.
47 */ 49 */
48 public function __construct($links) 50 public function __construct($links)
49 { 51 {
@@ -84,10 +86,10 @@ class LinkFilter
84 $filtered = $this->links; 86 $filtered = $this->links;
85 } 87 }
86 if (!empty($request[0])) { 88 if (!empty($request[0])) {
87 $filtered = (new LinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); 89 $filtered = (new LegacyLinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
88 } 90 }
89 if (!empty($request[1])) { 91 if (!empty($request[1])) {
90 $filtered = (new LinkFilter($filtered))->filterFulltext($request[1], $visibility); 92 $filtered = (new LegacyLinkFilter($filtered))->filterFulltext($request[1], $visibility);
91 } 93 }
92 return $filtered; 94 return $filtered;
93 case self::$FILTER_TEXT: 95 case self::$FILTER_TEXT:
@@ -137,7 +139,7 @@ class LinkFilter
137 * 139 *
138 * @return array $filtered array containing permalink data. 140 * @return array $filtered array containing permalink data.
139 * 141 *
140 * @throws \Shaarli\Bookmark\Exception\LinkNotFoundException if the smallhash doesn't match any link. 142 * @throws BookmarkNotFoundException if the smallhash doesn't match any link.
141 */ 143 */
142 private function filterSmallHash($smallHash) 144 private function filterSmallHash($smallHash)
143 { 145 {
@@ -151,7 +153,7 @@ class LinkFilter
151 } 153 }
152 154
153 if (empty($filtered)) { 155 if (empty($filtered)) {
154 throw new LinkNotFoundException(); 156 throw new BookmarkNotFoundException();
155 } 157 }
156 158
157 return $filtered; 159 return $filtered;
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
new file mode 100644
index 00000000..0ab3a55b
--- /dev/null
+++ b/application/legacy/LegacyUpdater.php
@@ -0,0 +1,618 @@
1<?php
2
3namespace Shaarli\Legacy;
4
5use Exception;
6use RainTPL;
7use ReflectionClass;
8use ReflectionException;
9use ReflectionMethod;
10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\Bookmark;
12use Shaarli\Bookmark\BookmarkArray;
13use Shaarli\Bookmark\BookmarkFilter;
14use Shaarli\Bookmark\BookmarkIO;
15use Shaarli\Bookmark\LinkDB;
16use Shaarli\Config\ConfigJson;
17use Shaarli\Config\ConfigManager;
18use Shaarli\Config\ConfigPhp;
19use Shaarli\Exceptions\IOException;
20use Shaarli\Thumbnailer;
21use Shaarli\Updater\Exception\UpdaterException;
22
23/**
24 * Class updater.
25 * Used to update stuff when a new Shaarli's version is reached.
26 * Update methods are ran only once, and the stored in a JSON file.
27 *
28 * @deprecated
29 */
30class LegacyUpdater
31{
32 /**
33 * @var array Updates which are already done.
34 */
35 protected $doneUpdates;
36
37 /**
38 * @var LegacyLinkDB instance.
39 */
40 protected $linkDB;
41
42 /**
43 * @var ConfigManager $conf Configuration Manager instance.
44 */
45 protected $conf;
46
47 /**
48 * @var bool True if the user is logged in, false otherwise.
49 */
50 protected $isLoggedIn;
51
52 /**
53 * @var array $_SESSION
54 */
55 protected $session;
56
57 /**
58 * @var ReflectionMethod[] List of current class methods.
59 */
60 protected $methods;
61
62 /**
63 * Object constructor.
64 *
65 * @param array $doneUpdates Updates which are already done.
66 * @param LegacyLinkDB $linkDB LinkDB instance.
67 * @param ConfigManager $conf Configuration Manager instance.
68 * @param boolean $isLoggedIn True if the user is logged in.
69 * @param array $session $_SESSION (by reference)
70 *
71 * @throws ReflectionException
72 */
73 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
74 {
75 $this->doneUpdates = $doneUpdates;
76 $this->linkDB = $linkDB;
77 $this->conf = $conf;
78 $this->isLoggedIn = $isLoggedIn;
79 $this->session = &$session;
80
81 // Retrieve all update methods.
82 $class = new ReflectionClass($this);
83 $this->methods = $class->getMethods();
84 }
85
86 /**
87 * Run all new updates.
88 * Update methods have to start with 'updateMethod' and return true (on success).
89 *
90 * @return array An array containing ran updates.
91 *
92 * @throws UpdaterException If something went wrong.
93 */
94 public function update()
95 {
96 $updatesRan = array();
97
98 // If the user isn't logged in, exit without updating.
99 if ($this->isLoggedIn !== true) {
100 return $updatesRan;
101 }
102
103 if ($this->methods === null) {
104 throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
105 }
106
107 foreach ($this->methods as $method) {
108 // Not an update method or already done, pass.
109 if (!startsWith($method->getName(), 'updateMethod')
110 || in_array($method->getName(), $this->doneUpdates)
111 ) {
112 continue;
113 }
114
115 try {
116 $method->setAccessible(true);
117 $res = $method->invoke($this);
118 // Update method must return true to be considered processed.
119 if ($res === true) {
120 $updatesRan[] = $method->getName();
121 }
122 } catch (Exception $e) {
123 throw new UpdaterException($method, $e);
124 }
125 }
126
127 $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan);
128
129 return $updatesRan;
130 }
131
132 /**
133 * @return array Updates methods already processed.
134 */
135 public function getDoneUpdates()
136 {
137 return $this->doneUpdates;
138 }
139
140 /**
141 * Move deprecated options.php to config.php.
142 *
143 * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
144 * options.php is not supported anymore.
145 */
146 public function updateMethodMergeDeprecatedConfigFile()
147 {
148 if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
149 include $this->conf->get('resource.data_dir') . '/options.php';
150
151 // Load GLOBALS into config
152 $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
153 $allowedKeys[] = 'config';
154 foreach ($GLOBALS as $key => $value) {
155 if (in_array($key, $allowedKeys)) {
156 $this->conf->set($key, $value);
157 }
158 }
159 $this->conf->write($this->isLoggedIn);
160 unlink($this->conf->get('resource.data_dir') . '/options.php');
161 }
162
163 return true;
164 }
165
166 /**
167 * Move old configuration in PHP to the new config system in JSON format.
168 *
169 * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
170 * It will also convert legacy setting keys to the new ones.
171 */
172 public function updateMethodConfigToJson()
173 {
174 // JSON config already exists, nothing to do.
175 if ($this->conf->getConfigIO() instanceof ConfigJson) {
176 return true;
177 }
178
179 $configPhp = new ConfigPhp();
180 $configJson = new ConfigJson();
181 $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
182 rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
183 $this->conf->setConfigIO($configJson);
184 $this->conf->reload();
185
186 $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
187 foreach (ConfigPhp::$ROOT_KEYS as $key) {
188 $this->conf->set($legacyMap[$key], $oldConfig[$key]);
189 }
190
191 // Set sub config keys (config and plugins)
192 $subConfig = array('config', 'plugins');
193 foreach ($subConfig as $sub) {
194 foreach ($oldConfig[$sub] as $key => $value) {
195 if (isset($legacyMap[$sub . '.' . $key])) {
196 $configKey = $legacyMap[$sub . '.' . $key];
197 } else {
198 $configKey = $sub . '.' . $key;
199 }
200 $this->conf->set($configKey, $value);
201 }
202 }
203
204 try {
205 $this->conf->write($this->isLoggedIn);
206 return true;
207 } catch (IOException $e) {
208 error_log($e->getMessage());
209 return false;
210 }
211 }
212
213 /**
214 * Escape settings which have been manually escaped in every request in previous versions:
215 * - general.title
216 * - general.header_link
217 * - redirector.url
218 *
219 * @return bool true if the update is successful, false otherwise.
220 */
221 public function updateMethodEscapeUnescapedConfig()
222 {
223 try {
224 $this->conf->set('general.title', escape($this->conf->get('general.title')));
225 $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
226 $this->conf->write($this->isLoggedIn);
227 } catch (Exception $e) {
228 error_log($e->getMessage());
229 return false;
230 }
231 return true;
232 }
233
234 /**
235 * Update the database to use the new ID system, which replaces linkdate primary keys.
236 * Also, creation and update dates are now DateTime objects (done by LinkDB).
237 *
238 * Since this update is very sensitve (changing the whole database), the datastore will be
239 * automatically backed up into the file datastore.<datetime>.php.
240 *
241 * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
242 * which will be saved by this method.
243 *
244 * @return bool true if the update is successful, false otherwise.
245 */
246 public function updateMethodDatastoreIds()
247 {
248 $first = 'update';
249 foreach ($this->linkDB as $key => $link) {
250 $first = $key;
251 break;
252 }
253
254 // up to date database
255 if (is_int($first)) {
256 return true;
257 }
258
259 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
260 copy($this->conf->get('resource.datastore'), $save);
261
262 $links = array();
263 foreach ($this->linkDB as $offset => $value) {
264 $links[] = $value;
265 unset($this->linkDB[$offset]);
266 }
267 $links = array_reverse($links);
268 $cpt = 0;
269 foreach ($links as $l) {
270 unset($l['linkdate']);
271 $l['id'] = $cpt;
272 $this->linkDB[$cpt++] = $l;
273 }
274
275 $this->linkDB->save($this->conf->get('resource.page_cache'));
276 $this->linkDB->reorder();
277
278 return true;
279 }
280
281 /**
282 * Rename tags starting with a '-' to work with tag exclusion search.
283 */
284 public function updateMethodRenameDashTags()
285 {
286 $linklist = $this->linkDB->filterSearch();
287 foreach ($linklist as $key => $link) {
288 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
289 $link['tags'] = implode(' ', array_unique(BookmarkFilter::tagsStrToArray($link['tags'], true)));
290 $this->linkDB[$key] = $link;
291 }
292 $this->linkDB->save($this->conf->get('resource.page_cache'));
293 return true;
294 }
295
296 /**
297 * Initialize API settings:
298 * - api.enabled: true
299 * - api.secret: generated secret
300 */
301 public function updateMethodApiSettings()
302 {
303 if ($this->conf->exists('api.secret')) {
304 return true;
305 }
306
307 $this->conf->set('api.enabled', true);
308 $this->conf->set(
309 'api.secret',
310 generate_api_secret(
311 $this->conf->get('credentials.login'),
312 $this->conf->get('credentials.salt')
313 )
314 );
315 $this->conf->write($this->isLoggedIn);
316 return true;
317 }
318
319 /**
320 * New setting: theme name. If the default theme is used, nothing to do.
321 *
322 * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
323 * and the current theme is set as default in the theme setting.
324 *
325 * @return bool true if the update is successful, false otherwise.
326 */
327 public function updateMethodDefaultTheme()
328 {
329 // raintpl_tpl isn't the root template directory anymore.
330 // We run the update only if this folder still contains the template files.
331 $tplDir = $this->conf->get('resource.raintpl_tpl');
332 $tplFile = $tplDir . '/linklist.html';
333 if (!file_exists($tplFile)) {
334 return true;
335 }
336
337 $parent = dirname($tplDir);
338 $this->conf->set('resource.raintpl_tpl', $parent);
339 $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
340 $this->conf->write($this->isLoggedIn);
341
342 // Dependency injection gore
343 RainTPL::$tpl_dir = $tplDir;
344
345 return true;
346 }
347
348 /**
349 * Move the file to inc/user.css to data/user.css.
350 *
351 * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
352 *
353 * @return bool true if the update is successful, false otherwise.
354 */
355 public function updateMethodMoveUserCss()
356 {
357 if (!is_file('inc/user.css')) {
358 return true;
359 }
360
361 return rename('inc/user.css', 'data/user.css');
362 }
363
364 /**
365 * * `markdown_escape` is a new setting, set to true as default.
366 *
367 * If the markdown plugin was already enabled, escaping is disabled to avoid
368 * breaking existing entries.
369 */
370 public function updateMethodEscapeMarkdown()
371 {
372 if ($this->conf->exists('security.markdown_escape')) {
373 return true;
374 }
375
376 if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
377 $this->conf->set('security.markdown_escape', false);
378 } else {
379 $this->conf->set('security.markdown_escape', true);
380 }
381 $this->conf->write($this->isLoggedIn);
382
383 return true;
384 }
385
386 /**
387 * Add 'http://' to Piwik URL the setting is set.
388 *
389 * @return bool true if the update is successful, false otherwise.
390 */
391 public function updateMethodPiwikUrl()
392 {
393 if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
394 return true;
395 }
396
397 $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
398 $this->conf->write($this->isLoggedIn);
399
400 return true;
401 }
402
403 /**
404 * Use ATOM feed as default.
405 */
406 public function updateMethodAtomDefault()
407 {
408 if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
409 return true;
410 }
411
412 $this->conf->set('feed.show_atom', true);
413 $this->conf->write($this->isLoggedIn);
414
415 return true;
416 }
417
418 /**
419 * Update updates.check_updates_branch setting.
420 *
421 * If the current major version digit matches the latest branch
422 * major version digit, we set the branch to `latest`,
423 * otherwise we'll check updates on the `stable` branch.
424 *
425 * No update required for the dev version.
426 *
427 * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
428 *
429 * FIXME! This needs to be removed when we switch to first digit major version
430 * instead of the second one since the versionning process will change.
431 */
432 public function updateMethodCheckUpdateRemoteBranch()
433 {
434 if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
435 return true;
436 }
437
438 // Get latest branch major version digit
439 $latestVersion = ApplicationUtils::getLatestGitVersionCode(
440 'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
441 5
442 );
443 if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
444 return false;
445 }
446 $latestMajor = $matches[1];
447
448 // Get current major version digit
449 preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
450 $currentMajor = $matches[1];
451
452 if ($currentMajor === $latestMajor) {
453 $branch = 'latest';
454 } else {
455 $branch = 'stable';
456 }
457 $this->conf->set('updates.check_updates_branch', $branch);
458 $this->conf->write($this->isLoggedIn);
459 return true;
460 }
461
462 /**
463 * Reset history store file due to date format change.
464 */
465 public function updateMethodResetHistoryFile()
466 {
467 if (is_file($this->conf->get('resource.history'))) {
468 unlink($this->conf->get('resource.history'));
469 }
470 return true;
471 }
472
473 /**
474 * Save the datastore -> the link order is now applied when bookmarks are saved.
475 */
476 public function updateMethodReorderDatastore()
477 {
478 $this->linkDB->save($this->conf->get('resource.page_cache'));
479 return true;
480 }
481
482 /**
483 * Change privateonly session key to visibility.
484 */
485 public function updateMethodVisibilitySession()
486 {
487 if (isset($_SESSION['privateonly'])) {
488 unset($_SESSION['privateonly']);
489 $_SESSION['visibility'] = 'private';
490 }
491 return true;
492 }
493
494 /**
495 * Add download size and timeout to the configuration file
496 *
497 * @return bool true if the update is successful, false otherwise.
498 */
499 public function updateMethodDownloadSizeAndTimeoutConf()
500 {
501 if ($this->conf->exists('general.download_max_size')
502 && $this->conf->exists('general.download_timeout')
503 ) {
504 return true;
505 }
506
507 if (!$this->conf->exists('general.download_max_size')) {
508 $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
509 }
510
511 if (!$this->conf->exists('general.download_timeout')) {
512 $this->conf->set('general.download_timeout', 30);
513 }
514
515 $this->conf->write($this->isLoggedIn);
516 return true;
517 }
518
519 /**
520 * * Move thumbnails management to WebThumbnailer, coming with new settings.
521 */
522 public function updateMethodWebThumbnailer()
523 {
524 if ($this->conf->exists('thumbnails.mode')) {
525 return true;
526 }
527
528 $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
529 $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
530 $this->conf->set('thumbnails.width', 125);
531 $this->conf->set('thumbnails.height', 90);
532 $this->conf->remove('thumbnail');
533 $this->conf->write(true);
534
535 if ($thumbnailsEnabled) {
536 $this->session['warnings'][] = t(
537 t('You have enabled or changed thumbnails mode.') .
538 '<a href="./admin/thumbnails">' . t('Please synchronize them.') . '</a>'
539 );
540 }
541
542 return true;
543 }
544
545 /**
546 * Set sticky = false on all bookmarks
547 *
548 * @return bool true if the update is successful, false otherwise.
549 */
550 public function updateMethodSetSticky()
551 {
552 foreach ($this->linkDB as $key => $link) {
553 if (isset($link['sticky'])) {
554 return true;
555 }
556 $link['sticky'] = false;
557 $this->linkDB[$key] = $link;
558 }
559
560 $this->linkDB->save($this->conf->get('resource.page_cache'));
561
562 return true;
563 }
564
565 /**
566 * Remove redirector settings.
567 */
568 public function updateMethodRemoveRedirector()
569 {
570 $this->conf->remove('redirector');
571 $this->conf->write(true);
572 return true;
573 }
574
575 /**
576 * Migrate the legacy arrays to Bookmark objects.
577 * Also make a backup of the datastore.
578 */
579 public function updateMethodMigrateDatabase()
580 {
581 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '_1.php';
582 if (! copy($this->conf->get('resource.datastore'), $save)) {
583 die('Could not backup the datastore.');
584 }
585
586 $linksArray = new BookmarkArray();
587 foreach ($this->linkDB as $key => $link) {
588 $linksArray[$key] = (new Bookmark())->fromArray($link);
589 }
590 $linksIo = new BookmarkIO($this->conf);
591 $linksIo->write($linksArray);
592
593 return true;
594 }
595
596 /**
597 * Write the `formatter` setting in config file.
598 * Use markdown if the markdown plugin is enabled, the default one otherwise.
599 * Also remove markdown plugin setting as it is now integrated to the core.
600 */
601 public function updateMethodFormatterSetting()
602 {
603 if (!$this->conf->exists('formatter') || $this->conf->get('formatter') === 'default') {
604 $enabledPlugins = $this->conf->get('general.enabled_plugins');
605 if (($pos = array_search('markdown', $enabledPlugins)) !== false) {
606 $formatter = 'markdown';
607 unset($enabledPlugins[$pos]);
608 $this->conf->set('general.enabled_plugins', array_values($enabledPlugins));
609 } else {
610 $formatter = 'default';
611 }
612 $this->conf->set('formatter', $formatter);
613 $this->conf->write(true);
614 }
615
616 return true;
617 }
618}
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 28665941..b83f16f8 100644
--- a/application/netscape/NetscapeBookmarkUtils.php
+++ b/application/netscape/NetscapeBookmarkUtils.php
@@ -6,56 +6,69 @@ 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\LinkDB; 11use Shaarli\Bookmark\Bookmark;
12use Shaarli\Bookmark\BookmarkServiceInterface;
11use Shaarli\Config\ConfigManager; 13use Shaarli\Config\ConfigManager;
14use Shaarli\Formatter\BookmarkFormatter;
12use Shaarli\History; 15use Shaarli\History;
13use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser; 16use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser;
14 17
15/** 18/**
16 * Utilities to import and export bookmarks using the Netscape format 19 * Utilities to import and export bookmarks using the Netscape format
17 * TODO: Not static, use a container.
18 */ 20 */
19class NetscapeBookmarkUtils 21class NetscapeBookmarkUtils
20{ 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 }
21 38
22 /** 39 /**
23 * Filters links and adds Netscape-formatted fields 40 * Filters bookmarks and adds Netscape-formatted fields
24 * 41 *
25 * Added fields: 42 * Added fields:
26 * - timestamp link addition date, using the Unix epoch format 43 * - timestamp link addition date, using the Unix epoch format
27 * - taglist comma-separated tag list 44 * - taglist comma-separated tag list
28 * 45 *
29 * @param LinkDB $linkDb Link datastore 46 * @param BookmarkFormatter $formatter instance
30 * @param string $selection Which links to export: (all|private|public) 47 * @param string $selection Which bookmarks to export: (all|private|public)
31 * @param bool $prependNoteUrl Prepend note permalinks with the server's URL 48 * @param bool $prependNoteUrl Prepend note permalinks with the server's URL
32 * @param string $indexUrl Absolute URL of the Shaarli index page 49 * @param string $indexUrl Absolute URL of the Shaarli index page
33 * 50 *
34 * @throws Exception Invalid export selection 51 * @return array The bookmarks to be exported, with additional fields
35 * 52 *
36 * @return array The links to be exported, with additional fields 53 * @throws Exception Invalid export selection
37 */ 54 */
38 public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $indexUrl) 55 public function filterAndFormat(
39 { 56 $formatter,
57 $selection,
58 $prependNoteUrl,
59 $indexUrl
60 ) {
40 // see tpl/export.html for possible values 61 // see tpl/export.html for possible values
41 if (!in_array($selection, array('all', 'public', 'private'))) { 62 if (!in_array($selection, array('all', 'public', 'private'))) {
42 throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"'); 63 throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"');
43 } 64 }
44 65
45 $bookmarkLinks = array(); 66 $bookmarkLinks = array();
46 foreach ($linkDb as $link) { 67 foreach ($this->bookmarkService->search([], $selection) as $bookmark) {
47 if ($link['private'] != 0 && $selection == 'public') { 68 $link = $formatter->format($bookmark);
48 continue; 69 $link['taglist'] = implode(',', $bookmark->getTags());
49 } 70 if ($bookmark->isNote() && $prependNoteUrl) {
50 if ($link['private'] == 0 && $selection == 'private') { 71 $link['url'] = rtrim($indexUrl, '/') . '/' . ltrim($link['url'], '/');
51 continue;
52 }
53 $date = $link['created'];
54 $link['timestamp'] = $date->getTimestamp();
55 $link['taglist'] = str_replace(' ', ',', $link['tags']);
56
57 if (is_note($link['url']) && $prependNoteUrl) {
58 $link['url'] = $indexUrl . $link['url'];
59 } 72 }
60 73
61 $bookmarkLinks[] = $link; 74 $bookmarkLinks[] = $link;
@@ -65,66 +78,28 @@ 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 links were imported
73 * @param int $overwriteCount how many links were overwritten
74 * @param int $skipCount how many links 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 links imported, %d links overwritten, %d links 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 LinkDB $linkDb 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, $linkDb, $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 links? 99 // Overwrite existing bookmarks?
125 $overwrite = !empty($post['overwrite']); 100 $overwrite = !empty($post['overwrite']);
126 101
127 // Add tags to all imported links? 102 // Add tags to all imported bookmarks?
128 if (empty($post['default_tags'])) { 103 if (empty($post['default_tags'])) {
129 $defaultTags = array(); 104 $defaultTags = array();
130 } else { 105 } else {
@@ -134,18 +109,18 @@ class NetscapeBookmarkUtils
134 ); 109 );
135 } 110 }
136 111
137 // links are imported as public by default 112 // bookmarks are imported as public by default
138 $defaultPrivacy = 0; 113 $defaultPrivacy = 0;
139 114
140 $parser = new NetscapeBookmarkParser( 115 $parser = new NetscapeBookmarkParser(
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',
@@ -164,22 +139,18 @@ class NetscapeBookmarkUtils
164 // use value from the imported file 139 // use value from the imported file
165 $private = $bkm['pub'] == '1' ? 0 : 1; 140 $private = $bkm['pub'] == '1' ? 0 : 1;
166 } elseif ($post['privacy'] == 'private') { 141 } elseif ($post['privacy'] == 'private') {
167 // all imported links are private 142 // all imported bookmarks are private
168 $private = 1; 143 $private = 1;
169 } elseif ($post['privacy'] == 'public') { 144 } elseif ($post['privacy'] == 'public') {
170 // all imported links are public 145 // all imported bookmarks are public
171 $private = 0; 146 $private = 0;
172 } 147 }
173 148
174 $newLink = array( 149 $link = $this->bookmarkService->findByUrl($bkm['uri']);
175 'title' => $bkm['title'], 150 $existingLink = $link !== null;
176 'url' => $bkm['uri'], 151 if (! $existingLink) {
177 'description' => $bkm['note'], 152 $link = new Bookmark();
178 'private' => $private, 153 }
179 'tags' => $bkm['tags']
180 );
181
182 $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
183 154
184 if ($existingLink !== false) { 155 if ($existingLink !== false) {
185 if ($overwrite === false) { 156 if ($overwrite === false) {
@@ -188,32 +159,30 @@ class NetscapeBookmarkUtils
188 continue; 159 continue;
189 } 160 }
190 161
191 // Overwrite an existing link, keep its date 162 $link->setUpdated(new DateTime());
192 $newLink['id'] = $existingLink['id'];
193 $newLink['created'] = $existingLink['created'];
194 $newLink['updated'] = new DateTime();
195 $newLink['shorturl'] = $existingLink['shorturl'];
196 $linkDb[$existingLink['id']] = $newLink;
197 $importCount++;
198 $overwriteCount++; 163 $overwriteCount++;
199 continue; 164 } else {
165 $newLinkDate = new DateTime('@' . strval($bkm['time']));
166 $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
167 $link->setCreated($newLinkDate);
200 } 168 }
201 169
202 // Add a new link - @ used for UNIX timestamps 170 $link->setTitle($bkm['title']);
203 $newLinkDate = new DateTime('@' . strval($bkm['time'])); 171 $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
204 $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get())); 172 $link->setDescription($bkm['note']);
205 $newLink['created'] = $newLinkDate; 173 $link->setPrivate($private);
206 $newLink['id'] = $linkDb->getNextId(); 174 $link->setTagsString($bkm['tags']);
207 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); 175
208 $linkDb[$newLink['id']] = $newLink; 176 $this->bookmarkService->addOrSet($link, false);
209 $importCount++; 177 $importCount++;
210 } 178 }
211 179
212 $linkDb->save($conf->get('resource.page_cache')); 180 $this->bookmarkService->save();
213 $history->importLinks(); 181 $this->history->importLinks();
214 182
215 $duration = time() - $start; 183 $duration = time() - $start;
216 return self::importStatus( 184
185 return $this->importStatus(
217 $filename, 186 $filename,
218 $filesize, 187 $filesize,
219 $importCount, 188 $importCount,
@@ -222,4 +191,39 @@ class NetscapeBookmarkUtils
222 $duration 191 $duration
223 ); 192 );
224 } 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 }
225} 229}
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php
index f7b24a8e..1b2197c9 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,35 @@ 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 'bookmarkService' => '_BOOKMARK_SERVICE_',
108 $data['_LOGGEDIN_'] = $params['loggedin']; 108 ];
109
110 foreach ($metadataParameters as $parameter => $metaKey) {
111 if (array_key_exists($parameter, $params)) {
112 $data[$metaKey] = $params[$parameter];
113 }
109 } 114 }
110 115
111 foreach ($this->loadedPlugins as $plugin) { 116 foreach ($this->loadedPlugins as $plugin) {
112 $hookFunction = $this->buildHookName($hook, $plugin); 117 $hookFunction = $this->buildHookName($hook, $plugin);
113 118
114 if (function_exists($hookFunction)) { 119 if (function_exists($hookFunction)) {
115 $data = call_user_func($hookFunction, $data, $this->conf); 120 try {
121 $data = call_user_func($hookFunction, $data, $this->conf);
122 } catch (\Throwable $e) {
123 $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
124 $this->errors = array_unique(array_merge($this->errors, [$error]));
125 }
116 } 126 }
117 } 127 }
128
129 foreach ($metadataParameters as $metaKey) {
130 unset($data[$metaKey]);
131 }
118 } 132 }
119 133
120 /** 134 /**
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index 3f86fc26..41b357dd 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 exceptions\MissingBasePathException;
6use RainTPL; 7use RainTPL;
7use Shaarli\ApplicationUtils; 8use Shaarli\ApplicationUtils;
8use Shaarli\Bookmark\LinkDB; 9use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager; 10use Shaarli\Config\ConfigManager;
11use Shaarli\Security\SessionManager;
10use Shaarli\Thumbnailer; 12use Shaarli\Thumbnailer;
11 13
12/** 14/**
@@ -34,9 +36,9 @@ class PageBuilder
34 protected $session; 36 protected $session;
35 37
36 /** 38 /**
37 * @var LinkDB $linkDB instance. 39 * @var BookmarkServiceInterface $bookmarkService instance.
38 */ 40 */
39 protected $linkDB; 41 protected $bookmarkService;
40 42
41 /** 43 /**
42 * @var null|string XSRF token 44 * @var null|string XSRF token
@@ -52,23 +54,32 @@ class PageBuilder
52 * PageBuilder constructor. 54 * PageBuilder constructor.
53 * $tpl is initialized at false for lazy loading. 55 * $tpl is initialized at false for lazy loading.
54 * 56 *
55 * @param ConfigManager $conf Configuration Manager instance (reference). 57 * @param ConfigManager $conf Configuration Manager instance (reference).
56 * @param array $session $_SESSION array 58 * @param array $session $_SESSION array
57 * @param LinkDB $linkDB instance. 59 * @param BookmarkServiceInterface $linkDB instance.
58 * @param string $token Session token 60 * @param string $token Session token
59 * @param bool $isLoggedIn 61 * @param bool $isLoggedIn
60 */ 62 */
61 public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false) 63 public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
62 { 64 {
63 $this->tpl = false; 65 $this->tpl = false;
64 $this->conf = $conf; 66 $this->conf = $conf;
65 $this->session = $session; 67 $this->session = $session;
66 $this->linkDB = $linkDB; 68 $this->bookmarkService = $linkDB;
67 $this->token = $token; 69 $this->token = $token;
68 $this->isLoggedIn = $isLoggedIn; 70 $this->isLoggedIn = $isLoggedIn;
69 } 71 }
70 72
71 /** 73 /**
74 * Reset current state of template rendering.
75 * Mostly useful for error handling. We remove everything, and display the error template.
76 */
77 public function reset(): void
78 {
79 $this->tpl = false;
80 }
81
82 /**
72 * Initialize all default tpl tags. 83 * Initialize all default tpl tags.
73 */ 84 */
74 private function initialize() 85 private function initialize()
@@ -125,8 +136,8 @@ class PageBuilder
125 136
126 $this->tpl->assign('language', $this->conf->get('translation.language')); 137 $this->tpl->assign('language', $this->conf->get('translation.language'));
127 138
128 if ($this->linkDB !== null) { 139 if ($this->bookmarkService !== null) {
129 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); 140 $this->tpl->assign('tags', escape($this->bookmarkService->bookmarksCountPerTag()));
130 } 141 }
131 142
132 $this->tpl->assign( 143 $this->tpl->assign(
@@ -136,16 +147,43 @@ class PageBuilder
136 $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width')); 147 $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
137 $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height')); 148 $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
138 149
139 if (!empty($_SESSION['warnings'])) { 150 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
140 $this->tpl->assign('global_warnings', $_SESSION['warnings']); 151
141 unset($_SESSION['warnings']); 152 $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE']);
142 }
143 153
144 // To be removed with a proper theme configuration. 154 // To be removed with a proper theme configuration.
145 $this->tpl->assign('conf', $this->conf); 155 $this->tpl->assign('conf', $this->conf);
146 } 156 }
147 157
148 /** 158 /**
159 * Affect variable after controller processing.
160 * Used for alert messages.
161 */
162 protected function finalize(string $basePath): void
163 {
164 // TODO: use the SessionManager
165 $messageKeys = [
166 SessionManager::KEY_SUCCESS_MESSAGES,
167 SessionManager::KEY_WARNING_MESSAGES,
168 SessionManager::KEY_ERROR_MESSAGES
169 ];
170 foreach ($messageKeys as $messageKey) {
171 if (!empty($_SESSION[$messageKey])) {
172 $this->tpl->assign('global_' . $messageKey, $_SESSION[$messageKey]);
173 unset($_SESSION[$messageKey]);
174 }
175 }
176
177 $this->assign('base_path', $basePath);
178 $this->assign(
179 'asset_path',
180 $basePath . '/' .
181 rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' .
182 $this->conf->get('resource.theme', 'default')
183 );
184 }
185
186 /**
149 * The following assign() method is basically the same as RainTPL (except lazy loading) 187 * The following assign() method is basically the same as RainTPL (except lazy loading)
150 * 188 *
151 * @param string $placeholder Template placeholder. 189 * @param string $placeholder Template placeholder.
@@ -183,33 +221,21 @@ class PageBuilder
183 } 221 }
184 222
185 /** 223 /**
186 * Render a specific page (using a template file). 224 * Render a specific page as string (using a template file).
187 * e.g. $pb->renderPage('picwall'); 225 * e.g. $pb->render('picwall');
188 * 226 *
189 * @param string $page Template filename (without extension). 227 * @param string $page Template filename (without extension).
228 *
229 * @return string Processed template content
190 */ 230 */
191 public function renderPage($page) 231 public function render(string $page, string $basePath): string
192 { 232 {
193 if ($this->tpl === false) { 233 if ($this->tpl === false) {
194 $this->initialize(); 234 $this->initialize();
195 } 235 }
196 236
197 $this->tpl->draw($page); 237 $this->finalize($basePath);
198 }
199 238
200 /** 239 return $this->tpl->draw($page, true);
201 * Render a 404 page (uses the template : tpl/404.tpl)
202 * usage: $PAGE->render404('The link was deleted')
203 *
204 * @param string $message A message to display what is not found
205 */
206 public function render404($message = '')
207 {
208 if (empty($message)) {
209 $message = t('The page you are trying to reach does not exist or has been deleted.');
210 }
211 header($_SERVER['SERVER_PROTOCOL'] . ' ' . t('404 Not Found'));
212 $this->tpl->assign('error_message', $message);
213 $this->renderPage('404');
214 } 240 }
215} 241}
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..8af8228a
--- /dev/null
+++ b/application/render/TemplatePage.php
@@ -0,0 +1,33 @@
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 ERROR = 'error';
18 public const EXPORT = 'export';
19 public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks';
20 public const FEED_ATOM = 'feed.atom';
21 public const FEED_RSS = 'feed.rss';
22 public const IMPORT = 'import';
23 public const INSTALL = 'install';
24 public const LINKLIST = 'linklist';
25 public const LOGIN = 'loginform';
26 public const OPEN_SEARCH = 'opensearch';
27 public const PICTURE_WALL = 'picwall';
28 public const PLUGINS_ADMIN = 'pluginsadmin';
29 public const TAG_CLOUD = 'tag.cloud';
30 public const TAG_LIST = 'tag.list';
31 public const THUMBNAILS = 'thumbnails';
32 public const TOOLS = 'tools';
33}
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 0b0ce0b1..d74c3118 100644
--- a/application/security/LoginManager.php
+++ b/application/security/LoginManager.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2namespace Shaarli\Security; 2namespace Shaarli\Security;
3 3
4use Exception;
4use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
5 6
6/** 7/**
@@ -8,9 +9,6 @@ use Shaarli\Config\ConfigManager;
8 */ 9 */
9class LoginManager 10class LoginManager
10{ 11{
11 /** @var string Name of the cookie set after logging in **/
12 public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
13
14 /** @var array A reference to the $_GLOBALS array */ 12 /** @var array A reference to the $_GLOBALS array */
15 protected $globals = []; 13 protected $globals = [];
16 14
@@ -31,17 +29,21 @@ class LoginManager
31 29
32 /** @var string User sign-in token depending on remote IP and credentials */ 30 /** @var string User sign-in token depending on remote IP and credentials */
33 protected $staySignedInToken = ''; 31 protected $staySignedInToken = '';
32 /** @var CookieManager */
33 protected $cookieManager;
34 34
35 /** 35 /**
36 * Constructor 36 * Constructor
37 * 37 *
38 * @param ConfigManager $configManager Configuration Manager instance 38 * @param ConfigManager $configManager Configuration Manager instance
39 * @param SessionManager $sessionManager SessionManager instance 39 * @param SessionManager $sessionManager SessionManager instance
40 * @param CookieManager $cookieManager CookieManager instance
40 */ 41 */
41 public function __construct($configManager, $sessionManager) 42 public function __construct($configManager, $sessionManager, $cookieManager)
42 { 43 {
43 $this->configManager = $configManager; 44 $this->configManager = $configManager;
44 $this->sessionManager = $sessionManager; 45 $this->sessionManager = $sessionManager;
46 $this->cookieManager = $cookieManager;
45 $this->banManager = new BanManager( 47 $this->banManager = new BanManager(
46 $this->configManager->get('security.trusted_proxies', []), 48 $this->configManager->get('security.trusted_proxies', []),
47 $this->configManager->get('security.ban_after'), 49 $this->configManager->get('security.ban_after'),
@@ -85,10 +87,9 @@ class LoginManager
85 /** 87 /**
86 * Check user session state and validity (expiration) 88 * Check user session state and validity (expiration)
87 * 89 *
88 * @param array $cookie The $_COOKIE array
89 * @param string $clientIpId Client IP address identifier 90 * @param string $clientIpId Client IP address identifier
90 */ 91 */
91 public function checkLoginState($cookie, $clientIpId) 92 public function checkLoginState($clientIpId)
92 { 93 {
93 if (! $this->configManager->exists('credentials.login')) { 94 if (! $this->configManager->exists('credentials.login')) {
94 // Shaarli is not configured yet 95 // Shaarli is not configured yet
@@ -96,9 +97,7 @@ class LoginManager
96 return; 97 return;
97 } 98 }
98 99
99 if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) 100 if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) {
100 && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
101 ) {
102 // The user client has a valid stay-signed-in cookie 101 // The user client has a valid stay-signed-in cookie
103 // Session information is updated with the current client information 102 // Session information is updated with the current client information
104 $this->sessionManager->storeLoginInfo($clientIpId); 103 $this->sessionManager->storeLoginInfo($clientIpId);
@@ -139,26 +138,86 @@ class LoginManager
139 */ 138 */
140 public function checkCredentials($remoteIp, $clientIpId, $login, $password) 139 public function checkCredentials($remoteIp, $clientIpId, $login, $password)
141 { 140 {
142 $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); 141 // Check login matches config
142 if ($login !== $this->configManager->get('credentials.login')) {
143 return false;
144 }
143 145
144 if ($login != $this->configManager->get('credentials.login') 146 // Check credentials
145 || $hash != $this->configManager->get('credentials.hash') 147 try {
146 ) { 148 $useLdapLogin = !empty($this->configManager->get('ldap.host'));
149 if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password))
150 || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password))
151 ) {
152 $this->sessionManager->storeLoginInfo($clientIpId);
153 logm(
154 $this->configManager->get('resource.log'),
155 $remoteIp,
156 'Login successful'
157 );
158 return true;
159 }
160 }
161 catch(Exception $exception) {
147 logm( 162 logm(
148 $this->configManager->get('resource.log'), 163 $this->configManager->get('resource.log'),
149 $remoteIp, 164 $remoteIp,
150 'Login failed for user ' . $login 165 'Exception while checking credentials: ' . $exception
151 ); 166 );
152 return false;
153 } 167 }
154 168
155 $this->sessionManager->storeLoginInfo($clientIpId);
156 logm( 169 logm(
157 $this->configManager->get('resource.log'), 170 $this->configManager->get('resource.log'),
158 $remoteIp, 171 $remoteIp,
159 'Login successful' 172 'Login failed for user ' . $login
173 );
174 return false;
175 }
176
177
178 /**
179 * Check user credentials from local config
180 *
181 * @param string $login Username
182 * @param string $password Password
183 *
184 * @return bool true if the provided credentials are valid, false otherwise
185 */
186 public function checkCredentialsFromLocalConfig($login, $password) {
187 $hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
188
189 return $login == $this->configManager->get('credentials.login')
190 && $hash == $this->configManager->get('credentials.hash');
191 }
192
193 /**
194 * Check user credentials are valid through LDAP bind
195 *
196 * @param string $remoteIp Remote client IP address
197 * @param string $clientIpId Client IP address identifier
198 * @param string $login Username
199 * @param string $password Password
200 *
201 * @return bool true if the provided credentials are valid, false otherwise
202 */
203 public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null)
204 {
205 $connect = $connect ?? function($host) {
206 $resource = ldap_connect($host);
207
208 ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3);
209
210 return $resource;
211 };
212 $bind = $bind ?? function($handle, $dn, $password) {
213 return ldap_bind($handle, $dn, $password);
214 };
215
216 return $bind(
217 $connect($this->configManager->get('ldap.host')),
218 sprintf($this->configManager->get('ldap.dn'), $login),
219 $password
160 ); 220 );
161 return true;
162 } 221 }
163 222
164 /** 223 /**
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
index b8b8ab8d..36df8c1c 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
@@ -196,4 +222,84 @@ class SessionManager
196 } 222 }
197 return true; 223 return true;
198 } 224 }
225
226 /** @return array Local reference to the global $_SESSION array */
227 public function getSession(): array
228 {
229 return $this->session;
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 public function cookieParameters(int $lifeTime, string $path, string $domain): bool
297 {
298 return session_set_cookie_params($lifeTime, $path, $domain);
299 }
300
301 public function regenerateId(bool $deleteOldSession = false): bool
302 {
303 return session_regenerate_id($deleteOldSession);
304 }
199} 305}
diff --git a/application/updater/Updater.php b/application/updater/Updater.php
index beb9ea9b..88a7bc7b 100644
--- a/application/updater/Updater.php
+++ b/application/updater/Updater.php
@@ -2,25 +2,14 @@
2 2
3namespace Shaarli\Updater; 3namespace Shaarli\Updater;
4 4
5use Exception; 5use Shaarli\Bookmark\BookmarkServiceInterface;
6use RainTPL;
7use ReflectionClass;
8use ReflectionException;
9use ReflectionMethod;
10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\LinkDB;
12use Shaarli\Bookmark\LinkFilter;
13use Shaarli\Config\ConfigJson;
14use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
15use Shaarli\Config\ConfigPhp;
16use Shaarli\Exceptions\IOException;
17use Shaarli\Thumbnailer;
18use Shaarli\Updater\Exception\UpdaterException; 7use Shaarli\Updater\Exception\UpdaterException;
19 8
20/** 9/**
21 * Class updater. 10 * Class Updater.
22 * Used to update stuff when a new Shaarli's version is reached. 11 * Used to update stuff when a new Shaarli's version is reached.
23 * Update methods are ran only once, and the stored in a JSON file. 12 * Update methods are ran only once, and the stored in a TXT file.
24 */ 13 */
25class Updater 14class Updater
26{ 15{
@@ -30,9 +19,9 @@ class Updater
30 protected $doneUpdates; 19 protected $doneUpdates;
31 20
32 /** 21 /**
33 * @var LinkDB instance. 22 * @var BookmarkServiceInterface instance.
34 */ 23 */
35 protected $linkDB; 24 protected $bookmarkService;
36 25
37 /** 26 /**
38 * @var ConfigManager $conf Configuration Manager instance. 27 * @var ConfigManager $conf Configuration Manager instance.
@@ -45,36 +34,32 @@ class Updater
45 protected $isLoggedIn; 34 protected $isLoggedIn;
46 35
47 /** 36 /**
48 * @var array $_SESSION 37 * @var \ReflectionMethod[] List of current class methods.
49 */ 38 */
50 protected $session; 39 protected $methods;
51 40
52 /** 41 /**
53 * @var ReflectionMethod[] List of current class methods. 42 * @var string $basePath Shaarli root directory (from HTTP Request)
54 */ 43 */
55 protected $methods; 44 protected $basePath = null;
56 45
57 /** 46 /**
58 * Object constructor. 47 * Object constructor.
59 * 48 *
60 * @param array $doneUpdates Updates which are already done. 49 * @param array $doneUpdates Updates which are already done.
61 * @param LinkDB $linkDB LinkDB instance. 50 * @param BookmarkServiceInterface $linkDB LinksService instance.
62 * @param ConfigManager $conf Configuration Manager instance. 51 * @param ConfigManager $conf Configuration Manager instance.
63 * @param boolean $isLoggedIn True if the user is logged in. 52 * @param boolean $isLoggedIn True if the user is logged in.
64 * @param array $session $_SESSION (by reference)
65 *
66 * @throws ReflectionException
67 */ 53 */
68 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = []) 54 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
69 { 55 {
70 $this->doneUpdates = $doneUpdates; 56 $this->doneUpdates = $doneUpdates;
71 $this->linkDB = $linkDB; 57 $this->bookmarkService = $linkDB;
72 $this->conf = $conf; 58 $this->conf = $conf;
73 $this->isLoggedIn = $isLoggedIn; 59 $this->isLoggedIn = $isLoggedIn;
74 $this->session = &$session;
75 60
76 // Retrieve all update methods. 61 // Retrieve all update methods.
77 $class = new ReflectionClass($this); 62 $class = new \ReflectionClass($this);
78 $this->methods = $class->getMethods(); 63 $this->methods = $class->getMethods();
79 } 64 }
80 65
@@ -82,13 +67,15 @@ class Updater
82 * Run all new updates. 67 * Run all new updates.
83 * 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).
84 * 69 *
70 * @param string $basePath Shaarli root directory (from HTTP Request)
71 *
85 * @return array An array containing ran updates. 72 * @return array An array containing ran updates.
86 * 73 *
87 * @throws UpdaterException If something went wrong. 74 * @throws UpdaterException If something went wrong.
88 */ 75 */
89 public function update() 76 public function update(string $basePath = null)
90 { 77 {
91 $updatesRan = array(); 78 $updatesRan = [];
92 79
93 // If the user isn't logged in, exit without updating. 80 // If the user isn't logged in, exit without updating.
94 if ($this->isLoggedIn !== true) { 81 if ($this->isLoggedIn !== true) {
@@ -96,12 +83,12 @@ class Updater
96 } 83 }
97 84
98 if ($this->methods === null) { 85 if ($this->methods === null) {
99 throw new UpdaterException(t('Couldn\'t retrieve updater class methods.')); 86 throw new UpdaterException('Couldn\'t retrieve LegacyUpdater class methods.');
100 } 87 }
101 88
102 foreach ($this->methods as $method) { 89 foreach ($this->methods as $method) {
103 // Not an update method or already done, pass. 90 // Not an update method or already done, pass.
104 if (!startsWith($method->getName(), 'updateMethod') 91 if (! startsWith($method->getName(), 'updateMethod')
105 || in_array($method->getName(), $this->doneUpdates) 92 || in_array($method->getName(), $this->doneUpdates)
106 ) { 93 ) {
107 continue; 94 continue;
@@ -114,7 +101,7 @@ class Updater
114 if ($res === true) { 101 if ($res === true) {
115 $updatesRan[] = $method->getName(); 102 $updatesRan[] = $method->getName();
116 } 103 }
117 } catch (Exception $e) { 104 } catch (\Exception $e) {
118 throw new UpdaterException($method, $e); 105 throw new UpdaterException($method, $e);
119 } 106 }
120 } 107 }
@@ -132,431 +119,61 @@ class Updater
132 return $this->doneUpdates; 119 return $this->doneUpdates;
133 } 120 }
134 121
135 /** 122 public function readUpdates(string $updatesFilepath): array
136 * Move deprecated options.php to config.php.
137 *
138 * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
139 * options.php is not supported anymore.
140 */
141 public function updateMethodMergeDeprecatedConfigFile()
142 {
143 if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
144 include $this->conf->get('resource.data_dir') . '/options.php';
145
146 // Load GLOBALS into config
147 $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
148 $allowedKeys[] = 'config';
149 foreach ($GLOBALS as $key => $value) {
150 if (in_array($key, $allowedKeys)) {
151 $this->conf->set($key, $value);
152 }
153 }
154 $this->conf->write($this->isLoggedIn);
155 unlink($this->conf->get('resource.data_dir') . '/options.php');
156 }
157
158 return true;
159 }
160
161 /**
162 * Move old configuration in PHP to the new config system in JSON format.
163 *
164 * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
165 * It will also convert legacy setting keys to the new ones.
166 */
167 public function updateMethodConfigToJson()
168 {
169 // JSON config already exists, nothing to do.
170 if ($this->conf->getConfigIO() instanceof ConfigJson) {
171 return true;
172 }
173
174 $configPhp = new ConfigPhp();
175 $configJson = new ConfigJson();
176 $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
177 rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
178 $this->conf->setConfigIO($configJson);
179 $this->conf->reload();
180
181 $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
182 foreach (ConfigPhp::$ROOT_KEYS as $key) {
183 $this->conf->set($legacyMap[$key], $oldConfig[$key]);
184 }
185
186 // Set sub config keys (config and plugins)
187 $subConfig = array('config', 'plugins');
188 foreach ($subConfig as $sub) {
189 foreach ($oldConfig[$sub] as $key => $value) {
190 if (isset($legacyMap[$sub . '.' . $key])) {
191 $configKey = $legacyMap[$sub . '.' . $key];
192 } else {
193 $configKey = $sub . '.' . $key;
194 }
195 $this->conf->set($configKey, $value);
196 }
197 }
198
199 try {
200 $this->conf->write($this->isLoggedIn);
201 return true;
202 } catch (IOException $e) {
203 error_log($e->getMessage());
204 return false;
205 }
206 }
207
208 /**
209 * Escape settings which have been manually escaped in every request in previous versions:
210 * - general.title
211 * - general.header_link
212 * - redirector.url
213 *
214 * @return bool true if the update is successful, false otherwise.
215 */
216 public function updateMethodEscapeUnescapedConfig()
217 {
218 try {
219 $this->conf->set('general.title', escape($this->conf->get('general.title')));
220 $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
221 $this->conf->write($this->isLoggedIn);
222 } catch (Exception $e) {
223 error_log($e->getMessage());
224 return false;
225 }
226 return true;
227 }
228
229 /**
230 * Update the database to use the new ID system, which replaces linkdate primary keys.
231 * Also, creation and update dates are now DateTime objects (done by LinkDB).
232 *
233 * Since this update is very sensitve (changing the whole database), the datastore will be
234 * automatically backed up into the file datastore.<datetime>.php.
235 *
236 * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
237 * which will be saved by this method.
238 *
239 * @return bool true if the update is successful, false otherwise.
240 */
241 public function updateMethodDatastoreIds()
242 {
243 // up to date database
244 if (isset($this->linkDB[0])) {
245 return true;
246 }
247
248 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
249 copy($this->conf->get('resource.datastore'), $save);
250
251 $links = array();
252 foreach ($this->linkDB as $offset => $value) {
253 $links[] = $value;
254 unset($this->linkDB[$offset]);
255 }
256 $links = array_reverse($links);
257 $cpt = 0;
258 foreach ($links as $l) {
259 unset($l['linkdate']);
260 $l['id'] = $cpt;
261 $this->linkDB[$cpt++] = $l;
262 }
263
264 $this->linkDB->save($this->conf->get('resource.page_cache'));
265 $this->linkDB->reorder();
266
267 return true;
268 }
269
270 /**
271 * Rename tags starting with a '-' to work with tag exclusion search.
272 */
273 public function updateMethodRenameDashTags()
274 { 123 {
275 $linklist = $this->linkDB->filterSearch(); 124 return UpdaterUtils::read_updates_file($updatesFilepath);
276 foreach ($linklist as $key => $link) {
277 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
278 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
279 $this->linkDB[$key] = $link;
280 }
281 $this->linkDB->save($this->conf->get('resource.page_cache'));
282 return true;
283 } 125 }
284 126
285 /** 127 public function writeUpdates(string $updatesFilepath, array $updates): void
286 * Initialize API settings:
287 * - api.enabled: true
288 * - api.secret: generated secret
289 */
290 public function updateMethodApiSettings()
291 { 128 {
292 if ($this->conf->exists('api.secret')) { 129 UpdaterUtils::write_updates_file($updatesFilepath, $updates);
293 return true;
294 }
295
296 $this->conf->set('api.enabled', true);
297 $this->conf->set(
298 'api.secret',
299 generate_api_secret(
300 $this->conf->get('credentials.login'),
301 $this->conf->get('credentials.salt')
302 )
303 );
304 $this->conf->write($this->isLoggedIn);
305 return true;
306 } 130 }
307 131
308 /** 132 /**
309 * New setting: theme name. If the default theme is used, nothing to do. 133 * With the Slim routing system, default header link should be `/subfolder/` instead of `?`.
310 * 134 * Otherwise you can not go back to the home page.
311 * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory, 135 * Example: `/subfolder/picture-wall` -> `/subfolder/picture-wall?` instead of `/subfolder/`.
312 * and the current theme is set as default in the theme setting.
313 *
314 * @return bool true if the update is successful, false otherwise.
315 */ 136 */
316 public function updateMethodDefaultTheme() 137 public function updateMethodRelativeHomeLink(): bool
317 { 138 {
318 // raintpl_tpl isn't the root template directory anymore. 139 if ('?' === trim($this->conf->get('general.header_link'))) {
319 // We run the update only if this folder still contains the template files. 140 $this->conf->set('general.header_link', $this->basePath . '/', true, true);
320 $tplDir = $this->conf->get('resource.raintpl_tpl');
321 $tplFile = $tplDir . '/linklist.html';
322 if (!file_exists($tplFile)) {
323 return true;
324 } 141 }
325 142
326 $parent = dirname($tplDir);
327 $this->conf->set('resource.raintpl_tpl', $parent);
328 $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
329 $this->conf->write($this->isLoggedIn);
330
331 // Dependency injection gore
332 RainTPL::$tpl_dir = $tplDir;
333
334 return true; 143 return true;
335 } 144 }
336 145
337 /** 146 /**
338 * Move the file to inc/user.css to data/user.css. 147 * With the Slim routing system, note bookmarks URL formatted `?abcdef`
339 * 148 * should be replaced with `/shaare/abcdef`
340 * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
341 *
342 * @return bool true if the update is successful, false otherwise.
343 */ 149 */
344 public function updateMethodMoveUserCss() 150 public function updateMethodMigrateExistingNotesUrl(): bool
345 { 151 {
346 if (!is_file('inc/user.css')) { 152 $updated = false;
347 return true;
348 }
349
350 return rename('inc/user.css', 'data/user.css');
351 }
352 153
353 /** 154 foreach ($this->bookmarkService->search() as $bookmark) {
354 * * `markdown_escape` is a new setting, set to true as default. 155 if ($bookmark->isNote()
355 * 156 && startsWith($bookmark->getUrl(), '?')
356 * If the markdown plugin was already enabled, escaping is disabled to avoid 157 && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match)
357 * breaking existing entries. 158 ) {
358 */ 159 $updated = true;
359 public function updateMethodEscapeMarkdown() 160 $bookmark = $bookmark->setUrl('/shaare/' . $match[1]);
360 {
361 if ($this->conf->exists('security.markdown_escape')) {
362 return true;
363 }
364
365 if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
366 $this->conf->set('security.markdown_escape', false);
367 } else {
368 $this->conf->set('security.markdown_escape', true);
369 }
370 $this->conf->write($this->isLoggedIn);
371
372 return true;
373 }
374
375 /**
376 * Add 'http://' to Piwik URL the setting is set.
377 *
378 * @return bool true if the update is successful, false otherwise.
379 */
380 public function updateMethodPiwikUrl()
381 {
382 if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
383 return true;
384 }
385
386 $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
387 $this->conf->write($this->isLoggedIn);
388
389 return true;
390 }
391
392 /**
393 * Use ATOM feed as default.
394 */
395 public function updateMethodAtomDefault()
396 {
397 if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
398 return true;
399 }
400
401 $this->conf->set('feed.show_atom', true);
402 $this->conf->write($this->isLoggedIn);
403
404 return true;
405 }
406
407 /**
408 * Update updates.check_updates_branch setting.
409 *
410 * If the current major version digit matches the latest branch
411 * major version digit, we set the branch to `latest`,
412 * otherwise we'll check updates on the `stable` branch.
413 *
414 * No update required for the dev version.
415 *
416 * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
417 *
418 * FIXME! This needs to be removed when we switch to first digit major version
419 * instead of the second one since the versionning process will change.
420 */
421 public function updateMethodCheckUpdateRemoteBranch()
422 {
423 if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
424 return true;
425 }
426
427 // Get latest branch major version digit
428 $latestVersion = ApplicationUtils::getLatestGitVersionCode(
429 'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
430 5
431 );
432 if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
433 return false;
434 }
435 $latestMajor = $matches[1];
436
437 // Get current major version digit
438 preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
439 $currentMajor = $matches[1];
440
441 if ($currentMajor === $latestMajor) {
442 $branch = 'latest';
443 } else {
444 $branch = 'stable';
445 }
446 $this->conf->set('updates.check_updates_branch', $branch);
447 $this->conf->write($this->isLoggedIn);
448 return true;
449 }
450
451 /**
452 * Reset history store file due to date format change.
453 */
454 public function updateMethodResetHistoryFile()
455 {
456 if (is_file($this->conf->get('resource.history'))) {
457 unlink($this->conf->get('resource.history'));
458 }
459 return true;
460 }
461
462 /**
463 * Save the datastore -> the link order is now applied when links are saved.
464 */
465 public function updateMethodReorderDatastore()
466 {
467 $this->linkDB->save($this->conf->get('resource.page_cache'));
468 return true;
469 }
470
471 /**
472 * Change privateonly session key to visibility.
473 */
474 public function updateMethodVisibilitySession()
475 {
476 if (isset($_SESSION['privateonly'])) {
477 unset($_SESSION['privateonly']);
478 $_SESSION['visibility'] = 'private';
479 }
480 return true;
481 }
482
483 /**
484 * Add download size and timeout to the configuration file
485 *
486 * @return bool true if the update is successful, false otherwise.
487 */
488 public function updateMethodDownloadSizeAndTimeoutConf()
489 {
490 if ($this->conf->exists('general.download_max_size')
491 && $this->conf->exists('general.download_timeout')
492 ) {
493 return true;
494 }
495
496 if (!$this->conf->exists('general.download_max_size')) {
497 $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
498 }
499
500 if (!$this->conf->exists('general.download_timeout')) {
501 $this->conf->set('general.download_timeout', 30);
502 }
503
504 $this->conf->write($this->isLoggedIn);
505 return true;
506 }
507 161
508 /** 162 $this->bookmarkService->set($bookmark, false);
509 * * Move thumbnails management to WebThumbnailer, coming with new settings. 163 }
510 */
511 public function updateMethodWebThumbnailer()
512 {
513 if ($this->conf->exists('thumbnails.mode')) {
514 return true;
515 } 164 }
516 165
517 $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true); 166 if ($updated) {
518 $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE); 167 $this->bookmarkService->save();
519 $this->conf->set('thumbnails.width', 125);
520 $this->conf->set('thumbnails.height', 90);
521 $this->conf->remove('thumbnail');
522 $this->conf->write(true);
523
524 if ($thumbnailsEnabled) {
525 $this->session['warnings'][] = t(
526 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
527 );
528 } 168 }
529 169
530 return true; 170 return true;
531 } 171 }
532 172
533 /** 173 public function setBasePath(string $basePath): self
534 * Set sticky = false on all links
535 *
536 * @return bool true if the update is successful, false otherwise.
537 */
538 public function updateMethodSetSticky()
539 { 174 {
540 foreach ($this->linkDB as $key => $link) { 175 $this->basePath = $basePath;
541 if (isset($link['sticky'])) {
542 return true;
543 }
544 $link['sticky'] = false;
545 $this->linkDB[$key] = $link;
546 }
547
548 $this->linkDB->save($this->conf->get('resource.page_cache'));
549
550 return true;
551 }
552 176
553 /** 177 return $this;
554 * Remove redirector settings.
555 */
556 public function updateMethodRemoveRedirector()
557 {
558 $this->conf->remove('redirector');
559 $this->conf->write(true);
560 return true;
561 } 178 }
562} 179}
diff --git a/application/updater/UpdaterUtils.php b/application/updater/UpdaterUtils.php
index 34d4f422..828a49fc 100644
--- a/application/updater/UpdaterUtils.php
+++ b/application/updater/UpdaterUtils.php
@@ -1,39 +1,44 @@
1<?php 1<?php
2 2
3/** 3namespace Shaarli\Updater;
4 * Read the updates file, and return already done updates. 4
5 * 5class UpdaterUtils
6 * @param string $updatesFilepath Updates file path.
7 *
8 * @return array Already done update methods.
9 */
10function read_updates_file($updatesFilepath)
11{ 6{
12 if (! empty($updatesFilepath) && is_file($updatesFilepath)) { 7 /**
13 $content = file_get_contents($updatesFilepath); 8 * Read the updates file, and return already done updates.
14 if (! empty($content)) { 9 *
15 return explode(';', $content); 10 * @param string $updatesFilepath Updates file path.
11 *
12 * @return array Already done update methods.
13 */
14 public static function read_updates_file($updatesFilepath)
15 {
16 if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
17 $content = file_get_contents($updatesFilepath);
18 if (! empty($content)) {
19 return explode(';', $content);
20 }
16 } 21 }
22 return array();
17 } 23 }
18 return array();
19}
20 24
21/** 25 /**
22 * Write updates file. 26 * Write updates file.
23 * 27 *
24 * @param string $updatesFilepath Updates file path. 28 * @param string $updatesFilepath Updates file path.
25 * @param array $updates Updates array to write. 29 * @param array $updates Updates array to write.
26 * 30 *
27 * @throws Exception Couldn't write version number. 31 * @throws \Exception Couldn't write version number.
28 */ 32 */
29function write_updates_file($updatesFilepath, $updates) 33 public static function write_updates_file($updatesFilepath, $updates)
30{ 34 {
31 if (empty($updatesFilepath)) { 35 if (empty($updatesFilepath)) {
32 throw new Exception(t('Updates file path is not set, can\'t write updates.')); 36 throw new \Exception('Updates file path is not set, can\'t write updates.');
33 } 37 }
34 38
35 $res = file_put_contents($updatesFilepath, implode(';', $updates)); 39 $res = file_put_contents($updatesFilepath, implode(';', $updates));
36 if ($res === false) { 40 if ($res === false) {
37 throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.')); 41 throw new \Exception('Unable to write updates in '. $updatesFilepath . '.');
42 }
38 } 43 }
39} 44}
diff --git a/plugins/markdown/markdown.css b/assets/common/css/markdown.css
index ce19cd2a..f651e67e 100644
--- a/plugins/markdown/markdown.css
+++ b/assets/common/css/markdown.css
@@ -140,7 +140,7 @@
140 -webkit-hyphens: none; 140 -webkit-hyphens: none;
141 -moz-hyphens: none; 141 -moz-hyphens: none;
142 -ms-hyphens: none; 142 -ms-hyphens: none;
143 hyphens: none; 143 hyphens: none;
144} 144}
145 145
146.markdown :not(pre) code { 146.markdown :not(pre) code {
@@ -155,7 +155,7 @@
155} 155}
156 156
157/* 157/*
158 Remove header links style 158 Remove header bookmarks style
159 */ 159 */
160#pageheader .md_help a { 160#pageheader .md_help a {
161 color: lightgray; 161 color: lightgray;
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..be986ae0 100644
--- a/assets/default/js/base.js
+++ b/assets/default/js/base.js
@@ -10,7 +10,7 @@ import Awesomplete from 'awesomplete';
10 * @returns Found element or null. 10 * @returns Found element or null.
11 */ 11 */
12function findParent(element, tagName, attributes) { 12function findParent(element, tagName, attributes) {
13 const parentMatch = key => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1; 13 const parentMatch = (key) => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1;
14 while (element) { 14 while (element) {
15 if (element.tagName.toLowerCase() === tagName) { 15 if (element.tagName.toLowerCase() === tagName) {
16 if (Object.keys(attributes).find(parentMatch)) { 16 if (Object.keys(attributes).find(parentMatch)) {
@@ -25,12 +25,18 @@ function findParent(element, tagName, attributes) {
25/** 25/**
26 * Ajax request to refresh the CSRF token. 26 * Ajax request to refresh the CSRF token.
27 */ 27 */
28function refreshToken() { 28function refreshToken(basePath, callback) {
29 const xhr = new XMLHttpRequest(); 29 const xhr = new XMLHttpRequest();
30 xhr.open('GET', '?do=token'); 30 xhr.open('GET', `${basePath}/admin/token`);
31 xhr.onload = () => { 31 xhr.onload = () => {
32 const token = document.getElementById('token'); 32 const elements = document.querySelectorAll('input[name="token"]');
33 token.setAttribute('value', xhr.responseText); 33 [...elements].forEach((element) => {
34 element.setAttribute('value', xhr.responseText);
35 });
36
37 if (callback) {
38 callback(xhr.response);
39 }
34 }; 40 };
35 xhr.send(); 41 xhr.send();
36} 42}
@@ -95,7 +101,7 @@ function updateAwesompleteList(selector, tags, instances) {
95 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript 101 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
96 */ 102 */
97function htmlEntities(str) { 103function htmlEntities(str) {
98 return str.replace(/[\u00A0-\u9999<>&]/gim, i => `&#${i.charCodeAt(0)};`); 104 return str.replace(/[\u00A0-\u9999<>&]/gim, (i) => `&#${i.charCodeAt(0)};`);
99} 105}
100 106
101/** 107/**
@@ -188,8 +194,8 @@ function removeClass(element, classname) {
188function init(description) { 194function init(description) {
189 function resize() { 195 function resize() {
190 /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */ 196 /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
191 const scrollTop = window.pageYOffset || 197 const scrollTop = window.pageYOffset
192 (document.documentElement || document.body.parentNode || document.body).scrollTop; 198 || (document.documentElement || document.body.parentNode || document.body).scrollTop;
193 199
194 description.style.height = 'auto'; 200 description.style.height = 'auto';
195 description.style.height = `${description.scrollHeight + 10}px`; 201 description.style.height = `${description.scrollHeight + 10}px`;
@@ -215,6 +221,8 @@ function init(description) {
215} 221}
216 222
217(() => { 223(() => {
224 const basePath = document.querySelector('input[name="js_base_path"]').value;
225
218 /** 226 /**
219 * Handle responsive menu. 227 * Handle responsive menu.
220 * Source: http://purecss.io/layouts/tucked-menu-vertical/ 228 * Source: http://purecss.io/layouts/tucked-menu-vertical/
@@ -461,7 +469,7 @@ function init(description) {
461 }); 469 });
462 470
463 if (window.confirm(message)) { 471 if (window.confirm(message)) {
464 window.location = `?delete_link&lf_linkdate=${ids.join('+')}&token=${token.value}`; 472 window.location = `${basePath}/admin/shaare/delete?id=${ids.join('+')}&token=${token.value}`;
465 } 473 }
466 }); 474 });
467 } 475 }
@@ -482,8 +490,10 @@ function init(description) {
482 }); 490 });
483 }); 491 });
484 492
485 const ids = links.map(item => item.id); 493 const ids = links.map((item) => item.id);
486 window.location = `?change_visibility&token=${token.value}&newVisibility=${visibility}&ids=${ids.join('+')}`; 494 window.location = (
495 `${basePath}/admin/shaare/visibility?token=${token.value}&newVisibility=${visibility}&id=${ids.join('+')}`
496 );
487 }); 497 });
488 }); 498 });
489 } 499 }
@@ -545,8 +555,9 @@ function init(description) {
545 } 555 }
546 const refreshedToken = document.getElementById('token').value; 556 const refreshedToken = document.getElementById('token').value;
547 const fromtag = block.getAttribute('data-tag'); 557 const fromtag = block.getAttribute('data-tag');
558 const fromtagUrl = block.getAttribute('data-tag-url');
548 const xhr = new XMLHttpRequest(); 559 const xhr = new XMLHttpRequest();
549 xhr.open('POST', '?do=changetag'); 560 xhr.open('POST', `${basePath}/admin/tags`);
550 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 561 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
551 xhr.onload = () => { 562 xhr.onload = () => {
552 if (xhr.status !== 200) { 563 if (xhr.status !== 200) {
@@ -554,20 +565,28 @@ function init(description) {
554 location.reload(); 565 location.reload();
555 } else { 566 } else {
556 block.setAttribute('data-tag', totag); 567 block.setAttribute('data-tag', totag);
568 block.setAttribute('data-tag-url', encodeURIComponent(totag));
557 input.setAttribute('name', totag); 569 input.setAttribute('name', totag);
558 input.setAttribute('value', totag); 570 input.setAttribute('value', totag);
559 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none'; 571 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
560 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag); 572 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
561 block.querySelector('a.tag-link').setAttribute('href', `?searchtags=${encodeURIComponent(totag)}`); 573 block
562 block.querySelector('a.rename-tag').setAttribute('href', `?do=changetag&fromtag=${encodeURIComponent(totag)}`); 574 .querySelector('a.tag-link')
575 .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
576 block
577 .querySelector('a.count')
578 .setAttribute('href', `${basePath}/add-tag/${encodeURIComponent(totag)}`);
579 block
580 .querySelector('a.rename-tag')
581 .setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
563 582
564 // Refresh awesomplete values 583 // Refresh awesomplete values
565 existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag)); 584 existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag));
566 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); 585 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
567 } 586 }
568 }; 587 };
569 xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`); 588 xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
570 refreshToken(); 589 refreshToken(basePath);
571 }); 590 });
572 }); 591 });
573 592
@@ -589,19 +608,20 @@ function init(description) {
589 event.preventDefault(); 608 event.preventDefault();
590 const block = findParent(event.target, 'div', { class: 'tag-list-item' }); 609 const block = findParent(event.target, 'div', { class: 'tag-list-item' });
591 const tag = block.getAttribute('data-tag'); 610 const tag = block.getAttribute('data-tag');
611 const tagUrl = block.getAttribute('data-tag-url');
592 const refreshedToken = document.getElementById('token').value; 612 const refreshedToken = document.getElementById('token').value;
593 613
594 if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) { 614 if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
595 const xhr = new XMLHttpRequest(); 615 const xhr = new XMLHttpRequest();
596 xhr.open('POST', '?do=changetag'); 616 xhr.open('POST', `${basePath}/admin/tags`);
597 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 617 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
598 xhr.onload = () => { 618 xhr.onload = () => {
599 block.remove(); 619 block.remove();
600 }; 620 };
601 xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`)); 621 xhr.send(`deletetag=1&fromtag=${tagUrl}&token=${refreshedToken}`);
602 refreshToken(); 622 refreshToken(basePath);
603 623
604 existingTags = existingTags.filter(tagItem => tagItem !== tag); 624 existingTags = existingTags.filter((tagItem) => tagItem !== tag);
605 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); 625 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
606 } 626 }
607 }); 627 });
@@ -611,4 +631,15 @@ function init(description) {
611 [...autocompleteFields].forEach((autocompleteField) => { 631 [...autocompleteFields].forEach((autocompleteField) => {
612 awesomepletes.push(createAwesompleteInstance(autocompleteField)); 632 awesomepletes.push(createAwesompleteInstance(autocompleteField));
613 }); 633 });
634
635 const exportForm = document.querySelector('#exportform');
636 if (exportForm != null) {
637 exportForm.addEventListener('submit', (event) => {
638 event.preventDefault();
639
640 refreshToken(basePath, () => {
641 event.target.submit();
642 });
643 });
644 }
614})(); 645})();
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index 61e382b6..a528adb0 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 {
@@ -1236,8 +1246,19 @@ form {
1236 color: $dark-grey; 1246 color: $dark-grey;
1237} 1247}
1238 1248
1239.page404-container { 1249.page-error-container {
1240 color: $dark-grey; 1250 color: $dark-grey;
1251
1252 h2 {
1253 margin: 70px 0 25px;
1254 }
1255
1256 pre {
1257 margin: 0 20%;
1258 padding: 20px 0;
1259 text-align: left;
1260 line-height: .7em;
1261 }
1241} 1262}
1242 1263
1243// EDIT LINK 1264// EDIT LINK
@@ -1436,6 +1457,8 @@ form {
1436 -webkit-transition: opacity 500ms ease-in-out; 1457 -webkit-transition: opacity 500ms ease-in-out;
1437 -moz-transition: opacity 500ms ease-in-out; 1458 -moz-transition: opacity 500ms ease-in-out;
1438 -o-transition: opacity 500ms ease-in-out; 1459 -o-transition: opacity 500ms ease-in-out;
1460 min-width: 1px;
1461 min-height: 1px;
1439 1462
1440 &.b-loaded { 1463 &.b-loaded {
1441 opacity: 1; 1464 opacity: 1;
@@ -1535,11 +1558,11 @@ form {
1535 text-align: center; 1558 text-align: center;
1536 1559
1537 a { 1560 a {
1561 background: $almost-white;
1538 display: inline-block; 1562 display: inline-block;
1539 margin: 0 15px; 1563 padding: 5px;
1540 text-decoration: none; 1564 text-decoration: none;
1541 color: $white; 1565 color: $dark-grey;
1542 font-weight: bold;
1543 } 1566 }
1544} 1567}
1545 1568
@@ -1587,13 +1610,14 @@ form {
1587 1610
1588 > div { 1611 > div {
1589 border-radius: 10px; 1612 border-radius: 10px;
1590 background: repeating-linear-gradient( 1613 background:
1591 -45deg, 1614 repeating-linear-gradient(
1592 $almost-white, 1615 -45deg,
1593 $almost-white 6px, 1616 $almost-white,
1594 var(--background-color) 6px, 1617 $almost-white 6px,
1595 var(--background-color) 12px 1618 var(--background-color) 6px,
1596 ); 1619 var(--background-color) 12px
1620 );
1597 width: 0%; 1621 width: 0%;
1598 height: 10px; 1622 height: 10px;
1599 } 1623 }
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 109d5d7b..cd9fcf5b 100644
--- a/composer.json
+++ b/composer.json
@@ -21,15 +21,14 @@
21 "shaarli/netscape-bookmark-parser": "^2.1", 21 "shaarli/netscape-bookmark-parser": "^2.1",
22 "erusev/parsedown": "^1.6", 22 "erusev/parsedown": "^1.6",
23 "slim/slim": "^3.0", 23 "slim/slim": "^3.0",
24 "arthurhoaro/web-thumbnailer": "^1.1", 24 "arthurhoaro/web-thumbnailer": "^2.0",
25 "pubsubhubbub/publisher": "dev-master", 25 "pubsubhubbub/publisher": "dev-master",
26 "gettext/gettext": "^4.4" 26 "gettext/gettext": "^4.4"
27 }, 27 },
28 "require-dev": { 28 "require-dev": {
29 "roave/security-advisories": "dev-master", 29 "roave/security-advisories": "dev-master",
30 "phpunit/phpcov": "*", 30 "squizlabs/php_codesniffer": "3.*",
31 "phpunit/phpunit": "^5.0", 31 "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
32 "squizlabs/php_codesniffer": "2.*"
33 }, 32 },
34 "suggest": { 33 "suggest": {
35 "ext-curl": "Allows fetching web pages and thumbnails in a more robust way", 34 "ext-curl": "Allows fetching web pages and thumbnails in a more robust way",
@@ -48,9 +47,16 @@
48 "Shaarli\\Bookmark\\Exception\\": "application/bookmark/exception", 47 "Shaarli\\Bookmark\\Exception\\": "application/bookmark/exception",
49 "Shaarli\\Config\\": "application/config/", 48 "Shaarli\\Config\\": "application/config/",
50 "Shaarli\\Config\\Exception\\": "application/config/exception", 49 "Shaarli\\Config\\Exception\\": "application/config/exception",
50 "Shaarli\\Container\\": "application/container",
51 "Shaarli\\Exceptions\\": "application/exceptions", 51 "Shaarli\\Exceptions\\": "application/exceptions",
52 "Shaarli\\Feed\\": "application/feed", 52 "Shaarli\\Feed\\": "application/feed",
53 "Shaarli\\Formatter\\": "application/formatter",
54 "Shaarli\\Front\\": "application/front",
55 "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin",
56 "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor",
57 "Shaarli\\Front\\Exception\\": "application/front/exceptions",
53 "Shaarli\\Http\\": "application/http", 58 "Shaarli\\Http\\": "application/http",
59 "Shaarli\\Legacy\\": "application/legacy",
54 "Shaarli\\Netscape\\": "application/netscape", 60 "Shaarli\\Netscape\\": "application/netscape",
55 "Shaarli\\Plugin\\": "application/plugin", 61 "Shaarli\\Plugin\\": "application/plugin",
56 "Shaarli\\Plugin\\Exception\\": "application/plugin/exception", 62 "Shaarli\\Plugin\\Exception\\": "application/plugin/exception",
diff --git a/composer.lock b/composer.lock
index e6c938db..2c8b0ea7 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,33 +4,32 @@
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": "085b03ce38cd1106ad0e3af1ef3014c2", 7 "content-hash": "98520a05a7185503ee13d05ffaa535f6",
8 "packages": [ 8 "packages": [
9 { 9 {
10 "name": "arthurhoaro/web-thumbnailer", 10 "name": "arthurhoaro/web-thumbnailer",
11 "version": "v1.3.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": "7142bd94ec93719a756a7012ebb8e1c5813c6860" 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/7142bd94ec93719a756a7012ebb8e1c5813c6860", 19 "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/39bfd4f3136d9e6096496b9720e877326cfe4775",
20 "reference": "7142bd94ec93719a756a7012ebb8e1c5813c6860", 20 "reference": "39bfd4f3136d9e6096496b9720e877326cfe4775",
21 "shasum": "" 21 "shasum": ""
22 }, 22 },
23 "require": { 23 "require": {
24 "php": ">=5.6", 24 "php": ">=7.1",
25 "phpunit/php-text-template": "^1.2" 25 "phpunit/php-text-template": "^1.2 || ^2.0"
26 },
27 "conflict": {
28 "phpunit/php-timer": ">=2"
29 }, 26 },
30 "require-dev": { 27 "require-dev": {
28 "gskema/phpcs-type-sniff": "^0.13.1",
31 "php-coveralls/php-coveralls": "^2.0", 29 "php-coveralls/php-coveralls": "^2.0",
32 "phpunit/phpunit": "5.2.*", 30 "phpstan/phpstan": "^0.12.9",
33 "squizlabs/php_codesniffer": "^3.2" 31 "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
32 "squizlabs/php_codesniffer": "^3.0"
34 }, 33 },
35 "type": "library", 34 "type": "library",
36 "autoload": { 35 "autoload": {
@@ -52,51 +51,24 @@
52 } 51 }
53 ], 52 ],
54 "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",
55 "time": "2018-08-11T12:21:52+00:00" 54 "support": {
56 }, 55 "issues": "https://github.com/ArthurHoaro/web-thumbnailer/issues",
57 { 56 "source": "https://github.com/ArthurHoaro/web-thumbnailer/tree/v2.0.3"
58 "name": "container-interop/container-interop",
59 "version": "1.2.0",
60 "source": {
61 "type": "git",
62 "url": "https://github.com/container-interop/container-interop.git",
63 "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8"
64 },
65 "dist": {
66 "type": "zip",
67 "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8",
68 "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8",
69 "shasum": ""
70 }, 57 },
71 "require": { 58 "time": "2020-09-29T15:51:03+00:00"
72 "psr/container": "^1.0"
73 },
74 "type": "library",
75 "autoload": {
76 "psr-4": {
77 "Interop\\Container\\": "src/Interop/Container/"
78 }
79 },
80 "notification-url": "https://packagist.org/downloads/",
81 "license": [
82 "MIT"
83 ],
84 "description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
85 "homepage": "https://github.com/container-interop/container-interop",
86 "time": "2017-02-14T19:40:03+00:00"
87 }, 59 },
88 { 60 {
89 "name": "erusev/parsedown", 61 "name": "erusev/parsedown",
90 "version": "1.7.3", 62 "version": "1.7.4",
91 "source": { 63 "source": {
92 "type": "git", 64 "type": "git",
93 "url": "https://github.com/erusev/parsedown.git", 65 "url": "https://github.com/erusev/parsedown.git",
94 "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7" 66 "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
95 }, 67 },
96 "dist": { 68 "dist": {
97 "type": "zip", 69 "type": "zip",
98 "url": "https://api.github.com/repos/erusev/parsedown/zipball/6d893938171a817f4e9bc9e86f2da1e370b7bcd7", 70 "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
99 "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7", 71 "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
100 "shasum": "" 72 "shasum": ""
101 }, 73 },
102 "require": { 74 "require": {
@@ -129,20 +101,24 @@
129 "markdown", 101 "markdown",
130 "parser" 102 "parser"
131 ], 103 ],
132 "time": "2019-03-17T18:48:37+00:00" 104 "support": {
105 "issues": "https://github.com/erusev/parsedown/issues",
106 "source": "https://github.com/erusev/parsedown/tree/1.7.x"
107 },
108 "time": "2019-12-30T22:54:17+00:00"
133 }, 109 },
134 { 110 {
135 "name": "gettext/gettext", 111 "name": "gettext/gettext",
136 "version": "v4.6.3", 112 "version": "v4.8.2",
137 "source": { 113 "source": {
138 "type": "git", 114 "type": "git",
139 "url": "https://github.com/oscarotero/Gettext.git", 115 "url": "https://github.com/php-gettext/Gettext.git",
140 "reference": "70c6ff2fecd275e6ef9cdd542f55939a3d1904d6" 116 "reference": "e474f872f2c8636cf53fd283ec4ce1218f3d236a"
141 }, 117 },
142 "dist": { 118 "dist": {
143 "type": "zip", 119 "type": "zip",
144 "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/70c6ff2fecd275e6ef9cdd542f55939a3d1904d6", 120 "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/e474f872f2c8636cf53fd283ec4ce1218f3d236a",
145 "reference": "70c6ff2fecd275e6ef9cdd542f55939a3d1904d6", 121 "reference": "e474f872f2c8636cf53fd283ec4ce1218f3d236a",
146 "shasum": "" 122 "shasum": ""
147 }, 123 },
148 "require": { 124 "require": {
@@ -191,31 +167,36 @@
191 "po", 167 "po",
192 "translation" 168 "translation"
193 ], 169 ],
194 "time": "2019-07-15T12:56:31+00:00" 170 "support": {
171 "email": "oom@oscarotero.com",
172 "issues": "https://github.com/oscarotero/Gettext/issues",
173 "source": "https://github.com/php-gettext/Gettext/tree/v4.8.2"
174 },
175 "time": "2019-12-02T10:21:14+00:00"
195 }, 176 },
196 { 177 {
197 "name": "gettext/languages", 178 "name": "gettext/languages",
198 "version": "2.5.0", 179 "version": "2.6.0",
199 "source": { 180 "source": {
200 "type": "git", 181 "type": "git",
201 "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git", 182 "url": "https://github.com/php-gettext/Languages.git",
202 "reference": "78db2d17933f0765a102f368a6663f057162ddbd" 183 "reference": "38ea0482f649e0802e475f0ed19fa993bcb7a618"
203 }, 184 },
204 "dist": { 185 "dist": {
205 "type": "zip", 186 "type": "zip",
206 "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/78db2d17933f0765a102f368a6663f057162ddbd", 187 "url": "https://api.github.com/repos/php-gettext/Languages/zipball/38ea0482f649e0802e475f0ed19fa993bcb7a618",
207 "reference": "78db2d17933f0765a102f368a6663f057162ddbd", 188 "reference": "38ea0482f649e0802e475f0ed19fa993bcb7a618",
208 "shasum": "" 189 "shasum": ""
209 }, 190 },
210 "require": { 191 "require": {
211 "php": ">=5.3" 192 "php": ">=5.3"
212 }, 193 },
213 "require-dev": { 194 "require-dev": {
214 "phpunit/phpunit": "^4" 195 "friendsofphp/php-cs-fixer": "^2.16.0",
196 "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4"
215 }, 197 },
216 "bin": [ 198 "bin": [
217 "bin/export-plural-rules", 199 "bin/export-plural-rules"
218 "bin/export-plural-rules.php"
219 ], 200 ],
220 "type": "library", 201 "type": "library",
221 "autoload": { 202 "autoload": {
@@ -235,7 +216,7 @@
235 } 216 }
236 ], 217 ],
237 "description": "gettext languages with plural rules", 218 "description": "gettext languages with plural rules",
238 "homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules", 219 "homepage": "https://github.com/php-gettext/Languages",
239 "keywords": [ 220 "keywords": [
240 "cldr", 221 "cldr",
241 "i18n", 222 "i18n",
@@ -252,7 +233,11 @@
252 "translations", 233 "translations",
253 "unicode" 234 "unicode"
254 ], 235 ],
255 "time": "2018-11-13T22:06:07+00:00" 236 "support": {
237 "issues": "https://github.com/php-gettext/Languages/issues",
238 "source": "https://github.com/php-gettext/Languages/tree/2.6.0"
239 },
240 "time": "2019-11-13T10:30:21+00:00"
256 }, 241 },
257 { 242 {
258 "name": "katzgrau/klogger", 243 "name": "katzgrau/klogger",
@@ -302,6 +287,10 @@
302 "keywords": [ 287 "keywords": [
303 "logging" 288 "logging"
304 ], 289 ],
290 "support": {
291 "issues": "https://github.com/katzgrau/KLogger/issues",
292 "source": "https://github.com/katzgrau/KLogger/tree/master"
293 },
305 "time": "2016-11-07T19:29:14+00:00" 294 "time": "2016-11-07T19:29:14+00:00"
306 }, 295 },
307 { 296 {
@@ -348,6 +337,10 @@
348 "router", 337 "router",
349 "routing" 338 "routing"
350 ], 339 ],
340 "support": {
341 "issues": "https://github.com/nikic/FastRoute/issues",
342 "source": "https://github.com/nikic/FastRoute/tree/master"
343 },
351 "time": "2018-02-13T20:26:39+00:00" 344 "time": "2018-02-13T20:26:39+00:00"
352 }, 345 },
353 { 346 {
@@ -389,6 +382,10 @@
389 "keywords": [ 382 "keywords": [
390 "template" 383 "template"
391 ], 384 ],
385 "support": {
386 "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
387 "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
388 },
392 "time": "2015-06-21T13:50:34+00:00" 389 "time": "2015-06-21T13:50:34+00:00"
393 }, 390 },
394 { 391 {
@@ -439,6 +436,10 @@
439 "container", 436 "container",
440 "dependency injection" 437 "dependency injection"
441 ], 438 ],
439 "support": {
440 "issues": "https://github.com/silexphp/Pimple/issues",
441 "source": "https://github.com/silexphp/Pimple/tree/master"
442 },
442 "time": "2018-01-21T07:42:36+00:00" 443 "time": "2018-01-21T07:42:36+00:00"
443 }, 444 },
444 { 445 {
@@ -488,6 +489,10 @@
488 "container-interop", 489 "container-interop",
489 "psr" 490 "psr"
490 ], 491 ],
492 "support": {
493 "issues": "https://github.com/php-fig/container/issues",
494 "source": "https://github.com/php-fig/container/tree/master"
495 },
491 "time": "2017-02-14T16:28:37+00:00" 496 "time": "2017-02-14T16:28:37+00:00"
492 }, 497 },
493 { 498 {
@@ -538,20 +543,23 @@
538 "request", 543 "request",
539 "response" 544 "response"
540 ], 545 ],
546 "support": {
547 "source": "https://github.com/php-fig/http-message/tree/master"
548 },
541 "time": "2016-08-06T14:39:51+00:00" 549 "time": "2016-08-06T14:39:51+00:00"
542 }, 550 },
543 { 551 {
544 "name": "psr/log", 552 "name": "psr/log",
545 "version": "1.1.0", 553 "version": "1.1.3",
546 "source": { 554 "source": {
547 "type": "git", 555 "type": "git",
548 "url": "https://github.com/php-fig/log.git", 556 "url": "https://github.com/php-fig/log.git",
549 "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" 557 "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
550 }, 558 },
551 "dist": { 559 "dist": {
552 "type": "zip", 560 "type": "zip",
553 "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", 561 "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
554 "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", 562 "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
555 "shasum": "" 563 "shasum": ""
556 }, 564 },
557 "require": { 565 "require": {
@@ -560,7 +568,7 @@
560 "type": "library", 568 "type": "library",
561 "extra": { 569 "extra": {
562 "branch-alias": { 570 "branch-alias": {
563 "dev-master": "1.0.x-dev" 571 "dev-master": "1.1.x-dev"
564 } 572 }
565 }, 573 },
566 "autoload": { 574 "autoload": {
@@ -585,7 +593,10 @@
585 "psr", 593 "psr",
586 "psr-3" 594 "psr-3"
587 ], 595 ],
588 "time": "2018-11-20T15:27:04+00:00" 596 "support": {
597 "source": "https://github.com/php-fig/log/tree/1.1.3"
598 },
599 "time": "2020-03-23T09:12:05+00:00"
589 }, 600 },
590 { 601 {
591 "name": "pubsubhubbub/publisher", 602 "name": "pubsubhubbub/publisher",
@@ -605,6 +616,7 @@
605 "ext-curl": "*", 616 "ext-curl": "*",
606 "php": "~5.4 || ~7.0" 617 "php": "~5.4 || ~7.0"
607 }, 618 },
619 "default-branch": true,
608 "type": "library", 620 "type": "library",
609 "autoload": { 621 "autoload": {
610 "psr-4": { 622 "psr-4": {
@@ -630,20 +642,24 @@
630 "pubsubhubbub", 642 "pubsubhubbub",
631 "websub" 643 "websub"
632 ], 644 ],
645 "support": {
646 "issues": "https://github.com/pubsubhubbub/php-publisher/issues",
647 "source": "https://github.com/pubsubhubbub/php-publisher/tree/master"
648 },
633 "time": "2018-10-09T05:20:28+00:00" 649 "time": "2018-10-09T05:20:28+00:00"
634 }, 650 },
635 { 651 {
636 "name": "shaarli/netscape-bookmark-parser", 652 "name": "shaarli/netscape-bookmark-parser",
637 "version": "v2.1.0", 653 "version": "v2.2.0",
638 "source": { 654 "source": {
639 "type": "git", 655 "type": "git",
640 "url": "https://github.com/shaarli/netscape-bookmark-parser.git", 656 "url": "https://github.com/shaarli/netscape-bookmark-parser.git",
641 "reference": "819008ee42c4dd7e45d988176a4a22d6ed689577" 657 "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df"
642 }, 658 },
643 "dist": { 659 "dist": {
644 "type": "zip", 660 "type": "zip",
645 "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/819008ee42c4dd7e45d988176a4a22d6ed689577", 661 "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/432a010af2bb1832d6fbc4763e6b0100b980a1df",
646 "reference": "819008ee42c4dd7e45d988176a4a22d6ed689577", 662 "reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df",
647 "shasum": "" 663 "shasum": ""
648 }, 664 },
649 "require": { 665 "require": {
@@ -683,26 +699,32 @@
683 "bookmark", 699 "bookmark",
684 "link", 700 "link",
685 "netscape", 701 "netscape",
686 "parser" 702 "parse"
687 ], 703 ],
688 "time": "2018-10-06T14:43:38+00:00" 704 "support": {
705 "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues",
706 "source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v2.2.0"
707 },
708 "time": "2020-06-06T15:53:53+00:00"
689 }, 709 },
690 { 710 {
691 "name": "slim/slim", 711 "name": "slim/slim",
692 "version": "3.12.1", 712 "version": "3.12.3",
693 "source": { 713 "source": {
694 "type": "git", 714 "type": "git",
695 "url": "https://github.com/slimphp/Slim.git", 715 "url": "https://github.com/slimphp/Slim.git",
696 "reference": "eaee12ef8d0750db62b8c548016d82fb33addb6b" 716 "reference": "1c9318a84ffb890900901136d620b4f03a59da38"
697 }, 717 },
698 "dist": { 718 "dist": {
699 "type": "zip", 719 "type": "zip",
700 "url": "https://api.github.com/repos/slimphp/Slim/zipball/eaee12ef8d0750db62b8c548016d82fb33addb6b", 720 "url": "https://api.github.com/repos/slimphp/Slim/zipball/1c9318a84ffb890900901136d620b4f03a59da38",
701 "reference": "eaee12ef8d0750db62b8c548016d82fb33addb6b", 721 "reference": "1c9318a84ffb890900901136d620b4f03a59da38",
702 "shasum": "" 722 "shasum": ""
703 }, 723 },
704 "require": { 724 "require": {
705 "container-interop/container-interop": "^1.2", 725 "ext-json": "*",
726 "ext-libxml": "*",
727 "ext-simplexml": "*",
706 "nikic/fast-route": "^1.0", 728 "nikic/fast-route": "^1.0",
707 "php": ">=5.5.0", 729 "php": ">=5.5.0",
708 "pimple/pimple": "^3.0", 730 "pimple/pimple": "^3.0",
@@ -728,24 +750,24 @@
728 ], 750 ],
729 "authors": [ 751 "authors": [
730 { 752 {
731 "name": "Rob Allen",
732 "email": "rob@akrabat.com",
733 "homepage": "http://akrabat.com"
734 },
735 {
736 "name": "Josh Lockhart", 753 "name": "Josh Lockhart",
737 "email": "hello@joshlockhart.com", 754 "email": "hello@joshlockhart.com",
738 "homepage": "https://joshlockhart.com" 755 "homepage": "https://joshlockhart.com"
739 }, 756 },
740 { 757 {
741 "name": "Gabriel Manricks",
742 "email": "gmanricks@me.com",
743 "homepage": "http://gabrielmanricks.com"
744 },
745 {
746 "name": "Andrew Smith", 758 "name": "Andrew Smith",
747 "email": "a.smith@silentworks.co.uk", 759 "email": "a.smith@silentworks.co.uk",
748 "homepage": "http://silentworks.co.uk" 760 "homepage": "http://silentworks.co.uk"
761 },
762 {
763 "name": "Rob Allen",
764 "email": "rob@akrabat.com",
765 "homepage": "http://akrabat.com"
766 },
767 {
768 "name": "Gabriel Manricks",
769 "email": "gmanricks@me.com",
770 "homepage": "http://gabrielmanricks.com"
749 } 771 }
750 ], 772 ],
751 "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", 773 "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
@@ -756,26 +778,30 @@
756 "micro", 778 "micro",
757 "router" 779 "router"
758 ], 780 ],
759 "time": "2019-04-16T16:47:29+00:00" 781 "support": {
782 "issues": "https://github.com/slimphp/Slim/issues",
783 "source": "https://github.com/slimphp/Slim/tree/3.x"
784 },
785 "time": "2019-11-28T17:40:33+00:00"
760 } 786 }
761 ], 787 ],
762 "packages-dev": [ 788 "packages-dev": [
763 { 789 {
764 "name": "doctrine/instantiator", 790 "name": "doctrine/instantiator",
765 "version": "1.2.0", 791 "version": "1.3.1",
766 "source": { 792 "source": {
767 "type": "git", 793 "type": "git",
768 "url": "https://github.com/doctrine/instantiator.git", 794 "url": "https://github.com/doctrine/instantiator.git",
769 "reference": "a2c590166b2133a4633738648b6b064edae0814a" 795 "reference": "f350df0268e904597e3bd9c4685c53e0e333feea"
770 }, 796 },
771 "dist": { 797 "dist": {
772 "type": "zip", 798 "type": "zip",
773 "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", 799 "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea",
774 "reference": "a2c590166b2133a4633738648b6b064edae0814a", 800 "reference": "f350df0268e904597e3bd9c4685c53e0e333feea",
775 "shasum": "" 801 "shasum": ""
776 }, 802 },
777 "require": { 803 "require": {
778 "php": "^7.1" 804 "php": "^7.1 || ^8.0"
779 }, 805 },
780 "require-dev": { 806 "require-dev": {
781 "doctrine/coding-standard": "^6.0", 807 "doctrine/coding-standard": "^6.0",
@@ -814,24 +840,42 @@
814 "constructor", 840 "constructor",
815 "instantiate" 841 "instantiate"
816 ], 842 ],
817 "time": "2019-03-17T17:37:11+00:00" 843 "support": {
844 "issues": "https://github.com/doctrine/instantiator/issues",
845 "source": "https://github.com/doctrine/instantiator/tree/1.3.x"
846 },
847 "funding": [
848 {
849 "url": "https://www.doctrine-project.org/sponsorship.html",
850 "type": "custom"
851 },
852 {
853 "url": "https://www.patreon.com/phpdoctrine",
854 "type": "patreon"
855 },
856 {
857 "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
858 "type": "tidelift"
859 }
860 ],
861 "time": "2020-05-29T17:27:14+00:00"
818 }, 862 },
819 { 863 {
820 "name": "myclabs/deep-copy", 864 "name": "myclabs/deep-copy",
821 "version": "1.9.1", 865 "version": "1.10.1",
822 "source": { 866 "source": {
823 "type": "git", 867 "type": "git",
824 "url": "https://github.com/myclabs/DeepCopy.git", 868 "url": "https://github.com/myclabs/DeepCopy.git",
825 "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72" 869 "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
826 }, 870 },
827 "dist": { 871 "dist": {
828 "type": "zip", 872 "type": "zip",
829 "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", 873 "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
830 "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", 874 "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
831 "shasum": "" 875 "shasum": ""
832 }, 876 },
833 "require": { 877 "require": {
834 "php": "^7.1" 878 "php": "^7.1 || ^8.0"
835 }, 879 },
836 "replace": { 880 "replace": {
837 "myclabs/deep-copy": "self.version" 881 "myclabs/deep-copy": "self.version"
@@ -862,39 +906,154 @@
862 "object", 906 "object",
863 "object graph" 907 "object graph"
864 ], 908 ],
865 "time": "2019-04-07T13:18:21+00:00" 909 "support": {
910 "issues": "https://github.com/myclabs/DeepCopy/issues",
911 "source": "https://github.com/myclabs/DeepCopy/tree/1.x"
912 },
913 "funding": [
914 {
915 "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
916 "type": "tidelift"
917 }
918 ],
919 "time": "2020-06-29T13:22:24+00:00"
920 },
921 {
922 "name": "phar-io/manifest",
923 "version": "1.0.3",
924 "source": {
925 "type": "git",
926 "url": "https://github.com/phar-io/manifest.git",
927 "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
928 },
929 "dist": {
930 "type": "zip",
931 "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
932 "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
933 "shasum": ""
934 },
935 "require": {
936 "ext-dom": "*",
937 "ext-phar": "*",
938 "phar-io/version": "^2.0",
939 "php": "^5.6 || ^7.0"
940 },
941 "type": "library",
942 "extra": {
943 "branch-alias": {
944 "dev-master": "1.0.x-dev"
945 }
946 },
947 "autoload": {
948 "classmap": [
949 "src/"
950 ]
951 },
952 "notification-url": "https://packagist.org/downloads/",
953 "license": [
954 "BSD-3-Clause"
955 ],
956 "authors": [
957 {
958 "name": "Arne Blankerts",
959 "email": "arne@blankerts.de",
960 "role": "Developer"
961 },
962 {
963 "name": "Sebastian Heuer",
964 "email": "sebastian@phpeople.de",
965 "role": "Developer"
966 },
967 {
968 "name": "Sebastian Bergmann",
969 "email": "sebastian@phpunit.de",
970 "role": "Developer"
971 }
972 ],
973 "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
974 "support": {
975 "issues": "https://github.com/phar-io/manifest/issues",
976 "source": "https://github.com/phar-io/manifest/tree/master"
977 },
978 "time": "2018-07-08T19:23:20+00:00"
979 },
980 {
981 "name": "phar-io/version",
982 "version": "2.0.1",
983 "source": {
984 "type": "git",
985 "url": "https://github.com/phar-io/version.git",
986 "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
987 },
988 "dist": {
989 "type": "zip",
990 "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
991 "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
992 "shasum": ""
993 },
994 "require": {
995 "php": "^5.6 || ^7.0"
996 },
997 "type": "library",
998 "autoload": {
999 "classmap": [
1000 "src/"
1001 ]
1002 },
1003 "notification-url": "https://packagist.org/downloads/",
1004 "license": [
1005 "BSD-3-Clause"
1006 ],
1007 "authors": [
1008 {
1009 "name": "Arne Blankerts",
1010 "email": "arne@blankerts.de",
1011 "role": "Developer"
1012 },
1013 {
1014 "name": "Sebastian Heuer",
1015 "email": "sebastian@phpeople.de",
1016 "role": "Developer"
1017 },
1018 {
1019 "name": "Sebastian Bergmann",
1020 "email": "sebastian@phpunit.de",
1021 "role": "Developer"
1022 }
1023 ],
1024 "description": "Library for handling version information and constraints",
1025 "support": {
1026 "issues": "https://github.com/phar-io/version/issues",
1027 "source": "https://github.com/phar-io/version/tree/master"
1028 },
1029 "time": "2018-07-08T19:19:57+00:00"
866 }, 1030 },
867 { 1031 {
868 "name": "phpdocumentor/reflection-common", 1032 "name": "phpdocumentor/reflection-common",
869 "version": "1.0.1", 1033 "version": "2.1.0",
870 "source": { 1034 "source": {
871 "type": "git", 1035 "type": "git",
872 "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 1036 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
873 "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" 1037 "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
874 }, 1038 },
875 "dist": { 1039 "dist": {
876 "type": "zip", 1040 "type": "zip",
877 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 1041 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
878 "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 1042 "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
879 "shasum": "" 1043 "shasum": ""
880 }, 1044 },
881 "require": { 1045 "require": {
882 "php": ">=5.5" 1046 "php": ">=7.1"
883 },
884 "require-dev": {
885 "phpunit/phpunit": "^4.6"
886 }, 1047 },
887 "type": "library", 1048 "type": "library",
888 "extra": { 1049 "extra": {
889 "branch-alias": { 1050 "branch-alias": {
890 "dev-master": "1.0.x-dev" 1051 "dev-master": "2.x-dev"
891 } 1052 }
892 }, 1053 },
893 "autoload": { 1054 "autoload": {
894 "psr-4": { 1055 "psr-4": {
895 "phpDocumentor\\Reflection\\": [ 1056 "phpDocumentor\\Reflection\\": "src/"
896 "src"
897 ]
898 } 1057 }
899 }, 1058 },
900 "notification-url": "https://packagist.org/downloads/", 1059 "notification-url": "https://packagist.org/downloads/",
@@ -916,31 +1075,36 @@
916 "reflection", 1075 "reflection",
917 "static analysis" 1076 "static analysis"
918 ], 1077 ],
919 "time": "2017-09-11T18:02:19+00:00" 1078 "support": {
1079 "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
1080 "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master"
1081 },
1082 "time": "2020-04-27T09:25:28+00:00"
920 }, 1083 },
921 { 1084 {
922 "name": "phpdocumentor/reflection-docblock", 1085 "name": "phpdocumentor/reflection-docblock",
923 "version": "4.3.1", 1086 "version": "4.3.4",
924 "source": { 1087 "source": {
925 "type": "git", 1088 "type": "git",
926 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 1089 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
927 "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c" 1090 "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c"
928 }, 1091 },
929 "dist": { 1092 "dist": {
930 "type": "zip", 1093 "type": "zip",
931 "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", 1094 "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c",
932 "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", 1095 "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c",
933 "shasum": "" 1096 "shasum": ""
934 }, 1097 },
935 "require": { 1098 "require": {
936 "php": "^7.0", 1099 "php": "^7.0",
937 "phpdocumentor/reflection-common": "^1.0.0", 1100 "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
938 "phpdocumentor/type-resolver": "^0.4.0", 1101 "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
939 "webmozart/assert": "^1.0" 1102 "webmozart/assert": "^1.0"
940 }, 1103 },
941 "require-dev": { 1104 "require-dev": {
942 "doctrine/instantiator": "~1.0.5", 1105 "doctrine/instantiator": "^1.0.5",
943 "mockery/mockery": "^1.0", 1106 "mockery/mockery": "^1.0",
1107 "phpdocumentor/type-resolver": "0.4.*",
944 "phpunit/phpunit": "^6.4" 1108 "phpunit/phpunit": "^6.4"
945 }, 1109 },
946 "type": "library", 1110 "type": "library",
@@ -967,41 +1131,44 @@
967 } 1131 }
968 ], 1132 ],
969 "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 1133 "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
970 "time": "2019-04-30T17:48:53+00:00" 1134 "support": {
1135 "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
1136 "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/4.x"
1137 },
1138 "time": "2019-12-28T18:55:12+00:00"
971 }, 1139 },
972 { 1140 {
973 "name": "phpdocumentor/type-resolver", 1141 "name": "phpdocumentor/type-resolver",
974 "version": "0.4.0", 1142 "version": "1.0.1",
975 "source": { 1143 "source": {
976 "type": "git", 1144 "type": "git",
977 "url": "https://github.com/phpDocumentor/TypeResolver.git", 1145 "url": "https://github.com/phpDocumentor/TypeResolver.git",
978 "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" 1146 "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
979 }, 1147 },
980 "dist": { 1148 "dist": {
981 "type": "zip", 1149 "type": "zip",
982 "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", 1150 "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
983 "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", 1151 "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
984 "shasum": "" 1152 "shasum": ""
985 }, 1153 },
986 "require": { 1154 "require": {
987 "php": "^5.5 || ^7.0", 1155 "php": "^7.1",
988 "phpdocumentor/reflection-common": "^1.0" 1156 "phpdocumentor/reflection-common": "^2.0"
989 }, 1157 },
990 "require-dev": { 1158 "require-dev": {
991 "mockery/mockery": "^0.9.4", 1159 "ext-tokenizer": "^7.1",
992 "phpunit/phpunit": "^5.2||^4.8.24" 1160 "mockery/mockery": "~1",
1161 "phpunit/phpunit": "^7.0"
993 }, 1162 },
994 "type": "library", 1163 "type": "library",
995 "extra": { 1164 "extra": {
996 "branch-alias": { 1165 "branch-alias": {
997 "dev-master": "1.0.x-dev" 1166 "dev-master": "1.x-dev"
998 } 1167 }
999 }, 1168 },
1000 "autoload": { 1169 "autoload": {
1001 "psr-4": { 1170 "psr-4": {
1002 "phpDocumentor\\Reflection\\": [ 1171 "phpDocumentor\\Reflection\\": "src"
1003 "src/"
1004 ]
1005 } 1172 }
1006 }, 1173 },
1007 "notification-url": "https://packagist.org/downloads/", 1174 "notification-url": "https://packagist.org/downloads/",
@@ -1014,37 +1181,42 @@
1014 "email": "me@mikevanriel.com" 1181 "email": "me@mikevanriel.com"
1015 } 1182 }
1016 ], 1183 ],
1017 "time": "2017-07-14T14:27:02+00:00" 1184 "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
1185 "support": {
1186 "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
1187 "source": "https://github.com/phpDocumentor/TypeResolver/tree/0.7.2"
1188 },
1189 "time": "2019-08-22T18:11:29+00:00"
1018 }, 1190 },
1019 { 1191 {
1020 "name": "phpspec/prophecy", 1192 "name": "phpspec/prophecy",
1021 "version": "1.8.1", 1193 "version": "v1.10.3",
1022 "source": { 1194 "source": {
1023 "type": "git", 1195 "type": "git",
1024 "url": "https://github.com/phpspec/prophecy.git", 1196 "url": "https://github.com/phpspec/prophecy.git",
1025 "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76" 1197 "reference": "451c3cd1418cf640de218914901e51b064abb093"
1026 }, 1198 },
1027 "dist": { 1199 "dist": {
1028 "type": "zip", 1200 "type": "zip",
1029 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76", 1201 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
1030 "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76", 1202 "reference": "451c3cd1418cf640de218914901e51b064abb093",
1031 "shasum": "" 1203 "shasum": ""
1032 }, 1204 },
1033 "require": { 1205 "require": {
1034 "doctrine/instantiator": "^1.0.2", 1206 "doctrine/instantiator": "^1.0.2",
1035 "php": "^5.3|^7.0", 1207 "php": "^5.3|^7.0",
1036 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", 1208 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
1037 "sebastian/comparator": "^1.1|^2.0|^3.0", 1209 "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
1038 "sebastian/recursion-context": "^1.0|^2.0|^3.0" 1210 "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
1039 }, 1211 },
1040 "require-dev": { 1212 "require-dev": {
1041 "phpspec/phpspec": "^2.5|^3.2", 1213 "phpspec/phpspec": "^2.5 || ^3.2",
1042 "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 1214 "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
1043 }, 1215 },
1044 "type": "library", 1216 "type": "library",
1045 "extra": { 1217 "extra": {
1046 "branch-alias": { 1218 "branch-alias": {
1047 "dev-master": "1.8.x-dev" 1219 "dev-master": "1.10.x-dev"
1048 } 1220 }
1049 }, 1221 },
1050 "autoload": { 1222 "autoload": {
@@ -1077,44 +1249,48 @@
1077 "spy", 1249 "spy",
1078 "stub" 1250 "stub"
1079 ], 1251 ],
1080 "time": "2019-06-13T12:50:23+00:00" 1252 "support": {
1253 "issues": "https://github.com/phpspec/prophecy/issues",
1254 "source": "https://github.com/phpspec/prophecy/tree/v1.10.3"
1255 },
1256 "time": "2020-03-05T15:02:03+00:00"
1081 }, 1257 },
1082 { 1258 {
1083 "name": "phpunit/php-code-coverage", 1259 "name": "phpunit/php-code-coverage",
1084 "version": "4.0.8", 1260 "version": "6.1.4",
1085 "source": { 1261 "source": {
1086 "type": "git", 1262 "type": "git",
1087 "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 1263 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
1088 "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" 1264 "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
1089 }, 1265 },
1090 "dist": { 1266 "dist": {
1091 "type": "zip", 1267 "type": "zip",
1092 "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", 1268 "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
1093 "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", 1269 "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
1094 "shasum": "" 1270 "shasum": ""
1095 }, 1271 },
1096 "require": { 1272 "require": {
1097 "ext-dom": "*", 1273 "ext-dom": "*",
1098 "ext-xmlwriter": "*", 1274 "ext-xmlwriter": "*",
1099 "php": "^5.6 || ^7.0", 1275 "php": "^7.1",
1100 "phpunit/php-file-iterator": "^1.3", 1276 "phpunit/php-file-iterator": "^2.0",
1101 "phpunit/php-text-template": "^1.2", 1277 "phpunit/php-text-template": "^1.2.1",
1102 "phpunit/php-token-stream": "^1.4.2 || ^2.0", 1278 "phpunit/php-token-stream": "^3.0",
1103 "sebastian/code-unit-reverse-lookup": "^1.0", 1279 "sebastian/code-unit-reverse-lookup": "^1.0.1",
1104 "sebastian/environment": "^1.3.2 || ^2.0", 1280 "sebastian/environment": "^3.1 || ^4.0",
1105 "sebastian/version": "^1.0 || ^2.0" 1281 "sebastian/version": "^2.0.1",
1282 "theseer/tokenizer": "^1.1"
1106 }, 1283 },
1107 "require-dev": { 1284 "require-dev": {
1108 "ext-xdebug": "^2.1.4", 1285 "phpunit/phpunit": "^7.0"
1109 "phpunit/phpunit": "^5.7"
1110 }, 1286 },
1111 "suggest": { 1287 "suggest": {
1112 "ext-xdebug": "^2.5.1" 1288 "ext-xdebug": "^2.6.0"
1113 }, 1289 },
1114 "type": "library", 1290 "type": "library",
1115 "extra": { 1291 "extra": {
1116 "branch-alias": { 1292 "branch-alias": {
1117 "dev-master": "4.0.x-dev" 1293 "dev-master": "6.1-dev"
1118 } 1294 }
1119 }, 1295 },
1120 "autoload": { 1296 "autoload": {
@@ -1129,7 +1305,7 @@
1129 "authors": [ 1305 "authors": [
1130 { 1306 {
1131 "name": "Sebastian Bergmann", 1307 "name": "Sebastian Bergmann",
1132 "email": "sb@sebastian-bergmann.de", 1308 "email": "sebastian@phpunit.de",
1133 "role": "lead" 1309 "role": "lead"
1134 } 1310 }
1135 ], 1311 ],
@@ -1140,29 +1316,36 @@
1140 "testing", 1316 "testing",
1141 "xunit" 1317 "xunit"
1142 ], 1318 ],
1143 "time": "2017-04-02T07:44:40+00:00" 1319 "support": {
1320 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
1321 "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/master"
1322 },
1323 "time": "2018-10-31T16:06:48+00:00"
1144 }, 1324 },
1145 { 1325 {
1146 "name": "phpunit/php-file-iterator", 1326 "name": "phpunit/php-file-iterator",
1147 "version": "1.4.5", 1327 "version": "2.0.2",
1148 "source": { 1328 "source": {
1149 "type": "git", 1329 "type": "git",
1150 "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 1330 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
1151 "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" 1331 "reference": "050bedf145a257b1ff02746c31894800e5122946"
1152 }, 1332 },
1153 "dist": { 1333 "dist": {
1154 "type": "zip", 1334 "type": "zip",
1155 "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", 1335 "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
1156 "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", 1336 "reference": "050bedf145a257b1ff02746c31894800e5122946",
1157 "shasum": "" 1337 "shasum": ""
1158 }, 1338 },
1159 "require": { 1339 "require": {
1160 "php": ">=5.3.3" 1340 "php": "^7.1"
1341 },
1342 "require-dev": {
1343 "phpunit/phpunit": "^7.1"
1161 }, 1344 },
1162 "type": "library", 1345 "type": "library",
1163 "extra": { 1346 "extra": {
1164 "branch-alias": { 1347 "branch-alias": {
1165 "dev-master": "1.4.x-dev" 1348 "dev-master": "2.0.x-dev"
1166 } 1349 }
1167 }, 1350 },
1168 "autoload": { 1351 "autoload": {
@@ -1177,7 +1360,7 @@
1177 "authors": [ 1360 "authors": [
1178 { 1361 {
1179 "name": "Sebastian Bergmann", 1362 "name": "Sebastian Bergmann",
1180 "email": "sb@sebastian-bergmann.de", 1363 "email": "sebastian@phpunit.de",
1181 "role": "lead" 1364 "role": "lead"
1182 } 1365 }
1183 ], 1366 ],
@@ -1187,32 +1370,36 @@
1187 "filesystem", 1370 "filesystem",
1188 "iterator" 1371 "iterator"
1189 ], 1372 ],
1190 "time": "2017-11-27T13:52:08+00:00" 1373 "support": {
1374 "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
1375 "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.2"
1376 },
1377 "time": "2018-09-13T20:33:42+00:00"
1191 }, 1378 },
1192 { 1379 {
1193 "name": "phpunit/php-timer", 1380 "name": "phpunit/php-timer",
1194 "version": "1.0.9", 1381 "version": "2.1.2",
1195 "source": { 1382 "source": {
1196 "type": "git", 1383 "type": "git",
1197 "url": "https://github.com/sebastianbergmann/php-timer.git", 1384 "url": "https://github.com/sebastianbergmann/php-timer.git",
1198 "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" 1385 "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
1199 }, 1386 },
1200 "dist": { 1387 "dist": {
1201 "type": "zip", 1388 "type": "zip",
1202 "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 1389 "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
1203 "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 1390 "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
1204 "shasum": "" 1391 "shasum": ""
1205 }, 1392 },
1206 "require": { 1393 "require": {
1207 "php": "^5.3.3 || ^7.0" 1394 "php": "^7.1"
1208 }, 1395 },
1209 "require-dev": { 1396 "require-dev": {
1210 "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" 1397 "phpunit/phpunit": "^7.0"
1211 }, 1398 },
1212 "type": "library", 1399 "type": "library",
1213 "extra": { 1400 "extra": {
1214 "branch-alias": { 1401 "branch-alias": {
1215 "dev-master": "1.0-dev" 1402 "dev-master": "2.1-dev"
1216 } 1403 }
1217 }, 1404 },
1218 "autoload": { 1405 "autoload": {
@@ -1227,7 +1414,7 @@
1227 "authors": [ 1414 "authors": [
1228 { 1415 {
1229 "name": "Sebastian Bergmann", 1416 "name": "Sebastian Bergmann",
1230 "email": "sb@sebastian-bergmann.de", 1417 "email": "sebastian@phpunit.de",
1231 "role": "lead" 1418 "role": "lead"
1232 } 1419 }
1233 ], 1420 ],
@@ -1236,33 +1423,37 @@
1236 "keywords": [ 1423 "keywords": [
1237 "timer" 1424 "timer"
1238 ], 1425 ],
1239 "time": "2017-02-26T11:10:40+00:00" 1426 "support": {
1427 "issues": "https://github.com/sebastianbergmann/php-timer/issues",
1428 "source": "https://github.com/sebastianbergmann/php-timer/tree/master"
1429 },
1430 "time": "2019-06-07T04:22:29+00:00"
1240 }, 1431 },
1241 { 1432 {
1242 "name": "phpunit/php-token-stream", 1433 "name": "phpunit/php-token-stream",
1243 "version": "2.0.2", 1434 "version": "3.1.1",
1244 "source": { 1435 "source": {
1245 "type": "git", 1436 "type": "git",
1246 "url": "https://github.com/sebastianbergmann/php-token-stream.git", 1437 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
1247 "reference": "791198a2c6254db10131eecfe8c06670700904db" 1438 "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff"
1248 }, 1439 },
1249 "dist": { 1440 "dist": {
1250 "type": "zip", 1441 "type": "zip",
1251 "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", 1442 "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff",
1252 "reference": "791198a2c6254db10131eecfe8c06670700904db", 1443 "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff",
1253 "shasum": "" 1444 "shasum": ""
1254 }, 1445 },
1255 "require": { 1446 "require": {
1256 "ext-tokenizer": "*", 1447 "ext-tokenizer": "*",
1257 "php": "^7.0" 1448 "php": "^7.1"
1258 }, 1449 },
1259 "require-dev": { 1450 "require-dev": {
1260 "phpunit/phpunit": "^6.2.4" 1451 "phpunit/phpunit": "^7.0"
1261 }, 1452 },
1262 "type": "library", 1453 "type": "library",
1263 "extra": { 1454 "extra": {
1264 "branch-alias": { 1455 "branch-alias": {
1265 "dev-master": "2.0-dev" 1456 "dev-master": "3.1-dev"
1266 } 1457 }
1267 }, 1458 },
1268 "autoload": { 1459 "autoload": {
@@ -1285,107 +1476,62 @@
1285 "keywords": [ 1476 "keywords": [
1286 "tokenizer" 1477 "tokenizer"
1287 ], 1478 ],
1288 "time": "2017-11-27T05:48:46+00:00" 1479 "support": {
1289 }, 1480 "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
1290 { 1481 "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.1"
1291 "name": "phpunit/phpcov",
1292 "version": "3.1.0",
1293 "source": {
1294 "type": "git",
1295 "url": "https://github.com/sebastianbergmann/phpcov.git",
1296 "reference": "2005bd90c2c8aae6d93ec82d9cda9d55dca96c3d"
1297 },
1298 "dist": {
1299 "type": "zip",
1300 "url": "https://api.github.com/repos/sebastianbergmann/phpcov/zipball/2005bd90c2c8aae6d93ec82d9cda9d55dca96c3d",
1301 "reference": "2005bd90c2c8aae6d93ec82d9cda9d55dca96c3d",
1302 "shasum": ""
1303 },
1304 "require": {
1305 "php": "^5.6 || ^7.0",
1306 "phpunit/php-code-coverage": "^4.0",
1307 "phpunit/phpunit": "^5.0",
1308 "sebastian/diff": "^1.1",
1309 "sebastian/finder-facade": "^1.1",
1310 "sebastian/version": "^1.0|^2.0",
1311 "symfony/console": "^2|^3"
1312 },
1313 "bin": [
1314 "phpcov"
1315 ],
1316 "type": "library",
1317 "extra": {
1318 "branch-alias": {
1319 "dev-master": "3.1.x-dev"
1320 }
1321 }, 1482 },
1322 "autoload": { 1483 "abandoned": true,
1323 "classmap": [ 1484 "time": "2019-09-17T06:23:10+00:00"
1324 "src/"
1325 ]
1326 },
1327 "notification-url": "https://packagist.org/downloads/",
1328 "license": [
1329 "BSD-3-Clause"
1330 ],
1331 "authors": [
1332 {
1333 "name": "Sebastian Bergmann",
1334 "email": "sebastian@phpunit.de",
1335 "role": "lead"
1336 }
1337 ],
1338 "description": "CLI frontend for PHP_CodeCoverage",
1339 "homepage": "https://github.com/sebastianbergmann/phpcov",
1340 "time": "2016-06-03T07:01:55+00:00"
1341 }, 1485 },
1342 { 1486 {
1343 "name": "phpunit/phpunit", 1487 "name": "phpunit/phpunit",
1344 "version": "5.7.27", 1488 "version": "7.5.20",
1345 "source": { 1489 "source": {
1346 "type": "git", 1490 "type": "git",
1347 "url": "https://github.com/sebastianbergmann/phpunit.git", 1491 "url": "https://github.com/sebastianbergmann/phpunit.git",
1348 "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c" 1492 "reference": "9467db479d1b0487c99733bb1e7944d32deded2c"
1349 }, 1493 },
1350 "dist": { 1494 "dist": {
1351 "type": "zip", 1495 "type": "zip",
1352 "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", 1496 "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c",
1353 "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", 1497 "reference": "9467db479d1b0487c99733bb1e7944d32deded2c",
1354 "shasum": "" 1498 "shasum": ""
1355 }, 1499 },
1356 "require": { 1500 "require": {
1501 "doctrine/instantiator": "^1.1",
1357 "ext-dom": "*", 1502 "ext-dom": "*",
1358 "ext-json": "*", 1503 "ext-json": "*",
1359 "ext-libxml": "*", 1504 "ext-libxml": "*",
1360 "ext-mbstring": "*", 1505 "ext-mbstring": "*",
1361 "ext-xml": "*", 1506 "ext-xml": "*",
1362 "myclabs/deep-copy": "~1.3", 1507 "myclabs/deep-copy": "^1.7",
1363 "php": "^5.6 || ^7.0", 1508 "phar-io/manifest": "^1.0.2",
1364 "phpspec/prophecy": "^1.6.2", 1509 "phar-io/version": "^2.0",
1365 "phpunit/php-code-coverage": "^4.0.4", 1510 "php": "^7.1",
1366 "phpunit/php-file-iterator": "~1.4", 1511 "phpspec/prophecy": "^1.7",
1367 "phpunit/php-text-template": "~1.2", 1512 "phpunit/php-code-coverage": "^6.0.7",
1368 "phpunit/php-timer": "^1.0.6", 1513 "phpunit/php-file-iterator": "^2.0.1",
1369 "phpunit/phpunit-mock-objects": "^3.2", 1514 "phpunit/php-text-template": "^1.2.1",
1370 "sebastian/comparator": "^1.2.4", 1515 "phpunit/php-timer": "^2.1",
1371 "sebastian/diff": "^1.4.3", 1516 "sebastian/comparator": "^3.0",
1372 "sebastian/environment": "^1.3.4 || ^2.0", 1517 "sebastian/diff": "^3.0",
1373 "sebastian/exporter": "~2.0", 1518 "sebastian/environment": "^4.0",
1374 "sebastian/global-state": "^1.1", 1519 "sebastian/exporter": "^3.1",
1375 "sebastian/object-enumerator": "~2.0", 1520 "sebastian/global-state": "^2.0",
1376 "sebastian/resource-operations": "~1.0", 1521 "sebastian/object-enumerator": "^3.0.3",
1377 "sebastian/version": "^1.0.6|^2.0.1", 1522 "sebastian/resource-operations": "^2.0",
1378 "symfony/yaml": "~2.1|~3.0|~4.0" 1523 "sebastian/version": "^2.0.1"
1379 }, 1524 },
1380 "conflict": { 1525 "conflict": {
1381 "phpdocumentor/reflection-docblock": "3.0.2" 1526 "phpunit/phpunit-mock-objects": "*"
1382 }, 1527 },
1383 "require-dev": { 1528 "require-dev": {
1384 "ext-pdo": "*" 1529 "ext-pdo": "*"
1385 }, 1530 },
1386 "suggest": { 1531 "suggest": {
1532 "ext-soap": "*",
1387 "ext-xdebug": "*", 1533 "ext-xdebug": "*",
1388 "phpunit/php-invoker": "~1.1" 1534 "phpunit/php-invoker": "^2.0"
1389 }, 1535 },
1390 "bin": [ 1536 "bin": [
1391 "phpunit" 1537 "phpunit"
@@ -1393,7 +1539,7 @@
1393 "type": "library", 1539 "type": "library",
1394 "extra": { 1540 "extra": {
1395 "branch-alias": { 1541 "branch-alias": {
1396 "dev-master": "5.7.x-dev" 1542 "dev-master": "7.5-dev"
1397 } 1543 }
1398 }, 1544 },
1399 "autoload": { 1545 "autoload": {
@@ -1419,67 +1565,11 @@
1419 "testing", 1565 "testing",
1420 "xunit" 1566 "xunit"
1421 ], 1567 ],
1422 "time": "2018-02-01T05:50:59+00:00" 1568 "support": {
1423 }, 1569 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
1424 { 1570 "source": "https://github.com/sebastianbergmann/phpunit/tree/7.5.20"
1425 "name": "phpunit/phpunit-mock-objects",
1426 "version": "3.4.4",
1427 "source": {
1428 "type": "git",
1429 "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
1430 "reference": "a23b761686d50a560cc56233b9ecf49597cc9118"
1431 },
1432 "dist": {
1433 "type": "zip",
1434 "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118",
1435 "reference": "a23b761686d50a560cc56233b9ecf49597cc9118",
1436 "shasum": ""
1437 },
1438 "require": {
1439 "doctrine/instantiator": "^1.0.2",
1440 "php": "^5.6 || ^7.0",
1441 "phpunit/php-text-template": "^1.2",
1442 "sebastian/exporter": "^1.2 || ^2.0"
1443 },
1444 "conflict": {
1445 "phpunit/phpunit": "<5.4.0"
1446 },
1447 "require-dev": {
1448 "phpunit/phpunit": "^5.4"
1449 }, 1571 },
1450 "suggest": { 1572 "time": "2020-01-08T08:45:45+00:00"
1451 "ext-soap": "*"
1452 },
1453 "type": "library",
1454 "extra": {
1455 "branch-alias": {
1456 "dev-master": "3.2.x-dev"
1457 }
1458 },
1459 "autoload": {
1460 "classmap": [
1461 "src/"
1462 ]
1463 },
1464 "notification-url": "https://packagist.org/downloads/",
1465 "license": [
1466 "BSD-3-Clause"
1467 ],
1468 "authors": [
1469 {
1470 "name": "Sebastian Bergmann",
1471 "email": "sb@sebastian-bergmann.de",
1472 "role": "lead"
1473 }
1474 ],
1475 "description": "Mock Object library for PHPUnit",
1476 "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
1477 "keywords": [
1478 "mock",
1479 "xunit"
1480 ],
1481 "abandoned": true,
1482 "time": "2017-06-30T09:13:00+00:00"
1483 }, 1573 },
1484 { 1574 {
1485 "name": "roave/security-advisories", 1575 "name": "roave/security-advisories",
@@ -1487,12 +1577,12 @@
1487 "source": { 1577 "source": {
1488 "type": "git", 1578 "type": "git",
1489 "url": "https://github.com/Roave/SecurityAdvisories.git", 1579 "url": "https://github.com/Roave/SecurityAdvisories.git",
1490 "reference": "ea693fa060702164985511acc3ceb5389c9ac761" 1580 "reference": "0749ceaf15c136d085b722a5bb88141398a54142"
1491 }, 1581 },
1492 "dist": { 1582 "dist": {
1493 "type": "zip", 1583 "type": "zip",
1494 "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ea693fa060702164985511acc3ceb5389c9ac761", 1584 "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/0749ceaf15c136d085b722a5bb88141398a54142",
1495 "reference": "ea693fa060702164985511acc3ceb5389c9ac761", 1585 "reference": "0749ceaf15c136d085b722a5bb88141398a54142",
1496 "shasum": "" 1586 "shasum": ""
1497 }, 1587 },
1498 "conflict": { 1588 "conflict": {
@@ -1501,22 +1591,32 @@
1501 "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", 1591 "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
1502 "amphp/artax": "<1.0.6|>=2,<2.0.6", 1592 "amphp/artax": "<1.0.6|>=2,<2.0.6",
1503 "amphp/http": "<1.0.1", 1593 "amphp/http": "<1.0.1",
1594 "amphp/http-client": ">=4,<4.4",
1504 "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6", 1595 "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
1505 "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99", 1596 "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
1506 "aws/aws-sdk-php": ">=3,<3.2.1", 1597 "aws/aws-sdk-php": ">=3,<3.2.1",
1598 "bagisto/bagisto": "<0.1.5",
1599 "barrelstrength/sprout-base-email": "<1.2.7",
1600 "barrelstrength/sprout-forms": "<3.9",
1601 "baserproject/basercms": ">=4,<=4.3.6",
1602 "bolt/bolt": "<3.7.1",
1507 "brightlocal/phpwhois": "<=4.2.5", 1603 "brightlocal/phpwhois": "<=4.2.5",
1604 "buddypress/buddypress": "<5.1.2",
1508 "bugsnag/bugsnag-laravel": ">=2,<2.0.2", 1605 "bugsnag/bugsnag-laravel": ">=2,<2.0.2",
1509 "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", 1606 "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",
1510 "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", 1607 "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
1511 "cartalyst/sentry": "<=2.1.6", 1608 "cartalyst/sentry": "<=2.1.6",
1609 "centreon/centreon": "<18.10.8|>=19,<19.4.5",
1610 "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
1512 "codeigniter/framework": "<=3.0.6", 1611 "codeigniter/framework": "<=3.0.6",
1513 "composer/composer": "<=1-alpha.11", 1612 "composer/composer": "<=1-alpha.11",
1514 "contao-components/mediaelement": ">=2.14.2,<2.21.1", 1613 "contao-components/mediaelement": ">=2.14.2,<2.21.1",
1515 "contao/core": ">=2,<3.5.39", 1614 "contao/core": ">=2,<3.5.39",
1516 "contao/core-bundle": ">=4,<4.4.39|>=4.5,<4.7.5", 1615 "contao/core-bundle": ">=4,<4.4.52|>=4.5,<4.9.6|= 4.10.0",
1517 "contao/listing-bundle": ">=4,<4.4.8", 1616 "contao/listing-bundle": ">=4,<4.4.8",
1518 "contao/newsletter-bundle": ">=4,<4.1", 1617 "datadog/dd-trace": ">=0.30,<0.30.2",
1519 "david-garcia/phpwhois": "<=4.3.1", 1618 "david-garcia/phpwhois": "<=4.3.1",
1619 "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1",
1520 "doctrine/annotations": ">=1,<1.2.7", 1620 "doctrine/annotations": ">=1,<1.2.7",
1521 "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", 1621 "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2",
1522 "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1", 1622 "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1",
@@ -1526,45 +1626,75 @@
1526 "doctrine/mongodb-odm": ">=1,<1.0.2", 1626 "doctrine/mongodb-odm": ">=1,<1.0.2",
1527 "doctrine/mongodb-odm-bundle": ">=2,<3.0.1", 1627 "doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
1528 "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1", 1628 "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1",
1629 "dolibarr/dolibarr": "<11.0.4",
1529 "dompdf/dompdf": ">=0.6,<0.6.2", 1630 "dompdf/dompdf": ">=0.6,<0.6.2",
1530 "drupal/core": ">=7,<7.67|>=8,<8.6.16|>=8.7,<8.7.1|>8.7.3,<8.7.5", 1631 "drupal/core": ">=7,<7.73|>=8,<8.8.10|>=8.9,<8.9.6|>=9,<9.0.6",
1531 "drupal/drupal": ">=7,<7.67|>=8,<8.6.16|>=8.7,<8.7.1|>8.7.3,<8.7.5", 1632 "drupal/drupal": ">=7,<7.73|>=8,<8.8.10|>=8.9,<8.9.6|>=9,<9.0.6",
1633 "endroid/qr-code-bundle": "<3.4.2",
1634 "enshrined/svg-sanitize": "<0.13.1",
1532 "erusev/parsedown": "<1.7.2", 1635 "erusev/parsedown": "<1.7.2",
1533 "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.4", 1636 "ezsystems/demobundle": ">=5.4,<5.4.6.1",
1534 "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", 1637 "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1",
1535 "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", 1638 "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1",
1639 "ezsystems/ezplatform": ">=1.7,<1.7.9.1|>=1.13,<1.13.5.1|>=2.5,<2.5.4",
1640 "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6",
1641 "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1",
1642 "ezsystems/ezplatform-kernel": ">=1,<1.0.2.1",
1643 "ezsystems/ezplatform-user": ">=1,<1.0.1",
1644 "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",
1645 "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.14.1|>=2011,<2017.12.7.2|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3|>=2019.3,<2019.3.4.2",
1646 "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
1536 "ezsystems/repository-forms": ">=2.3,<2.3.2.1", 1647 "ezsystems/repository-forms": ">=2.3,<2.3.2.1",
1537 "ezyang/htmlpurifier": "<4.1.1", 1648 "ezyang/htmlpurifier": "<4.1.1",
1538 "firebase/php-jwt": "<2", 1649 "firebase/php-jwt": "<2",
1539 "fooman/tcpdf": "<6.2.22", 1650 "fooman/tcpdf": "<6.2.22",
1540 "fossar/tcpdf-parser": "<6.2.22", 1651 "fossar/tcpdf-parser": "<6.2.22",
1652 "friendsofsymfony/oauth2-php": "<1.3",
1541 "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", 1653 "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
1542 "friendsofsymfony/user-bundle": ">=1.2,<1.3.5", 1654 "friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
1655 "friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
1543 "fuel/core": "<1.8.1", 1656 "fuel/core": "<1.8.1",
1657 "getgrav/grav": "<1.7-beta.8",
1658 "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3",
1544 "gree/jose": "<=2.2", 1659 "gree/jose": "<=2.2",
1545 "gregwar/rst": "<1.0.3", 1660 "gregwar/rst": "<1.0.3",
1546 "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1", 1661 "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1",
1547 "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", 1662 "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",
1548 "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", 1663 "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",
1549 "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29", 1664 "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29|>=5.5,<=5.5.44|>=6,<6.18.34|>=7,<7.23.2",
1550 "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", 1665 "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",
1666 "illuminate/view": ">=7,<7.1.2",
1551 "ivankristianto/phpwhois": "<=4.3", 1667 "ivankristianto/phpwhois": "<=4.3",
1552 "james-heinrich/getid3": "<1.9.9", 1668 "james-heinrich/getid3": "<1.9.9",
1553 "joomla/session": "<1.3.1", 1669 "joomla/session": "<1.3.1",
1554 "jsmitty12/phpwhois": "<5.1", 1670 "jsmitty12/phpwhois": "<5.1",
1555 "kazist/phpwhois": "<=4.2.6", 1671 "kazist/phpwhois": "<=4.2.6",
1672 "kitodo/presentation": "<3.1.2",
1556 "kreait/firebase-php": ">=3.2,<3.8.1", 1673 "kreait/firebase-php": ">=3.2,<3.8.1",
1557 "la-haute-societe/tcpdf": "<6.2.22", 1674 "la-haute-societe/tcpdf": "<6.2.22",
1558 "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", 1675 "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",
1559 "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", 1676 "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
1560 "league/commonmark": "<0.18.3", 1677 "league/commonmark": "<0.18.3",
1561 "magento/magento1ce": "<1.9.4.1", 1678 "librenms/librenms": "<1.53",
1562 "magento/magento1ee": ">=1.9,<1.14.4.1", 1679 "livewire/livewire": ">2.2.4,<2.2.6",
1563 "magento/product-community-edition": ">=2,<2.2.8|>=2.3,<2.3.1", 1680 "magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
1681 "magento/magento1ce": "<1.9.4.3",
1682 "magento/magento1ee": ">=1,<1.14.4.3",
1683 "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
1684 "marcwillmann/turn": "<0.3.3",
1685 "mittwald/typo3_forum": "<1.2.1",
1564 "monolog/monolog": ">=1.8,<1.12", 1686 "monolog/monolog": ">=1.8,<1.12",
1565 "namshi/jose": "<2.2", 1687 "namshi/jose": "<2.2",
1688 "nystudio107/craft-seomatic": "<3.3",
1689 "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
1690 "october/backend": ">=1.0.319,<1.0.467",
1691 "october/cms": ">=1.0.319,<1.0.466",
1692 "october/october": ">=1.0.319,<1.0.466",
1693 "october/rain": ">=1.0.319,<1.0.468",
1566 "onelogin/php-saml": "<2.10.4", 1694 "onelogin/php-saml": "<2.10.4",
1695 "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
1567 "openid/php-openid": "<2.3", 1696 "openid/php-openid": "<2.3",
1697 "openmage/magento-lts": "<19.4.6|>=20,<20.0.2",
1568 "oro/crm": ">=1.7,<1.7.4", 1698 "oro/crm": ">=1.7,<1.7.4",
1569 "oro/platform": ">=1.7,<1.7.4", 1699 "oro/platform": ">=1.7,<1.7.4",
1570 "padraic/humbug_get_contents": "<1.1.2", 1700 "padraic/humbug_get_contents": "<1.1.2",
@@ -1572,66 +1702,97 @@
1572 "paragonie/random_compat": "<2", 1702 "paragonie/random_compat": "<2",
1573 "paypal/merchant-sdk-php": "<3.12", 1703 "paypal/merchant-sdk-php": "<3.12",
1574 "pear/archive_tar": "<1.4.4", 1704 "pear/archive_tar": "<1.4.4",
1575 "phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6", 1705 "personnummer/personnummer": "<3.0.2",
1576 "phpoffice/phpexcel": "<=1.8.1", 1706 "phpfastcache/phpfastcache": ">=5,<5.0.13",
1577 "phpoffice/phpspreadsheet": "<=1.5", 1707 "phpmailer/phpmailer": "<6.1.6",
1708 "phpmussel/phpmussel": ">=1,<1.6",
1709 "phpmyadmin/phpmyadmin": "<4.9.2",
1710 "phpoffice/phpexcel": "<1.8.2",
1711 "phpoffice/phpspreadsheet": "<1.8",
1578 "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", 1712 "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
1579 "phpwhois/phpwhois": "<=4.2.5", 1713 "phpwhois/phpwhois": "<=4.2.5",
1580 "phpxmlrpc/extras": "<0.6.1", 1714 "phpxmlrpc/extras": "<0.6.1",
1715 "pimcore/pimcore": "<6.3",
1716 "prestashop/autoupgrade": ">=4,<4.10.1",
1717 "prestashop/contactform": ">1.0.1,<4.3",
1718 "prestashop/gamification": "<2.3.2",
1719 "prestashop/ps_facetedsearch": "<3.4.1",
1720 "privatebin/privatebin": "<1.2.2|>=1.3,<1.3.2",
1581 "propel/propel": ">=2-alpha.1,<=2-alpha.7", 1721 "propel/propel": ">=2-alpha.1,<=2-alpha.7",
1582 "propel/propel1": ">=1,<=1.7.1", 1722 "propel/propel1": ">=1,<=1.7.1",
1583 "pusher/pusher-php-server": "<2.2.1", 1723 "pusher/pusher-php-server": "<2.2.1",
1584 "robrichards/xmlseclibs": ">=1,<3.0.2", 1724 "rainlab/debugbar-plugin": "<3.1",
1725 "robrichards/xmlseclibs": "<3.0.4",
1726 "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",
1585 "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9", 1727 "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9",
1728 "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
1586 "sensiolabs/connect": "<4.2.3", 1729 "sensiolabs/connect": "<4.2.3",
1587 "serluck/phpwhois": "<=4.2.6", 1730 "serluck/phpwhois": "<=4.2.6",
1731 "shopware/core": "<=6.3.1",
1732 "shopware/platform": "<=6.3.1",
1588 "shopware/shopware": "<5.3.7", 1733 "shopware/shopware": "<5.3.7",
1589 "silverstripe/cms": ">=3,<=3.0.11|>=3.1,<3.1.11", 1734 "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
1735 "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
1736 "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
1737 "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
1590 "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", 1738 "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
1591 "silverstripe/framework": ">=3,<3.6.7|>=3.7,<3.7.3|>=4,<4.4", 1739 "silverstripe/framework": "<4.4.7|>=4.5,<4.5.4",
1592 "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2", 1740 "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2|>=3.2,<3.2.4",
1593 "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", 1741 "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
1594 "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4", 1742 "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
1743 "silverstripe/subsites": ">=2,<2.1.1",
1744 "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1",
1595 "silverstripe/userforms": "<3", 1745 "silverstripe/userforms": "<3",
1596 "simple-updates/phpwhois": "<=1", 1746 "simple-updates/phpwhois": "<=1",
1597 "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4", 1747 "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4",
1598 "simplesamlphp/simplesamlphp": "<1.17.3", 1748 "simplesamlphp/simplesamlphp": "<1.18.6",
1599 "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", 1749 "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
1750 "simplito/elliptic-php": "<1.0.6",
1600 "slim/slim": "<2.6", 1751 "slim/slim": "<2.6",
1601 "smarty/smarty": "<3.1.33", 1752 "smarty/smarty": "<3.1.33",
1602 "socalnick/scn-social-auth": "<1.15.2", 1753 "socalnick/scn-social-auth": "<1.15.2",
1603 "spoonity/tcpdf": "<6.2.22", 1754 "spoonity/tcpdf": "<6.2.22",
1604 "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", 1755 "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
1756 "ssddanbrown/bookstack": "<0.29.2",
1605 "stormpath/sdk": ">=0,<9.9.99", 1757 "stormpath/sdk": ">=0,<9.9.99",
1758 "studio-42/elfinder": "<2.1.49",
1759 "sulu/sulu": "<1.6.34|>=2,<2.0.10|>=2.1,<2.1.1",
1606 "swiftmailer/swiftmailer": ">=4,<5.4.5", 1760 "swiftmailer/swiftmailer": ">=4,<5.4.5",
1607 "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", 1761 "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
1608 "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", 1762 "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",
1609 "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", 1763 "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",
1610 "sylius/sylius": ">=1,<1.1.18|>=1.2,<1.2.17|>=1.3,<1.3.12|>=1.4,<1.4.4", 1764 "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4",
1611 "symfony/cache": ">=3.1,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1765 "sylius/sylius": "<1.3.16|>=1.4,<1.4.12|>=1.5,<1.5.9|>=1.6,<1.6.5",
1766 "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
1767 "symbiote/silverstripe-versionedfiles": "<=2.0.3",
1768 "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
1612 "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", 1769 "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",
1770 "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4",
1613 "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", 1771 "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",
1614 "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", 1772 "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",
1615 "symfony/http-foundation": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1773 "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",
1616 "symfony/http-kernel": ">=2,<2.3.29|>=2.4,<2.5.12|>=2.6,<2.6.8", 1774 "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.4.13|>=5,<5.1.5",
1617 "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", 1775 "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
1776 "symfony/mime": ">=4.3,<4.3.8",
1618 "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1777 "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
1619 "symfony/polyfill": ">=1,<1.10", 1778 "symfony/polyfill": ">=1,<1.10",
1620 "symfony/polyfill-php55": ">=1,<1.10", 1779 "symfony/polyfill-php55": ">=1,<1.10",
1621 "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", 1780 "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",
1622 "symfony/routing": ">=2,<2.0.19", 1781 "symfony/routing": ">=2,<2.0.19",
1623 "symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1782 "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",
1624 "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", 1783 "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",
1625 "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", 1784 "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",
1626 "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", 1785 "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",
1627 "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", 1786 "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
1628 "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1787 "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",
1629 "symfony/serializer": ">=2,<2.0.11", 1788 "symfony/serializer": ">=2,<2.0.11",
1630 "symfony/symfony": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", 1789 "symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.4.13|>=5,<5.1.5",
1631 "symfony/translation": ">=2,<2.0.17", 1790 "symfony/translation": ">=2,<2.0.17",
1632 "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", 1791 "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
1792 "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
1633 "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", 1793 "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4",
1634 "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7", 1794 "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7",
1795 "t3g/svg-sanitizer": "<1.0.3",
1635 "tecnickcom/tcpdf": "<6.2.22", 1796 "tecnickcom/tcpdf": "<6.2.22",
1636 "thelia/backoffice-default-template": ">=2.1,<2.1.2", 1797 "thelia/backoffice-default-template": ">=2.1,<2.1.2",
1637 "thelia/thelia": ">=2.1-beta.1,<2.1.3", 1798 "thelia/thelia": ">=2.1-beta.1,<2.1.3",
@@ -1639,22 +1800,26 @@
1639 "titon/framework": ">=0,<9.9.99", 1800 "titon/framework": ">=0,<9.9.99",
1640 "truckersmp/phpwhois": "<=4.3.1", 1801 "truckersmp/phpwhois": "<=4.3.1",
1641 "twig/twig": "<1.38|>=2,<2.7", 1802 "twig/twig": "<1.38|>=2,<2.7",
1642 "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.27|>=9,<9.5.8", 1803 "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.20|>=10,<10.4.6",
1643 "typo3/cms-core": ">=8,<8.7.27|>=9,<9.5.8", 1804 "typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.20|>=10,<10.4.6",
1644 "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", 1805 "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",
1645 "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4", 1806 "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4",
1646 "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", 1807 "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
1647 "ua-parser/uap-php": "<3.8", 1808 "ua-parser/uap-php": "<3.8",
1809 "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
1810 "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
1648 "wallabag/tcpdf": "<6.2.22", 1811 "wallabag/tcpdf": "<6.2.22",
1649 "willdurand/js-translation-bundle": "<2.1.1", 1812 "willdurand/js-translation-bundle": "<2.1.1",
1813 "yii2mod/yii2-cms": "<1.9.2",
1650 "yiisoft/yii": ">=1.1.14,<1.1.15", 1814 "yiisoft/yii": ">=1.1.14,<1.1.15",
1651 "yiisoft/yii2": "<2.0.15", 1815 "yiisoft/yii2": "<2.0.38",
1652 "yiisoft/yii2-bootstrap": "<2.0.4", 1816 "yiisoft/yii2-bootstrap": "<2.0.4",
1653 "yiisoft/yii2-dev": "<2.0.15", 1817 "yiisoft/yii2-dev": "<2.0.15",
1654 "yiisoft/yii2-elasticsearch": "<2.0.5", 1818 "yiisoft/yii2-elasticsearch": "<2.0.5",
1655 "yiisoft/yii2-gii": "<2.0.4", 1819 "yiisoft/yii2-gii": "<2.0.4",
1656 "yiisoft/yii2-jui": "<2.0.4", 1820 "yiisoft/yii2-jui": "<2.0.4",
1657 "yiisoft/yii2-redis": "<2.0.8", 1821 "yiisoft/yii2-redis": "<2.0.8",
1822 "yourls/yourls": "<1.7.4",
1658 "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", 1823 "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
1659 "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", 1824 "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
1660 "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", 1825 "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
@@ -1691,10 +1856,29 @@
1691 "name": "Marco Pivetta", 1856 "name": "Marco Pivetta",
1692 "email": "ocramius@gmail.com", 1857 "email": "ocramius@gmail.com",
1693 "role": "maintainer" 1858 "role": "maintainer"
1859 },
1860 {
1861 "name": "Ilya Tribusean",
1862 "email": "slash3b@gmail.com",
1863 "role": "maintainer"
1694 } 1864 }
1695 ], 1865 ],
1696 "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", 1866 "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
1697 "time": "2019-07-18T15:17:58+00:00" 1867 "support": {
1868 "issues": "https://github.com/Roave/SecurityAdvisories/issues",
1869 "source": "https://github.com/Roave/SecurityAdvisories/tree/latest"
1870 },
1871 "funding": [
1872 {
1873 "url": "https://github.com/Ocramius",
1874 "type": "github"
1875 },
1876 {
1877 "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories",
1878 "type": "tidelift"
1879 }
1880 ],
1881 "time": "2020-09-24T17:02:11+00:00"
1698 }, 1882 },
1699 { 1883 {
1700 "name": "sebastian/code-unit-reverse-lookup", 1884 "name": "sebastian/code-unit-reverse-lookup",
@@ -1739,34 +1923,38 @@
1739 ], 1923 ],
1740 "description": "Looks up which function or method a line of code belongs to", 1924 "description": "Looks up which function or method a line of code belongs to",
1741 "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 1925 "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
1926 "support": {
1927 "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
1928 "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/master"
1929 },
1742 "time": "2017-03-04T06:30:41+00:00" 1930 "time": "2017-03-04T06:30:41+00:00"
1743 }, 1931 },
1744 { 1932 {
1745 "name": "sebastian/comparator", 1933 "name": "sebastian/comparator",
1746 "version": "1.2.4", 1934 "version": "3.0.2",
1747 "source": { 1935 "source": {
1748 "type": "git", 1936 "type": "git",
1749 "url": "https://github.com/sebastianbergmann/comparator.git", 1937 "url": "https://github.com/sebastianbergmann/comparator.git",
1750 "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" 1938 "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
1751 }, 1939 },
1752 "dist": { 1940 "dist": {
1753 "type": "zip", 1941 "type": "zip",
1754 "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", 1942 "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
1755 "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", 1943 "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
1756 "shasum": "" 1944 "shasum": ""
1757 }, 1945 },
1758 "require": { 1946 "require": {
1759 "php": ">=5.3.3", 1947 "php": "^7.1",
1760 "sebastian/diff": "~1.2", 1948 "sebastian/diff": "^3.0",
1761 "sebastian/exporter": "~1.2 || ~2.0" 1949 "sebastian/exporter": "^3.1"
1762 }, 1950 },
1763 "require-dev": { 1951 "require-dev": {
1764 "phpunit/phpunit": "~4.4" 1952 "phpunit/phpunit": "^7.1"
1765 }, 1953 },
1766 "type": "library", 1954 "type": "library",
1767 "extra": { 1955 "extra": {
1768 "branch-alias": { 1956 "branch-alias": {
1769 "dev-master": "1.2.x-dev" 1957 "dev-master": "3.0-dev"
1770 } 1958 }
1771 }, 1959 },
1772 "autoload": { 1960 "autoload": {
@@ -1797,38 +1985,43 @@
1797 } 1985 }
1798 ], 1986 ],
1799 "description": "Provides the functionality to compare PHP values for equality", 1987 "description": "Provides the functionality to compare PHP values for equality",
1800 "homepage": "http://www.github.com/sebastianbergmann/comparator", 1988 "homepage": "https://github.com/sebastianbergmann/comparator",
1801 "keywords": [ 1989 "keywords": [
1802 "comparator", 1990 "comparator",
1803 "compare", 1991 "compare",
1804 "equality" 1992 "equality"
1805 ], 1993 ],
1806 "time": "2017-01-29T09:50:25+00:00" 1994 "support": {
1995 "issues": "https://github.com/sebastianbergmann/comparator/issues",
1996 "source": "https://github.com/sebastianbergmann/comparator/tree/master"
1997 },
1998 "time": "2018-07-12T15:12:46+00:00"
1807 }, 1999 },
1808 { 2000 {
1809 "name": "sebastian/diff", 2001 "name": "sebastian/diff",
1810 "version": "1.4.3", 2002 "version": "3.0.2",
1811 "source": { 2003 "source": {
1812 "type": "git", 2004 "type": "git",
1813 "url": "https://github.com/sebastianbergmann/diff.git", 2005 "url": "https://github.com/sebastianbergmann/diff.git",
1814 "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" 2006 "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
1815 }, 2007 },
1816 "dist": { 2008 "dist": {
1817 "type": "zip", 2009 "type": "zip",
1818 "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", 2010 "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
1819 "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", 2011 "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
1820 "shasum": "" 2012 "shasum": ""
1821 }, 2013 },
1822 "require": { 2014 "require": {
1823 "php": "^5.3.3 || ^7.0" 2015 "php": "^7.1"
1824 }, 2016 },
1825 "require-dev": { 2017 "require-dev": {
1826 "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" 2018 "phpunit/phpunit": "^7.5 || ^8.0",
2019 "symfony/process": "^2 || ^3.3 || ^4"
1827 }, 2020 },
1828 "type": "library", 2021 "type": "library",
1829 "extra": { 2022 "extra": {
1830 "branch-alias": { 2023 "branch-alias": {
1831 "dev-master": "1.4-dev" 2024 "dev-master": "3.0-dev"
1832 } 2025 }
1833 }, 2026 },
1834 "autoload": { 2027 "autoload": {
@@ -1853,34 +2046,44 @@
1853 "description": "Diff implementation", 2046 "description": "Diff implementation",
1854 "homepage": "https://github.com/sebastianbergmann/diff", 2047 "homepage": "https://github.com/sebastianbergmann/diff",
1855 "keywords": [ 2048 "keywords": [
1856 "diff" 2049 "diff",
2050 "udiff",
2051 "unidiff",
2052 "unified diff"
1857 ], 2053 ],
1858 "time": "2017-05-22T07:24:03+00:00" 2054 "support": {
2055 "issues": "https://github.com/sebastianbergmann/diff/issues",
2056 "source": "https://github.com/sebastianbergmann/diff/tree/master"
2057 },
2058 "time": "2019-02-04T06:01:07+00:00"
1859 }, 2059 },
1860 { 2060 {
1861 "name": "sebastian/environment", 2061 "name": "sebastian/environment",
1862 "version": "2.0.0", 2062 "version": "4.2.3",
1863 "source": { 2063 "source": {
1864 "type": "git", 2064 "type": "git",
1865 "url": "https://github.com/sebastianbergmann/environment.git", 2065 "url": "https://github.com/sebastianbergmann/environment.git",
1866 "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" 2066 "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368"
1867 }, 2067 },
1868 "dist": { 2068 "dist": {
1869 "type": "zip", 2069 "type": "zip",
1870 "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", 2070 "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
1871 "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", 2071 "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
1872 "shasum": "" 2072 "shasum": ""
1873 }, 2073 },
1874 "require": { 2074 "require": {
1875 "php": "^5.6 || ^7.0" 2075 "php": "^7.1"
1876 }, 2076 },
1877 "require-dev": { 2077 "require-dev": {
1878 "phpunit/phpunit": "^5.0" 2078 "phpunit/phpunit": "^7.5"
2079 },
2080 "suggest": {
2081 "ext-posix": "*"
1879 }, 2082 },
1880 "type": "library", 2083 "type": "library",
1881 "extra": { 2084 "extra": {
1882 "branch-alias": { 2085 "branch-alias": {
1883 "dev-master": "2.0.x-dev" 2086 "dev-master": "4.2-dev"
1884 } 2087 }
1885 }, 2088 },
1886 "autoload": { 2089 "autoload": {
@@ -1905,34 +2108,38 @@
1905 "environment", 2108 "environment",
1906 "hhvm" 2109 "hhvm"
1907 ], 2110 ],
1908 "time": "2016-11-26T07:53:53+00:00" 2111 "support": {
2112 "issues": "https://github.com/sebastianbergmann/environment/issues",
2113 "source": "https://github.com/sebastianbergmann/environment/tree/4.2.3"
2114 },
2115 "time": "2019-11-20T08:46:58+00:00"
1909 }, 2116 },
1910 { 2117 {
1911 "name": "sebastian/exporter", 2118 "name": "sebastian/exporter",
1912 "version": "2.0.0", 2119 "version": "3.1.2",
1913 "source": { 2120 "source": {
1914 "type": "git", 2121 "type": "git",
1915 "url": "https://github.com/sebastianbergmann/exporter.git", 2122 "url": "https://github.com/sebastianbergmann/exporter.git",
1916 "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" 2123 "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
1917 }, 2124 },
1918 "dist": { 2125 "dist": {
1919 "type": "zip", 2126 "type": "zip",
1920 "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", 2127 "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
1921 "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", 2128 "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
1922 "shasum": "" 2129 "shasum": ""
1923 }, 2130 },
1924 "require": { 2131 "require": {
1925 "php": ">=5.3.3", 2132 "php": "^7.0",
1926 "sebastian/recursion-context": "~2.0" 2133 "sebastian/recursion-context": "^3.0"
1927 }, 2134 },
1928 "require-dev": { 2135 "require-dev": {
1929 "ext-mbstring": "*", 2136 "ext-mbstring": "*",
1930 "phpunit/phpunit": "~4.4" 2137 "phpunit/phpunit": "^6.0"
1931 }, 2138 },
1932 "type": "library", 2139 "type": "library",
1933 "extra": { 2140 "extra": {
1934 "branch-alias": { 2141 "branch-alias": {
1935 "dev-master": "2.0.x-dev" 2142 "dev-master": "3.1.x-dev"
1936 } 2143 }
1937 }, 2144 },
1938 "autoload": { 2145 "autoload": {
@@ -1946,6 +2153,10 @@
1946 ], 2153 ],
1947 "authors": [ 2154 "authors": [
1948 { 2155 {
2156 "name": "Sebastian Bergmann",
2157 "email": "sebastian@phpunit.de"
2158 },
2159 {
1949 "name": "Jeff Welch", 2160 "name": "Jeff Welch",
1950 "email": "whatthejeff@gmail.com" 2161 "email": "whatthejeff@gmail.com"
1951 }, 2162 },
@@ -1954,16 +2165,12 @@
1954 "email": "github@wallbash.com" 2165 "email": "github@wallbash.com"
1955 }, 2166 },
1956 { 2167 {
1957 "name": "Bernhard Schussek",
1958 "email": "bschussek@2bepublished.at"
1959 },
1960 {
1961 "name": "Sebastian Bergmann",
1962 "email": "sebastian@phpunit.de"
1963 },
1964 {
1965 "name": "Adam Harvey", 2168 "name": "Adam Harvey",
1966 "email": "aharvey@php.net" 2169 "email": "aharvey@php.net"
2170 },
2171 {
2172 "name": "Bernhard Schussek",
2173 "email": "bschussek@gmail.com"
1967 } 2174 }
1968 ], 2175 ],
1969 "description": "Provides the functionality to export PHP variables for visualization", 2176 "description": "Provides the functionality to export PHP variables for visualization",
@@ -1972,27 +2179,41 @@
1972 "export", 2179 "export",
1973 "exporter" 2180 "exporter"
1974 ], 2181 ],
1975 "time": "2016-11-19T08:54:04+00:00" 2182 "support": {
2183 "issues": "https://github.com/sebastianbergmann/exporter/issues",
2184 "source": "https://github.com/sebastianbergmann/exporter/tree/master"
2185 },
2186 "time": "2019-09-14T09:02:43+00:00"
1976 }, 2187 },
1977 { 2188 {
1978 "name": "sebastian/finder-facade", 2189 "name": "sebastian/global-state",
1979 "version": "1.2.2", 2190 "version": "2.0.0",
1980 "source": { 2191 "source": {
1981 "type": "git", 2192 "type": "git",
1982 "url": "https://github.com/sebastianbergmann/finder-facade.git", 2193 "url": "https://github.com/sebastianbergmann/global-state.git",
1983 "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f" 2194 "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
1984 }, 2195 },
1985 "dist": { 2196 "dist": {
1986 "type": "zip", 2197 "type": "zip",
1987 "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f", 2198 "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
1988 "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f", 2199 "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
1989 "shasum": "" 2200 "shasum": ""
1990 }, 2201 },
1991 "require": { 2202 "require": {
1992 "symfony/finder": "~2.3|~3.0|~4.0", 2203 "php": "^7.0"
1993 "theseer/fdomdocument": "~1.3" 2204 },
2205 "require-dev": {
2206 "phpunit/phpunit": "^6.0"
2207 },
2208 "suggest": {
2209 "ext-uopz": "*"
1994 }, 2210 },
1995 "type": "library", 2211 "type": "library",
2212 "extra": {
2213 "branch-alias": {
2214 "dev-master": "2.0-dev"
2215 }
2216 },
1996 "autoload": { 2217 "autoload": {
1997 "classmap": [ 2218 "classmap": [
1998 "src/" 2219 "src/"
@@ -2005,41 +2226,46 @@
2005 "authors": [ 2226 "authors": [
2006 { 2227 {
2007 "name": "Sebastian Bergmann", 2228 "name": "Sebastian Bergmann",
2008 "email": "sebastian@phpunit.de", 2229 "email": "sebastian@phpunit.de"
2009 "role": "lead"
2010 } 2230 }
2011 ], 2231 ],
2012 "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.", 2232 "description": "Snapshotting of global state",
2013 "homepage": "https://github.com/sebastianbergmann/finder-facade", 2233 "homepage": "http://www.github.com/sebastianbergmann/global-state",
2014 "time": "2017-11-18T17:31:49+00:00" 2234 "keywords": [
2235 "global state"
2236 ],
2237 "support": {
2238 "issues": "https://github.com/sebastianbergmann/global-state/issues",
2239 "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0"
2240 },
2241 "time": "2017-04-27T15:39:26+00:00"
2015 }, 2242 },
2016 { 2243 {
2017 "name": "sebastian/global-state", 2244 "name": "sebastian/object-enumerator",
2018 "version": "1.1.1", 2245 "version": "3.0.3",
2019 "source": { 2246 "source": {
2020 "type": "git", 2247 "type": "git",
2021 "url": "https://github.com/sebastianbergmann/global-state.git", 2248 "url": "https://github.com/sebastianbergmann/object-enumerator.git",
2022 "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" 2249 "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
2023 }, 2250 },
2024 "dist": { 2251 "dist": {
2025 "type": "zip", 2252 "type": "zip",
2026 "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", 2253 "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5",
2027 "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", 2254 "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
2028 "shasum": "" 2255 "shasum": ""
2029 }, 2256 },
2030 "require": { 2257 "require": {
2031 "php": ">=5.3.3" 2258 "php": "^7.0",
2259 "sebastian/object-reflector": "^1.1.1",
2260 "sebastian/recursion-context": "^3.0"
2032 }, 2261 },
2033 "require-dev": { 2262 "require-dev": {
2034 "phpunit/phpunit": "~4.2" 2263 "phpunit/phpunit": "^6.0"
2035 },
2036 "suggest": {
2037 "ext-uopz": "*"
2038 }, 2264 },
2039 "type": "library", 2265 "type": "library",
2040 "extra": { 2266 "extra": {
2041 "branch-alias": { 2267 "branch-alias": {
2042 "dev-master": "1.0-dev" 2268 "dev-master": "3.0.x-dev"
2043 } 2269 }
2044 }, 2270 },
2045 "autoload": { 2271 "autoload": {
@@ -2057,38 +2283,38 @@
2057 "email": "sebastian@phpunit.de" 2283 "email": "sebastian@phpunit.de"
2058 } 2284 }
2059 ], 2285 ],
2060 "description": "Snapshotting of global state", 2286 "description": "Traverses array structures and object graphs to enumerate all referenced objects",
2061 "homepage": "http://www.github.com/sebastianbergmann/global-state", 2287 "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
2062 "keywords": [ 2288 "support": {
2063 "global state" 2289 "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
2064 ], 2290 "source": "https://github.com/sebastianbergmann/object-enumerator/tree/master"
2065 "time": "2015-10-12T03:26:01+00:00" 2291 },
2292 "time": "2017-08-03T12:35:26+00:00"
2066 }, 2293 },
2067 { 2294 {
2068 "name": "sebastian/object-enumerator", 2295 "name": "sebastian/object-reflector",
2069 "version": "2.0.1", 2296 "version": "1.1.1",
2070 "source": { 2297 "source": {
2071 "type": "git", 2298 "type": "git",
2072 "url": "https://github.com/sebastianbergmann/object-enumerator.git", 2299 "url": "https://github.com/sebastianbergmann/object-reflector.git",
2073 "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" 2300 "reference": "773f97c67f28de00d397be301821b06708fca0be"
2074 }, 2301 },
2075 "dist": { 2302 "dist": {
2076 "type": "zip", 2303 "type": "zip",
2077 "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", 2304 "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be",
2078 "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", 2305 "reference": "773f97c67f28de00d397be301821b06708fca0be",
2079 "shasum": "" 2306 "shasum": ""
2080 }, 2307 },
2081 "require": { 2308 "require": {
2082 "php": ">=5.6", 2309 "php": "^7.0"
2083 "sebastian/recursion-context": "~2.0"
2084 }, 2310 },
2085 "require-dev": { 2311 "require-dev": {
2086 "phpunit/phpunit": "~5" 2312 "phpunit/phpunit": "^6.0"
2087 }, 2313 },
2088 "type": "library", 2314 "type": "library",
2089 "extra": { 2315 "extra": {
2090 "branch-alias": { 2316 "branch-alias": {
2091 "dev-master": "2.0.x-dev" 2317 "dev-master": "1.1-dev"
2092 } 2318 }
2093 }, 2319 },
2094 "autoload": { 2320 "autoload": {
@@ -2106,34 +2332,38 @@
2106 "email": "sebastian@phpunit.de" 2332 "email": "sebastian@phpunit.de"
2107 } 2333 }
2108 ], 2334 ],
2109 "description": "Traverses array structures and object graphs to enumerate all referenced objects", 2335 "description": "Allows reflection of object attributes, including inherited and non-public ones",
2110 "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 2336 "homepage": "https://github.com/sebastianbergmann/object-reflector/",
2111 "time": "2017-02-18T15:18:39+00:00" 2337 "support": {
2338 "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
2339 "source": "https://github.com/sebastianbergmann/object-reflector/tree/master"
2340 },
2341 "time": "2017-03-29T09:07:27+00:00"
2112 }, 2342 },
2113 { 2343 {
2114 "name": "sebastian/recursion-context", 2344 "name": "sebastian/recursion-context",
2115 "version": "2.0.0", 2345 "version": "3.0.0",
2116 "source": { 2346 "source": {
2117 "type": "git", 2347 "type": "git",
2118 "url": "https://github.com/sebastianbergmann/recursion-context.git", 2348 "url": "https://github.com/sebastianbergmann/recursion-context.git",
2119 "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" 2349 "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
2120 }, 2350 },
2121 "dist": { 2351 "dist": {
2122 "type": "zip", 2352 "type": "zip",
2123 "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", 2353 "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
2124 "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", 2354 "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
2125 "shasum": "" 2355 "shasum": ""
2126 }, 2356 },
2127 "require": { 2357 "require": {
2128 "php": ">=5.3.3" 2358 "php": "^7.0"
2129 }, 2359 },
2130 "require-dev": { 2360 "require-dev": {
2131 "phpunit/phpunit": "~4.4" 2361 "phpunit/phpunit": "^6.0"
2132 }, 2362 },
2133 "type": "library", 2363 "type": "library",
2134 "extra": { 2364 "extra": {
2135 "branch-alias": { 2365 "branch-alias": {
2136 "dev-master": "2.0.x-dev" 2366 "dev-master": "3.0.x-dev"
2137 } 2367 }
2138 }, 2368 },
2139 "autoload": { 2369 "autoload": {
@@ -2161,29 +2391,33 @@
2161 ], 2391 ],
2162 "description": "Provides functionality to recursively process PHP variables", 2392 "description": "Provides functionality to recursively process PHP variables",
2163 "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 2393 "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
2164 "time": "2016-11-19T07:33:16+00:00" 2394 "support": {
2395 "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
2396 "source": "https://github.com/sebastianbergmann/recursion-context/tree/master"
2397 },
2398 "time": "2017-03-03T06:23:57+00:00"
2165 }, 2399 },
2166 { 2400 {
2167 "name": "sebastian/resource-operations", 2401 "name": "sebastian/resource-operations",
2168 "version": "1.0.0", 2402 "version": "2.0.1",
2169 "source": { 2403 "source": {
2170 "type": "git", 2404 "type": "git",
2171 "url": "https://github.com/sebastianbergmann/resource-operations.git", 2405 "url": "https://github.com/sebastianbergmann/resource-operations.git",
2172 "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" 2406 "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
2173 }, 2407 },
2174 "dist": { 2408 "dist": {
2175 "type": "zip", 2409 "type": "zip",
2176 "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 2410 "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
2177 "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 2411 "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
2178 "shasum": "" 2412 "shasum": ""
2179 }, 2413 },
2180 "require": { 2414 "require": {
2181 "php": ">=5.6.0" 2415 "php": "^7.1"
2182 }, 2416 },
2183 "type": "library", 2417 "type": "library",
2184 "extra": { 2418 "extra": {
2185 "branch-alias": { 2419 "branch-alias": {
2186 "dev-master": "1.0.x-dev" 2420 "dev-master": "2.0-dev"
2187 } 2421 }
2188 }, 2422 },
2189 "autoload": { 2423 "autoload": {
@@ -2203,7 +2437,11 @@
2203 ], 2437 ],
2204 "description": "Provides a list of PHP built-in functions that operate on resources", 2438 "description": "Provides a list of PHP built-in functions that operate on resources",
2205 "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 2439 "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
2206 "time": "2015-07-28T20:34:47+00:00" 2440 "support": {
2441 "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
2442 "source": "https://github.com/sebastianbergmann/resource-operations/tree/master"
2443 },
2444 "time": "2018-10-04T04:07:39+00:00"
2207 }, 2445 },
2208 { 2446 {
2209 "name": "sebastian/version", 2447 "name": "sebastian/version",
@@ -2246,68 +2484,45 @@
2246 ], 2484 ],
2247 "description": "Library that helps with managing the version number of Git-hosted PHP projects", 2485 "description": "Library that helps with managing the version number of Git-hosted PHP projects",
2248 "homepage": "https://github.com/sebastianbergmann/version", 2486 "homepage": "https://github.com/sebastianbergmann/version",
2487 "support": {
2488 "issues": "https://github.com/sebastianbergmann/version/issues",
2489 "source": "https://github.com/sebastianbergmann/version/tree/master"
2490 },
2249 "time": "2016-10-03T07:35:21+00:00" 2491 "time": "2016-10-03T07:35:21+00:00"
2250 }, 2492 },
2251 { 2493 {
2252 "name": "squizlabs/php_codesniffer", 2494 "name": "squizlabs/php_codesniffer",
2253 "version": "2.9.2", 2495 "version": "3.5.6",
2254 "source": { 2496 "source": {
2255 "type": "git", 2497 "type": "git",
2256 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 2498 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
2257 "reference": "2acf168de78487db620ab4bc524135a13cfe6745" 2499 "reference": "e97627871a7eab2f70e59166072a6b767d5834e0"
2258 }, 2500 },
2259 "dist": { 2501 "dist": {
2260 "type": "zip", 2502 "type": "zip",
2261 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/2acf168de78487db620ab4bc524135a13cfe6745", 2503 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0",
2262 "reference": "2acf168de78487db620ab4bc524135a13cfe6745", 2504 "reference": "e97627871a7eab2f70e59166072a6b767d5834e0",
2263 "shasum": "" 2505 "shasum": ""
2264 }, 2506 },
2265 "require": { 2507 "require": {
2266 "ext-simplexml": "*", 2508 "ext-simplexml": "*",
2267 "ext-tokenizer": "*", 2509 "ext-tokenizer": "*",
2268 "ext-xmlwriter": "*", 2510 "ext-xmlwriter": "*",
2269 "php": ">=5.1.2" 2511 "php": ">=5.4.0"
2270 }, 2512 },
2271 "require-dev": { 2513 "require-dev": {
2272 "phpunit/phpunit": "~4.0" 2514 "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
2273 }, 2515 },
2274 "bin": [ 2516 "bin": [
2275 "scripts/phpcs", 2517 "bin/phpcs",
2276 "scripts/phpcbf" 2518 "bin/phpcbf"
2277 ], 2519 ],
2278 "type": "library", 2520 "type": "library",
2279 "extra": { 2521 "extra": {
2280 "branch-alias": { 2522 "branch-alias": {
2281 "dev-master": "2.x-dev" 2523 "dev-master": "3.x-dev"
2282 } 2524 }
2283 }, 2525 },
2284 "autoload": {
2285 "classmap": [
2286 "CodeSniffer.php",
2287 "CodeSniffer/CLI.php",
2288 "CodeSniffer/Exception.php",
2289 "CodeSniffer/File.php",
2290 "CodeSniffer/Fixer.php",
2291 "CodeSniffer/Report.php",
2292 "CodeSniffer/Reporting.php",
2293 "CodeSniffer/Sniff.php",
2294 "CodeSniffer/Tokens.php",
2295 "CodeSniffer/Reports/",
2296 "CodeSniffer/Tokenizers/",
2297 "CodeSniffer/DocGenerators/",
2298 "CodeSniffer/Standards/AbstractPatternSniff.php",
2299 "CodeSniffer/Standards/AbstractScopeSniff.php",
2300 "CodeSniffer/Standards/AbstractVariableSniff.php",
2301 "CodeSniffer/Standards/IncorrectPatternException.php",
2302 "CodeSniffer/Standards/Generic/Sniffs/",
2303 "CodeSniffer/Standards/MySource/Sniffs/",
2304 "CodeSniffer/Standards/PEAR/Sniffs/",
2305 "CodeSniffer/Standards/PSR1/Sniffs/",
2306 "CodeSniffer/Standards/PSR2/Sniffs/",
2307 "CodeSniffer/Standards/Squiz/Sniffs/",
2308 "CodeSniffer/Standards/Zend/Sniffs/"
2309 ]
2310 },
2311 "notification-url": "https://packagist.org/downloads/", 2526 "notification-url": "https://packagist.org/downloads/",
2312 "license": [ 2527 "license": [
2313 "BSD-3-Clause" 2528 "BSD-3-Clause"
@@ -2319,202 +2534,30 @@
2319 } 2534 }
2320 ], 2535 ],
2321 "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 2536 "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
2322 "homepage": "http://www.squizlabs.com/php-codesniffer", 2537 "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
2323 "keywords": [ 2538 "keywords": [
2324 "phpcs", 2539 "phpcs",
2325 "standards" 2540 "standards"
2326 ], 2541 ],
2327 "time": "2018-11-07T22:31:41+00:00" 2542 "support": {
2328 }, 2543 "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
2329 { 2544 "source": "https://github.com/squizlabs/PHP_CodeSniffer",
2330 "name": "symfony/console", 2545 "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
2331 "version": "v3.4.30",
2332 "source": {
2333 "type": "git",
2334 "url": "https://github.com/symfony/console.git",
2335 "reference": "12940f20a816c978860fa4925b3f1bbb27e9ac46"
2336 },
2337 "dist": {
2338 "type": "zip",
2339 "url": "https://api.github.com/repos/symfony/console/zipball/12940f20a816c978860fa4925b3f1bbb27e9ac46",
2340 "reference": "12940f20a816c978860fa4925b3f1bbb27e9ac46",
2341 "shasum": ""
2342 },
2343 "require": {
2344 "php": "^5.5.9|>=7.0.8",
2345 "symfony/debug": "~2.8|~3.0|~4.0",
2346 "symfony/polyfill-mbstring": "~1.0"
2347 },
2348 "conflict": {
2349 "symfony/dependency-injection": "<3.4",
2350 "symfony/process": "<3.3"
2351 },
2352 "provide": {
2353 "psr/log-implementation": "1.0"
2354 },
2355 "require-dev": {
2356 "psr/log": "~1.0",
2357 "symfony/config": "~3.3|~4.0",
2358 "symfony/dependency-injection": "~3.4|~4.0",
2359 "symfony/event-dispatcher": "~2.8|~3.0|~4.0",
2360 "symfony/lock": "~3.4|~4.0",
2361 "symfony/process": "~3.3|~4.0"
2362 },
2363 "suggest": {
2364 "psr/log": "For using the console logger",
2365 "symfony/event-dispatcher": "",
2366 "symfony/lock": "",
2367 "symfony/process": ""
2368 },
2369 "type": "library",
2370 "extra": {
2371 "branch-alias": {
2372 "dev-master": "3.4-dev"
2373 }
2374 },
2375 "autoload": {
2376 "psr-4": {
2377 "Symfony\\Component\\Console\\": ""
2378 },
2379 "exclude-from-classmap": [
2380 "/Tests/"
2381 ]
2382 },
2383 "notification-url": "https://packagist.org/downloads/",
2384 "license": [
2385 "MIT"
2386 ],
2387 "authors": [
2388 {
2389 "name": "Fabien Potencier",
2390 "email": "fabien@symfony.com"
2391 },
2392 {
2393 "name": "Symfony Community",
2394 "homepage": "https://symfony.com/contributors"
2395 }
2396 ],
2397 "description": "Symfony Console Component",
2398 "homepage": "https://symfony.com",
2399 "time": "2019-07-24T14:46:41+00:00"
2400 },
2401 {
2402 "name": "symfony/debug",
2403 "version": "v4.3.3",
2404 "source": {
2405 "type": "git",
2406 "url": "https://github.com/symfony/debug.git",
2407 "reference": "527887c3858a2462b0137662c74837288b998ee3"
2408 },
2409 "dist": {
2410 "type": "zip",
2411 "url": "https://api.github.com/repos/symfony/debug/zipball/527887c3858a2462b0137662c74837288b998ee3",
2412 "reference": "527887c3858a2462b0137662c74837288b998ee3",
2413 "shasum": ""
2414 },
2415 "require": {
2416 "php": "^7.1.3",
2417 "psr/log": "~1.0"
2418 },
2419 "conflict": {
2420 "symfony/http-kernel": "<3.4"
2421 },
2422 "require-dev": {
2423 "symfony/http-kernel": "~3.4|~4.0"
2424 },
2425 "type": "library",
2426 "extra": {
2427 "branch-alias": {
2428 "dev-master": "4.3-dev"
2429 }
2430 },
2431 "autoload": {
2432 "psr-4": {
2433 "Symfony\\Component\\Debug\\": ""
2434 },
2435 "exclude-from-classmap": [
2436 "/Tests/"
2437 ]
2438 },
2439 "notification-url": "https://packagist.org/downloads/",
2440 "license": [
2441 "MIT"
2442 ],
2443 "authors": [
2444 {
2445 "name": "Fabien Potencier",
2446 "email": "fabien@symfony.com"
2447 },
2448 {
2449 "name": "Symfony Community",
2450 "homepage": "https://symfony.com/contributors"
2451 }
2452 ],
2453 "description": "Symfony Debug Component",
2454 "homepage": "https://symfony.com",
2455 "time": "2019-07-23T11:21:36+00:00"
2456 },
2457 {
2458 "name": "symfony/finder",
2459 "version": "v4.3.3",
2460 "source": {
2461 "type": "git",
2462 "url": "https://github.com/symfony/finder.git",
2463 "reference": "9638d41e3729459860bb96f6247ccb61faaa45f2"
2464 }, 2546 },
2465 "dist": { 2547 "time": "2020-08-10T04:50:15+00:00"
2466 "type": "zip",
2467 "url": "https://api.github.com/repos/symfony/finder/zipball/9638d41e3729459860bb96f6247ccb61faaa45f2",
2468 "reference": "9638d41e3729459860bb96f6247ccb61faaa45f2",
2469 "shasum": ""
2470 },
2471 "require": {
2472 "php": "^7.1.3"
2473 },
2474 "type": "library",
2475 "extra": {
2476 "branch-alias": {
2477 "dev-master": "4.3-dev"
2478 }
2479 },
2480 "autoload": {
2481 "psr-4": {
2482 "Symfony\\Component\\Finder\\": ""
2483 },
2484 "exclude-from-classmap": [
2485 "/Tests/"
2486 ]
2487 },
2488 "notification-url": "https://packagist.org/downloads/",
2489 "license": [
2490 "MIT"
2491 ],
2492 "authors": [
2493 {
2494 "name": "Fabien Potencier",
2495 "email": "fabien@symfony.com"
2496 },
2497 {
2498 "name": "Symfony Community",
2499 "homepage": "https://symfony.com/contributors"
2500 }
2501 ],
2502 "description": "Symfony Finder Component",
2503 "homepage": "https://symfony.com",
2504 "time": "2019-06-28T13:16:30+00:00"
2505 }, 2548 },
2506 { 2549 {
2507 "name": "symfony/polyfill-ctype", 2550 "name": "symfony/polyfill-ctype",
2508 "version": "v1.11.0", 2551 "version": "v1.18.1",
2509 "source": { 2552 "source": {
2510 "type": "git", 2553 "type": "git",
2511 "url": "https://github.com/symfony/polyfill-ctype.git", 2554 "url": "https://github.com/symfony/polyfill-ctype.git",
2512 "reference": "82ebae02209c21113908c229e9883c419720738a" 2555 "reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
2513 }, 2556 },
2514 "dist": { 2557 "dist": {
2515 "type": "zip", 2558 "type": "zip",
2516 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", 2559 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
2517 "reference": "82ebae02209c21113908c229e9883c419720738a", 2560 "reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
2518 "shasum": "" 2561 "shasum": ""
2519 }, 2562 },
2520 "require": { 2563 "require": {
@@ -2526,7 +2569,11 @@
2526 "type": "library", 2569 "type": "library",
2527 "extra": { 2570 "extra": {
2528 "branch-alias": { 2571 "branch-alias": {
2529 "dev-master": "1.11-dev" 2572 "dev-master": "1.18-dev"
2573 },
2574 "thanks": {
2575 "name": "symfony/polyfill",
2576 "url": "https://github.com/symfony/polyfill"
2530 } 2577 }
2531 }, 2578 },
2532 "autoload": { 2579 "autoload": {
@@ -2543,160 +2590,60 @@
2543 ], 2590 ],
2544 "authors": [ 2591 "authors": [
2545 { 2592 {
2546 "name": "Symfony Community",
2547 "homepage": "https://symfony.com/contributors"
2548 },
2549 {
2550 "name": "Gert de Pagter", 2593 "name": "Gert de Pagter",
2551 "email": "BackEndTea@gmail.com" 2594 "email": "BackEndTea@gmail.com"
2552 }
2553 ],
2554 "description": "Symfony polyfill for ctype functions",
2555 "homepage": "https://symfony.com",
2556 "keywords": [
2557 "compatibility",
2558 "ctype",
2559 "polyfill",
2560 "portable"
2561 ],
2562 "time": "2019-02-06T07:57:58+00:00"
2563 },
2564 {
2565 "name": "symfony/polyfill-mbstring",
2566 "version": "v1.11.0",
2567 "source": {
2568 "type": "git",
2569 "url": "https://github.com/symfony/polyfill-mbstring.git",
2570 "reference": "fe5e94c604826c35a32fa832f35bd036b6799609"
2571 },
2572 "dist": {
2573 "type": "zip",
2574 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609",
2575 "reference": "fe5e94c604826c35a32fa832f35bd036b6799609",
2576 "shasum": ""
2577 },
2578 "require": {
2579 "php": ">=5.3.3"
2580 },
2581 "suggest": {
2582 "ext-mbstring": "For best performance"
2583 },
2584 "type": "library",
2585 "extra": {
2586 "branch-alias": {
2587 "dev-master": "1.11-dev"
2588 }
2589 },
2590 "autoload": {
2591 "psr-4": {
2592 "Symfony\\Polyfill\\Mbstring\\": ""
2593 },
2594 "files": [
2595 "bootstrap.php"
2596 ]
2597 },
2598 "notification-url": "https://packagist.org/downloads/",
2599 "license": [
2600 "MIT"
2601 ],
2602 "authors": [
2603 {
2604 "name": "Nicolas Grekas",
2605 "email": "p@tchwork.com"
2606 }, 2595 },
2607 { 2596 {
2608 "name": "Symfony Community", 2597 "name": "Symfony Community",
2609 "homepage": "https://symfony.com/contributors" 2598 "homepage": "https://symfony.com/contributors"
2610 } 2599 }
2611 ], 2600 ],
2612 "description": "Symfony polyfill for the Mbstring extension", 2601 "description": "Symfony polyfill for ctype functions",
2613 "homepage": "https://symfony.com", 2602 "homepage": "https://symfony.com",
2614 "keywords": [ 2603 "keywords": [
2615 "compatibility", 2604 "compatibility",
2616 "mbstring", 2605 "ctype",
2617 "polyfill", 2606 "polyfill",
2618 "portable", 2607 "portable"
2619 "shim"
2620 ], 2608 ],
2621 "time": "2019-02-06T07:57:58+00:00" 2609 "support": {
2622 }, 2610 "source": "https://github.com/symfony/polyfill-ctype/tree/v1.18.0"
2623 {
2624 "name": "symfony/yaml",
2625 "version": "v4.3.3",
2626 "source": {
2627 "type": "git",
2628 "url": "https://github.com/symfony/yaml.git",
2629 "reference": "34d29c2acd1ad65688f58452fd48a46bd996d5a6"
2630 },
2631 "dist": {
2632 "type": "zip",
2633 "url": "https://api.github.com/repos/symfony/yaml/zipball/34d29c2acd1ad65688f58452fd48a46bd996d5a6",
2634 "reference": "34d29c2acd1ad65688f58452fd48a46bd996d5a6",
2635 "shasum": ""
2636 },
2637 "require": {
2638 "php": "^7.1.3",
2639 "symfony/polyfill-ctype": "~1.8"
2640 },
2641 "conflict": {
2642 "symfony/console": "<3.4"
2643 },
2644 "require-dev": {
2645 "symfony/console": "~3.4|~4.0"
2646 }, 2611 },
2647 "suggest": { 2612 "funding": [
2648 "symfony/console": "For validating YAML files using the lint command" 2613 {
2649 }, 2614 "url": "https://symfony.com/sponsor",
2650 "type": "library", 2615 "type": "custom"
2651 "extra": {
2652 "branch-alias": {
2653 "dev-master": "4.3-dev"
2654 }
2655 },
2656 "autoload": {
2657 "psr-4": {
2658 "Symfony\\Component\\Yaml\\": ""
2659 }, 2616 },
2660 "exclude-from-classmap": [
2661 "/Tests/"
2662 ]
2663 },
2664 "notification-url": "https://packagist.org/downloads/",
2665 "license": [
2666 "MIT"
2667 ],
2668 "authors": [
2669 { 2617 {
2670 "name": "Fabien Potencier", 2618 "url": "https://github.com/fabpot",
2671 "email": "fabien@symfony.com" 2619 "type": "github"
2672 }, 2620 },
2673 { 2621 {
2674 "name": "Symfony Community", 2622 "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
2675 "homepage": "https://symfony.com/contributors" 2623 "type": "tidelift"
2676 } 2624 }
2677 ], 2625 ],
2678 "description": "Symfony Yaml Component", 2626 "time": "2020-07-14T12:35:20+00:00"
2679 "homepage": "https://symfony.com",
2680 "time": "2019-07-24T14:47:54+00:00"
2681 }, 2627 },
2682 { 2628 {
2683 "name": "theseer/fdomdocument", 2629 "name": "theseer/tokenizer",
2684 "version": "1.6.6", 2630 "version": "1.1.3",
2685 "source": { 2631 "source": {
2686 "type": "git", 2632 "type": "git",
2687 "url": "https://github.com/theseer/fDOMDocument.git", 2633 "url": "https://github.com/theseer/tokenizer.git",
2688 "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca" 2634 "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
2689 }, 2635 },
2690 "dist": { 2636 "dist": {
2691 "type": "zip", 2637 "type": "zip",
2692 "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca", 2638 "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
2693 "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca", 2639 "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
2694 "shasum": "" 2640 "shasum": ""
2695 }, 2641 },
2696 "require": { 2642 "require": {
2697 "ext-dom": "*", 2643 "ext-dom": "*",
2698 "lib-libxml": "*", 2644 "ext-tokenizer": "*",
2699 "php": ">=5.3.3" 2645 "ext-xmlwriter": "*",
2646 "php": "^7.0"
2700 }, 2647 },
2701 "type": "library", 2648 "type": "library",
2702 "autoload": { 2649 "autoload": {
@@ -2712,41 +2659,42 @@
2712 { 2659 {
2713 "name": "Arne Blankerts", 2660 "name": "Arne Blankerts",
2714 "email": "arne@blankerts.de", 2661 "email": "arne@blankerts.de",
2715 "role": "lead" 2662 "role": "Developer"
2716 } 2663 }
2717 ], 2664 ],
2718 "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.", 2665 "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
2719 "homepage": "https://github.com/theseer/fDOMDocument", 2666 "support": {
2720 "time": "2017-06-30T11:53:12+00:00" 2667 "issues": "https://github.com/theseer/tokenizer/issues",
2668 "source": "https://github.com/theseer/tokenizer/tree/master"
2669 },
2670 "time": "2019-06-13T22:48:21+00:00"
2721 }, 2671 },
2722 { 2672 {
2723 "name": "webmozart/assert", 2673 "name": "webmozart/assert",
2724 "version": "1.4.0", 2674 "version": "1.9.1",
2725 "source": { 2675 "source": {
2726 "type": "git", 2676 "type": "git",
2727 "url": "https://github.com/webmozart/assert.git", 2677 "url": "https://github.com/webmozart/assert.git",
2728 "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" 2678 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
2729 }, 2679 },
2730 "dist": { 2680 "dist": {
2731 "type": "zip", 2681 "type": "zip",
2732 "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", 2682 "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
2733 "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", 2683 "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
2734 "shasum": "" 2684 "shasum": ""
2735 }, 2685 },
2736 "require": { 2686 "require": {
2737 "php": "^5.3.3 || ^7.0", 2687 "php": "^5.3.3 || ^7.0 || ^8.0",
2738 "symfony/polyfill-ctype": "^1.8" 2688 "symfony/polyfill-ctype": "^1.8"
2739 }, 2689 },
2690 "conflict": {
2691 "phpstan/phpstan": "<0.12.20",
2692 "vimeo/psalm": "<3.9.1"
2693 },
2740 "require-dev": { 2694 "require-dev": {
2741 "phpunit/phpunit": "^4.6", 2695 "phpunit/phpunit": "^4.8.36 || ^7.5.13"
2742 "sebastian/version": "^1.0.1"
2743 }, 2696 },
2744 "type": "library", 2697 "type": "library",
2745 "extra": {
2746 "branch-alias": {
2747 "dev-master": "1.3-dev"
2748 }
2749 },
2750 "autoload": { 2698 "autoload": {
2751 "psr-4": { 2699 "psr-4": {
2752 "Webmozart\\Assert\\": "src/" 2700 "Webmozart\\Assert\\": "src/"
@@ -2768,7 +2716,11 @@
2768 "check", 2716 "check",
2769 "validate" 2717 "validate"
2770 ], 2718 ],
2771 "time": "2018-12-25T11:19:39+00:00" 2719 "support": {
2720 "issues": "https://github.com/webmozart/assert/issues",
2721 "source": "https://github.com/webmozart/assert/tree/master"
2722 },
2723 "time": "2020-07-08T17:02:28+00:00"
2772 } 2724 }
2773 ], 2725 ],
2774 "aliases": [], 2726 "aliases": [],
@@ -2787,5 +2739,6 @@
2787 "platform-dev": [], 2739 "platform-dev": [],
2788 "platform-overrides": { 2740 "platform-overrides": {
2789 "php": "7.1.29" 2741 "php": "7.1.29"
2790 } 2742 },
2743 "plugin-api-version": "2.0.0"
2791} 2744}
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 c66d1c70..53a7555e 100644
--- a/doc/md/Community-&-Related-software.md
+++ b/doc/md/Community-and-related-software.md
@@ -1,64 +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.
15- [emojione](https://github.com/NerosTie/emojione) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli. 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
16- [twemoji](https://github.com/NerosTie/twemoji) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli (Twemoji version) 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.
17- [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
18- [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.
19- [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.
20- [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.
21- [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.
22- [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli
23- [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.
24- [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.
25- [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.
26- [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
27 32
28### Third-party themes 33### Third-party themes
34
29See [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.
30 36
31 37
32### Integration with other platforms 38### Integration with other platforms
39
33- [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
34- [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
35- [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
36- [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)
37- [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)
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)
46
38 47
39### Mobile Apps 48### Mobile Apps
49
40- [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension. 50- [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension.
41- [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
42- [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
43- [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
44 54
55
56### Desktop Apps
57
58- [Ulauncher Extension](https://github.com/sebw/ulauncher-shaarli) - Ulauncher is an an application launcher for Linux, this extension allows research in your Shaarli
59
60
45### Browser addons 61### Browser addons
62
46- [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.
47- [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.
48 65
66
49### Server apps 67### Server apps
68
50- [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
51- [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
52- [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
53- [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
54- [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
55- [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).
56- [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.
57 76
77
58## Alternatives to Shaarli 78## Alternatives to Shaarli
79
59See [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).
60 81
82
61## Community 83## Community
84
62- [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
63- [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)
64- [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)
@@ -69,8 +92,17 @@ See [awesome-selfhosted: bookmarks & link sharing](https://github.com/Kickball/a
69- [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)
70- [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)
71 94
95
72### Articles and social media discussions 96### Articles and social media discussions
73- 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)
74- 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/)
75- 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)
76- 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 4ca6bdc7..00000000
--- a/doc/md/Continuous-integration-tools.md
+++ /dev/null
@@ -1,29 +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
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 88eed8e6..297d7c29 100644
--- a/doc/md/Server-configuration.md
+++ b/doc/md/Server-configuration.md
@@ -1,20 +1,46 @@
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:---:|:---:|:---:
437.3 | Supported | Yes
187.2 | Supported | Yes 447.2 | Supported | Yes
197.1 | Supported | Yes 457.1 | Supported | Yes
207.0 | EOL: 2018-12-03 | Yes (up to Shaarli 0.10.x) 467.0 | EOL: 2018-12-03 | Yes (up to Shaarli 0.10.x)
@@ -23,71 +49,132 @@ Version | Status | Shaarli compatibility
235.4 | EOL: 2015-09-14 | Yes (up to Shaarli 0.8.x) 495.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) 505.3 | EOL: 2014-08-14 | Yes (up to Shaarli 0.8.x)
25 51
26- The following PHP extensions are installed on the server: 52Required PHP extensions:
27 53
28Extension | Required? | Usage 54Extension | Required? | Usage
29---|:---:|--- 55---|:---:|---
30[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS 56[`openssl`](http://php.net/manual/en/book.openssl.php) | requires | OpenSSL, HTTPS
57[`php-json`](http://php.net/manual/en/book.json.php) | required | configuration parsing
58[`php-simplexml`](https://www.php.net/manual/en/book.simplexml.php) | required | REST API (Slim framework)
31[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support 59[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
32[`php-gd`](http://php.net/manual/en/book.image.php) | optional | required to use thumbnails 60[`php-gd`](http://php.net/manual/en/book.image.php) | optional | required to use thumbnails
33[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`) 61[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
34[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way 62[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
35[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster) 63[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)
36 64
37-------------------------------------------------------------------------------- 65Some [plugins](Plugins.md) may require additional configuration.
66
67- [PHP: Supported versions](http://php.net/supported-versions.php)
68- [PHP: Unsupported versions (EOL/End-of-life)](http://php.net/eol.php)
69- [PHP 7 Changelog](http://php.net/ChangeLog-7.php)
70- [PHP 5 Changelog](http://php.net/ChangeLog-5.php)
71- [PHP: Bugs](https://bugs.php.net/)
38 72
39### SSL/TLS configuration
40 73
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**. 74## SSL/TLS (HTTPS)
42 75
43#### Let's Encrypt 76We 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.
44 77
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. 78### Let's Encrypt
46 79
47 * Install `certbot` using the appropriate method described on https://certbot.eff.org/. 80For 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.
48
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:
50 81
51 * Stop the apache2/nginx service. 82 - [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)
52 * Run `certbot --agree-tos --standalone --preferred-challenges tls-sni --email "youremail@example.com" --domain yourdomain.example.com` 83 - [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)
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) 84 - [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).
54 * For Nginx: TODO
55 * Setup your webserver as described below
56 * Restart the apache2/nginx service.
57 85
58#### Self-signed certificates 86In short:
59 87
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. 88```bash
89# install certbot
90sudo apt install certbot
61 91
62* Apache: run `make-ssl-cert generate-default-snakeoil --force-overwrite` 92# stop your webserver if you already have one running
63* Nginx: TODO 93# certbot in standalone mode needs to bind to port 80 (only needed on initial generation)
94sudo systemctl stop apache2
95sudo systemctl stop nginx
96
97# generate initial certificates
98# Let's Encrypt ACME servers must be able to access your server! port forwarding and firewall must be properly configured
99sudo certbot certonly --standalone --noninteractive --agree-tos --email "admin@shaarli.mydomain.org" -d shaarli.mydomain.org
100# this will generate a private key and certificate at /etc/letsencrypt/live/shaarli.mydomain.org/{privkey,fullchain}.pem
101
102# restart the web server
103sudo systemctl start apache2
104sudo systemctl start nginx
105```
106
107On 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.
108
109### Self-signed
110
111If 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:
112
113- [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)
114- [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)
115- [How to Create Self-Signed SSL Certificates with OpenSSL](http://www.xenocafe.com/tutorials/linux/centos/openssl/self_signed_certificates/index.php)
116- [How do I create my own Certificate Authority?](https://workaround.org/certificate-authority)
64 117
65-------------------------------------------------------------------------------- 118--------------------------------------------------------------------------------
66 119
67## Apache 120## Examples
121
122The 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/`:
123
124```bash
125# create the document root (replace with your own domain name)
126sudo mkdir -p /var/www/shaarli.mydomain.org/
127```
128
129You can install Shaarli at the root of your virtualhost, or in a subdirectory as well. See [Directory structure](Directory-structure)
68 130
69Here is a basic configuration example for the Apache web server with `mod_php`.
70 131
71In `/etc/apache2/sites-available/shaarli.conf`: 132### Apache
133
134```bash
135# Install apache + mod_php and PHP modules
136sudo apt update
137sudo apt install apache2 libapache2-mod-php php-json php-mbstring php-gd php-intl php-curl php-gettext
138
139# Edit the virtualhost configuration file with your favorite editor (replace the example domain name)
140sudo nano /etc/apache2/sites-available/shaarli.mydomain.org.conf
141```
72 142
73```apache 143```apache
74<VirtualHost *:443> 144<VirtualHost *:80>
75 ServerName shaarli.my-domain.org 145 ServerName shaarli.mydomain.org
76 DocumentRoot /absolute/path/to/shaarli/ 146 DocumentRoot /var/www/shaarli.mydomain.org/
147
148 # For SSL/TLS certificates acquired with certbot or self-signed certificates
149 # Redirect HTTP requests to HTTPS, except Let's Encrypt ACME challenge requests
150 RewriteEngine on
151 RewriteRule ^.well-known/acme-challenge/ - [L]
152 RewriteCond %{HTTP_HOST} =shaarli.mydomain.org
153 RewriteRule ^ https://shaarli.mydomain.org%{REQUEST_URI} [END,NE,R=permanent]
154</VirtualHost>
77 155
78 # Logging 156# SSL/TLS configuration for Let's Encrypt certificates managed with mod_md
79 # Possible values include: debug, info, notice, warn, error, crit, alert, emerg. 157#MDomain shaarli.mydomain.org
80 LogLevel warn 158#MDCertificateAgreement accepted
81 ErrorLog /var/log/apache2/shaarli-error.log 159#MDContactEmail admin@shaarli.mydomain.org
82 CustomLog /var/log/apache2/shaarli-access.log combined 160#MDPrivateKeys RSA 4096
83 161
84 # Let's Encrypt SSL configuration (recommended) 162<VirtualHost *:443>
85 SSLEngine on 163 ServerName shaarli.mydomain.org
86 SSLCertificateFile /etc/letsencrypt/live/yourdomain.example.com/fullchain.pem 164 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 165
90 # Self-signed SSL cert configuration 166 # SSL/TLS configuration for Let's Encrypt certificates acquired with certbot standalone
167 SSLEngine on
168 SSLCertificateFile /etc/letsencrypt/live/shaarli.mydomain.org/fullchain.pem
169 SSLCertificateKeyFile /etc/letsencrypt/live/shaarli.mydomain.org/privkey.pem
170 # Let's Encrypt settings from https://github.com/certbot/certbot/blob/master/certbot-apache/certbot_apache/_internal/tls_configs/current-options-ssl-apache.conf
171 SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
172 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
173 SSLHonorCipherOrder off
174 SSLSessionTickets off
175 SSLOptions +StrictRequire
176
177 # SSL/TLS configuration for self-signed certificates
91 #SSLEngine on 178 #SSLEngine on
92 #SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem 179 #SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
93 #SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key 180 #SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
@@ -98,345 +185,262 @@ In `/etc/apache2/sites-available/shaarli.conf`:
98 #php_value error_reporting 2147483647 185 #php_value error_reporting 2147483647
99 #php_value error_log /var/log/apache2/shaarli-php-error.log 186 #php_value error_log /var/log/apache2/shaarli-php-error.log
100 187
101 <Directory /absolute/path/to/shaarli/> 188 <Directory /var/www/shaarli.mydomain.org/>
102 #Required for .htaccess support 189 # Required for .htaccess support
103 AllowOverride All 190 AllowOverride All
104 Order allow,deny 191 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> 192 </Directory>
112 193
113</VirtualHost> 194 <LocationMatch "/\.">
114``` 195 # Prevent accessing dotfiles
115 196 RedirectMatch 404 ".*"
116Enable this configuration with `sudo a2ensite shaarli` 197 </LocationMatch>
117
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._
119 198
120_Note: Apache module `mod_rewrite` must be enabled to use the REST API._ 199 <LocationMatch "\.(?:ico|css|js|gif|jpe?g|png)$">
200 # allow client-side caching of static files
201 Header set Cache-Control "max-age=2628000, public, must-revalidate, proxy-revalidate"
202 </LocationMatch>
121 203
204 # serve the Shaarli favicon from its custom location
205 Alias favicon.ico /var/www/shaarli.mydomain.org/images/favicon.ico
122 206
123## Nginx 207</VirtualHost>
208```
124 209
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. 210```bash
211# Enable the virtualhost
212sudo a2ensite shaarli.mydomain.org
126 213
127<!--- TODO refactor everything below this point ---> 214# mod_ssl must be enabled to use TLS/SSL certificates
215# https://httpd.apache.org/docs/current/mod/mod_ssl.html
216sudo a2enmod ssl
128 217
129### Common setup 218# mod_rewrite must be enabled to use the REST API
130Once Nginx and PHP-FPM are installed, we need to ensure: 219# https://httpd.apache.org/docs/current/mod/mod_rewrite.html
220sudo a2enmod rewrite
131 221
132- Nginx and PHP-FPM are running using the _same user and group_ 222# mod_headers must be enabled to set custom headers from the server config
133- both these user and group have 223sudo a2enmod headers
134 - `read` permissions for Shaarli resources
135 - `execute` permissions for Shaarli directories _AND_ their parent directories
136 224
137On a production server: 225# mod_version must only be enabled if you use Apache 2.2 or lower
226# https://httpd.apache.org/docs/current/mod/mod_version.html
227# sudo a2enmod version
138 228
139- `user:group` will likely be `http:http`, `www:www` or `www-data:www-data` 229# restart the apache service
140- files will be located under `/var/www`, `/var/http` or `/usr/share/nginx` 230sudo systemctl restart apache2
231```
141 232
142On a development server: 233- [How to install the Apache web server](https://www.digitalocean.com/community/tutorials/how-to-install-the-apache-web-server-on-debian-10)
234- [Apache/PHP - error log per VirtualHost - StackOverflow](http://stackoverflow.com/q/176)
235- [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/)
236- [Server-side TLS (Apache) - Mozilla](https://wiki.mozilla.org/Security/Server_Side_TLS#Apache)
237- [Apache 2.4 documentation](https://httpd.apache.org/docs/2.4/)
238- [Apache mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
239- [Apache Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)
143 240
144- files may be located in a user's home directory
145- in this case, make sure both Nginx and PHP-FPM are running as the local user/group!
146 241
147For all following configuration examples, this user/group pair will be used: 242### Nginx
148 243
149- `user:group = john:users`, 244This 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`.
150 245
151which corresponds to the following service configuration:
152 246
153```ini 247```bash
154; /etc/php/php-fpm.conf 248# install nginx and php-fpm
155user = john 249sudo apt update
156group = users 250sudo apt install nginx php-fpm
157 251
158[...] 252# Edit the virtualhost configuration file with your favorite editor
159listen.owner = john 253sudo nano /etc/nginx/sites-available/shaarli.mydomain.org
160listen.group = users
161``` 254```
162 255
163```nginx 256```nginx
164# /etc/nginx/nginx.conf 257server {
165user john users; 258 listen 80;
259 server_name shaarli.mydomain.org;
166 260
167http { 261 # redirect all plain HTTP requests to HTTPS
168 [...] 262 return 301 https://shaarli.mydomain.org$request_uri;
169} 263}
170```
171
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
178# /etc/nginx/nginx.conf
179
180http {
181 [...]
182
183 client_max_body_size 10m;
184
185 [...]
186}
187```
188
189```ini
190# /etc/php5/fpm/php.ini
191
192[...]
193post_max_size = 10M
194[...]
195upload_max_filesize = 10M
196```
197 264
198### Minimal 265server {
199_WARNING: Use for development only!_ 266 # ipv4 listening port/protocol
200 267 listen 443 ssl http2;
201```nginx 268 # ipv6 listening port/protocol
202user john users; 269 listen [::]:443 ssl http2;
203worker_processes 1; 270 server_name shaarli.mydomain.org;
204events { 271 root /var/www/shaarli.mydomain.org;
205 worker_connections 1024; 272
206} 273 # log file locations
274 # combined log format prepends the virtualhost/domain name to log entries
275 access_log /var/log/nginx/access.log combined;
276 error_log /var/log/nginx/error.log;
207 277
208http { 278 # paths to private key and certificates for SSL/TLS
209 include mime.types; 279 ssl_certificate /etc/ssl/shaarli.mydomain.org.crt;
210 default_type application/octet-stream; 280 ssl_certificate_key /etc/ssl/private/shaarli.mydomain.org.key;
211 keepalive_timeout 20; 281
212 282 # Let's Encrypt SSL settings from https://github.com/certbot/certbot/blob/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf
213 index index.html index.php; 283 ssl_session_cache shared:le_nginx_SSL:10m;
214 284 ssl_session_timeout 1440m;
215 server { 285 ssl_session_tickets off;
216 listen 80; 286 ssl_protocols TLSv1.2 TLSv1.3;
217 server_name localhost; 287 ssl_prefer_server_ciphers off;
218 root /home/john/web; 288 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";
219 289
220 access_log /var/log/nginx/access.log; 290 # increase the maximum file upload size if needed: by default nginx limits file upload to 1MB (413 Entity Too Large error)
221 error_log /var/log/nginx/error.log; 291 client_max_body_size 100m;
222 292
223 location /shaarli/ { 293 # relative path to shaarli from the root of the webserver
224 try_files $uri /shaarli/index.php$is_args$args; 294 location / {
225 access_log /var/log/nginx/shaarli.access.log; 295 # default index file when no file URI is requested
226 error_log /var/log/nginx/shaarli.error.log; 296 index index.php;
227 } 297 try_files $uri /index.php$is_args$args;
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 } 298 }
237}
238```
239 299
240### Modular 300 location ~ (index)\.php$ {
241The previous setup is sufficient for development purposes, but has several major caveats: 301 try_files $uri =404;
302 # slim API - split URL path into (script_filename, path_info)
303 fastcgi_split_path_info ^(.+\.php)(/.+)$;
304 # pass PHP requests to PHP-FPM
305 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
306 fastcgi_index index.php;
307 include fastcgi.conf;
308 }
242 309
243- every content that does not match the PHP rule will be sent to client browsers: 310 location ~ \.php$ {
244 - dotfiles - in our case, `.htaccess` 311 # deny access to all other PHP scripts
245 - temporary files, e.g. Vim or Emacs files: `index.php~` 312 # disable this if you host other PHP applications on the same virtualhost
246- asset / static resource caching is not optimized 313 deny all;
247- if serving several PHP sites, there will be a lot of duplication: `location /shaarli/`, `location /mysite/`, etc. 314 }
248 315
249To solve this, we will split Nginx configuration in several parts, that will be included when needed: 316 location ~ /\. {
317 # deny access to dotfiles
318 deny all;
319 }
250 320
251```nginx 321 location ~ ~$ {
252# /etc/nginx/deny.conf 322 # deny access to temp editor files, e.g. "script.php~"
253location ~ /\. { 323 deny all;
254 # deny access to dotfiles 324 }
255 access_log off;
256 log_not_found off;
257 deny all;
258}
259 325
260location ~ ~$ { 326 location = /favicon.ico {
261 # deny access to temp editor files, e.g. "script.php~" 327 # serve the Shaarli favicon from its custom location
262 access_log off; 328 alias /var/www/shaarli/images/favicon.ico;
263 log_not_found off; 329 }
264 deny all;
265}
266```
267 330
268```nginx 331 # allow client-side caching of static files
269# /etc/nginx/php.conf 332 location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
270location ~ (index)\.php$ { 333 expires max;
271 # Slim - split URL path into (script_filename, path_info) 334 add_header Cache-Control "public, must-revalidate, proxy-revalidate";
272 try_files $uri =404; 335 # HTTP 1.0 compatibility
273 fastcgi_split_path_info ^(.+\.php)(/.+)$; 336 add_header Pragma public;
274 337 }
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 338
281location ~ \.php$ {
282 # deny access to all other PHP scripts
283 deny all;
284} 339}
285``` 340```
286 341
287```nginx 342```bash
288# /etc/nginx/static_assets.conf 343# enable the configuration/virtualhost
289location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { 344sudo ln -s /etc/nginx/sites-available/shaarli.mydomain.org /etc/nginx/sites-enabled/shaarli.mydomain.org
290 expires max; 345# reload nginx configuration
291 add_header Pragma public; 346sudo systemctl reload nginx
292 add_header Cache-Control "public, must-revalidate, proxy-revalidate";
293}
294``` 347```
295 348
296```nginx 349- [How to install the Nginx web server](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-debian-10)
297# /etc/nginx/nginx.conf 350- [Nginx Beginner's guide](http://nginx.org/en/docs/beginners_guide.html)
298[...] 351- [Nginx documentation](https://nginx.org/en/docs/)
299 352- [Nginx ngx_http_fastcgi_module](http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html)
300http { 353- [Nginx Pitfalls](http://wiki.nginx.org/Pitfalls)
301 [...] 354- [Nginx PHP configuration examples - Karl Blessing](http://kbeezie.com/nginx-configuration-examples/)
302 355- [Server-side TLS (Nginx) - Mozilla](https://wiki.mozilla.org/Security/Server_Side_TLS#Nginx)
303 root /home/john/web;
304 access_log /var/log/nginx/access.log;
305 error_log /var/log/nginx/error.log;
306 356
307 server {
308 # virtual host for a first domain
309 listen 80;
310 server_name my.first.domain.org;
311 357
312 location /shaarli/ {
313 # Slim - rewrite URLs
314 try_files $uri /shaarli/index.php$is_args$args;
315 358
316 access_log /var/log/nginx/shaarli.access.log; 359## Reverse proxies
317 error_log /var/log/nginx/shaarli.error.log;
318 }
319 360
320 location = /shaarli/favicon.ico { 361If 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.
321 # serve the Shaarli favicon from its custom location
322 alias /var/www/shaarli/images/favicon.ico;
323 }
324 362
325 include deny.conf;
326 include static_assets.conf;
327 include php.conf;
328 }
329 363
330 server {
331 # virtual host for a second domain
332 listen 80;
333 server_name second.domain.com;
334 364
335 location /minigal/ { 365## 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 366
340 include deny.conf; 367Web 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 368
347### Redirect HTTP to HTTPS 369- Apache: `/etc/php/<PHP_VERSION>/apache2/php.ini`
348Assuming you have generated a (self-signed) key and certificate, and they are 370- 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 371
352```nginx 372```ini
353# /etc/nginx/nginx.conf
354[...] 373[...]
374# (optional) increase the maximum file upload size:
375post_max_size = 100M
376[...]
377# (optional) increase the maximum file upload size:
378upload_max_filesize = 100M
379```
355 380
356http { 381To 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 382
369 return 301 https://localhost$request_uri; 383```bash
370 } 384# example
385echo '<?php phpinfo(); ?>' | sudo tee /var/www/shaarli.mydomain.org/phpinfo.php
386#give read-only access to this file to the webserver user
387sudo chown www-data:root /var/www/shaarli.mydomain.org/phpinfo.php
388sudo chmod 0400 /var/www/shaarli.mydomain.org/phpinfo.php
389```
371 390
372 server { 391Access 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 392
376 ssl_certificate /home/john/ssl/localhost.crt; 393It 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 394
379 location /shaarli/ {
380 # Slim - rewrite URLs
381 try_files $uri /index.php$is_args$args;
382 395
383 access_log /var/log/nginx/shaarli.access.log; 396## Robots and crawlers
384 error_log /var/log/nginx/shaarli.error.log;
385 }
386 397
387 location = /shaarli/favicon.ico { 398To 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 399
392 include deny.conf; 400```
393 include static_assets.conf; 401User-agent: *
394 include php.conf; 402Disallow: /
395 }
396}
397``` 403```
398 404
399## Proxies 405By 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 406
403- `X-Forwarded-Proto` 407- [Robots exclusion standard](https://en.wikipedia.org/wiki/Robots_exclusion_standard)
404- `X-Forwarded-Host` 408- [Introduction to robots.txt](https://support.google.com/webmasters/answer/6062608?hl=en)
405- `X-Forwarded-For` 409- [Robots meta tag, data-nosnippet, and X-Robots-Tag specifications](https://developers.google.com/search/reference/robots_meta_tag)
410- [About robots.txt](http://www.robotstxt.org)
411- [About the robots META tag](https://www.robotstxt.org/meta.html)
406 412
407In you [Shaarli configuration](Shaarli-configuration) `data/config.json.php`, add the public IP of your proxy under `security.trusted_proxies`.
408 413
409See also [proxy-related](https://github.com/shaarli/Shaarli/issues?utf8=%E2%9C%93&q=label%3Aproxy+) issues. 414## Fail2ban
410 415
411## Robots and crawlers 416[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:
412 417
413Shaarli disallows indexing and crawling of your local documentation pages by search engines, using `<meta name="robots">` HTML tags. 418```ini
414Your Shaarli instance and other pages you host may still be indexed by various robots on the public Internet. 419# /etc/fail2ban/filter.d/shaarli-auth.conf
415You may want to setup a robots.txt file or other crawler control mechanism on your server. 420[INCLUDES]
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) 421before = common.conf
422[Definition]
423failregex = \s-\s<HOST>\s-\sLogin failed for user.*$
424ignoreregex =
425```
417 426
418## See also 427```ini
428# /etc/fail2ban/jail.local
429[shaarli-auth]
430enabled = true
431port = https,http
432filter = shaarli-auth
433logpath = /var/www/shaarli.mydomain.org/data/log.txt
434# allow 3 login attempts per IP address
435# (over a period specified by findtime = in /etc/fail2ban/jail.conf)
436maxretry = 3
437# permanently ban the IP address after reaching the limit
438bantime = -1
439```
419 440
420 * [Server security](Server-security.md) 441Then restart the service: `sudo systemctl restart fail2ban`
421 442
422#### Webservers
423 443
424- [Apache/PHP - error log per VirtualHost](http://stackoverflow.com/q/176) (StackOverflow) 444## What next?
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
435#### PHP
436 445
437- [Travis configuration](https://github.com/shaarli/Shaarli/blob/master/.travis.yml) 446[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 700084e2..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/php5/php.ini`; some distributions provide different configuration environments, e.g.
5 - `/etc/php5/php.ini` - used when running console scripts
6 - `/etc/php5/apache2/php.ini` - used when a client requests PHP resources from Apache
7 - `/etc/php5/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 664e36dd..263fb761 100644
--- a/doc/md/Shaarli-configuration.md
+++ b/doc/md/Shaarli-configuration.md
@@ -1,126 +1,24 @@
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 13
100### Updates 14Some settings can be configured directly from a web browser by accesing the `Tools` menu. Values are read/written to/from the configuration file.
101 15
102- **check_updates**: Enable or disable update check to the git repository. 16![](https://i.imgur.com/boaaibC.png)
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 17
106### Privacy 18### LDAP
107 19
108- **default_private_links**: Check the private checkbox by default for every new link. 20- **host**: LDAP host used for user authentication
109- **hide_public_links**: All links are hidden while logged out. 21- **dn**: user DN template (`sprintf` format, `%s` being replaced by user login)
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
122- **enable_thumbnails**: Enable or disable thumbnail display.
123- **enable_localcache**: Enable or disable local cache.
124 22
125## Configuration file example 23## Configuration file example
126 24
@@ -177,6 +75,9 @@ Must be an associative array: `translation domain => translation path`.
177 "title": "My Shaarli", 75 "title": "My Shaarli",
178 "header_link": "?" 76 "header_link": "?"
179 }, 77 },
78 "dev": {
79 "debug": false,
80 }
180 "extras": { 81 "extras": {
181 "show_atom": false, 82 "show_atom": false,
182 "hide_public_links": false, 83 "hide_public_links": false,
@@ -223,13 +124,98 @@ Must be an associative array: `translation domain => translation path`.
223 "extensions": { 124 "extensions": {
224 "demo": "plugins/demo_plugin/languages/" 125 "demo": "plugins/demo_plugin/languages/"
225 } 126 }
127 },
128 "ldap": {
129 "host": "ldap://localhost",
130 "dn": "uid=%s,ou=people,dc=example,dc=org"
226 } 131 }
227} ?> 132} ?>
228``` 133```
229 134
230## 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- **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.
154- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`.
155
156### Security
157
158- **session_protection_disabled**: Disable session cookie hijacking protection (not recommended).
159 It might be useful if your IP adress often changes.
160- **ban_after**: Failed login attempts before being IP banned.
161- **ban_duration**: IP ban duration in seconds.
162- **open_shaarli**: Anyone can add a new Shaare while logged out if enabled.
163- **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy.
164- **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"]`).
165
166### Resources
167
168- **data_dir**: Data directory.
169- **datastore**: Shaarli's Shaares database file path.
170- **history**: Shaarli's operation history file path.
171- **updates**: File path for the ran updates file.
172- **log**: Log file path.
173- **update_check**: Last update check file path.
174- **raintpl_tpl**: Templates directory.
175- **raintpl_tmp**: Template engine cache directory.
176- **thumbnails_cache**: Thumbnails cache directory.
177- **page_cache**: Shaarli's internal cache directory.
178- **ban_file**: Banned IP file path.
179
180### Translation
181
182- **language**: translation language (also see [Translations](Translations))
183 - **auto** (default): The translation language is chosen from the browser locale.
184 It means that the language can be different for 2 different visitors depending on their locale.
185 - **en**: Use the English translation.
186 - **fr**: Use the French translation.
187- **mode**:
188 - **auto** or **php** (default): Use the PHP implementation of gettext (slower)
189 - **gettext**: Use PHP builtin gettext extension
190 (faster, but requires `php-gettext` to be installed and to reload the web server on update)
191- **extension**: Translation extensions for custom themes or plugins.
192Must be an associative array: `translation domain => translation path`.
193
194### Updates
195
196- **check_updates**: Enable or disable update check to the git repository.
197- **check_updates_branch**: Git branch used to check updates (e.g. `stable` or `master`).
198- **check_updates_interval**: Look for new version every N seconds (default: every day).
199
200### Privacy
201
202- **default_private_links**: Check the private checkbox by default for every new Shaare.
203- **hide_public_links**: All Shaares are hidden while logged out.
204- **force_login**: if **hide_public_links** and this are set to `true`, all anonymous users are redirected to the login page.
205- **hide_timestamps**: Timestamps are hidden.
206- **remember_user_default**: Default state of the login page's *remember me* checkbox
207 - `true`: checked by default, `false`: unchecked by default
208
209### Feed
210
211- **rss_permalinks**: Enable this to redirect RSS links to Shaarli's permalinks instead of shaared URL.
212- **show_atom**: Display ATOM feed button.
213
214### Thumbnail
215
216- **enable_thumbnails**: Enable or disable thumbnail display.
217- **enable_localcache**: Enable or disable local cache.
231 218
232The `playvideos` plugin may require that you adapt your server's 219## Plugins configuration
233[Content Security Policy](https://github.com/shaarli/Shaarli/blob/master/plugins/playvideos/README.md#troubleshooting)
234configuration to work properly.
235 220
221See [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 570f6956..e1ed5e00 100644
--- a/doc/md/Troubleshooting.md
+++ b/doc/md/Troubleshooting.md
@@ -1,97 +1,58 @@
1# Troubleshooting 1# Troubleshooting
2 2
3## Browser 3First of all, ensure that both the [web server](Server-configuration.md) and [Shaarli](Shaarli-configuration.md) are correctly configured.
4 4
5### Redirection issues (HTTP Referer)
6
7Depending on its configuration and installed plugins, the browser may remove or alter (spoof) HTTP referers, thus preventing Shaarli from properly redirecting between pages.
8 5
9See: 6## Login
10 7
11- [HTTP referer](https://en.wikipedia.org/wiki/HTTP_referer) (Wikipedia) 8### I forgot my password!
12- [Improve online privacy by controlling referrer information](http://www.ghacks.net/2015/01/22/improve-online-privacy-by-controlling-referrer-information/)
13- [Better security, privacy and anonymity in Firefox](http://b.agilob.net/better-security-privacy-and-anonymity-in-firefox/)
14 9
15### Firefox HTTP Referer options 10Delete the file `data/config.json.php` and display the page again. You will be asked for a new login/password.
16 11
17HTTP settings are available by browsing `about:config`, here are the available settings and their values. 12### I'm locked out - Login bruteforce protection
18 13
19`network.http.sendRefererHeader` - determines when to send the Referer HTTP header 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.
20 15
21- `0`: Never send the referring URL 16- To remove the current IP bans, delete the file `data/ipbans.php`
22 - not recommended, may break some sites 17- To list all login attempts, see `data/log.txt` (succesful/failed logins, bans/lifted bans)
23- `1`: Send only on clicked links
24- `2` (default): Send for links and images
25 18
26`network.http.referer.XOriginPolicy` - Cross-domain origin policy 19--------------------------------------
27 20
28- `0` (default): Always send 21## Browser issues
29- `1`: Send if base domains match
30- `2`: Send if hosts match
31 22
32`network.http.referer.spoofSource` - Referer spoofing (~faking) 23### Redirection issues (HTTP Referer)
33 24
34- `false` (default): real referer 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.
35- `true`: spoof referer (use target URI as referer)
36 - known to break some functionality in Shaarli
37 26
38`network.http.referer.trimmingPolicy` - trim the URI not to send a full Referer 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.
39 28
40- `0`: (default): send full URI
41- `1`: scheme+host+port+path
42- `2`: scheme+host+port
43 29
44### Firefox, localhost and redirections 30### Firefox, localhost and redirections
45 31
46`localhost` is not a proper Fully Qualified Domain Name (FQDN); if Firefox has 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,
47been set up to spoof referers, or only accept requests from the same base domain/host, 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/.
48Shaarli redirections will not work properly.
49
50To solve this, assign a local domain to your host, e.g.
51```
52127.0.0.1 localhost desktop localhost.lan
53::1 localhost desktop localhost.lan
54```
55 34
56and browse Shaarli at http://localhost.lan/. 35-----------------------------------------
57
58Related threads:
59- [What is localhost.localdomain for?](https://bbs.archlinux.org/viewtopic.php?id=156064)
60- [Stop returning to the first page after editing a bookmark from another page](https://github.com/shaarli/Shaarli/issues/311)
61
62## Login
63
64### I forgot my password!
65
66Delete the file `data/config.json.php` and display the page again. You will be asked for a new login/password.
67
68### I'm locked out - Login bruteforce protection
69
70Login 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.
71
72To remove the current IP bans, delete the file `data/ipbans.php`
73
74### List of all login attempts
75
76The file `data/log.txt` shows all logins (successful or failed) and bans/lifted bans.
77Search for `failed` in this file to look for unauthorized login attempts.
78 36
79## Hosting problems 37## Hosting problems
80 38
81### Old PHP versions 39### Old PHP versions
82 40
83On **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:
84and so support now the tag autocompletion but you have to do the following.
85
86At the root of your webspace create a `sessions` directory and a `.htaccess` file containing:
87 42
88```xml 43```xml
89<IfDefine Free> 44<IfDefine Free>
90php56 1 45php56 1
91</IfDefine> 46</IfDefine>
47<Files ".ht*">
48Order allow,deny
49Deny from all
50Satisfy all
51</Files>
52Options -Indexes
92``` 53```
93 54
94- 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`
95- 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).
96- 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:
97 58
@@ -101,9 +62,11 @@ php56 1
101//if (strpos($status,'200 OK')) $title=html_extract_title($data); 62//if (strpos($status,'200 OK')) $title=html_extract_title($data);
102``` 63```
103 64
104- 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).
105- 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`
106 68
69
107### Dates are not properly formatted 70### Dates are not properly formatted
108 71
109Shaarli 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)
@@ -123,6 +86,118 @@ This can be caused by several things:
123- 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.
124- 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.
125 88
126## Sessions do not seem to work correctly on your server 89
90### Old apache versions, Internal Server Error
91
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).
93
94
95### Sessions do not seem to work correctly on your server
127 96
128Follow 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)).
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-Docker.md b/doc/md/Unit-tests-Docker.md
deleted file mode 100644
index 59bd5b45..00000000
--- a/doc/md/Unit-tests-Docker.md
+++ /dev/null
@@ -1,56 +0,0 @@
1## Running tests inside Docker containers
2
3Read first:
4
5- [Docker 101](docker/docker-101.md)
6- [Docker resources](docker/resources.md)
7- [Unit tests](Unit-tests.md)
8
9### Docker test images
10
11Test Dockerfiles are located under `tests/docker/<distribution>/Dockerfile`,
12and can be used to build Docker images to run Shaarli test suites under common
13Linux environments.
14
15Dockerfiles are provided for the following environments:
16
17- `alpine36` - [Alpine 3.6](https://www.alpinelinux.org/downloads/)
18- `debian8` - [Debian 8 Jessie](https://www.debian.org/DebianJessie) (oldstable)
19- `debian9` - [Debian 9 Stretch](https://wiki.debian.org/DebianStretch) (stable)
20- `ubuntu16` - [Ubuntu 16.04 Xenial Xerus](http://releases.ubuntu.com/16.04/) (LTS)
21
22What's behind the curtains:
23
24- each image provides:
25 - a base Linux OS
26 - Shaarli PHP dependencies (OS packages)
27 - test PHP dependencies (OS packages)
28 - Composer
29- the local workspace is mapped to the container's `/shaarli/` directory,
30- the files are rsync'd so tests are run using a standard Linux user account
31 (running tests as `root` would bypass permission checks and may hide issues)
32- the tests are run inside the container.
33
34### Building test images
35
36```bash
37# build the Debian 9 Docker image
38$ cd /path/to/shaarli
39$ cd tests/docker/debian9
40$ docker build -t shaarli-test:debian9 .
41```
42
43### Running tests
44
45```bash
46$ cd /path/to/shaarli
47
48# install/update 3rd-party test dependencies
49$ composer install --prefer-dist
50
51# run tests using the freshly built image
52$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_test
53
54# run the full test campaign
55$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_all_tests
56```
diff --git a/doc/md/Unit-tests.md b/doc/md/Unit-tests.md
deleted file mode 100644
index f6030d5c..00000000
--- a/doc/md/Unit-tests.md
+++ /dev/null
@@ -1,157 +0,0 @@
1### Setup your environment for tests
2
3The framework used is [PHPUnit](https://phpunit.de/); 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# system-wide version
14$ composer install
15$ composer update
16
17# local version
18$ php composer.phar self-update
19$ php composer.phar install
20$ php composer.phar update
21```
22
23#### Install Shaarli dev dependencies
24
25```bash
26$ cd /path/to/shaarli
27$ composer update
28```
29
30#### Install and enable Xdebug to generate PHPUnit coverage reports
31
32See http://xdebug.org/docs/install
33
34For Debian-based distros:
35```bash
36$ aptitude install php5-xdebug
37```
38For ArchLinux:
39```bash
40$ pacman -S xdebug
41```
42
43Then add the following line to `/etc/php/php.ini`:
44```ini
45zend_extension=xdebug.so
46```
47
48#### Run unit tests
49
50Successful test suite:
51```bash
52$ make test
53
54-------
55PHPUNIT
56-------
57PHPUnit 4.6.9 by Sebastian Bergmann and contributors.
58
59Configuration read from /home/virtualtam/public_html/shaarli/phpunit.xml
60
61....................................
62
63Time: 759 ms, Memory: 8.25Mb
64
65OK (36 tests, 65 assertions)
66```
67
68Test suite with failures and errors:
69```bash
70$ make test
71-------
72PHPUNIT
73-------
74PHPUnit 4.6.9 by Sebastian Bergmann and contributors.
75
76Configuration read from /home/virtualtam/public_html/shaarli/phpunit.xml
77
78E..FF...............................
79
80Time: 802 ms, Memory: 8.25Mb
81
82There was 1 error:
83
841) LinkDBTest::testConstructLoggedIn
85Missing argument 2 for LinkDB::__construct(), called in /home/virtualtam/public_html/shaarli/tests/Link\
86DBTest.php on line 79 and defined
87
88/home/virtualtam/public_html/shaarli/application/LinkDB.php:58
89/home/virtualtam/public_html/shaarli/tests/LinkDBTest.php:79
90
91--
92
93There were 2 failures:
94
951) LinkDBTest::testCheckDBNew
96Failed asserting that two strings are equal.
97--- Expected
98+++ Actual
99@@ @@
100-'e3edea8ea7bb50be4bcb404df53fbb4546a7156e'
101+'85eab0c610d4f68025f6ed6e6b6b5fabd4b55834'
102
103/home/virtualtam/public_html/shaarli/tests/LinkDBTest.php:121
104
1052) LinkDBTest::testCheckDBLoad
106Failed asserting that two strings are equal.
107--- Expected
108+++ Actual
109@@ @@
110-'e3edea8ea7bb50be4bcb404df53fbb4546a7156e'
111+'85eab0c610d4f68025f6ed6e6b6b5fabd4b55834'
112
113/home/virtualtam/public_html/shaarli/tests/LinkDBTest.php:133
114
115FAILURES!
116Tests: 36, Assertions: 63, Errors: 1, Failures: 2.
117```
118
119#### Test results and coverage
120
121By default, PHPUnit will run all suitable tests found under the `tests` directory.
122
123Each test has 3 possible outcomes:
124
125- `.` - success
126- `F` - failure: the test was run but its results are invalid
127 - the code does not behave as expected
128 - dependencies to external elements: globals, session, cache...
129- `E` - error: something went wrong and the tested code has crashed
130 - typos in the code, or in the test code
131 - dependencies to missing external elements
132
133If Xdebug has been installed and activated, two coverage reports will be generated:
134
135- a summary in the console
136- a detailed HTML report with metrics for tested code
137 - to open it in a web browser: `firefox coverage/index.html &`
138
139### Executing specific tests
140
141Add a [`@group`](https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.group) annotation in a test class or method comment:
142
143```php
144/**
145 * Netscape bookmark import
146 * @group WIP
147 */
148class BookmarkImportTest extends PHPUnit_Framework_TestCase
149{
150 [...]
151}
152```
153
154To run all tests annotated with `@group WIP`:
155```bash
156$ vendor/bin/phpunit --group WIP tests/
157```
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 9b0d3a7d..c29774de 100644
--- a/doc/md/Plugin-System.md
+++ b/doc/md/dev/Plugin-system.md
@@ -1,24 +1,21 @@
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`.
20 17
21Under `plugin` folder, create a folder named with your plugin name. Then create a <plugin_name>.php file in that folder. 18Under `plugin` folder, create a folder named with your plugin name. Then create a <plugin_name>.meta file and a <plugin_name>.php file in that folder.
22 19
23You should have the following tree view: 20You should have the following tree view:
24 21
@@ -26,17 +23,23 @@ You should have the following tree view:
26| index.php 23| index.php
27| plugins/ 24| plugins/
28|---| demo_plugin/ 25|---| demo_plugin/
26| |---| demo_plugin.meta
29| |---| demo_plugin.php 27| |---| demo_plugin.php
30``` 28```
31 29
30
32### Plugin initialization 31### Plugin initialization
33 32
34At the beginning of Shaarli execution, all enabled plugins are loaded. At this point, the plugin system looks for an `init()` function 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.
35 34
36 <plugin_name>_init($conf) 35 <plugin_name>_init($conf)
37 36
38This function can be used to create initial data, load default settings, etc. But also to set *plugin errors*. If the initialization function returns an array of strings, they will be understand as errors, and displayed in the header to logged in users. 37This function can be used to create initial data, load default settings, etc. But also to set *plugin errors*. If the initialization function returns an array of strings, they will be understand as errors, and displayed in the header to logged in users.
39 38
39The plugin system also looks for a `description` variable in the <plugin_name>.meta file, to be displayed in the plugin administration page.
40
41 description="The plugin does this and that."
42
40### Understanding hooks 43### Understanding hooks
41 44
42A plugin is a set of functions. Each function will be triggered by the plugin system at certain point in Shaarli execution. 45A plugin is a set of functions. Each function will be triggered by the plugin system at certain point in Shaarli execution.
@@ -58,6 +61,7 @@ For example, if my plugin want to add data to the header, this function is neede
58 61
59If 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.
60 63
64
61### Plugin's data 65### Plugin's data
62 66
63#### Parameters 67#### Parameters
@@ -68,6 +72,26 @@ Every hook function has a `$data` parameter. Its content differs for each hooks.
68 72
69 return $data; 73 return $data;
70 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
71#### Filling templates placeholder 95#### Filling templates placeholder
72 96
73Template placeholders are displayed in template in specific places. 97Template placeholders are displayed in template in specific places.
@@ -84,13 +108,14 @@ array_push($data['top_placeholder'], 'My', 'content');
84return $data; 108return $data;
85``` 109```
86 110
111
87#### Data manipulation 112#### Data manipulation
88 113
89When 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`.
90 115
91The data contained by this array can be altered before template rendering. 116The data contained by this array can be altered before template rendering.
92 117
93For exemple, in linklist, it is possible to alter every title: 118For example, in linklist, it is possible to alter every title:
94 119
95```php 120```php
96// mind the reference if you want $data to be altered 121// mind the reference if you want $data to be altered
@@ -114,19 +139,35 @@ Each file contain two keys:
114 139
115> 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.
116 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 `_BASE_PATH_`:
152`($data['_BASE_PATH_'] ?? '') . '/' . PluginManager::$PLUGINS_PATH . '/mything/picture.png`.
153
154Note that special placeholders for CSS and JS files (respectively `css_files` and `js_files`) are already prefixed
155with the base path in template files.
156
117### It's not working! 157### It's not working!
118 158
119Use `demo_plugin` as a functional example. It covers most of the plugin system features. 159Use `demo_plugin` as a functional example. It covers most of the plugin system features.
120 160
121If it's still not working, please [open an issue](https://github.com/shaarli/Shaarli/issues/new). 161If it's still not working, please [open an issue](https://github.com/shaarli/Shaarli/issues/new).
122 162
163
123### Hooks 164### Hooks
124 165
125| Hooks | Description | 166| Hooks | Description |
126| ------------- |:-------------:| 167| ------------- |:-------------:|
127| [render_header](#render_header) | Allow plugin to add content in page headers. | 168| [render_header](#render_header) | Allow plugin to add content in page headers. |
128| [render_includes](#render_includes) | Allow plugin to include their own CSS files. | 169| [render_includes](#render_includes) | Allow plugin to include their own CSS files. |
129| [render_footer](#render_footer) | Allow plugin to add content in page footer and include their own JS files. | 170| [render_footer](#render_footer) | Allow plugin to add content in page footer and include their own JS files. |
130| [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. | 171| [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. |
131| [render_editlink](#render_editlink) | Allow to add fields in the form, or display elements. | 172| [render_editlink](#render_editlink) | Allow to add fields in the form, or display elements. |
132| [render_tools](#render_tools) | Allow to add content at the end of the page. | 173| [render_tools](#render_tools) | Allow to add content at the end of the page. |
@@ -140,19 +181,16 @@ If it's still not working, please [open an issue](https://github.com/shaarli/Sha
140| [save_plugin_parameters](#save_plugin_parameters) | Allow to manipulate plugin parameters before they're saved. | 181| [save_plugin_parameters](#save_plugin_parameters) | Allow to manipulate plugin parameters before they're saved. |
141 182
142 183
143
144#### render_header 184#### render_header
145 185
146Triggered on every page. 186Triggered on every page - allows plugins to add content in page headers.
147 187
148Allow plugin to add content in page headers.
149 188
150##### Data 189##### Data
151 190
152`$data` is an array containing: 191`$data` is an array containing:
153 192
154- `_PAGE_`: current target page (eg: `linklist`, `picwall`, etc.). 193 - [Special data](#special-data)
155- `_LOGGEDIN_`: true if user is logged in, false otherwise.
156 194
157##### Template placeholders 195##### Template placeholders
158 196
@@ -170,18 +208,16 @@ List of placeholders:
170 208
171![fields_toolbar_example](http://i.imgur.com/3GMifI2.png) 209![fields_toolbar_example](http://i.imgur.com/3GMifI2.png)
172 210
173#### render_includes
174 211
175Triggered on every page. 212#### render_includes
176 213
177Allow plugin to include their own CSS files. 214Triggered on every page - allows plugins to include their own CSS files.
178 215
179##### Data 216##### data
180 217
181`$data` is an array containing: 218`$data` is an array containing:
182 219
183- `_PAGE_`: current target page (eg: `linklist`, `picwall`, etc.). 220 - [Special data](#special-data)
184- `_LOGGEDIN_`: true if user is logged in, false otherwise.
185 221
186##### Template placeholders 222##### Template placeholders
187 223
@@ -193,18 +229,18 @@ List of placeholders:
193 229
194> Note: only add the path of the CSS file. E.g: `plugins/demo_plugin/custom_demo.css`. 230> Note: only add the path of the CSS file. E.g: `plugins/demo_plugin/custom_demo.css`.
195 231
232
196#### render_footer 233#### render_footer
197 234
198Triggered on every page. 235Triggered on every page.
199 236
200Allow plugin to add content in page footer and include their own JS files. 237Allow plugin to add content in page footer and include their own JS files.
201 238
202##### Data 239##### data
203 240
204`$data` is an array containing: 241`$data` is an array containing:
205 242
206- `_PAGE_`: current target page (eg: `linklist`, `picwall`, etc.). 243 - [Special data](#special-data)
207- `_LOGGEDIN_`: true if user is logged in, false otherwise.
208 244
209##### Template placeholders 245##### Template placeholders
210 246
@@ -221,20 +257,21 @@ List of placeholders:
221 257
222> Note: only add the path of the JS file. E.g: `plugins/demo_plugin/custom_demo.js`. 258> Note: only add the path of the JS file. E.g: `plugins/demo_plugin/custom_demo.js`.
223 259
260
224#### render_linklist 261#### render_linklist
225 262
226Triggered when `linklist` is displayed (list of links, permalink, search, tag filtered, etc.). 263Triggered when `linklist` is displayed (list of links, permalink, search, tag filtered, etc.).
227 264
228It allows to add content at the begining and end of the page, after every link displayed and to alter link data. 265It allows to add content at the begining and end of the page, after every link displayed and to alter link data.
229 266
230##### Data 267##### data
231 268
232`$data` is an array containing: 269`$data` is an array containing:
233 270
234- `_LOGGEDIN_`: true if user is logged in, false otherwise. 271 - All templates data, including links.
235- All templates data, including links. 272 - [Special data](#special-data)
236 273
237##### Template placeholders 274##### template placeholders
238 275
239Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array. 276Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array.
240 277
@@ -256,19 +293,21 @@ List of placeholders:
256 293
257![plugin_end_zone_example](http://i.imgur.com/6IoRuop.png) 294![plugin_end_zone_example](http://i.imgur.com/6IoRuop.png)
258 295
296
259#### render_editlink 297#### render_editlink
260 298
261Triggered when the link edition form is displayed. 299Triggered when the link edition form is displayed.
262 300
263Allow to add fields in the form, or display elements. 301Allow to add fields in the form, or display elements.
264 302
265##### Data 303##### data
266 304
267`$data` is an array containing: 305`$data` is an array containing:
268 306
269- All templates data. 307 - All templates data.
308 - [Special data](#special-data)
270 309
271##### Template placeholders 310##### template placeholders
272 311
273Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array. 312Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array.
274 313
@@ -278,19 +317,21 @@ List of placeholders:
278 317
279![edit_link_plugin_example](http://i.imgur.com/5u17Ens.png) 318![edit_link_plugin_example](http://i.imgur.com/5u17Ens.png)
280 319
320
281#### render_tools 321#### render_tools
282 322
283Triggered when the "tools" page is displayed. 323Triggered when the "tools" page is displayed.
284 324
285Allow to add content at the end of the page. 325Allow to add content at the end of the page.
286 326
287##### Data 327##### data
288 328
289`$data` is an array containing: 329`$data` is an array containing:
290 330
291- All templates data. 331 - All templates data.
332 - [Special data](#special-data)
292 333
293##### Template placeholders 334##### template placeholders
294 335
295Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array. 336Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array.
296 337
@@ -300,20 +341,21 @@ List of placeholders:
300 341
301![tools_plugin_example](http://i.imgur.com/Bqhu9oQ.png) 342![tools_plugin_example](http://i.imgur.com/Bqhu9oQ.png)
302 343
344
303#### render_picwall 345#### render_picwall
304 346
305Triggered when picwall is displayed. 347Triggered when picwall is displayed.
306 348
307Allow to add content at the top and bottom of the page. 349Allow to add content at the top and bottom of the page.
308 350
309##### Data 351##### data
310 352
311`$data` is an array containing: 353`$data` is an array containing:
312 354
313- `_LOGGEDIN_`: true if user is logged in, false otherwise. 355 - All templates data.
314- All templates data. 356 - [Special data](#special-data)
315 357
316##### Template placeholders 358##### template placeholders
317 359
318Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array. 360Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array.
319 361
@@ -324,18 +366,19 @@ List of placeholders:
324 366
325![plugin_start_end_zone_example](http://i.imgur.com/tVTQFER.png) 367![plugin_start_end_zone_example](http://i.imgur.com/tVTQFER.png)
326 368
369
327#### render_tagcloud 370#### render_tagcloud
328 371
329Triggered when tagcloud is displayed. 372Triggered when tagcloud is displayed.
330 373
331Allow to add content at the top and bottom of the page. 374Allow to add content at the top and bottom of the page.
332 375
333##### Data 376##### data
334 377
335`$data` is an array containing: 378`$data` is an array containing:
336 379
337- `_LOGGEDIN_`: true if user is logged in, false otherwise. 380 - All templates data.
338- All templates data. 381 - [Special data](#special-data)
339 382
340##### Template placeholders 383##### Template placeholders
341 384
@@ -355,16 +398,14 @@ For each tag, the following placeholder can be used:
355 398
356#### render_taglist 399#### render_taglist
357 400
358Triggered when taglist is displayed. 401Triggered when taglist is displayed - allows to add content at the top and bottom of the page.
359
360Allow to add content at the top and bottom of the page.
361 402
362##### Data 403##### data
363 404
364`$data` is an array containing: 405`$data` is an array containing:
365 406
366- `_LOGGEDIN_`: true if user is logged in, false otherwise. 407 - All templates data.
367- All templates data. 408 - [Special data](#special-data)
368 409
369##### Template placeholders 410##### Template placeholders
370 411
@@ -385,12 +426,13 @@ Triggered when tagcloud is displayed.
385 426
386Allow to add content at the top and bottom of the page, the bottom of each link and to alter data. 427Allow to add content at the top and bottom of the page, the bottom of each link and to alter data.
387 428
388##### Data 429
430##### data
389 431
390`$data` is an array containing: 432`$data` is an array containing:
391 433
392- `_LOGGEDIN_`: true if user is logged in, false otherwise. 434 - All templates data, including links.
393- All templates data, including links. 435 - [Special data](#special-data)
394 436
395##### Template placeholders 437##### Template placeholders
396 438
@@ -405,19 +447,19 @@ List of placeholders:
405- `plugin_start_zone`: before displaying the template content. 447- `plugin_start_zone`: before displaying the template content.
406- `plugin_end_zone`: after displaying the template content. 448- `plugin_end_zone`: after displaying the template content.
407 449
450
408#### render_feed 451#### render_feed
409 452
410Triggered when the ATOM or RSS feed is displayed. 453Triggered when the ATOM or RSS feed is displayed.
411 454
412Allow to add tags in the feed, either in the header or for each items. Items (links) can also be altered before being rendered. 455Allow to add tags in the feed, either in the header or for each items. Items (links) can also be altered before being rendered.
413 456
414##### Data 457##### data
415 458
416`$data` is an array containing: 459`$data` is an array containing:
417 460
418- `_LOGGEDIN_`: true if user is logged in, false otherwise. 461 - All templates data, including links.
419- `_PAGE_`: containing either `rss` or `atom`. 462 - [Special data](#special-data)
420- All templates data, including links.
421 463
422##### Template placeholders 464##### Template placeholders
423 465
@@ -431,13 +473,14 @@ For each links:
431 473
432- `feed_plugins`: additional tag for every link entry. 474- `feed_plugins`: additional tag for every link entry.
433 475
476
434#### save_link 477#### save_link
435 478
436Triggered when a link is save (new link or edit). 479Triggered when a link is save (new link or edit).
437 480
438Allow to alter the link being saved in the datastore. 481Allow to alter the link being saved in the datastore.
439 482
440##### Data 483##### data
441 484
442`$data` is an array containing the link being saved: 485`$data` is an array containing the link being saved:
443 486
@@ -451,6 +494,8 @@ Allow to alter the link being saved in the datastore.
451- created 494- created
452- updated 495- updated
453 496
497Also [special data](#special-data).
498
454 499
455#### delete_link 500#### delete_link
456 501
@@ -458,9 +503,9 @@ Triggered when a link is deleted.
458 503
459Allow to execute any action before the link is actually removed from the datastore 504Allow to execute any action before the link is actually removed from the datastore
460 505
461##### Data 506##### data
462 507
463`$data` is an array containing the link being saved: 508`$data` is an array containing the link being deleted:
464 509
465- id 510- id
466- title 511- title
@@ -472,6 +517,7 @@ Allow to execute any action before the link is actually removed from the datasto
472- created 517- created
473- updated 518- updated
474 519
520Also [special data](#special-data).
475 521
476#### save_plugin_parameters 522#### save_plugin_parameters
477 523
@@ -480,15 +526,16 @@ Triggered when the plugin parameters are saved from the plugin administration pa
480Plugins can perform an action every times their settings are updated. 526Plugins can perform an action every times their settings are updated.
481For example it is used to update the CSS file of the `default_colors` plugins. 527For example it is used to update the CSS file of the `default_colors` plugins.
482 528
483##### Data 529##### data
484 530
485`$data` input contains the `$_POST` array. 531`$data` input contains the `$_POST` array.
486 532
487So if the plugin has a parameter called `MYPLUGIN_PARAMETER`, 533So if the plugin has a parameter called `MYPLUGIN_PARAMETER`,
488the array will contain an entry with `MYPLUGIN_PARAMETER` as a key. 534the array will contain an entry with `MYPLUGIN_PARAMETER` as a key.
489 535
536Also [special data](#special-data).
490 537
491## Guide for template designer 538## Guide for template designers
492 539
493### Plugin administration 540### Plugin administration
494 541
@@ -510,7 +557,7 @@ Otherwise, you can use your own JS as long as this field is send by the form:
510 557
511### Placeholder system 558### Placeholder system
512 559
513In order to make plugins work with every custom themes, you need to add variable placeholder in your templates. 560In order to make plugins work with every custom themes, you need to add variable placeholder in your templates.
514 561
515It's a RainTPL loop like this: 562It's a RainTPL loop like this:
516 563
@@ -532,7 +579,7 @@ At the end of the menu:
532 579
533At the end of file, before clearing floating blocks: 580At the end of file, before clearing floating blocks:
534 581
535 {if="!empty($plugin_errors) && isLoggedIn()"} 582 {if="!empty($plugin_errors) && $is_logged_in"}
536 <ul class="errors"> 583 <ul class="errors">
537 {loop="plugin_errors"} 584 {loop="plugin_errors"}
538 <li>{$value}</li> 585 <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 bd400776..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:
@@ -16,8 +18,6 @@ This file allows overriding rules defined in the template CSS files (only add ch
16 18
17**Note**: Do not edit `tpl/default/css/shaarli.css`! Your changes would be overridden when updating Shaarli. 19**Note**: Do not edit `tpl/default/css/shaarli.css`! Your changes would be overridden when updating Shaarli.
18 20
19See also [Download CSS styles from an OPML list](Download CSS styles from an OPML list)
20
21## Themes 21## Themes
22 22
23Installation: 23Installation:
@@ -45,6 +45,7 @@ Installation:
45- [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
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 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
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 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
48 49
49### Shaarli forks 50### Shaarli forks
50 51
diff --git a/doc/md/Translations.md b/doc/md/dev/Translations.md
index c7d33855..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>/
25http://<replace_domain>/login
26http://<replace_domain>/daily
27http://<replace_domain>/tags/cloud
28http://<replace_domain>/tags/list
29http://<replace_domain>/picture-wall
34http://<replace_domain>/?nonope 30http://<replace_domain>/?nonope
35http://<replace_domain>/?do=addlink 31http://<replace_domain>/admin/add-shaare
36http://<replace_domain>/?do=changepasswd 32http://<replace_domain>/admin/password
37http://<replace_domain>/?do=changetag 33http://<replace_domain>/admin/tags
38http://<replace_domain>/?do=configure 34http://<replace_domain>/admin/configure
39http://<replace_domain>/?do=tools 35http://<replace_domain>/admin/tools
40http://<replace_domain>/?do=daily 36http://<replace_domain>/admin/shaare
41http://<replace_domain>/?post 37http://<replace_domain>/admin/export
42http://<replace_domain>/?do=export 38http://<replace_domain>/admin/import
43http://<replace_domain>/?do=import 39http://<replace_domain>/admin/plugins
44http://<replace_domain>/?do=login
45http://<replace_domain>/?do=picwall
46http://<replace_domain>/?do=pluginadmin
47http://<replace_domain>/?do=tagcloud
48http://<replace_domain>/?do=taglist
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
79### Theme translations
80 67
81Theme translation extensions are loaded automatically if they're present. 68### Theme translations
69
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```
78
79Where `<lang>` is the ISO 3166-1 alpha-2 language code.
87 80
88Where `<lang>` is the ISO 3166-1 alpha-2 language code.
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,
@@ -106,7 +99,7 @@ First, create your translation files tree directory:
106Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be 99Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
107`my_theme.po`. 100`my_theme.po`.
108 101
109Users have to register your extension in their configuration with the parameter 102Users have to register your extension in their configuration with the parameter
110`translation.extensions.<domain>: <translation files path>`. 103`translation.extensions.<domain>: <translation files path>`.
111 104
112Example: 105Example:
@@ -151,11 +144,11 @@ When you're done, open Poedit and load translation strings from sources:
151 1. `File > New` 144 1. `File > New`
152 2. Choose your language 145 2. Choose your language
153 3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`. 146 3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
154 4. Go to `Catalog > Properties...` 147 4. Go to `Catalog > Properties...`
155 5. Fill the `Translation Properties` tab 148 5. Fill the `Translation Properties` tab
156 6. Add your source path in the `Sources Paths` tab 149 6. Add your source path in the `Sources Paths` tab
157 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines: 150 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
158 151
159``` 152```
160my_theme_t 153my_theme_t
161my_theme_t:1,2 154my_theme_t:1,2
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 5948949a..00000000
--- a/doc/md/docker/shaarli-images.md
+++ /dev/null
@@ -1,124 +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` and `master` images rely on:
16
17- [Alpine Linux](https://www.alpinelinux.org/)
18- [PHP7-FPM](http://php-fpm.org/)
19- [Nginx](http://nginx.org/)
20
21The `stable` image relies on:
22
23- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
24- [PHP5-FPM](http://php-fpm.org/)
25- [Nginx](http://nginx.org/)
26
27Additional Dockerfiles are provided for the `arm32v7` platform, relying on
28[Linuxserver.io Alpine armhf
29images](https://hub.docker.com/r/lsiobase/alpine.armhf/). These images must be
30built using [`docker
31build`](https://docs.docker.com/engine/reference/commandline/build/) on an
32`arm32v7` machine or using an emulator such as
33[qemu](https://resin.io/blog/building-arm-containers-on-any-x86-machine-even-dockerhub/).
34
35### Download from Docker Hub
36```shell
37$ docker pull shaarli/shaarli
38
39latest: Pulling from shaarli/shaarli
4032716d9fcddb: Pull complete
4184899d045435: Pull complete
424b6ad7444763: Pull complete
43e0345ef7a3e0: Pull complete
445c1dd344094f: Pull complete
456422305a200b: Pull complete
467d63f861dbef: Pull complete
473eb97210645c: Pull complete
48869319d746ff: Already exists
49869319d746ff: Pulling fs layer
50902b87aaaec9: Already exists
51Digest: sha256:f836b4627b958b3f83f59c332f22f02fcd495ace3056f2be2c4912bd8704cc98
52Status: Downloaded newer image for shaarli/shaarli:latest
53```
54
55### Create and start a new container from the image
56```shell
57# map the host's :8000 port to the container's :80 port
58$ docker create -p 8000:80 shaarli/shaarli
59d40b7af693d678958adedfb88f87d6ea0237186c23de5c4102a55a8fcb499101
60
61# launch the container in the background
62$ docker start d40b7af693d678958adedfb88f87d6ea0237186c23de5c4102a55a8fcb499101
63d40b7af693d678958adedfb88f87d6ea0237186c23de5c4102a55a8fcb499101
64
65# list active containers
66$ docker ps
67CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
68d40b7af693d6 shaarli/shaarli /usr/bin/supervisor 15 seconds ago Up 4 seconds 0.0.0.0:8000->80/tcp backstabbing_galileo
69```
70
71### Stop and destroy a container
72```shell
73$ docker stop backstabbing_galileo # those docker guys are really rude to physicists!
74backstabbing_galileo
75
76# check the container is stopped
77$ docker ps
78CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
79
80# list ALL containers
81$ docker ps -a
82CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
83d40b7af693d6 shaarli/shaarli /usr/bin/supervisor 5 minutes ago Exited (0) 48 seconds ago backstabbing_galileo
84
85# destroy the container
86$ docker rm backstabbing_galileo # let's put an end to these barbarian practices
87backstabbing_galileo
88
89$ docker ps -a
90CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
91```
92
93### Automatic builds
94Docker users can start a personal instance from an
95[autobuild image](https://hub.docker.com/r/shaarli/shaarli/).
96For example to start a temporary Shaarli at ``localhost:8000``, and keep session
97data (config, storage):
98
99```shell
100MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
101docker run -ti --rm \
102 -p 8000:80 \
103 -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
104 shaarli/shaarli
105```
106
107### Volumes and data persistence
108Data can be persisted by [using volumes](https://docs.docker.com/storage/volumes/).
109Volumes allow to keep your data when renewing and/or updating container images:
110
111```shell
112# Create data volumes
113$ docker volume create shaarli-data
114$ docker volume create shaarli-cache
115
116# Create and start a Shaarli container using these volumes to persist data
117$ docker create \
118 --name shaarli \
119 -v shaarli-cache:/var/www/shaarli/cache \
120 -v shaarli-data:/var/www/shaarli/data \
121 -p 8000:80 \
122 shaarli/shaarli:master
123$ docker start shaarli
124```
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 0074ae9f..00000000
--- a/doc/md/guides/various-hacks.md
+++ /dev/null
@@ -1,33 +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### Changing the timestamp for a shaare
21
22- Look for `<input type="hidden" name="lf_linkdate" value="{$link.linkdate}">` in `tpl/editlink.tpl` (line 14)
23- Replace `type="hidden"` with `type="text"` from this line
24- A new date/time field becomes available in the edit/new link dialog.
25- You can set the timestamp manually by entering it in the format `YYYMMDD_HHMMS`.
26
27
28### See also
29
30- [Add a new custom field to shaares (example patch)](https://gist.github.com/nodiscc/8b0194921f059d7b9ad89a581ecd482c)
31- [Download CSS styles for shaarlis listed in an opml file](https://gist.github.com/nodiscc/dede231c92cab22c3ad2cc24d5035012)
32- [Copy an existing Shaarli installation over SSH, and serve it locally](https://gist.github.com/nodiscc/ed161c66e5b028b5299b0a3733d01c77)
33- [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 220eeec1..2c4995f8 100644
--- a/doc/md/index.md
+++ b/doc/md/index.md
@@ -2,113 +2,101 @@
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
20### Demo 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
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).
23It runs the latest development version of Shaarli and is updated/reset daily. 21It runs the latest development version of Shaarli and is updated/reset daily.
24 22
25Login: `demo`; Password: `demo` 23Login: `demo`; Password: `demo`
26 24
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
27## Features 33## Features
28 34
29Shaarli can be used: 35Shaarli can be used:
30 36
31- to share, comment and save interesting links and news 37- to share, comment and save interesting links
32- to bookmark useful/frequent links and share them between computers 38- to bookmark useful/frequent links and share them between computers
33- as a minimal blog/microblog/writing platform 39- as a minimal blog/microblog/writing platform
34- as a read-it-later list 40- as a read-it-later/todo list
35- to draft and save articles/posts/ideas 41- as a notepad to draft and save articles/posts/ideas
36- to keep notes, documentation and code snippets 42- as a knowledge base to keep notes, documentation and code snippets
37- as a shared clipboard/notepad/pastebin between machines 43- as a shared clipboard/notepad/pastebin between computers
38- as a todo list 44- as playlist manager for online media
39- to store media playlists 45- to feed other blogs, aggregators, social networks...
40- to keep extracts/comments from webpages that may disappear.
41- to keep track of ongoing discussions
42- to feed other blogs, aggregators, social networks... using RSS feeds
43 46
44### Edit, view and search your links 47### Edit, view and search your links
45 48
46- Minimalist design 49- Editable URL, title, description, tags, private/public status for all your [Shaares](Usage.md)
47- FAST 50- [Tags](Usage.md#tags) to organize your Shaares
48- Customizable link titles and descriptions 51- [Search](Usage.md#search) in all fields
49- Tags to organize your links (features tag autocompletion, renaming, merging and deletion) 52- Unique [permalinks](Usage.md#permalinks) for easy reference
50- Search by tag or using the full-text search 53- Paginated Shaares list view (with image and video thumbnails)
51- Public and private links (visible only to logged-in users) 54- [Tag cloud/list](Usage#tag-cloud) views
52- Unique permalinks for easy reference 55- [Picture wall](Usage#picture-wall)/thumbnails view (with lazy loading)
53- 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)
54- Tag cloud and list views 57- [Daily](Usage.md#daily): newspaper-like daily digest (and daily RSS feed)
55- Picture wall: image and video thumbnails view (with lazy loading) 58- URL cleanup: automatic removal of `?utm_source=...`, `fb=...` tracking parameters
56- ATOM and RSS feeds (can also be filtered using tags or text search) 59- Extensible through [plugins](Plugins.md)
57- 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
58- URL cleanup: automatic removal of `?utm_source=...`, `fb=...` 61- Bookmarklet and [other tools](Community-and-related-software.md) to share links in one click
59- Extensible through [plugins](https://shaarli.readthedocs.io/en/master/Plugins/#plugin-usage) 62- Responsive/support for mobile browsers, degrades gracefully with Javascript disabled
63
60 64
61### Easy setup 65### Easy setup
62 66
63- Dead-simple installation: drop the files, open the page 67- Dead-simple [installation](Installation.md): drop the files on your server, open the page
64- Links are stored in a file (no database required, easy backup: simply copy the datastore file) 68- Shaares are stored in a file (no database required, easy [backup](Backup-and-restore.md))
65- Import and export links as Netscape bookmarks compatible with most Web browsers 69- [Configurable](Shaarli-configuration.md) from dialog and configuration file
70- Extensible through third-party [plugins and themes](Community-and-related-software.md)
66 71
67### Accessibility
68 72
69- Bookmarklet and other tools to share links in one click 73### Fast
70- Support for mobile browsers
71- Degrades gracefully with Javascript disabled
72- Easy page customization through HTML/CSS/RainTPL
73 74
74### Security 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!
75 77
76- Discreet pop-up notification when a new release is available
77- Bruteforce protection on the login form
78- Protected against [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) and session cookie hijacking
79 78
80<!-- TODO Limitations --> 79### Self-hosted
81 80
82### REST API 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
83 86
84- Easily extensible by any client using the REST API exposed by Shaarli ([API documentation](http://shaarli.github.io/api-documentation/)).
85 87
86## About 88## About
87 89
88### 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.
89
90This friendly fork is maintained by the Shaarli community at <https://github.com/shaarli/Shaarli>
91
92This is a community fork of the original [Shaarli](https://github.com/sebsauvage/Shaarli/) project by [Sébastien Sauvage](http://sebsauvage.net/).
93
94The 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.
95 91
96The Shaarli community has carried on the work to provide [many 92The original Shaarli instance is still available [here](https://sebsauvage.net/links/) (+25000 shaares!)
97patches](https://github.com/shaarli/Shaarli/compare/sebsauvage:master...master) for
98[bug fixes and enhancements](https://github.com/shaarli/Shaarli/issues?q=is%3Aclosed+)
99in this repository, and will keep maintaining the project for the foreseeable
100future, while keeping Shaarli simple and efficient.
101 93
102 94
103### Contributing and getting help 95### Contributing and getting help
104 96
105Feedback 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 :-)
106 98
107- 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.
108- Have a look at the open [issues](https://github.com/shaarli/Shaarli/issues) and [pull requests](https://github.com/shaarli/Shaarli/pulls)
109- 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).
110- If you've found a bug, please create a [new issue](https://github.com/shaarli/Shaarli/issues/new).
111- Feel free to propose solutions to existing problems, help us improve the documentation and translations, and submit pull requests :-)
112 100
113 101
114### 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..9a6e3958 100644
--- a/inc/languages/fr/LC_MESSAGES/shaarli.po
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -1,24 +1,26 @@
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-09-10 16:06+0200\n"
5"PO-Revision-Date: 2019-07-13 10:49+0200\n" 5"PO-Revision-Date: 2020-09-10 16:07+0200\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/ApplicationUtils.php:161
22#, php-format 24#, php-format
23msgid "" 25msgid ""
24"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " 26"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
@@ -29,27 +31,27 @@ msgstr ""
29"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " 31"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." 32"connues et devrait être mise à jour au plus tôt."
31 33
32#: application/ApplicationUtils.php:189 application/ApplicationUtils.php:201 34#: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204
33msgid "directory is not readable" 35msgid "directory is not readable"
34msgstr "le répertoire n'est pas accessible en lecture" 36msgstr "le répertoire n'est pas accessible en lecture"
35 37
36#: application/ApplicationUtils.php:204 38#: application/ApplicationUtils.php:207
37msgid "directory is not writable" 39msgid "directory is not writable"
38msgstr "le répertoire n'est pas accessible en écriture" 40msgstr "le répertoire n'est pas accessible en écriture"
39 41
40#: application/ApplicationUtils.php:222 42#: application/ApplicationUtils.php:225
41msgid "file is not readable" 43msgid "file is not readable"
42msgstr "le fichier n'est pas accessible en lecture" 44msgstr "le fichier n'est pas accessible en lecture"
43 45
44#: application/ApplicationUtils.php:225 46#: application/ApplicationUtils.php:228
45msgid "file is not writable" 47msgid "file is not writable"
46msgstr "le fichier n'est pas accessible en écriture" 48msgstr "le fichier n'est pas accessible en écriture"
47 49
48#: application/History.php:178 50#: application/History.php:179
49msgid "History file isn't readable or writable" 51msgid "History file isn't readable or writable"
50msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" 52msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
51 53
52#: application/History.php:189 54#: application/History.php:190
53msgid "Could not parse history file" 55msgid "Could not parse history file"
54msgstr "Format incorrect pour le fichier d'historique" 56msgstr "Format incorrect pour le fichier d'historique"
55 57
@@ -58,16 +60,20 @@ msgid "Automatic"
58msgstr "Automatique" 60msgstr "Automatique"
59 61
60#: application/Languages.php:182 62#: application/Languages.php:182
63msgid "German"
64msgstr "Allemand"
65
66#: application/Languages.php:183
61msgid "English" 67msgid "English"
62msgstr "Anglais" 68msgstr "Anglais"
63 69
64#: application/Languages.php:183 70#: application/Languages.php:184
65msgid "French" 71msgid "French"
66msgstr "Français" 72msgstr "Français"
67 73
68#: application/Languages.php:184 74#: application/Languages.php:185
69msgid "German" 75msgid "Japanese"
70msgstr "Allemand" 76msgstr "Japonais"
71 77
72#: application/Thumbnailer.php:62 78#: application/Thumbnailer.php:62
73msgid "" 79msgid ""
@@ -77,50 +83,144 @@ msgstr ""
77"l'extension php-gd doit être chargée pour utiliser les miniatures. Les " 83"l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
78"miniatures sont désormais désactivées. Rechargez la page." 84"miniatures sont désormais désactivées. Rechargez la page."
79 85
80#: application/Utils.php:379 tests/UtilsTest.php:343 86#: application/Utils.php:383
81msgid "Setting not set" 87msgid "Setting not set"
82msgstr "Paramètre non défini" 88msgstr "Paramètre non défini"
83 89
84#: application/Utils.php:386 tests/UtilsTest.php:341 tests/UtilsTest.php:342 90#: application/Utils.php:390
85msgid "Unlimited" 91msgid "Unlimited"
86msgstr "Illimité" 92msgstr "Illimité"
87 93
88#: application/Utils.php:389 tests/UtilsTest.php:338 tests/UtilsTest.php:339 94#: application/Utils.php:393
89#: tests/UtilsTest.php:353
90msgid "B" 95msgid "B"
91msgstr "o" 96msgstr "o"
92 97
93#: application/Utils.php:389 tests/UtilsTest.php:332 tests/UtilsTest.php:333 98#: application/Utils.php:393
94#: tests/UtilsTest.php:340
95msgid "kiB" 99msgid "kiB"
96msgstr "ko" 100msgstr "ko"
97 101
98#: application/Utils.php:389 tests/UtilsTest.php:334 tests/UtilsTest.php:335 102#: application/Utils.php:393
99#: tests/UtilsTest.php:351 tests/UtilsTest.php:352
100msgid "MiB" 103msgid "MiB"
101msgstr "Mo" 104msgstr "Mo"
102 105
103#: application/Utils.php:389 tests/UtilsTest.php:336 tests/UtilsTest.php:337 106#: application/Utils.php:393
104msgid "GiB" 107msgid "GiB"
105msgstr "Go" 108msgstr "Go"
106 109
107#: application/bookmark/LinkDB.php:128 110#: application/bookmark/BookmarkFileService.php:174
108msgid "You are not authorized to add a link." 111#: application/bookmark/BookmarkFileService.php:199
109msgstr "Vous n'êtes pas autorisé à ajouter un lien." 112#: application/bookmark/BookmarkFileService.php:224
113#: application/bookmark/BookmarkFileService.php:241
114msgid "You're not authorized to alter the datastore"
115msgstr "Vous n'êtes pas autorisé à modifier les données"
110 116
111#: application/bookmark/LinkDB.php:131 117#: application/bookmark/BookmarkFileService.php:177
112msgid "Internal Error: A link should always have an id and URL." 118#: application/bookmark/BookmarkFileService.php:202
113msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL." 119#: application/bookmark/BookmarkFileService.php:244
120msgid "Provided data is invalid"
121msgstr "Les informations fournies ne sont pas valides"
114 122
115#: application/bookmark/LinkDB.php:134 123#: application/bookmark/BookmarkFileService.php:205
116msgid "You must specify an integer as a key." 124msgid "This bookmarks already exists"
117msgstr "Vous devez utiliser un entier comme clé." 125msgstr "Ce marque-page existe déjà."
118 126
119#: application/bookmark/LinkDB.php:137 127#: application/bookmark/BookmarkInitializer.php:37
120msgid "Array offset and link ID must be equal." 128msgid "(private bookmark with thumbnail demo)"
121msgstr "La clé du tableau et l'ID du lien doivent être identiques." 129msgstr "(marque page privé avec une miniature)"
130
131#: application/bookmark/BookmarkInitializer.php:40
132msgid ""
133"Shaarli will automatically pick up the thumbnail for links to a variety of "
134"websites.\n"
135"\n"
136"Explore your new Shaarli instance by trying out controls and menus.\n"
137"Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the "
138"documentation](https://shaarli.readthedocs.io/en/master/) to learn more "
139"about Shaarli.\n"
140"\n"
141"Now you can edit or delete the default shaares.\n"
142msgstr ""
143"Shaarli récupérera automatiquement la miniature associée au liens pour de "
144"nombreux sites web.\n"
145"\n"
146"Explorez votre nouvelle instance de Shaarli en essayant les différents "
147"contrôles et menus.\n"
148"Visitez le projet sur [Github](https://github.com/shaarli/Shaarli) ou [la "
149"documentation](https://shaarli.readthedocs.io/en/master/) pour en apprendre "
150"plus sur Shaarli.\n"
151"\n"
152"Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n"
122 153
123#: application/bookmark/LinkDB.php:243 154#: application/bookmark/BookmarkInitializer.php:53
155msgid "Note: Shaare descriptions"
156msgstr "Note : Description des Shaares"
157
158#: application/bookmark/BookmarkInitializer.php:55
159msgid ""
160"Adding a shaare without entering a URL creates a text-only \"note\" post "
161"such as this one.\n"
162"This note is private, so you are the only one able to see it while logged "
163"in.\n"
164"\n"
165"You can use this to keep notes, post articles, code snippets, and much "
166"more.\n"
167"\n"
168"The Markdown formatting setting allows you to format your notes and bookmark "
169"description:\n"
170"\n"
171"### Title headings\n"
172"\n"
173"#### Multiple headings levels\n"
174" * bullet lists\n"
175" * _italic_ text\n"
176" * **bold** text\n"
177" * ~~strike through~~ text\n"
178" * `code` blocks\n"
179" * images\n"
180" * [links](https://en.wikipedia.org/wiki/Markdown)\n"
181"\n"
182"Markdown also supports tables:\n"
183"\n"
184"| Name | Type | Color | Qty |\n"
185"| ------- | --------- | ------ | ----- |\n"
186"| Orange | Fruit | Orange | 126 |\n"
187"| Apple | Fruit | Any | 62 |\n"
188"| Lemon | Fruit | Yellow | 30 |\n"
189"| Carrot | Vegetable | Red | 14 |\n"
190msgstr ""
191"Ajouter un shaare sans préciser d'URL créé une « note » textuelle, telle que "
192"celle-ci.\n"
193"Cette note est privée, donc vous êtes seul à pouvoir la voir lorsque vous "
194"êtes connecté.\n"
195"\n"
196"Vous pouvez utiliser cette fonctionnalité pour prendre des notes, publier "
197"des articles, des extraits de code, et bien plus.\n"
198"\n"
199"L'option du formatage par Markdown vous permet de formater vos description "
200"de notes et marque-pages :\n"
201"\n"
202"### Titre d'en-tête\n"
203"\n"
204"#### Sur plusieurs niveaux\n"
205" * liste à puce\n"
206" * texte en _italique_\n"
207" * texte en **gras**\n"
208" * texte ~~barré~~\n"
209" * blocs de `code`\n"
210" * images\n"
211" * [liens](https://en.wikipedia.org/wiki/Markdown)\n"
212"\n"
213"Markdown supporte aussi les tableaux :\n"
214"\n"
215"| Nom | Type | Couleur | Qte |\n"
216"| ------- | --------- | ------ | ----- |\n"
217"| Orange | Fruit | Orange | 126 |\n"
218"| Pomme | Fruit | Multiple | 62 |\n"
219"| Citron | Fruit | Jaune | 30 |\n"
220"| Carotte | Légume | Orange | 14 |\n"
221
222#: application/bookmark/BookmarkInitializer.php:89
223#: application/legacy/LegacyLinkDB.php:246
124#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 224#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
125#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 225#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
126#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 226#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
@@ -131,37 +231,56 @@ msgstr ""
131"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " 231"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
132"données" 232"données"
133 233
134#: application/bookmark/LinkDB.php:246 234#: application/bookmark/BookmarkInitializer.php:92
135msgid "" 235msgid ""
136"Welcome to Shaarli! This is your first public bookmark. To edit or delete " 236"Welcome to Shaarli!\n"
137"me, you must first login.\n"
138"\n" 237"\n"
139"To learn how to use Shaarli, consult the link \"Documentation\" at the " 238"Shaarli allows you to bookmark your favorite pages, and share them with "
140"bottom of this page.\n" 239"others or store them privately.\n"
240"You can add a description to your bookmarks, such as this one, and tag "
241"them.\n"
141"\n" 242"\n"
142"You use the community supported version of the original Shaarli project, by " 243"Create a new shaare by clicking the `+Shaare` button, or using any of the "
143"Sebastien Sauvage." 244"recommended tools (browser extension, mobile app, bookmarklet, REST API, "
144msgstr "" 245"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" 246"\n"
148"Pour apprendre à utiliser Shaarli, consultez le lien « Documentation » en " 247"You can easily retrieve your links, even with thousands of them, using the "
149"bas de page.\n" 248"internal search engine, or search through tags (e.g. this Shaare is tagged "
249"with `shaarli` and `help`).\n"
250"Hashtags such as #shaarli #help are also supported.\n"
251"You can also filter the available [RSS feed](/feed/atom) and picture wall by "
252"tag or plaintext search.\n"
150"\n" 253"\n"
151"Vous utilisez la version supportée par la communauté du projet original " 254"We hope that you will enjoy using Shaarli, maintained with â¤ï¸ by the "
152"Shaarli de Sébastien Sauvage." 255"community!\n"
153 256"Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if "
154#: application/bookmark/LinkDB.php:263 257"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 "" 258msgstr ""
161"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me " 259"Bienvenue sur Shaarli !\n"
162"supprimer aussi." 260"\n"
261"Shaarli vous permet de sauvegarder des marque-pages de vos pages favorites, "
262"et de les partager avec d'autres, ou de les enregistrer en privé.\n"
263"Vous pouvez ajouter une description à vos marque-pages, comme celle-ci, et y "
264"ajouter des tags.\n"
265"\n"
266"Créez un nouveau shaare en cliquant sur le bouton `+Shaare`, ou en utilisant "
267"l'un des outils recommandés (extension de navigateur, application mobile, "
268"bookmarklet, REST API, etc.).\n"
269"\n"
270"Vous pouvez facilement retrouver vos liens, même parmi des milliers, en "
271"utilisant le moteur de recherche interne, ou en filtrant par tags (par "
272"exemple ce Shaare est taggé avec `shaarli` et `help`).\n"
273"Les hashtags comme #shaarli #help sont aussi supportés.\n"
274"Vous pouvez aussi filtrer les [flux RSS](/feed/atom) et [mur d'images]() par "
275"tag ou par texte brut.\n"
276"\n"
277"Nous espérons que vous apprécierez utiliser Shaarli, maintenu avec â¤ï¸ par la "
278"communauté !\n"
279"N'hésitez pas à ouvrir [un ticket (en)](https://github.com/shaarli/Shaarli/"
280"issues) si vous avez une suggestion ou si vous rencontrez un problème.\n"
281" \n"
163 282
164#: application/bookmark/exception/LinkNotFoundException.php:13 283#: application/bookmark/exception/BookmarkNotFoundException.php:13
165msgid "The link you are trying to reach does not exist or has been deleted." 284msgid "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é." 285msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé."
167 286
@@ -173,8 +292,8 @@ msgstr ""
173"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que " 292"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é." 293"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
175 294
176#: application/config/ConfigManager.php:135 295#: application/config/ConfigManager.php:136
177#: application/config/ConfigManager.php:162 296#: application/config/ConfigManager.php:163
178msgid "Invalid setting key parameter. String expected, got: " 297msgid "Invalid setting key parameter. String expected, got: "
179msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " 298msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
180 299
@@ -196,268 +315,376 @@ msgstr "Vous n'êtes pas autorisé à modifier la configuration."
196msgid "Error accessing" 315msgid "Error accessing"
197msgstr "Une erreur s'est produite en accédant à" 316msgstr "Une erreur s'est produite en accédant à"
198 317
199#: application/feed/Cache.php:16 318#: 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" 319msgid "Direct link"
206msgstr "Liens directs" 320msgstr "Liens directs"
207 321
208#: application/feed/FeedBuilder.php:157 322#: application/feed/FeedBuilder.php:181
209#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 323#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
210#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177 324#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
211msgid "Permalink" 325msgid "Permalink"
212msgstr "Permalien" 326msgstr "Permalien"
213 327
214#: application/netscape/NetscapeBookmarkUtils.php:42 328#: application/front/controller/admin/ConfigureController.php:54
215msgid "Invalid export selection:" 329#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
216msgstr "Sélection d'export invalide :" 330msgid "Configure"
331msgstr "Configurer"
217 332
218#: application/netscape/NetscapeBookmarkUtils.php:87 333#: application/front/controller/admin/ConfigureController.php:102
219#, php-format 334#: application/legacy/LegacyUpdater.php:537
220msgid "File %s (%d bytes) " 335msgid "You have enabled or changed thumbnails mode."
221msgstr "Le fichier %s (%d octets) " 336msgstr "Vous avez activé ou changé le mode de miniatures."
222 337
223#: application/netscape/NetscapeBookmarkUtils.php:89 338#: application/front/controller/admin/ConfigureController.php:103
224msgid "has an unknown file format. Nothing was imported." 339#: application/legacy/LegacyUpdater.php:538
225msgstr "a un format inconnu. Rien n'a été importé." 340msgid "Please synchronize them."
341msgstr "Merci de les synchroniser."
342
343#: application/front/controller/admin/ConfigureController.php:113
344#: application/front/controller/visitor/InstallController.php:136
345msgid "Error while writing config file after configuration update."
346msgstr ""
347"Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
226 348
227#: application/netscape/NetscapeBookmarkUtils.php:93 349#: application/front/controller/admin/ConfigureController.php:122
350msgid "Configuration was saved."
351msgstr "La configuration a été sauvegardée."
352
353#: application/front/controller/admin/ExportController.php:26
354#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
355msgid "Export"
356msgstr "Exporter"
357
358#: application/front/controller/admin/ExportController.php:42
359msgid "Please select an export mode."
360msgstr "Merci de choisir un mode d'export."
361
362#: application/front/controller/admin/ImportController.php:41
363#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
364msgid "Import"
365msgstr "Importer"
366
367#: application/front/controller/admin/ImportController.php:55
368msgid "No import file provided."
369msgstr "Aucun fichier à importer n'a été fourni."
370
371#: application/front/controller/admin/ImportController.php:66
228#, php-format 372#, php-format
229msgid "" 373msgid ""
230"was successfully processed in %d seconds: %d links imported, %d links " 374"The file you are trying to upload is probably bigger than what this "
231"overwritten, %d links skipped." 375"webserver can accept (%s). Please upload in smaller chunks."
232msgstr "" 376msgstr ""
233"a été importé avec succès en %d secondes : %d liens importés, %d liens " 377"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
234"écrasés, %d liens ignorés." 378"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
379"légères."
235 380
236#: application/plugin/exception/PluginFileNotFoundException.php:21 381#: application/front/controller/admin/ManageShaareController.php:29
382#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
383msgid "Shaare a new link"
384msgstr "Partager un nouveau lien"
385
386#: application/front/controller/admin/ManageShaareController.php:78
387msgid "Note: "
388msgstr "Note : "
389
390#: application/front/controller/admin/ManageShaareController.php:109
391#: application/front/controller/admin/ManageShaareController.php:206
392#: application/front/controller/admin/ManageShaareController.php:275
393#: application/front/controller/admin/ManageShaareController.php:315
237#, php-format 394#, php-format
238msgid "Plugin \"%s\" files not found." 395msgid "Bookmark with identifier %s could not be found."
239msgstr "Les fichiers de l'extension \"%s\" sont introuvables." 396msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
240 397
241#: application/render/PageBuilder.php:209 398#: application/front/controller/admin/ManageShaareController.php:194
242msgid "The page you are trying to reach does not exist or has been deleted." 399#: application/front/controller/admin/ManageShaareController.php:252
243msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée." 400msgid "Invalid bookmark ID provided."
401msgstr "ID du lien non valide."
244 402
245#: application/render/PageBuilder.php:211 403#: application/front/controller/admin/ManageShaareController.php:260
246msgid "404 Not Found" 404msgid "Invalid visibility provided."
247msgstr "404 Introuvable" 405msgstr "Visibilité du lien non valide."
248 406
249#: application/updater/Updater.php:99 407#: application/front/controller/admin/ManageShaareController.php:363
250#, fuzzy 408#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
251#| msgid "Couldn't retrieve Updater class methods." 409msgid "Edit"
252msgid "Couldn't retrieve updater class methods." 410msgstr "Modifier"
253msgstr "Impossible de récupérer les méthodes de la classe Updater."
254 411
255#: application/updater/Updater.php:526 index.php:1034 412#: application/front/controller/admin/ManageShaareController.php:366
256msgid "" 413#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
257"You have enabled or changed thumbnails mode. <a href=\"?do=thumbs_update" 414#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
258"\">Please synchronize them</a>." 415msgid "Shaare"
259msgstr "" 416msgstr "Shaare"
260"Vous avez activé ou changé le mode de miniatures. <a href=\"?do=thumbs_update"
261"\">Merci de les synchroniser</a>."
262 417
263#: application/updater/UpdaterUtils.php:32 418#: application/front/controller/admin/ManageTagController.php:29
264msgid "Updates file path is not set, can't write updates." 419#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
420#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
421msgid "Manage tags"
422msgstr "Gérer les tags"
423
424#: application/front/controller/admin/ManageTagController.php:48
425msgid "Invalid tags provided."
426msgstr "Les tags fournis ne sont pas valides."
427
428#: application/front/controller/admin/ManageTagController.php:72
429#, php-format
430msgid "The tag was removed from %d bookmark."
431msgid_plural "The tag was removed from %d bookmarks."
432msgstr[0] "Le tag a été supprimé du %d lien."
433msgstr[1] "Le tag a été supprimé de %d liens."
434
435#: application/front/controller/admin/ManageTagController.php:77
436#, php-format
437msgid "The tag was renamed in %d bookmark."
438msgid_plural "The tag was renamed in %d bookmarks."
439msgstr[0] "Le tag a été renommé dans %d lien."
440msgstr[1] "Le tag a été renommé dans %d liens."
441
442#: application/front/controller/admin/PasswordController.php:28
443#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
444#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
445msgid "Change password"
446msgstr "Modifier le mot de passe"
447
448#: application/front/controller/admin/PasswordController.php:55
449msgid "You must provide the current and new password to change it."
265msgstr "" 450msgstr ""
266"Le chemin vers le fichier de mise à jour n'est pas défini, impossible " 451"Vous devez fournir les mots de passe actuel et nouveau pour pouvoir le "
267"d'écrire les mises à jour." 452"modifier."
268 453
269#: application/updater/UpdaterUtils.php:37 454#: application/front/controller/admin/PasswordController.php:71
270msgid "Unable to write updates in " 455msgid "The old password is not correct."
271msgstr "Impossible d'écrire les mises à jour dans " 456msgstr "L'ancien mot de passe est incorrect."
272 457
273#: application/updater/exception/UpdaterException.php:51 458#: application/front/controller/admin/PasswordController.php:97
274msgid "An error occurred while running the update " 459msgid "Your password has been changed"
275msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " 460msgstr "Votre mot de passe a été modifié"
276 461
277#: index.php:145 462#: application/front/controller/admin/PluginsController.php:45
278msgid "Shared links on " 463msgid "Plugin Administration"
279msgstr "Liens partagés sur " 464msgstr "Administration des plugins"
280 465
281#: index.php:167 466#: application/front/controller/admin/PluginsController.php:75
282msgid "Insufficient permissions:" 467msgid "Setting successfully saved."
283msgstr "Permissions insuffisantes :" 468msgstr "Les paramètres ont été sauvegardés avec succès."
284 469
285#: index.php:203 470#: application/front/controller/admin/PluginsController.php:78
286msgid "I said: NO. You are banned for the moment. Go away." 471msgid "Error while saving plugin configuration: "
287msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard." 472msgstr ""
473"Une erreur s'est produite lors de la sauvegarde de la configuration des "
474"plugins : "
288 475
289#: index.php:275 476#: application/front/controller/admin/ThumbnailsController.php:37
290msgid "Wrong login/password." 477#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
291msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." 478msgid "Thumbnails update"
479msgstr "Mise à jour des miniatures"
480
481#: application/front/controller/admin/ToolsController.php:31
482#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
483#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33
484msgid "Tools"
485msgstr "Outils"
486
487#: application/front/controller/visitor/BookmarkListController.php:115
488msgid "Search: "
489msgstr "Recherche : "
292 490
293#: index.php:398 index.php:404 491#: application/front/controller/visitor/DailyController.php:45
294msgid "Today" 492msgid "Today"
295msgstr "Aujourd'hui" 493msgstr "Aujourd'hui"
296 494
297#: index.php:400 495#: application/front/controller/visitor/DailyController.php:47
298msgid "Yesterday" 496msgid "Yesterday"
299msgstr "Hier" 497msgstr "Hier"
300 498
301#: index.php:484 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 499#: application/front/controller/visitor/DailyController.php:85
302#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:46 500#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
501#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
303msgid "Daily" 502msgid "Daily"
304msgstr "Quotidien" 503msgstr "Quotidien"
305 504
306#: index.php:593 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 505#: application/front/controller/visitor/ErrorController.php:36
307#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 506msgid "An unexpected error occurred."
308#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 507msgstr "Une erreur inattendue s'est produite."
309#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 508
310#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:75 509#: application/front/controller/visitor/InstallController.php:73
311#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:99 510#, php-format
511msgid ""
512"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
513"variable \"session.save_path\" is set correctly in your PHP config, and that "
514"you have write access to it.<br>It currently points to %s.<br>On some "
515"browsers, accessing your server via a hostname like 'localhost' or any "
516"custom hostname without a dot causes cookie storage to fail. We recommend "
517"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
518msgstr ""
519"<pre>Les sesssions ne semblent pas fonctionner sur ce serveur.<br>Assurez "
520"vous que la variable « session.save_path » est correctement définie dans "
521"votre fichier de configuration PHP, et que vous avez les droits d'écriture "
522"dessus.<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains "
523"navigateurs, accéder à votre serveur depuis un nom d'hôte comme « localhost "
524"» ou autre nom personnalisé sans point '.' entraine l'échec de la sauvegarde "
525"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
526"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
527
528#: application/front/controller/visitor/InstallController.php:144
529msgid ""
530"Shaarli is now configured. Please login and start shaaring your bookmarks!"
531msgstr ""
532"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
533"shaare vos liens !"
534
535#: application/front/controller/visitor/InstallController.php:158
536msgid "Insufficient permissions:"
537msgstr "Permissions insuffisantes :"
538
539#: application/front/controller/visitor/LoginController.php:46
540#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
541#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
542#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
543#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
544#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:77
545#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:101
312msgid "Login" 546msgid "Login"
313msgstr "Connexion" 547msgstr "Connexion"
314 548
315#: index.php:608 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 549#: application/front/controller/visitor/LoginController.php:78
316#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:41 550msgid "Wrong login/password."
551msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
552
553#: application/front/controller/visitor/PictureWallController.php:29
554#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
555#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:43
317msgid "Picture wall" 556msgid "Picture wall"
318msgstr "Mur d'images" 557msgstr "Mur d'images"
319 558
320#: index.php:683 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 559#: application/front/controller/visitor/TagCloudController.php:80
321#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36 560#, fuzzy
322#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 561#| msgid "Tag list"
323msgid "Tag cloud" 562msgid "Tag "
324msgstr "Nuage de tags"
325
326#: index.php:715 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
327msgid "Tag list"
328msgstr "Liste des tags" 563msgstr "Liste des tags"
329 564
330#: index.php:944 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 565#: application/front/exceptions/AlreadyInstalledException.php:11
331#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31 566msgid "Shaarli has already been installed. Login to edit the configuration."
332msgid "Tools" 567msgstr ""
333msgstr "Outils" 568"Shaarli est déjà installé. Connectez-vous pour modifier la configuration."
569
570#: application/front/exceptions/LoginBannedException.php:11
571msgid ""
572"You have been banned after too many failed login attempts. Try again later."
573msgstr ""
574"Vous avez été banni après trop d'échecs d'authentification. Merci de "
575"réessayer plus tard."
334 576
335#: index.php:952 577#: application/front/exceptions/OpenShaarliPasswordException.php:16
336msgid "You are not supposed to change a password on an Open Shaarli." 578msgid "You are not supposed to change a password on an Open Shaarli."
337msgstr "" 579msgstr ""
338"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert." 580"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
339 581
340#: index.php:957 index.php:1007 index.php:1094 index.php:1124 index.php:1234 582#: application/front/exceptions/ThumbnailsDisabledException.php:11
341#: index.php:1281 583msgid "Picture wall unavailable (thumbnails are disabled)."
584msgstr ""
585"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
586
587#: application/front/exceptions/WrongTokenException.php:16
342msgid "Wrong token." 588msgid "Wrong token."
343msgstr "Jeton invalide." 589msgstr "Jeton invalide."
344 590
345#: index.php:966 591#: application/legacy/LegacyLinkDB.php:131
346msgid "The old password is not correct." 592msgid "You are not authorized to add a link."
347msgstr "L'ancien mot de passe est incorrect." 593msgstr "Vous n'êtes pas autorisé à ajouter un lien."
348
349#: index.php:993
350msgid "Your password has been changed"
351msgstr "Votre mot de passe a été modifié"
352
353#: index.php:997
354#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
355#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
356msgid "Change password"
357msgstr "Modifier le mot de passe"
358
359#: index.php:1054
360msgid "Configuration was saved."
361msgstr "La configuration a été sauvegardée."
362 594
363#: index.php:1078 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 595#: application/legacy/LegacyLinkDB.php:134
364msgid "Configure" 596msgid "Internal Error: A link should always have an id and URL."
365msgstr "Configurer" 597msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
366 598
367#: index.php:1088 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 599#: application/legacy/LegacyLinkDB.php:137
368#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 600msgid "You must specify an integer as a key."
369msgid "Manage tags" 601msgstr "Vous devez utiliser un entier comme clé."
370msgstr "Gérer les tags"
371 602
372#: index.php:1107 603#: application/legacy/LegacyLinkDB.php:140
373#, php-format 604msgid "Array offset and link ID must be equal."
374msgid "The tag was removed from %d link." 605msgstr "La clé du tableau et l'ID du lien doivent être identiques."
375msgid_plural "The tag was removed from %d links."
376msgstr[0] "Le tag a été supprimé de %d lien."
377msgstr[1] "Le tag a été supprimé de %d liens."
378 606
379#: index.php:1108 607#: application/legacy/LegacyLinkDB.php:249
380#, php-format 608msgid ""
381msgid "The tag was renamed in %d link." 609"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
382msgid_plural "The tag was renamed in %d links." 610"me, you must first login.\n"
383msgstr[0] "Le tag a été renommé dans %d lien." 611"\n"
384msgstr[1] "Le tag a été renommé dans %d liens." 612"To learn how to use Shaarli, consult the link \"Documentation\" at the "
613"bottom of this page.\n"
614"\n"
615"You use the community supported version of the original Shaarli project, by "
616"Sebastien Sauvage."
617msgstr ""
618"Bienvenue sur Shaarli ! Ceci est votre premier marque-page public. Pour me "
619"modifier ou me supprimer, vous devez d'abord vous connecter.\n"
620"\n"
621"Pour apprendre à utiliser Shaarli, consultez le lien « Documentation » en "
622"bas de page.\n"
623"\n"
624"Vous utilisez la version supportée par la communauté du projet original "
625"Shaarli de Sébastien Sauvage."
385 626
386#: index.php:1115 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 627#: application/legacy/LegacyLinkDB.php:266
387msgid "Shaare a new link" 628msgid "My secret stuff... - Pastebin.com"
388msgstr "Partager un nouveau lien" 629msgstr "Mes trucs secrets... - Pastebin.com"
389 630
390#: index.php:1344 tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 631#: application/legacy/LegacyLinkDB.php:268
391msgid "Edit" 632msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
392msgstr "Modifier" 633msgstr ""
634"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
635"supprimer aussi."
393 636
394#: index.php:1344 index.php:1416 637#: application/legacy/LegacyUpdater.php:104
395#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 638msgid "Couldn't retrieve updater class methods."
396#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26 639msgstr "Impossible de récupérer les méthodes de la classe Updater."
397msgid "Shaare"
398msgstr "Shaare"
399 640
400#: index.php:1385 641#: application/legacy/LegacyUpdater.php:538
401msgid "Note: " 642msgid "<a href=\"./admin/thumbnails\">"
402msgstr "Note : " 643msgstr "<a href=\"./admin/thumbnails\">"
403 644
404#: index.php:1424 645#: application/netscape/NetscapeBookmarkUtils.php:63
405msgid "Invalid link ID provided" 646msgid "Invalid export selection:"
406msgstr "ID du lien non valide" 647msgstr "Sélection d'export invalide :"
407 648
408#: index.php:1444 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 649#: application/netscape/NetscapeBookmarkUtils.php:215
409msgid "Export" 650#, php-format
410msgstr "Exporter" 651msgid "File %s (%d bytes) "
652msgstr "Le fichier %s (%d octets) "
411 653
412#: index.php:1506 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 654#: application/netscape/NetscapeBookmarkUtils.php:217
413msgid "Import" 655msgid "has an unknown file format. Nothing was imported."
414msgstr "Importer" 656msgstr "a un format inconnu. Rien n'a été importé."
415 657
416#: index.php:1516 658#: application/netscape/NetscapeBookmarkUtils.php:221
417#, php-format 659#, php-format
418msgid "" 660msgid ""
419"The file you are trying to upload is probably bigger than what this " 661"was successfully processed in %d seconds: %d bookmarks imported, %d "
420"webserver can accept (%s). Please upload in smaller chunks." 662"bookmarks overwritten, %d bookmarks skipped."
421msgstr "" 663msgstr ""
422"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que " 664"a été importé avec succès en %d secondes : %d liens importés, %d liens "
423"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " 665"écrasés, %d liens ignorés."
424"légères."
425 666
426#: index.php:1561 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 667#: application/plugin/PluginManager.php:122
427#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 668msgid " [plugin incompatibility]: "
428msgid "Plugin administration" 669msgstr " [incompatibilité de l'extension] : "
429msgstr "Administration des plugins"
430 670
431#: index.php:1616 tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 671#: application/plugin/exception/PluginFileNotFoundException.php:21
432msgid "Thumbnails update" 672#, php-format
433msgstr "Mise à jour des miniatures" 673msgid "Plugin \"%s\" files not found."
434 674msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
435#: index.php:1782
436msgid "Search: "
437msgstr "Recherche : "
438 675
439#: index.php:1825 676#: application/render/PageCacheManager.php:32
440#, php-format 677#, php-format
441msgid "" 678msgid "Cannot purge %s: no directory"
442"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " 679msgstr "Impossible de purger %s : le répertoire n'existe pas"
443"variable \"session.save_path\" is set correctly in your PHP config, and that " 680
444"you have write access to it.<br>It currently points to %s.<br>On some " 681#: application/updater/exception/UpdaterException.php:51
445"browsers, accessing your server via a hostname like 'localhost' or any " 682msgid "An error occurred while running the update "
446"custom hostname without a dot causes cookie storage to fail. We recommend " 683msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
447"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
448msgstr ""
449"<pre>Les sesssions ne semblent pas fonctionner sur ce serveur.<br>Assurez "
450"vous que la variable « session.save_path » est correctement définie dans "
451"votre fichier de configuration PHP, et que vous avez les droits d'écriture "
452"dessus.<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains "
453"navigateurs, accéder à votre serveur depuis un nom d'hôte comme « localhost "
454"» ou autre nom personnalisé sans point '.' entraine l'échec de la sauvegarde "
455"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
456"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
457 684
458#: index.php:1835 685#: index.php:62
459msgid "Click to try again." 686msgid "Shared bookmarks on "
460msgstr "Cliquer ici pour réessayer." 687msgstr "Liens partagés sur "
461 688
462#: plugins/addlink_toolbar/addlink_toolbar.php:31 689#: plugins/addlink_toolbar/addlink_toolbar.php:31
463msgid "URI" 690msgid "URI"
@@ -472,15 +699,15 @@ msgstr "Shaare"
472msgid "Adds the addlink input on the linklist page." 699msgid "Adds the addlink input on the linklist page."
473msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." 700msgstr "Ajoute le formulaire d'ajout de liens sur la page principale."
474 701
475#: plugins/archiveorg/archiveorg.php:25 702#: plugins/archiveorg/archiveorg.php:26
476msgid "View on archive.org" 703msgid "View on archive.org"
477msgstr "Voir sur archive.org" 704msgstr "Voir sur archive.org"
478 705
479#: plugins/archiveorg/archiveorg.php:38 706#: plugins/archiveorg/archiveorg.php:39
480msgid "For each link, add an Archive.org icon." 707msgid "For each link, add an Archive.org icon."
481msgstr "Pour chaque lien, ajoute une icône pour Archive.org." 708msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
482 709
483#: plugins/default_colors/default_colors.php:33 710#: plugins/default_colors/default_colors.php:38
484msgid "" 711msgid ""
485"Default colors plugin error: This plugin is active and no custom color is " 712"Default colors plugin error: This plugin is active and no custom color is "
486"configured." 713"configured."
@@ -488,25 +715,25 @@ msgstr ""
488"Erreur du plugin default colors : ce plugin est actif et aucune couleur " 715"Erreur du plugin default colors : ce plugin est actif et aucune couleur "
489"n'est configurée." 716"n'est configurée."
490 717
491#: plugins/default_colors/default_colors.php:107 718#: plugins/default_colors/default_colors.php:113
492msgid "Override default theme colors. Use any CSS valid color." 719msgid "Override default theme colors. Use any CSS valid color."
493msgstr "" 720msgstr ""
494"Remplacer les couleurs du thème par défaut. Utiliser n'importe quelle " 721"Remplacer les couleurs du thème par défaut. Utiliser n'importe quelle "
495"couleur CSS valide." 722"couleur CSS valide."
496 723
497#: plugins/default_colors/default_colors.php:108 724#: plugins/default_colors/default_colors.php:114
498msgid "Main color (navbar green)" 725msgid "Main color (navbar green)"
499msgstr "Couleur principale (vert de la barre de navigation)" 726msgstr "Couleur principale (vert de la barre de navigation)"
500 727
501#: plugins/default_colors/default_colors.php:109 728#: plugins/default_colors/default_colors.php:115
502msgid "Background color (light grey)" 729msgid "Background color (light grey)"
503msgstr "Couleur de fond (gris léger)" 730msgstr "Couleur de fond (gris léger)"
504 731
505#: plugins/default_colors/default_colors.php:110 732#: plugins/default_colors/default_colors.php:116
506msgid "Dark main color (e.g. visited links)" 733msgid "Dark main color (e.g. visited links)"
507msgstr "Couleur principale sombre (ex : les liens visités)" 734msgstr "Couleur principale sombre (ex : les liens visités)"
508 735
509#: plugins/demo_plugin/demo_plugin.php:482 736#: plugins/demo_plugin/demo_plugin.php:477
510msgid "" 737msgid ""
511"A demo plugin covering all use cases for template designers and plugin " 738"A demo plugin covering all use cases for template designers and plugin "
512"developers." 739"developers."
@@ -514,11 +741,11 @@ msgstr ""
514"Une extension de démonstration couvrant tous les cas d'utilisation pour les " 741"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
515"designers de thèmes et les développeurs d'extensions." 742"designers de thèmes et les développeurs d'extensions."
516 743
517#: plugins/demo_plugin/demo_plugin.php:483 744#: plugins/demo_plugin/demo_plugin.php:478
518msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." 745msgid "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é." 746msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé."
520 747
521#: plugins/demo_plugin/demo_plugin.php:484 748#: plugins/demo_plugin/demo_plugin.php:479
522msgid "Other demo parameter" 749msgid "Other demo parameter"
523msgstr "Un autre paramètre de démo" 750msgstr "Un autre paramètre de démo"
524 751
@@ -540,36 +767,6 @@ msgstr ""
540msgid "Isso server URL (without 'http://')" 767msgid "Isso server URL (without 'http://')"
541msgstr "URL du serveur Isso (sans 'http://')" 768msgstr "URL du serveur Isso (sans 'http://')"
542 769
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 770#: plugins/piwik/piwik.php:23
574msgid "" 771msgid ""
575"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " 772"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
@@ -626,7 +823,7 @@ msgstr "Mauvaise réponse du hub %s"
626msgid "Enable PubSubHubbub feed publishing." 823msgid "Enable PubSubHubbub feed publishing."
627msgstr "Active la publication de flux vers PubSubHubbub." 824msgstr "Active la publication de flux vers PubSubHubbub."
628 825
629#: plugins/qrcode/qrcode.php:72 plugins/wallabag/wallabag.php:68 826#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:70
630msgid "For each link, add a QRCode icon." 827msgid "For each link, add a QRCode icon."
631msgstr "Pour chaque lien, ajouter une icône de QRCode." 828msgstr "Pour chaque lien, ajouter une icône de QRCode."
632 829
@@ -642,24 +839,14 @@ msgstr ""
642msgid "Save to wallabag" 839msgid "Save to wallabag"
643msgstr "Sauvegarder dans Wallabag" 840msgstr "Sauvegarder dans Wallabag"
644 841
645#: plugins/wallabag/wallabag.php:69 842#: plugins/wallabag/wallabag.php:71
646msgid "Wallabag API URL" 843msgid "Wallabag API URL"
647msgstr "URL de l'API Wallabag" 844msgstr "URL de l'API Wallabag"
648 845
649#: plugins/wallabag/wallabag.php:70 846#: plugins/wallabag/wallabag.php:72
650msgid "Wallabag API version (1 or 2)" 847msgid "Wallabag API version (1 or 2)"
651msgstr "Version de l'API Wallabag (1 ou 2)" 848msgstr "Version de l'API Wallabag (1 ou 2)"
652 849
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 850#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
664msgid "Sorry, nothing to see here." 851msgid "Sorry, nothing to see here."
665msgstr "Désolé, il y a rien à voir ici." 852msgstr "Désolé, il y a rien à voir ici."
@@ -698,10 +885,11 @@ msgid "Rename"
698msgstr "Renommer" 885msgstr "Renommer"
699 886
700#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 887#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
701#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 888#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93
702#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 889#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
703#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145 890#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
704#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:145 891#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
892#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
705msgid "Delete" 893msgid "Delete"
706msgstr "Supprimer" 894msgstr "Supprimer"
707 895
@@ -713,33 +901,6 @@ msgstr "Vous pouvez aussi modifier les tags dans la"
713msgid "tag list" 901msgid "tag list"
714msgstr "liste des tags" 902msgstr "liste des tags"
715 903
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 904#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
744msgid "title" 905msgid "title"
745msgstr "titre" 906msgstr "titre"
@@ -756,109 +917,132 @@ msgstr "Valeur par défaut"
756msgid "Theme" 917msgid "Theme"
757msgstr "Thème" 918msgstr "Thème"
758 919
759#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 920#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
760#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 921msgid "Description formatter"
922msgstr "Format des descriptions"
923
924#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
925#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
761msgid "Language" 926msgid "Language"
762msgstr "Langue" 927msgstr "Langue"
763 928
764#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116 929#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
765#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 930#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
766msgid "Timezone" 931msgid "Timezone"
767msgstr "Fuseau horaire" 932msgstr "Fuseau horaire"
768 933
769#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 934#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
770#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 935#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
771msgid "Continent" 936msgid "Continent"
772msgstr "Continent" 937msgstr "Continent"
773 938
774#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 939#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
775#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 940#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
776msgid "City" 941msgid "City"
777msgstr "Ville" 942msgstr "Ville"
778 943
779#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164 944#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191
780msgid "Disable session cookie hijacking protection" 945msgid "Disable session cookie hijacking protection"
781msgstr "Désactiver la protection contre le détournement de cookies" 946msgstr "Désactiver la protection contre le détournement de cookies"
782 947
783#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166 948#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193
784msgid "Check this if you get disconnected or if your IP address changes often" 949msgid "Check this if you get disconnected or if your IP address changes often"
785msgstr "" 950msgstr ""
786"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP " 951"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP "
787"change souvent" 952"change souvent"
788 953
789#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 954#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210
790msgid "Private links by default" 955msgid "Private links by default"
791msgstr "Liens privés par défaut" 956msgstr "Liens privés par défaut"
792 957
793#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184 958#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211
794msgid "All new links are private by default" 959msgid "All new links are private by default"
795msgstr "Tous les nouveaux liens sont privés par défaut" 960msgstr "Tous les nouveaux liens sont privés par défaut"
796 961
797#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 962#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226
798msgid "RSS direct links" 963msgid "RSS direct links"
799msgstr "Liens directs dans le flux RSS" 964msgstr "Liens directs dans le flux RSS"
800 965
801#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200 966#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227
802msgid "Check this to use direct URL instead of permalink in feeds" 967msgid "Check this to use direct URL instead of permalink in feeds"
803msgstr "" 968msgstr ""
804"Cocher cette case pour utiliser des liens directs au lieu des permaliens " 969"Cocher cette case pour utiliser des liens directs au lieu des permaliens "
805"dans le flux RSS" 970"dans le flux RSS"
806 971
807#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215 972#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242
808msgid "Hide public links" 973msgid "Hide public links"
809msgstr "Cacher les liens publics" 974msgstr "Cacher les liens publics"
810 975
811#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216 976#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243
812msgid "Do not show any links if the user is not logged in" 977msgid "Do not show any links if the user is not logged in"
813msgstr "N'afficher aucun lien sans être connecté" 978msgstr "N'afficher aucun lien sans être connecté"
814 979
815#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231 980#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258
816#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 981#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:149
817msgid "Check updates" 982msgid "Check updates"
818msgstr "Vérifier les mises à jour" 983msgstr "Vérifier les mises à jour"
819 984
820#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232 985#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259
821#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 986#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
822msgid "Notify me when a new release is ready" 987msgid "Notify me when a new release is ready"
823msgstr "Me notifier lorsqu'une nouvelle version est disponible" 988msgstr "Me notifier lorsqu'une nouvelle version est disponible"
824 989
825#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247 990#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
826msgid "Automatically retrieve description for new bookmarks" 991msgid "Automatically retrieve description for new bookmarks"
827msgstr "Récupérer automatiquement la description" 992msgstr "Récupérer automatiquement la description"
828 993
829#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248 994#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275
830msgid "Shaarli will try to retrieve the description from meta HTML headers" 995msgid "Shaarli will try to retrieve the description from meta HTML headers"
831msgstr "" 996msgstr ""
832"Shaarli essaiera de récupérer la description depuis les balises HTML meta " 997"Shaarli essaiera de récupérer la description depuis les balises HTML meta "
833"dans les entêtes" 998"dans les entêtes"
834 999
835#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263 1000#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290
836#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 1001#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
837msgid "Enable REST API" 1002msgid "Enable REST API"
838msgstr "Activer l'API REST" 1003msgstr "Activer l'API REST"
839 1004
840#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:264 1005#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:291
841#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 1006#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
842msgid "Allow third party software to use Shaarli such as mobile application" 1007msgid "Allow third party software to use Shaarli such as mobile application"
843msgstr "" 1008msgstr ""
844"Permet aux applications tierces d'utiliser Shaarli, par exemple les " 1009"Permet aux applications tierces d'utiliser Shaarli, par exemple les "
845"applications mobiles" 1010"applications mobiles"
846 1011
847#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:279 1012#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306
848msgid "API secret" 1013msgid "API secret"
849msgstr "Clé d'API secrète" 1014msgstr "Clé d'API secrète"
850 1015
851#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:293 1016#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
852msgid "Enable thumbnails" 1017msgid "Enable thumbnails"
853msgstr "Activer les miniatures" 1018msgstr "Activer les miniatures"
854 1019
855#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:301 1020#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324
1021msgid "You need to enable the extension <code>php-gd</code> to use thumbnails."
1022msgstr ""
1023"Vous devez activer l'extension <code>php-gd</code> pour utiliser les "
1024"miniatures."
1025
1026#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
856#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 1027#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
857msgid "Synchronize thumbnails" 1028msgid "Synchronize thumbnails"
858msgstr "Synchroniser les miniatures" 1029msgstr "Synchroniser les miniatures"
859 1030
860#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 1031#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
861#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 1032#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
1033msgid "All"
1034msgstr "Tous"
1035
1036#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
1037msgid "Only common media hosts"
1038msgstr "Seulement les hébergeurs de média connus"
1039
1040#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
1041msgid "None"
1042msgstr "Aucune"
1043
1044#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355
1045#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
862#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 1046#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
863#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 1047#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
864msgid "Save" 1048msgid "Save"
@@ -884,27 +1068,27 @@ msgstr "Tous les liens d'un jour sur une page."
884msgid "Next day" 1068msgid "Next day"
885msgstr "Jour suivant" 1069msgstr "Jour suivant"
886 1070
887#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1071#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
888msgid "Edit Shaare" 1072msgid "Edit Shaare"
889msgstr "Modifier le Shaare" 1073msgstr "Modifier le Shaare"
890 1074
891#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1075#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
892msgid "New Shaare" 1076msgid "New Shaare"
893msgstr "Nouveau Shaare" 1077msgstr "Nouveau Shaare"
894 1078
895#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 1079#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
896msgid "Created:" 1080msgid "Created:"
897msgstr "Création :" 1081msgstr "Création :"
898 1082
899#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 1083#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
900msgid "URL" 1084msgid "URL"
901msgstr "URL" 1085msgstr "URL"
902 1086
903#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 1087#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
904msgid "Title" 1088msgid "Title"
905msgstr "Titre" 1089msgstr "Titre"
906 1090
907#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 1091#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
908#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 1092#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
909#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 1093#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
910#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 1094#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@@ -912,17 +1096,29 @@ msgstr "Titre"
912msgid "Description" 1096msgid "Description"
913msgstr "Description" 1097msgstr "Description"
914 1098
915#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 1099#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
916msgid "Tags" 1100msgid "Tags"
917msgstr "Tags" 1101msgstr "Tags"
918 1102
919#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 1103#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
920#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1104#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
921#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167 1105#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
922msgid "Private" 1106msgid "Private"
923msgstr "Privé" 1107msgstr "Privé"
924 1108
925#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 1109#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1110msgid "Description will be rendered with"
1111msgstr "La description sera générée avec"
1112
1113#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
1114msgid "Markdown syntax documentation"
1115msgstr "Documentation sur la syntaxe Markdown"
1116
1117#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
1118msgid "Markdown syntax"
1119msgstr "la syntaxe Markdown"
1120
1121#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
926msgid "Apply Changes" 1122msgid "Apply Changes"
927msgstr "Appliquer les changements" 1123msgstr "Appliquer les changements"
928 1124
@@ -930,19 +1126,19 @@ msgstr "Appliquer les changements"
930msgid "Export Database" 1126msgid "Export Database"
931msgstr "Exporter les données" 1127msgstr "Exporter les données"
932 1128
933#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 1129#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
934msgid "Selection" 1130msgid "Selection"
935msgstr "Choisir" 1131msgstr "Choisir"
936 1132
937#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 1133#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
938msgid "Public" 1134msgid "Public"
939msgstr "Publics" 1135msgstr "Publics"
940 1136
941#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 1137#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
942msgid "Prepend note permalinks with this Shaarli instance's URL" 1138msgid "Prepend note permalinks with this Shaarli instance's URL"
943msgstr "Préfixer les liens de note avec l'URL de l'instance de Shaarli" 1139msgstr "Préfixer les liens de note avec l'URL de l'instance de Shaarli"
944 1140
945#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 1141#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
946msgid "Useful to import bookmarks in a web browser" 1142msgid "Useful to import bookmarks in a web browser"
947msgstr "Utile pour importer les marques-pages dans un navigateur" 1143msgstr "Utile pour importer les marques-pages dans un navigateur"
948 1144
@@ -993,29 +1189,29 @@ msgstr ""
993"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de " 1189"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de "
994"le configurer." 1190"le configurer."
995 1191
996#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 1192#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
997#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 1193#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
998#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 1194#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167
999#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:165 1195#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:167
1000msgid "Username" 1196msgid "Username"
1001msgstr "Nom d'utilisateur" 1197msgstr "Nom d'utilisateur"
1002 1198
1003#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 1199#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1004#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 1200#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
1005#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166 1201#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
1006#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:166 1202#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:168
1007msgid "Password" 1203msgid "Password"
1008msgstr "Mot de passe" 1204msgstr "Mot de passe"
1009 1205
1010#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 1206#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62
1011msgid "Shaarli title" 1207msgid "Shaarli title"
1012msgstr "Titre du Shaarli" 1208msgstr "Titre du Shaarli"
1013 1209
1014#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 1210#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
1015msgid "My links" 1211msgid "My links"
1016msgstr "Mes liens" 1212msgstr "Mes liens"
1017 1213
1018#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 1214#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
1019msgid "Install" 1215msgid "Install"
1020msgstr "Installer" 1216msgstr "Installer"
1021 1217
@@ -1034,21 +1230,31 @@ msgstr[0] "lien privé"
1034msgstr[1] "liens privés" 1230msgstr[1] "liens privés"
1035 1231
1036#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 1232#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
1037#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 1233#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1038#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:121 1234#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123
1039msgid "Search text" 1235msgid "Search text"
1040msgstr "Recherche texte" 1236msgstr "Recherche texte"
1041 1237
1042#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 1238#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
1043#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:128 1239#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
1044#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:128 1240#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:130
1045#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1241#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1046#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 1242#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
1047#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1243#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1048#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 1244#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1049msgid "Filter by tag" 1245msgid "Filter by tag"
1050msgstr "Filtrer par tag" 1246msgstr "Filtrer par tag"
1051 1247
1248#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1249#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
1250#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
1251#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:87
1252#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:139
1253#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1254#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
1255msgid "Search"
1256msgstr "Rechercher"
1257
1052#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 1258#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
1053msgid "Nothing found." 1259msgid "Nothing found."
1054msgstr "Aucun résultat." 1260msgstr "Aucun résultat."
@@ -1069,60 +1275,61 @@ msgid "tagged"
1069msgstr "taggé" 1275msgstr "taggé"
1070 1276
1071#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133 1277#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133
1278#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1072msgid "Remove tag" 1279msgid "Remove tag"
1073msgstr "Retirer le tag" 1280msgstr "Retirer le tag"
1074 1281
1075#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:142 1282#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
1076msgid "with status" 1283msgid "with status"
1077msgstr "avec le statut" 1284msgstr "avec le statut"
1078 1285
1079#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153 1286#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1080msgid "without any tag" 1287msgid "without any tag"
1081msgstr "sans tag" 1288msgstr "sans tag"
1082 1289
1083#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 1290#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
1084#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 1291#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
1085#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 1292#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
1086msgid "Fold" 1293msgid "Fold"
1087msgstr "Replier" 1294msgstr "Replier"
1088 1295
1089#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 1296#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
1090msgid "Edited: " 1297msgid "Edited: "
1091msgstr "Modifié : " 1298msgstr "Modifié : "
1092 1299
1093#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 1300#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
1094msgid "permalink" 1301msgid "permalink"
1095msgstr "permalien" 1302msgstr "permalien"
1096 1303
1097#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 1304#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
1098msgid "Add tag" 1305msgid "Add tag"
1099msgstr "Ajouter un tag" 1306msgstr "Ajouter un tag"
1100 1307
1101#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 1308#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185
1102msgid "Toggle sticky" 1309msgid "Toggle sticky"
1103msgstr "Changer statut épinglé" 1310msgstr "Changer statut épinglé"
1104 1311
1105#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185 1312#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187
1106msgid "Sticky" 1313msgid "Sticky"
1107msgstr "Épinglé" 1314msgstr "Épinglé"
1108 1315
1109#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 1316#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
1110#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7 1317#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5
1111msgid "Filters" 1318msgid "Filters"
1112msgstr "Filtres" 1319msgstr "Filtres"
1113 1320
1114#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 1321#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:10
1115#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12 1322#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:10
1116msgid "Only display private links" 1323msgid "Only display private links"
1117msgstr "Afficher uniquement les liens privés" 1324msgstr "Afficher uniquement les liens privés"
1118 1325
1119#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 1326#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
1120#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:15 1327#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:13
1121msgid "Only display public links" 1328msgid "Only display public links"
1122msgstr "Afficher uniquement les liens publics" 1329msgstr "Afficher uniquement les liens publics"
1123 1330
1124#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 1331#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
1125#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:20 1332#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
1126msgid "Filter untagged links" 1333msgid "Filter untagged links"
1127msgstr "Filtrer par liens privés" 1334msgstr "Filtrer par liens privés"
1128 1335
@@ -1131,30 +1338,23 @@ msgstr "Filtrer par liens privés"
1131msgid "Select all" 1338msgid "Select all"
1132msgstr "Tout sélectionner" 1339msgstr "Tout sélectionner"
1133 1340
1134#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 1341#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1135#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 1342#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89
1136#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:27 1343#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29
1137#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:79 1344#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89
1138#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 1345#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1139#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 1346#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
1140msgid "Fold all" 1347msgid "Fold all"
1141msgstr "Replier tout" 1348msgstr "Replier tout"
1142 1349
1143#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 1350#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1144#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:72 1351#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76
1145msgid "Links per page" 1352msgid "Links per page"
1146msgstr "Liens par page" 1353msgstr "Liens par page"
1147 1354
1148#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 1355#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
1149msgid "" 1356#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
1150"You have been banned after too many failed login attempts. Try again later." 1357#: 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" 1358msgid "Remember me"
1159msgstr "Rester connecté" 1359msgstr "Rester connecté"
1160 1360
@@ -1185,62 +1385,64 @@ msgstr "Déplier tout"
1185msgid "Are you sure you want to delete this link?" 1385msgid "Are you sure you want to delete this link?"
1186msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" 1386msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
1187 1387
1188#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 1388#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11
1189#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:90 1389#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11
1190#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:65 1390msgid "Menu"
1191#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:90 1391msgstr "Menu"
1392
1393#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
1394#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:38
1395#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1396msgid "Tag cloud"
1397msgstr "Nuage de tags"
1398
1399#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
1400#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
1401#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:67
1402#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:92
1192msgid "RSS Feed" 1403msgid "RSS Feed"
1193msgstr "Flux RSS" 1404msgstr "Flux RSS"
1194 1405
1195#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 1406#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
1196#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 1407#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
1197#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:70 1408#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:72
1198#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:106 1409#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:108
1199msgid "Logout" 1410msgid "Logout"
1200msgstr "Déconnexion" 1411msgstr "Déconnexion"
1201 1412
1202#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 1413#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
1203#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:150 1414#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152
1204msgid "Set public" 1415msgid "Set public"
1205msgstr "Rendre public" 1416msgstr "Rendre public"
1206 1417
1207#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 1418#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157
1208#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:155 1419#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157
1209msgid "Set private" 1420msgid "Set private"
1210msgstr "Rendre privé" 1421msgstr "Rendre privé"
1211 1422
1212#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187 1423#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
1213#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:187 1424#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189
1214msgid "is available" 1425msgid "is available"
1215msgstr "est disponible" 1426msgstr "est disponible"
1216 1427
1217#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:194 1428#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196
1218#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:194 1429#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196
1219msgid "Error" 1430msgid "Error"
1220msgstr "Erreur" 1431msgstr "Erreur"
1221 1432
1222#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 1433#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1223msgid "Picture wall unavailable (thumbnails are disabled)." 1434msgid "There is no cached thumbnail."
1224msgstr "" 1435msgstr "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 1436
1227#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 1437#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
1228#, fuzzy 1438msgid "Try to synchronize them."
1229#| msgid "" 1439msgstr "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 1440
1239#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1441#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1240msgid "Picture Wall" 1442msgid "Picture Wall"
1241msgstr "Mur d'images" 1443msgstr "Mur d'images"
1242 1444
1243#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 1445#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1244msgid "pics" 1446msgid "pics"
1245msgstr "images" 1447msgstr "images"
1246 1448
@@ -1249,6 +1451,11 @@ msgid "You need to enable Javascript to change plugin loading order."
1249msgstr "" 1451msgstr ""
1250"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions." 1452"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions."
1251 1453
1454#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
1455#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
1456msgid "Plugin administration"
1457msgstr "Administration des plugins"
1458
1252#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 1459#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1253msgid "Enabled Plugins" 1460msgid "Enabled Plugins"
1254msgstr "Extensions activées" 1461msgstr "Extensions activées"
@@ -1314,6 +1521,14 @@ msgstr "tags"
1314msgid "List all links with those tags" 1521msgid "List all links with those tags"
1315msgstr "Lister tous les liens avec ces tags" 1522msgstr "Lister tous les liens avec ces tags"
1316 1523
1524#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1525msgid "Tag list"
1526msgstr "Liste des tags"
1527
1528#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
1529msgid "Rename tag"
1530msgstr "Renommer le tag"
1531
1317#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 1532#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
1318#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 1533#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
1319msgid "Sort by:" 1534msgid "Sort by:"
@@ -1359,32 +1574,24 @@ msgid "Rename or delete a tag in all links"
1359msgstr "Renommer ou supprimer un tag dans tous les liens" 1574msgstr "Renommer ou supprimer un tag dans tous les liens"
1360 1575
1361#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 1576#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1362#, fuzzy
1363#| msgid ""
1364#| "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1365#| "delicious…)"
1366msgid "" 1577msgid ""
1367"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " 1578"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1368"delicious...)" 1579"delicious...)"
1369msgstr "" 1580msgstr ""
1370"Importer des marques pages au format Netscape HTML (comme exportés depuis " 1581"Importer des marques pages au format Netscape HTML (comme exportés depuis "
1371"Firefox, Chrome, Opera, delicious…)" 1582"Firefox, Chrome, Opera, delicious...)"
1372 1583
1373#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 1584#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1374msgid "Import links" 1585msgid "Import links"
1375msgstr "Importer des liens" 1586msgstr "Importer des liens"
1376 1587
1377#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 1588#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1378#, fuzzy
1379#| msgid ""
1380#| "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1381#| "Opera, delicious…)"
1382msgid "" 1589msgid ""
1383"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " 1590"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1384"Opera, delicious...)" 1591"Opera, delicious...)"
1385msgstr "" 1592msgstr ""
1386"Exporter les marques pages au format Netscape HTML (comme exportés depuis " 1593"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
1387"Firefox, Chrome, Opera, delicious…)" 1594"Firefox, Chrome, Opera, delicious...)"
1388 1595
1389#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 1596#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1390msgid "Export database" 1597msgid "Export database"
@@ -1457,6 +1664,68 @@ msgstr ""
1457"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " 1664"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1458"Ajouter aux favoris »" 1665"Ajouter aux favoris »"
1459 1666
1667#, fuzzy
1668#~| msgid "Selection"
1669#~ msgid ".ui-selecting"
1670#~ msgstr "Choisir"
1671
1672#, fuzzy
1673#~| msgid "Documentation"
1674#~ msgid "document"
1675#~ msgstr "Documentation"
1676
1677#~ msgid "The page you are trying to reach does not exist or has been deleted."
1678#~ msgstr ""
1679#~ "La page que vous essayez de consulter n'existe pas ou a été supprimée."
1680
1681#~ msgid "404 Not Found"
1682#~ msgstr "404 Introuvable"
1683
1684#~ msgid "Updates file path is not set, can't write updates."
1685#~ msgstr ""
1686#~ "Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
1687#~ "d'écrire les mises à jour."
1688
1689#~ msgid "Unable to write updates in "
1690#~ msgstr "Impossible d'écrire les mises à jour dans "
1691
1692#~ msgid "I said: NO. You are banned for the moment. Go away."
1693#~ msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
1694
1695#~ msgid "Click to try again."
1696#~ msgstr "Cliquer ici pour réessayer."
1697
1698#~ msgid ""
1699#~ "Render shaare description with Markdown syntax.<br><strong>Warning</"
1700#~ "strong>:\n"
1701#~ "If your shaared descriptions contained HTML tags before enabling the "
1702#~ "markdown plugin,\n"
1703#~ "enabling it might break your page.\n"
1704#~ "See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
1705#~ "markdown#html-rendering\">README</a>."
1706#~ msgstr ""
1707#~ "Utilise la syntaxe Markdown pour la description des liens."
1708#~ "<br><strong>Attention</strong> :\n"
1709#~ "Si vous aviez des descriptions contenant du HTML avant d'activer cette "
1710#~ "extension,\n"
1711#~ "l'activer pourrait déformer vos pages.\n"
1712#~ "Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
1713#~ "markdown#html-rendering\">README</a>."
1714
1715#~ msgid "Synchonize thumbnails"
1716#~ msgstr "Synchroniser les miniatures"
1717
1718#, fuzzy
1719#~| msgid ""
1720#~| "You don't have any cached thumbnail. Try to <a href=\"?do=thumbs_update"
1721#~| "\">synchronize them</a>."
1722#~ msgid ""
1723#~ "There is no cached thumbnail. Try to <a href=\"?do=thumbs_update"
1724#~ "\">synchronize them</a>."
1725#~ msgstr ""
1726#~ "Il n'y a aucune miniature en cache. Essayer de <a href=\"?do=thumbs_update"
1727#~ "\">les synchroniser</a>."
1728
1460#~ msgid "" 1729#~ msgid ""
1461#~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this " 1730#~ "You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1462#~ "functionality." 1731#~ "functionality."
diff --git a/inc/languages/jp/LC_MESSAGES/shaarli.po b/inc/languages/jp/LC_MESSAGES/shaarli.po
new file mode 100644
index 00000000..b420bb51
--- /dev/null
+++ b/inc/languages/jp/LC_MESSAGES/shaarli.po
@@ -0,0 +1,1293 @@
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/index.php b/index.php
index 1f979d3b..b10397dd 100644
--- a/index.php
+++ b/index.php
@@ -10,129 +10,49 @@
10 * - https://github.com/sebsauvage/Shaarli 10 * - https://github.com/sebsauvage/Shaarli
11 * 11 *
12 * Licence: http://www.opensource.org/licenses/zlib-license.php 12 * Licence: http://www.opensource.org/licenses/zlib-license.php
13 *
14 * Requires: PHP 5.5.x
15 */
16
17// Set 'UTC' as the default timezone if it is not defined in php.ini
18// See http://php.net/manual/en/datetime.configuration.php#ini.date.timezone
19if (date_default_timezone_get() == '') {
20 date_default_timezone_set('UTC');
21}
22
23/*
24 * PHP configuration
25 */ 13 */
26 14
27// http://server.com/x/shaarli --> /shaarli/
28define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0)));
29
30// High execution time in case of problematic imports/exports.
31ini_set('max_input_time', '60');
32
33// Try to set max upload file size and read
34ini_set('memory_limit', '128M');
35ini_set('post_max_size', '16M');
36ini_set('upload_max_filesize', '16M');
37
38// See all error except warnings
39error_reporting(E_ALL^E_WARNING);
40// See all errors (for debugging only)
41//error_reporting(-1);
42
43
44// 3rd-party libraries
45if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
46 header('Content-Type: text/plain; charset=utf-8');
47 echo "Error: missing Composer configuration\n\n"
48 ."If you installed Shaarli through Git or using the development branch,\n"
49 ."please refer to the installation documentation to install PHP"
50 ." dependencies using Composer:\n"
51 ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
52 ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
53 exit;
54}
55require_once 'inc/rain.tpl.class.php'; 15require_once 'inc/rain.tpl.class.php';
56require_once __DIR__ . '/vendor/autoload.php'; 16require_once __DIR__ . '/vendor/autoload.php';
57 17
58// Shaarli library 18// Shaarli library
59require_once 'application/bookmark/LinkUtils.php'; 19require_once 'application/bookmark/LinkUtils.php';
60require_once 'application/config/ConfigPlugin.php'; 20require_once 'application/config/ConfigPlugin.php';
61require_once 'application/feed/Cache.php';
62require_once 'application/http/HttpUtils.php'; 21require_once 'application/http/HttpUtils.php';
63require_once 'application/http/UrlUtils.php'; 22require_once 'application/http/UrlUtils.php';
64require_once 'application/updater/UpdaterUtils.php';
65require_once 'application/FileUtils.php';
66require_once 'application/TimeZone.php'; 23require_once 'application/TimeZone.php';
67require_once 'application/Utils.php'; 24require_once 'application/Utils.php';
68 25
69use \Shaarli\ApplicationUtils; 26require_once __DIR__ . '/init.php';
70use \Shaarli\Bookmark\Exception\LinkNotFoundException;
71use \Shaarli\Bookmark\LinkDB;
72use \Shaarli\Config\ConfigManager;
73use \Shaarli\Feed\CachedPage;
74use \Shaarli\Feed\FeedBuilder;
75use \Shaarli\History;
76use \Shaarli\Languages;
77use \Shaarli\Netscape\NetscapeBookmarkUtils;
78use \Shaarli\Plugin\PluginManager;
79use \Shaarli\Render\PageBuilder;
80use \Shaarli\Render\ThemeUtils;
81use \Shaarli\Router;
82use \Shaarli\Security\LoginManager;
83use \Shaarli\Security\SessionManager;
84use \Shaarli\Thumbnailer;
85use \Shaarli\Updater\Updater;
86 27
87// Ensure the PHP version is supported 28use Shaarli\Config\ConfigManager;
88try { 29use Shaarli\Container\ContainerBuilder;
89 ApplicationUtils::checkPHPVersion('5.5', PHP_VERSION); 30use Shaarli\Languages;
90} catch (Exception $exc) { 31use Shaarli\Security\CookieManager;
91 header('Content-Type: text/plain; charset=utf-8'); 32use Shaarli\Security\LoginManager;
92 echo $exc->getMessage(); 33use Shaarli\Security\SessionManager;
93 exit; 34use Slim\App;
94}
95 35
96define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); 36$conf = new ConfigManager();
97 37
98// Force cookie path (but do not change lifetime) 38// Manually override root URL for complex server configurations
99$cookie = session_get_cookie_params(); 39define('SHAARLI_ROOT_URL', $conf->get('general.root_url', null));
100$cookiedir = '';
101if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
102 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
103}
104// Set default cookie expiration and path.
105session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
106// Set session parameters on server side.
107// Use cookies to store session.
108ini_set('session.use_cookies', 1);
109// Force cookies for session (phpsessionID forbidden in URL).
110ini_set('session.use_only_cookies', 1);
111// Prevent PHP form using sessionID in URL if cookies are disabled.
112ini_set('session.use_trans_sid', false);
113 40
114session_name('shaarli'); 41// In dev mode, throw exception on any warning
115// Start session if needed (Some server auto-start sessions). 42if ($conf->get('dev.debug', false)) {
116if (session_status() == PHP_SESSION_NONE) { 43 // See all errors (for debugging only)
117 session_start(); 44 error_reporting(-1);
118}
119 45
120// Regenerate session ID if invalid or not defined in cookie. 46 set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) {
121if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) { 47 throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
122 session_regenerate_id(true); 48 });
123 $_COOKIE['shaarli'] = session_id();
124} 49}
125 50
126$conf = new ConfigManager(); 51$sessionManager = new SessionManager($_SESSION, $conf, session_save_path());
127$sessionManager = new SessionManager($_SESSION, $conf); 52$sessionManager->initialize();
128$loginManager = new LoginManager($conf, $sessionManager); 53$cookieManager = new CookieManager($_COOKIE);
54$loginManager = new LoginManager($conf, $sessionManager, $cookieManager);
129$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); 55$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
130$clientIpId = client_ip_id($_SERVER);
131
132// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
133if (! defined('LC_MESSAGES')) {
134 define('LC_MESSAGES', LC_COLLATE);
135}
136 56
137// Sniff browser language and set date format accordingly. 57// Sniff browser language and set date format accordingly.
138if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { 58if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
@@ -142,1792 +62,78 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
142new Languages(setlocale(LC_MESSAGES, 0), $conf); 62new Languages(setlocale(LC_MESSAGES, 0), $conf);
143 63
144$conf->setEmpty('general.timezone', date_default_timezone_get()); 64$conf->setEmpty('general.timezone', date_default_timezone_get());
145$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER))); 65$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER)));
66
146RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory 67RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
147RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory 68RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
148 69
149$pluginManager = new PluginManager($conf);
150$pluginManager->load($conf->get('general.enabled_plugins'));
151
152date_default_timezone_set($conf->get('general.timezone', 'UTC')); 70date_default_timezone_set($conf->get('general.timezone', 'UTC'));
153 71
154ob_start(); // Output buffering for the page cache. 72$loginManager->checkLoginState(client_ip_id($_SERVER));
155 73
156// Prevent caching on client side or proxy: (yes, it's ugly) 74$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager);
157header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); 75$container = $containerBuilder->build();
158header("Cache-Control: no-store, no-cache, must-revalidate"); 76$app = new App($container);
159header("Cache-Control: post-check=0, pre-check=0", false); 77
160header("Pragma: no-cache"); 78// Main Shaarli routes
161 79$app->group('', function () {
162if (! is_file($conf->getConfigFileExt())) { 80 $this->get('/install', '\Shaarli\Front\Controller\Visitor\InstallController:index')->setName('displayInstall');
163 // Ensure Shaarli has proper access to its resources 81 $this->get('/install/session-test', '\Shaarli\Front\Controller\Visitor\InstallController:sessionTest');
164 $errors = ApplicationUtils::checkResourcePermissions($conf); 82 $this->post('/install', '\Shaarli\Front\Controller\Visitor\InstallController:save')->setName('saveInstall');
165 83
166 if ($errors != array()) { 84 /* -- PUBLIC --*/
167 $message = '<p>'. t('Insufficient permissions:') .'</p><ul>'; 85 $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index');
168 86 $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink');
169 foreach ($errors as $error) { 87 $this->get('/login', '\Shaarli\Front\Controller\Visitor\LoginController:index')->setName('login');
170 $message .= '<li>'.$error.'</li>'; 88 $this->post('/login', '\Shaarli\Front\Controller\Visitor\LoginController:login')->setName('processLogin');
171 } 89 $this->get('/picture-wall', '\Shaarli\Front\Controller\Visitor\PictureWallController:index');
172 $message .= '</ul>'; 90 $this->get('/tags/cloud', '\Shaarli\Front\Controller\Visitor\TagCloudController:cloud');
173 91 $this->get('/tags/list', '\Shaarli\Front\Controller\Visitor\TagCloudController:list');
174 header('Content-Type: text/html; charset=utf-8'); 92 $this->get('/daily', '\Shaarli\Front\Controller\Visitor\DailyController:index');
175 echo $message; 93 $this->get('/daily-rss', '\Shaarli\Front\Controller\Visitor\DailyController:rss')->setName('rss');
176 exit; 94 $this->get('/feed/atom', '\Shaarli\Front\Controller\Visitor\FeedController:atom')->setName('atom');
177 } 95 $this->get('/feed/rss', '\Shaarli\Front\Controller\Visitor\FeedController:rss');
178 96 $this->get('/open-search', '\Shaarli\Front\Controller\Visitor\OpenSearchController:index');
179 // Display the installation form if no existing config is found 97
180 install($conf, $sessionManager, $loginManager); 98 $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\Visitor\TagController:addTag');
181} 99 $this->get('/remove-tag/{tag}', '\Shaarli\Front\Controller\Visitor\TagController:removeTag');
182 100 $this->get('/links-per-page', '\Shaarli\Front\Controller\Visitor\PublicSessionFilterController:linksPerPage');
183$loginManager->checkLoginState($_COOKIE, $clientIpId); 101 $this->get('/untagged-only', '\Shaarli\Front\Controller\Visitor\PublicSessionFilterController:untaggedOnly');
184 102})->add('\Shaarli\Front\ShaarliMiddleware');
185/** 103
186 * Adapter function to ensure compatibility with third-party templates 104$app->group('/admin', function () {
187 * 105 $this->get('/logout', '\Shaarli\Front\Controller\Admin\LogoutController:index');
188 * @see https://github.com/shaarli/Shaarli/pull/1086 106 $this->get('/tools', '\Shaarli\Front\Controller\Admin\ToolsController:index');
189 * 107 $this->get('/password', '\Shaarli\Front\Controller\Admin\PasswordController:index');
190 * @return bool true when the user is logged in, false otherwise 108 $this->post('/password', '\Shaarli\Front\Controller\Admin\PasswordController:change');
191 */ 109 $this->get('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:index');
192function isLoggedIn() 110 $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
193{ 111 $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
194 global $loginManager; 112 $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
195 return $loginManager->isLoggedIn(); 113 $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:addShaare');
196} 114 $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayCreateForm');
197 115 $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ManageShaareController:displayEditForm');
198 116 $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ManageShaareController:save');
199// ------------------------------------------------------------------------------------------ 117 $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ManageShaareController:deleteBookmark');
200// Process login form: Check if login/password is correct. 118 $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ManageShaareController:changeVisibility');
201if (isset($_POST['login'])) { 119 $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ManageShaareController:pinBookmark');
202 if (! $loginManager->canLogin($_SERVER)) { 120 $this->patch(
203 die(t('I said: NO. You are banned for the moment. Go away.')); 121 '/shaare/{id:[0-9]+}/update-thumbnail',
204 } 122 '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate'
205 if (isset($_POST['password'])
206 && $sessionManager->checkToken($_POST['token'])
207 && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
208 ) {
209 $loginManager->handleSuccessfulLogin($_SERVER);
210
211 $cookiedir = '';
212 if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
213 // Note: Never forget the trailing slash on the cookie path!
214 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
215 }
216
217 if (!empty($_POST['longlastingsession'])) {
218 // Keep the session cookie even after the browser closes
219 $sessionManager->setStaySignedIn(true);
220 $expirationTime = $sessionManager->extendSession();
221
222 setcookie(
223 $loginManager::$STAY_SIGNED_IN_COOKIE,
224 $loginManager->getStaySignedInToken(),
225 $expirationTime,
226 WEB_PATH
227 );
228 } else {
229 // Standard session expiration (=when browser closes)
230 $expirationTime = 0;
231 }
232
233 // Send cookie with the new expiration date to the browser
234 session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
235 session_regenerate_id(true);
236
237 // Optional redirect after login:
238 if (isset($_GET['post'])) {
239 $uri = '?post='. urlencode($_GET['post']);
240 foreach (array('description', 'source', 'title', 'tags') as $param) {
241 if (!empty($_GET[$param])) {
242 $uri .= '&'.$param.'='.urlencode($_GET[$param]);
243 }
244 }
245 header('Location: '. $uri);
246 exit;
247 }
248
249 if (isset($_GET['edit_link'])) {
250 header('Location: ?edit_link='. escape($_GET['edit_link']));
251 exit;
252 }
253
254 if (isset($_POST['returnurl'])) {
255 // Prevent loops over login screen.
256 if (strpos($_POST['returnurl'], 'do=login') === false) {
257 header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
258 exit;
259 }
260 }
261 header('Location: ?');
262 exit;
263 } else {
264 $loginManager->handleFailedLogin($_SERVER);
265 $redir = '&username='. urlencode($_POST['login']);
266 if (isset($_GET['post'])) {
267 $redir .= '&post=' . urlencode($_GET['post']);
268 foreach (array('description', 'source', 'title', 'tags') as $param) {
269 if (!empty($_GET[$param])) {
270 $redir .= '&' . $param . '=' . urlencode($_GET[$param]);
271 }
272 }
273 }
274 // Redirect to login screen.
275 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
276 exit;
277 }
278}
279
280// ------------------------------------------------------------------------------------------
281// Token management for XSRF protection
282// Token should be used in any form which acts on data (create,update,delete,import...).
283if (!isset($_SESSION['tokens'])) {
284 $_SESSION['tokens']=array(); // Token are attached to the session.
285}
286
287/**
288 * Daily RSS feed: 1 RSS entry per day giving all the links on that day.
289 * Gives the last 7 days (which have links).
290 * This RSS feed cannot be filtered.
291 *
292 * @param ConfigManager $conf Configuration Manager instance
293 * @param LoginManager $loginManager LoginManager instance
294 */
295function showDailyRSS($conf, $loginManager)
296{
297 // Cache system
298 $query = $_SERVER['QUERY_STRING'];
299 $cache = new CachedPage(
300 $conf->get('config.PAGE_CACHE'),
301 page_url($_SERVER),
302 startsWith($query, 'do=dailyrss') && !$loginManager->isLoggedIn()
303 );
304 $cached = $cache->cachedVersion();
305 if (!empty($cached)) {
306 echo $cached;
307 exit;
308 }
309
310 // If cached was not found (or not usable), then read the database and build the response:
311 // Read links from database (and filter private links if used it not logged in).
312 $LINKSDB = new LinkDB(
313 $conf->get('resource.datastore'),
314 $loginManager->isLoggedIn(),
315 $conf->get('privacy.hide_public_links')
316 );
317
318 /* Some Shaarlies may have very few links, so we need to look
319 back in time until we have enough days ($nb_of_days).
320 */
321 $nb_of_days = 7; // We take 7 days.
322 $today = date('Ymd');
323 $days = array();
324
325 foreach ($LINKSDB as $link) {
326 $day = $link['created']->format('Ymd'); // Extract day (without time)
327 if (strcmp($day, $today) < 0) {
328 if (empty($days[$day])) {
329 $days[$day] = array();
330 }
331 $days[$day][] = $link;
332 }
333
334 if (count($days) > $nb_of_days) {
335 break; // Have we collected enough days?
336 }
337 }
338
339 // Build the RSS feed.
340 header('Content-Type: application/rss+xml; charset=utf-8');
341 $pageaddr = escape(index_url($_SERVER));
342 echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">';
343 echo '<channel>';
344 echo '<title>Daily - '. $conf->get('general.title') . '</title>';
345 echo '<link>'. $pageaddr .'</link>';
346 echo '<description>Daily shared links</description>';
347 echo '<language>en-en</language>';
348 echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
349
350 // For each day.
351 foreach ($days as $day => $links) {
352 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
353 $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page.
354
355 // We pre-format some fields for proper output.
356 foreach ($links as &$link) {
357 $link['formatedDescription'] = format_description($link['description']);
358 $link['timestamp'] = $link['created']->getTimestamp();
359 if (is_note($link['url'])) {
360 $link['url'] = index_url($_SERVER) . $link['url']; // make permalink URL absolute
361 }
362 }
363
364 // Then build the HTML for this day:
365 $tpl = new RainTPL;
366 $tpl->assign('title', $conf->get('general.title'));
367 $tpl->assign('daydate', $dayDate->getTimestamp());
368 $tpl->assign('absurl', $absurl);
369 $tpl->assign('links', $links);
370 $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
371 $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
372 $tpl->assign('index_url', $pageaddr);
373 $html = $tpl->draw('dailyrss', true);
374
375 echo $html . PHP_EOL;
376 }
377 echo '</channel></rss><!-- Cached version of '. escape(page_url($_SERVER)) .' -->';
378
379 $cache->cache(ob_get_contents());
380 ob_end_flush();
381 exit;
382}
383
384/**
385 * Show the 'Daily' page.
386 *
387 * @param PageBuilder $pageBuilder Template engine wrapper.
388 * @param LinkDB $LINKSDB LinkDB instance.
389 * @param ConfigManager $conf Configuration Manager instance.
390 * @param PluginManager $pluginManager Plugin Manager instance.
391 * @param LoginManager $loginManager Login Manager instance
392 */
393function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
394{
395 if (isset($_GET['day'])) {
396 $day = $_GET['day'];
397 if ($day === date('Ymd', strtotime('now'))) {
398 $pageBuilder->assign('dayDesc', t('Today'));
399 } elseif ($day === date('Ymd', strtotime('-1 days'))) {
400 $pageBuilder->assign('dayDesc', t('Yesterday'));
401 }
402 } else {
403 $day = date('Ymd', strtotime('now')); // Today, in format YYYYMMDD.
404 $pageBuilder->assign('dayDesc', t('Today'));
405 }
406
407 $days = $LINKSDB->days();
408 $i = array_search($day, $days);
409 if ($i === false && count($days)) {
410 // no links for day, but at least one day with links
411 $i = count($days) - 1;
412 $day = $days[$i];
413 }
414 $previousday = '';
415 $nextday = '';
416
417 if ($i !== false) {
418 if ($i >= 1) {
419 $previousday=$days[$i - 1];
420 }
421 if ($i < count($days) - 1) {
422 $nextday = $days[$i + 1];
423 }
424 }
425 try {
426 $linksToDisplay = $LINKSDB->filterDay($day);
427 } catch (Exception $exc) {
428 error_log($exc);
429 $linksToDisplay = array();
430 }
431
432 // We pre-format some fields for proper output.
433 foreach ($linksToDisplay as $key => $link) {
434 $taglist = explode(' ', $link['tags']);
435 uasort($taglist, 'strcasecmp');
436 $linksToDisplay[$key]['taglist']=$taglist;
437 $linksToDisplay[$key]['formatedDescription'] = format_description($link['description']);
438 $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp();
439 }
440
441 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
442 $data = array(
443 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
444 'linksToDisplay' => $linksToDisplay,
445 'day' => $dayDate->getTimestamp(),
446 'dayDate' => $dayDate,
447 'previousday' => $previousday,
448 'nextday' => $nextday,
449 );
450
451 /* Hook is called before column construction so that plugins don't have
452 to deal with columns. */
453 $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
454
455 /* We need to spread the articles on 3 columns.
456 I did not want to use a JavaScript lib like http://masonry.desandro.com/
457 so I manually spread entries with a simple method: I roughly evaluate the
458 height of a div according to title and description length.
459 */
460 $columns = array(array(), array(), array()); // Entries to display, for each column.
461 $fill = array(0, 0, 0); // Rough estimate of columns fill.
462 foreach ($data['linksToDisplay'] as $key => $link) {
463 // Roughly estimate length of entry (by counting characters)
464 // Title: 30 chars = 1 line. 1 line is 30 pixels height.
465 // Description: 836 characters gives roughly 342 pixel height.
466 // This is not perfect, but it's usually OK.
467 $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836;
468 if ($link['thumbnail']) {
469 $length += 100; // 1 thumbnails roughly takes 100 pixels height.
470 }
471 // Then put in column which is the less filled:
472 $smallest = min($fill); // find smallest value in array.
473 $index = array_search($smallest, $fill); // find index of this smallest value.
474 array_push($columns[$index], $link); // Put entry in this column.
475 $fill[$index] += $length;
476 }
477
478 $data['cols'] = $columns;
479
480 foreach ($data as $key => $value) {
481 $pageBuilder->assign($key, $value);
482 }
483
484 $pageBuilder->assign('pagetitle', t('Daily') .' - '. $conf->get('general.title', 'Shaarli'));
485 $pageBuilder->renderPage('daily');
486 exit;
487}
488
489/**
490 * Renders the linklist
491 *
492 * @param pageBuilder $PAGE pageBuilder instance.
493 * @param LinkDB $LINKSDB LinkDB instance.
494 * @param ConfigManager $conf Configuration Manager instance.
495 * @param PluginManager $pluginManager Plugin Manager instance.
496 */
497function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
498{
499 buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
500 $PAGE->renderPage('linklist');
501}
502
503/**
504 * Render HTML page (according to URL parameters and user rights)
505 *
506 * @param ConfigManager $conf Configuration Manager instance.
507 * @param PluginManager $pluginManager Plugin Manager instance,
508 * @param LinkDB $LINKSDB
509 * @param History $history instance
510 * @param SessionManager $sessionManager SessionManager instance
511 * @param LoginManager $loginManager LoginManager instance
512 */
513function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $loginManager)
514{
515 $updater = new Updater(
516 read_updates_file($conf->get('resource.updates')),
517 $LINKSDB,
518 $conf,
519 $loginManager->isLoggedIn(),
520 $_SESSION
521 );
522 try {
523 $newUpdates = $updater->update();
524 if (! empty($newUpdates)) {
525 write_updates_file(
526 $conf->get('resource.updates'),
527 $updater->getDoneUpdates()
528 );
529 }
530 } catch (Exception $e) {
531 die($e->getMessage());
532 }
533
534 $PAGE = new PageBuilder($conf, $_SESSION, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
535 $PAGE->assign('linkcount', count($LINKSDB));
536 $PAGE->assign('privateLinkcount', count_private($LINKSDB));
537 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
538
539 // Determine which page will be rendered.
540 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
541 $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
542
543 if (// if the user isn't logged in
544 !$loginManager->isLoggedIn() &&
545 // and Shaarli doesn't have public content...
546 $conf->get('privacy.hide_public_links') &&
547 // and is configured to enforce the login
548 $conf->get('privacy.force_login') &&
549 // and the current page isn't already the login page
550 $targetPage !== Router::$PAGE_LOGIN &&
551 // and the user is not requesting a feed (which would lead to a different content-type as expected)
552 $targetPage !== Router::$PAGE_FEED_ATOM &&
553 $targetPage !== Router::$PAGE_FEED_RSS
554 ) {
555 // force current page to be the login page
556 $targetPage = Router::$PAGE_LOGIN;
557 }
558
559 // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
560 // Then assign generated data to RainTPL.
561 $common_hooks = array(
562 'includes',
563 'header',
564 'footer',
565 );
566
567 foreach ($common_hooks as $name) {
568 $plugin_data = array();
569 $pluginManager->executeHooks(
570 'render_' . $name,
571 $plugin_data,
572 array(
573 'target' => $targetPage,
574 'loggedin' => $loginManager->isLoggedIn()
575 )
576 );
577 $PAGE->assign('plugins_' . $name, $plugin_data);
578 }
579
580 // -------- Display login form.
581 if ($targetPage == Router::$PAGE_LOGIN) {
582 if ($conf->get('security.open_shaarli')) {
583 header('Location: ?');
584 exit;
585 } // No need to login for open Shaarli
586 if (isset($_GET['username'])) {
587 $PAGE->assign('username', escape($_GET['username']));
588 }
589 $PAGE->assign('returnurl', (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
590 // add default state of the 'remember me' checkbox
591 $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
592 $PAGE->assign('user_can_login', $loginManager->canLogin($_SERVER));
593 $PAGE->assign('pagetitle', t('Login') .' - '. $conf->get('general.title', 'Shaarli'));
594 $PAGE->renderPage('loginform');
595 exit;
596 }
597 // -------- User wants to logout.
598 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) {
599 invalidateCaches($conf->get('resource.page_cache'));
600 $sessionManager->logout();
601 setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
602 header('Location: ?');
603 exit;
604 }
605
606 // -------- Picture wall
607 if ($targetPage == Router::$PAGE_PICWALL) {
608 $PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
609 if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
610 $PAGE->assign('linksToDisplay', []);
611 $PAGE->renderPage('picwall');
612 exit;
613 }
614
615 // Optionally filter the results:
616 $links = $LINKSDB->filterSearch($_GET);
617 $linksToDisplay = array();
618
619 // Get only links which have a thumbnail.
620 // Note: we do not retrieve thumbnails here, the request is too heavy.
621 foreach ($links as $key => $link) {
622 if (isset($link['thumbnail']) && $link['thumbnail'] !== false) {
623 $linksToDisplay[] = $link; // Add to array.
624 }
625 }
626
627 $data = array(
628 'linksToDisplay' => $linksToDisplay,
629 );
630 $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn()));
631
632 foreach ($data as $key => $value) {
633 $PAGE->assign($key, $value);
634 }
635
636
637 $PAGE->renderPage('picwall');
638 exit;
639 }
640
641 // -------- Tag cloud
642 if ($targetPage == Router::$PAGE_TAGCLOUD) {
643 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
644 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
645 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
646
647 // We sort tags alphabetically, then choose a font size according to count.
648 // First, find max value.
649 $maxcount = 0;
650 foreach ($tags as $value) {
651 $maxcount = max($maxcount, $value);
652 }
653
654 alphabetical_sort($tags, false, true);
655
656 $tagList = array();
657 foreach ($tags as $key => $value) {
658 if (in_array($key, $filteringTags)) {
659 continue;
660 }
661 // Tag font size scaling:
662 // default 15 and 30 logarithm bases affect scaling,
663 // 22 and 6 are arbitrary font sizes for max and min sizes.
664 $size = log($value, 15) / log($maxcount, 30) * 2.2 + 0.8;
665 $tagList[$key] = array(
666 'count' => $value,
667 'size' => number_format($size, 2, '.', ''),
668 );
669 }
670
671 $searchTags = implode(' ', escape($filteringTags));
672 $data = array(
673 'search_tags' => $searchTags,
674 'tags' => $tagList,
675 );
676 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
677
678 foreach ($data as $key => $value) {
679 $PAGE->assign($key, $value);
680 }
681
682 $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
683 $PAGE->assign('pagetitle', $searchTags. t('Tag cloud') .' - '. $conf->get('general.title', 'Shaarli'));
684 $PAGE->renderPage('tag.cloud');
685 exit;
686 }
687
688 // -------- Tag list
689 if ($targetPage == Router::$PAGE_TAGLIST) {
690 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
691 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
692 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
693 foreach ($filteringTags as $tag) {
694 if (array_key_exists($tag, $tags)) {
695 unset($tags[$tag]);
696 }
697 }
698
699 if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
700 alphabetical_sort($tags, false, true);
701 }
702
703 $searchTags = implode(' ', escape($filteringTags));
704 $data = [
705 'search_tags' => $searchTags,
706 'tags' => $tags,
707 ];
708 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
709
710 foreach ($data as $key => $value) {
711 $PAGE->assign($key, $value);
712 }
713
714 $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
715 $PAGE->assign('pagetitle', $searchTags . t('Tag list') .' - '. $conf->get('general.title', 'Shaarli'));
716 $PAGE->renderPage('tag.list');
717 exit;
718 }
719
720 // Daily page.
721 if ($targetPage == Router::$PAGE_DAILY) {
722 showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
723 }
724
725 // ATOM and RSS feed.
726 if ($targetPage == Router::$PAGE_FEED_ATOM || $targetPage == Router::$PAGE_FEED_RSS) {
727 $feedType = $targetPage == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
728 header('Content-Type: application/'. $feedType .'+xml; charset=utf-8');
729
730 // Cache system
731 $query = $_SERVER['QUERY_STRING'];
732 $cache = new CachedPage(
733 $conf->get('resource.page_cache'),
734 page_url($_SERVER),
735 startsWith($query, 'do='. $targetPage) && !$loginManager->isLoggedIn()
736 );
737 $cached = $cache->cachedVersion();
738 if (!empty($cached)) {
739 echo $cached;
740 exit;
741 }
742
743 // Generate data.
744 $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn());
745 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
746 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
747 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
748 $data = $feedGenerator->buildData();
749
750 // Process plugin hook.
751 $pluginManager->executeHooks('render_feed', $data, array(
752 'loggedin' => $loginManager->isLoggedIn(),
753 'target' => $targetPage,
754 ));
755
756 // Render the template.
757 $PAGE->assignAll($data);
758 $PAGE->renderPage('feed.'. $feedType);
759 $cache->cache(ob_get_contents());
760 ob_end_flush();
761 exit;
762 }
763
764 // Display opensearch plugin (XML)
765 if ($targetPage == Router::$PAGE_OPENSEARCH) {
766 header('Content-Type: application/xml; charset=utf-8');
767 $PAGE->assign('serverurl', index_url($_SERVER));
768 $PAGE->renderPage('opensearch');
769 exit;
770 }
771
772 // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...)
773 if (isset($_GET['addtag'])) {
774 // Get previous URL (http_referer) and add the tag to the searchtags parameters in query.
775 if (empty($_SERVER['HTTP_REFERER'])) {
776 // In case browser does not send HTTP_REFERER
777 header('Location: ?searchtags='.urlencode($_GET['addtag']));
778 exit;
779 }
780 parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
781
782 // Prevent redirection loop
783 if (isset($params['addtag'])) {
784 unset($params['addtag']);
785 }
786
787 // Check if this tag is already in the search query and ignore it if it is.
788 // Each tag is always separated by a space
789 if (isset($params['searchtags'])) {
790 $current_tags = explode(' ', $params['searchtags']);
791 } else {
792 $current_tags = array();
793 }
794 $addtag = true;
795 foreach ($current_tags as $value) {
796 if ($value === $_GET['addtag']) {
797 $addtag = false;
798 break;
799 }
800 }
801 // Append the tag if necessary
802 if (empty($params['searchtags'])) {
803 $params['searchtags'] = trim($_GET['addtag']);
804 } elseif ($addtag) {
805 $params['searchtags'] = trim($params['searchtags']).' '.trim($_GET['addtag']);
806 }
807
808 // We also remove page (keeping the same page has no sense, since the
809 // results are different)
810 unset($params['page']);
811
812 header('Location: ?'.http_build_query($params));
813 exit;
814 }
815
816 // -------- User clicks on a tag in result count: Remove the tag from the list of searched tags (searchtags=...)
817 if (isset($_GET['removetag'])) {
818 // Get previous URL (http_referer) and remove the tag from the searchtags parameters in query.
819 if (empty($_SERVER['HTTP_REFERER'])) {
820 header('Location: ?');
821 exit;
822 }
823
824 // In case browser does not send HTTP_REFERER
825 parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
826
827 // Prevent redirection loop
828 if (isset($params['removetag'])) {
829 unset($params['removetag']);
830 }
831
832 if (isset($params['searchtags'])) {
833 $tags = explode(' ', $params['searchtags']);
834 // Remove value from array $tags.
835 $tags = array_diff($tags, array($_GET['removetag']));
836 $params['searchtags'] = implode(' ', $tags);
837
838 if (empty($params['searchtags'])) {
839 unset($params['searchtags']);
840 }
841
842 // We also remove page (keeping the same page has no sense, since
843 // the results are different)
844 unset($params['page']);
845 }
846 header('Location: ?'.http_build_query($params));
847 exit;
848 }
849
850 // -------- User wants to change the number of links per page (linksperpage=...)
851 if (isset($_GET['linksperpage'])) {
852 if (is_numeric($_GET['linksperpage'])) {
853 $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
854 }
855
856 if (! empty($_SERVER['HTTP_REFERER'])) {
857 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'));
858 } else {
859 $location = '?';
860 }
861 header('Location: '. $location);
862 exit;
863 }
864
865 // -------- User wants to see only private links (toggle)
866 if (isset($_GET['visibility'])) {
867 if ($_GET['visibility'] === 'private') {
868 // Visibility not set or not already private, set private, otherwise reset it
869 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
870 // See only private links
871 $_SESSION['visibility'] = 'private';
872 } else {
873 unset($_SESSION['visibility']);
874 }
875 } elseif ($_GET['visibility'] === 'public') {
876 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
877 // See only public links
878 $_SESSION['visibility'] = 'public';
879 } else {
880 unset($_SESSION['visibility']);
881 }
882 }
883
884 if (! empty($_SERVER['HTTP_REFERER'])) {
885 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('visibility'));
886 } else {
887 $location = '?';
888 }
889 header('Location: '. $location);
890 exit;
891 }
892
893 // -------- User wants to see only untagged links (toggle)
894 if (isset($_GET['untaggedonly'])) {
895 $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
896
897 if (! empty($_SERVER['HTTP_REFERER'])) {
898 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
899 } else {
900 $location = '?';
901 }
902 header('Location: '. $location);
903 exit;
904 }
905
906 // -------- Handle other actions allowed for non-logged in users:
907 if (!$loginManager->isLoggedIn()) {
908 // User tries to post new link but is not logged in:
909 // Show login screen, then redirect to ?post=...
910 if (isset($_GET['post'])) {
911 header( // Redirect to login page, then back to post link.
912 'Location: ?do=login&post='.urlencode($_GET['post']).
913 (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
914 (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
915 (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
916 (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
917 );
918 exit;
919 }
920
921 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
922 if (isset($_GET['edit_link'])) {
923 header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
924 exit;
925 }
926
927 exit; // Never remove this one! All operations below are reserved for logged in user.
928 }
929
930 // -------- All other functions are reserved for the registered user:
931
932 // -------- Display the Tools menu if requested (import/export/bookmarklet...)
933 if ($targetPage == Router::$PAGE_TOOLS) {
934 $data = [
935 'pageabsaddr' => index_url($_SERVER),
936 'sslenabled' => is_https($_SERVER),
937 ];
938 $pluginManager->executeHooks('render_tools', $data);
939
940 foreach ($data as $key => $value) {
941 $PAGE->assign($key, $value);
942 }
943
944 $PAGE->assign('pagetitle', t('Tools') .' - '. $conf->get('general.title', 'Shaarli'));
945 $PAGE->renderPage('tools');
946 exit;
947 }
948
949 // -------- User wants to change his/her password.
950 if ($targetPage == Router::$PAGE_CHANGEPASSWORD) {
951 if ($conf->get('security.open_shaarli')) {
952 die(t('You are not supposed to change a password on an Open Shaarli.'));
953 }
954
955 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) {
956 if (!$sessionManager->checkToken($_POST['token'])) {
957 die(t('Wrong token.')); // Go away!
958 }
959
960 // Make sure old password is correct.
961 $oldhash = sha1(
962 $_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')
963 );
964 if ($oldhash != $conf->get('credentials.hash')) {
965 echo '<script>alert("'
966 . t('The old password is not correct.')
967 .'");document.location=\'?do=changepasswd\';</script>';
968 exit;
969 }
970 // Save new password
971 // Salt renders rainbow-tables attacks useless.
972 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
973 $conf->set(
974 'credentials.hash',
975 sha1(
976 $_POST['setpassword']
977 . $conf->get('credentials.login')
978 . $conf->get('credentials.salt')
979 )
980 );
981 try {
982 $conf->write($loginManager->isLoggedIn());
983 } catch (Exception $e) {
984 error_log(
985 'ERROR while writing config file after changing password.' . PHP_EOL .
986 $e->getMessage()
987 );
988
989 // TODO: do not handle exceptions/errors in JS.
990 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
991 exit;
992 }
993 echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
994 exit;
995 } else {
996 // show the change password form.
997 $PAGE->assign('pagetitle', t('Change password') .' - '. $conf->get('general.title', 'Shaarli'));
998 $PAGE->renderPage('changepassword');
999 exit;
1000 }
1001 }
1002
1003 // -------- User wants to change configuration
1004 if ($targetPage == Router::$PAGE_CONFIGURE) {
1005 if (!empty($_POST['title'])) {
1006 if (!$sessionManager->checkToken($_POST['token'])) {
1007 die(t('Wrong token.')); // Go away!
1008 }
1009 $tz = 'UTC';
1010 if (!empty($_POST['continent']) && !empty($_POST['city'])
1011 && isTimeZoneValid($_POST['continent'], $_POST['city'])
1012 ) {
1013 $tz = $_POST['continent'] . '/' . $_POST['city'];
1014 }
1015 $conf->set('general.timezone', $tz);
1016 $conf->set('general.title', escape($_POST['title']));
1017 $conf->set('general.header_link', escape($_POST['titleLink']));
1018 $conf->set('general.retrieve_description', !empty($_POST['retrieveDescription']));
1019 $conf->set('resource.theme', escape($_POST['theme']));
1020 $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
1021 $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
1022 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
1023 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1024 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
1025 $conf->set('api.enabled', !empty($_POST['enableApi']));
1026 $conf->set('api.secret', escape($_POST['apiSecret']));
1027 $conf->set('translation.language', escape($_POST['language']));
1028
1029 $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
1030 if ($thumbnailsMode !== Thumbnailer::MODE_NONE
1031 && $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
1032 ) {
1033 $_SESSION['warnings'][] = t(
1034 'You have enabled or changed thumbnails mode. '
1035 .'<a href="?do=thumbs_update">Please synchronize them</a>.'
1036 );
1037 }
1038 $conf->set('thumbnails.mode', $thumbnailsMode);
1039
1040 try {
1041 $conf->write($loginManager->isLoggedIn());
1042 $history->updateSettings();
1043 invalidateCaches($conf->get('resource.page_cache'));
1044 } catch (Exception $e) {
1045 error_log(
1046 'ERROR while writing config file after configuration update.' . PHP_EOL .
1047 $e->getMessage()
1048 );
1049
1050 // TODO: do not handle exceptions/errors in JS.
1051 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
1052 exit;
1053 }
1054 echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
1055 exit;
1056 } else {
1057 // Show the configuration form.
1058 $PAGE->assign('title', $conf->get('general.title'));
1059 $PAGE->assign('theme', $conf->get('resource.theme'));
1060 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
1061 list($continents, $cities) = generateTimeZoneData(
1062 timezone_identifiers_list(),
1063 $conf->get('general.timezone')
1064 );
1065 $PAGE->assign('continents', $continents);
1066 $PAGE->assign('cities', $cities);
1067 $PAGE->assign('retrieve_description', $conf->get('general.retrieve_description'));
1068 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
1069 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
1070 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
1071 $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true));
1072 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
1073 $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
1074 $PAGE->assign('api_secret', $conf->get('api.secret'));
1075 $PAGE->assign('languages', Languages::getAvailableLanguages());
1076 $PAGE->assign('gd_enabled', extension_loaded('gd'));
1077 $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
1078 $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
1079 $PAGE->renderPage('configure');
1080 exit;
1081 }
1082 }
1083
1084 // -------- User wants to rename a tag or delete it
1085 if ($targetPage == Router::$PAGE_CHANGETAG) {
1086 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
1087 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
1088 $PAGE->assign('pagetitle', t('Manage tags') .' - '. $conf->get('general.title', 'Shaarli'));
1089 $PAGE->renderPage('changetag');
1090 exit;
1091 }
1092
1093 if (!$sessionManager->checkToken($_POST['token'])) {
1094 die(t('Wrong token.'));
1095 }
1096
1097 $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
1098 $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), $toTag);
1099 $LINKSDB->save($conf->get('resource.page_cache'));
1100 foreach ($alteredLinks as $link) {
1101 $history->updateLink($link);
1102 }
1103 $delete = empty($_POST['totag']);
1104 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
1105 $count = count($alteredLinks);
1106 $alert = $delete
1107 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
1108 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
1109 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
1110 exit;
1111 }
1112
1113 // -------- User wants to add a link without using the bookmarklet: Show form.
1114 if ($targetPage == Router::$PAGE_ADDLINK) {
1115 $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli'));
1116 $PAGE->renderPage('addlink');
1117 exit;
1118 }
1119
1120 // -------- User clicked the "Save" button when editing a link: Save link to database.
1121 if (isset($_POST['save_edit'])) {
1122 // Go away!
1123 if (! $sessionManager->checkToken($_POST['token'])) {
1124 die(t('Wrong token.'));
1125 }
1126
1127 // lf_id should only be present if the link exists.
1128 $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
1129 $link['id'] = $id;
1130 // Linkdate is kept here to:
1131 // - use the same permalink for notes as they're displayed when creating them
1132 // - let users hack creation date of their posts
1133 // See: https://shaarli.readthedocs.io/en/master/guides/various-hacks/#changing-the-timestamp-for-a-shaare
1134 $linkdate = escape($_POST['lf_linkdate']);
1135 $link['created'] = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
1136 if (isset($LINKSDB[$id])) {
1137 // Edit
1138 $link['updated'] = new DateTime();
1139 $link['shorturl'] = $LINKSDB[$id]['shorturl'];
1140 $link['sticky'] = isset($LINKSDB[$id]['sticky']) ? $LINKSDB[$id]['sticky'] : false;
1141 $new = false;
1142 } else {
1143 // New link
1144 $link['updated'] = null;
1145 $link['shorturl'] = link_small_hash($link['created'], $id);
1146 $link['sticky'] = false;
1147 $new = true;
1148 }
1149
1150 // Remove multiple spaces.
1151 $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags']));
1152 // Remove first '-' char in tags.
1153 $tags = preg_replace('/(^| )\-/', '$1', $tags);
1154 // Remove duplicates.
1155 $tags = implode(' ', array_unique(explode(' ', $tags)));
1156
1157 if (empty(trim($_POST['lf_url']))) {
1158 $_POST['lf_url'] = '?' . smallHash($linkdate . $id);
1159 }
1160 $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols'));
1161
1162 $link = array_merge($link, [
1163 'title' => trim($_POST['lf_title']),
1164 'url' => $url,
1165 'description' => $_POST['lf_description'],
1166 'private' => (isset($_POST['lf_private']) ? 1 : 0),
1167 'tags' => str_replace(',', ' ', $tags),
1168 ]);
1169
1170 // If title is empty, use the URL as title.
1171 if ($link['title'] == '') {
1172 $link['title'] = $link['url'];
1173 }
1174
1175 if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
1176 && ! is_note($link['url'])
1177 ) {
1178 $thumbnailer = new Thumbnailer($conf);
1179 $link['thumbnail'] = $thumbnailer->get($url);
1180 }
1181
1182 $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
1183
1184 $pluginManager->executeHooks('save_link', $link);
1185
1186 $LINKSDB[$id] = $link;
1187 $LINKSDB->save($conf->get('resource.page_cache'));
1188 if ($new) {
1189 $history->addLink($link);
1190 } else {
1191 $history->updateLink($link);
1192 }
1193
1194 // If we are called from the bookmarklet, we must close the popup:
1195 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
1196 echo '<script>self.close();</script>';
1197 exit;
1198 }
1199
1200 $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
1201 $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
1202 // Scroll to the link which has been edited.
1203 $location .= '#' . $link['shorturl'];
1204 // After saving the link, redirect to the page the user was on.
1205 header('Location: '. $location);
1206 exit;
1207 }
1208
1209 // -------- User clicked the "Cancel" button when editing a link.
1210 if (isset($_POST['cancel_edit'])) {
1211 $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false;
1212 if (! isset($LINKSDB[$id])) {
1213 header('Location: ?');
1214 }
1215 // If we are called from the bookmarklet, we must close the popup:
1216 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
1217 echo '<script>self.close();</script>';
1218 exit;
1219 }
1220 $link = $LINKSDB[$id];
1221 $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
1222 // Scroll to the link which has been edited.
1223 $returnurl .= '#'. $link['shorturl'];
1224 $returnurl = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
1225 header('Location: '.$returnurl); // After canceling, redirect to the page the user was on.
1226 exit;
1227 }
1228
1229 // -------- User clicked the "Delete" button when editing a link: Delete link from database.
1230 if ($targetPage == Router::$PAGE_DELETELINK) {
1231 if (! $sessionManager->checkToken($_GET['token'])) {
1232 die(t('Wrong token.'));
1233 }
1234
1235 $ids = trim($_GET['lf_linkdate']);
1236 if (strpos($ids, ' ') !== false) {
1237 // multiple, space-separated ids provided
1238 $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
1239 } else {
1240 // only a single id provided
1241 $ids = [$ids];
1242 }
1243 // assert at least one id is given
1244 if (!count($ids)) {
1245 die('no id provided');
1246 }
1247 foreach ($ids as $id) {
1248 $id = (int) escape($id);
1249 $link = $LINKSDB[$id];
1250 $pluginManager->executeHooks('delete_link', $link);
1251 $history->deleteLink($link);
1252 unset($LINKSDB[$id]);
1253 }
1254 $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
1255
1256 // If we are called from the bookmarklet, we must close the popup:
1257 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
1258 echo '<script>self.close();</script>';
1259 exit;
1260 }
1261
1262 $location = '?';
1263 if (isset($_SERVER['HTTP_REFERER'])) {
1264 // Don't redirect to where we were previously if it was a permalink or an edit_link, because it would 404.
1265 $location = generateLocation(
1266 $_SERVER['HTTP_REFERER'],
1267 $_SERVER['HTTP_HOST'],
1268 ['delete_link', 'edit_link', $link['shorturl']]
1269 );
1270 }
1271
1272 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
1273 exit;
1274 }
1275
1276 // -------- User clicked either "Set public" or "Set private" bulk operation
1277 if ($targetPage == Router::$PAGE_CHANGE_VISIBILITY) {
1278 if (! $sessionManager->checkToken($_GET['token'])) {
1279 die(t('Wrong token.'));
1280 }
1281
1282 $ids = trim($_GET['ids']);
1283 if (strpos($ids, ' ') !== false) {
1284 // multiple, space-separated ids provided
1285 $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
1286 } else {
1287 // only a single id provided
1288 $ids = [$ids];
1289 }
1290
1291 // assert at least one id is given
1292 if (!count($ids)) {
1293 die('no id provided');
1294 }
1295 // assert that the visibility is valid
1296 if (!isset($_GET['newVisibility']) || !in_array($_GET['newVisibility'], ['public', 'private'])) {
1297 die('invalid visibility');
1298 } else {
1299 $private = $_GET['newVisibility'] === 'private';
1300 }
1301 foreach ($ids as $id) {
1302 $id = (int) escape($id);
1303 $link = $LINKSDB[$id];
1304 $link['private'] = $private;
1305 $pluginManager->executeHooks('save_link', $link);
1306 $LINKSDB[$id] = $link;
1307 }
1308 $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
1309
1310 $location = '?';
1311 if (isset($_SERVER['HTTP_REFERER'])) {
1312 $location = generateLocation(
1313 $_SERVER['HTTP_REFERER'],
1314 $_SERVER['HTTP_HOST']
1315 );
1316 }
1317 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
1318 exit;
1319 }
1320
1321 // -------- User clicked the "EDIT" button on a link: Display link edit form.
1322 if (isset($_GET['edit_link'])) {
1323 $id = (int) escape($_GET['edit_link']);
1324 $link = $LINKSDB[$id]; // Read database
1325 if (!$link) {
1326 header('Location: ?');
1327 exit;
1328 } // Link not found in database.
1329 $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
1330 $data = array(
1331 'link' => $link,
1332 'link_is_new' => false,
1333 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1334 'tags' => $LINKSDB->linksCountPerTag(),
1335 );
1336 $pluginManager->executeHooks('render_editlink', $data);
1337
1338 foreach ($data as $key => $value) {
1339 $PAGE->assign($key, $value);
1340 }
1341
1342 $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1343 $PAGE->renderPage('editlink');
1344 exit;
1345 }
1346
1347 // -------- User want to post a new link: Display link edit form.
1348 if (isset($_GET['post'])) {
1349 $url = cleanup_url($_GET['post']);
1350
1351 $link_is_new = false;
1352 // Check if URL is not already in database (in this case, we will edit the existing link)
1353 $link = $LINKSDB->getLinkFromUrl($url);
1354 if (! $link) {
1355 $link_is_new = true;
1356 $linkdate = strval(date(LinkDB::LINK_DATE_FORMAT));
1357 // Get title if it was provided in URL (by the bookmarklet).
1358 $title = empty($_GET['title']) ? '' : escape($_GET['title']);
1359 // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
1360 $description = empty($_GET['description']) ? '' : escape($_GET['description']);
1361 $tags = empty($_GET['tags']) ? '' : escape($_GET['tags']);
1362 $private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0;
1363
1364 // If this is an HTTP(S) link, we try go get the page to extract
1365 // the title (otherwise we will to straight to the edit form.)
1366 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
1367 $retrieveDescription = $conf->get('general.retrieve_description');
1368 // Short timeout to keep the application responsive
1369 // The callback will fill $charset and $title with data from the downloaded page.
1370 get_http_response(
1371 $url,
1372 $conf->get('general.download_timeout', 30),
1373 $conf->get('general.download_max_size', 4194304),
1374 get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription)
1375 );
1376 if (! empty($title) && strtolower($charset) != 'utf-8') {
1377 $title = mb_convert_encoding($title, 'utf-8', $charset);
1378 }
1379 }
1380
1381 if ($url == '') {
1382 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
1383 $title = $conf->get('general.default_note_title', t('Note: '));
1384 }
1385 $url = escape($url);
1386 $title = escape($title);
1387
1388 $link = array(
1389 'linkdate' => $linkdate,
1390 'title' => $title,
1391 'url' => $url,
1392 'description' => $description,
1393 'tags' => $tags,
1394 'private' => $private,
1395 );
1396 } else {
1397 $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
1398 }
1399
1400 $data = array(
1401 'link' => $link,
1402 'link_is_new' => $link_is_new,
1403 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1404 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
1405 'tags' => $LINKSDB->linksCountPerTag(),
1406 'default_private_links' => $conf->get('privacy.default_private_links', false),
1407 );
1408 $pluginManager->executeHooks('render_editlink', $data);
1409
1410 foreach ($data as $key => $value) {
1411 $PAGE->assign($key, $value);
1412 }
1413
1414 $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1415 $PAGE->renderPage('editlink');
1416 exit;
1417 }
1418
1419 if ($targetPage == Router::$PAGE_PINLINK) {
1420 if (! isset($_GET['id']) || empty($LINKSDB[$_GET['id']])) {
1421 // FIXME! Use a proper error system.
1422 $msg = t('Invalid link ID provided');
1423 echo '<script>alert("'. $msg .'");document.location=\''. index_url($_SERVER) .'\';</script>';
1424 exit;
1425 }
1426 if (! $sessionManager->checkToken($_GET['token'])) {
1427 die('Wrong token.');
1428 }
1429
1430 $link = $LINKSDB[$_GET['id']];
1431 $link['sticky'] = ! $link['sticky'];
1432 $LINKSDB[(int) $_GET['id']] = $link;
1433 $LINKSDB->save($conf->get('resource.page_cache'));
1434 header('Location: '.index_url($_SERVER));
1435 exit;
1436 }
1437
1438 if ($targetPage == Router::$PAGE_EXPORT) {
1439 // Export links 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 $PAGE->assign(
1457 'links',
1458 NetscapeBookmarkUtils::filterAndFormat(
1459 $LINKSDB,
1460 $selection,
1461 $prependNoteUrl,
1462 index_url($_SERVER)
1463 )
1464 );
1465 } catch (Exception $exc) {
1466 header('Content-Type: text/plain; charset=utf-8');
1467 echo $exc->getMessage();
1468 exit;
1469 }
1470 $now = new DateTime();
1471 header('Content-Type: text/html; charset=utf-8');
1472 header(
1473 'Content-disposition: attachment; filename=bookmarks_'
1474 .$selection.'_'.$now->format(LinkDB::LINK_DATE_FORMAT).'.html'
1475 );
1476 $PAGE->assign('date', $now->format(DateTime::RFC822));
1477 $PAGE->assign('eol', PHP_EOL);
1478 $PAGE->assign('selection', $selection);
1479 $PAGE->renderPage('export.bookmarks');
1480 exit;
1481 }
1482
1483 if ($targetPage == Router::$PAGE_IMPORT) {
1484 // Upload a Netscape bookmark dump to import its contents
1485
1486 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1487 // Show import dialog
1488 $PAGE->assign(
1489 'maxfilesize',
1490 get_max_upload_size(
1491 ini_get('post_max_size'),
1492 ini_get('upload_max_filesize'),
1493 false
1494 )
1495 );
1496 $PAGE->assign(
1497 'maxfilesizeHuman',
1498 get_max_upload_size(
1499 ini_get('post_max_size'),
1500 ini_get('upload_max_filesize'),
1501 true
1502 )
1503 );
1504 $PAGE->assign('pagetitle', t('Import') .' - '. $conf->get('general.title', 'Shaarli'));
1505 $PAGE->renderPage('import');
1506 exit;
1507 }
1508
1509 // Import bookmarks from an uploaded file
1510 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1511 // The file is too big or some form field may be missing.
1512 $msg = sprintf(
1513 t(
1514 'The file you are trying to upload is probably bigger than what this webserver can accept'
1515 .' (%s). Please upload in smaller chunks.'
1516 ),
1517 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
1518 );
1519 echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
1520 exit;
1521 }
1522 if (! $sessionManager->checkToken($_POST['token'])) {
1523 die('Wrong token.');
1524 }
1525 $status = NetscapeBookmarkUtils::import(
1526 $_POST,
1527 $_FILES,
1528 $LINKSDB,
1529 $conf,
1530 $history
1531 );
1532 echo '<script>alert("'.$status.'");document.location=\'?do='
1533 .Router::$PAGE_IMPORT .'\';</script>';
1534 exit;
1535 }
1536
1537 // Plugin administration page
1538 if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
1539 $pluginMeta = $pluginManager->getPluginsMeta();
1540
1541 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
1542 $enabledPlugins = array_filter($pluginMeta, function ($v) {
1543 return $v['order'] !== false;
1544 });
1545 // Load parameters.
1546 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $conf->get('plugins', array()));
1547 uasort(
1548 $enabledPlugins,
1549 function ($a, $b) {
1550 return $a['order'] - $b['order'];
1551 }
1552 );
1553 $disabledPlugins = array_filter($pluginMeta, function ($v) {
1554 return $v['order'] === false;
1555 });
1556
1557 $PAGE->assign('enabledPlugins', $enabledPlugins);
1558 $PAGE->assign('disabledPlugins', $disabledPlugins);
1559 $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli'));
1560 $PAGE->renderPage('pluginsadmin');
1561 exit;
1562 }
1563
1564 // Plugin administration form action
1565 if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
1566 try {
1567 if (isset($_POST['parameters_form'])) {
1568 $pluginManager->executeHooks('save_plugin_parameters', $_POST);
1569 unset($_POST['parameters_form']);
1570 foreach ($_POST as $param => $value) {
1571 $conf->set('plugins.'. $param, escape($value));
1572 }
1573 } else {
1574 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
1575 }
1576 $conf->write($loginManager->isLoggedIn());
1577 $history->updateSettings();
1578 } catch (Exception $e) {
1579 error_log(
1580 'ERROR while saving plugin configuration:.' . PHP_EOL .
1581 $e->getMessage()
1582 );
1583
1584 // TODO: do not handle exceptions/errors in JS.
1585 echo '<script>alert("'
1586 . $e->getMessage()
1587 .'");document.location=\'?do='
1588 . Router::$PAGE_PLUGINSADMIN
1589 .'\';</script>';
1590 exit;
1591 }
1592 header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
1593 exit;
1594 }
1595
1596 // Get a fresh token
1597 if ($targetPage == Router::$GET_TOKEN) {
1598 header('Content-Type:text/plain');
1599 echo $sessionManager->generateToken($conf);
1600 exit;
1601 }
1602
1603 // -------- Thumbnails Update
1604 if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
1605 $ids = [];
1606 foreach ($LINKSDB as $link) {
1607 // A note or not HTTP(S)
1608 if (is_note($link['url']) || ! startsWith(strtolower($link['url']), 'http')) {
1609 continue;
1610 }
1611 $ids[] = $link['id'];
1612 }
1613 $PAGE->assign('ids', $ids);
1614 $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
1615 $PAGE->renderPage('thumbnails');
1616 exit;
1617 }
1618
1619 // -------- Single Thumbnail Update
1620 if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
1621 if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
1622 http_response_code(400);
1623 exit;
1624 }
1625 $id = (int) $_POST['id'];
1626 if (empty($LINKSDB[$id])) {
1627 http_response_code(404);
1628 exit;
1629 }
1630 $thumbnailer = new Thumbnailer($conf);
1631 $link = $LINKSDB[$id];
1632 $link['thumbnail'] = $thumbnailer->get($link['url']);
1633 $LINKSDB[$id] = $link;
1634 $LINKSDB->save($conf->get('resource.page_cache'));
1635
1636 echo json_encode($link);
1637 exit;
1638 }
1639
1640 // -------- Otherwise, simply display search form and links:
1641 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
1642 exit;
1643}
1644
1645/**
1646 * Template for the list of links (<div id="linklist">)
1647 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1648 *
1649 * @param pageBuilder $PAGE pageBuilder instance.
1650 * @param LinkDB $LINKSDB LinkDB instance.
1651 * @param ConfigManager $conf Configuration Manager instance.
1652 * @param PluginManager $pluginManager Plugin Manager instance.
1653 * @param LoginManager $loginManager LoginManager instance
1654 */
1655function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1656{
1657 // Used in templates
1658 if (isset($_GET['searchtags'])) {
1659 if (! empty($_GET['searchtags'])) {
1660 $searchtags = escape(normalize_spaces($_GET['searchtags']));
1661 } else {
1662 $searchtags = false;
1663 }
1664 } else {
1665 $searchtags = '';
1666 }
1667 $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : '';
1668
1669 // Smallhash filter
1670 if (! empty($_SERVER['QUERY_STRING'])
1671 && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
1672 try {
1673 $linksToDisplay = $LINKSDB->filterHash($_SERVER['QUERY_STRING']);
1674 } catch (LinkNotFoundException $e) {
1675 $PAGE->render404($e->getMessage());
1676 exit;
1677 }
1678 } else {
1679 // Filter links according search parameters.
1680 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
1681 $request = [
1682 'searchtags' => $searchtags,
1683 'searchterm' => $searchterm,
1684 ];
1685 $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility, !empty($_SESSION['untaggedonly']));
1686 }
1687
1688 // ---- Handle paging.
1689 $keys = array();
1690 foreach ($linksToDisplay as $key => $value) {
1691 $keys[] = $key;
1692 }
1693
1694 // Select articles according to paging.
1695 $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
1696 $pagecount = $pagecount == 0 ? 1 : $pagecount;
1697 $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
1698 $page = $page < 1 ? 1 : $page;
1699 $page = $page > $pagecount ? $pagecount : $page;
1700 // Start index.
1701 $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
1702 $end = $i + $_SESSION['LINKS_PER_PAGE'];
1703
1704 $thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE;
1705 if ($thumbnailsEnabled) {
1706 $thumbnailer = new Thumbnailer($conf);
1707 }
1708
1709 $linkDisp = array();
1710 while ($i<$end && $i<count($keys)) {
1711 $link = $linksToDisplay[$keys[$i]];
1712 $link['description'] = format_description($link['description']);
1713 $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight';
1714 $link['class'] = $link['private'] == 0 ? $classLi : 'private';
1715 $link['timestamp'] = $link['created']->getTimestamp();
1716 if (! empty($link['updated'])) {
1717 $link['updated_timestamp'] = $link['updated']->getTimestamp();
1718 } else {
1719 $link['updated_timestamp'] = '';
1720 }
1721 $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
1722 uasort($taglist, 'strcasecmp');
1723 $link['taglist'] = $taglist;
1724
1725 // Logged in, thumbnails enabled, not a note,
1726 // and (never retrieved yet or no valid cache file)
1727 if ($loginManager->isLoggedIn() && $thumbnailsEnabled && $link['url'][0] != '?'
1728 && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail'])))
1729 ) {
1730 $elem = $LINKSDB[$keys[$i]];
1731 $elem['thumbnail'] = $thumbnailer->get($link['url']);
1732 $LINKSDB[$keys[$i]] = $elem;
1733 $updateDB = true;
1734 $link['thumbnail'] = $elem['thumbnail'];
1735 }
1736
1737 // Check for both signs of a note: starting with ? and 7 chars long.
1738 if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
1739 $link['url'] = index_url($_SERVER) . $link['url'];
1740 }
1741
1742 $linkDisp[$keys[$i]] = $link;
1743 $i++;
1744 }
1745
1746 // If we retrieved new thumbnails, we update the database.
1747 if (!empty($updateDB)) {
1748 $LINKSDB->save($conf->get('resource.page_cache'));
1749 }
1750
1751 // Compute paging navigation
1752 $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags);
1753 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
1754 $previous_page_url = '';
1755 if ($i != count($keys)) {
1756 $previous_page_url = '?page=' . ($page+1) . $searchtermUrl . $searchtagsUrl;
1757 }
1758 $next_page_url='';
1759 if ($page>1) {
1760 $next_page_url = '?page=' . ($page-1) . $searchtermUrl . $searchtagsUrl;
1761 }
1762
1763 // Fill all template fields.
1764 $data = array(
1765 'previous_page_url' => $previous_page_url,
1766 'next_page_url' => $next_page_url,
1767 'page_current' => $page,
1768 'page_max' => $pagecount,
1769 'result_count' => count($linksToDisplay),
1770 'search_term' => $searchterm,
1771 'search_tags' => $searchtags,
1772 'visibility' => ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '',
1773 'links' => $linkDisp,
1774 ); 123 );
124 $this->get('/export', '\Shaarli\Front\Controller\Admin\ExportController:index');
125 $this->post('/export', '\Shaarli\Front\Controller\Admin\ExportController:export');
126 $this->get('/import', '\Shaarli\Front\Controller\Admin\ImportController:index');
127 $this->post('/import', '\Shaarli\Front\Controller\Admin\ImportController:import');
128 $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
129 $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
130 $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
131 $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
1775 132
1776 // If there is only a single link, we change on-the-fly the title of the page. 133 $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
1777 if (count($linksToDisplay) == 1) { 134})->add('\Shaarli\Front\ShaarliAdminMiddleware');
1778 $data['pagetitle'] = $linksToDisplay[$keys[0]]['title'] .' - '. $conf->get('general.title');
1779 } elseif (! empty($searchterm) || ! empty($searchtags)) {
1780 $data['pagetitle'] = t('Search: ');
1781 $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : '';
1782 $bracketWrap = function ($tag) {
1783 return '['. $tag .']';
1784 };
1785 $data['pagetitle'] .= ! empty($searchtags)
1786 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchtags))).' '
1787 : '';
1788 $data['pagetitle'] .= '- '. $conf->get('general.title');
1789 }
1790
1791 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
1792
1793 foreach ($data as $key => $value) {
1794 $PAGE->assign($key, $value);
1795 }
1796
1797 return;
1798}
1799
1800/**
1801 * Installation
1802 * This function should NEVER be called if the file data/config.php exists.
1803 *
1804 * @param ConfigManager $conf Configuration Manager instance.
1805 * @param SessionManager $sessionManager SessionManager instance
1806 * @param LoginManager $loginManager LoginManager instance
1807 */
1808function install($conf, $sessionManager, $loginManager)
1809{
1810 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
1811 if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) {
1812 mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705);
1813 }
1814 135
1815 136
1816 // This part makes sure sessions works correctly.
1817 // (Because on some hosts, session.save_path may not be set correctly,
1818 // or we may not have write access to it.)
1819 if (isset($_GET['test_session'])
1820 && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) {
1821 // Step 2: Check if data in session is correct.
1822 $msg = t(
1823 '<pre>Sessions do not seem to work correctly on your server.<br>'.
1824 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
1825 'and that you have write access to it.<br>'.
1826 'It currently points to %s.<br>'.
1827 'On some browsers, accessing your server via a hostname like \'localhost\' '.
1828 'or any custom hostname without a dot causes cookie storage to fail. '.
1829 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
1830 );
1831 $msg = sprintf($msg, session_save_path());
1832 echo $msg;
1833 echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
1834 die;
1835 }
1836 if (!isset($_SESSION['session_tested'])) {
1837 // Step 1 : Try to store data in session and reload page.
1838 $_SESSION['session_tested'] = 'Working'; // Try to set a variable in session.
1839 header('Location: '.index_url($_SERVER).'?test_session'); // Redirect to check stored data.
1840 }
1841 if (isset($_GET['test_session'])) {
1842 // Step 3: Sessions are OK. Remove test parameter from URL.
1843 header('Location: '.index_url($_SERVER));
1844 }
1845
1846
1847 if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) {
1848 $tz = 'UTC';
1849 if (!empty($_POST['continent']) && !empty($_POST['city'])
1850 && isTimeZoneValid($_POST['continent'], $_POST['city'])
1851 ) {
1852 $tz = $_POST['continent'].'/'.$_POST['city'];
1853 }
1854 $conf->set('general.timezone', $tz);
1855 $login = $_POST['setlogin'];
1856 $conf->set('credentials.login', $login);
1857 $salt = sha1(uniqid('', true) .'_'. mt_rand());
1858 $conf->set('credentials.salt', $salt);
1859 $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
1860 if (!empty($_POST['title'])) {
1861 $conf->set('general.title', escape($_POST['title']));
1862 } else {
1863 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
1864 }
1865 $conf->set('translation.language', escape($_POST['language']));
1866 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1867 $conf->set('api.enabled', !empty($_POST['enableApi']));
1868 $conf->set(
1869 'api.secret',
1870 generate_api_secret(
1871 $conf->get('credentials.login'),
1872 $conf->get('credentials.salt')
1873 )
1874 );
1875 try {
1876 // Everything is ok, let's create config file.
1877 $conf->write($loginManager->isLoggedIn());
1878 } catch (Exception $e) {
1879 error_log(
1880 'ERROR while writing config file after installation.' . PHP_EOL .
1881 $e->getMessage()
1882 );
1883
1884 // TODO: do not handle exceptions/errors in JS.
1885 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
1886 exit;
1887 }
1888 echo '<script>alert('
1889 .'"Shaarli is now configured. '
1890 .'Please enter your login/password and start shaaring your links!"'
1891 .');document.location=\'?do=login\';</script>';
1892 exit;
1893 }
1894
1895 $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
1896 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
1897 $PAGE->assign('continents', $continents);
1898 $PAGE->assign('cities', $cities);
1899 $PAGE->assign('languages', Languages::getAvailableLanguages());
1900 $PAGE->renderPage('install');
1901 exit;
1902}
1903
1904if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
1905 showDailyRSS($conf, $loginManager);
1906 exit;
1907}
1908
1909if (!isset($_SESSION['LINKS_PER_PAGE'])) {
1910 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
1911}
1912
1913try {
1914 $history = new History($conf->get('resource.history'));
1915} catch (Exception $e) {
1916 die($e->getMessage());
1917}
1918
1919$linkDb = new LinkDB(
1920 $conf->get('resource.datastore'),
1921 $loginManager->isLoggedIn(),
1922 $conf->get('privacy.hide_public_links')
1923);
1924
1925$container = new \Slim\Container();
1926$container['conf'] = $conf;
1927$container['plugins'] = $pluginManager;
1928$container['history'] = $history;
1929$app = new \Slim\App($container);
1930
1931// REST API routes 137// REST API routes
1932$app->group('/api/v1', function () { 138$app->group('/api/v1', function () {
1933 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo'); 139 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
@@ -1947,19 +153,4 @@ $app->group('/api/v1', function () {
1947 153
1948$response = $app->run(true); 154$response = $app->run(true);
1949 155
1950// Hack to make Slim and Shaarli router work together: 156$app->respond($response);
1951// If a Slim route isn't found and NOT API call, we call renderPage().
1952if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
1953 // We use UTF-8 for proper international characters handling.
1954 header('Content-Type: text/html; charset=utf-8');
1955 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);
1956} else {
1957 $response = $response
1958 ->withHeader('Access-Control-Allow-Origin', '*')
1959 ->withHeader(
1960 'Access-Control-Allow-Headers',
1961 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
1962 )
1963 ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
1964 $app->respond($response);
1965}
diff --git a/init.php b/init.php
new file mode 100644
index 00000000..f0b84368
--- /dev/null
+++ b/init.php
@@ -0,0 +1,85 @@
1<?php
2
3require_once __DIR__ . '/vendor/autoload.php';
4
5use Shaarli\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));
63
64session_name('shaarli');
65// Start session if needed (Some server auto-start sessions).
66if (session_status() == PHP_SESSION_NONE) {
67 session_start();
68}
69
70// Regenerate session ID if invalid or not defined in cookie.
71if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
72 session_regenerate_id(true);
73 $_COOKIE['shaarli'] = session_id();
74}
75
76// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
77if (! defined('LC_MESSAGES')) {
78 define('LC_MESSAGES', LC_COLLATE);
79}
80
81// Prevent caching on client side or proxy: (yes, it's ugly)
82header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
83header("Cache-Control: no-store, no-cache, must-revalidate");
84header("Cache-Control: post-check=0, pre-check=0", false);
85header("Pragma: no-cache");
diff --git a/mkdocs.yml b/mkdocs.yml
index 248fdbfe..2e201d03 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -15,42 +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 - Unit tests inside Docker: Unit-tests-Docker.md
55- FAQ: FAQ.md
56- Troubleshooting: Troubleshooting.md 39- Troubleshooting: Troubleshooting.md
diff --git a/package.json b/package.json
index f3d9b51e..8a24512a 100644
--- a/package.json
+++ b/package.json
@@ -11,22 +11,23 @@
11 "purecss": "^1.0.0" 11 "purecss": "^1.0.0"
12 }, 12 },
13 "devDependencies": { 13 "devDependencies": {
14 "babel-core": "^6.26.0", 14 "@babel/core": "^7.11.6",
15 "babel-loader": "^7.1.2", 15 "@babel/preset-env": "^7.11.5",
16 "babel-minify-webpack-plugin": "^0.2.0", 16 "babel-loader": "^8.1.0",
17 "babel-preset-env": "^1.6.1", 17 "css-loader": "^4.3.0",
18 "css-loader": "^0.28.9", 18 "eslint": "^7.9.0",
19 "eslint": "^4.16.0", 19 "eslint-config-airbnb-base": "^14.2.0",
20 "eslint-config-airbnb-base": "^12.1.0", 20 "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", 21 "file-loader": "^1.1.6",
24 "node-sass": "^4.12.0", 22 "mini-css-extract-plugin": "^0.11.2",
25 "sass-lint": "^1.12.1", 23 "sass": "^1.26.11",
26 "sass-loader": "^6.0.6", 24 "sass-loader": "^10.0.2",
27 "style-loader": "^0.19.1", 25 "stylelint": "^13.7.1",
28 "url-loader": "^0.6.2", 26 "stylelint-config-standard": "^20.0.0",
29 "webpack": "^3.10.0" 27 "stylelint-scss": "^3.18.0",
28 "terser-webpack-plugin": "^4.2.2",
29 "webpack": "^4.44.2",
30 "webpack-cli": "^3.3.12"
30 }, 31 },
31 "scripts": { 32 "scripts": {
32 "build": "webpack", 33 "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..922b5966 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['_BASE_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 71ba7495..defb01f7 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -2,8 +2,8 @@
2/** 2/**
3 * Demo Plugin. 3 * Demo Plugin.
4 * 4 *
5 * This plugin try to cover Shaarli's plugin API entirely. 5 * This plugin tries to completely cover Shaarli's plugin API.
6 * Can be used by plugin developper to make their own. 6 * Can be used by plugin developers to make their own plugin.
7 */ 7 */
8 8
9/* 9/*
@@ -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.
@@ -61,7 +61,7 @@ function demo_plugin_init($conf)
61 61
62/** 62/**
63 * Hook render_header. 63 * Hook render_header.
64 * Executed on every page redering. 64 * Executed on every page render.
65 * 65 *
66 * Template placeholders: 66 * Template placeholders:
67 * - buttons_toolbar 67 * - buttons_toolbar
@@ -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(
@@ -145,7 +145,7 @@ function hook_demo_plugin_render_header($data)
145 145
146/** 146/**
147 * Hook render_includes. 147 * Hook render_includes.
148 * Executed on every page redering. 148 * Executed on every page render.
149 * 149 *
150 * Template placeholders: 150 * Template placeholders:
151 * - css_files 151 * - css_files
@@ -169,7 +169,7 @@ function hook_demo_plugin_render_includes($data)
169 169
170/** 170/**
171 * Hook render_footer. 171 * Hook render_footer.
172 * Executed on every page redering. 172 * Executed on every page render.
173 * 173 *
174 * Template placeholders: 174 * Template placeholders:
175 * - text 175 * - text
@@ -186,7 +186,7 @@ function hook_demo_plugin_render_includes($data)
186 */ 186 */
187function hook_demo_plugin_render_footer($data) 187function hook_demo_plugin_render_footer($data)
188{ 188{
189 // footer text 189 // Footer text
190 $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); 190 $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
191 191
192 // Free elements at the end of the page. 192 // Free elements at the end of the page.
@@ -277,7 +277,7 @@ function hook_demo_plugin_render_editlink($data)
277 // Load HTML into a string 277 // Load HTML into a string
278 $html = file_get_contents(PluginManager::$PLUGINS_PATH .'/demo_plugin/field.html'); 278 $html = file_get_contents(PluginManager::$PLUGINS_PATH .'/demo_plugin/field.html');
279 279
280 // replace value in HTML if it exists in $data 280 // Replace value in HTML if it exists in $data
281 if (!empty($data['link']['stuff'])) { 281 if (!empty($data['link']['stuff'])) {
282 $html = sprintf($html, $data['link']['stuff']); 282 $html = sprintf($html, $data['link']['stuff']);
283 } else { 283 } else {
@@ -324,9 +324,7 @@ function hook_demo_plugin_render_tools($data)
324 */ 324 */
325function hook_demo_plugin_render_picwall($data) 325function hook_demo_plugin_render_picwall($data)
326{ 326{
327 // plugin_start_zone
328 $data['plugin_start_zone'][] = '<center>BEFORE</center>'; 327 $data['plugin_start_zone'][] = '<center>BEFORE</center>';
329 // plugin_end_zone
330 $data['plugin_end_zone'][] = '<center>AFTER</center>'; 328 $data['plugin_end_zone'][] = '<center>AFTER</center>';
331 329
332 return $data; 330 return $data;
@@ -348,9 +346,7 @@ function hook_demo_plugin_render_picwall($data)
348 */ 346 */
349function hook_demo_plugin_render_tagcloud($data) 347function hook_demo_plugin_render_tagcloud($data)
350{ 348{
351 // plugin_start_zone
352 $data['plugin_start_zone'][] = '<center>BEFORE</center>'; 349 $data['plugin_start_zone'][] = '<center>BEFORE</center>';
353 // plugin_end_zone
354 $data['plugin_end_zone'][] = '<center>AFTER</center>'; 350 $data['plugin_end_zone'][] = '<center>AFTER</center>';
355 351
356 return $data; 352 return $data;
@@ -372,9 +368,7 @@ function hook_demo_plugin_render_tagcloud($data)
372 */ 368 */
373function hook_demo_plugin_render_daily($data) 369function hook_demo_plugin_render_daily($data)
374{ 370{
375 // plugin_start_zone
376 $data['plugin_start_zone'][] = '<center>BEFORE</center>'; 371 $data['plugin_start_zone'][] = '<center>BEFORE</center>';
377 // plugin_end_zone
378 $data['plugin_end_zone'][] = '<center>AFTER</center>'; 372 $data['plugin_end_zone'][] = '<center>AFTER</center>';
379 373
380 374
@@ -447,9 +441,9 @@ function hook_demo_plugin_delete_link($data)
447function hook_demo_plugin_render_feed($data) 441function hook_demo_plugin_render_feed($data)
448{ 442{
449 foreach ($data['links'] as &$link) { 443 foreach ($data['links'] as &$link) {
450 if ($data['_PAGE_'] == Router::$PAGE_FEED_ATOM) { 444 if ($data['_PAGE_'] == TemplatePage::FEED_ATOM) {
451 $link['description'] .= ' - ATOM Feed' ; 445 $link['description'] .= ' - ATOM Feed' ;
452 } elseif ($data['_PAGE_'] == Router::$PAGE_FEED_RSS) { 446 } elseif ($data['_PAGE_'] == TemplatePage::FEED_RSS) {
453 $link['description'] .= ' - RSS Feed'; 447 $link['description'] .= ' - RSS Feed';
454 } 448 }
455 } 449 }
@@ -465,7 +459,8 @@ function hook_demo_plugin_render_feed($data)
465 */ 459 */
466function hook_demo_plugin_save_plugin_parameters($data) 460function hook_demo_plugin_save_plugin_parameters($data)
467{ 461{
468 // Here we edit the provided value, but we can use this to generate config files, etc. 462 // Here we edit the provided value.
463 // This hook can also be used to generate config files, etc.
469 if (! empty($data['DEMO_PLUGIN_PARAMETER']) && ! endsWith($data['DEMO_PLUGIN_PARAMETER'], '_SUFFIX')) { 464 if (! empty($data['DEMO_PLUGIN_PARAMETER']) && ! endsWith($data['DEMO_PLUGIN_PARAMETER'], '_SUFFIX')) {
470 $data['DEMO_PLUGIN_PARAMETER'] .= '_SUFFIX'; 465 $data['DEMO_PLUGIN_PARAMETER'] .= '_SUFFIX';
471 } 466 }
diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php
index dab75dd5..79e7380b 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,7 +49,7 @@ 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>';
@@ -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/markdown/README.md b/plugins/markdown/README.md
deleted file mode 100644
index bc9427e2..00000000
--- a/plugins/markdown/README.md
+++ /dev/null
@@ -1,102 +0,0 @@
1## Markdown Shaarli plugin
2
3Convert all your shaares description to HTML formatted Markdown.
4
5[Read more about Markdown syntax](http://daringfireball.net/projects/markdown/syntax).
6
7Markdown processing is done with [Parsedown library](https://github.com/erusev/parsedown).
8
9### Installation
10
11As a default plugin, it should already be in `tpl/plugins/` directory.
12If not, download and unpack it there.
13
14The directory structure should look like:
15
16```
17--- plugins
18 |--- markdown
19 |--- help.html
20 |--- markdown.css
21 |--- markdown.meta
22 |--- markdown.php
23 |--- README.md
24```
25
26To enable the plugin, just check it in the plugin administration page.
27
28You can also add `markdown` to your list of enabled plugins in `data/config.json.php`
29(`general.enabled_plugins` list).
30
31This should look like:
32
33```
34"general": {
35 "enabled_plugins": [
36 "markdown",
37 [...]
38 ],
39}
40```
41
42Parsedown parsing library is imported using Composer. If you installed Shaarli using `git`,
43or the `master` branch, run
44
45 composer update --no-dev --prefer-dist
46
47### No Markdown tag
48
49If the tag `nomarkdown` is set for a shaare, it won't be converted to Markdown syntax.
50
51> Note: this is a special tag, so it won't be displayed in link list.
52
53### HTML escape
54
55By default, HTML tags are escaped. You can enable HTML tags rendering
56by setting `security.markdwon_escape` to `false` in `data/config.json.php`:
57
58```json
59{
60 "security": {
61 "markdown_escape": false
62 }
63}
64```
65
66With this setting, Markdown support HTML tags. For example:
67
68 > <strong>strong</strong><strike>strike</strike>
69
70Will render as:
71
72> <strong>strong</strong><strike>strike</strike>
73
74
75**Warning:**
76
77 * This setting might present **security risks** (XSS) on shared instances, even though tags
78 such as script, iframe, etc should be disabled.
79 * If you want to shaare HTML code, it is necessary to use inline code or code blocks.
80 * If your shaared descriptions contained HTML tags before enabling the markdown plugin,
81enabling it might break your page.
82
83### Known issue
84
85#### Redirector
86
87If you're using a redirector, you *need* to add a space after a link,
88otherwise the rest of the line will be `urlencode`.
89
90```
91[link](http://domain.tld)-->test
92```
93
94Will consider `http://domain.tld)-->test` as URL.
95
96Instead, add an additional space.
97
98```
99[link](http://domain.tld) -->test
100```
101
102> Won't fix because a `)` is a valid part of an URL.
diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html
deleted file mode 100644
index ded3d347..00000000
--- a/plugins/markdown/help.html
+++ /dev/null
@@ -1,5 +0,0 @@
1<div class="md_help">
2 %s
3 <a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
4 %s</a>.
5</div>
diff --git a/plugins/markdown/markdown.meta b/plugins/markdown/markdown.meta
deleted file mode 100644
index 322856ea..00000000
--- a/plugins/markdown/markdown.meta
+++ /dev/null
@@ -1,4 +0,0 @@
1description="Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
2If your shaared descriptions contained HTML tags before enabling the markdown plugin,
3enabling it might break your page.
4See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering\">README</a>."
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php
deleted file mode 100644
index 628970d6..00000000
--- a/plugins/markdown/markdown.php
+++ /dev/null
@@ -1,365 +0,0 @@
1<?php
2
3/**
4 * Plugin Markdown.
5 *
6 * Shaare's descriptions are parsed with Markdown.
7 */
8
9use Shaarli\Config\ConfigManager;
10use Shaarli\Plugin\PluginManager;
11use Shaarli\Router;
12
13/*
14 * If this tag is used on a shaare, the description won't be processed by Parsedown.
15 */
16define('NO_MD_TAG', 'nomarkdown');
17
18/**
19 * Parse linklist descriptions.
20 *
21 * @param array $data linklist data.
22 * @param ConfigManager $conf instance.
23 *
24 * @return mixed linklist data parsed in markdown (and converted to HTML).
25 */
26function hook_markdown_render_linklist($data, $conf)
27{
28 foreach ($data['links'] as &$value) {
29 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
30 $value = stripNoMarkdownTag($value);
31 continue;
32 }
33 $value['description_src'] = $value['description'];
34 $value['description'] = process_markdown(
35 $value['description'],
36 $conf->get('security.markdown_escape', true),
37 $conf->get('security.allowed_protocols')
38 );
39 }
40 return $data;
41}
42
43/**
44 * Parse feed linklist descriptions.
45 *
46 * @param array $data linklist data.
47 * @param ConfigManager $conf instance.
48 *
49 * @return mixed linklist data parsed in markdown (and converted to HTML).
50 */
51function hook_markdown_render_feed($data, $conf)
52{
53 foreach ($data['links'] as &$value) {
54 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
55 $value = stripNoMarkdownTag($value);
56 continue;
57 }
58 $value['description'] = reverse_feed_permalink($value['description']);
59 $value['description'] = process_markdown(
60 $value['description'],
61 $conf->get('security.markdown_escape', true),
62 $conf->get('security.allowed_protocols')
63 );
64 }
65
66 return $data;
67}
68
69/**
70 * Parse daily descriptions.
71 *
72 * @param array $data daily data.
73 * @param ConfigManager $conf instance.
74 *
75 * @return mixed daily data parsed in markdown (and converted to HTML).
76 */
77function hook_markdown_render_daily($data, $conf)
78{
79 //var_dump($data);die;
80 // Manipulate columns data
81 foreach ($data['linksToDisplay'] as &$value) {
82 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
83 $value = stripNoMarkdownTag($value);
84 continue;
85 }
86 $value['formatedDescription'] = process_markdown(
87 $value['formatedDescription'],
88 $conf->get('security.markdown_escape', true),
89 $conf->get('security.allowed_protocols')
90 );
91 }
92
93 return $data;
94}
95
96/**
97 * Check if noMarkdown is set in tags.
98 *
99 * @param string $tags tag list
100 *
101 * @return bool true if markdown should be disabled on this link.
102 */
103function noMarkdownTag($tags)
104{
105 return preg_match('/(^|\s)'. NO_MD_TAG .'(\s|$)/', $tags);
106}
107
108/**
109 * Remove the no-markdown meta tag so it won't be displayed.
110 *
111 * @param array $link Link data.
112 *
113 * @return array Updated link without no markdown tag.
114 */
115function stripNoMarkdownTag($link)
116{
117 if (! empty($link['taglist'])) {
118 $offset = array_search(NO_MD_TAG, $link['taglist']);
119 if ($offset !== false) {
120 unset($link['taglist'][$offset]);
121 }
122 }
123
124 if (!empty($link['tags'])) {
125 str_replace(NO_MD_TAG, '', $link['tags']);
126 }
127
128 return $link;
129}
130
131/**
132 * When link list is displayed, include markdown CSS.
133 *
134 * @param array $data includes data.
135 *
136 * @return mixed - includes data with markdown CSS file added.
137 */
138function hook_markdown_render_includes($data)
139{
140 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST
141 || $data['_PAGE_'] == Router::$PAGE_DAILY
142 || $data['_PAGE_'] == Router::$PAGE_EDITLINK
143 ) {
144 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/markdown/markdown.css';
145 }
146
147 return $data;
148}
149
150/**
151 * Hook render_editlink.
152 * Adds an help link to markdown syntax.
153 *
154 * @param array $data data passed to plugin
155 *
156 * @return array altered $data.
157 */
158function hook_markdown_render_editlink($data)
159{
160 // Load help HTML into a string
161 $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
162 $translations = [
163 t('Description will be rendered with'),
164 t('Markdown syntax documentation'),
165 t('Markdown syntax'),
166 ];
167 $data['edit_link_plugin'][] = vsprintf($txt, $translations);
168 // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
169 if (! in_array(NO_MD_TAG, $data['tags'])) {
170 $data['tags'][NO_MD_TAG] = 0;
171 }
172
173 return $data;
174}
175
176
177/**
178 * Remove HTML links auto generated by Shaarli core system.
179 * Keeps HREF attributes.
180 *
181 * @param string $description input description text.
182 *
183 * @return string $description without HTML links.
184 */
185function reverse_text2clickable($description)
186{
187 $descriptionLines = explode(PHP_EOL, $description);
188 $descriptionOut = '';
189 $codeBlockOn = false;
190 $lineCount = 0;
191
192 foreach ($descriptionLines as $descriptionLine) {
193 // Detect line of code: starting with 4 spaces,
194 // except lists which can start with +/*/- or `2.` after spaces.
195 $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0;
196 // Detect and toggle block of code
197 if (!$codeBlockOn) {
198 $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0;
199 } elseif (preg_match('/^```/', $descriptionLine) > 0) {
200 $codeBlockOn = false;
201 }
202
203 $hashtagTitle = ' title="Hashtag [^"]+"';
204 // Reverse `inline code` hashtags.
205 $descriptionLine = preg_replace(
206 '!(`[^`\n]*)<a href="[^ ]*"'. $hashtagTitle .'>([^<]+)</a>([^`\n]*`)!m',
207 '$1$2$3',
208 $descriptionLine
209 );
210
211 // Reverse all links in code blocks, only non hashtag elsewhere.
212 $hashtagFilter = (!$codeBlockOn && !$codeLineOn) ? '(?!'. $hashtagTitle .')': '(?:'. $hashtagTitle .')?';
213 $descriptionLine = preg_replace(
214 '#<a href="[^ ]*"'. $hashtagFilter .'>([^<]+)</a>#m',
215 '$1',
216 $descriptionLine
217 );
218
219 // Make hashtag links markdown ready, otherwise the links will be ignored with escape set to true
220 if (!$codeBlockOn && !$codeLineOn) {
221 $descriptionLine = preg_replace(
222 '#<a href="([^ ]*)"'. $hashtagTitle .'>([^<]+)</a>#m',
223 '[$2]($1)',
224 $descriptionLine
225 );
226 }
227
228 $descriptionOut .= $descriptionLine;
229 if ($lineCount++ < count($descriptionLines) - 1) {
230 $descriptionOut .= PHP_EOL;
231 }
232 }
233 return $descriptionOut;
234}
235
236/**
237 * Remove <br> tag to let markdown handle it.
238 *
239 * @param string $description input description text.
240 *
241 * @return string $description without <br> tags.
242 */
243function reverse_nl2br($description)
244{
245 return preg_replace('!<br */?>!im', '', $description);
246}
247
248/**
249 * Remove HTML spaces '&nbsp;' auto generated by Shaarli core system.
250 *
251 * @param string $description input description text.
252 *
253 * @return string $description without HTML links.
254 */
255function reverse_space2nbsp($description)
256{
257 return preg_replace('/(^| )&nbsp;/m', '$1 ', $description);
258}
259
260function reverse_feed_permalink($description)
261{
262 return preg_replace('@&#8212; <a href="([^"]+)" title="[^"]+">(\w+)</a>$@im', '&#8212; [$2]($1)', $description);
263}
264
265/**
266 * Replace not whitelisted protocols with http:// in given description.
267 *
268 * @param string $description input description text.
269 * @param array $allowedProtocols list of allowed protocols.
270 *
271 * @return string $description without malicious link.
272 */
273function filter_protocols($description, $allowedProtocols)
274{
275 return preg_replace_callback(
276 '#]\((.*?)\)#is',
277 function ($match) use ($allowedProtocols) {
278 return ']('. whitelist_protocols($match[1], $allowedProtocols) .')';
279 },
280 $description
281 );
282}
283
284/**
285 * Remove dangerous HTML tags (tags, iframe, etc.).
286 * Doesn't affect <code> content (already escaped by Parsedown).
287 *
288 * @param string $description input description text.
289 *
290 * @return string given string escaped.
291 */
292function sanitize_html($description)
293{
294 $escapeTags = array(
295 'script',
296 'style',
297 'link',
298 'iframe',
299 'frameset',
300 'frame',
301 );
302 foreach ($escapeTags as $tag) {
303 $description = preg_replace_callback(
304 '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is',
305 function ($match) {
306 return escape($match[0]);
307 },
308 $description
309 );
310 }
311 $description = preg_replace(
312 '#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is',
313 '$1',
314 $description
315 );
316 return $description;
317}
318
319/**
320 * Render shaare contents through Markdown parser.
321 * 1. Remove HTML generated by Shaarli core.
322 * 2. Reverse the escape function.
323 * 3. Generate markdown descriptions.
324 * 4. Sanitize sensible HTML tags for security.
325 * 5. Wrap description in 'markdown' CSS class.
326 *
327 * @param string $description input description text.
328 * @param bool $escape escape HTML entities
329 *
330 * @return string HTML processed $description.
331 */
332function process_markdown($description, $escape = true, $allowedProtocols = [])
333{
334 $parsedown = new Parsedown();
335
336 $processedDescription = $description;
337 $processedDescription = reverse_nl2br($processedDescription);
338 $processedDescription = reverse_space2nbsp($processedDescription);
339 $processedDescription = reverse_text2clickable($processedDescription);
340 $processedDescription = filter_protocols($processedDescription, $allowedProtocols);
341 $processedDescription = unescape($processedDescription);
342 $processedDescription = $parsedown
343 ->setMarkupEscaped($escape)
344 ->setBreaksEnabled(true)
345 ->text($processedDescription);
346 $processedDescription = sanitize_html($processedDescription);
347
348 if (!empty($processedDescription)) {
349 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
350 }
351
352 return $processedDescription;
353}
354
355/**
356 * This function is never called, but contains translation calls for GNU gettext extraction.
357 */
358function markdown_dummy_translation()
359{
360 // meta
361 t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
362If your shaared descriptions contained HTML tags before enabling the markdown plugin,
363enabling it might break your page.
364See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
365}
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..95499e39 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['_BASE_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..805c1ad9 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['_BASE_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/shaarli_version.php b/shaarli_version.php
index 1941d6c3..8d94352f 100644
--- a/shaarli_version.php
+++ b/shaarli_version.php
@@ -1 +1 @@
<?php /* 0.11.1 */ ?> <?php /* 0.12.0 */ ?>
diff --git a/tests/ApplicationUtilsTest.php b/tests/ApplicationUtilsTest.php
index 82f8804d..a232b351 100644
--- a/tests/ApplicationUtilsTest.php
+++ b/tests/ApplicationUtilsTest.php
@@ -8,7 +8,7 @@ require_once 'tests/utils/FakeApplicationUtils.php';
8/** 8/**
9 * Unitary tests for Shaarli utilities 9 * Unitary tests for Shaarli utilities
10 */ 10 */
11class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase 11class ApplicationUtilsTest extends \Shaarli\TestCase
12{ 12{
13 protected static $testUpdateFile = 'sandbox/update.txt'; 13 protected static $testUpdateFile = 'sandbox/update.txt';
14 protected static $testVersion = '0.5.0'; 14 protected static $testVersion = '0.5.0';
@@ -17,7 +17,7 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
17 /** 17 /**
18 * Reset test data for each test 18 * Reset test data for each test
19 */ 19 */
20 public function setUp() 20 protected function setUp(): void
21 { 21 {
22 FakeApplicationUtils::$VERSION_CODE = ''; 22 FakeApplicationUtils::$VERSION_CODE = '';
23 if (file_exists(self::$testUpdateFile)) { 23 if (file_exists(self::$testUpdateFile)) {
@@ -28,7 +28,7 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
28 /** 28 /**
29 * Remove test version file if it exists 29 * Remove test version file if it exists
30 */ 30 */
31 public function tearDown() 31 protected function tearDown(): void
32 { 32 {
33 if (is_file('sandbox/version.php')) { 33 if (is_file('sandbox/version.php')) {
34 unlink('sandbox/version.php'); 34 unlink('sandbox/version.php');
@@ -144,11 +144,12 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
144 144
145 /** 145 /**
146 * Test update checks - invalid Git branch 146 * Test update checks - invalid Git branch
147 * @expectedException Exception
148 * @expectedExceptionMessageRegExp /Invalid branch selected for updates/
149 */ 147 */
150 public function testCheckUpdateInvalidGitBranch() 148 public function testCheckUpdateInvalidGitBranch()
151 { 149 {
150 $this->expectException(\Exception::class);
151 $this->expectExceptionMessageRegExp('/Invalid branch selected for updates/');
152
152 ApplicationUtils::checkUpdate('', 'null', 0, true, true, 'unstable'); 153 ApplicationUtils::checkUpdate('', 'null', 0, true, true, 'unstable');
153 } 154 }
154 155
@@ -253,29 +254,31 @@ class ApplicationUtilsTest extends \PHPUnit\Framework\TestCase
253 public function testCheckSupportedPHPVersion() 254 public function testCheckSupportedPHPVersion()
254 { 255 {
255 $minVersion = '5.3'; 256 $minVersion = '5.3';
256 ApplicationUtils::checkPHPVersion($minVersion, '5.4.32'); 257 $this->assertTrue(ApplicationUtils::checkPHPVersion($minVersion, '5.4.32'));
257 ApplicationUtils::checkPHPVersion($minVersion, '5.5'); 258 $this->assertTrue(ApplicationUtils::checkPHPVersion($minVersion, '5.5'));
258 ApplicationUtils::checkPHPVersion($minVersion, '5.6.10'); 259 $this->assertTrue(ApplicationUtils::checkPHPVersion($minVersion, '5.6.10'));
259 } 260 }
260 261
261 /** 262 /**
262 * Check a unsupported PHP version 263 * Check a unsupported PHP version
263 * @expectedException Exception
264 * @expectedExceptionMessageRegExp /Your PHP version is obsolete/
265 */ 264 */
266 public function testCheckSupportedPHPVersion51() 265 public function testCheckSupportedPHPVersion51()
267 { 266 {
268 ApplicationUtils::checkPHPVersion('5.3', '5.1.0'); 267 $this->expectException(\Exception::class);
268 $this->expectExceptionMessageRegExp('/Your PHP version is obsolete/');
269
270 $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.1.0'));
269 } 271 }
270 272
271 /** 273 /**
272 * Check another unsupported PHP version 274 * Check another unsupported PHP version
273 * @expectedException Exception
274 * @expectedExceptionMessageRegExp /Your PHP version is obsolete/
275 */ 275 */
276 public function testCheckSupportedPHPVersion52() 276 public function testCheckSupportedPHPVersion52()
277 { 277 {
278 ApplicationUtils::checkPHPVersion('5.3', '5.2'); 278 $this->expectException(\Exception::class);
279 $this->expectExceptionMessageRegExp('/Your PHP version is obsolete/');
280
281 $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.2'));
279 } 282 }
280 283
281 /** 284 /**
diff --git a/tests/FileUtilsTest.php b/tests/FileUtilsTest.php
index 57719175..9163bdf1 100644
--- a/tests/FileUtilsTest.php
+++ b/tests/FileUtilsTest.php
@@ -9,7 +9,7 @@ use Exception;
9 * 9 *
10 * Test file utility class. 10 * Test file utility class.
11 */ 11 */
12class FileUtilsTest extends \PHPUnit\Framework\TestCase 12class FileUtilsTest extends \Shaarli\TestCase
13{ 13{
14 /** 14 /**
15 * @var string Test file path. 15 * @var string Test file path.
@@ -19,7 +19,7 @@ class FileUtilsTest extends \PHPUnit\Framework\TestCase
19 /** 19 /**
20 * Delete test file after every test. 20 * Delete test file after every test.
21 */ 21 */
22 public function tearDown() 22 protected function tearDown(): void
23 { 23 {
24 @unlink(self::$file); 24 @unlink(self::$file);
25 } 25 }
@@ -49,12 +49,12 @@ class FileUtilsTest extends \PHPUnit\Framework\TestCase
49 49
50 /** 50 /**
51 * File not writable: raise an exception. 51 * File not writable: raise an exception.
52 *
53 * @expectedException Shaarli\Exceptions\IOException
54 * @expectedExceptionMessage Error accessing "sandbox/flat.db"
55 */ 52 */
56 public function testWriteWithoutPermission() 53 public function testWriteWithoutPermission()
57 { 54 {
55 $this->expectException(\Shaarli\Exceptions\IOException::class);
56 $this->expectExceptionMessage('Error accessing "sandbox/flat.db"');
57
58 touch(self::$file); 58 touch(self::$file);
59 chmod(self::$file, 0440); 59 chmod(self::$file, 0440);
60 FileUtils::writeFlatDB(self::$file, null); 60 FileUtils::writeFlatDB(self::$file, null);
@@ -62,23 +62,23 @@ class FileUtilsTest extends \PHPUnit\Framework\TestCase
62 62
63 /** 63 /**
64 * Folder non existent: raise an exception. 64 * Folder non existent: raise an exception.
65 *
66 * @expectedException Shaarli\Exceptions\IOException
67 * @expectedExceptionMessage Error accessing "nopefolder"
68 */ 65 */
69 public function testWriteFolderDoesNotExist() 66 public function testWriteFolderDoesNotExist()
70 { 67 {
68 $this->expectException(\Shaarli\Exceptions\IOException::class);
69 $this->expectExceptionMessage('Error accessing "nopefolder"');
70
71 FileUtils::writeFlatDB('nopefolder/file', null); 71 FileUtils::writeFlatDB('nopefolder/file', null);
72 } 72 }
73 73
74 /** 74 /**
75 * Folder non writable: raise an exception. 75 * Folder non writable: raise an exception.
76 *
77 * @expectedException Shaarli\Exceptions\IOException
78 * @expectedExceptionMessage Error accessing "sandbox"
79 */ 76 */
80 public function testWriteFolderPermission() 77 public function testWriteFolderPermission()
81 { 78 {
79 $this->expectException(\Shaarli\Exceptions\IOException::class);
80 $this->expectExceptionMessage('Error accessing "sandbox"');
81
82 chmod(dirname(self::$file), 0555); 82 chmod(dirname(self::$file), 0555);
83 try { 83 try {
84 FileUtils::writeFlatDB(self::$file, null); 84 FileUtils::writeFlatDB(self::$file, null);
diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php
index 8303e53a..6dc0e5b7 100644
--- a/tests/HistoryTest.php
+++ b/tests/HistoryTest.php
@@ -3,9 +3,9 @@
3namespace Shaarli; 3namespace Shaarli;
4 4
5use DateTime; 5use DateTime;
6use Exception; 6use Shaarli\Bookmark\Bookmark;
7 7
8class HistoryTest extends \PHPUnit\Framework\TestCase 8class HistoryTest extends \Shaarli\TestCase
9{ 9{
10 /** 10 /**
11 * @var string History file path 11 * @var string History file path
@@ -15,9 +15,11 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
15 /** 15 /**
16 * Delete history file. 16 * Delete history file.
17 */ 17 */
18 public function tearDown() 18 protected function setUp(): void
19 { 19 {
20 @unlink(self::$historyFilePath); 20 if (file_exists(self::$historyFilePath)) {
21 unlink(self::$historyFilePath);
22 }
21 } 23 }
22 24
23 /** 25 /**
@@ -41,12 +43,12 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
41 43
42 /** 44 /**
43 * Not writable history file: raise an exception. 45 * Not writable history file: raise an exception.
44 *
45 * @expectedException Exception
46 * @expectedExceptionMessage History file isn't readable or writable
47 */ 46 */
48 public function testConstructNotWritable() 47 public function testConstructNotWritable()
49 { 48 {
49 $this->expectException(\Exception::class);
50 $this->expectExceptionMessage('History file isn\'t readable or writable');
51
50 touch(self::$historyFilePath); 52 touch(self::$historyFilePath);
51 chmod(self::$historyFilePath, 0440); 53 chmod(self::$historyFilePath, 0440);
52 $history = new History(self::$historyFilePath); 54 $history = new History(self::$historyFilePath);
@@ -55,12 +57,12 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
55 57
56 /** 58 /**
57 * Not parsable history file: raise an exception. 59 * Not parsable history file: raise an exception.
58 *
59 * @expectedException Exception
60 * @expectedExceptionMessageRegExp /Could not parse history file/
61 */ 60 */
62 public function testConstructNotParsable() 61 public function testConstructNotParsable()
63 { 62 {
63 $this->expectException(\Exception::class);
64 $this->expectExceptionMessageRegExp('/Could not parse history file/');
65
64 file_put_contents(self::$historyFilePath, 'not parsable'); 66 file_put_contents(self::$historyFilePath, 'not parsable');
65 $history = new History(self::$historyFilePath); 67 $history = new History(self::$historyFilePath);
66 // gzinflate generates a warning 68 // gzinflate generates a warning
@@ -73,137 +75,140 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
73 public function testAddLink() 75 public function testAddLink()
74 { 76 {
75 $history = new History(self::$historyFilePath); 77 $history = new History(self::$historyFilePath);
76 $history->addLink(['id' => 0]); 78 $bookmark = (new Bookmark())->setId(0);
79 $history->addLink($bookmark);
77 $actual = $history->getHistory()[0]; 80 $actual = $history->getHistory()[0];
78 $this->assertEquals(History::CREATED, $actual['event']); 81 $this->assertEquals(History::CREATED, $actual['event']);
79 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 82 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
80 $this->assertEquals(0, $actual['id']); 83 $this->assertEquals(0, $actual['id']);
81 84
82 $history = new History(self::$historyFilePath); 85 $history = new History(self::$historyFilePath);
83 $history->addLink(['id' => 1]); 86 $bookmark = (new Bookmark())->setId(1);
87 $history->addLink($bookmark);
84 $actual = $history->getHistory()[0]; 88 $actual = $history->getHistory()[0];
85 $this->assertEquals(History::CREATED, $actual['event']); 89 $this->assertEquals(History::CREATED, $actual['event']);
86 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 90 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
87 $this->assertEquals(1, $actual['id']); 91 $this->assertEquals(1, $actual['id']);
88 92
89 $history = new History(self::$historyFilePath); 93 $history = new History(self::$historyFilePath);
90 $history->addLink(['id' => 'str']); 94 $bookmark = (new Bookmark())->setId('str');
95 $history->addLink($bookmark);
91 $actual = $history->getHistory()[0]; 96 $actual = $history->getHistory()[0];
92 $this->assertEquals(History::CREATED, $actual['event']); 97 $this->assertEquals(History::CREATED, $actual['event']);
93 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 98 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
94 $this->assertEquals('str', $actual['id']); 99 $this->assertEquals('str', $actual['id']);
95 } 100 }
96 101
97 /** 102// /**
98 * Test updated link event 103// * Test updated link event
99 */ 104// */
100 public function testUpdateLink() 105// public function testUpdateLink()
101 { 106// {
102 $history = new History(self::$historyFilePath); 107// $history = new History(self::$historyFilePath);
103 $history->updateLink(['id' => 1]); 108// $history->updateLink(['id' => 1]);
104 $actual = $history->getHistory()[0]; 109// $actual = $history->getHistory()[0];
105 $this->assertEquals(History::UPDATED, $actual['event']); 110// $this->assertEquals(History::UPDATED, $actual['event']);
106 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 111// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
107 $this->assertEquals(1, $actual['id']); 112// $this->assertEquals(1, $actual['id']);
108 } 113// }
109 114//
110 /** 115// /**
111 * Test delete link event 116// * Test delete link event
112 */ 117// */
113 public function testDeleteLink() 118// public function testDeleteLink()
114 { 119// {
115 $history = new History(self::$historyFilePath); 120// $history = new History(self::$historyFilePath);
116 $history->deleteLink(['id' => 1]); 121// $history->deleteLink(['id' => 1]);
117 $actual = $history->getHistory()[0]; 122// $actual = $history->getHistory()[0];
118 $this->assertEquals(History::DELETED, $actual['event']); 123// $this->assertEquals(History::DELETED, $actual['event']);
119 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 124// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
120 $this->assertEquals(1, $actual['id']); 125// $this->assertEquals(1, $actual['id']);
121 } 126// }
122 127//
123 /** 128// /**
124 * Test updated settings event 129// * Test updated settings event
125 */ 130// */
126 public function testUpdateSettings() 131// public function testUpdateSettings()
127 { 132// {
128 $history = new History(self::$historyFilePath); 133// $history = new History(self::$historyFilePath);
129 $history->updateSettings(); 134// $history->updateSettings();
130 $actual = $history->getHistory()[0]; 135// $actual = $history->getHistory()[0];
131 $this->assertEquals(History::SETTINGS, $actual['event']); 136// $this->assertEquals(History::SETTINGS, $actual['event']);
132 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 137// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
133 $this->assertEmpty($actual['id']); 138// $this->assertEmpty($actual['id']);
134 } 139// }
135 140//
136 /** 141// /**
137 * Make sure that new items are stored at the beginning 142// * Make sure that new items are stored at the beginning
138 */ 143// */
139 public function testHistoryOrder() 144// public function testHistoryOrder()
140 { 145// {
141 $history = new History(self::$historyFilePath); 146// $history = new History(self::$historyFilePath);
142 $history->updateLink(['id' => 1]); 147// $history->updateLink(['id' => 1]);
143 $actual = $history->getHistory()[0]; 148// $actual = $history->getHistory()[0];
144 $this->assertEquals(History::UPDATED, $actual['event']); 149// $this->assertEquals(History::UPDATED, $actual['event']);
145 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 150// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
146 $this->assertEquals(1, $actual['id']); 151// $this->assertEquals(1, $actual['id']);
147 152//
148 $history->addLink(['id' => 1]); 153// $history->addLink(['id' => 1]);
149 $actual = $history->getHistory()[0]; 154// $actual = $history->getHistory()[0];
150 $this->assertEquals(History::CREATED, $actual['event']); 155// $this->assertEquals(History::CREATED, $actual['event']);
151 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 156// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
152 $this->assertEquals(1, $actual['id']); 157// $this->assertEquals(1, $actual['id']);
153 } 158// }
154 159//
155 /** 160// /**
156 * Re-read history from file after writing an event 161// * Re-read history from file after writing an event
157 */ 162// */
158 public function testHistoryRead() 163// public function testHistoryRead()
159 { 164// {
160 $history = new History(self::$historyFilePath); 165// $history = new History(self::$historyFilePath);
161 $history->updateLink(['id' => 1]); 166// $history->updateLink(['id' => 1]);
162 $history = new History(self::$historyFilePath); 167// $history = new History(self::$historyFilePath);
163 $actual = $history->getHistory()[0]; 168// $actual = $history->getHistory()[0];
164 $this->assertEquals(History::UPDATED, $actual['event']); 169// $this->assertEquals(History::UPDATED, $actual['event']);
165 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 170// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
166 $this->assertEquals(1, $actual['id']); 171// $this->assertEquals(1, $actual['id']);
167 } 172// }
168 173//
169 /** 174// /**
170 * Re-read history from file after writing an event and make sure that the order is correct 175// * Re-read history from file after writing an event and make sure that the order is correct
171 */ 176// */
172 public function testHistoryOrderRead() 177// public function testHistoryOrderRead()
173 { 178// {
174 $history = new History(self::$historyFilePath); 179// $history = new History(self::$historyFilePath);
175 $history->updateLink(['id' => 1]); 180// $history->updateLink(['id' => 1]);
176 $history->addLink(['id' => 1]); 181// $history->addLink(['id' => 1]);
177 182//
178 $history = new History(self::$historyFilePath); 183// $history = new History(self::$historyFilePath);
179 $actual = $history->getHistory()[0]; 184// $actual = $history->getHistory()[0];
180 $this->assertEquals(History::CREATED, $actual['event']); 185// $this->assertEquals(History::CREATED, $actual['event']);
181 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 186// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
182 $this->assertEquals(1, $actual['id']); 187// $this->assertEquals(1, $actual['id']);
183 188//
184 $actual = $history->getHistory()[1]; 189// $actual = $history->getHistory()[1];
185 $this->assertEquals(History::UPDATED, $actual['event']); 190// $this->assertEquals(History::UPDATED, $actual['event']);
186 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 191// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
187 $this->assertEquals(1, $actual['id']); 192// $this->assertEquals(1, $actual['id']);
188 } 193// }
189 194//
190 /** 195// /**
191 * Test retention time: delete old entries. 196// * Test retention time: delete old entries.
192 */ 197// */
193 public function testHistoryRententionTime() 198// public function testHistoryRententionTime()
194 { 199// {
195 $history = new History(self::$historyFilePath, 5); 200// $history = new History(self::$historyFilePath, 5);
196 $history->updateLink(['id' => 1]); 201// $history->updateLink(['id' => 1]);
197 $this->assertEquals(1, count($history->getHistory())); 202// $this->assertEquals(1, count($history->getHistory()));
198 $arr = $history->getHistory(); 203// $arr = $history->getHistory();
199 $arr[0]['datetime'] = new DateTime('-1 hour'); 204// $arr[0]['datetime'] = new DateTime('-1 hour');
200 FileUtils::writeFlatDB(self::$historyFilePath, $arr); 205// FileUtils::writeFlatDB(self::$historyFilePath, $arr);
201 206//
202 $history = new History(self::$historyFilePath, 60); 207// $history = new History(self::$historyFilePath, 60);
203 $this->assertEquals(1, count($history->getHistory())); 208// $this->assertEquals(1, count($history->getHistory()));
204 $this->assertEquals(1, $history->getHistory()[0]['id']); 209// $this->assertEquals(1, $history->getHistory()[0]['id']);
205 $history->updateLink(['id' => 2]); 210// $history->updateLink(['id' => 2]);
206 $this->assertEquals(1, count($history->getHistory())); 211// $this->assertEquals(1, count($history->getHistory()));
207 $this->assertEquals(2, $history->getHistory()[0]['id']); 212// $this->assertEquals(2, $history->getHistory()[0]['id']);
208 } 213// }
209} 214}
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 71761ac1..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,58 +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 *
62 * @return void
63 */ 93 */
64 public function testPluginNotFound() 94 public function testPluginNotFound(): void
65 { 95 {
66 $this->pluginManager->load(array()); 96 $this->pluginManager->load([]);
67 $this->pluginManager->load(array('nope', 'renope')); 97 $this->pluginManager->load(['nope', 'renope']);
98 $this->addToAssertionCount(1);
68 } 99 }
69 100
70 /** 101 /**
71 * Test plugin metadata loading. 102 * Test plugin metadata loading.
72 */ 103 */
73 public function testGetPluginsMeta() 104 public function testGetPluginsMeta(): void
74 { 105 {
75 PluginManager::$PLUGINS_PATH = self::$pluginPath; 106 PluginManager::$PLUGINS_PATH = self::$pluginPath;
76 $this->pluginManager->load(array(self::$pluginName)); 107 $this->pluginManager->load([self::$pluginName]);
77 108
78 $expectedParameters = array( 109 $expectedParameters = [
79 'pop' => array( 110 'pop' => [
80 'value' => '', 111 'value' => '',
81 'desc' => 'pop description', 112 'desc' => 'pop description',
82 ), 113 ],
83 'hip' => array( 114 'hip' => [
84 'value' => '', 115 'value' => '',
85 'desc' => '', 116 'desc' => '',
86 ), 117 ],
87 ); 118 ];
88 $meta = $this->pluginManager->getPluginsMeta(); 119 $meta = $this->pluginManager->getPluginsMeta();
89 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']); 120 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
90 $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 8225d95a..6e787d7f 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);
@@ -203,7 +203,7 @@ class UtilsTest extends PHPUnit\Framework\TestCase
203 public function testGenerateLocationLoop() 203 public function testGenerateLocationLoop()
204 { 204 {
205 $ref = 'http://localhost/?test'; 205 $ref = 'http://localhost/?test';
206 $this->assertEquals('?', generateLocation($ref, 'localhost', array('test'))); 206 $this->assertEquals('./?', generateLocation($ref, 'localhost', array('test')));
207 } 207 }
208 208
209 /** 209 /**
@@ -212,7 +212,7 @@ class UtilsTest extends PHPUnit\Framework\TestCase
212 public function testGenerateLocationOut() 212 public function testGenerateLocationOut()
213 { 213 {
214 $ref = 'http://somewebsite.com/?test'; 214 $ref = 'http://somewebsite.com/?test';
215 $this->assertEquals('?', generateLocation($ref, 'localhost')); 215 $this->assertEquals('./?', generateLocation($ref, 'localhost'));
216 } 216 }
217 217
218 218
diff --git a/tests/api/ApiMiddlewareTest.php b/tests/api/ApiMiddlewareTest.php
index 0b9b03f2..86700840 100644
--- a/tests/api/ApiMiddlewareTest.php
+++ b/tests/api/ApiMiddlewareTest.php
@@ -2,6 +2,7 @@
2namespace Shaarli\Api; 2namespace Shaarli\Api;
3 3
4use Shaarli\Config\ConfigManager; 4use Shaarli\Config\ConfigManager;
5use Shaarli\History;
5use Slim\Container; 6use Slim\Container;
6use Slim\Http\Environment; 7use Slim\Http\Environment;
7use Slim\Http\Request; 8use Slim\Http\Request;
@@ -17,7 +18,7 @@ use Slim\Http\Response;
17 * 18 *
18 * @package Api 19 * @package Api
19 */ 20 */
20class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase 21class ApiMiddlewareTest extends \Shaarli\TestCase
21{ 22{
22 /** 23 /**
23 * @var string datastore to test write operations 24 * @var string datastore to test write operations
@@ -25,7 +26,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
25 protected static $testDatastore = 'sandbox/datastore.php'; 26 protected static $testDatastore = 'sandbox/datastore.php';
26 27
27 /** 28 /**
28 * @var \ConfigManager instance 29 * @var ConfigManager instance
29 */ 30 */
30 protected $conf; 31 protected $conf;
31 32
@@ -40,29 +41,79 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
40 protected $container; 41 protected $container;
41 42
42 /** 43 /**
43 * Before every test, instantiate a new Api with its config, plugins and links. 44 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
44 */ 45 */
45 public function setUp() 46 protected function setUp(): void
46 { 47 {
47 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 48 $this->conf = new ConfigManager('tests/utils/config/configJson');
48 $this->conf->set('api.secret', 'NapoleonWasALizard'); 49 $this->conf->set('api.secret', 'NapoleonWasALizard');
49 50
50 $this->refDB = new \ReferenceLinkDB(); 51 $this->refDB = new \ReferenceLinkDB();
51 $this->refDB->write(self::$testDatastore); 52 $this->refDB->write(self::$testDatastore);
52 53
54 $history = new History('sandbox/history.php');
55
53 $this->container = new Container(); 56 $this->container = new Container();
54 $this->container['conf'] = $this->conf; 57 $this->container['conf'] = $this->conf;
58 $this->container['history'] = $history;
55 } 59 }
56 60
57 /** 61 /**
58 * After every test, remove the test datastore. 62 * After every test, remove the test datastore.
59 */ 63 */
60 public function tearDown() 64 protected function tearDown(): void
61 { 65 {
62 @unlink(self::$testDatastore); 66 @unlink(self::$testDatastore);
63 } 67 }
64 68
65 /** 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 /**
66 * Invoke the middleware with the API disabled: 117 * Invoke the middleware with the API disabled:
67 * should return a 401 error Unauthorized. 118 * should return a 401 error Unauthorized.
68 */ 119 */
@@ -105,7 +156,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
105 $this->assertEquals(401, $response->getStatusCode()); 156 $this->assertEquals(401, $response->getStatusCode());
106 $body = json_decode((string) $response->getBody()); 157 $body = json_decode((string) $response->getBody());
107 $this->assertEquals('Not authorized: API is disabled', $body->message); 158 $this->assertEquals('Not authorized: API is disabled', $body->message);
108 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 159 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
109 } 160 }
110 161
111 /** 162 /**
@@ -128,7 +179,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
128 $this->assertEquals(401, $response->getStatusCode()); 179 $this->assertEquals(401, $response->getStatusCode());
129 $body = json_decode((string) $response->getBody()); 180 $body = json_decode((string) $response->getBody());
130 $this->assertEquals('Not authorized: JWT token not provided', $body->message); 181 $this->assertEquals('Not authorized: JWT token not provided', $body->message);
131 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 182 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
132 } 183 }
133 184
134 /** 185 /**
@@ -153,7 +204,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
153 $this->assertEquals(401, $response->getStatusCode()); 204 $this->assertEquals(401, $response->getStatusCode());
154 $body = json_decode((string) $response->getBody()); 205 $body = json_decode((string) $response->getBody());
155 $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);
156 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 207 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
157 } 208 }
158 209
159 /** 210 /**
@@ -176,7 +227,7 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
176 $this->assertEquals(401, $response->getStatusCode()); 227 $this->assertEquals(401, $response->getStatusCode());
177 $body = json_decode((string) $response->getBody()); 228 $body = json_decode((string) $response->getBody());
178 $this->assertEquals('Not authorized: Invalid JWT header', $body->message); 229 $this->assertEquals('Not authorized: Invalid JWT header', $body->message);
179 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 230 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
180 } 231 }
181 232
182 /** 233 /**
@@ -202,6 +253,6 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
202 $this->assertEquals(401, $response->getStatusCode()); 253 $this->assertEquals(401, $response->getStatusCode());
203 $body = json_decode((string) $response->getBody()); 254 $body = json_decode((string) $response->getBody());
204 $this->assertEquals('Not authorized: Malformed JWT token', $body->message); 255 $this->assertEquals('Not authorized: Malformed JWT token', $body->message);
205 $this->assertContains('ApiAuthorizationException', $body->stacktrace); 256 $this->assertContainsPolyfill('ApiAuthorizationException', $body->stacktrace);
206 } 257 }
207} 258}
diff --git a/tests/api/ApiUtilsTest.php b/tests/api/ApiUtilsTest.php
index ea0ae500..7a143859 100644
--- a/tests/api/ApiUtilsTest.php
+++ b/tests/api/ApiUtilsTest.php
@@ -2,17 +2,18 @@
2 2
3namespace Shaarli\Api; 3namespace Shaarli\Api;
4 4
5use Shaarli\Bookmark\Bookmark;
5use Shaarli\Http\Base64Url; 6use Shaarli\Http\Base64Url;
6 7
7/** 8/**
8 * Class ApiUtilsTest 9 * Class ApiUtilsTest
9 */ 10 */
10class ApiUtilsTest extends \PHPUnit\Framework\TestCase 11class ApiUtilsTest extends \Shaarli\TestCase
11{ 12{
12 /** 13 /**
13 * Force the timezone for ISO datetimes. 14 * Force the timezone for ISO datetimes.
14 */ 15 */
15 public static function setUpBeforeClass() 16 public static function setUpBeforeClass(): void
16 { 17 {
17 date_default_timezone_set('UTC'); 18 date_default_timezone_set('UTC');
18 } 19 }
@@ -60,148 +61,148 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
60 public function testValidateJwtTokenValid() 61 public function testValidateJwtTokenValid()
61 { 62 {
62 $secret = 'WarIsPeace'; 63 $secret = 'WarIsPeace';
63 ApiUtils::validateJwtToken(self::generateValidJwtToken($secret), $secret); 64 $this->assertTrue(ApiUtils::validateJwtToken(self::generateValidJwtToken($secret), $secret));
64 } 65 }
65 66
66 /** 67 /**
67 * Test validateJwtToken() with a malformed JWT token. 68 * Test validateJwtToken() with a malformed JWT token.
68 *
69 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
70 * @expectedExceptionMessage Malformed JWT token
71 */ 69 */
72 public function testValidateJwtTokenMalformed() 70 public function testValidateJwtTokenMalformed()
73 { 71 {
72 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
73 $this->expectExceptionMessage('Malformed JWT token');
74
74 $token = 'ABC.DEF'; 75 $token = 'ABC.DEF';
75 ApiUtils::validateJwtToken($token, 'foo'); 76 ApiUtils::validateJwtToken($token, 'foo');
76 } 77 }
77 78
78 /** 79 /**
79 * Test validateJwtToken() with an empty JWT token. 80 * Test validateJwtToken() with an empty JWT token.
80 *
81 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
82 * @expectedExceptionMessage Malformed JWT token
83 */ 81 */
84 public function testValidateJwtTokenMalformedEmpty() 82 public function testValidateJwtTokenMalformedEmpty()
85 { 83 {
84 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
85 $this->expectExceptionMessage('Malformed JWT token');
86
86 $token = false; 87 $token = false;
87 ApiUtils::validateJwtToken($token, 'foo'); 88 ApiUtils::validateJwtToken($token, 'foo');
88 } 89 }
89 90
90 /** 91 /**
91 * Test validateJwtToken() with a JWT token without header. 92 * Test validateJwtToken() with a JWT token without header.
92 *
93 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
94 * @expectedExceptionMessage Malformed JWT token
95 */ 93 */
96 public function testValidateJwtTokenMalformedEmptyHeader() 94 public function testValidateJwtTokenMalformedEmptyHeader()
97 { 95 {
96 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
97 $this->expectExceptionMessage('Malformed JWT token');
98
98 $token = '.payload.signature'; 99 $token = '.payload.signature';
99 ApiUtils::validateJwtToken($token, 'foo'); 100 ApiUtils::validateJwtToken($token, 'foo');
100 } 101 }
101 102
102 /** 103 /**
103 * Test validateJwtToken() with a JWT token without payload 104 * Test validateJwtToken() with a JWT token without payload
104 *
105 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
106 * @expectedExceptionMessage Malformed JWT token
107 */ 105 */
108 public function testValidateJwtTokenMalformedEmptyPayload() 106 public function testValidateJwtTokenMalformedEmptyPayload()
109 { 107 {
108 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
109 $this->expectExceptionMessage('Malformed JWT token');
110
110 $token = 'header..signature'; 111 $token = 'header..signature';
111 ApiUtils::validateJwtToken($token, 'foo'); 112 ApiUtils::validateJwtToken($token, 'foo');
112 } 113 }
113 114
114 /** 115 /**
115 * Test validateJwtToken() with a JWT token with an empty signature. 116 * Test validateJwtToken() with a JWT token with an empty signature.
116 *
117 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
118 * @expectedExceptionMessage Invalid JWT signature
119 */ 117 */
120 public function testValidateJwtTokenInvalidSignatureEmpty() 118 public function testValidateJwtTokenInvalidSignatureEmpty()
121 { 119 {
120 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
121 $this->expectExceptionMessage('Invalid JWT signature');
122
122 $token = 'header.payload.'; 123 $token = 'header.payload.';
123 ApiUtils::validateJwtToken($token, 'foo'); 124 ApiUtils::validateJwtToken($token, 'foo');
124 } 125 }
125 126
126 /** 127 /**
127 * Test validateJwtToken() with a JWT token with an invalid signature. 128 * Test validateJwtToken() with a JWT token with an invalid signature.
128 *
129 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
130 * @expectedExceptionMessage Invalid JWT signature
131 */ 129 */
132 public function testValidateJwtTokenInvalidSignature() 130 public function testValidateJwtTokenInvalidSignature()
133 { 131 {
132 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
133 $this->expectExceptionMessage('Invalid JWT signature');
134
134 $token = 'header.payload.nope'; 135 $token = 'header.payload.nope';
135 ApiUtils::validateJwtToken($token, 'foo'); 136 ApiUtils::validateJwtToken($token, 'foo');
136 } 137 }
137 138
138 /** 139 /**
139 * 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.
140 *
141 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
142 * @expectedExceptionMessage Invalid JWT signature
143 */ 141 */
144 public function testValidateJwtTokenInvalidSignatureSecret() 142 public function testValidateJwtTokenInvalidSignatureSecret()
145 { 143 {
144 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
145 $this->expectExceptionMessage('Invalid JWT signature');
146
146 ApiUtils::validateJwtToken(self::generateValidJwtToken('foo'), 'bar'); 147 ApiUtils::validateJwtToken(self::generateValidJwtToken('foo'), 'bar');
147 } 148 }
148 149
149 /** 150 /**
150 * 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).
151 *
152 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
153 * @expectedExceptionMessage Invalid JWT header
154 */ 152 */
155 public function testValidateJwtTokenInvalidHeader() 153 public function testValidateJwtTokenInvalidHeader()
156 { 154 {
155 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
156 $this->expectExceptionMessage('Invalid JWT header');
157
157 $token = $this->generateCustomJwtToken('notJSON', '{"JSON":1}', 'secret'); 158 $token = $this->generateCustomJwtToken('notJSON', '{"JSON":1}', 'secret');
158 ApiUtils::validateJwtToken($token, 'secret'); 159 ApiUtils::validateJwtToken($token, 'secret');
159 } 160 }
160 161
161 /** 162 /**
162 * 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).
163 *
164 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
165 * @expectedExceptionMessage Invalid JWT payload
166 */ 164 */
167 public function testValidateJwtTokenInvalidPayload() 165 public function testValidateJwtTokenInvalidPayload()
168 { 166 {
167 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
168 $this->expectExceptionMessage('Invalid JWT payload');
169
169 $token = $this->generateCustomJwtToken('{"JSON":1}', 'notJSON', 'secret'); 170 $token = $this->generateCustomJwtToken('{"JSON":1}', 'notJSON', 'secret');
170 ApiUtils::validateJwtToken($token, 'secret'); 171 ApiUtils::validateJwtToken($token, 'secret');
171 } 172 }
172 173
173 /** 174 /**
174 * Test validateJwtToken() with a JWT token without issued time. 175 * Test validateJwtToken() with a JWT token without issued time.
175 *
176 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
177 * @expectedExceptionMessage Invalid JWT issued time
178 */ 176 */
179 public function testValidateJwtTokenInvalidTimeEmpty() 177 public function testValidateJwtTokenInvalidTimeEmpty()
180 { 178 {
179 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
180 $this->expectExceptionMessage('Invalid JWT issued time');
181
181 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"JSON":1}', 'secret'); 182 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"JSON":1}', 'secret');
182 ApiUtils::validateJwtToken($token, 'secret'); 183 ApiUtils::validateJwtToken($token, 'secret');
183 } 184 }
184 185
185 /** 186 /**
186 * Test validateJwtToken() with an expired JWT token. 187 * Test validateJwtToken() with an expired JWT token.
187 *
188 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
189 * @expectedExceptionMessage Invalid JWT issued time
190 */ 188 */
191 public function testValidateJwtTokenInvalidTimeExpired() 189 public function testValidateJwtTokenInvalidTimeExpired()
192 { 190 {
191 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
192 $this->expectExceptionMessage('Invalid JWT issued time');
193
193 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() - 600) . '}', 'secret'); 194 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() - 600) . '}', 'secret');
194 ApiUtils::validateJwtToken($token, 'secret'); 195 ApiUtils::validateJwtToken($token, 'secret');
195 } 196 }
196 197
197 /** 198 /**
198 * Test validateJwtToken() with a JWT token issued in the future. 199 * Test validateJwtToken() with a JWT token issued in the future.
199 *
200 * @expectedException \Shaarli\Api\Exceptions\ApiAuthorizationException
201 * @expectedExceptionMessage Invalid JWT issued time
202 */ 200 */
203 public function testValidateJwtTokenInvalidTimeFuture() 201 public function testValidateJwtTokenInvalidTimeFuture()
204 { 202 {
203 $this->expectException(\Shaarli\Api\Exceptions\ApiAuthorizationException::class);
204 $this->expectExceptionMessage('Invalid JWT issued time');
205
205 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() + 60) . '}', 'secret'); 206 $token = $this->generateCustomJwtToken('{"JSON":1}', '{"iat":' . (time() + 60) . '}', 'secret');
206 ApiUtils::validateJwtToken($token, 'secret'); 207 ApiUtils::validateJwtToken($token, 'secret');
207 } 208 }
@@ -212,7 +213,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
212 public function testFormatLinkComplete() 213 public function testFormatLinkComplete()
213 { 214 {
214 $indexUrl = 'https://domain.tld/sub/'; 215 $indexUrl = 'https://domain.tld/sub/';
215 $link = [ 216 $data = [
216 'id' => 12, 217 'id' => 12,
217 'url' => 'http://lol.lol', 218 'url' => 'http://lol.lol',
218 'shorturl' => 'abc', 219 'shorturl' => 'abc',
@@ -223,6 +224,8 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
223 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'), 224 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'),
224 'updated' => \DateTime::createFromFormat('Ymd_His', '20170107_160612'), 225 'updated' => \DateTime::createFromFormat('Ymd_His', '20170107_160612'),
225 ]; 226 ];
227 $bookmark = new Bookmark();
228 $bookmark->fromArray($data);
226 229
227 $expected = [ 230 $expected = [
228 'id' => 12, 231 'id' => 12,
@@ -236,7 +239,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
236 'updated' => '2017-01-07T16:06:12+00:00', 239 'updated' => '2017-01-07T16:06:12+00:00',
237 ]; 240 ];
238 241
239 $this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); 242 $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
240 } 243 }
241 244
242 /** 245 /**
@@ -245,7 +248,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
245 public function testFormatLinkMinimalNote() 248 public function testFormatLinkMinimalNote()
246 { 249 {
247 $indexUrl = 'https://domain.tld/sub/'; 250 $indexUrl = 'https://domain.tld/sub/';
248 $link = [ 251 $data = [
249 'id' => 12, 252 'id' => 12,
250 'url' => '?abc', 253 'url' => '?abc',
251 'shorturl' => 'abc', 254 'shorturl' => 'abc',
@@ -255,6 +258,8 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
255 'private' => '', 258 'private' => '',
256 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'), 259 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'),
257 ]; 260 ];
261 $bookmark = new Bookmark();
262 $bookmark->fromArray($data);
258 263
259 $expected = [ 264 $expected = [
260 'id' => 12, 265 'id' => 12,
@@ -268,7 +273,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
268 'updated' => '', 273 'updated' => '',
269 ]; 274 ];
270 275
271 $this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); 276 $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
272 } 277 }
273 278
274 /** 279 /**
@@ -277,7 +282,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
277 public function testUpdateLink() 282 public function testUpdateLink()
278 { 283 {
279 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); 284 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
280 $old = [ 285 $data = [
281 'id' => 12, 286 'id' => 12,
282 'url' => '?abc', 287 'url' => '?abc',
283 'shorturl' => 'abc', 288 'shorturl' => 'abc',
@@ -287,8 +292,10 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
287 'private' => '', 292 'private' => '',
288 'created' => $created, 293 'created' => $created,
289 ]; 294 ];
295 $old = new Bookmark();
296 $old->fromArray($data);
290 297
291 $new = [ 298 $data = [
292 'id' => 13, 299 'id' => 13,
293 'shorturl' => 'nope', 300 'shorturl' => 'nope',
294 'url' => 'http://somewhere.else', 301 'url' => 'http://somewhere.else',
@@ -299,17 +306,18 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
299 'created' => 'creation', 306 'created' => 'creation',
300 'updated' => 'updation', 307 'updated' => 'updation',
301 ]; 308 ];
309 $new = new Bookmark();
310 $new->fromArray($data);
302 311
303 $result = ApiUtils::updateLink($old, $new); 312 $result = ApiUtils::updateLink($old, $new);
304 $this->assertEquals(12, $result['id']); 313 $this->assertEquals(12, $result->getId());
305 $this->assertEquals('http://somewhere.else', $result['url']); 314 $this->assertEquals('http://somewhere.else', $result->getUrl());
306 $this->assertEquals('abc', $result['shorturl']); 315 $this->assertEquals('abc', $result->getShortUrl());
307 $this->assertEquals('Le Cid', $result['title']); 316 $this->assertEquals('Le Cid', $result->getTitle());
308 $this->assertEquals('Percé jusques au fond du cœur [...]', $result['description']); 317 $this->assertEquals('Percé jusques au fond du cœur [...]', $result->getDescription());
309 $this->assertEquals('corneille rodrigue', $result['tags']); 318 $this->assertEquals('corneille rodrigue', $result->getTagsString());
310 $this->assertEquals(true, $result['private']); 319 $this->assertEquals(true, $result->isPrivate());
311 $this->assertEquals($created, $result['created']); 320 $this->assertEquals($created, $result->getCreated());
312 $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']);
313 } 321 }
314 322
315 /** 323 /**
@@ -318,7 +326,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
318 public function testUpdateLinkMinimal() 326 public function testUpdateLinkMinimal()
319 { 327 {
320 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); 328 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
321 $old = [ 329 $data = [
322 'id' => 12, 330 'id' => 12,
323 'url' => '?abc', 331 'url' => '?abc',
324 'shorturl' => 'abc', 332 'shorturl' => 'abc',
@@ -328,24 +336,19 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
328 'private' => true, 336 'private' => true,
329 'created' => $created, 337 'created' => $created,
330 ]; 338 ];
339 $old = new Bookmark();
340 $old->fromArray($data);
331 341
332 $new = [ 342 $new = new Bookmark();
333 'url' => '',
334 'title' => '',
335 'description' => '',
336 'tags' => '',
337 'private' => false,
338 ];
339 343
340 $result = ApiUtils::updateLink($old, $new); 344 $result = ApiUtils::updateLink($old, $new);
341 $this->assertEquals(12, $result['id']); 345 $this->assertEquals(12, $result->getId());
342 $this->assertEquals('?abc', $result['url']); 346 $this->assertEquals('', $result->getUrl());
343 $this->assertEquals('abc', $result['shorturl']); 347 $this->assertEquals('abc', $result->getShortUrl());
344 $this->assertEquals('?abc', $result['title']); 348 $this->assertEquals('', $result->getTitle());
345 $this->assertEquals('', $result['description']); 349 $this->assertEquals('', $result->getDescription());
346 $this->assertEquals('', $result['tags']); 350 $this->assertEquals('', $result->getTagsString());
347 $this->assertEquals(false, $result['private']); 351 $this->assertEquals(false, $result->isPrivate());
348 $this->assertEquals($created, $result['created']); 352 $this->assertEquals($created, $result->getCreated());
349 $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']);
350 } 353 }
351} 354}
diff --git a/tests/api/controllers/history/HistoryTest.php b/tests/api/controllers/history/HistoryTest.php
index e287f239..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
@@ -39,11 +39,11 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
39 protected $controller; 39 protected $controller;
40 40
41 /** 41 /**
42 * Before every test, instantiate a new Api with its config, plugins and links. 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.json.php'); 46 $this->conf = new ConfigManager('tests/utils/config/configJson');
47 $this->refHistory = new \ReferenceHistory(); 47 $this->refHistory = new \ReferenceHistory();
48 $this->refHistory->write(self::$testHistory); 48 $this->refHistory->write(self::$testHistory);
49 $this->container = new Container(); 49 $this->container = new Container();
@@ -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 e70d371b..1598e1e8 100644
--- a/tests/api/controllers/info/InfoTest.php
+++ b/tests/api/controllers/info/InfoTest.php
@@ -1,7 +1,10 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use Shaarli\Bookmark\BookmarkFileService;
4use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
6use Shaarli\History;
7use Shaarli\TestCase;
5use Slim\Container; 8use Slim\Container;
6use Slim\Http\Environment; 9use Slim\Http\Environment;
7use Slim\Http\Request; 10use Slim\Http\Request;
@@ -14,7 +17,7 @@ use Slim\Http\Response;
14 * 17 *
15 * @package Api\Controllers 18 * @package Api\Controllers
16 */ 19 */
17class InfoTest extends \PHPUnit\Framework\TestCase 20class InfoTest extends TestCase
18{ 21{
19 /** 22 /**
20 * @var string datastore to test write operations 23 * @var string datastore to test write operations
@@ -42,17 +45,20 @@ class InfoTest extends \PHPUnit\Framework\TestCase
42 protected $controller; 45 protected $controller;
43 46
44 /** 47 /**
45 * Before every test, instantiate a new Api with its config, plugins and links. 48 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
46 */ 49 */
47 public function setUp() 50 protected function setUp(): void
48 { 51 {
49 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 52 $this->conf = new ConfigManager('tests/utils/config/configJson');
53 $this->conf->set('resource.datastore', self::$testDatastore);
50 $this->refDB = new \ReferenceLinkDB(); 54 $this->refDB = new \ReferenceLinkDB();
51 $this->refDB->write(self::$testDatastore); 55 $this->refDB->write(self::$testDatastore);
52 56
57 $history = new History('sandbox/history.php');
58
53 $this->container = new Container(); 59 $this->container = new Container();
54 $this->container['conf'] = $this->conf; 60 $this->container['conf'] = $this->conf;
55 $this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); 61 $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
56 $this->container['history'] = null; 62 $this->container['history'] = null;
57 63
58 $this->controller = new Info($this->container); 64 $this->controller = new Info($this->container);
@@ -61,7 +67,7 @@ class InfoTest extends \PHPUnit\Framework\TestCase
61 /** 67 /**
62 * After every test, remove the test datastore. 68 * After every test, remove the test datastore.
63 */ 69 */
64 public function tearDown() 70 protected function tearDown(): void
65 { 71 {
66 @unlink(self::$testDatastore); 72 @unlink(self::$testDatastore);
67 } 73 }
@@ -84,11 +90,11 @@ class InfoTest extends \PHPUnit\Framework\TestCase
84 $this->assertEquals(2, $data['private_counter']); 90 $this->assertEquals(2, $data['private_counter']);
85 $this->assertEquals('Shaarli', $data['settings']['title']); 91 $this->assertEquals('Shaarli', $data['settings']['title']);
86 $this->assertEquals('?', $data['settings']['header_link']); 92 $this->assertEquals('?', $data['settings']['header_link']);
87 $this->assertEquals('UTC', $data['settings']['timezone']); 93 $this->assertEquals('Europe/Paris', $data['settings']['timezone']);
88 $this->assertEquals(ConfigManager::$DEFAULT_PLUGINS, $data['settings']['enabled_plugins']); 94 $this->assertEquals(ConfigManager::$DEFAULT_PLUGINS, $data['settings']['enabled_plugins']);
89 $this->assertEquals(false, $data['settings']['default_private_links']); 95 $this->assertEquals(true, $data['settings']['default_private_links']);
90 96
91 $title = 'My links'; 97 $title = 'My bookmarks';
92 $headerLink = 'http://shaarli.tld'; 98 $headerLink = 'http://shaarli.tld';
93 $timezone = 'Europe/Paris'; 99 $timezone = 'Europe/Paris';
94 $enabledPlugins = array('foo', 'bar'); 100 $enabledPlugins = array('foo', 'bar');
diff --git a/tests/api/controllers/links/DeleteLinkTest.php b/tests/api/controllers/links/DeleteLinkTest.php
index 90193e28..cf9464f0 100644
--- a/tests/api/controllers/links/DeleteLinkTest.php
+++ b/tests/api/controllers/links/DeleteLinkTest.php
@@ -3,7 +3,7 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use Shaarli\Bookmark\LinkDB; 6use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History; 8use Shaarli\History;
9use Slim\Container; 9use Slim\Container;
@@ -11,7 +11,7 @@ use Slim\Http\Environment;
11use Slim\Http\Request; 11use Slim\Http\Request;
12use Slim\Http\Response; 12use Slim\Http\Response;
13 13
14class DeleteLinkTest extends \PHPUnit\Framework\TestCase 14class DeleteLinkTest extends \Shaarli\TestCase
15{ 15{
16 /** 16 /**
17 * @var string datastore to test write operations 17 * @var string datastore to test write operations
@@ -34,9 +34,9 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
34 protected $refDB = null; 34 protected $refDB = null;
35 35
36 /** 36 /**
37 * @var LinkDB instance. 37 * @var BookmarkFileService instance.
38 */ 38 */
39 protected $linkDB; 39 protected $bookmarkService;
40 40
41 /** 41 /**
42 * @var HistoryController instance. 42 * @var HistoryController instance.
@@ -54,20 +54,22 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
54 protected $controller; 54 protected $controller;
55 55
56 /** 56 /**
57 * Before each test, instantiate a new Api with its config, plugins and links. 57 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
58 */ 58 */
59 public function setUp() 59 protected function setUp(): void
60 { 60 {
61 $this->conf = new ConfigManager('tests/utils/config/configJson'); 61 $this->conf = new ConfigManager('tests/utils/config/configJson');
62 $this->conf->set('resource.datastore', self::$testDatastore);
62 $this->refDB = new \ReferenceLinkDB(); 63 $this->refDB = new \ReferenceLinkDB();
63 $this->refDB->write(self::$testDatastore); 64 $this->refDB->write(self::$testDatastore);
64 $this->linkDB = new LinkDB(self::$testDatastore, true, false);
65 $refHistory = new \ReferenceHistory(); 65 $refHistory = new \ReferenceHistory();
66 $refHistory->write(self::$testHistory); 66 $refHistory->write(self::$testHistory);
67 $this->history = new History(self::$testHistory); 67 $this->history = new History(self::$testHistory);
68 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
69
68 $this->container = new Container(); 70 $this->container = new Container();
69 $this->container['conf'] = $this->conf; 71 $this->container['conf'] = $this->conf;
70 $this->container['db'] = $this->linkDB; 72 $this->container['db'] = $this->bookmarkService;
71 $this->container['history'] = $this->history; 73 $this->container['history'] = $this->history;
72 74
73 $this->controller = new Links($this->container); 75 $this->controller = new Links($this->container);
@@ -76,7 +78,7 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
76 /** 78 /**
77 * After each test, remove the test datastore. 79 * After each test, remove the test datastore.
78 */ 80 */
79 public function tearDown() 81 protected function tearDown(): void
80 { 82 {
81 @unlink(self::$testDatastore); 83 @unlink(self::$testDatastore);
82 @unlink(self::$testHistory); 84 @unlink(self::$testHistory);
@@ -88,7 +90,7 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
88 public function testDeleteLinkValid() 90 public function testDeleteLinkValid()
89 { 91 {
90 $id = '41'; 92 $id = '41';
91 $this->assertTrue(isset($this->linkDB[$id])); 93 $this->assertTrue($this->bookmarkService->exists($id));
92 $env = Environment::mock([ 94 $env = Environment::mock([
93 'REQUEST_METHOD' => 'DELETE', 95 'REQUEST_METHOD' => 'DELETE',
94 ]); 96 ]);
@@ -98,8 +100,8 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
98 $this->assertEquals(204, $response->getStatusCode()); 100 $this->assertEquals(204, $response->getStatusCode());
99 $this->assertEmpty((string) $response->getBody()); 101 $this->assertEmpty((string) $response->getBody());
100 102
101 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 103 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
102 $this->assertFalse(isset($this->linkDB[$id])); 104 $this->assertFalse($this->bookmarkService->exists($id));
103 105
104 $historyEntry = $this->history->getHistory()[0]; 106 $historyEntry = $this->history->getHistory()[0];
105 $this->assertEquals(History::DELETED, $historyEntry['event']); 107 $this->assertEquals(History::DELETED, $historyEntry['event']);
@@ -111,13 +113,13 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
111 113
112 /** 114 /**
113 * Test DELETE link endpoint: reach not existing ID. 115 * Test DELETE link endpoint: reach not existing ID.
114 *
115 * @expectedException \Shaarli\Api\Exceptions\ApiLinkNotFoundException
116 */ 116 */
117 public function testDeleteLink404() 117 public function testDeleteLink404()
118 { 118 {
119 $this->expectException(\Shaarli\Api\Exceptions\ApiLinkNotFoundException::class);
120
119 $id = -1; 121 $id = -1;
120 $this->assertFalse(isset($this->linkDB[$id])); 122 $this->assertFalse($this->bookmarkService->exists($id));
121 $env = Environment::mock([ 123 $env = Environment::mock([
122 'REQUEST_METHOD' => 'DELETE', 124 'REQUEST_METHOD' => 'DELETE',
123 ]); 125 ]);
diff --git a/tests/api/controllers/links/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php
index cb9b7f6a..99dc606f 100644
--- a/tests/api/controllers/links/GetLinkIdTest.php
+++ b/tests/api/controllers/links/GetLinkIdTest.php
@@ -2,7 +2,10 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
6use Slim\Container; 9use Slim\Container;
7use Slim\Http\Environment; 10use Slim\Http\Environment;
8use Slim\Http\Request; 11use Slim\Http\Request;
@@ -17,7 +20,7 @@ use Slim\Http\Response;
17 * 20 *
18 * @package Shaarli\Api\Controllers 21 * @package Shaarli\Api\Controllers
19 */ 22 */
20class GetLinkIdTest extends \PHPUnit\Framework\TestCase 23class GetLinkIdTest extends \Shaarli\TestCase
21{ 24{
22 /** 25 /**
23 * @var string datastore to test write operations 26 * @var string datastore to test write operations
@@ -50,17 +53,19 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
50 const NB_FIELDS_LINK = 9; 53 const NB_FIELDS_LINK = 9;
51 54
52 /** 55 /**
53 * Before each test, instantiate a new Api with its config, plugins and links. 56 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
54 */ 57 */
55 public function setUp() 58 protected function setUp(): void
56 { 59 {
57 $this->conf = new ConfigManager('tests/utils/config/configJson'); 60 $this->conf = new ConfigManager('tests/utils/config/configJson');
61 $this->conf->set('resource.datastore', self::$testDatastore);
58 $this->refDB = new \ReferenceLinkDB(); 62 $this->refDB = new \ReferenceLinkDB();
59 $this->refDB->write(self::$testDatastore); 63 $this->refDB->write(self::$testDatastore);
64 $history = new History('sandbox/history.php');
60 65
61 $this->container = new Container(); 66 $this->container = new Container();
62 $this->container['conf'] = $this->conf; 67 $this->container['conf'] = $this->conf;
63 $this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); 68 $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
64 $this->container['history'] = null; 69 $this->container['history'] = null;
65 70
66 $this->controller = new Links($this->container); 71 $this->controller = new Links($this->container);
@@ -69,7 +74,7 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
69 /** 74 /**
70 * After each test, remove the test datastore. 75 * After each test, remove the test datastore.
71 */ 76 */
72 public function tearDown() 77 protected function tearDown(): void
73 { 78 {
74 @unlink(self::$testDatastore); 79 @unlink(self::$testDatastore);
75 } 80 }
@@ -97,7 +102,7 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
97 $this->assertEquals($id, $data['id']); 102 $this->assertEquals($id, $data['id']);
98 103
99 // Check link elements 104 // Check link elements
100 $this->assertEquals('http://domain.tld/?WDWyig', $data['url']); 105 $this->assertEquals('http://domain.tld/shaare/WDWyig', $data['url']);
101 $this->assertEquals('WDWyig', $data['shorturl']); 106 $this->assertEquals('WDWyig', $data['shorturl']);
102 $this->assertEquals('Link title: @website', $data['title']); 107 $this->assertEquals('Link title: @website', $data['title']);
103 $this->assertEquals( 108 $this->assertEquals(
@@ -107,7 +112,7 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
107 $this->assertEquals('sTuff', $data['tags'][0]); 112 $this->assertEquals('sTuff', $data['tags'][0]);
108 $this->assertEquals(false, $data['private']); 113 $this->assertEquals(false, $data['private']);
109 $this->assertEquals( 114 $this->assertEquals(
110 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM), 115 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM),
111 $data['created'] 116 $data['created']
112 ); 117 );
113 $this->assertEmpty($data['updated']); 118 $this->assertEmpty($data['updated']);
@@ -115,12 +120,12 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
115 120
116 /** 121 /**
117 * Test basic getLink service: get non existent link => ApiLinkNotFoundException. 122 * Test basic getLink service: get non existent link => ApiLinkNotFoundException.
118 *
119 * @expectedException Shaarli\Api\Exceptions\ApiLinkNotFoundException
120 * @expectedExceptionMessage Link not found
121 */ 123 */
122 public function testGetLink404() 124 public function testGetLink404()
123 { 125 {
126 $this->expectException(\Shaarli\Api\Exceptions\ApiLinkNotFoundException::class);
127 $this->expectExceptionMessage('Link not found');
128
124 $env = Environment::mock([ 129 $env = Environment::mock([
125 'REQUEST_METHOD' => 'GET', 130 'REQUEST_METHOD' => 'GET',
126 ]); 131 ]);
diff --git a/tests/api/controllers/links/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php
index 711a3152..ca1bfc63 100644
--- a/tests/api/controllers/links/GetLinksTest.php
+++ b/tests/api/controllers/links/GetLinksTest.php
@@ -1,8 +1,11 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use Shaarli\Bookmark\Bookmark;
5use Shaarli\Bookmark\BookmarkFileService;
4use Shaarli\Bookmark\LinkDB; 6use Shaarli\Bookmark\LinkDB;
5use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
6use Slim\Container; 9use Slim\Container;
7use Slim\Http\Environment; 10use Slim\Http\Environment;
8use Slim\Http\Request; 11use Slim\Http\Request;
@@ -17,7 +20,7 @@ use Slim\Http\Response;
17 * 20 *
18 * @package Shaarli\Api\Controllers 21 * @package Shaarli\Api\Controllers
19 */ 22 */
20class GetLinksTest extends \PHPUnit\Framework\TestCase 23class GetLinksTest extends \Shaarli\TestCase
21{ 24{
22 /** 25 /**
23 * @var string datastore to test write operations 26 * @var string datastore to test write operations
@@ -50,17 +53,19 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
50 const NB_FIELDS_LINK = 9; 53 const NB_FIELDS_LINK = 9;
51 54
52 /** 55 /**
53 * Before every test, instantiate a new Api with its config, plugins and links. 56 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
54 */ 57 */
55 public function setUp() 58 protected function setUp(): void
56 { 59 {
57 $this->conf = new ConfigManager('tests/utils/config/configJson'); 60 $this->conf = new ConfigManager('tests/utils/config/configJson');
61 $this->conf->set('resource.datastore', self::$testDatastore);
58 $this->refDB = new \ReferenceLinkDB(); 62 $this->refDB = new \ReferenceLinkDB();
59 $this->refDB->write(self::$testDatastore); 63 $this->refDB->write(self::$testDatastore);
64 $history = new History('sandbox/history.php');
60 65
61 $this->container = new Container(); 66 $this->container = new Container();
62 $this->container['conf'] = $this->conf; 67 $this->container['conf'] = $this->conf;
63 $this->container['db'] = new LinkDB(self::$testDatastore, true, false); 68 $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
64 $this->container['history'] = null; 69 $this->container['history'] = null;
65 70
66 $this->controller = new Links($this->container); 71 $this->controller = new Links($this->container);
@@ -69,13 +74,13 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
69 /** 74 /**
70 * After every test, remove the test datastore. 75 * After every test, remove the test datastore.
71 */ 76 */
72 public function tearDown() 77 protected function tearDown(): void
73 { 78 {
74 @unlink(self::$testDatastore); 79 @unlink(self::$testDatastore);
75 } 80 }
76 81
77 /** 82 /**
78 * Test basic getLinks service: returns all links. 83 * Test basic getLinks service: returns all bookmarks.
79 */ 84 */
80 public function testGetLinks() 85 public function testGetLinks()
81 { 86 {
@@ -104,7 +109,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
104 109
105 // Check first element fields 110 // Check first element fields
106 $first = $data[2]; 111 $first = $data[2];
107 $this->assertEquals('http://domain.tld/?WDWyig', $first['url']); 112 $this->assertEquals('http://domain.tld/shaare/WDWyig', $first['url']);
108 $this->assertEquals('WDWyig', $first['shorturl']); 113 $this->assertEquals('WDWyig', $first['shorturl']);
109 $this->assertEquals('Link title: @website', $first['title']); 114 $this->assertEquals('Link title: @website', $first['title']);
110 $this->assertEquals( 115 $this->assertEquals(
@@ -114,7 +119,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
114 $this->assertEquals('sTuff', $first['tags'][0]); 119 $this->assertEquals('sTuff', $first['tags'][0]);
115 $this->assertEquals(false, $first['private']); 120 $this->assertEquals(false, $first['private']);
116 $this->assertEquals( 121 $this->assertEquals(
117 \DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM), 122 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM),
118 $first['created'] 123 $first['created']
119 ); 124 );
120 $this->assertEmpty($first['updated']); 125 $this->assertEmpty($first['updated']);
@@ -125,7 +130,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
125 130
126 // Update date 131 // Update date
127 $this->assertEquals( 132 $this->assertEquals(
128 \DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160803_093033')->format(\DateTime::ATOM), 133 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160803_093033')->format(\DateTime::ATOM),
129 $link['updated'] 134 $link['updated']
130 ); 135 );
131 } 136 }
diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php
index d683a984..fe3de66f 100644
--- a/tests/api/controllers/links/PostLinkTest.php
+++ b/tests/api/controllers/links/PostLinkTest.php
@@ -2,9 +2,11 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use PHPUnit\Framework\TestCase; 5use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
7use Shaarli\History; 8use Shaarli\History;
9use Shaarli\TestCase;
8use Slim\Container; 10use Slim\Container;
9use Slim\Http\Environment; 11use Slim\Http\Environment;
10use Slim\Http\Request; 12use Slim\Http\Request;
@@ -41,6 +43,11 @@ class PostLinkTest extends TestCase
41 protected $refDB = null; 43 protected $refDB = null;
42 44
43 /** 45 /**
46 * @var BookmarkFileService instance.
47 */
48 protected $bookmarkService;
49
50 /**
44 * @var HistoryController instance. 51 * @var HistoryController instance.
45 */ 52 */
46 protected $history; 53 protected $history;
@@ -61,29 +68,30 @@ class PostLinkTest extends TestCase
61 const NB_FIELDS_LINK = 9; 68 const NB_FIELDS_LINK = 9;
62 69
63 /** 70 /**
64 * Before every test, instantiate a new Api with its config, plugins and links. 71 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
65 */ 72 */
66 public function setUp() 73 protected function setUp(): void
67 { 74 {
68 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 75 $this->conf = new ConfigManager('tests/utils/config/configJson');
76 $this->conf->set('resource.datastore', self::$testDatastore);
69 $this->refDB = new \ReferenceLinkDB(); 77 $this->refDB = new \ReferenceLinkDB();
70 $this->refDB->write(self::$testDatastore); 78 $this->refDB->write(self::$testDatastore);
71
72 $refHistory = new \ReferenceHistory(); 79 $refHistory = new \ReferenceHistory();
73 $refHistory->write(self::$testHistory); 80 $refHistory->write(self::$testHistory);
74 $this->history = new History(self::$testHistory); 81 $this->history = new History(self::$testHistory);
82 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
75 83
76 $this->container = new Container(); 84 $this->container = new Container();
77 $this->container['conf'] = $this->conf; 85 $this->container['conf'] = $this->conf;
78 $this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); 86 $this->container['db'] = $this->bookmarkService;
79 $this->container['history'] = new History(self::$testHistory); 87 $this->container['history'] = $this->history;
80 88
81 $this->controller = new Links($this->container); 89 $this->controller = new Links($this->container);
82 90
83 $mock = $this->createMock(Router::class); 91 $mock = $this->createMock(Router::class);
84 $mock->expects($this->any()) 92 $mock->expects($this->any())
85 ->method('relativePathFor') 93 ->method('relativePathFor')
86 ->willReturn('api/v1/links/1'); 94 ->willReturn('api/v1/bookmarks/1');
87 95
88 // affect @property-read... seems to work 96 // affect @property-read... seems to work
89 $this->controller->getCi()->router = $mock; 97 $this->controller->getCi()->router = $mock;
@@ -99,7 +107,7 @@ class PostLinkTest extends TestCase
99 /** 107 /**
100 * After every test, remove the test datastore. 108 * After every test, remove the test datastore.
101 */ 109 */
102 public function tearDown() 110 protected function tearDown(): void
103 { 111 {
104 @unlink(self::$testDatastore); 112 @unlink(self::$testDatastore);
105 @unlink(self::$testHistory); 113 @unlink(self::$testHistory);
@@ -118,16 +126,16 @@ class PostLinkTest extends TestCase
118 126
119 $response = $this->controller->postLink($request, new Response()); 127 $response = $this->controller->postLink($request, new Response());
120 $this->assertEquals(201, $response->getStatusCode()); 128 $this->assertEquals(201, $response->getStatusCode());
121 $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]); 129 $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]);
122 $data = json_decode((string) $response->getBody(), true); 130 $data = json_decode((string) $response->getBody(), true);
123 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 131 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
124 $this->assertEquals(43, $data['id']); 132 $this->assertEquals(43, $data['id']);
125 $this->assertRegExp('/[\w_-]{6}/', $data['shorturl']); 133 $this->assertRegExp('/[\w_-]{6}/', $data['shorturl']);
126 $this->assertEquals('http://domain.tld/?' . $data['shorturl'], $data['url']); 134 $this->assertEquals('http://domain.tld/shaare/' . $data['shorturl'], $data['url']);
127 $this->assertEquals('?' . $data['shorturl'], $data['title']); 135 $this->assertEquals('/shaare/' . $data['shorturl'], $data['title']);
128 $this->assertEquals('', $data['description']); 136 $this->assertEquals('', $data['description']);
129 $this->assertEquals([], $data['tags']); 137 $this->assertEquals([], $data['tags']);
130 $this->assertEquals(false, $data['private']); 138 $this->assertEquals(true, $data['private']);
131 $this->assertTrue( 139 $this->assertTrue(
132 new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 140 new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
133 ); 141 );
@@ -163,7 +171,7 @@ class PostLinkTest extends TestCase
163 $response = $this->controller->postLink($request, new Response()); 171 $response = $this->controller->postLink($request, new Response());
164 172
165 $this->assertEquals(201, $response->getStatusCode()); 173 $this->assertEquals(201, $response->getStatusCode());
166 $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]); 174 $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]);
167 $data = json_decode((string) $response->getBody(), true); 175 $data = json_decode((string) $response->getBody(), true);
168 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 176 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
169 $this->assertEquals(43, $data['id']); 177 $this->assertEquals(43, $data['id']);
@@ -211,11 +219,11 @@ class PostLinkTest extends TestCase
211 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']); 219 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
212 $this->assertEquals(false, $data['private']); 220 $this->assertEquals(false, $data['private']);
213 $this->assertEquals( 221 $this->assertEquals(
214 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 222 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130614_184135'),
215 \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 223 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
216 ); 224 );
217 $this->assertEquals( 225 $this->assertEquals(
218 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130615_184230'), 226 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130615_184230'),
219 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) 227 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
220 ); 228 );
221 } 229 }
diff --git a/tests/api/controllers/links/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php
index cd815b66..a2e87c59 100644
--- a/tests/api/controllers/links/PutLinkTest.php
+++ b/tests/api/controllers/links/PutLinkTest.php
@@ -3,6 +3,8 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
7use Shaarli\History; 9use Shaarli\History;
8use Slim\Container; 10use Slim\Container;
@@ -10,7 +12,7 @@ use Slim\Http\Environment;
10use Slim\Http\Request; 12use Slim\Http\Request;
11use Slim\Http\Response; 13use Slim\Http\Response;
12 14
13class PutLinkTest extends \PHPUnit\Framework\TestCase 15class PutLinkTest extends \Shaarli\TestCase
14{ 16{
15 /** 17 /**
16 * @var string datastore to test write operations 18 * @var string datastore to test write operations
@@ -33,6 +35,11 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
33 protected $refDB = null; 35 protected $refDB = null;
34 36
35 /** 37 /**
38 * @var BookmarkFileService instance.
39 */
40 protected $bookmarkService;
41
42 /**
36 * @var HistoryController instance. 43 * @var HistoryController instance.
37 */ 44 */
38 protected $history; 45 protected $history;
@@ -53,22 +60,23 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
53 const NB_FIELDS_LINK = 9; 60 const NB_FIELDS_LINK = 9;
54 61
55 /** 62 /**
56 * Before every test, instantiate a new Api with its config, plugins and links. 63 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
57 */ 64 */
58 public function setUp() 65 protected function setUp(): void
59 { 66 {
60 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 67 $this->conf = new ConfigManager('tests/utils/config/configJson');
68 $this->conf->set('resource.datastore', self::$testDatastore);
61 $this->refDB = new \ReferenceLinkDB(); 69 $this->refDB = new \ReferenceLinkDB();
62 $this->refDB->write(self::$testDatastore); 70 $this->refDB->write(self::$testDatastore);
63
64 $refHistory = new \ReferenceHistory(); 71 $refHistory = new \ReferenceHistory();
65 $refHistory->write(self::$testHistory); 72 $refHistory->write(self::$testHistory);
66 $this->history = new History(self::$testHistory); 73 $this->history = new History(self::$testHistory);
74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
67 75
68 $this->container = new Container(); 76 $this->container = new Container();
69 $this->container['conf'] = $this->conf; 77 $this->container['conf'] = $this->conf;
70 $this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); 78 $this->container['db'] = $this->bookmarkService;
71 $this->container['history'] = new History(self::$testHistory); 79 $this->container['history'] = $this->history;
72 80
73 $this->controller = new Links($this->container); 81 $this->controller = new Links($this->container);
74 82
@@ -83,7 +91,7 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
83 /** 91 /**
84 * After every test, remove the test datastore. 92 * After every test, remove the test datastore.
85 */ 93 */
86 public function tearDown() 94 protected function tearDown(): void
87 { 95 {
88 @unlink(self::$testDatastore); 96 @unlink(self::$testDatastore);
89 @unlink(self::$testHistory); 97 @unlink(self::$testHistory);
@@ -106,11 +114,11 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
106 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 114 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
107 $this->assertEquals($id, $data['id']); 115 $this->assertEquals($id, $data['id']);
108 $this->assertEquals('WDWyig', $data['shorturl']); 116 $this->assertEquals('WDWyig', $data['shorturl']);
109 $this->assertEquals('http://domain.tld/?WDWyig', $data['url']); 117 $this->assertEquals('http://domain.tld/shaare/WDWyig', $data['url']);
110 $this->assertEquals('?WDWyig', $data['title']); 118 $this->assertEquals('/shaare/WDWyig', $data['title']);
111 $this->assertEquals('', $data['description']); 119 $this->assertEquals('', $data['description']);
112 $this->assertEquals([], $data['tags']); 120 $this->assertEquals([], $data['tags']);
113 $this->assertEquals(false, $data['private']); 121 $this->assertEquals(true, $data['private']);
114 $this->assertEquals( 122 $this->assertEquals(
115 \DateTime::createFromFormat('Ymd_His', '20150310_114651'), 123 \DateTime::createFromFormat('Ymd_His', '20150310_114651'),
116 \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 124 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
@@ -199,23 +207,23 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
199 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']); 207 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
200 $this->assertEquals(false, $data['private']); 208 $this->assertEquals(false, $data['private']);
201 $this->assertEquals( 209 $this->assertEquals(
202 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 210 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130614_184135'),
203 \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 211 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
204 ); 212 );
205 $this->assertEquals( 213 $this->assertEquals(
206 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130615_184230'), 214 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130615_184230'),
207 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) 215 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
208 ); 216 );
209 } 217 }
210 218
211 /** 219 /**
212 * Test link update on non existent link => ApiLinkNotFoundException. 220 * Test link update on non existent link => ApiLinkNotFoundException.
213 *
214 * @expectedException Shaarli\Api\Exceptions\ApiLinkNotFoundException
215 * @expectedExceptionMessage Link not found
216 */ 221 */
217 public function testGetLink404() 222 public function testGetLink404()
218 { 223 {
224 $this->expectException(\Shaarli\Api\Exceptions\ApiLinkNotFoundException::class);
225 $this->expectExceptionMessage('Link not found');
226
219 $env = Environment::mock([ 227 $env = Environment::mock([
220 'REQUEST_METHOD' => 'PUT', 228 'REQUEST_METHOD' => 'PUT',
221 ]); 229 ]);
diff --git a/tests/api/controllers/tags/DeleteTagTest.php b/tests/api/controllers/tags/DeleteTagTest.php
index 84e1d56e..1326eb47 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 Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Bookmark\LinkDB; 7use Shaarli\Bookmark\LinkDB;
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 DeleteTagTest extends \PHPUnit\Framework\TestCase 15class DeleteTagTest extends \Shaarli\TestCase
15{ 16{
16 /** 17 /**
17 * @var string datastore to test write operations 18 * @var string datastore to test write operations
@@ -34,9 +35,9 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
34 protected $refDB = null; 35 protected $refDB = null;
35 36
36 /** 37 /**
37 * @var LinkDB instance. 38 * @var BookmarkFileService instance.
38 */ 39 */
39 protected $linkDB; 40 protected $bookmarkService;
40 41
41 /** 42 /**
42 * @var HistoryController instance. 43 * @var HistoryController instance.
@@ -54,20 +55,22 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
54 protected $controller; 55 protected $controller;
55 56
56 /** 57 /**
57 * Before each test, instantiate a new Api with its config, plugins and links. 58 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
58 */ 59 */
59 public function setUp() 60 protected function setUp(): void
60 { 61 {
61 $this->conf = new ConfigManager('tests/utils/config/configJson'); 62 $this->conf = new ConfigManager('tests/utils/config/configJson');
63 $this->conf->set('resource.datastore', self::$testDatastore);
62 $this->refDB = new \ReferenceLinkDB(); 64 $this->refDB = new \ReferenceLinkDB();
63 $this->refDB->write(self::$testDatastore); 65 $this->refDB->write(self::$testDatastore);
64 $this->linkDB = new LinkDB(self::$testDatastore, true, false);
65 $refHistory = new \ReferenceHistory(); 66 $refHistory = new \ReferenceHistory();
66 $refHistory->write(self::$testHistory); 67 $refHistory->write(self::$testHistory);
67 $this->history = new History(self::$testHistory); 68 $this->history = new History(self::$testHistory);
69 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
70
68 $this->container = new Container(); 71 $this->container = new Container();
69 $this->container['conf'] = $this->conf; 72 $this->container['conf'] = $this->conf;
70 $this->container['db'] = $this->linkDB; 73 $this->container['db'] = $this->bookmarkService;
71 $this->container['history'] = $this->history; 74 $this->container['history'] = $this->history;
72 75
73 $this->controller = new Tags($this->container); 76 $this->controller = new Tags($this->container);
@@ -76,7 +79,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
76 /** 79 /**
77 * After each test, remove the test datastore. 80 * After each test, remove the test datastore.
78 */ 81 */
79 public function tearDown() 82 protected function tearDown(): void
80 { 83 {
81 @unlink(self::$testDatastore); 84 @unlink(self::$testDatastore);
82 @unlink(self::$testHistory); 85 @unlink(self::$testHistory);
@@ -88,7 +91,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
88 public function testDeleteTagValid() 91 public function testDeleteTagValid()
89 { 92 {
90 $tagName = 'gnu'; 93 $tagName = 'gnu';
91 $tags = $this->linkDB->linksCountPerTag(); 94 $tags = $this->bookmarkService->bookmarksCountPerTag();
92 $this->assertTrue($tags[$tagName] > 0); 95 $this->assertTrue($tags[$tagName] > 0);
93 $env = Environment::mock([ 96 $env = Environment::mock([
94 'REQUEST_METHOD' => 'DELETE', 97 'REQUEST_METHOD' => 'DELETE',
@@ -99,11 +102,11 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
99 $this->assertEquals(204, $response->getStatusCode()); 102 $this->assertEquals(204, $response->getStatusCode());
100 $this->assertEmpty((string) $response->getBody()); 103 $this->assertEmpty((string) $response->getBody());
101 104
102 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 105 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
103 $tags = $this->linkDB->linksCountPerTag(); 106 $tags = $this->bookmarkService->bookmarksCountPerTag();
104 $this->assertFalse(isset($tags[$tagName])); 107 $this->assertFalse(isset($tags[$tagName]));
105 108
106 // 2 links affected 109 // 2 bookmarks affected
107 $historyEntry = $this->history->getHistory()[0]; 110 $historyEntry = $this->history->getHistory()[0];
108 $this->assertEquals(History::UPDATED, $historyEntry['event']); 111 $this->assertEquals(History::UPDATED, $historyEntry['event']);
109 $this->assertTrue( 112 $this->assertTrue(
@@ -122,7 +125,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
122 public function testDeleteTagCaseSensitivity() 125 public function testDeleteTagCaseSensitivity()
123 { 126 {
124 $tagName = 'sTuff'; 127 $tagName = 'sTuff';
125 $tags = $this->linkDB->linksCountPerTag(); 128 $tags = $this->bookmarkService->bookmarksCountPerTag();
126 $this->assertTrue($tags[$tagName] > 0); 129 $this->assertTrue($tags[$tagName] > 0);
127 $env = Environment::mock([ 130 $env = Environment::mock([
128 'REQUEST_METHOD' => 'DELETE', 131 'REQUEST_METHOD' => 'DELETE',
@@ -133,8 +136,8 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
133 $this->assertEquals(204, $response->getStatusCode()); 136 $this->assertEquals(204, $response->getStatusCode());
134 $this->assertEmpty((string) $response->getBody()); 137 $this->assertEmpty((string) $response->getBody());
135 138
136 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 139 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
137 $tags = $this->linkDB->linksCountPerTag(); 140 $tags = $this->bookmarkService->bookmarksCountPerTag();
138 $this->assertFalse(isset($tags[$tagName])); 141 $this->assertFalse(isset($tags[$tagName]));
139 $this->assertTrue($tags[strtolower($tagName)] > 0); 142 $this->assertTrue($tags[strtolower($tagName)] > 0);
140 143
@@ -147,14 +150,14 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
147 150
148 /** 151 /**
149 * Test DELETE tag endpoint: reach not existing tag. 152 * Test DELETE tag endpoint: reach not existing tag.
150 *
151 * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException
152 * @expectedExceptionMessage Tag not found
153 */ 153 */
154 public function testDeleteLink404() 154 public function testDeleteLink404()
155 { 155 {
156 $this->expectException(\Shaarli\Api\Exceptions\ApiTagNotFoundException::class);
157 $this->expectExceptionMessage('Tag not found');
158
156 $tagName = 'nopenope'; 159 $tagName = 'nopenope';
157 $tags = $this->linkDB->linksCountPerTag(); 160 $tags = $this->bookmarkService->bookmarksCountPerTag();
158 $this->assertFalse(isset($tags[$tagName])); 161 $this->assertFalse(isset($tags[$tagName]));
159 $env = Environment::mock([ 162 $env = Environment::mock([
160 'REQUEST_METHOD' => 'DELETE', 163 'REQUEST_METHOD' => 'DELETE',
diff --git a/tests/api/controllers/tags/GetTagNameTest.php b/tests/api/controllers/tags/GetTagNameTest.php
index a2525c17..9c05954b 100644
--- a/tests/api/controllers/tags/GetTagNameTest.php
+++ b/tests/api/controllers/tags/GetTagNameTest.php
@@ -2,8 +2,10 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Bookmark\LinkDB; 6use Shaarli\Bookmark\LinkDB;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
7use Slim\Container; 9use Slim\Container;
8use Slim\Http\Environment; 10use Slim\Http\Environment;
9use Slim\Http\Request; 11use Slim\Http\Request;
@@ -16,7 +18,7 @@ use Slim\Http\Response;
16 * 18 *
17 * @package Shaarli\Api\Controllers 19 * @package Shaarli\Api\Controllers
18 */ 20 */
19class GetTagNameTest extends \PHPUnit\Framework\TestCase 21class GetTagNameTest extends \Shaarli\TestCase
20{ 22{
21 /** 23 /**
22 * @var string datastore to test write operations 24 * @var string datastore to test write operations
@@ -49,17 +51,19 @@ class GetTagNameTest extends \PHPUnit\Framework\TestCase
49 const NB_FIELDS_TAG = 2; 51 const NB_FIELDS_TAG = 2;
50 52
51 /** 53 /**
52 * Before each test, instantiate a new Api with its config, plugins and links. 54 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
53 */ 55 */
54 public function setUp() 56 protected function setUp(): void
55 { 57 {
56 $this->conf = new ConfigManager('tests/utils/config/configJson'); 58 $this->conf = new ConfigManager('tests/utils/config/configJson');
59 $this->conf->set('resource.datastore', self::$testDatastore);
57 $this->refDB = new \ReferenceLinkDB(); 60 $this->refDB = new \ReferenceLinkDB();
58 $this->refDB->write(self::$testDatastore); 61 $this->refDB->write(self::$testDatastore);
62 $history = new History('sandbox/history.php');
59 63
60 $this->container = new Container(); 64 $this->container = new Container();
61 $this->container['conf'] = $this->conf; 65 $this->container['conf'] = $this->conf;
62 $this->container['db'] = new LinkDB(self::$testDatastore, true, false); 66 $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
63 $this->container['history'] = null; 67 $this->container['history'] = null;
64 68
65 $this->controller = new Tags($this->container); 69 $this->controller = new Tags($this->container);
@@ -68,7 +72,7 @@ class GetTagNameTest extends \PHPUnit\Framework\TestCase
68 /** 72 /**
69 * After each test, remove the test datastore. 73 * After each test, remove the test datastore.
70 */ 74 */
71 public function tearDown() 75 protected function tearDown(): void
72 { 76 {
73 @unlink(self::$testDatastore); 77 @unlink(self::$testDatastore);
74 } 78 }
@@ -113,12 +117,12 @@ class GetTagNameTest extends \PHPUnit\Framework\TestCase
113 117
114 /** 118 /**
115 * Test basic getTag service: get non existent tag => ApiTagNotFoundException. 119 * Test basic getTag service: get non existent tag => ApiTagNotFoundException.
116 *
117 * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException
118 * @expectedExceptionMessage Tag not found
119 */ 120 */
120 public function testGetTag404() 121 public function testGetTag404()
121 { 122 {
123 $this->expectException(\Shaarli\Api\Exceptions\ApiTagNotFoundException::class);
124 $this->expectExceptionMessage('Tag not found');
125
122 $env = Environment::mock([ 126 $env = Environment::mock([
123 'REQUEST_METHOD' => 'GET', 127 'REQUEST_METHOD' => 'GET',
124 ]); 128 ]);
diff --git a/tests/api/controllers/tags/GetTagsTest.php b/tests/api/controllers/tags/GetTagsTest.php
index 98628c98..3459fdfa 100644
--- a/tests/api/controllers/tags/GetTagsTest.php
+++ b/tests/api/controllers/tags/GetTagsTest.php
@@ -1,8 +1,10 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use Shaarli\Bookmark\BookmarkFileService;
4use Shaarli\Bookmark\LinkDB; 5use Shaarli\Bookmark\LinkDB;
5use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Shaarli\History;
6use Slim\Container; 8use Slim\Container;
7use Slim\Http\Environment; 9use Slim\Http\Environment;
8use Slim\Http\Request; 10use Slim\Http\Request;
@@ -15,7 +17,7 @@ use Slim\Http\Response;
15 * 17 *
16 * @package Shaarli\Api\Controllers 18 * @package Shaarli\Api\Controllers
17 */ 19 */
18class GetTagsTest extends \PHPUnit\Framework\TestCase 20class GetTagsTest extends \Shaarli\TestCase
19{ 21{
20 /** 22 /**
21 * @var string datastore to test write operations 23 * @var string datastore to test write operations
@@ -38,9 +40,9 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
38 protected $container; 40 protected $container;
39 41
40 /** 42 /**
41 * @var LinkDB instance. 43 * @var BookmarkFileService instance.
42 */ 44 */
43 protected $linkDB; 45 protected $bookmarkService;
44 46
45 /** 47 /**
46 * @var Tags controller instance. 48 * @var Tags controller instance.
@@ -53,18 +55,21 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
53 const NB_FIELDS_TAG = 2; 55 const NB_FIELDS_TAG = 2;
54 56
55 /** 57 /**
56 * Before every test, instantiate a new Api with its config, plugins and links. 58 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
57 */ 59 */
58 public function setUp() 60 protected function setUp(): void
59 { 61 {
60 $this->conf = new ConfigManager('tests/utils/config/configJson'); 62 $this->conf = new ConfigManager('tests/utils/config/configJson');
63 $this->conf->set('resource.datastore', self::$testDatastore);
61 $this->refDB = new \ReferenceLinkDB(); 64 $this->refDB = new \ReferenceLinkDB();
62 $this->refDB->write(self::$testDatastore); 65 $this->refDB->write(self::$testDatastore);
66 $history = new History('sandbox/history.php');
67
68 $this->bookmarkService = new BookmarkFileService($this->conf, $history, true);
63 69
64 $this->container = new Container(); 70 $this->container = new Container();
65 $this->container['conf'] = $this->conf; 71 $this->container['conf'] = $this->conf;
66 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 72 $this->container['db'] = $this->bookmarkService;
67 $this->container['db'] = $this->linkDB;
68 $this->container['history'] = null; 73 $this->container['history'] = null;
69 74
70 $this->controller = new Tags($this->container); 75 $this->controller = new Tags($this->container);
@@ -73,7 +78,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
73 /** 78 /**
74 * After every test, remove the test datastore. 79 * After every test, remove the test datastore.
75 */ 80 */
76 public function tearDown() 81 protected function tearDown(): void
77 { 82 {
78 @unlink(self::$testDatastore); 83 @unlink(self::$testDatastore);
79 } 84 }
@@ -83,7 +88,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
83 */ 88 */
84 public function testGetTagsAll() 89 public function testGetTagsAll()
85 { 90 {
86 $tags = $this->linkDB->linksCountPerTag(); 91 $tags = $this->bookmarkService->bookmarksCountPerTag();
87 $env = Environment::mock([ 92 $env = Environment::mock([
88 'REQUEST_METHOD' => 'GET', 93 'REQUEST_METHOD' => 'GET',
89 ]); 94 ]);
@@ -136,7 +141,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
136 */ 141 */
137 public function testGetTagsLimitAll() 142 public function testGetTagsLimitAll()
138 { 143 {
139 $tags = $this->linkDB->linksCountPerTag(); 144 $tags = $this->bookmarkService->bookmarksCountPerTag();
140 $env = Environment::mock([ 145 $env = Environment::mock([
141 'REQUEST_METHOD' => 'GET', 146 'REQUEST_METHOD' => 'GET',
142 'QUERY_STRING' => 'limit=all' 147 'QUERY_STRING' => 'limit=all'
@@ -170,7 +175,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
170 */ 175 */
171 public function testGetTagsVisibilityPrivate() 176 public function testGetTagsVisibilityPrivate()
172 { 177 {
173 $tags = $this->linkDB->linksCountPerTag([], 'private'); 178 $tags = $this->bookmarkService->bookmarksCountPerTag([], 'private');
174 $env = Environment::mock([ 179 $env = Environment::mock([
175 'REQUEST_METHOD' => 'GET', 180 'REQUEST_METHOD' => 'GET',
176 'QUERY_STRING' => 'visibility=private' 181 'QUERY_STRING' => 'visibility=private'
@@ -190,7 +195,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
190 */ 195 */
191 public function testGetTagsVisibilityPublic() 196 public function testGetTagsVisibilityPublic()
192 { 197 {
193 $tags = $this->linkDB->linksCountPerTag([], 'public'); 198 $tags = $this->bookmarkService->bookmarksCountPerTag([], 'public');
194 $env = Environment::mock( 199 $env = Environment::mock(
195 [ 200 [
196 'REQUEST_METHOD' => 'GET', 201 'REQUEST_METHOD' => 'GET',
diff --git a/tests/api/controllers/tags/PutTagTest.php b/tests/api/controllers/tags/PutTagTest.php
index 86106fc7..74edde78 100644
--- a/tests/api/controllers/tags/PutTagTest.php
+++ b/tests/api/controllers/tags/PutTagTest.php
@@ -3,6 +3,7 @@
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Api\Exceptions\ApiBadParametersException; 5use Shaarli\Api\Exceptions\ApiBadParametersException;
6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Bookmark\LinkDB; 7use Shaarli\Bookmark\LinkDB;
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 PutTagTest extends \PHPUnit\Framework\TestCase 15class PutTagTest extends \Shaarli\TestCase
15{ 16{
16 /** 17 /**
17 * @var string datastore to test write operations 18 * @var string datastore to test write operations
@@ -44,9 +45,9 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
44 protected $container; 45 protected $container;
45 46
46 /** 47 /**
47 * @var LinkDB instance. 48 * @var BookmarkFileService instance.
48 */ 49 */
49 protected $linkDB; 50 protected $bookmarkService;
50 51
51 /** 52 /**
52 * @var Tags controller instance. 53 * @var Tags controller instance.
@@ -59,22 +60,22 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
59 const NB_FIELDS_TAG = 2; 60 const NB_FIELDS_TAG = 2;
60 61
61 /** 62 /**
62 * Before every test, instantiate a new Api with its config, plugins and links. 63 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
63 */ 64 */
64 public function setUp() 65 protected function setUp(): void
65 { 66 {
66 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 67 $this->conf = new ConfigManager('tests/utils/config/configJson');
68 $this->conf->set('resource.datastore', self::$testDatastore);
67 $this->refDB = new \ReferenceLinkDB(); 69 $this->refDB = new \ReferenceLinkDB();
68 $this->refDB->write(self::$testDatastore); 70 $this->refDB->write(self::$testDatastore);
69
70 $refHistory = new \ReferenceHistory(); 71 $refHistory = new \ReferenceHistory();
71 $refHistory->write(self::$testHistory); 72 $refHistory->write(self::$testHistory);
72 $this->history = new History(self::$testHistory); 73 $this->history = new History(self::$testHistory);
74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
73 75
74 $this->container = new Container(); 76 $this->container = new Container();
75 $this->container['conf'] = $this->conf; 77 $this->container['conf'] = $this->conf;
76 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 78 $this->container['db'] = $this->bookmarkService;
77 $this->container['db'] = $this->linkDB;
78 $this->container['history'] = $this->history; 79 $this->container['history'] = $this->history;
79 80
80 $this->controller = new Tags($this->container); 81 $this->controller = new Tags($this->container);
@@ -83,7 +84,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
83 /** 84 /**
84 * After every test, remove the test datastore. 85 * After every test, remove the test datastore.
85 */ 86 */
86 public function tearDown() 87 protected function tearDown(): void
87 { 88 {
88 @unlink(self::$testDatastore); 89 @unlink(self::$testDatastore);
89 @unlink(self::$testHistory); 90 @unlink(self::$testHistory);
@@ -109,7 +110,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
109 $this->assertEquals($newName, $data['name']); 110 $this->assertEquals($newName, $data['name']);
110 $this->assertEquals(2, $data['occurrences']); 111 $this->assertEquals(2, $data['occurrences']);
111 112
112 $tags = $this->linkDB->linksCountPerTag(); 113 $tags = $this->bookmarkService->bookmarksCountPerTag();
113 $this->assertNotTrue(isset($tags[$tagName])); 114 $this->assertNotTrue(isset($tags[$tagName]));
114 $this->assertEquals(2, $tags[$newName]); 115 $this->assertEquals(2, $tags[$newName]);
115 116
@@ -133,7 +134,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
133 $tagName = 'gnu'; 134 $tagName = 'gnu';
134 $newName = 'w3c'; 135 $newName = 'w3c';
135 136
136 $tags = $this->linkDB->linksCountPerTag(); 137 $tags = $this->bookmarkService->bookmarksCountPerTag();
137 $this->assertEquals(1, $tags[$newName]); 138 $this->assertEquals(1, $tags[$newName]);
138 $this->assertEquals(2, $tags[$tagName]); 139 $this->assertEquals(2, $tags[$tagName]);
139 140
@@ -151,23 +152,23 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
151 $this->assertEquals($newName, $data['name']); 152 $this->assertEquals($newName, $data['name']);
152 $this->assertEquals(3, $data['occurrences']); 153 $this->assertEquals(3, $data['occurrences']);
153 154
154 $tags = $this->linkDB->linksCountPerTag(); 155 $tags = $this->bookmarkService->bookmarksCountPerTag();
155 $this->assertNotTrue(isset($tags[$tagName])); 156 $this->assertNotTrue(isset($tags[$tagName]));
156 $this->assertEquals(3, $tags[$newName]); 157 $this->assertEquals(3, $tags[$newName]);
157 } 158 }
158 159
159 /** 160 /**
160 * Test tag update with an empty new tag name => ApiBadParametersException 161 * Test tag update with an empty new tag name => ApiBadParametersException
161 *
162 * @expectedException Shaarli\Api\Exceptions\ApiBadParametersException
163 * @expectedExceptionMessage New tag name is required in the request body
164 */ 162 */
165 public function testPutTagEmpty() 163 public function testPutTagEmpty()
166 { 164 {
165 $this->expectException(\Shaarli\Api\Exceptions\ApiBadParametersException::class);
166 $this->expectExceptionMessage('New tag name is required in the request body');
167
167 $tagName = 'gnu'; 168 $tagName = 'gnu';
168 $newName = ''; 169 $newName = '';
169 170
170 $tags = $this->linkDB->linksCountPerTag(); 171 $tags = $this->bookmarkService->bookmarksCountPerTag();
171 $this->assertEquals(2, $tags[$tagName]); 172 $this->assertEquals(2, $tags[$tagName]);
172 173
173 $env = Environment::mock([ 174 $env = Environment::mock([
@@ -185,7 +186,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
185 try { 186 try {
186 $this->controller->putTag($request, new Response(), ['tagName' => $tagName]); 187 $this->controller->putTag($request, new Response(), ['tagName' => $tagName]);
187 } catch (ApiBadParametersException $e) { 188 } catch (ApiBadParametersException $e) {
188 $tags = $this->linkDB->linksCountPerTag(); 189 $tags = $this->bookmarkService->bookmarksCountPerTag();
189 $this->assertEquals(2, $tags[$tagName]); 190 $this->assertEquals(2, $tags[$tagName]);
190 throw $e; 191 throw $e;
191 } 192 }
@@ -193,12 +194,12 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
193 194
194 /** 195 /**
195 * Test tag update on non existent tag => ApiTagNotFoundException. 196 * Test tag update on non existent tag => ApiTagNotFoundException.
196 *
197 * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException
198 * @expectedExceptionMessage Tag not found
199 */ 197 */
200 public function testPutTag404() 198 public function testPutTag404()
201 { 199 {
200 $this->expectException(\Shaarli\Api\Exceptions\ApiTagNotFoundException::class);
201 $this->expectExceptionMessage('Tag not found');
202
202 $env = Environment::mock([ 203 $env = Environment::mock([
203 'REQUEST_METHOD' => 'PUT', 204 'REQUEST_METHOD' => 'PUT',
204 ]); 205 ]);
diff --git a/tests/bookmark/BookmarkArrayTest.php b/tests/bookmark/BookmarkArrayTest.php
new file mode 100644
index 00000000..ebed9bfc
--- /dev/null
+++ b/tests/bookmark/BookmarkArrayTest.php
@@ -0,0 +1,236 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Shaarli\TestCase;
6
7/**
8 * Class BookmarkArrayTest
9 */
10class BookmarkArrayTest extends TestCase
11{
12 /**
13 * Test the constructor and make sure that the instance is properly initialized
14 */
15 public function testArrayConstructorEmpty()
16 {
17 $array = new BookmarkArray();
18 $this->assertTrue(is_iterable($array));
19 $this->assertEmpty($array);
20 }
21
22 /**
23 * Test adding entries to the array, specifying the key offset or not.
24 */
25 public function testArrayAccessAddEntries()
26 {
27 $array = new BookmarkArray();
28 $bookmark = new Bookmark();
29 $bookmark->setId(11)->validate();
30 $array[] = $bookmark;
31 $this->assertCount(1, $array);
32 $this->assertTrue(isset($array[11]));
33 $this->assertNull($array[0]);
34 $this->assertEquals($bookmark, $array[11]);
35
36 $bookmark = new Bookmark();
37 $bookmark->setId(14)->validate();
38 $array[14] = $bookmark;
39 $this->assertCount(2, $array);
40 $this->assertTrue(isset($array[14]));
41 $this->assertNull($array[0]);
42 $this->assertEquals($bookmark, $array[14]);
43 }
44
45 /**
46 * Test adding a bad entry: wrong type
47 */
48 public function testArrayAccessAddBadEntryInstance()
49 {
50 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
51
52 $array = new BookmarkArray();
53 $array[] = 'nope';
54 }
55
56 /**
57 * Test adding a bad entry: no id
58 */
59 public function testArrayAccessAddBadEntryNoId()
60 {
61 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
62
63 $array = new BookmarkArray();
64 $bookmark = new Bookmark();
65 $array[] = $bookmark;
66 }
67
68 /**
69 * Test adding a bad entry: no url
70 */
71 public function testArrayAccessAddBadEntryNoUrl()
72 {
73 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
74
75 $array = new BookmarkArray();
76 $bookmark = (new Bookmark())->setId(11);
77 $array[] = $bookmark;
78 }
79
80 /**
81 * Test adding a bad entry: invalid offset
82 */
83 public function testArrayAccessAddBadEntryOffset()
84 {
85 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
86
87 $array = new BookmarkArray();
88 $bookmark = (new Bookmark())->setId(11);
89 $bookmark->validate();
90 $array['nope'] = $bookmark;
91 }
92
93 /**
94 * Test adding a bad entry: invalid ID type
95 */
96 public function testArrayAccessAddBadEntryIdType()
97 {
98 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
99
100 $array = new BookmarkArray();
101 $bookmark = (new Bookmark())->setId('nope');
102 $bookmark->validate();
103 $array[] = $bookmark;
104 }
105
106 /**
107 * Test adding a bad entry: ID/offset not consistent
108 */
109 public function testArrayAccessAddBadEntryIdOffset()
110 {
111 $this->expectException(\Shaarli\Bookmark\Exception\InvalidBookmarkException::class);
112
113 $array = new BookmarkArray();
114 $bookmark = (new Bookmark())->setId(11);
115 $bookmark->validate();
116 $array[14] = $bookmark;
117 }
118
119 /**
120 * Test update entries through array access.
121 */
122 public function testArrayAccessUpdateEntries()
123 {
124 $array = new BookmarkArray();
125 $bookmark = new Bookmark();
126 $bookmark->setId(11)->validate();
127 $bookmark->setTitle('old');
128 $array[] = $bookmark;
129 $bookmark = new Bookmark();
130 $bookmark->setId(11)->validate();
131 $bookmark->setTitle('test');
132 $array[] = $bookmark;
133 $this->assertCount(1, $array);
134 $this->assertEquals('test', $array[11]->getTitle());
135
136 $bookmark = new Bookmark();
137 $bookmark->setId(11)->validate();
138 $bookmark->setTitle('test2');
139 $array[11] = $bookmark;
140 $this->assertCount(1, $array);
141 $this->assertEquals('test2', $array[11]->getTitle());
142 }
143
144 /**
145 * Test delete entries through array access.
146 */
147 public function testArrayAccessDeleteEntries()
148 {
149 $array = new BookmarkArray();
150 $bookmark11 = new Bookmark();
151 $bookmark11->setId(11)->validate();
152 $array[] = $bookmark11;
153 $bookmark14 = new Bookmark();
154 $bookmark14->setId(14)->validate();
155 $array[] = $bookmark14;
156 $bookmark23 = new Bookmark();
157 $bookmark23->setId(23)->validate();
158 $array[] = $bookmark23;
159 $bookmark0 = new Bookmark();
160 $bookmark0->setId(0)->validate();
161 $array[] = $bookmark0;
162 $this->assertCount(4, $array);
163
164 unset($array[14]);
165 $this->assertCount(3, $array);
166 $this->assertEquals($bookmark11, $array[11]);
167 $this->assertEquals($bookmark23, $array[23]);
168 $this->assertEquals($bookmark0, $array[0]);
169
170 unset($array[23]);
171 $this->assertCount(2, $array);
172 $this->assertEquals($bookmark11, $array[11]);
173 $this->assertEquals($bookmark0, $array[0]);
174
175 unset($array[11]);
176 $this->assertCount(1, $array);
177 $this->assertEquals($bookmark0, $array[0]);
178
179 unset($array[0]);
180 $this->assertCount(0, $array);
181 }
182
183 /**
184 * Test iterating through array access.
185 */
186 public function testArrayAccessIterate()
187 {
188 $array = new BookmarkArray();
189 $bookmark11 = new Bookmark();
190 $bookmark11->setId(11)->validate();
191 $array[] = $bookmark11;
192 $bookmark14 = new Bookmark();
193 $bookmark14->setId(14)->validate();
194 $array[] = $bookmark14;
195 $bookmark23 = new Bookmark();
196 $bookmark23->setId(23)->validate();
197 $array[] = $bookmark23;
198 $this->assertCount(3, $array);
199
200 foreach ($array as $id => $bookmark) {
201 $this->assertEquals(${'bookmark'. $id}, $bookmark);
202 }
203 }
204
205 /**
206 * Test reordering the array.
207 */
208 public function testReorder()
209 {
210 $refDB = new \ReferenceLinkDB();
211 $refDB->write('sandbox/datastore.php');
212
213
214 $bookmarks = $refDB->getLinks();
215 $bookmarks->reorder('ASC');
216 $this->assertInstanceOf(BookmarkArray::class, $bookmarks);
217
218 $stickyIds = [11, 10];
219 $standardIds = [42, 4, 9, 1, 0, 7, 6, 8, 41];
220 $linkIds = array_merge($stickyIds, $standardIds);
221 $cpt = 0;
222 foreach ($bookmarks as $key => $value) {
223 $this->assertEquals($linkIds[$cpt++], $key);
224 }
225
226 $bookmarks = $refDB->getLinks();
227 $bookmarks->reorder('DESC');
228 $this->assertInstanceOf(BookmarkArray::class, $bookmarks);
229
230 $linkIds = array_merge(array_reverse($stickyIds), array_reverse($standardIds));
231 $cpt = 0;
232 foreach ($bookmarks as $key => $value) {
233 $this->assertEquals($linkIds[$cpt++], $key);
234 }
235 }
236}
diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php
new file mode 100644
index 00000000..c399822b
--- /dev/null
+++ b/tests/bookmark/BookmarkFileServiceTest.php
@@ -0,0 +1,1111 @@
1<?php
2/**
3 * Link datastore tests
4 */
5
6namespace Shaarli\Bookmark;
7
8use DateTime;
9use ReferenceLinkDB;
10use ReflectionClass;
11use Shaarli;
12use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
13use Shaarli\Config\ConfigManager;
14use Shaarli\Formatter\BookmarkMarkdownFormatter;
15use Shaarli\History;
16use Shaarli\TestCase;
17
18/**
19 * Unitary tests for LegacyLinkDBTest
20 */
21class BookmarkFileServiceTest extends TestCase
22{
23 // datastore to test write operations
24 protected static $testDatastore = 'sandbox/datastore.php';
25
26 protected static $testConf = 'sandbox/config';
27
28 protected static $testUpdates = 'sandbox/updates.txt';
29
30 /**
31 * @var ConfigManager instance.
32 */
33 protected $conf;
34
35 /**
36 * @var History instance.
37 */
38 protected $history;
39
40 /**
41 * @var ReferenceLinkDB instance.
42 */
43 protected $refDB = null;
44
45 /**
46 * @var BookmarkFileService public LinkDB instance.
47 */
48 protected $publicLinkDB = null;
49
50 /**
51 * @var BookmarkFileService private LinkDB instance.
52 */
53 protected $privateLinkDB = null;
54
55 /**
56 * Instantiates public and private LinkDBs with test data
57 *
58 * The reference datastore contains public and private bookmarks that
59 * will be used to test LinkDB's methods:
60 * - access filtering (public/private),
61 * - link searches:
62 * - by day,
63 * - by tag,
64 * - by text,
65 * - etc.
66 *
67 * Resets test data for each test
68 */
69 protected function setUp(): void
70 {
71 if (file_exists(self::$testDatastore)) {
72 unlink(self::$testDatastore);
73 }
74
75 if (file_exists(self::$testConf .'.json.php')) {
76 unlink(self::$testConf .'.json.php');
77 }
78
79 if (file_exists(self::$testUpdates)) {
80 unlink(self::$testUpdates);
81 }
82
83 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
84 $this->conf = new ConfigManager(self::$testConf);
85 $this->conf->set('resource.datastore', self::$testDatastore);
86 $this->conf->set('resource.updates', self::$testUpdates);
87 $this->refDB = new \ReferenceLinkDB();
88 $this->refDB->write(self::$testDatastore);
89 $this->history = new History('sandbox/history.php');
90 $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, false);
91 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
92 }
93
94 /**
95 * Test migrate() method with a legacy datastore.
96 */
97 public function testDatabaseMigration()
98 {
99 if (!defined('SHAARLI_VERSION')) {
100 define('SHAARLI_VERSION', 'dev');
101 }
102
103 $this->refDB = new \ReferenceLinkDB(true);
104 $this->refDB->write(self::$testDatastore);
105 $db = self::getMethod('migrate');
106 $db->invokeArgs($this->privateLinkDB, []);
107
108 $db = new \FakeBookmarkService($this->conf, $this->history, true);
109 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
110 $this->assertEquals($this->refDB->countLinks(), $db->count());
111 }
112
113 /**
114 * Test get() method for a defined and saved bookmark
115 */
116 public function testGetDefinedSaved()
117 {
118 $bookmark = $this->privateLinkDB->get(42);
119 $this->assertEquals(42, $bookmark->getId());
120 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
121 }
122
123 /**
124 * Test get() method for a defined and not saved bookmark
125 */
126 public function testGetDefinedNotSaved()
127 {
128 $bookmark = new Bookmark();
129 $this->privateLinkDB->add($bookmark);
130 $createdBookmark = $this->privateLinkDB->get(43);
131 $this->assertEquals(43, $createdBookmark->getId());
132 $this->assertEmpty($createdBookmark->getDescription());
133 }
134
135 /**
136 * Test get() method for an undefined bookmark
137 */
138 public function testGetUndefined()
139 {
140 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
141
142 $this->privateLinkDB->get(666);
143 }
144
145 /**
146 * Test add() method for a bookmark fully built
147 */
148 public function testAddFull()
149 {
150 $bookmark = new Bookmark();
151 $bookmark->setUrl($url = 'https://domain.tld/index.php');
152 $bookmark->setShortUrl('abc');
153 $bookmark->setTitle($title = 'This a brand new bookmark');
154 $bookmark->setDescription($desc = 'It should be created and written');
155 $bookmark->setTags($tags = ['tag1', 'tagssss']);
156 $bookmark->setThumbnail($thumb = 'http://thumb.tld/dle.png');
157 $bookmark->setPrivate(true);
158 $bookmark->setSticky(true);
159 $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190518_140354'));
160 $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190518_150354'));
161
162 $this->privateLinkDB->add($bookmark);
163 $bookmark = $this->privateLinkDB->get(43);
164 $this->assertEquals(43, $bookmark->getId());
165 $this->assertEquals($url, $bookmark->getUrl());
166 $this->assertEquals('abc', $bookmark->getShortUrl());
167 $this->assertEquals($title, $bookmark->getTitle());
168 $this->assertEquals($desc, $bookmark->getDescription());
169 $this->assertEquals($tags, $bookmark->getTags());
170 $this->assertEquals($thumb, $bookmark->getThumbnail());
171 $this->assertTrue($bookmark->isPrivate());
172 $this->assertTrue($bookmark->isSticky());
173 $this->assertEquals($created, $bookmark->getCreated());
174 $this->assertEquals($updated, $bookmark->getUpdated());
175
176 // reload from file
177 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
178
179 $bookmark = $this->privateLinkDB->get(43);
180 $this->assertEquals(43, $bookmark->getId());
181 $this->assertEquals($url, $bookmark->getUrl());
182 $this->assertEquals('abc', $bookmark->getShortUrl());
183 $this->assertEquals($title, $bookmark->getTitle());
184 $this->assertEquals($desc, $bookmark->getDescription());
185 $this->assertEquals($tags, $bookmark->getTags());
186 $this->assertEquals($thumb, $bookmark->getThumbnail());
187 $this->assertTrue($bookmark->isPrivate());
188 $this->assertTrue($bookmark->isSticky());
189 $this->assertEquals($created, $bookmark->getCreated());
190 $this->assertEquals($updated, $bookmark->getUpdated());
191 }
192
193 /**
194 * Test add() method for a bookmark without any field set
195 */
196 public function testAddMinimal()
197 {
198 $bookmark = new Bookmark();
199 $this->privateLinkDB->add($bookmark);
200
201 $bookmark = $this->privateLinkDB->get(43);
202 $this->assertEquals(43, $bookmark->getId());
203 $this->assertRegExp('#/shaare/[\w\-]{6}#', $bookmark->getUrl());
204 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
205 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
206 $this->assertEmpty($bookmark->getDescription());
207 $this->assertEmpty($bookmark->getTags());
208 $this->assertEmpty($bookmark->getThumbnail());
209 $this->assertFalse($bookmark->isPrivate());
210 $this->assertFalse($bookmark->isSticky());
211 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getCreated());
212 $this->assertNull($bookmark->getUpdated());
213
214 // reload from file
215 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
216
217 $bookmark = $this->privateLinkDB->get(43);
218 $this->assertEquals(43, $bookmark->getId());
219 $this->assertRegExp('#/shaare/[\w\-]{6}#', $bookmark->getUrl());
220 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
221 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
222 $this->assertEmpty($bookmark->getDescription());
223 $this->assertEmpty($bookmark->getTags());
224 $this->assertEmpty($bookmark->getThumbnail());
225 $this->assertFalse($bookmark->isPrivate());
226 $this->assertFalse($bookmark->isSticky());
227 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getCreated());
228 $this->assertNull($bookmark->getUpdated());
229 }
230
231 /**
232 * Test add() method for a bookmark without any field set and without writing the data store
233 */
234 public function testAddMinimalNoWrite()
235 {
236 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
237
238 $bookmark = new Bookmark();
239 $this->privateLinkDB->add($bookmark, false);
240
241 $bookmark = $this->privateLinkDB->get(43);
242 $this->assertEquals(43, $bookmark->getId());
243
244 // reload from file
245 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
246
247 $this->privateLinkDB->get(43);
248 }
249
250 /**
251 * Test add() method while logged out
252 */
253 public function testAddLoggedOut()
254 {
255 $this->expectException(\Exception::class);
256 $this->expectExceptionMessage('You\'re not authorized to alter the datastore');
257
258 $this->publicLinkDB->add(new Bookmark());
259 }
260
261 /**
262 * Test add() method with an entry which is not a bookmark instance
263 */
264 public function testAddNotABookmark()
265 {
266 $this->expectException(\Exception::class);
267 $this->expectExceptionMessage('Provided data is invalid');
268
269 $this->privateLinkDB->add(['title' => 'hi!']);
270 }
271
272 /**
273 * Test add() method with a Bookmark already containing an ID
274 */
275 public function testAddWithId()
276 {
277 $this->expectException(\Exception::class);
278 $this->expectExceptionMessage('This bookmarks already exists');
279
280 $bookmark = new Bookmark();
281 $bookmark->setId(43);
282 $this->privateLinkDB->add($bookmark);
283 }
284
285 /**
286 * Test set() method for a bookmark fully built
287 */
288 public function testSetFull()
289 {
290 $bookmark = $this->privateLinkDB->get(42);
291 $bookmark->setUrl($url = 'https://domain.tld/index.php');
292 $bookmark->setShortUrl('abc');
293 $bookmark->setTitle($title = 'This a brand new bookmark');
294 $bookmark->setDescription($desc = 'It should be created and written');
295 $bookmark->setTags($tags = ['tag1', 'tagssss']);
296 $bookmark->setThumbnail($thumb = 'http://thumb.tld/dle.png');
297 $bookmark->setPrivate(true);
298 $bookmark->setSticky(true);
299 $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190518_140354'));
300 $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190518_150354'));
301
302 $this->privateLinkDB->set($bookmark);
303 $bookmark = $this->privateLinkDB->get(42);
304 $this->assertEquals(42, $bookmark->getId());
305 $this->assertEquals($url, $bookmark->getUrl());
306 $this->assertEquals('abc', $bookmark->getShortUrl());
307 $this->assertEquals($title, $bookmark->getTitle());
308 $this->assertEquals($desc, $bookmark->getDescription());
309 $this->assertEquals($tags, $bookmark->getTags());
310 $this->assertEquals($thumb, $bookmark->getThumbnail());
311 $this->assertTrue($bookmark->isPrivate());
312 $this->assertTrue($bookmark->isSticky());
313 $this->assertEquals($created, $bookmark->getCreated());
314 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
315
316 // reload from file
317 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
318
319 $bookmark = $this->privateLinkDB->get(42);
320 $this->assertEquals(42, $bookmark->getId());
321 $this->assertEquals($url, $bookmark->getUrl());
322 $this->assertEquals('abc', $bookmark->getShortUrl());
323 $this->assertEquals($title, $bookmark->getTitle());
324 $this->assertEquals($desc, $bookmark->getDescription());
325 $this->assertEquals($tags, $bookmark->getTags());
326 $this->assertEquals($thumb, $bookmark->getThumbnail());
327 $this->assertTrue($bookmark->isPrivate());
328 $this->assertTrue($bookmark->isSticky());
329 $this->assertEquals($created, $bookmark->getCreated());
330 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
331 }
332
333 /**
334 * Test set() method for a bookmark without any field set
335 */
336 public function testSetMinimal()
337 {
338 $bookmark = $this->privateLinkDB->get(42);
339 $this->privateLinkDB->set($bookmark);
340
341 $bookmark = $this->privateLinkDB->get(42);
342 $this->assertEquals(42, $bookmark->getId());
343 $this->assertEquals('/shaare/WDWyig', $bookmark->getUrl());
344 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
345 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
346 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
347 $this->assertEquals(['ut'], $bookmark->getTags());
348 $this->assertFalse($bookmark->getThumbnail());
349 $this->assertFalse($bookmark->isPrivate());
350 $this->assertFalse($bookmark->isSticky());
351 $this->assertEquals(
352 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
353 $bookmark->getCreated()
354 );
355 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
356
357 // reload from file
358 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
359
360 $bookmark = $this->privateLinkDB->get(42);
361 $this->assertEquals(42, $bookmark->getId());
362 $this->assertEquals('/shaare/WDWyig', $bookmark->getUrl());
363 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
364 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
365 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
366 $this->assertEquals(['ut'], $bookmark->getTags());
367 $this->assertFalse($bookmark->getThumbnail());
368 $this->assertFalse($bookmark->isPrivate());
369 $this->assertFalse($bookmark->isSticky());
370 $this->assertEquals(
371 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
372 $bookmark->getCreated()
373 );
374 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
375 }
376
377 /**
378 * Test set() method for a bookmark without any field set and without writing the data store
379 */
380 public function testSetMinimalNoWrite()
381 {
382 $bookmark = $this->privateLinkDB->get(42);
383 $bookmark->setTitle($title = 'hi!');
384 $this->privateLinkDB->set($bookmark, false);
385
386 $bookmark = $this->privateLinkDB->get(42);
387 $this->assertEquals(42, $bookmark->getId());
388 $this->assertEquals($title, $bookmark->getTitle());
389
390 // reload from file
391 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
392
393 $bookmark = $this->privateLinkDB->get(42);
394 $this->assertEquals(42, $bookmark->getId());
395 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
396 }
397
398 /**
399 * Test set() method while logged out
400 */
401 public function testSetLoggedOut()
402 {
403 $this->expectException(\Exception::class);
404 $this->expectExceptionMessage('You\'re not authorized to alter the datastore');
405
406 $this->publicLinkDB->set(new Bookmark());
407 }
408
409 /**
410 * Test set() method with an entry which is not a bookmark instance
411 */
412 public function testSetNotABookmark()
413 {
414 $this->expectException(\Exception::class);
415 $this->expectExceptionMessage('Provided data is invalid');
416
417 $this->privateLinkDB->set(['title' => 'hi!']);
418 }
419
420 /**
421 * Test set() method with a Bookmark without an ID defined.
422 */
423 public function testSetWithoutId()
424 {
425 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
426
427 $bookmark = new Bookmark();
428 $this->privateLinkDB->set($bookmark);
429 }
430
431 /**
432 * Test set() method with a Bookmark with an unknow ID
433 */
434 public function testSetWithUnknownId()
435 {
436 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
437
438 $bookmark = new Bookmark();
439 $bookmark->setId(666);
440 $this->privateLinkDB->set($bookmark);
441 }
442
443 /**
444 * Test addOrSet() method with a new ID
445 */
446 public function testAddOrSetNew()
447 {
448 $bookmark = new Bookmark();
449 $this->privateLinkDB->addOrSet($bookmark);
450
451 $bookmark = $this->privateLinkDB->get(43);
452 $this->assertEquals(43, $bookmark->getId());
453
454 // reload from file
455 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
456
457 $bookmark = $this->privateLinkDB->get(43);
458 $this->assertEquals(43, $bookmark->getId());
459 }
460
461 /**
462 * Test addOrSet() method with an existing ID
463 */
464 public function testAddOrSetExisting()
465 {
466 $bookmark = $this->privateLinkDB->get(42);
467 $bookmark->setTitle($title = 'hi!');
468 $this->privateLinkDB->addOrSet($bookmark);
469
470 $bookmark = $this->privateLinkDB->get(42);
471 $this->assertEquals(42, $bookmark->getId());
472 $this->assertEquals($title, $bookmark->getTitle());
473
474 // reload from file
475 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
476
477 $bookmark = $this->privateLinkDB->get(42);
478 $this->assertEquals(42, $bookmark->getId());
479 $this->assertEquals($title, $bookmark->getTitle());
480 }
481
482 /**
483 * Test addOrSet() method while logged out
484 */
485 public function testAddOrSetLoggedOut()
486 {
487 $this->expectException(\Exception::class);
488 $this->expectExceptionMessage('You\'re not authorized to alter the datastore');
489
490 $this->publicLinkDB->addOrSet(new Bookmark());
491 }
492
493 /**
494 * Test addOrSet() method with an entry which is not a bookmark instance
495 */
496 public function testAddOrSetNotABookmark()
497 {
498 $this->expectException(\Exception::class);
499 $this->expectExceptionMessage('Provided data is invalid');
500
501 $this->privateLinkDB->addOrSet(['title' => 'hi!']);
502 }
503
504 /**
505 * Test addOrSet() method for a bookmark without any field set and without writing the data store
506 */
507 public function testAddOrSetMinimalNoWrite()
508 {
509 $bookmark = $this->privateLinkDB->get(42);
510 $bookmark->setTitle($title = 'hi!');
511 $this->privateLinkDB->addOrSet($bookmark, false);
512
513 $bookmark = $this->privateLinkDB->get(42);
514 $this->assertEquals(42, $bookmark->getId());
515 $this->assertEquals($title, $bookmark->getTitle());
516
517 // reload from file
518 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
519
520 $bookmark = $this->privateLinkDB->get(42);
521 $this->assertEquals(42, $bookmark->getId());
522 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
523 }
524
525 /**
526 * Test remove() method with an existing Bookmark
527 */
528 public function testRemoveExisting()
529 {
530 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
531
532 $bookmark = $this->privateLinkDB->get(42);
533 $this->privateLinkDB->remove($bookmark);
534
535 $exception = null;
536 try {
537 $this->privateLinkDB->get(42);
538 } catch (BookmarkNotFoundException $e) {
539 $exception = $e;
540 }
541 $this->assertInstanceOf(BookmarkNotFoundException::class, $exception);
542
543 // reload from file
544 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
545
546 $this->privateLinkDB->get(42);
547 }
548
549 /**
550 * Test remove() method while logged out
551 */
552 public function testRemoveLoggedOut()
553 {
554 $this->expectException(\Exception::class);
555 $this->expectExceptionMessage('You\'re not authorized to alter the datastore');
556
557 $bookmark = $this->privateLinkDB->get(42);
558 $this->publicLinkDB->remove($bookmark);
559 }
560
561 /**
562 * Test remove() method with an entry which is not a bookmark instance
563 */
564 public function testRemoveNotABookmark()
565 {
566 $this->expectException(\Exception::class);
567 $this->expectExceptionMessage('Provided data is invalid');
568
569 $this->privateLinkDB->remove(['title' => 'hi!']);
570 }
571
572 /**
573 * Test remove() method with a Bookmark with an unknown ID
574 */
575 public function testRemoveWithUnknownId()
576 {
577 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
578
579 $bookmark = new Bookmark();
580 $bookmark->setId(666);
581 $this->privateLinkDB->remove($bookmark);
582 }
583
584 /**
585 * Test exists() method
586 */
587 public function testExists()
588 {
589 $this->assertTrue($this->privateLinkDB->exists(42)); // public
590 $this->assertTrue($this->privateLinkDB->exists(6)); // private
591
592 $this->assertTrue($this->privateLinkDB->exists(42, BookmarkFilter::$ALL));
593 $this->assertTrue($this->privateLinkDB->exists(6, BookmarkFilter::$ALL));
594
595 $this->assertTrue($this->privateLinkDB->exists(42, BookmarkFilter::$PUBLIC));
596 $this->assertFalse($this->privateLinkDB->exists(6, BookmarkFilter::$PUBLIC));
597
598 $this->assertFalse($this->privateLinkDB->exists(42, BookmarkFilter::$PRIVATE));
599 $this->assertTrue($this->privateLinkDB->exists(6, BookmarkFilter::$PRIVATE));
600
601 $this->assertTrue($this->publicLinkDB->exists(42));
602 $this->assertFalse($this->publicLinkDB->exists(6));
603
604 $this->assertTrue($this->publicLinkDB->exists(42, BookmarkFilter::$PUBLIC));
605 $this->assertFalse($this->publicLinkDB->exists(6, BookmarkFilter::$PUBLIC));
606
607 $this->assertFalse($this->publicLinkDB->exists(42, BookmarkFilter::$PRIVATE));
608 $this->assertTrue($this->publicLinkDB->exists(6, BookmarkFilter::$PRIVATE));
609 }
610
611 /**
612 * Test initialize() method
613 */
614 public function testInitialize()
615 {
616 $dbSize = $this->privateLinkDB->count();
617 $this->privateLinkDB->initialize();
618 $this->assertEquals($dbSize + 3, $this->privateLinkDB->count());
619 $this->assertStringStartsWith(
620 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.',
621 $this->privateLinkDB->get(43)->getDescription()
622 );
623 $this->assertStringStartsWith(
624 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.',
625 $this->privateLinkDB->get(44)->getDescription()
626 );
627 $this->assertStringStartsWith(
628 'Welcome to Shaarli!',
629 $this->privateLinkDB->get(45)->getDescription()
630 );
631 }
632
633 /*
634 * The following tests have been taken from the legacy LinkDB test and adapted
635 * to make sure that nothing have been broken in the migration process.
636 * They mostly cover search/filters. Some of them might be redundant with the previous ones.
637 */
638 /**
639 * Attempt to instantiate a LinkDB whereas the datastore is not writable
640 */
641 public function testConstructDatastoreNotWriteable()
642 {
643 $this->expectException(\Shaarli\Bookmark\Exception\NotWritableDataStoreException::class);
644 $this->expectExceptionMessageRegExp('#Couldn\'t load data from the data store file "null".*#');
645
646 $conf = new ConfigManager('tests/utils/config/configJson');
647 $conf->set('resource.datastore', 'null/store.db');
648 new BookmarkFileService($conf, $this->history, true);
649 }
650
651 /**
652 * The DB doesn't exist, ensure it is created with an empty datastore
653 */
654 public function testCheckDBNewLoggedIn()
655 {
656 unlink(self::$testDatastore);
657 $this->assertFileNotExists(self::$testDatastore);
658 new BookmarkFileService($this->conf, $this->history, true);
659 $this->assertFileExists(self::$testDatastore);
660
661 // ensure the correct data has been written
662 $this->assertGreaterThan(0, filesize(self::$testDatastore));
663 }
664
665 /**
666 * The DB doesn't exist, but not logged in, ensure it initialized, but the file is not written
667 */
668 public function testCheckDBNewLoggedOut()
669 {
670 unlink(self::$testDatastore);
671 $this->assertFileNotExists(self::$testDatastore);
672 $db = new \FakeBookmarkService($this->conf, $this->history, false);
673 $this->assertFileNotExists(self::$testDatastore);
674 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
675 $this->assertCount(0, $db->getBookmarks());
676 }
677
678 /**
679 * Load public bookmarks from the DB
680 */
681 public function testReadPublicDB()
682 {
683 $this->assertEquals(
684 $this->refDB->countPublicLinks(),
685 $this->publicLinkDB->count()
686 );
687 }
688
689 /**
690 * Load public and private bookmarks from the DB
691 */
692 public function testReadPrivateDB()
693 {
694 $this->assertEquals(
695 $this->refDB->countLinks(),
696 $this->privateLinkDB->count()
697 );
698 }
699
700 /**
701 * Save the bookmarks to the DB
702 */
703 public function testSave()
704 {
705 $testDB = new BookmarkFileService($this->conf, $this->history, true);
706 $dbSize = $testDB->count();
707
708 $bookmark = new Bookmark();
709 $testDB->add($bookmark);
710
711 $testDB = new BookmarkFileService($this->conf, $this->history, true);
712 $this->assertEquals($dbSize + 1, $testDB->count());
713 }
714
715 /**
716 * Count existing bookmarks - public bookmarks hidden
717 */
718 public function testCountHiddenPublic()
719 {
720 $this->conf->set('privacy.hide_public_links', true);
721 $linkDB = new BookmarkFileService($this->conf, $this->history, false);
722
723 $this->assertEquals(0, $linkDB->count());
724 }
725
726 /**
727 * List the days for which bookmarks have been posted
728 */
729 public function testDays()
730 {
731 $this->assertEquals(
732 ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'],
733 $this->publicLinkDB->days()
734 );
735
736 $this->assertEquals(
737 ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'],
738 $this->privateLinkDB->days()
739 );
740 }
741
742 /**
743 * The URL corresponds to an existing entry in the DB
744 */
745 public function testGetKnownLinkFromURL()
746 {
747 $link = $this->publicLinkDB->findByUrl('http://mediagoblin.org/');
748
749 $this->assertNotEquals(false, $link);
750 $this->assertContainsPolyfill(
751 'A free software media publishing platform',
752 $link->getDescription()
753 );
754 }
755
756 /**
757 * The URL is not in the DB
758 */
759 public function testGetUnknownLinkFromURL()
760 {
761 $this->assertEquals(
762 false,
763 $this->publicLinkDB->findByUrl('http://dev.null')
764 );
765 }
766
767 /**
768 * Lists all tags
769 */
770 public function testAllTags()
771 {
772 $this->assertEquals(
773 [
774 'web' => 3,
775 'cartoon' => 2,
776 'gnu' => 2,
777 'dev' => 1,
778 'samba' => 1,
779 'media' => 1,
780 'software' => 1,
781 'stallman' => 1,
782 'free' => 1,
783 '-exclude' => 1,
784 'hashtag' => 2,
785 // The DB contains a link with `sTuff` and another one with `stuff` tag.
786 // They need to be grouped with the first case found - order by date DESC: `sTuff`.
787 'sTuff' => 2,
788 'ut' => 1,
789 ],
790 $this->publicLinkDB->bookmarksCountPerTag()
791 );
792
793 $this->assertEquals(
794 [
795 'web' => 4,
796 'cartoon' => 3,
797 'gnu' => 2,
798 'dev' => 2,
799 'samba' => 1,
800 'media' => 1,
801 'software' => 1,
802 'stallman' => 1,
803 'free' => 1,
804 'html' => 1,
805 'w3c' => 1,
806 'css' => 1,
807 'Mercurial' => 1,
808 'sTuff' => 2,
809 '-exclude' => 1,
810 '.hidden' => 1,
811 'hashtag' => 2,
812 'tag1' => 1,
813 'tag2' => 1,
814 'tag3' => 1,
815 'tag4' => 1,
816 'ut' => 1,
817 ],
818 $this->privateLinkDB->bookmarksCountPerTag()
819 );
820 $this->assertEquals(
821 [
822 'cartoon' => 2,
823 'gnu' => 1,
824 'dev' => 1,
825 'samba' => 1,
826 'media' => 1,
827 'html' => 1,
828 'w3c' => 1,
829 'css' => 1,
830 'Mercurial' => 1,
831 '.hidden' => 1,
832 'hashtag' => 1,
833 ],
834 $this->privateLinkDB->bookmarksCountPerTag(['web'])
835 );
836 $this->assertEquals(
837 [
838 'html' => 1,
839 'w3c' => 1,
840 'css' => 1,
841 'Mercurial' => 1,
842 ],
843 $this->privateLinkDB->bookmarksCountPerTag(['web'], 'private')
844 );
845 }
846
847 /**
848 * Test filter with string.
849 */
850 public function testFilterString()
851 {
852 $tags = 'dev cartoon';
853 $request = ['searchtags' => $tags];
854 $this->assertEquals(
855 2,
856 count($this->privateLinkDB->search($request, null, true))
857 );
858 }
859
860 /**
861 * Test filter with array.
862 */
863 public function testFilterArray()
864 {
865 $tags = ['dev', 'cartoon'];
866 $request = ['searchtags' => $tags];
867 $this->assertEquals(
868 2,
869 count($this->privateLinkDB->search($request, null, true))
870 );
871 }
872
873 /**
874 * Test hidden tags feature:
875 * tags starting with a dot '.' are only visible when logged in.
876 */
877 public function testHiddenTags()
878 {
879 $tags = '.hidden';
880 $request = ['searchtags' => $tags];
881 $this->assertEquals(
882 1,
883 count($this->privateLinkDB->search($request, 'all', true))
884 );
885
886 $this->assertEquals(
887 0,
888 count($this->publicLinkDB->search($request, 'public', true))
889 );
890 }
891
892 /**
893 * Test filterHash() with a valid smallhash.
894 */
895 public function testFilterHashValid()
896 {
897 $request = smallHash('20150310_114651');
898 $this->assertSame(
899 $request,
900 $this->publicLinkDB->findByHash($request)->getShortUrl()
901 );
902 $request = smallHash('20150310_114633' . 8);
903 $this->assertSame(
904 $request,
905 $this->publicLinkDB->findByHash($request)->getShortUrl()
906 );
907 }
908
909 /**
910 * Test filterHash() with an invalid smallhash.
911 */
912 public function testFilterHashInValid1()
913 {
914 $this->expectException(BookmarkNotFoundException::class);
915
916 $request = 'blabla';
917 $this->publicLinkDB->findByHash($request);
918 }
919
920 /**
921 * Test filterHash() with an empty smallhash.
922 */
923 public function testFilterHashInValid()
924 {
925 $this->expectException(BookmarkNotFoundException::class);
926
927 $this->publicLinkDB->findByHash('');
928 }
929
930 /**
931 * Test linksCountPerTag all tags without filter.
932 * Equal occurrences should be sorted alphabetically.
933 */
934 public function testCountLinkPerTagAllNoFilter()
935 {
936 $expected = [
937 'web' => 4,
938 'cartoon' => 3,
939 'dev' => 2,
940 'gnu' => 2,
941 'hashtag' => 2,
942 'sTuff' => 2,
943 '-exclude' => 1,
944 '.hidden' => 1,
945 'Mercurial' => 1,
946 'css' => 1,
947 'free' => 1,
948 'html' => 1,
949 'media' => 1,
950 'samba' => 1,
951 'software' => 1,
952 'stallman' => 1,
953 'tag1' => 1,
954 'tag2' => 1,
955 'tag3' => 1,
956 'tag4' => 1,
957 'ut' => 1,
958 'w3c' => 1,
959 ];
960 $tags = $this->privateLinkDB->bookmarksCountPerTag();
961
962 $this->assertEquals($expected, $tags, var_export($tags, true));
963 }
964
965 /**
966 * Test linksCountPerTag all tags with filter.
967 * Equal occurrences should be sorted alphabetically.
968 */
969 public function testCountLinkPerTagAllWithFilter()
970 {
971 $expected = [
972 'hashtag' => 2,
973 '-exclude' => 1,
974 '.hidden' => 1,
975 'free' => 1,
976 'media' => 1,
977 'software' => 1,
978 'stallman' => 1,
979 'stuff' => 1,
980 'web' => 1,
981 ];
982 $tags = $this->privateLinkDB->bookmarksCountPerTag(['gnu']);
983
984 $this->assertEquals($expected, $tags, var_export($tags, true));
985 }
986
987 /**
988 * Test linksCountPerTag public tags with filter.
989 * Equal occurrences should be sorted alphabetically.
990 */
991 public function testCountLinkPerTagPublicWithFilter()
992 {
993 $expected = [
994 'hashtag' => 2,
995 '-exclude' => 1,
996 '.hidden' => 1,
997 'free' => 1,
998 'media' => 1,
999 'software' => 1,
1000 'stallman' => 1,
1001 'stuff' => 1,
1002 'web' => 1,
1003 ];
1004 $tags = $this->privateLinkDB->bookmarksCountPerTag(['gnu'], 'public');
1005
1006 $this->assertEquals($expected, $tags, var_export($tags, true));
1007 }
1008
1009 /**
1010 * Test linksCountPerTag public tags with filter.
1011 * Equal occurrences should be sorted alphabetically.
1012 */
1013 public function testCountLinkPerTagPrivateWithFilter()
1014 {
1015 $expected = [
1016 'cartoon' => 1,
1017 'tag1' => 1,
1018 'tag2' => 1,
1019 'tag3' => 1,
1020 'tag4' => 1,
1021 ];
1022 $tags = $this->privateLinkDB->bookmarksCountPerTag(['dev'], 'private');
1023
1024 $this->assertEquals($expected, $tags, var_export($tags, true));
1025 }
1026
1027 /**
1028 * Test linksCountPerTag public tags with filter.
1029 * Equal occurrences should be sorted alphabetically.
1030 */
1031 public function testCountTagsNoMarkdown()
1032 {
1033 $expected = [
1034 'cartoon' => 3,
1035 'dev' => 2,
1036 'tag1' => 1,
1037 'tag2' => 1,
1038 'tag3' => 1,
1039 'tag4' => 1,
1040 'web' => 4,
1041 'gnu' => 2,
1042 'hashtag' => 2,
1043 'sTuff' => 2,
1044 '-exclude' => 1,
1045 '.hidden' => 1,
1046 'Mercurial' => 1,
1047 'css' => 1,
1048 'free' => 1,
1049 'html' => 1,
1050 'media' => 1,
1051 'newTagToCount' => 1,
1052 'samba' => 1,
1053 'software' => 1,
1054 'stallman' => 1,
1055 'ut' => 1,
1056 'w3c' => 1,
1057 ];
1058 $bookmark = new Bookmark();
1059 $bookmark->setTags(['newTagToCount', BookmarkMarkdownFormatter::NO_MD_TAG]);
1060 $this->privateLinkDB->add($bookmark);
1061
1062 $tags = $this->privateLinkDB->bookmarksCountPerTag();
1063
1064 $this->assertEquals($expected, $tags, var_export($tags, true));
1065 }
1066
1067 /**
1068 * Test filterDay while logged in
1069 */
1070 public function testFilterDayLoggedIn(): void
1071 {
1072 $bookmarks = $this->privateLinkDB->filterDay('20121206');
1073 $expectedIds = [4, 9, 1, 0];
1074
1075 static::assertCount(4, $bookmarks);
1076 foreach ($bookmarks as $bookmark) {
1077 $i = ($i ?? -1) + 1;
1078 static::assertSame($expectedIds[$i], $bookmark->getId());
1079 }
1080 }
1081
1082 /**
1083 * Test filterDay while logged out
1084 */
1085 public function testFilterDayLoggedOut(): void
1086 {
1087 $bookmarks = $this->publicLinkDB->filterDay('20121206');
1088 $expectedIds = [4, 9, 1];
1089
1090 static::assertCount(3, $bookmarks);
1091 foreach ($bookmarks as $bookmark) {
1092 $i = ($i ?? -1) + 1;
1093 static::assertSame($expectedIds[$i], $bookmark->getId());
1094 }
1095 }
1096
1097 /**
1098 * Allows to test LinkDB's private methods
1099 *
1100 * @see
1101 * https://sebastian-bergmann.de/archives/881-Testing-Your-Privates.html
1102 * http://stackoverflow.com/a/2798203
1103 */
1104 protected static function getMethod($name)
1105 {
1106 $class = new ReflectionClass('Shaarli\Bookmark\BookmarkFileService');
1107 $method = $class->getMethod($name);
1108 $method->setAccessible(true);
1109 return $method;
1110 }
1111}
diff --git a/tests/bookmark/BookmarkFilterTest.php b/tests/bookmark/BookmarkFilterTest.php
new file mode 100644
index 00000000..48c7f824
--- /dev/null
+++ b/tests/bookmark/BookmarkFilterTest.php
@@ -0,0 +1,526 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Exception;
6use ReferenceLinkDB;
7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
9use Shaarli\TestCase;
10
11/**
12 * Class BookmarkFilterTest.
13 */
14class BookmarkFilterTest extends TestCase
15{
16 /**
17 * @var string Test datastore path.
18 */
19 protected static $testDatastore = 'sandbox/datastore.php';
20 /**
21 * @var BookmarkFilter instance.
22 */
23 protected static $linkFilter;
24
25 /**
26 * @var ReferenceLinkDB instance
27 */
28 protected static $refDB;
29
30 /**
31 * @var BookmarkFileService instance
32 */
33 protected static $bookmarkService;
34
35 /**
36 * Instantiate linkFilter with ReferenceLinkDB data.
37 */
38 public static function setUpBeforeClass(): void
39 {
40 $conf = new ConfigManager('tests/utils/config/configJson');
41 $conf->set('resource.datastore', self::$testDatastore);
42 self::$refDB = new \ReferenceLinkDB();
43 self::$refDB->write(self::$testDatastore);
44 $history = new History('sandbox/history.php');
45 self::$bookmarkService = new \FakeBookmarkService($conf, $history, true);
46 self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks());
47 }
48
49 /**
50 * Blank filter.
51 */
52 public function testFilter()
53 {
54 $this->assertEquals(
55 self::$refDB->countLinks(),
56 count(self::$linkFilter->filter('', ''))
57 );
58
59 $this->assertEquals(
60 self::$refDB->countLinks(),
61 count(self::$linkFilter->filter('', '', 'all'))
62 );
63
64 $this->assertEquals(
65 self::$refDB->countLinks(),
66 count(self::$linkFilter->filter('', '', 'randomstr'))
67 );
68
69 // Private only.
70 $this->assertEquals(
71 self::$refDB->countPrivateLinks(),
72 count(self::$linkFilter->filter('', '', false, 'private'))
73 );
74
75 // Public only.
76 $this->assertEquals(
77 self::$refDB->countPublicLinks(),
78 count(self::$linkFilter->filter('', '', false, 'public'))
79 );
80
81 $this->assertEquals(
82 ReferenceLinkDB::$NB_LINKS_TOTAL,
83 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, ''))
84 );
85
86 $this->assertEquals(
87 self::$refDB->countUntaggedLinks(),
88 count(
89 self::$linkFilter->filter(
90 BookmarkFilter::$FILTER_TAG,
91 /*$request=*/
92 '',
93 /*$casesensitive=*/
94 false,
95 /*$visibility=*/
96 'all',
97 /*$untaggedonly=*/
98 true
99 )
100 )
101 );
102
103 $this->assertEquals(
104 ReferenceLinkDB::$NB_LINKS_TOTAL,
105 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, ''))
106 );
107 }
108
109 /**
110 * Filter bookmarks using a tag
111 */
112 public function testFilterOneTag()
113 {
114 $this->assertEquals(
115 4,
116 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false))
117 );
118
119 $this->assertEquals(
120 4,
121 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'all'))
122 );
123
124 $this->assertEquals(
125 4,
126 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'default-blabla'))
127 );
128
129 // Private only.
130 $this->assertEquals(
131 1,
132 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'private'))
133 );
134
135 // Public only.
136 $this->assertEquals(
137 3,
138 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'public'))
139 );
140 }
141
142 /**
143 * Filter bookmarks using a tag - case-sensitive
144 */
145 public function testFilterCaseSensitiveTag()
146 {
147 $this->assertEquals(
148 0,
149 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'mercurial', true))
150 );
151
152 $this->assertEquals(
153 1,
154 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'Mercurial', true))
155 );
156 }
157
158 /**
159 * Filter bookmarks using a tag combination
160 */
161 public function testFilterMultipleTags()
162 {
163 $this->assertEquals(
164 2,
165 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'dev cartoon', false))
166 );
167 }
168
169 /**
170 * Filter bookmarks using a non-existent tag
171 */
172 public function testFilterUnknownTag()
173 {
174 $this->assertEquals(
175 0,
176 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'null', false))
177 );
178 }
179
180 /**
181 * Return bookmarks for a given day
182 */
183 public function testFilterDay()
184 {
185 $this->assertEquals(
186 4,
187 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20121206'))
188 );
189 }
190
191 /**
192 * Return bookmarks for a given day
193 */
194 public function testFilterDayRestrictedVisibility(): void
195 {
196 $this->assertEquals(
197 3,
198 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20121206', false, BookmarkFilter::$PUBLIC))
199 );
200 }
201
202 /**
203 * 404 - day not found
204 */
205 public function testFilterUnknownDay()
206 {
207 $this->assertEquals(
208 0,
209 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '19700101'))
210 );
211 }
212
213 /**
214 * Use an invalid date format
215 */
216 public function testFilterInvalidDayWithChars()
217 {
218 $this->expectException(\Exception::class);
219 $this->expectExceptionMessageRegExp('/Invalid date format/');
220
221 self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, 'Rainy day, dream away');
222 }
223
224 /**
225 * Use an invalid date format
226 */
227 public function testFilterInvalidDayDigits()
228 {
229 $this->expectException(\Exception::class);
230 $this->expectExceptionMessageRegExp('/Invalid date format/');
231
232 self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20');
233 }
234
235 /**
236 * Retrieve a link entry with its hash
237 */
238 public function testFilterSmallHash()
239 {
240 $links = self::$linkFilter->filter(BookmarkFilter::$FILTER_HASH, 'IuWvgA');
241
242 $this->assertEquals(
243 1,
244 count($links)
245 );
246
247 $this->assertEquals(
248 'MediaGoblin',
249 $links[7]->getTitle()
250 );
251 }
252
253 /**
254 * No link for this hash
255 */
256 public function testFilterUnknownSmallHash()
257 {
258 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
259
260 self::$linkFilter->filter(BookmarkFilter::$FILTER_HASH, 'Iblaah');
261 }
262
263 /**
264 * Full-text search - no result found.
265 */
266 public function testFilterFullTextNoResult()
267 {
268 $this->assertEquals(
269 0,
270 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'azertyuiop'))
271 );
272 }
273
274 /**
275 * Full-text search - result from a link's URL
276 */
277 public function testFilterFullTextURL()
278 {
279 $this->assertEquals(
280 2,
281 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'ars.userfriendly.org'))
282 );
283
284 $this->assertEquals(
285 2,
286 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'ars org'))
287 );
288 }
289
290 /**
291 * Full-text search - result from a link's title only
292 */
293 public function testFilterFullTextTitle()
294 {
295 // use miscellaneous cases
296 $this->assertEquals(
297 2,
298 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'userfriendly -'))
299 );
300 $this->assertEquals(
301 2,
302 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'UserFriendly -'))
303 );
304 $this->assertEquals(
305 2,
306 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'uSeRFrIendlY -'))
307 );
308
309 // use miscellaneous case and offset
310 $this->assertEquals(
311 2,
312 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'RFrIendL'))
313 );
314 }
315
316 /**
317 * Full-text search - result from the link's description only
318 */
319 public function testFilterFullTextDescription()
320 {
321 $this->assertEquals(
322 1,
323 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'publishing media'))
324 );
325
326 $this->assertEquals(
327 1,
328 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'mercurial w3c'))
329 );
330
331 $this->assertEquals(
332 3,
333 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, '"free software"'))
334 );
335 }
336
337 /**
338 * Full-text search - result from the link's tags only
339 */
340 public function testFilterFullTextTags()
341 {
342 $this->assertEquals(
343 6,
344 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web'))
345 );
346
347 $this->assertEquals(
348 6,
349 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', 'all'))
350 );
351
352 $this->assertEquals(
353 6,
354 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', 'bla'))
355 );
356
357 // Private only.
358 $this->assertEquals(
359 1,
360 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', false, 'private'))
361 );
362
363 // Public only.
364 $this->assertEquals(
365 5,
366 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', false, 'public'))
367 );
368 }
369
370 /**
371 * Full-text search - result set from mixed sources
372 */
373 public function testFilterFullTextMixed()
374 {
375 $this->assertEquals(
376 3,
377 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'free software'))
378 );
379 }
380
381 /**
382 * Full-text search - test exclusion with '-'.
383 */
384 public function testExcludeSearch()
385 {
386 $this->assertEquals(
387 1,
388 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'free -gnu'))
389 );
390
391 $this->assertEquals(
392 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
393 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, '-revolution'))
394 );
395 }
396
397 /**
398 * Full-text search - test AND, exact terms and exclusion combined, across fields.
399 */
400 public function testMultiSearch()
401 {
402 $this->assertEquals(
403 2,
404 count(self::$linkFilter->filter(
405 BookmarkFilter::$FILTER_TEXT,
406 '"Free Software " stallman "read this" @website stuff'
407 ))
408 );
409
410 $this->assertEquals(
411 1,
412 count(self::$linkFilter->filter(
413 BookmarkFilter::$FILTER_TEXT,
414 '"free software " stallman "read this" -beard @website stuff'
415 ))
416 );
417 }
418
419 /**
420 * Full-text search - make sure that exact search won't work across fields.
421 */
422 public function testSearchExactTermMultiFieldsKo()
423 {
424 $this->assertEquals(
425 0,
426 count(self::$linkFilter->filter(
427 BookmarkFilter::$FILTER_TEXT,
428 '"designer naming"'
429 ))
430 );
431
432 $this->assertEquals(
433 0,
434 count(self::$linkFilter->filter(
435 BookmarkFilter::$FILTER_TEXT,
436 '"designernaming"'
437 ))
438 );
439 }
440
441 /**
442 * Tag search with exclusion.
443 */
444 public function testTagFilterWithExclusion()
445 {
446 $this->assertEquals(
447 1,
448 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'gnu -free'))
449 );
450
451 $this->assertEquals(
452 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
453 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, '-free'))
454 );
455 }
456
457 /**
458 * Test crossed search (terms + tags).
459 */
460 public function testFilterCrossedSearch()
461 {
462 $terms = '"Free Software " stallman "read this" @website stuff';
463 $tags = 'free';
464 $this->assertEquals(
465 1,
466 count(self::$linkFilter->filter(
467 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
468 array($tags, $terms)
469 ))
470 );
471 $this->assertEquals(
472 2,
473 count(self::$linkFilter->filter(
474 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
475 array('', $terms)
476 ))
477 );
478 $this->assertEquals(
479 1,
480 count(self::$linkFilter->filter(
481 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
482 array(false, 'PSR-2')
483 ))
484 );
485 $this->assertEquals(
486 1,
487 count(self::$linkFilter->filter(
488 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
489 array($tags, '')
490 ))
491 );
492 $this->assertEquals(
493 ReferenceLinkDB::$NB_LINKS_TOTAL,
494 count(self::$linkFilter->filter(
495 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
496 ''
497 ))
498 );
499 }
500
501 /**
502 * Filter bookmarks by #hashtag.
503 */
504 public function testFilterByHashtag()
505 {
506 $hashtag = 'hashtag';
507 $this->assertEquals(
508 3,
509 count(self::$linkFilter->filter(
510 BookmarkFilter::$FILTER_TAG,
511 $hashtag
512 ))
513 );
514
515 $hashtag = 'private';
516 $this->assertEquals(
517 1,
518 count(self::$linkFilter->filter(
519 BookmarkFilter::$FILTER_TAG,
520 $hashtag,
521 false,
522 'private'
523 ))
524 );
525 }
526}
diff --git a/tests/bookmark/BookmarkInitializerTest.php b/tests/bookmark/BookmarkInitializerTest.php
new file mode 100644
index 00000000..25704004
--- /dev/null
+++ b/tests/bookmark/BookmarkInitializerTest.php
@@ -0,0 +1,150 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Shaarli\Config\ConfigManager;
6use Shaarli\History;
7use Shaarli\TestCase;
8
9/**
10 * Class BookmarkInitializerTest
11 * @package Shaarli\Bookmark
12 */
13class BookmarkInitializerTest extends TestCase
14{
15 /** @var string Path of test data store */
16 protected static $testDatastore = 'sandbox/datastore.php';
17
18 /** @var string Path of test config file */
19 protected static $testConf = 'sandbox/config';
20
21 /**
22 * @var ConfigManager instance.
23 */
24 protected $conf;
25
26 /**
27 * @var History instance.
28 */
29 protected $history;
30
31 /** @var BookmarkServiceInterface instance */
32 protected $bookmarkService;
33
34 /** @var BookmarkInitializer instance */
35 protected $initializer;
36
37 /**
38 * Initialize an empty BookmarkFileService
39 */
40 public function setUp(): void
41 {
42 if (file_exists(self::$testDatastore)) {
43 unlink(self::$testDatastore);
44 }
45
46 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
47 $this->conf = new ConfigManager(self::$testConf);
48 $this->conf->set('resource.datastore', self::$testDatastore);
49 $this->history = new History('sandbox/history.php');
50 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
51
52 $this->initializer = new BookmarkInitializer($this->bookmarkService);
53 }
54
55 /**
56 * Test initialize() with a data store containing bookmarks.
57 */
58 public function testInitializeNotEmptyDataStore(): void
59 {
60 $refDB = new \ReferenceLinkDB();
61 $refDB->write(self::$testDatastore);
62 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
63 $this->initializer = new BookmarkInitializer($this->bookmarkService);
64
65 $this->initializer->initialize();
66
67 $this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count());
68
69 $bookmark = $this->bookmarkService->get(43);
70 $this->assertStringStartsWith(
71 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.',
72 $bookmark->getDescription()
73 );
74 $this->assertTrue($bookmark->isPrivate());
75
76 $bookmark = $this->bookmarkService->get(44);
77 $this->assertStringStartsWith(
78 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.',
79 $bookmark->getDescription()
80 );
81 $this->assertTrue($bookmark->isPrivate());
82
83 $bookmark = $this->bookmarkService->get(45);
84 $this->assertStringStartsWith(
85 'Welcome to Shaarli!',
86 $bookmark->getDescription()
87 );
88 $this->assertFalse($bookmark->isPrivate());
89
90 $this->bookmarkService->save();
91
92 // Reload from file
93 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
94 $this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count());
95
96 $bookmark = $this->bookmarkService->get(43);
97 $this->assertStringStartsWith(
98 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.',
99 $bookmark->getDescription()
100 );
101 $this->assertTrue($bookmark->isPrivate());
102
103 $bookmark = $this->bookmarkService->get(44);
104 $this->assertStringStartsWith(
105 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.',
106 $bookmark->getDescription()
107 );
108 $this->assertTrue($bookmark->isPrivate());
109
110 $bookmark = $this->bookmarkService->get(45);
111 $this->assertStringStartsWith(
112 'Welcome to Shaarli!',
113 $bookmark->getDescription()
114 );
115 $this->assertFalse($bookmark->isPrivate());
116 }
117
118 /**
119 * Test initialize() with an a non existent datastore file .
120 */
121 public function testInitializeNonExistentDataStore(): void
122 {
123 $this->conf->set('resource.datastore', static::$testDatastore . '_empty');
124 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
125
126 $this->initializer->initialize();
127
128 $this->assertEquals(3, $this->bookmarkService->count());
129 $bookmark = $this->bookmarkService->get(0);
130 $this->assertStringStartsWith(
131 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.',
132 $bookmark->getDescription()
133 );
134 $this->assertTrue($bookmark->isPrivate());
135
136 $bookmark = $this->bookmarkService->get(1);
137 $this->assertStringStartsWith(
138 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.',
139 $bookmark->getDescription()
140 );
141 $this->assertTrue($bookmark->isPrivate());
142
143 $bookmark = $this->bookmarkService->get(2);
144 $this->assertStringStartsWith(
145 'Welcome to Shaarli!',
146 $bookmark->getDescription()
147 );
148 $this->assertFalse($bookmark->isPrivate());
149 }
150}
diff --git a/tests/bookmark/BookmarkTest.php b/tests/bookmark/BookmarkTest.php
new file mode 100644
index 00000000..afec2440
--- /dev/null
+++ b/tests/bookmark/BookmarkTest.php
@@ -0,0 +1,388 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Shaarli\Bookmark\Exception\InvalidBookmarkException;
6use Shaarli\TestCase;
7
8/**
9 * Class BookmarkTest
10 */
11class BookmarkTest extends TestCase
12{
13 /**
14 * Test fromArray() with a link with full data
15 */
16 public function testFromArrayFull()
17 {
18 $data = [
19 'id' => 1,
20 'shorturl' => 'abc',
21 'url' => 'https://domain.tld/oof.html?param=value#anchor',
22 'title' => 'This is an array link',
23 'description' => 'HTML desc<br><p>hi!</p>',
24 'thumbnail' => 'https://domain.tld/pic.png',
25 'sticky' => true,
26 'created' => new \DateTime('-1 minute'),
27 'tags' => ['tag1', 'tag2', 'chair'],
28 'updated' => new \DateTime(),
29 'private' => true,
30 ];
31
32 $bookmark = (new Bookmark())->fromArray($data);
33 $this->assertEquals($data['id'], $bookmark->getId());
34 $this->assertEquals($data['shorturl'], $bookmark->getShortUrl());
35 $this->assertEquals($data['url'], $bookmark->getUrl());
36 $this->assertEquals($data['title'], $bookmark->getTitle());
37 $this->assertEquals($data['description'], $bookmark->getDescription());
38 $this->assertEquals($data['thumbnail'], $bookmark->getThumbnail());
39 $this->assertEquals($data['sticky'], $bookmark->isSticky());
40 $this->assertEquals($data['created'], $bookmark->getCreated());
41 $this->assertEquals($data['tags'], $bookmark->getTags());
42 $this->assertEquals('tag1 tag2 chair', $bookmark->getTagsString());
43 $this->assertEquals($data['updated'], $bookmark->getUpdated());
44 $this->assertEquals($data['private'], $bookmark->isPrivate());
45 $this->assertFalse($bookmark->isNote());
46 }
47
48 /**
49 * Test fromArray() with a link with minimal data.
50 * Note that I use null values everywhere but this should not happen in the real world.
51 */
52 public function testFromArrayMinimal()
53 {
54 $data = [
55 'id' => null,
56 'shorturl' => null,
57 'url' => null,
58 'title' => null,
59 'description' => null,
60 'created' => null,
61 'tags' => null,
62 'private' => null,
63 ];
64
65 $bookmark = (new Bookmark())->fromArray($data);
66 $this->assertNull($bookmark->getId());
67 $this->assertNull($bookmark->getShortUrl());
68 $this->assertNull($bookmark->getUrl());
69 $this->assertNull($bookmark->getTitle());
70 $this->assertEquals('', $bookmark->getDescription());
71 $this->assertNull($bookmark->getCreated());
72 $this->assertEquals([], $bookmark->getTags());
73 $this->assertEquals('', $bookmark->getTagsString());
74 $this->assertNull($bookmark->getUpdated());
75 $this->assertFalse($bookmark->getThumbnail());
76 $this->assertFalse($bookmark->isSticky());
77 $this->assertFalse($bookmark->isPrivate());
78 $this->assertTrue($bookmark->isNote());
79 }
80
81 /**
82 * Test validate() with a valid minimal bookmark
83 */
84 public function testValidateValidFullBookmark()
85 {
86 $bookmark = new Bookmark();
87 $bookmark->setId(2);
88 $bookmark->setShortUrl('abc');
89 $bookmark->setCreated($date = \DateTime::createFromFormat('Ymd_His', '20190514_200102'));
90 $bookmark->setUpdated($dateUp = \DateTime::createFromFormat('Ymd_His', '20190514_210203'));
91 $bookmark->setUrl($url = 'https://domain.tld/oof.html?param=value#anchor');
92 $bookmark->setTitle($title = 'This is an array link');
93 $bookmark->setDescription($desc = 'HTML desc<br><p>hi!</p>');
94 $bookmark->setTags($tags = ['tag1', 'tag2', 'chair']);
95 $bookmark->setThumbnail($thumb = 'https://domain.tld/pic.png');
96 $bookmark->setPrivate(true);
97 $bookmark->validate();
98
99 $this->assertEquals(2, $bookmark->getId());
100 $this->assertEquals('abc', $bookmark->getShortUrl());
101 $this->assertEquals($date, $bookmark->getCreated());
102 $this->assertEquals($dateUp, $bookmark->getUpdated());
103 $this->assertEquals($url, $bookmark->getUrl());
104 $this->assertEquals($title, $bookmark->getTitle());
105 $this->assertEquals($desc, $bookmark->getDescription());
106 $this->assertEquals($tags, $bookmark->getTags());
107 $this->assertEquals(implode(' ', $tags), $bookmark->getTagsString());
108 $this->assertEquals($thumb, $bookmark->getThumbnail());
109 $this->assertTrue($bookmark->isPrivate());
110 $this->assertFalse($bookmark->isNote());
111 }
112
113 /**
114 * Test validate() with a valid minimal bookmark
115 */
116 public function testValidateValidMinimalBookmark()
117 {
118 $bookmark = new Bookmark();
119 $bookmark->setId(1);
120 $bookmark->setShortUrl('abc');
121 $bookmark->setCreated($date = \DateTime::createFromFormat('Ymd_His', '20190514_200102'));
122 $bookmark->validate();
123
124 $this->assertEquals(1, $bookmark->getId());
125 $this->assertEquals('abc', $bookmark->getShortUrl());
126 $this->assertEquals($date, $bookmark->getCreated());
127 $this->assertEquals('/shaare/abc', $bookmark->getUrl());
128 $this->assertEquals('/shaare/abc', $bookmark->getTitle());
129 $this->assertEquals('', $bookmark->getDescription());
130 $this->assertEquals([], $bookmark->getTags());
131 $this->assertEquals('', $bookmark->getTagsString());
132 $this->assertFalse($bookmark->getThumbnail());
133 $this->assertFalse($bookmark->isPrivate());
134 $this->assertTrue($bookmark->isNote());
135 $this->assertNull($bookmark->getUpdated());
136 }
137
138 /**
139 * Test validate() with a a bookmark without ID.
140 */
141 public function testValidateNotValidNoId()
142 {
143 $bookmark = new Bookmark();
144 $bookmark->setShortUrl('abc');
145 $bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
146 $exception = null;
147 try {
148 $bookmark->validate();
149 } catch (InvalidBookmarkException $e) {
150 $exception = $e;
151 }
152 $this->assertNotNull($exception);
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->assertContainsPolyfill('- ID: str'. PHP_EOL, $exception->getMessage());
173 }
174
175 /**
176 * Test validate() with a a bookmark without short url.
177 */
178 public function testValidateNotValidNoShortUrl()
179 {
180 $bookmark = new Bookmark();
181 $bookmark->setId(1);
182 $bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
183 $bookmark->setShortUrl(null);
184 $exception = null;
185 try {
186 $bookmark->validate();
187 } catch (InvalidBookmarkException $e) {
188 $exception = $e;
189 }
190 $this->assertNotNull($exception);
191 $this->assertContainsPolyfill('- ShortUrl: '. PHP_EOL, $exception->getMessage());
192 }
193
194 /**
195 * Test validate() with a a bookmark without created datetime.
196 */
197 public function testValidateNotValidNoCreated()
198 {
199 $bookmark = new Bookmark();
200 $bookmark->setId(1);
201 $bookmark->setShortUrl('abc');
202 $bookmark->setCreated(null);
203 $exception = null;
204 try {
205 $bookmark->validate();
206 } catch (InvalidBookmarkException $e) {
207 $exception = $e;
208 }
209 $this->assertNotNull($exception);
210 $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->assertContainsPolyfill('- Created: Not a DateTime object'. PHP_EOL, $exception->getMessage());
230 }
231
232 /**
233 * Test setId() and make sure that default fields are generated.
234 */
235 public function testSetIdEmptyGeneratedFields()
236 {
237 $bookmark = new Bookmark();
238 $bookmark->setId(2);
239
240 $this->assertEquals(2, $bookmark->getId());
241 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
242 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getCreated());
243 }
244
245 /**
246 * Test setId() and with generated fields already set.
247 */
248 public function testSetIdSetGeneratedFields()
249 {
250 $bookmark = new Bookmark();
251 $bookmark->setShortUrl('abc');
252 $bookmark->setCreated($date = \DateTime::createFromFormat('Ymd_His', '20190514_200102'));
253 $bookmark->setId(2);
254
255 $this->assertEquals(2, $bookmark->getId());
256 $this->assertEquals('abc', $bookmark->getShortUrl());
257 $this->assertEquals($date, $bookmark->getCreated());
258 }
259
260 /**
261 * Test setUrl() and make sure it accepts custom protocols
262 */
263 public function testGetUrlWithValidProtocols()
264 {
265 $bookmark = new Bookmark();
266 $bookmark->setUrl($url = 'myprotocol://helloworld', ['myprotocol']);
267 $this->assertEquals($url, $bookmark->getUrl());
268
269 $bookmark->setUrl($url = 'https://helloworld.tld', ['myprotocol']);
270 $this->assertEquals($url, $bookmark->getUrl());
271 }
272
273 /**
274 * Test setUrl() and make sure it accepts custom protocols
275 */
276 public function testGetUrlWithNotValidProtocols()
277 {
278 $bookmark = new Bookmark();
279 $bookmark->setUrl('myprotocol://helloworld', []);
280 $this->assertEquals('http://helloworld', $bookmark->getUrl());
281
282 $bookmark->setUrl($url = 'https://helloworld.tld', []);
283 $this->assertEquals($url, $bookmark->getUrl());
284 }
285
286 /**
287 * Test setTagsString() with exotic data
288 */
289 public function testSetTagsString()
290 {
291 $bookmark = new Bookmark();
292
293 $str = 'tag1 tag2 tag3.tag3-2, tag4 , -tag5 ';
294 $bookmark->setTagsString($str);
295 $this->assertEquals(
296 [
297 'tag1',
298 'tag2',
299 'tag3.tag3-2',
300 'tag4',
301 'tag5',
302 ],
303 $bookmark->getTags()
304 );
305 }
306
307 /**
308 * Test setTags() with exotic data
309 */
310 public function testSetTags()
311 {
312 $bookmark = new Bookmark();
313
314 $array = [
315 'tag1 ',
316 ' tag2',
317 'tag3.tag3-2,',
318 ', tag4',
319 ', ',
320 '-tag5 ',
321 ];
322 $bookmark->setTags($array);
323 $this->assertEquals(
324 [
325 'tag1',
326 'tag2',
327 'tag3.tag3-2',
328 'tag4',
329 'tag5',
330 ],
331 $bookmark->getTags()
332 );
333 }
334
335 /**
336 * Test renameTag()
337 */
338 public function testRenameTag()
339 {
340 $bookmark = new Bookmark();
341 $bookmark->setTags(['tag1', 'tag2', 'chair']);
342 $bookmark->renameTag('chair', 'table');
343 $this->assertEquals(['tag1', 'tag2', 'table'], $bookmark->getTags());
344 $bookmark->renameTag('tag1', 'tag42');
345 $this->assertEquals(['tag42', 'tag2', 'table'], $bookmark->getTags());
346 $bookmark->renameTag('tag42', 'tag43');
347 $this->assertEquals(['tag43', 'tag2', 'table'], $bookmark->getTags());
348 $bookmark->renameTag('table', 'desk');
349 $this->assertEquals(['tag43', 'tag2', 'desk'], $bookmark->getTags());
350 }
351
352 /**
353 * Test renameTag() with a tag that is not present in the bookmark
354 */
355 public function testRenameTagNotExists()
356 {
357 $bookmark = new Bookmark();
358 $bookmark->setTags(['tag1', 'tag2', 'chair']);
359 $bookmark->renameTag('nope', 'table');
360 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags());
361 }
362
363 /**
364 * Test deleteTag()
365 */
366 public function testDeleteTag()
367 {
368 $bookmark = new Bookmark();
369 $bookmark->setTags(['tag1', 'tag2', 'chair']);
370 $bookmark->deleteTag('chair');
371 $this->assertEquals(['tag1', 'tag2'], $bookmark->getTags());
372 $bookmark->deleteTag('tag1');
373 $this->assertEquals(['tag2'], $bookmark->getTags());
374 $bookmark->deleteTag('tag2');
375 $this->assertEquals([], $bookmark->getTags());
376 }
377
378 /**
379 * Test deleteTag() with a tag that is not present in the bookmark
380 */
381 public function testDeleteTagNotExists()
382 {
383 $bookmark = new Bookmark();
384 $bookmark->setTags(['tag1', 'tag2', 'chair']);
385 $bookmark->deleteTag('nope');
386 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags());
387 }
388}
diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php
index 78cb8f2a..ef00b92f 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()
@@ -389,15 +400,6 @@ class LinkUtilsTest extends TestCase
389 } 400 }
390 401
391 /** 402 /**
392 * Test count_private.
393 */
394 public function testCountPrivateLinks()
395 {
396 $refDB = new ReferenceLinkDB();
397 $this->assertEquals($refDB->countPrivateLinks(), count_private($refDB->getLinks()));
398 }
399
400 /**
401 * Test text2clickable. 403 * Test text2clickable.
402 */ 404 */
403 public function testText2clickable() 405 public function testText2clickable()
@@ -448,13 +450,13 @@ class LinkUtilsTest extends TestCase
448 カタカナ #カタカナã€ã‚«ã‚¿ã‚«ãƒŠ\n'; 450 カタカナ #カタカナã€ã‚«ã‚¿ã‚«ãƒŠ\n';
449 $autolinkedDescription = hashtag_autolink($rawDescription, $index); 451 $autolinkedDescription = hashtag_autolink($rawDescription, $index);
450 452
451 $this->assertContains($this->getHashtagLink('hashtag', $index), $autolinkedDescription); 453 $this->assertContainsPolyfill($this->getHashtagLink('hashtag', $index), $autolinkedDescription);
452 $this->assertNotContains(' #hashtag', $autolinkedDescription); 454 $this->assertNotContainsPolyfill(' #hashtag', $autolinkedDescription);
453 $this->assertNotContains('>#nothashtag', $autolinkedDescription); 455 $this->assertNotContainsPolyfill('>#nothashtag', $autolinkedDescription);
454 $this->assertContains($this->getHashtagLink('ашок', $index), $autolinkedDescription); 456 $this->assertContainsPolyfill($this->getHashtagLink('ашок', $index), $autolinkedDescription);
455 $this->assertContains($this->getHashtagLink('カタカナ', $index), $autolinkedDescription); 457 $this->assertContainsPolyfill($this->getHashtagLink('カタカナ', $index), $autolinkedDescription);
456 $this->assertContains($this->getHashtagLink('hashtag_hashtag', $index), $autolinkedDescription); 458 $this->assertContainsPolyfill($this->getHashtagLink('hashtag_hashtag', $index), $autolinkedDescription);
457 $this->assertNotContains($this->getHashtagLink('hashtag-nothashtag', $index), $autolinkedDescription); 459 $this->assertNotContainsPolyfill($this->getHashtagLink('hashtag-nothashtag', $index), $autolinkedDescription);
458 } 460 }
459 461
460 /** 462 /**
@@ -465,9 +467,9 @@ class LinkUtilsTest extends TestCase
465 $rawDescription = 'blabla #hashtag x#nothashtag'; 467 $rawDescription = 'blabla #hashtag x#nothashtag';
466 $autolinkedDescription = hashtag_autolink($rawDescription); 468 $autolinkedDescription = hashtag_autolink($rawDescription);
467 469
468 $this->assertContains($this->getHashtagLink('hashtag'), $autolinkedDescription); 470 $this->assertContainsPolyfill($this->getHashtagLink('hashtag'), $autolinkedDescription);
469 $this->assertNotContains(' #hashtag', $autolinkedDescription); 471 $this->assertNotContainsPolyfill(' #hashtag', $autolinkedDescription);
470 $this->assertNotContains('>#nothashtag', $autolinkedDescription); 472 $this->assertNotContainsPolyfill('>#nothashtag', $autolinkedDescription);
471 } 473 }
472 474
473 /** 475 /**
@@ -500,7 +502,7 @@ class LinkUtilsTest extends TestCase
500 */ 502 */
501 private function getHashtagLink($hashtag, $index = '') 503 private function getHashtagLink($hashtag, $index = '')
502 { 504 {
503 $hashtagLink = '<a href="' . $index . '?addtag=$1" title="Hashtag $1">#$1</a>'; 505 $hashtagLink = '<a href="' . $index . './add-tag/$1" title="Hashtag $1">#$1</a>';
504 return str_replace('$1', $hashtag, $hashtagLink); 506 return str_replace('$1', $hashtag, $hashtagLink);
505 } 507 }
506} 508}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index d36d73cd..2d675c9a 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -4,3 +4,29 @@ require_once 'vendor/autoload.php';
4 4
5$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson'); 5$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
6new \Shaarli\Languages('en', $conf); 6new \Shaarli\Languages('en', $conf);
7
8// is_iterable is only compatible with PHP 7.1+
9if (!function_exists('is_iterable')) {
10 function is_iterable($var)
11 {
12 return is_array($var) || $var instanceof \Traversable;
13 }
14}
15
16// TODO: remove this after fixing UT
17require_once 'application/bookmark/LinkUtils.php';
18require_once 'application/Utils.php';
19require_once 'application/http/UrlUtils.php';
20require_once 'application/http/HttpUtils.php';
21require_once 'tests/TestCase.php';
22require_once 'tests/container/ShaarliTestContainer.php';
23require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php';
24require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php';
25require_once 'tests/updater/DummyUpdater.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();
diff --git a/tests/config/ConfigJsonTest.php b/tests/config/ConfigJsonTest.php
index 95ad060b..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 }
@@ -24,7 +24,7 @@ class ConfigJsonTest extends \PHPUnit\Framework\TestCase
24 $conf = $this->configIO->read('tests/utils/config/configJson.json.php'); 24 $conf = $this->configIO->read('tests/utils/config/configJson.json.php');
25 $this->assertEquals('root', $conf['credentials']['login']); 25 $this->assertEquals('root', $conf['credentials']['login']);
26 $this->assertEquals('lala', $conf['redirector']['url']); 26 $this->assertEquals('lala', $conf['redirector']['url']);
27 $this->assertEquals('tests/utils/config/datastore.php', $conf['resource']['datastore']); 27 $this->assertEquals('sandbox/datastore.php', $conf['resource']['datastore']);
28 $this->assertEquals('1', $conf['plugins']['WALLABAG_VERSION']); 28 $this->assertEquals('1', $conf['plugins']['WALLABAG_VERSION']);
29 } 29 }
30 30
@@ -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 67d878ce..7bf9fe64 100644
--- a/tests/config/ConfigPhpTest.php
+++ b/tests/config/ConfigPhpTest.php
@@ -3,15 +3,19 @@ namespace Shaarli\Config;
3 3
4/** 4/**
5 * Class ConfigPhpTest 5 * Class ConfigPhpTest
6 *
7 * We run tests in separate processes due to the usage for $GLOBALS
8 * which are kept between tests.
9 * @runTestsInSeparateProcesses
6 */ 10 */
7class ConfigPhpTest extends \PHPUnit\Framework\TestCase 11class ConfigPhpTest extends \Shaarli\TestCase
8{ 12{
9 /** 13 /**
10 * @var ConfigPhp 14 * @var ConfigPhp
11 */ 15 */
12 protected $configIO; 16 protected $configIO;
13 17
14 public function setUp() 18 protected function setUp(): void
15 { 19 {
16 $this->configIO = new ConfigPhp(); 20 $this->configIO = new ConfigPhp();
17 } 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
new file mode 100644
index 00000000..5d52daef
--- /dev/null
+++ b/tests/container/ContainerBuilderTest.php
@@ -0,0 +1,88 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Container;
6
7use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Feed\FeedBuilder;
10use Shaarli\Formatter\FormatterFactory;
11use Shaarli\Front\Controller\Visitor\ErrorController;
12use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
13use Shaarli\History;
14use Shaarli\Http\HttpAccess;
15use Shaarli\Netscape\NetscapeBookmarkUtils;
16use Shaarli\Plugin\PluginManager;
17use Shaarli\Render\PageBuilder;
18use Shaarli\Render\PageCacheManager;
19use Shaarli\Security\CookieManager;
20use Shaarli\Security\LoginManager;
21use Shaarli\Security\SessionManager;
22use Shaarli\TestCase;
23use Shaarli\Thumbnailer;
24use Shaarli\Updater\Updater;
25use Slim\Http\Environment;
26
27class ContainerBuilderTest extends TestCase
28{
29 /** @var ConfigManager */
30 protected $conf;
31
32 /** @var SessionManager */
33 protected $sessionManager;
34
35 /** @var LoginManager */
36 protected $loginManager;
37
38 /** @var ContainerBuilder */
39 protected $containerBuilder;
40
41 /** @var CookieManager */
42 protected $cookieManager;
43
44 public function setUp(): void
45 {
46 $this->conf = new ConfigManager('tests/utils/config/configJson');
47 $this->sessionManager = $this->createMock(SessionManager::class);
48 $this->cookieManager = $this->createMock(CookieManager::class);
49
50 $this->loginManager = $this->createMock(LoginManager::class);
51 $this->loginManager->method('isLoggedIn')->willReturn(true);
52
53 $this->containerBuilder = new ContainerBuilder(
54 $this->conf,
55 $this->sessionManager,
56 $this->cookieManager,
57 $this->loginManager
58 );
59 }
60
61 public function testBuildContainer(): void
62 {
63 $container = $this->containerBuilder->build();
64
65 static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
66 static::assertInstanceOf(CookieManager::class, $container->cookieManager);
67 static::assertInstanceOf(ConfigManager::class, $container->conf);
68 static::assertInstanceOf(ErrorController::class, $container->errorHandler);
69 static::assertInstanceOf(Environment::class, $container->environment);
70 static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder);
71 static::assertInstanceOf(FormatterFactory::class, $container->formatterFactory);
72 static::assertInstanceOf(History::class, $container->history);
73 static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
74 static::assertInstanceOf(LoginManager::class, $container->loginManager);
75 static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
76 static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
77 static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager);
78 static::assertInstanceOf(ErrorController::class, $container->phpErrorHandler);
79 static::assertInstanceOf(ErrorNotFoundController::class, $container->notFoundHandler);
80 static::assertInstanceOf(PluginManager::class, $container->pluginManager);
81 static::assertInstanceOf(SessionManager::class, $container->sessionManager);
82 static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer);
83 static::assertInstanceOf(Updater::class, $container->updater);
84
85 // Set by the middleware
86 static::assertNull($container->basePath);
87 }
88}
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 0bcc1442..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,9 @@ 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 } 48 }
48 49
49 /** 50 /**
diff --git a/tests/feed/FeedBuilderTest.php b/tests/feed/FeedBuilderTest.php
index b496cb4c..c29e8ef3 100644
--- a/tests/feed/FeedBuilderTest.php
+++ b/tests/feed/FeedBuilderTest.php
@@ -4,14 +4,20 @@ namespace Shaarli\Feed;
4 4
5use DateTime; 5use DateTime;
6use ReferenceLinkDB; 6use ReferenceLinkDB;
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\LinkDB; 9use Shaarli\Bookmark\LinkDB;
10use Shaarli\Config\ConfigManager;
11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\History;
13use Shaarli\TestCase;
8 14
9/** 15/**
10 * FeedBuilderTest class. 16 * FeedBuilderTest class.
11 * 17 *
12 * Unit tests for FeedBuilder. 18 * Unit tests for FeedBuilder.
13 */ 19 */
14class FeedBuilderTest extends \PHPUnit\Framework\TestCase 20class FeedBuilderTest extends TestCase
15{ 21{
16 /** 22 /**
17 * @var string locale Basque (Spain). 23 * @var string locale Basque (Spain).
@@ -30,74 +36,70 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
30 36
31 protected static $testDatastore = 'sandbox/datastore.php'; 37 protected static $testDatastore = 'sandbox/datastore.php';
32 38
33 public static $linkDB; 39 public static $bookmarkService;
40
41 public static $formatter;
34 42
35 public static $serverInfo; 43 public static $serverInfo;
36 44
37 /** 45 /**
38 * Called before every test method. 46 * Called before every test method.
39 */ 47 */
40 public static function setUpBeforeClass() 48 public static function setUpBeforeClass(): void
41 { 49 {
42 $refLinkDB = new ReferenceLinkDB(); 50 $conf = new ConfigManager('tests/utils/config/configJson');
51 $conf->set('resource.datastore', self::$testDatastore);
52 $refLinkDB = new \ReferenceLinkDB();
43 $refLinkDB->write(self::$testDatastore); 53 $refLinkDB->write(self::$testDatastore);
44 self::$linkDB = new LinkDB(self::$testDatastore, true, false); 54 $history = new History('sandbox/history.php');
55 $factory = new FormatterFactory($conf, true);
56 self::$formatter = $factory->getFormatter();
57 self::$bookmarkService = new BookmarkFileService($conf, $history, true);
58
45 self::$serverInfo = array( 59 self::$serverInfo = array(
46 'HTTPS' => 'Off', 60 'HTTPS' => 'Off',
47 'SERVER_NAME' => 'host.tld', 61 'SERVER_NAME' => 'host.tld',
48 'SERVER_PORT' => '80', 62 'SERVER_PORT' => '80',
49 'SCRIPT_NAME' => '/index.php', 63 'SCRIPT_NAME' => '/index.php',
50 'REQUEST_URI' => '/index.php?do=feed', 64 'REQUEST_URI' => '/feed/atom',
51 ); 65 );
52 } 66 }
53 67
54 /** 68 /**
55 * Test GetTypeLanguage().
56 */
57 public function testGetTypeLanguage()
58 {
59 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_ATOM, null, null, false);
60 $feedBuilder->setLocale(self::$LOCALE);
61 $this->assertEquals(self::$ATOM_LANGUAGUE, $feedBuilder->getTypeLanguage());
62 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_RSS, null, null, false);
63 $feedBuilder->setLocale(self::$LOCALE);
64 $this->assertEquals(self::$RSS_LANGUAGE, $feedBuilder->getTypeLanguage());
65 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_ATOM, null, null, false);
66 $this->assertEquals('en', $feedBuilder->getTypeLanguage());
67 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_RSS, null, null, false);
68 $this->assertEquals('en-en', $feedBuilder->getTypeLanguage());
69 }
70
71 /**
72 * Test buildData with RSS feed. 69 * Test buildData with RSS feed.
73 */ 70 */
74 public function testRSSBuildData() 71 public function testRSSBuildData()
75 { 72 {
76 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_RSS, self::$serverInfo, null, false); 73 $feedBuilder = new FeedBuilder(
74 self::$bookmarkService,
75 self::$formatter,
76 static::$serverInfo,
77 false
78 );
77 $feedBuilder->setLocale(self::$LOCALE); 79 $feedBuilder->setLocale(self::$LOCALE);
78 $data = $feedBuilder->buildData(); 80 $data = $feedBuilder->buildData(FeedBuilder::$FEED_RSS, null);
79 // Test headers (RSS) 81 // Test headers (RSS)
80 $this->assertEquals(self::$RSS_LANGUAGE, $data['language']); 82 $this->assertEquals(self::$RSS_LANGUAGE, $data['language']);
81 $this->assertRegExp('/Wed, 03 Aug 2016 09:30:33 \+\d{4}/', $data['last_update']); 83 $this->assertRegExp('/Wed, 03 Aug 2016 09:30:33 \+\d{4}/', $data['last_update']);
82 $this->assertEquals(true, $data['show_dates']); 84 $this->assertEquals(true, $data['show_dates']);
83 $this->assertEquals('http://host.tld/index.php?do=feed', $data['self_link']); 85 $this->assertEquals('http://host.tld/feed/atom', $data['self_link']);
84 $this->assertEquals('http://host.tld/', $data['index_url']); 86 $this->assertEquals('http://host.tld/', $data['index_url']);
85 $this->assertFalse($data['usepermalinks']); 87 $this->assertFalse($data['usepermalinks']);
86 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 88 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
87 89
88 // Test first not pinned link (note link) 90 // Test first not pinned link (note link)
89 $link = $data['links'][array_keys($data['links'])[2]]; 91 $link = $data['links'][array_keys($data['links'])[0]];
90 $this->assertEquals(41, $link['id']); 92 $this->assertEquals(41, $link['id']);
91 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 93 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
92 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']); 94 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['guid']);
93 $this->assertEquals('http://host.tld/?WDWyig', $link['url']); 95 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['url']);
94 $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']); 96 $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']);
95 $pub = DateTime::createFromFormat(DateTime::RSS, $link['pub_iso_date']); 97 $pub = DateTime::createFromFormat(DateTime::RSS, $link['pub_iso_date']);
96 $up = DateTime::createFromFormat(DateTime::ATOM, $link['up_iso_date']); 98 $up = DateTime::createFromFormat(DateTime::ATOM, $link['up_iso_date']);
97 $this->assertEquals($pub, $up); 99 $this->assertEquals($pub, $up);
98 $this->assertContains('Stallman has a beard', $link['description']); 100 $this->assertContainsPolyfill('Stallman has a beard', $link['description']);
99 $this->assertContains('Permalink', $link['description']); 101 $this->assertContainsPolyfill('Permalink', $link['description']);
100 $this->assertContains('http://host.tld/?WDWyig', $link['description']); 102 $this->assertContainsPolyfill('http://host.tld/shaare/WDWyig', $link['description']);
101 $this->assertEquals(1, count($link['taglist'])); 103 $this->assertEquals(1, count($link['taglist']));
102 $this->assertEquals('sTuff', $link['taglist'][0]); 104 $this->assertEquals('sTuff', $link['taglist'][0]);
103 105
@@ -117,12 +119,17 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
117 */ 119 */
118 public function testAtomBuildData() 120 public function testAtomBuildData()
119 { 121 {
120 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false); 122 $feedBuilder = new FeedBuilder(
123 self::$bookmarkService,
124 self::$formatter,
125 static::$serverInfo,
126 false
127 );
121 $feedBuilder->setLocale(self::$LOCALE); 128 $feedBuilder->setLocale(self::$LOCALE);
122 $data = $feedBuilder->buildData(); 129 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
123 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 130 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
124 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']); 131 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']);
125 $link = $data['links'][array_keys($data['links'])[2]]; 132 $link = $data['links'][array_keys($data['links'])[0]];
126 $this->assertRegExp('/2015-03-10T11:46:51\+\d{2}:\d{2}/', $link['pub_iso_date']); 133 $this->assertRegExp('/2015-03-10T11:46:51\+\d{2}:\d{2}/', $link['pub_iso_date']);
127 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links'][8]['up_iso_date']); 134 $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links'][8]['up_iso_date']);
128 } 135 }
@@ -136,13 +143,18 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
136 'searchtags' => 'stuff', 143 'searchtags' => 'stuff',
137 'searchterm' => 'beard', 144 'searchterm' => 'beard',
138 ); 145 );
139 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, $criteria, false); 146 $feedBuilder = new FeedBuilder(
147 self::$bookmarkService,
148 self::$formatter,
149 static::$serverInfo,
150 false
151 );
140 $feedBuilder->setLocale(self::$LOCALE); 152 $feedBuilder->setLocale(self::$LOCALE);
141 $data = $feedBuilder->buildData(); 153 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, $criteria);
142 $this->assertEquals(1, count($data['links'])); 154 $this->assertEquals(1, count($data['links']));
143 $link = array_shift($data['links']); 155 $link = array_shift($data['links']);
144 $this->assertEquals(41, $link['id']); 156 $this->assertEquals(41, $link['id']);
145 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 157 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
146 } 158 }
147 159
148 /** 160 /**
@@ -153,13 +165,18 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
153 $criteria = array( 165 $criteria = array(
154 'nb' => '3', 166 'nb' => '3',
155 ); 167 );
156 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, $criteria, false); 168 $feedBuilder = new FeedBuilder(
169 self::$bookmarkService,
170 self::$formatter,
171 static::$serverInfo,
172 false
173 );
157 $feedBuilder->setLocale(self::$LOCALE); 174 $feedBuilder->setLocale(self::$LOCALE);
158 $data = $feedBuilder->buildData(); 175 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, $criteria);
159 $this->assertEquals(3, count($data['links'])); 176 $this->assertEquals(3, count($data['links']));
160 $link = $data['links'][array_keys($data['links'])[2]]; 177 $link = $data['links'][array_keys($data['links'])[0]];
161 $this->assertEquals(41, $link['id']); 178 $this->assertEquals(41, $link['id']);
162 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 179 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
163 } 180 }
164 181
165 /** 182 /**
@@ -167,28 +184,33 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
167 */ 184 */
168 public function testBuildDataPermalinks() 185 public function testBuildDataPermalinks()
169 { 186 {
170 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false); 187 $feedBuilder = new FeedBuilder(
188 self::$bookmarkService,
189 self::$formatter,
190 static::$serverInfo,
191 false
192 );
171 $feedBuilder->setLocale(self::$LOCALE); 193 $feedBuilder->setLocale(self::$LOCALE);
172 $feedBuilder->setUsePermalinks(true); 194 $feedBuilder->setUsePermalinks(true);
173 $data = $feedBuilder->buildData(); 195 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
174 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 196 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
175 $this->assertTrue($data['usepermalinks']); 197 $this->assertTrue($data['usepermalinks']);
176 // First link is a permalink 198 // First link is a permalink
177 $link = $data['links'][array_keys($data['links'])[2]]; 199 $link = $data['links'][array_keys($data['links'])[0]];
178 $this->assertEquals(41, $link['id']); 200 $this->assertEquals(41, $link['id']);
179 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 201 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
180 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']); 202 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['guid']);
181 $this->assertEquals('http://host.tld/?WDWyig', $link['url']); 203 $this->assertEquals('http://host.tld/shaare/WDWyig', $link['url']);
182 $this->assertContains('Direct link', $link['description']); 204 $this->assertContainsPolyfill('Direct link', $link['description']);
183 $this->assertContains('http://host.tld/?WDWyig', $link['description']); 205 $this->assertContainsPolyfill('http://host.tld/shaare/WDWyig', $link['description']);
184 // Second link is a direct link 206 // Second link is a direct link
185 $link = $data['links'][array_keys($data['links'])[3]]; 207 $link = $data['links'][array_keys($data['links'])[1]];
186 $this->assertEquals(8, $link['id']); 208 $this->assertEquals(8, $link['id']);
187 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'), $link['created']); 209 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'), $link['created']);
188 $this->assertEquals('http://host.tld/?RttfEw', $link['guid']); 210 $this->assertEquals('http://host.tld/shaare/RttfEw', $link['guid']);
189 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']); 211 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
190 $this->assertContains('Direct link', $link['description']); 212 $this->assertContainsPolyfill('Direct link', $link['description']);
191 $this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']); 213 $this->assertContainsPolyfill('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']);
192 } 214 }
193 215
194 /** 216 /**
@@ -196,18 +218,28 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
196 */ 218 */
197 public function testBuildDataHideDates() 219 public function testBuildDataHideDates()
198 { 220 {
199 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false); 221 $feedBuilder = new FeedBuilder(
222 self::$bookmarkService,
223 self::$formatter,
224 static::$serverInfo,
225 false
226 );
200 $feedBuilder->setLocale(self::$LOCALE); 227 $feedBuilder->setLocale(self::$LOCALE);
201 $feedBuilder->setHideDates(true); 228 $feedBuilder->setHideDates(true);
202 $data = $feedBuilder->buildData(); 229 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
203 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 230 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
204 $this->assertFalse($data['show_dates']); 231 $this->assertFalse($data['show_dates']);
205 232
206 // Show dates while logged in 233 // Show dates while logged in
207 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, true); 234 $feedBuilder = new FeedBuilder(
235 self::$bookmarkService,
236 self::$formatter,
237 static::$serverInfo,
238 true
239 );
208 $feedBuilder->setLocale(self::$LOCALE); 240 $feedBuilder->setLocale(self::$LOCALE);
209 $feedBuilder->setHideDates(true); 241 $feedBuilder->setHideDates(true);
210 $data = $feedBuilder->buildData(); 242 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
211 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 243 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
212 $this->assertTrue($data['show_dates']); 244 $this->assertTrue($data['show_dates']);
213 } 245 }
@@ -222,27 +254,26 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
222 'SERVER_NAME' => 'host.tld', 254 'SERVER_NAME' => 'host.tld',
223 'SERVER_PORT' => '8080', 255 'SERVER_PORT' => '8080',
224 'SCRIPT_NAME' => '/~user/shaarli/index.php', 256 'SCRIPT_NAME' => '/~user/shaarli/index.php',
225 'REQUEST_URI' => '/~user/shaarli/index.php?do=feed', 257 'REQUEST_URI' => '/~user/shaarli/feed/atom',
226 ); 258 );
227 $feedBuilder = new FeedBuilder( 259 $feedBuilder = new FeedBuilder(
228 self::$linkDB, 260 self::$bookmarkService,
229 FeedBuilder::$FEED_ATOM, 261 self::$formatter,
230 $serverInfo, 262 $serverInfo,
231 null,
232 false 263 false
233 ); 264 );
234 $feedBuilder->setLocale(self::$LOCALE); 265 $feedBuilder->setLocale(self::$LOCALE);
235 $data = $feedBuilder->buildData(); 266 $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
236 267
237 $this->assertEquals( 268 $this->assertEquals(
238 'http://host.tld:8080/~user/shaarli/index.php?do=feed', 269 'http://host.tld:8080/~user/shaarli/feed/atom',
239 $data['self_link'] 270 $data['self_link']
240 ); 271 );
241 272
242 // Test first link (note link) 273 // Test first link (note link)
243 $link = $data['links'][array_keys($data['links'])[2]]; 274 $link = $data['links'][array_keys($data['links'])[0]];
244 $this->assertEquals('http://host.tld:8080/~user/shaarli/?WDWyig', $link['guid']); 275 $this->assertEquals('http://host.tld:8080/~user/shaarli/shaare/WDWyig', $link['guid']);
245 $this->assertEquals('http://host.tld:8080/~user/shaarli/?WDWyig', $link['url']); 276 $this->assertEquals('http://host.tld:8080/~user/shaarli/shaare/WDWyig', $link['url']);
246 $this->assertContains('http://host.tld:8080/~user/shaarli/?addtag=hashtag', $link['description']); 277 $this->assertContainsPolyfill('http://host.tld:8080/~user/shaarli/./add-tag/hashtag', $link['description']);
247 } 278 }
248} 279}
diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php
new file mode 100644
index 00000000..9534436e
--- /dev/null
+++ b/tests/formatter/BookmarkDefaultFormatterTest.php
@@ -0,0 +1,177 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigManager;
8use Shaarli\TestCase;
9
10/**
11 * Class BookmarkDefaultFormatterTest
12 * @package Shaarli\Formatter
13 */
14class BookmarkDefaultFormatterTest 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 protected 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 BookmarkDefaultFormatter($this->conf, true);
33 }
34
35 /**
36 * Test formatting a bookmark with all its attribute filled.
37 */
38 public function testFormatFull()
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($desc = '<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 '&lt;h2&gt;Content&lt;/h2&gt;&lt;p&gt;`Here is some content&lt;/p&gt;',
64 $link['description']
65 );
66 $tags[3] = '&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;';
67 $this->assertEquals($tags, $link['taglist']);
68 $this->assertEquals(implode(' ', $tags), $link['tags']);
69 $this->assertEquals(
70 'http://domain2.tdl2/?type=img&amp;name=file.png',
71 $link['thumbnail']
72 );
73 $this->assertEquals($created, $link['created']);
74 $this->assertEquals($created->getTimestamp(), $link['timestamp']);
75 $this->assertEquals($updated, $link['updated']);
76 $this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
77 $this->assertTrue($link['private']);
78 $this->assertTrue($link['sticky']);
79 $this->assertEquals('private', $link['class']);
80 }
81
82 /**
83 * Test formatting a bookmark with all its attribute filled.
84 */
85 public function testFormatMinimal()
86 {
87 $bookmark = new Bookmark();
88
89 $link = $this->formatter->format($bookmark);
90 $this->assertEmpty($link['id']);
91 $this->assertEmpty($link['shorturl']);
92 $this->assertEmpty($link['url']);
93 $this->assertEmpty($link['real_url']);
94 $this->assertEmpty($link['title']);
95 $this->assertEmpty($link['description']);
96 $this->assertEmpty($link['taglist']);
97 $this->assertEmpty($link['tags']);
98 $this->assertEmpty($link['thumbnail']);
99 $this->assertEmpty($link['created']);
100 $this->assertEmpty($link['timestamp']);
101 $this->assertEmpty($link['updated']);
102 $this->assertEmpty($link['updated_timestamp']);
103 $this->assertFalse($link['private']);
104 $this->assertFalse($link['sticky']);
105 $this->assertEmpty($link['class']);
106 }
107
108 /**
109 * Make sure that the description is properly formatted by the default formatter.
110 */
111 public function testFormatDescription()
112 {
113 $description = [];
114 $description[] = 'This a <strong>description</strong>' . PHP_EOL;
115 $description[] = 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL;
116 $description[] = 'Also, there is an #hashtag added'. PHP_EOL;
117 $description[] = ' A N D KEEP SPACES ! '. PHP_EOL;
118
119 $bookmark = new Bookmark();
120 $bookmark->setDescription(implode('', $description));
121 $link = $this->formatter->format($bookmark);
122
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';
125 $description[1] = 'text <a href="'. $url .'">'. $url .'</a> more text<br />';
126 $description[2] = 'Also, there is an <a href="./add-tag/hashtag" '.
127 'title="Hashtag hashtag">#hashtag</a> added<br />';
128 $description[3] = '&nbsp; &nbsp; A &nbsp;N &nbsp;D KEEP &nbsp; &nbsp; '.
129 'SPACES &nbsp; &nbsp;! &nbsp; <br />';
130
131 $this->assertEquals(implode(PHP_EOL, $description) . PHP_EOL, $link['description']);
132 }
133
134 /**
135 * Test formatting URL with an index_url set
136 * It should prepend relative links.
137 */
138 public function testFormatNoteWithIndexUrl()
139 {
140 $bookmark = new Bookmark();
141 $bookmark->setUrl($short = '?abcdef');
142 $description = 'Text #hashtag more text';
143 $bookmark->setDescription($description);
144
145 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
146
147 $link = $this->formatter->format($bookmark);
148 $this->assertEquals($root . $short, $link['url']);
149 $this->assertEquals($root . $short, $link['real_url']);
150 $this->assertEquals(
151 'Text <a href="'. $root .'./add-tag/hashtag" title="Hashtag hashtag">'.
152 '#hashtag</a> more text',
153 $link['description']
154 );
155 }
156
157 /**
158 * Make sure that private tags are properly filtered out when the user is logged out.
159 */
160 public function testFormatTagListRemovePrivate(): void
161 {
162 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
163
164 $bookmark = new Bookmark();
165 $bookmark->setId($id = 11);
166 $bookmark->setTags($tags = ['bookmark', '.private', 'othertag']);
167
168 $link = $this->formatter->format($bookmark);
169
170 unset($tags[1]);
171 $tags = array_values($tags);
172
173 $this->assertSame(11, $link['id']);
174 $this->assertSame($tags, $link['taglist']);
175 $this->assertSame(implode(' ', $tags), $link['tags']);
176 }
177}
diff --git a/tests/formatter/BookmarkMarkdownFormatterTest.php b/tests/formatter/BookmarkMarkdownFormatterTest.php
new file mode 100644
index 00000000..ab6b4080
--- /dev/null
+++ b/tests/formatter/BookmarkMarkdownFormatterTest.php
@@ -0,0 +1,160 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigManager;
8use Shaarli\TestCase;
9
10/**
11 * Class BookmarkMarkdownFormatterTest
12 * @package Shaarli\Formatter
13 */
14class BookmarkMarkdownFormatterTest 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 protected 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 BookmarkMarkdownFormatter($this->conf, true);
33 }
34
35 /**
36 * Test formatting a bookmark with all its attribute filled.
37 */
38 public function testFormatFull()
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 testFormatMinimal()
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 testFormatDescription()
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
120 $bookmark = new Bookmark();
121 $bookmark->setDescription($description);
122 $link = $this->formatter->format($bookmark);
123
124 $description = '<div class="markdown"><p>';
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';
127 $description .= 'text <a href="'. $url .'">'. $url .'</a> more text<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 ! ';
130 $description .= '</p></div>';
131
132 $this->assertEquals($description, $link['description']);
133 }
134
135 /**
136 * Test formatting URL with an index_url set
137 * It should prepend relative links.
138 */
139 public function testFormatNoteWithIndexUrl()
140 {
141 $bookmark = new Bookmark();
142 $bookmark->setUrl($short = '?abcdef');
143 $description = 'Text #hashtag more text';
144 $bookmark->setDescription($description);
145
146 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
147
148 $description = '<div class="markdown"><p>';
149 $description .= 'Text <a href="'. $root .'./add-tag/hashtag">#hashtag</a> more text';
150 $description .= '</p></div>';
151
152 $link = $this->formatter->format($bookmark);
153 $this->assertEquals($root . $short, $link['url']);
154 $this->assertEquals($root . $short, $link['real_url']);
155 $this->assertEquals(
156 $description,
157 $link['description']
158 );
159 }
160}
diff --git a/tests/formatter/BookmarkRawFormatterTest.php b/tests/formatter/BookmarkRawFormatterTest.php
new file mode 100644
index 00000000..c76bb7b9
--- /dev/null
+++ b/tests/formatter/BookmarkRawFormatterTest.php
@@ -0,0 +1,97 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigManager;
8use Shaarli\TestCase;
9
10/**
11 * Class BookmarkRawFormatterTest
12 * @package Shaarli\Formatter
13 */
14class BookmarkRawFormatterTest 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 protected 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 BookmarkRawFormatter($this->conf, true);
33 }
34
35 /**
36 * Test formatting a bookmark with all its attribute filled.
37 */
38 public function testFormatFull()
39 {
40 $bookmark = new Bookmark();
41 $bookmark->setId($id = 11);
42 $bookmark->setShortUrl($short = 'abcdef');
43 $bookmark->setUrl($url = 'https://sub.domain.tld?query=here&for=real#hash');
44 $bookmark->setTitle($title = 'This is a <strong>bookmark</strong>');
45 $bookmark->setDescription($desc = '<h2>Content</h2><p>`Here is some content</p>');
46 $bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']);
47 $bookmark->setThumbnail($thumb = 'http://domain2.tdl2/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($url, $link['url']);
57 $this->assertEquals($url, $link['real_url']);
58 $this->assertEquals($title, $link['title']);
59 $this->assertEquals($desc, $link['description']);
60 $this->assertEquals($tags, $link['taglist']);
61 $this->assertEquals(implode(' ', $tags), $link['tags']);
62 $this->assertEquals($thumb, $link['thumbnail']);
63 $this->assertEquals($created, $link['created']);
64 $this->assertEquals($created->getTimestamp(), $link['timestamp']);
65 $this->assertEquals($updated, $link['updated']);
66 $this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
67 $this->assertTrue($link['private']);
68 $this->assertTrue($link['sticky']);
69 $this->assertEquals('private', $link['class']);
70 }
71
72 /**
73 * Test formatting a bookmark with all its attribute filled.
74 */
75 public function testFormatMinimal()
76 {
77 $bookmark = new Bookmark();
78
79 $link = $this->formatter->format($bookmark);
80 $this->assertEmpty($link['id']);
81 $this->assertEmpty($link['shorturl']);
82 $this->assertEmpty($link['url']);
83 $this->assertEmpty($link['real_url']);
84 $this->assertEmpty($link['title']);
85 $this->assertEmpty($link['description']);
86 $this->assertEmpty($link['taglist']);
87 $this->assertEmpty($link['tags']);
88 $this->assertEmpty($link['thumbnail']);
89 $this->assertEmpty($link['created']);
90 $this->assertEmpty($link['timestamp']);
91 $this->assertEmpty($link['updated']);
92 $this->assertEmpty($link['updated_timestamp']);
93 $this->assertFalse($link['private']);
94 $this->assertFalse($link['sticky']);
95 $this->assertEmpty($link['class']);
96 }
97}
diff --git a/tests/formatter/FormatterFactoryTest.php b/tests/formatter/FormatterFactoryTest.php
new file mode 100644
index 00000000..ae476cb5
--- /dev/null
+++ b/tests/formatter/FormatterFactoryTest.php
@@ -0,0 +1,101 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6use Shaarli\TestCase;
7
8/**
9 * Class FormatterFactoryTest
10 *
11 * @package Shaarli\Formatter
12 */
13class FormatterFactoryTest extends TestCase
14{
15 /** @var string Path of test config file */
16 protected static $testConf = 'sandbox/config';
17
18 /** @var FormatterFactory instance */
19 protected $factory;
20
21 /** @var ConfigManager instance */
22 protected $conf;
23
24 /**
25 * Initialize FormatterFactory instance
26 */
27 protected function setUp(): void
28 {
29 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
30 $this->conf = new ConfigManager(self::$testConf);
31 $this->factory = new FormatterFactory($this->conf, true);
32 }
33
34 /**
35 * Test creating an instance of BookmarkFormatter without any setting -> default formatter
36 */
37 public function testCreateInstanceDefault()
38 {
39 $this->assertInstanceOf(BookmarkDefaultFormatter::class, $this->factory->getFormatter());
40 }
41
42 /**
43 * Test creating an instance of BookmarkDefaultFormatter from settings
44 */
45 public function testCreateInstanceDefaultSetting()
46 {
47 $this->conf->set('formatter', 'default');
48 $this->assertInstanceOf(BookmarkDefaultFormatter::class, $this->factory->getFormatter());
49 }
50
51 /**
52 * Test creating an instance of BookmarkDefaultFormatter from parameter
53 */
54 public function testCreateInstanceDefaultParameter()
55 {
56 $this->assertInstanceOf(
57 BookmarkDefaultFormatter::class,
58 $this->factory->getFormatter('default')
59 );
60 }
61
62 /**
63 * Test creating an instance of BookmarkRawFormatter from settings
64 */
65 public function testCreateInstanceRawSetting()
66 {
67 $this->conf->set('formatter', 'raw');
68 $this->assertInstanceOf(BookmarkRawFormatter::class, $this->factory->getFormatter());
69 }
70
71 /**
72 * Test creating an instance of BookmarkRawFormatter from parameter
73 */
74 public function testCreateInstanceRawParameter()
75 {
76 $this->assertInstanceOf(
77 BookmarkRawFormatter::class,
78 $this->factory->getFormatter('raw')
79 );
80 }
81
82 /**
83 * Test creating an instance of BookmarkMarkdownFormatter from settings
84 */
85 public function testCreateInstanceMarkdownSetting()
86 {
87 $this->conf->set('formatter', 'markdown');
88 $this->assertInstanceOf(BookmarkMarkdownFormatter::class, $this->factory->getFormatter());
89 }
90
91 /**
92 * Test creating an instance of BookmarkMarkdownFormatter from parameter
93 */
94 public function testCreateInstanceMarkdownParameter()
95 {
96 $this->assertInstanceOf(
97 BookmarkMarkdownFormatter::class,
98 $this->factory->getFormatter('markdown')
99 );
100 }
101}
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
new file mode 100644
index 00000000..655c5bba
--- /dev/null
+++ b/tests/front/ShaarliMiddlewareTest.php
@@ -0,0 +1,221 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front;
6
7use Shaarli\Config\ConfigManager;
8use Shaarli\Container\ShaarliContainer;
9use Shaarli\Front\Exception\LoginBannedException;
10use Shaarli\Front\Exception\UnauthorizedException;
11use Shaarli\Render\PageBuilder;
12use Shaarli\Render\PageCacheManager;
13use Shaarli\Security\LoginManager;
14use Shaarli\TestCase;
15use Shaarli\Updater\Updater;
16use Slim\Http\Request;
17use Slim\Http\Response;
18use Slim\Http\Uri;
19
20class ShaarliMiddlewareTest extends TestCase
21{
22 protected const TMP_MOCK_FILE = '.tmp';
23
24 /** @var ShaarliContainer */
25 protected $container;
26
27 /** @var ShaarliMiddleware */
28 protected $middleware;
29
30 public function setUp(): void
31 {
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
43 $this->middleware = new ShaarliMiddleware($this->container);
44 }
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 */
54 public function testMiddlewareExecution(): void
55 {
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
64 $response = new Response();
65 $controller = function (Request $request, Response $response): Response {
66 return $response->withStatus(418); // I'm a tea pot
67 };
68
69 /** @var Response $result */
70 $result = $this->middleware->__invoke($request, $response, $controller);
71
72 static::assertInstanceOf(Response::class, $result);
73 static::assertSame(418, $result->getStatusCode());
74 }
75
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
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 (): void {
92 $exception = new LoginBannedException();
93
94 throw new $exception;
95 };
96
97 $pageBuilder = $this->createMock(PageBuilder::class);
98 $pageBuilder->method('render')->willReturnCallback(function (string $message): string {
99 return $message;
100 });
101 $this->container->pageBuilder = $pageBuilder;
102
103 $this->expectException(LoginBannedException::class);
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 ;
214
215 /** @var Response $result */
216 $result = $this->middleware->__invoke($request, $response, $controller);
217
218 static::assertInstanceOf(Response::class, $result);
219 static::assertSame(418, $result->getStatusCode());
220 }
221}
diff --git a/tests/front/controller/admin/ConfigureControllerTest.php b/tests/front/controller/admin/ConfigureControllerTest.php
new file mode 100644
index 00000000..aca6cff3
--- /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'], $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/ManageShaareControllerTest/AddShaareTest.php b/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php
new file mode 100644
index 00000000..0f27ec2f
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/AddShaareTest.php
@@ -0,0 +1,47 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
8use Shaarli\Front\Controller\Admin\ManageShaareController;
9use Shaarli\Http\HttpAccess;
10use Shaarli\TestCase;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class AddShaareTest extends TestCase
15{
16 use FrontAdminControllerMockHelper;
17
18 /** @var ManageShaareController */
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 ManageShaareController($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 $result = $this->controller->addShaare($request, $response);
41
42 static::assertSame(200, $result->getStatusCode());
43 static::assertSame('addlink', (string) $result->getBody());
44
45 static::assertSame('Shaare a new link - Shaarli', $assignedVariables['pagetitle']);
46 }
47}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php
new file mode 100644
index 00000000..096d0774
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/ChangeVisibilityBookmarkTest.php
@@ -0,0 +1,418 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
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\ManageShaareController;
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 ManageShaareController */
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 ManageShaareController($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/ManageShaareControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php
new file mode 100644
index 00000000..ba774e21
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/DeleteBookmarkTest.php
@@ -0,0 +1,376 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
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\ManageShaareController;
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 ManageShaareController */
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 ManageShaareController($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->formatterFactory = $this->createMock(FormatterFactory::class);
360 $this->container->formatterFactory
361 ->expects(static::once())
362 ->method('getFormatter')
363 ->willReturnCallback(function (): BookmarkFormatter {
364 $formatter = $this->createMock(BookmarkFormatter::class);
365 $formatter->method('format')->willReturn(['formatted']);
366
367 return $formatter;
368 })
369 ;
370
371 $result = $this->controller->deleteBookmark($request, $response);
372
373 static::assertSame(200, $result->getStatusCode());
374 static::assertSame('<script>self.close();</script>', (string) $result->getBody('location'));
375 }
376}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php
new file mode 100644
index 00000000..2eb95251
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/DisplayCreateFormTest.php
@@ -0,0 +1,317 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController;
11use Shaarli\Http\HttpAccess;
12use Shaarli\TestCase;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class DisplayCreateFormTest extends TestCase
17{
18 use FrontAdminControllerMockHelper;
19
20 /** @var ManageShaareController */
21 protected $controller;
22
23 public function setUp(): void
24 {
25 $this->createContainer();
26
27 $this->container->httpAccess = $this->createMock(HttpAccess::class);
28 $this->controller = new ManageShaareController($this->container);
29 }
30
31 /**
32 * Test displaying bookmark create form
33 * Ensure that every step of the standard workflow works properly.
34 */
35 public function testDisplayCreateFormWithUrl(): void
36 {
37 $this->container->environment = [
38 'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
39 ];
40
41 $assignedVariables = [];
42 $this->assignTemplateVars($assignedVariables);
43
44 $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
45 $expectedUrl = str_replace('&utm_ad=pay', '', $url);
46 $remoteTitle = 'Remote Title';
47 $remoteDesc = 'Sometimes the meta description is relevant.';
48 $remoteTags = 'abc def';
49
50 $request = $this->createMock(Request::class);
51 $request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
52 return $key === 'post' ? $url : null;
53 });
54 $response = new Response();
55
56 $this->container->httpAccess
57 ->expects(static::once())
58 ->method('getCurlDownloadCallback')
59 ->willReturnCallback(
60 function (&$charset, &$title, &$description, &$tags) use (
61 $remoteTitle,
62 $remoteDesc,
63 $remoteTags
64 ): callable {
65 return function () use (
66 &$charset,
67 &$title,
68 &$description,
69 &$tags,
70 $remoteTitle,
71 $remoteDesc,
72 $remoteTags
73 ): void {
74 $charset = 'ISO-8859-1';
75 $title = $remoteTitle;
76 $description = $remoteDesc;
77 $tags = $remoteTags;
78 };
79 }
80 )
81 ;
82 $this->container->httpAccess
83 ->expects(static::once())
84 ->method('getHttpResponse')
85 ->with($expectedUrl, 30, 4194304)
86 ->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
87 $callback();
88 })
89 ;
90
91 $this->container->bookmarkService
92 ->expects(static::once())
93 ->method('bookmarksCountPerTag')
94 ->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
95 ;
96
97 // Make sure that PluginManager hook is triggered
98 $this->container->pluginManager
99 ->expects(static::atLeastOnce())
100 ->method('executeHooks')
101 ->withConsecutive(['render_editlink'], ['render_includes'])
102 ->willReturnCallback(function (string $hook, array $data) use ($remoteTitle, $remoteDesc): array {
103 if ('render_editlink' === $hook) {
104 static::assertSame($remoteTitle, $data['link']['title']);
105 static::assertSame($remoteDesc, $data['link']['description']);
106 }
107
108 return $data;
109 })
110 ;
111
112 $result = $this->controller->displayCreateForm($request, $response);
113
114 static::assertSame(200, $result->getStatusCode());
115 static::assertSame('editlink', (string) $result->getBody());
116
117 static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
118
119 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
120 static::assertSame($remoteTitle, $assignedVariables['link']['title']);
121 static::assertSame($remoteDesc, $assignedVariables['link']['description']);
122 static::assertSame($remoteTags, $assignedVariables['link']['tags']);
123 static::assertFalse($assignedVariables['link']['private']);
124
125 static::assertTrue($assignedVariables['link_is_new']);
126 static::assertSame($referer, $assignedVariables['http_referer']);
127 static::assertSame($tags, $assignedVariables['tags']);
128 static::assertArrayHasKey('source', $assignedVariables);
129 static::assertArrayHasKey('default_private_links', $assignedVariables);
130 }
131
132 /**
133 * Test displaying bookmark create form
134 * Ensure all available query parameters are handled properly.
135 */
136 public function testDisplayCreateFormWithFullParameters(): void
137 {
138 $assignedVariables = [];
139 $this->assignTemplateVars($assignedVariables);
140
141 $parameters = [
142 'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
143 'title' => 'Provided Title',
144 'description' => 'Provided description.',
145 'tags' => 'abc def',
146 'private' => '1',
147 'source' => 'apps',
148 ];
149 $expectedUrl = str_replace('&utm_ad=pay', '', $parameters['post']);
150
151 $request = $this->createMock(Request::class);
152 $request
153 ->method('getParam')
154 ->willReturnCallback(function (string $key) use ($parameters): ?string {
155 return $parameters[$key] ?? null;
156 });
157 $response = new Response();
158
159 $result = $this->controller->displayCreateForm($request, $response);
160
161 static::assertSame(200, $result->getStatusCode());
162 static::assertSame('editlink', (string) $result->getBody());
163
164 static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
165
166 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
167 static::assertSame($parameters['title'], $assignedVariables['link']['title']);
168 static::assertSame($parameters['description'], $assignedVariables['link']['description']);
169 static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
170 static::assertTrue($assignedVariables['link']['private']);
171 static::assertTrue($assignedVariables['link_is_new']);
172 static::assertSame($parameters['source'], $assignedVariables['source']);
173 }
174
175 /**
176 * Test displaying bookmark create form
177 * Without any parameter.
178 */
179 public function testDisplayCreateFormEmpty(): void
180 {
181 $assignedVariables = [];
182 $this->assignTemplateVars($assignedVariables);
183
184 $request = $this->createMock(Request::class);
185 $response = new Response();
186
187 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
188 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
189
190 $result = $this->controller->displayCreateForm($request, $response);
191
192 static::assertSame(200, $result->getStatusCode());
193 static::assertSame('editlink', (string) $result->getBody());
194 static::assertSame('', $assignedVariables['link']['url']);
195 static::assertSame('Note: ', $assignedVariables['link']['title']);
196 static::assertSame('', $assignedVariables['link']['description']);
197 static::assertSame('', $assignedVariables['link']['tags']);
198 static::assertFalse($assignedVariables['link']['private']);
199 static::assertTrue($assignedVariables['link_is_new']);
200 }
201
202 /**
203 * Test displaying bookmark create form
204 * URL not using HTTP protocol: do not try to retrieve the title
205 */
206 public function testDisplayCreateFormNotHttp(): void
207 {
208 $assignedVariables = [];
209 $this->assignTemplateVars($assignedVariables);
210
211 $url = 'magnet://kubuntu.torrent';
212 $request = $this->createMock(Request::class);
213 $request
214 ->method('getParam')
215 ->willReturnCallback(function (string $key) use ($url): ?string {
216 return $key === 'post' ? $url : null;
217 });
218 $response = new Response();
219
220 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
221 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
222
223 $result = $this->controller->displayCreateForm($request, $response);
224
225 static::assertSame(200, $result->getStatusCode());
226 static::assertSame('editlink', (string) $result->getBody());
227 static::assertSame($url, $assignedVariables['link']['url']);
228 static::assertTrue($assignedVariables['link_is_new']);
229 }
230
231 /**
232 * Test displaying bookmark create form
233 * When markdown formatter is enabled, the no markdown tag should be added to existing tags.
234 */
235 public function testDisplayCreateFormWithMarkdownEnabled(): void
236 {
237 $assignedVariables = [];
238 $this->assignTemplateVars($assignedVariables);
239
240 $this->container->conf = $this->createMock(ConfigManager::class);
241 $this->container->conf
242 ->expects(static::atLeastOnce())
243 ->method('get')->willReturnCallback(function (string $key): ?string {
244 if ($key === 'formatter') {
245 return 'markdown';
246 }
247
248 return $key;
249 })
250 ;
251
252 $request = $this->createMock(Request::class);
253 $response = new Response();
254
255 $result = $this->controller->displayCreateForm($request, $response);
256
257 static::assertSame(200, $result->getStatusCode());
258 static::assertSame('editlink', (string) $result->getBody());
259 static::assertSame(['nomarkdown' => 1], $assignedVariables['tags']);
260 }
261
262 /**
263 * Test displaying bookmark create form
264 * When an existing URL is submitted, we want to edit the existing link.
265 */
266 public function testDisplayCreateFormWithExistingUrl(): void
267 {
268 $assignedVariables = [];
269 $this->assignTemplateVars($assignedVariables);
270
271 $url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
272 $expectedUrl = str_replace('&utm_ad=pay', '', $url);
273
274 $request = $this->createMock(Request::class);
275 $request
276 ->method('getParam')
277 ->willReturnCallback(function (string $key) use ($url): ?string {
278 return $key === 'post' ? $url : null;
279 });
280 $response = new Response();
281
282 $this->container->httpAccess->expects(static::never())->method('getHttpResponse');
283 $this->container->httpAccess->expects(static::never())->method('getCurlDownloadCallback');
284
285 $this->container->bookmarkService
286 ->expects(static::once())
287 ->method('findByUrl')
288 ->with($expectedUrl)
289 ->willReturn(
290 (new Bookmark())
291 ->setId($id = 23)
292 ->setUrl($expectedUrl)
293 ->setTitle($title = 'Bookmark Title')
294 ->setDescription($description = 'Bookmark description.')
295 ->setTags($tags = ['abc', 'def'])
296 ->setPrivate(true)
297 ->setCreated($createdAt = new \DateTime('2020-06-10 18:45:44'))
298 )
299 ;
300
301 $result = $this->controller->displayCreateForm($request, $response);
302
303 static::assertSame(200, $result->getStatusCode());
304 static::assertSame('editlink', (string) $result->getBody());
305
306 static::assertSame('Edit Shaare - Shaarli', $assignedVariables['pagetitle']);
307 static::assertFalse($assignedVariables['link_is_new']);
308
309 static::assertSame($id, $assignedVariables['link']['id']);
310 static::assertSame($expectedUrl, $assignedVariables['link']['url']);
311 static::assertSame($title, $assignedVariables['link']['title']);
312 static::assertSame($description, $assignedVariables['link']['description']);
313 static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
314 static::assertTrue($assignedVariables['link']['private']);
315 static::assertSame($createdAt, $assignedVariables['link']['created']);
316 }
317}
diff --git a/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php b/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php
new file mode 100644
index 00000000..2dc3f41c
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/DisplayEditFormTest.php
@@ -0,0 +1,155 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController;
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 ManageShaareController */
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 ManageShaareController($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/ManageShaareControllerTest/PinBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php
new file mode 100644
index 00000000..50ce7df1
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/PinBookmarkTest.php
@@ -0,0 +1,145 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController;
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 ManageShaareController */
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 ManageShaareController($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/ManageShaareControllerTest/SaveBookmarkTest.php b/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
new file mode 100644
index 00000000..f7a68226
--- /dev/null
+++ b/tests/front/controller/admin/ManageShaareControllerTest/SaveBookmarkTest.php
@@ -0,0 +1,308 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin\ManageShaareControllerTest;
6
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
10use Shaarli\Front\Controller\Admin\ManageShaareController;
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 ManageShaareController */
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 ManageShaareController($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): void {
70 static::assertFalse($save);
71
72 $checkBookmark($bookmark);
73
74 $bookmark->setId($id);
75 })
76 ;
77 $this->container->bookmarkService
78 ->expects(static::once())
79 ->method('set')
80 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
81 static::assertTrue($save);
82
83 $checkBookmark($bookmark);
84
85 static::assertSame($id, $bookmark->getId());
86 })
87 ;
88
89 // Make sure that PluginManager hook is triggered
90 $this->container->pluginManager
91 ->expects(static::atLeastOnce())
92 ->method('executeHooks')
93 ->withConsecutive(['save_link'])
94 ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
95 if ('save_link' === $hook) {
96 static::assertSame($id, $data['id']);
97 static::assertSame($parameters['lf_url'], $data['url']);
98 static::assertSame($parameters['lf_title'], $data['title']);
99 static::assertSame($parameters['lf_description'], $data['description']);
100 static::assertSame($parameters['lf_tags'], $data['tags']);
101 static::assertTrue($data['private']);
102 }
103
104 return $data;
105 })
106 ;
107
108 $result = $this->controller->save($request, $response);
109
110 static::assertSame(302, $result->getStatusCode());
111 static::assertRegExp('@/subfolder/#[\w\-]{6}@', $result->getHeader('location')[0]);
112 }
113
114
115 /**
116 * Test save an existing bookmark
117 */
118 public function testSaveExistingBookmark(): void
119 {
120 $id = 21;
121 $parameters = [
122 'lf_id' => (string) $id,
123 'lf_url' => 'http://url.tld/other?part=3#hash',
124 'lf_title' => 'Provided Title',
125 'lf_description' => 'Provided description.',
126 'lf_tags' => 'abc def',
127 'lf_private' => '1',
128 'returnurl' => 'http://shaarli/subfolder/?page=2'
129 ];
130
131 $request = $this->createMock(Request::class);
132 $request
133 ->method('getParam')
134 ->willReturnCallback(function (string $key) use ($parameters): ?string {
135 return $parameters[$key] ?? null;
136 })
137 ;
138 $response = new Response();
139
140 $checkBookmark = function (Bookmark $bookmark) use ($parameters, $id) {
141 static::assertSame($id, $bookmark->getId());
142 static::assertSame($parameters['lf_url'], $bookmark->getUrl());
143 static::assertSame($parameters['lf_title'], $bookmark->getTitle());
144 static::assertSame($parameters['lf_description'], $bookmark->getDescription());
145 static::assertSame($parameters['lf_tags'], $bookmark->getTagsString());
146 static::assertTrue($bookmark->isPrivate());
147 };
148
149 $this->container->bookmarkService->expects(static::atLeastOnce())->method('exists')->willReturn(true);
150 $this->container->bookmarkService
151 ->expects(static::once())
152 ->method('get')
153 ->willReturn((new Bookmark())->setId($id)->setUrl('http://other.url'))
154 ;
155 $this->container->bookmarkService
156 ->expects(static::once())
157 ->method('addOrSet')
158 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
159 static::assertFalse($save);
160
161 $checkBookmark($bookmark);
162 })
163 ;
164 $this->container->bookmarkService
165 ->expects(static::once())
166 ->method('set')
167 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($checkBookmark, $id): void {
168 static::assertTrue($save);
169
170 $checkBookmark($bookmark);
171
172 static::assertSame($id, $bookmark->getId());
173 })
174 ;
175
176 // Make sure that PluginManager hook is triggered
177 $this->container->pluginManager
178 ->expects(static::atLeastOnce())
179 ->method('executeHooks')
180 ->withConsecutive(['save_link'])
181 ->willReturnCallback(function (string $hook, array $data) use ($parameters, $id): array {
182 if ('save_link' === $hook) {
183 static::assertSame($id, $data['id']);
184 static::assertSame($parameters['lf_url'], $data['url']);
185 static::assertSame($parameters['lf_title'], $data['title']);
186 static::assertSame($parameters['lf_description'], $data['description']);
187 static::assertSame($parameters['lf_tags'], $data['tags']);
188 static::assertTrue($data['private']);
189 }
190
191 return $data;
192 })
193 ;
194
195 $result = $this->controller->save($request, $response);
196
197 static::assertSame(302, $result->getStatusCode());
198 static::assertRegExp('@/subfolder/\?page=2#[\w\-]{6}@', $result->getHeader('location')[0]);
199 }
200
201 /**
202 * Test save a bookmark - try to retrieve the thumbnail
203 */
204 public function testSaveBookmarkWithThumbnail(): void
205 {
206 $parameters = ['lf_url' => 'http://url.tld/other?part=3#hash'];
207
208 $request = $this->createMock(Request::class);
209 $request
210 ->method('getParam')
211 ->willReturnCallback(function (string $key) use ($parameters): ?string {
212 return $parameters[$key] ?? null;
213 })
214 ;
215 $response = new Response();
216
217 $this->container->conf = $this->createMock(ConfigManager::class);
218 $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
219 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
220 });
221
222 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
223 $this->container->thumbnailer
224 ->expects(static::once())
225 ->method('get')
226 ->with($parameters['lf_url'])
227 ->willReturn($thumb = 'http://thumb.url')
228 ;
229
230 $this->container->bookmarkService
231 ->expects(static::once())
232 ->method('addOrSet')
233 ->willReturnCallback(function (Bookmark $bookmark, bool $save) use ($thumb): void {
234 static::assertSame($thumb, $bookmark->getThumbnail());
235 })
236 ;
237
238 $result = $this->controller->save($request, $response);
239
240 static::assertSame(302, $result->getStatusCode());
241 }
242
243 /**
244 * Test save a bookmark - with ID #0
245 */
246 public function testSaveBookmarkWithIdZero(): void
247 {
248 $parameters = ['lf_id' => '0'];
249
250 $request = $this->createMock(Request::class);
251 $request
252 ->method('getParam')
253 ->willReturnCallback(function (string $key) use ($parameters): ?string {
254 return $parameters[$key] ?? null;
255 })
256 ;
257 $response = new Response();
258
259 $this->container->bookmarkService->expects(static::once())->method('exists')->with(0)->willReturn(true);
260 $this->container->bookmarkService->expects(static::once())->method('get')->with(0)->willReturn(new Bookmark());
261
262 $result = $this->controller->save($request, $response);
263
264 static::assertSame(302, $result->getStatusCode());
265 }
266
267 /**
268 * Change the password with a wrong existing password
269 */
270 public function testSaveBookmarkFromBookmarklet(): void
271 {
272 $parameters = ['source' => 'bookmarklet'];
273
274 $request = $this->createMock(Request::class);
275 $request
276 ->method('getParam')
277 ->willReturnCallback(function (string $key) use ($parameters): ?string {
278 return $parameters[$key] ?? null;
279 })
280 ;
281 $response = new Response();
282
283 $result = $this->controller->save($request, $response);
284
285 static::assertSame(200, $result->getStatusCode());
286 static::assertSame('<script>self.close();</script>', (string) $result->getBody());
287 }
288
289 /**
290 * Change the password with a wrong existing password
291 */
292 public function testSaveBookmarkWrongToken(): void
293 {
294 $this->container->sessionManager = $this->createMock(SessionManager::class);
295 $this->container->sessionManager->method('checkToken')->willReturn(false);
296
297 $this->container->bookmarkService->expects(static::never())->method('addOrSet');
298 $this->container->bookmarkService->expects(static::never())->method('set');
299
300 $request = $this->createMock(Request::class);
301 $response = new Response();
302
303 $this->expectException(WrongTokenException::class);
304
305 $this->controller->save($request, $response);
306 }
307
308}
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/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/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..f4a8acff
--- /dev/null
+++ b/tests/front/controller/admin/ThumbnailsControllerTest.php
@@ -0,0 +1,154 @@
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) {
93 static::assertSame($thumb, $bookmark->getThumbnail());
94 })
95 ;
96
97 $result = $this->controller->ajaxUpdate($request, $response, ['id' => (string) $id]);
98
99 static::assertSame(200, $result->getStatusCode());
100
101 $payload = json_decode((string) $result->getBody(), true);
102
103 static::assertSame($id, $payload['id']);
104 static::assertSame($url, $payload['url']);
105 static::assertSame($thumb, $payload['thumbnail']);
106 }
107
108 /**
109 * Test updating a bookmark thumbnail - Invalid ID
110 */
111 public function testAjaxUpdateInvalidId(): void
112 {
113 $request = $this->createMock(Request::class);
114 $response = new Response();
115
116 $result = $this->controller->ajaxUpdate($request, $response, ['id' => 'nope']);
117
118 static::assertSame(400, $result->getStatusCode());
119 }
120
121 /**
122 * Test updating a bookmark thumbnail - No ID
123 */
124 public function testAjaxUpdateNoId(): void
125 {
126 $request = $this->createMock(Request::class);
127 $response = new Response();
128
129 $result = $this->controller->ajaxUpdate($request, $response, []);
130
131 static::assertSame(400, $result->getStatusCode());
132 }
133
134 /**
135 * Test updating a bookmark thumbnail with valid parameters
136 */
137 public function testAjaxUpdateBookmarkNotFound(): void
138 {
139 $id = 123;
140 $request = $this->createMock(Request::class);
141 $response = new Response();
142
143 $this->container->bookmarkService
144 ->expects(static::once())
145 ->method('get')
146 ->with($id)
147 ->willThrowException(new BookmarkNotFoundException())
148 ;
149
150 $result = $this->controller->ajaxUpdate($request, $response, ['id' => (string) $id]);
151
152 static::assertSame(404, $result->getStatusCode());
153 }
154}
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..0c95df97
--- /dev/null
+++ b/tests/front/controller/visitor/BookmarkListControllerTest.php
@@ -0,0 +1,448 @@
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 getting link list with thumbnail updates.
296 * -> 2 thumbnails update, only 1 datastore write
297 */
298 public function testThumbnailUpdateFromLinkList(): void
299 {
300 $request = $this->createMock(Request::class);
301 $response = new Response();
302
303 $this->container->loginManager = $this->createMock(LoginManager::class);
304 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
305
306 $this->container->conf = $this->createMock(ConfigManager::class);
307 $this->container->conf
308 ->method('get')
309 ->willReturnCallback(function (string $key, $default) {
310 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
311 })
312 ;
313
314 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
315 $this->container->thumbnailer
316 ->expects(static::exactly(2))
317 ->method('get')
318 ->withConsecutive(['https://url2.tld'], ['https://url4.tld'])
319 ;
320
321 $this->container->bookmarkService
322 ->expects(static::once())
323 ->method('search')
324 ->willReturn([
325 (new Bookmark())->setId(1)->setUrl('https://url1.tld')->setTitle('Title 1')->setThumbnail(false),
326 $b1 = (new Bookmark())->setId(2)->setUrl('https://url2.tld')->setTitle('Title 2'),
327 (new Bookmark())->setId(3)->setUrl('https://url3.tld')->setTitle('Title 3')->setThumbnail(false),
328 $b2 = (new Bookmark())->setId(2)->setUrl('https://url4.tld')->setTitle('Title 4'),
329 (new Bookmark())->setId(2)->setUrl('ftp://url5.tld', ['ftp'])->setTitle('Title 5'),
330 ])
331 ;
332 $this->container->bookmarkService
333 ->expects(static::exactly(2))
334 ->method('set')
335 ->withConsecutive([$b1, false], [$b2, false])
336 ;
337 $this->container->bookmarkService->expects(static::once())->method('save');
338
339 $result = $this->controller->index($request, $response);
340
341 static::assertSame(200, $result->getStatusCode());
342 static::assertSame('linklist', (string) $result->getBody());
343 }
344
345 /**
346 * Test getting a permalink with thumbnail update.
347 */
348 public function testThumbnailUpdateFromPermalink(): void
349 {
350 $request = $this->createMock(Request::class);
351 $response = new Response();
352
353 $this->container->loginManager = $this->createMock(LoginManager::class);
354 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
355
356 $this->container->conf = $this->createMock(ConfigManager::class);
357 $this->container->conf
358 ->method('get')
359 ->willReturnCallback(function (string $key, $default) {
360 return $key === 'thumbnails.mode' ? Thumbnailer::MODE_ALL : $default;
361 })
362 ;
363
364 $this->container->thumbnailer = $this->createMock(Thumbnailer::class);
365 $this->container->thumbnailer->expects(static::once())->method('get')->withConsecutive(['https://url.tld']);
366
367 $this->container->bookmarkService
368 ->expects(static::once())
369 ->method('findByHash')
370 ->willReturn($bookmark = (new Bookmark())->setId(2)->setUrl('https://url.tld')->setTitle('Title 1'))
371 ;
372 $this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, true);
373 $this->container->bookmarkService->expects(static::never())->method('save');
374
375 $result = $this->controller->permalink($request, $response, ['hash' => 'abc']);
376
377 static::assertSame(200, $result->getStatusCode());
378 static::assertSame('linklist', (string) $result->getBody());
379 }
380
381 /**
382 * Trigger legacy controller in link list controller: permalink
383 */
384 public function testLegacyControllerPermalink(): void
385 {
386 $hash = 'abcdef';
387 $this->container->environment['QUERY_STRING'] = $hash;
388
389 $request = $this->createMock(Request::class);
390 $response = new Response();
391
392 $result = $this->controller->index($request, $response);
393
394 static::assertSame(302, $result->getStatusCode());
395 static::assertSame('/subfolder/shaare/' . $hash, $result->getHeader('location')[0]);
396 }
397
398 /**
399 * Trigger legacy controller in link list controller: ?do= query parameter
400 */
401 public function testLegacyControllerDoPage(): void
402 {
403 $request = $this->createMock(Request::class);
404 $request->method('getQueryParam')->with('do')->willReturn('picwall');
405 $response = new Response();
406
407 $result = $this->controller->index($request, $response);
408
409 static::assertSame(302, $result->getStatusCode());
410 static::assertSame('/subfolder/picture-wall', $result->getHeader('location')[0]);
411 }
412
413 /**
414 * Trigger legacy controller in link list controller: ?do= query parameter with unknown legacy route
415 */
416 public function testLegacyControllerUnknownDoPage(): void
417 {
418 $request = $this->createMock(Request::class);
419 $request->method('getQueryParam')->with('do')->willReturn('nope');
420 $response = new Response();
421
422 $result = $this->controller->index($request, $response);
423
424 static::assertSame(200, $result->getStatusCode());
425 static::assertSame('linklist', (string) $result->getBody());
426 }
427
428 /**
429 * Trigger legacy controller in link list controller: other GET route (e.g. ?post)
430 */
431 public function testLegacyControllerGetParameter(): void
432 {
433 $request = $this->createMock(Request::class);
434 $request->method('getQueryParams')->willReturn(['post' => $url = 'http://url.tld']);
435 $response = new Response();
436
437 $this->container->loginManager = $this->createMock(LoginManager::class);
438 $this->container->loginManager->method('isLoggedIn')->willReturn(true);
439
440 $result = $this->controller->index($request, $response);
441
442 static::assertSame(302, $result->getStatusCode());
443 static::assertSame(
444 '/subfolder/admin/shaare?post=' . urlencode($url),
445 $result->getHeader('location')[0]
446 );
447 }
448}
diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php
new file mode 100644
index 00000000..fc78bc13
--- /dev/null
+++ b/tests/front/controller/visitor/DailyControllerTest.php
@@ -0,0 +1,478 @@
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
32 $request = $this->createMock(Request::class);
33 $request->method('getQueryParam')->willReturn($currentDay->format('Ymd'));
34 $response = new Response();
35
36 // Save RainTPL assigned variables
37 $assignedVariables = [];
38 $this->assignTemplateVars($assignedVariables);
39
40 // Links dataset: 2 links with thumbnails
41 $this->container->bookmarkService
42 ->expects(static::once())
43 ->method('days')
44 ->willReturnCallback(function () use ($currentDay): array {
45 return [
46 '20200510',
47 $currentDay->format('Ymd'),
48 '20200516',
49 ];
50 })
51 ;
52 $this->container->bookmarkService
53 ->expects(static::once())
54 ->method('filterDay')
55 ->willReturnCallback(function (): array {
56 return [
57 (new Bookmark())
58 ->setId(1)
59 ->setUrl('http://url.tld')
60 ->setTitle(static::generateString(50))
61 ->setDescription(static::generateString(500))
62 ,
63 (new Bookmark())
64 ->setId(2)
65 ->setUrl('http://url2.tld')
66 ->setTitle(static::generateString(50))
67 ->setDescription(static::generateString(500))
68 ,
69 (new Bookmark())
70 ->setId(3)
71 ->setUrl('http://url3.tld')
72 ->setTitle(static::generateString(50))
73 ->setDescription(static::generateString(500))
74 ,
75 ];
76 })
77 ;
78
79 // Make sure that PluginManager hook is triggered
80 $this->container->pluginManager
81 ->expects(static::atLeastOnce())
82 ->method('executeHooks')
83 ->withConsecutive(['render_daily'])
84 ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array {
85 if ('render_daily' === $hook) {
86 static::assertArrayHasKey('linksToDisplay', $data);
87 static::assertCount(3, $data['linksToDisplay']);
88 static::assertSame(1, $data['linksToDisplay'][0]['id']);
89 static::assertSame($currentDay->getTimestamp(), $data['day']);
90 static::assertSame('20200510', $data['previousday']);
91 static::assertSame('20200516', $data['nextday']);
92
93 static::assertArrayHasKey('loggedin', $param);
94 }
95
96 return $data;
97 })
98 ;
99
100 $result = $this->controller->index($request, $response);
101
102 static::assertSame(200, $result->getStatusCode());
103 static::assertSame('daily', (string) $result->getBody());
104 static::assertSame(
105 'Daily - '. format_date($currentDay, false, true) .' - Shaarli',
106 $assignedVariables['pagetitle']
107 );
108 static::assertEquals($currentDay, $assignedVariables['dayDate']);
109 static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']);
110 static::assertCount(3, $assignedVariables['linksToDisplay']);
111
112 $link = $assignedVariables['linksToDisplay'][0];
113
114 static::assertSame(1, $link['id']);
115 static::assertSame('http://url.tld', $link['url']);
116 static::assertNotEmpty($link['title']);
117 static::assertNotEmpty($link['description']);
118 static::assertNotEmpty($link['formatedDescription']);
119
120 $link = $assignedVariables['linksToDisplay'][1];
121
122 static::assertSame(2, $link['id']);
123 static::assertSame('http://url2.tld', $link['url']);
124 static::assertNotEmpty($link['title']);
125 static::assertNotEmpty($link['description']);
126 static::assertNotEmpty($link['formatedDescription']);
127
128 $link = $assignedVariables['linksToDisplay'][2];
129
130 static::assertSame(3, $link['id']);
131 static::assertSame('http://url3.tld', $link['url']);
132 static::assertNotEmpty($link['title']);
133 static::assertNotEmpty($link['description']);
134 static::assertNotEmpty($link['formatedDescription']);
135
136 static::assertCount(3, $assignedVariables['cols']);
137 static::assertCount(1, $assignedVariables['cols'][0]);
138 static::assertCount(1, $assignedVariables['cols'][1]);
139 static::assertCount(1, $assignedVariables['cols'][2]);
140
141 $link = $assignedVariables['cols'][0][0];
142
143 static::assertSame(1, $link['id']);
144 static::assertSame('http://url.tld', $link['url']);
145 static::assertNotEmpty($link['title']);
146 static::assertNotEmpty($link['description']);
147 static::assertNotEmpty($link['formatedDescription']);
148
149 $link = $assignedVariables['cols'][1][0];
150
151 static::assertSame(2, $link['id']);
152 static::assertSame('http://url2.tld', $link['url']);
153 static::assertNotEmpty($link['title']);
154 static::assertNotEmpty($link['description']);
155 static::assertNotEmpty($link['formatedDescription']);
156
157 $link = $assignedVariables['cols'][2][0];
158
159 static::assertSame(3, $link['id']);
160 static::assertSame('http://url3.tld', $link['url']);
161 static::assertNotEmpty($link['title']);
162 static::assertNotEmpty($link['description']);
163 static::assertNotEmpty($link['formatedDescription']);
164 }
165
166 /**
167 * Daily page - test that everything goes fine with no future or past bookmarks
168 */
169 public function testValidIndexControllerInvokeNoFutureOrPast(): void
170 {
171 $currentDay = new \DateTimeImmutable('2020-05-13');
172
173 $request = $this->createMock(Request::class);
174 $response = new Response();
175
176 // Save RainTPL assigned variables
177 $assignedVariables = [];
178 $this->assignTemplateVars($assignedVariables);
179
180 // Links dataset: 2 links with thumbnails
181 $this->container->bookmarkService
182 ->expects(static::once())
183 ->method('days')
184 ->willReturnCallback(function () use ($currentDay): array {
185 return [
186 $currentDay->format($currentDay->format('Ymd')),
187 ];
188 })
189 ;
190 $this->container->bookmarkService
191 ->expects(static::once())
192 ->method('filterDay')
193 ->willReturnCallback(function (): array {
194 return [
195 (new Bookmark())
196 ->setId(1)
197 ->setUrl('http://url.tld')
198 ->setTitle(static::generateString(50))
199 ->setDescription(static::generateString(500))
200 ,
201 ];
202 })
203 ;
204
205 // Make sure that PluginManager hook is triggered
206 $this->container->pluginManager
207 ->expects(static::atLeastOnce())
208 ->method('executeHooks')
209 ->withConsecutive(['render_daily'])
210 ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array {
211 if ('render_daily' === $hook) {
212 static::assertArrayHasKey('linksToDisplay', $data);
213 static::assertCount(1, $data['linksToDisplay']);
214 static::assertSame(1, $data['linksToDisplay'][0]['id']);
215 static::assertSame($currentDay->getTimestamp(), $data['day']);
216 static::assertEmpty($data['previousday']);
217 static::assertEmpty($data['nextday']);
218
219 static::assertArrayHasKey('loggedin', $param);
220 }
221
222 return $data;
223 });
224
225 $result = $this->controller->index($request, $response);
226
227 static::assertSame(200, $result->getStatusCode());
228 static::assertSame('daily', (string) $result->getBody());
229 static::assertSame(
230 'Daily - '. format_date($currentDay, false, true) .' - Shaarli',
231 $assignedVariables['pagetitle']
232 );
233 static::assertCount(1, $assignedVariables['linksToDisplay']);
234
235 $link = $assignedVariables['linksToDisplay'][0];
236 static::assertSame(1, $link['id']);
237 }
238
239 /**
240 * Daily page - test that height adjustment in columns is working
241 */
242 public function testValidIndexControllerInvokeHeightAdjustment(): void
243 {
244 $currentDay = new \DateTimeImmutable('2020-05-13');
245
246 $request = $this->createMock(Request::class);
247 $response = new Response();
248
249 // Save RainTPL assigned variables
250 $assignedVariables = [];
251 $this->assignTemplateVars($assignedVariables);
252
253 // Links dataset: 2 links with thumbnails
254 $this->container->bookmarkService
255 ->expects(static::once())
256 ->method('days')
257 ->willReturnCallback(function () use ($currentDay): array {
258 return [
259 $currentDay->format($currentDay->format('Ymd')),
260 ];
261 })
262 ;
263 $this->container->bookmarkService
264 ->expects(static::once())
265 ->method('filterDay')
266 ->willReturnCallback(function (): array {
267 return [
268 (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'),
269 (new Bookmark())
270 ->setId(2)
271 ->setUrl('http://url.tld')
272 ->setTitle(static::generateString(50))
273 ->setDescription(static::generateString(5000))
274 ,
275 (new Bookmark())->setId(3)->setUrl('http://url.tld')->setTitle('title'),
276 (new Bookmark())->setId(4)->setUrl('http://url.tld')->setTitle('title'),
277 (new Bookmark())->setId(5)->setUrl('http://url.tld')->setTitle('title'),
278 (new Bookmark())->setId(6)->setUrl('http://url.tld')->setTitle('title'),
279 (new Bookmark())->setId(7)->setUrl('http://url.tld')->setTitle('title'),
280 ];
281 })
282 ;
283
284 // Make sure that PluginManager hook is triggered
285 $this->container->pluginManager
286 ->expects(static::atLeastOnce())
287 ->method('executeHooks')
288 ->willReturnCallback(function (string $hook, array $data, array $param): array {
289 return $data;
290 })
291 ;
292
293 $result = $this->controller->index($request, $response);
294
295 static::assertSame(200, $result->getStatusCode());
296 static::assertSame('daily', (string) $result->getBody());
297 static::assertCount(7, $assignedVariables['linksToDisplay']);
298
299 $columnIds = function (array $column): array {
300 return array_map(function (array $item): int { return $item['id']; }, $column);
301 };
302
303 static::assertSame([1, 4, 6], $columnIds($assignedVariables['cols'][0]));
304 static::assertSame([2], $columnIds($assignedVariables['cols'][1]));
305 static::assertSame([3, 5, 7], $columnIds($assignedVariables['cols'][2]));
306 }
307
308 /**
309 * Daily page - no bookmark
310 */
311 public function testValidIndexControllerInvokeNoBookmark(): void
312 {
313 $request = $this->createMock(Request::class);
314 $response = new Response();
315
316 // Save RainTPL assigned variables
317 $assignedVariables = [];
318 $this->assignTemplateVars($assignedVariables);
319
320 // Links dataset: 2 links with thumbnails
321 $this->container->bookmarkService
322 ->expects(static::once())
323 ->method('days')
324 ->willReturnCallback(function (): array {
325 return [];
326 })
327 ;
328 $this->container->bookmarkService
329 ->expects(static::once())
330 ->method('filterDay')
331 ->willReturnCallback(function (): array {
332 return [];
333 })
334 ;
335
336 // Make sure that PluginManager hook is triggered
337 $this->container->pluginManager
338 ->expects(static::atLeastOnce())
339 ->method('executeHooks')
340 ->willReturnCallback(function (string $hook, array $data, array $param): array {
341 return $data;
342 })
343 ;
344
345 $result = $this->controller->index($request, $response);
346
347 static::assertSame(200, $result->getStatusCode());
348 static::assertSame('daily', (string) $result->getBody());
349 static::assertCount(0, $assignedVariables['linksToDisplay']);
350 static::assertSame('Today', $assignedVariables['dayDesc']);
351 static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']);
352 static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']);
353 }
354
355 /**
356 * Daily RSS - default behaviour
357 */
358 public function testValidRssControllerInvokeDefault(): void
359 {
360 $dates = [
361 new \DateTimeImmutable('2020-05-17'),
362 new \DateTimeImmutable('2020-05-15'),
363 new \DateTimeImmutable('2020-05-13'),
364 ];
365
366 $request = $this->createMock(Request::class);
367 $response = new Response();
368
369 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([
370 (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
371 (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
372 (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
373 (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
374 ]);
375
376 $this->container->pageCacheManager
377 ->expects(static::once())
378 ->method('getCachePage')
379 ->willReturnCallback(function (): CachedPage {
380 $cachedPage = $this->createMock(CachedPage::class);
381 $cachedPage->expects(static::once())->method('cache')->with('dailyrss');
382
383 return $cachedPage;
384 }
385 );
386
387 // Save RainTPL assigned variables
388 $assignedVariables = [];
389 $this->assignTemplateVars($assignedVariables);
390
391 $result = $this->controller->rss($request, $response);
392
393 static::assertSame(200, $result->getStatusCode());
394 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
395 static::assertSame('dailyrss', (string) $result->getBody());
396 static::assertSame('Shaarli', $assignedVariables['title']);
397 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
398 static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
399 static::assertFalse($assignedVariables['hide_timestamps']);
400 static::assertCount(2, $assignedVariables['days']);
401
402 $day = $assignedVariables['days'][$dates[0]->format('Ymd')];
403
404 static::assertEquals($dates[0], $day['date']);
405 static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']);
406 static::assertSame(format_date($dates[0], false), $day['date_human']);
407 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
408 static::assertCount(1, $day['links']);
409 static::assertSame(1, $day['links'][0]['id']);
410 static::assertSame('http://domain.tld/1', $day['links'][0]['url']);
411 static::assertEquals($dates[0], $day['links'][0]['created']);
412
413 $day = $assignedVariables['days'][$dates[1]->format('Ymd')];
414
415 static::assertEquals($dates[1], $day['date']);
416 static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']);
417 static::assertSame(format_date($dates[1], false), $day['date_human']);
418 static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
419 static::assertCount(2, $day['links']);
420
421 static::assertSame(2, $day['links'][0]['id']);
422 static::assertSame('http://domain.tld/2', $day['links'][0]['url']);
423 static::assertEquals($dates[1], $day['links'][0]['created']);
424 static::assertSame(3, $day['links'][1]['id']);
425 static::assertSame('http://domain.tld/3', $day['links'][1]['url']);
426 static::assertEquals($dates[1], $day['links'][1]['created']);
427 }
428
429 /**
430 * Daily RSS - trigger cache rendering
431 */
432 public function testValidRssControllerInvokeTriggerCache(): void
433 {
434 $request = $this->createMock(Request::class);
435 $response = new Response();
436
437 $this->container->pageCacheManager->method('getCachePage')->willReturnCallback(function (): CachedPage {
438 $cachedPage = $this->createMock(CachedPage::class);
439 $cachedPage->method('cachedVersion')->willReturn('this is cache!');
440
441 return $cachedPage;
442 });
443
444 $this->container->bookmarkService->expects(static::never())->method('search');
445
446 $result = $this->controller->rss($request, $response);
447
448 static::assertSame(200, $result->getStatusCode());
449 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
450 static::assertSame('this is cache!', (string) $result->getBody());
451 }
452
453 /**
454 * Daily RSS - No bookmark
455 */
456 public function testValidRssControllerInvokeNoBookmark(): void
457 {
458 $request = $this->createMock(Request::class);
459 $response = new Response();
460
461 $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([]);
462
463 // Save RainTPL assigned variables
464 $assignedVariables = [];
465 $this->assignTemplateVars($assignedVariables);
466
467 $result = $this->controller->rss($request, $response);
468
469 static::assertSame(200, $result->getStatusCode());
470 static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
471 static::assertSame('dailyrss', (string) $result->getBody());
472 static::assertSame('Shaarli', $assignedVariables['title']);
473 static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
474 static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
475 static::assertFalse($assignedVariables['hide_timestamps']);
476 static::assertCount(0, $assignedVariables['days']);
477 }
478}
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..345ad544
--- /dev/null
+++ b/tests/front/controller/visitor/InstallControllerTest.php
@@ -0,0 +1,295 @@
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
84 /**
85 * Instantiate the install controller with an existing config file: exception.
86 */
87 public function testInstallWithExistingConfigFile(): void
88 {
89 $this->expectException(AlreadyInstalledException::class);
90
91 touch(static::MOCK_FILE);
92
93 $this->controller = new InstallController($this->container);
94 }
95
96 /**
97 * Call controller without session yet defined, redirect to test session install page.
98 */
99 public function testInstallRedirectToSessionTest(): void
100 {
101 $request = $this->createMock(Request::class);
102 $response = new Response();
103
104 $this->container->sessionManager = $this->createMock(SessionManager::class);
105 $this->container->sessionManager
106 ->expects(static::once())
107 ->method('setSessionParameter')
108 ->with(InstallController::SESSION_TEST_KEY, InstallController::SESSION_TEST_VALUE)
109 ;
110
111 $result = $this->controller->index($request, $response);
112
113 static::assertSame(302, $result->getStatusCode());
114 static::assertSame('/subfolder/install/session-test', $result->getHeader('location')[0]);
115 }
116
117 /**
118 * Call controller in session test mode: valid session then redirect to install page.
119 */
120 public function testInstallSessionTestValid(): void
121 {
122 $request = $this->createMock(Request::class);
123 $response = new Response();
124
125 $this->container->sessionManager = $this->createMock(SessionManager::class);
126 $this->container->sessionManager
127 ->method('getSessionParameter')
128 ->with(InstallController::SESSION_TEST_KEY)
129 ->willReturn(InstallController::SESSION_TEST_VALUE)
130 ;
131
132 $result = $this->controller->sessionTest($request, $response);
133
134 static::assertSame(302, $result->getStatusCode());
135 static::assertSame('/subfolder/install', $result->getHeader('location')[0]);
136 }
137
138 /**
139 * Call controller in session test mode: invalid session then redirect to error page.
140 */
141 public function testInstallSessionTestError(): void
142 {
143 $assignedVars = [];
144 $this->assignTemplateVars($assignedVars);
145
146 $request = $this->createMock(Request::class);
147 $response = new Response();
148
149 $this->container->sessionManager = $this->createMock(SessionManager::class);
150 $this->container->sessionManager
151 ->method('getSessionParameter')
152 ->with(InstallController::SESSION_TEST_KEY)
153 ->willReturn('KO')
154 ;
155
156 $result = $this->controller->sessionTest($request, $response);
157
158 static::assertSame(200, $result->getStatusCode());
159 static::assertSame('error', (string) $result->getBody());
160 static::assertStringStartsWith(
161 '<pre>Sessions do not seem to work correctly on your server',
162 $assignedVars['message']
163 );
164 }
165
166 /**
167 * Test saving valid data from install form. Also initialize datastore.
168 */
169 public function testSaveInstallValid(): void
170 {
171 $providedParameters = [
172 'continent' => 'Europe',
173 'city' => 'Berlin',
174 'setlogin' => 'bob',
175 'setpassword' => 'password',
176 'title' => 'Shaarli',
177 'language' => 'fr',
178 'updateCheck' => true,
179 'enableApi' => true,
180 ];
181
182 $expectedSettings = [
183 'general.timezone' => 'Europe/Berlin',
184 'credentials.login' => 'bob',
185 'credentials.salt' => '_NOT_EMPTY',
186 'credentials.hash' => '_NOT_EMPTY',
187 'general.title' => 'Shaarli',
188 'translation.language' => 'en',
189 'updates.check_updates' => true,
190 'api.enabled' => true,
191 'api.secret' => '_NOT_EMPTY',
192 'general.header_link' => '/subfolder',
193 ];
194
195 $request = $this->createMock(Request::class);
196 $request->method('getParam')->willReturnCallback(function (string $key) use ($providedParameters) {
197 return $providedParameters[$key] ?? null;
198 });
199 $response = new Response();
200
201 $this->container->conf = $this->createMock(ConfigManager::class);
202 $this->container->conf
203 ->method('get')
204 ->willReturnCallback(function (string $key, $value) {
205 if ($key === 'credentials.login') {
206 return 'bob';
207 } elseif ($key === 'credentials.salt') {
208 return 'salt';
209 }
210
211 return $value;
212 })
213 ;
214 $this->container->conf
215 ->expects(static::exactly(count($expectedSettings)))
216 ->method('set')
217 ->willReturnCallback(function (string $key, $value) use ($expectedSettings) {
218 if ($expectedSettings[$key] ?? null === '_NOT_EMPTY') {
219 static::assertNotEmpty($value);
220 } else {
221 static::assertSame($expectedSettings[$key], $value);
222 }
223 })
224 ;
225 $this->container->conf->expects(static::once())->method('write');
226
227 $this->container->sessionManager
228 ->expects(static::once())
229 ->method('setSessionParameter')
230 ->with(SessionManager::KEY_SUCCESS_MESSAGES)
231 ;
232
233 $result = $this->controller->save($request, $response);
234
235 static::assertSame(302, $result->getStatusCode());
236 static::assertSame('/subfolder/login', $result->getHeader('location')[0]);
237 }
238
239 /**
240 * Test default settings (timezone and title).
241 * Also check that bookmarks are not initialized if
242 */
243 public function testSaveInstallDefaultValues(): void
244 {
245 $confSettings = [];
246
247 $request = $this->createMock(Request::class);
248 $response = new Response();
249
250 $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) {
251 $confSettings[$key] = $value;
252 });
253
254 $result = $this->controller->save($request, $response);
255
256 static::assertSame(302, $result->getStatusCode());
257 static::assertSame('/subfolder/login', $result->getHeader('location')[0]);
258
259 static::assertSame('UTC', $confSettings['general.timezone']);
260 static::assertSame('Shared bookmarks on http://shaarli/subfolder/', $confSettings['general.title']);
261 }
262
263 /**
264 * Same test as testSaveInstallDefaultValues() but for an instance install in root directory.
265 */
266 public function testSaveInstallDefaultValuesWithoutSubfolder(): void
267 {
268 $confSettings = [];
269
270 $this->container->environment = [
271 'SERVER_NAME' => 'shaarli',
272 'SERVER_PORT' => '80',
273 'REQUEST_URI' => '/install',
274 'REMOTE_ADDR' => '1.2.3.4',
275 'SCRIPT_NAME' => '/index.php',
276 ];
277
278 $this->container->basePath = '';
279
280 $request = $this->createMock(Request::class);
281 $response = new Response();
282
283 $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) {
284 $confSettings[$key] = $value;
285 });
286
287 $result = $this->controller->save($request, $response);
288
289 static::assertSame(302, $result->getStatusCode());
290 static::assertSame('/login', $result->getHeader('location')[0]);
291
292 static::assertSame('UTC', $confSettings['general.timezone']);
293 static::assertSame('Shared bookmarks on http://shaarli/', $confSettings['general.title']);
294 }
295}
diff --git a/tests/front/controller/visitor/LoginControllerTest.php b/tests/front/controller/visitor/LoginControllerTest.php
new file mode 100644
index 00000000..1312ccb7
--- /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', '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/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/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/LegacyDummyUpdater.php b/tests/legacy/LegacyDummyUpdater.php
new file mode 100644
index 00000000..10e0a5b7
--- /dev/null
+++ b/tests/legacy/LegacyDummyUpdater.php
@@ -0,0 +1,74 @@
1<?php
2namespace Shaarli\Updater;
3
4use Exception;
5use ReflectionClass;
6use ReflectionMethod;
7use Shaarli\Config\ConfigManager;
8use Shaarli\Legacy\LegacyLinkDB;
9use Shaarli\Legacy\LegacyUpdater;
10
11/**
12 * Class LegacyDummyUpdater.
13 * Extends updater to add update method designed for unit tests.
14 */
15class LegacyDummyUpdater extends LegacyUpdater
16{
17 /**
18 * Object constructor.
19 *
20 * @param array $doneUpdates Updates which are already done.
21 * @param LegacyLinkDB $linkDB LinkDB instance.
22 * @param ConfigManager $conf Configuration Manager instance.
23 * @param boolean $isLoggedIn True if the user is logged in.
24 */
25 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
26 {
27 parent::__construct($doneUpdates, $linkDB, $conf, $isLoggedIn);
28
29 // Retrieve all update methods.
30 // For unit test, only retrieve final methods,
31 $class = new ReflectionClass($this);
32 $this->methods = $class->getMethods(ReflectionMethod::IS_FINAL);
33 }
34
35 /**
36 * Update method 1.
37 *
38 * @return bool true.
39 */
40 final private function updateMethodDummy1()
41 {
42 return true;
43 }
44
45 /**
46 * Update method 2.
47 *
48 * @return bool true.
49 */
50 final private function updateMethodDummy2()
51 {
52 return true;
53 }
54
55 /**
56 * Update method 3.
57 *
58 * @return bool true.
59 */
60 final private function updateMethodDummy3()
61 {
62 return true;
63 }
64
65 /**
66 * Update method 4, raise an exception.
67 *
68 * @throws Exception error.
69 */
70 final private function updateMethodException()
71 {
72 throw new Exception('whatever');
73 }
74}
diff --git a/tests/bookmark/LinkDBTest.php b/tests/legacy/LegacyLinkDBTest.php
index 2990a6b5..df2cad62 100644
--- a/tests/bookmark/LinkDBTest.php
+++ b/tests/legacy/LegacyLinkDBTest.php
@@ -3,22 +3,22 @@
3 * Link datastore tests 3 * Link datastore tests
4 */ 4 */
5 5
6namespace Shaarli\Bookmark; 6namespace Shaarli\Legacy;
7 7
8use DateTime; 8use DateTime;
9use ReferenceLinkDB; 9use ReferenceLinkDB;
10use ReflectionClass; 10use ReflectionClass;
11use Shaarli; 11use Shaarli;
12use Shaarli\Bookmark\Bookmark;
12 13
13require_once 'application/feed/Cache.php';
14require_once 'application/Utils.php'; 14require_once 'application/Utils.php';
15require_once 'tests/utils/ReferenceLinkDB.php'; 15require_once 'tests/utils/ReferenceLinkDB.php';
16 16
17 17
18/** 18/**
19 * Unitary tests for LinkDB 19 * Unitary tests for LegacyLinkDBTest
20 */ 20 */
21class LinkDBTest extends \PHPUnit\Framework\TestCase 21class LegacyLinkDBTest extends \Shaarli\TestCase
22{ 22{
23 // datastore to test write operations 23 // datastore to test write operations
24 protected static $testDatastore = 'sandbox/datastore.php'; 24 protected static $testDatastore = 'sandbox/datastore.php';
@@ -29,19 +29,19 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
29 protected static $refDB = null; 29 protected static $refDB = null;
30 30
31 /** 31 /**
32 * @var LinkDB public LinkDB instance. 32 * @var LegacyLinkDB public LinkDB instance.
33 */ 33 */
34 protected static $publicLinkDB = null; 34 protected static $publicLinkDB = null;
35 35
36 /** 36 /**
37 * @var LinkDB private LinkDB instance. 37 * @var LegacyLinkDB private LinkDB instance.
38 */ 38 */
39 protected static $privateLinkDB = null; 39 protected static $privateLinkDB = null;
40 40
41 /** 41 /**
42 * Instantiates public and private LinkDBs with test data 42 * Instantiates public and private LinkDBs with test data
43 * 43 *
44 * The reference datastore contains public and private links that 44 * The reference datastore contains public and private bookmarks that
45 * will be used to test LinkDB's methods: 45 * will be used to test LinkDB's methods:
46 * - access filtering (public/private), 46 * - access filtering (public/private),
47 * - link searches: 47 * - link searches:
@@ -49,24 +49,19 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
49 * - by tag, 49 * - by tag,
50 * - by text, 50 * - by text,
51 * - etc. 51 * - etc.
52 */ 52 *
53 public static function setUpBeforeClass()
54 {
55 self::$refDB = new ReferenceLinkDB();
56 self::$refDB->write(self::$testDatastore);
57
58 self::$publicLinkDB = new LinkDB(self::$testDatastore, false, false);
59 self::$privateLinkDB = new LinkDB(self::$testDatastore, true, false);
60 }
61
62 /**
63 * Resets test data for each test 53 * Resets test data for each test
64 */ 54 */
65 protected function setUp() 55 protected function setUp(): void
66 { 56 {
67 if (file_exists(self::$testDatastore)) { 57 if (file_exists(self::$testDatastore)) {
68 unlink(self::$testDatastore); 58 unlink(self::$testDatastore);
69 } 59 }
60
61 self::$refDB = new ReferenceLinkDB(true);
62 self::$refDB->write(self::$testDatastore);
63 self::$publicLinkDB = new LegacyLinkDB(self::$testDatastore, false, false);
64 self::$privateLinkDB = new LegacyLinkDB(self::$testDatastore, true, false);
70 } 65 }
71 66
72 /** 67 /**
@@ -78,7 +73,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
78 */ 73 */
79 protected static function getMethod($name) 74 protected static function getMethod($name)
80 { 75 {
81 $class = new ReflectionClass('Shaarli\Bookmark\LinkDB'); 76 $class = new ReflectionClass('Shaarli\Legacy\LegacyLinkDB');
82 $method = $class->getMethod($name); 77 $method = $class->getMethod($name);
83 $method->setAccessible(true); 78 $method->setAccessible(true);
84 return $method; 79 return $method;
@@ -89,7 +84,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
89 */ 84 */
90 public function testConstructLoggedIn() 85 public function testConstructLoggedIn()
91 { 86 {
92 new LinkDB(self::$testDatastore, true, false); 87 new LegacyLinkDB(self::$testDatastore, true, false);
93 $this->assertFileExists(self::$testDatastore); 88 $this->assertFileExists(self::$testDatastore);
94 } 89 }
95 90
@@ -98,19 +93,19 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
98 */ 93 */
99 public function testConstructLoggedOut() 94 public function testConstructLoggedOut()
100 { 95 {
101 new LinkDB(self::$testDatastore, false, false); 96 new LegacyLinkDB(self::$testDatastore, false, false);
102 $this->assertFileExists(self::$testDatastore); 97 $this->assertFileExists(self::$testDatastore);
103 } 98 }
104 99
105 /** 100 /**
106 * Attempt to instantiate a LinkDB whereas the datastore is not writable 101 * Attempt to instantiate a LinkDB whereas the datastore is not writable
107 *
108 * @expectedException Shaarli\Exceptions\IOException
109 * @expectedExceptionMessageRegExp /Error accessing "null"/
110 */ 102 */
111 public function testConstructDatastoreNotWriteable() 103 public function testConstructDatastoreNotWriteable()
112 { 104 {
113 new LinkDB('null/store.db', false, false); 105 $this->expectException(\Shaarli\Exceptions\IOException::class);
106 $this->expectExceptionMessageRegExp('/Error accessing "null"/');
107
108 new LegacyLinkDB('null/store.db', false, false);
114 } 109 }
115 110
116 /** 111 /**
@@ -118,7 +113,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
118 */ 113 */
119 public function testCheckDBNew() 114 public function testCheckDBNew()
120 { 115 {
121 $linkDB = new LinkDB(self::$testDatastore, false, false); 116 $linkDB = new LegacyLinkDB(self::$testDatastore, false, false);
122 unlink(self::$testDatastore); 117 unlink(self::$testDatastore);
123 $this->assertFileNotExists(self::$testDatastore); 118 $this->assertFileNotExists(self::$testDatastore);
124 119
@@ -135,7 +130,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
135 */ 130 */
136 public function testCheckDBLoad() 131 public function testCheckDBLoad()
137 { 132 {
138 $linkDB = new LinkDB(self::$testDatastore, false, false); 133 $linkDB = new LegacyLinkDB(self::$testDatastore, false, false);
139 $datastoreSize = filesize(self::$testDatastore); 134 $datastoreSize = filesize(self::$testDatastore);
140 $this->assertGreaterThan(0, $datastoreSize); 135 $this->assertGreaterThan(0, $datastoreSize);
141 136
@@ -155,13 +150,13 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
155 public function testReadEmptyDB() 150 public function testReadEmptyDB()
156 { 151 {
157 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>'); 152 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>');
158 $emptyDB = new LinkDB(self::$testDatastore, false, false); 153 $emptyDB = new LegacyLinkDB(self::$testDatastore, false, false);
159 $this->assertEquals(0, sizeof($emptyDB)); 154 $this->assertEquals(0, sizeof($emptyDB));
160 $this->assertEquals(0, count($emptyDB)); 155 $this->assertEquals(0, count($emptyDB));
161 } 156 }
162 157
163 /** 158 /**
164 * Load public links from the DB 159 * Load public bookmarks from the DB
165 */ 160 */
166 public function testReadPublicDB() 161 public function testReadPublicDB()
167 { 162 {
@@ -172,7 +167,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
172 } 167 }
173 168
174 /** 169 /**
175 * Load public and private links from the DB 170 * Load public and private bookmarks from the DB
176 */ 171 */
177 public function testReadPrivateDB() 172 public function testReadPrivateDB()
178 { 173 {
@@ -183,31 +178,31 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
183 } 178 }
184 179
185 /** 180 /**
186 * Save the links to the DB 181 * Save the bookmarks to the DB
187 */ 182 */
188 public function testSave() 183 public function testSave()
189 { 184 {
190 $testDB = new LinkDB(self::$testDatastore, true, false); 185 $testDB = new LegacyLinkDB(self::$testDatastore, true, false);
191 $dbSize = sizeof($testDB); 186 $dbSize = sizeof($testDB);
192 187
193 $link = array( 188 $link = array(
194 'id' => 42, 189 'id' => 43,
195 'title' => 'an additional link', 190 'title' => 'an additional link',
196 'url' => 'http://dum.my', 191 'url' => 'http://dum.my',
197 'description' => 'One more', 192 'description' => 'One more',
198 'private' => 0, 193 'private' => 0,
199 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150518_190000'), 194 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150518_190000'),
200 'tags' => 'unit test' 195 'tags' => 'unit test'
201 ); 196 );
202 $testDB[$link['id']] = $link; 197 $testDB[$link['id']] = $link;
203 $testDB->save('tests'); 198 $testDB->save('tests');
204 199
205 $testDB = new LinkDB(self::$testDatastore, true, false); 200 $testDB = new LegacyLinkDB(self::$testDatastore, true, false);
206 $this->assertEquals($dbSize + 1, sizeof($testDB)); 201 $this->assertEquals($dbSize + 1, sizeof($testDB));
207 } 202 }
208 203
209 /** 204 /**
210 * Count existing links 205 * Count existing bookmarks
211 */ 206 */
212 public function testCount() 207 public function testCount()
213 { 208 {
@@ -222,11 +217,11 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
222 } 217 }
223 218
224 /** 219 /**
225 * Count existing links - public links hidden 220 * Count existing bookmarks - public bookmarks hidden
226 */ 221 */
227 public function testCountHiddenPublic() 222 public function testCountHiddenPublic()
228 { 223 {
229 $linkDB = new LinkDB(self::$testDatastore, false, true); 224 $linkDB = new LegacyLinkDB(self::$testDatastore, false, true);
230 225
231 $this->assertEquals( 226 $this->assertEquals(
232 0, 227 0,
@@ -239,7 +234,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
239 } 234 }
240 235
241 /** 236 /**
242 * List the days for which links have been posted 237 * List the days for which bookmarks have been posted
243 */ 238 */
244 public function testDays() 239 public function testDays()
245 { 240 {
@@ -262,7 +257,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
262 $link = self::$publicLinkDB->getLinkFromUrl('http://mediagoblin.org/'); 257 $link = self::$publicLinkDB->getLinkFromUrl('http://mediagoblin.org/');
263 258
264 $this->assertNotEquals(false, $link); 259 $this->assertNotEquals(false, $link);
265 $this->assertContains( 260 $this->assertContainsPolyfill(
266 'A free software media publishing platform', 261 'A free software media publishing platform',
267 $link['description'] 262 $link['description']
268 ); 263 );
@@ -425,22 +420,22 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
425 420
426 /** 421 /**
427 * Test filterHash() with an invalid smallhash. 422 * Test filterHash() with an invalid smallhash.
428 *
429 * @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException
430 */ 423 */
431 public function testFilterHashInValid1() 424 public function testFilterHashInValid1()
432 { 425 {
426 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
427
433 $request = 'blabla'; 428 $request = 'blabla';
434 self::$publicLinkDB->filterHash($request); 429 self::$publicLinkDB->filterHash($request);
435 } 430 }
436 431
437 /** 432 /**
438 * Test filterHash() with an empty smallhash. 433 * Test filterHash() with an empty smallhash.
439 *
440 * @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException
441 */ 434 */
442 public function testFilterHashInValid() 435 public function testFilterHashInValid()
443 { 436 {
437 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
438
444 self::$publicLinkDB->filterHash(''); 439 self::$publicLinkDB->filterHash('');
445 } 440 }
446 441
@@ -466,18 +461,18 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
466 } 461 }
467 462
468 /** 463 /**
469 * Test rename tag with a valid value present in multiple links 464 * Test rename tag with a valid value present in multiple bookmarks
470 */ 465 */
471 public function testRenameTagMultiple() 466 public function testRenameTagMultiple()
472 { 467 {
473 self::$refDB->write(self::$testDatastore); 468 self::$refDB->write(self::$testDatastore);
474 $linkDB = new LinkDB(self::$testDatastore, true, false); 469 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
475 470
476 $res = $linkDB->renameTag('cartoon', 'Taz'); 471 $res = $linkDB->renameTag('cartoon', 'Taz');
477 $this->assertEquals(3, count($res)); 472 $this->assertEquals(3, count($res));
478 $this->assertContains(' Taz ', $linkDB[4]['tags']); 473 $this->assertContainsPolyfill(' Taz ', $linkDB[4]['tags']);
479 $this->assertContains(' Taz ', $linkDB[1]['tags']); 474 $this->assertContainsPolyfill(' Taz ', $linkDB[1]['tags']);
480 $this->assertContains(' Taz ', $linkDB[0]['tags']); 475 $this->assertContainsPolyfill(' Taz ', $linkDB[0]['tags']);
481 } 476 }
482 477
483 /** 478 /**
@@ -486,7 +481,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
486 public function testRenameTagCaseSensitive() 481 public function testRenameTagCaseSensitive()
487 { 482 {
488 self::$refDB->write(self::$testDatastore); 483 self::$refDB->write(self::$testDatastore);
489 $linkDB = new LinkDB(self::$testDatastore, true, false); 484 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
490 485
491 $res = $linkDB->renameTag('sTuff', 'Taz'); 486 $res = $linkDB->renameTag('sTuff', 'Taz');
492 $this->assertEquals(1, count($res)); 487 $this->assertEquals(1, count($res));
@@ -498,7 +493,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
498 */ 493 */
499 public function testRenameTagInvalid() 494 public function testRenameTagInvalid()
500 { 495 {
501 $linkDB = new LinkDB(self::$testDatastore, false, false); 496 $linkDB = new LegacyLinkDB(self::$testDatastore, false, false);
502 497
503 $this->assertFalse($linkDB->renameTag('', 'test')); 498 $this->assertFalse($linkDB->renameTag('', 'test'));
504 $this->assertFalse($linkDB->renameTag('', '')); 499 $this->assertFalse($linkDB->renameTag('', ''));
@@ -513,11 +508,11 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
513 public function testDeleteTag() 508 public function testDeleteTag()
514 { 509 {
515 self::$refDB->write(self::$testDatastore); 510 self::$refDB->write(self::$testDatastore);
516 $linkDB = new LinkDB(self::$testDatastore, true, false); 511 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
517 512
518 $res = $linkDB->renameTag('cartoon', null); 513 $res = $linkDB->renameTag('cartoon', null);
519 $this->assertEquals(3, count($res)); 514 $this->assertEquals(3, count($res));
520 $this->assertNotContains('cartoon', $linkDB[4]['tags']); 515 $this->assertNotContainsPolyfill('cartoon', $linkDB[4]['tags']);
521 } 516 }
522 517
523 /** 518 /**
@@ -619,4 +614,42 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
619 614
620 $this->assertEquals($expected, $tags, var_export($tags, true)); 615 $this->assertEquals($expected, $tags, var_export($tags, true));
621 } 616 }
617
618 /**
619 * Make sure that bookmarks with the same timestamp have a consistent order:
620 * if their creation date is equal, bookmarks are sorted by ID DESC.
621 */
622 public function testConsistentOrder()
623 {
624 $nextId = 43;
625 $creation = DateTime::createFromFormat('Ymd_His', '20190807_130444');
626 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
627 for ($i = 0; $i < 4; ++$i) {
628 $linkDB[$nextId + $i] = [
629 'id' => $nextId + $i,
630 'url' => 'http://'. $i,
631 'created' => $creation,
632 'title' => true,
633 'description' => true,
634 'tags' => true,
635 ];
636 }
637
638 // Check 4 new links 4 times
639 for ($i = 0; $i < 4; ++$i) {
640 $linkDB->save('tests');
641 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
642 $count = 3;
643 foreach ($linkDB as $link) {
644 if ($link['sticky'] === true) {
645 continue;
646 }
647 $this->assertEquals($nextId + $count, $link['id']);
648 $this->assertEquals('http://'. $count, $link['url']);
649 if (--$count < 0) {
650 break;
651 }
652 }
653 }
654 }
622} 655}
diff --git a/tests/bookmark/LinkFilterTest.php b/tests/legacy/LegacyLinkFilterTest.php
index 808f8122..45d7754d 100644
--- a/tests/bookmark/LinkFilterTest.php
+++ b/tests/legacy/LegacyLinkFilterTest.php
@@ -4,18 +4,20 @@ namespace Shaarli\Bookmark;
4 4
5use Exception; 5use Exception;
6use ReferenceLinkDB; 6use ReferenceLinkDB;
7use Shaarli\Legacy\LegacyLinkDB;
8use Shaarli\Legacy\LegacyLinkFilter;
7 9
8/** 10/**
9 * Class LinkFilterTest. 11 * Class LegacyLinkFilterTest.
10 */ 12 */
11class LinkFilterTest extends \PHPUnit\Framework\TestCase 13class LegacyLinkFilterTest extends \Shaarli\TestCase
12{ 14{
13 /** 15 /**
14 * @var string Test datastore path. 16 * @var string Test datastore path.
15 */ 17 */
16 protected static $testDatastore = 'sandbox/datastore.php'; 18 protected static $testDatastore = 'sandbox/datastore.php';
17 /** 19 /**
18 * @var LinkFilter instance. 20 * @var BookmarkFilter instance.
19 */ 21 */
20 protected static $linkFilter; 22 protected static $linkFilter;
21 23
@@ -25,19 +27,19 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
25 protected static $refDB; 27 protected static $refDB;
26 28
27 /** 29 /**
28 * @var LinkDB instance 30 * @var LegacyLinkDB instance
29 */ 31 */
30 protected static $linkDB; 32 protected static $linkDB;
31 33
32 /** 34 /**
33 * Instantiate linkFilter with ReferenceLinkDB data. 35 * Instantiate linkFilter with ReferenceLinkDB data.
34 */ 36 */
35 public static function setUpBeforeClass() 37 public static function setUpBeforeClass(): void
36 { 38 {
37 self::$refDB = new ReferenceLinkDB(); 39 self::$refDB = new ReferenceLinkDB(true);
38 self::$refDB->write(self::$testDatastore); 40 self::$refDB->write(self::$testDatastore);
39 self::$linkDB = new LinkDB(self::$testDatastore, true, false); 41 self::$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
40 self::$linkFilter = new LinkFilter(self::$linkDB); 42 self::$linkFilter = new LegacyLinkFilter(self::$linkDB);
41 } 43 }
42 44
43 /** 45 /**
@@ -74,14 +76,14 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
74 76
75 $this->assertEquals( 77 $this->assertEquals(
76 ReferenceLinkDB::$NB_LINKS_TOTAL, 78 ReferenceLinkDB::$NB_LINKS_TOTAL,
77 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '')) 79 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, ''))
78 ); 80 );
79 81
80 $this->assertEquals( 82 $this->assertEquals(
81 self::$refDB->countUntaggedLinks(), 83 self::$refDB->countUntaggedLinks(),
82 count( 84 count(
83 self::$linkFilter->filter( 85 self::$linkFilter->filter(
84 LinkFilter::$FILTER_TAG, 86 LegacyLinkFilter::$FILTER_TAG,
85 /*$request=*/ 87 /*$request=*/
86 '', 88 '',
87 /*$casesensitive=*/ 89 /*$casesensitive=*/
@@ -96,89 +98,89 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
96 98
97 $this->assertEquals( 99 $this->assertEquals(
98 ReferenceLinkDB::$NB_LINKS_TOTAL, 100 ReferenceLinkDB::$NB_LINKS_TOTAL,
99 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '')) 101 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, ''))
100 ); 102 );
101 } 103 }
102 104
103 /** 105 /**
104 * Filter links using a tag 106 * Filter bookmarks using a tag
105 */ 107 */
106 public function testFilterOneTag() 108 public function testFilterOneTag()
107 { 109 {
108 $this->assertEquals( 110 $this->assertEquals(
109 4, 111 4,
110 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false)) 112 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false))
111 ); 113 );
112 114
113 $this->assertEquals( 115 $this->assertEquals(
114 4, 116 4,
115 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'all')) 117 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'all'))
116 ); 118 );
117 119
118 $this->assertEquals( 120 $this->assertEquals(
119 4, 121 4,
120 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'default-blabla')) 122 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'default-blabla'))
121 ); 123 );
122 124
123 // Private only. 125 // Private only.
124 $this->assertEquals( 126 $this->assertEquals(
125 1, 127 1,
126 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'private')) 128 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'private'))
127 ); 129 );
128 130
129 // Public only. 131 // Public only.
130 $this->assertEquals( 132 $this->assertEquals(
131 3, 133 3,
132 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'public')) 134 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'public'))
133 ); 135 );
134 } 136 }
135 137
136 /** 138 /**
137 * Filter links using a tag - case-sensitive 139 * Filter bookmarks using a tag - case-sensitive
138 */ 140 */
139 public function testFilterCaseSensitiveTag() 141 public function testFilterCaseSensitiveTag()
140 { 142 {
141 $this->assertEquals( 143 $this->assertEquals(
142 0, 144 0,
143 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'mercurial', true)) 145 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'mercurial', true))
144 ); 146 );
145 147
146 $this->assertEquals( 148 $this->assertEquals(
147 1, 149 1,
148 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'Mercurial', true)) 150 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'Mercurial', true))
149 ); 151 );
150 } 152 }
151 153
152 /** 154 /**
153 * Filter links using a tag combination 155 * Filter bookmarks using a tag combination
154 */ 156 */
155 public function testFilterMultipleTags() 157 public function testFilterMultipleTags()
156 { 158 {
157 $this->assertEquals( 159 $this->assertEquals(
158 2, 160 2,
159 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'dev cartoon', false)) 161 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'dev cartoon', false))
160 ); 162 );
161 } 163 }
162 164
163 /** 165 /**
164 * Filter links using a non-existent tag 166 * Filter bookmarks using a non-existent tag
165 */ 167 */
166 public function testFilterUnknownTag() 168 public function testFilterUnknownTag()
167 { 169 {
168 $this->assertEquals( 170 $this->assertEquals(
169 0, 171 0,
170 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'null', false)) 172 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'null', false))
171 ); 173 );
172 } 174 }
173 175
174 /** 176 /**
175 * Return links for a given day 177 * Return bookmarks for a given day
176 */ 178 */
177 public function testFilterDay() 179 public function testFilterDay()
178 { 180 {
179 $this->assertEquals( 181 $this->assertEquals(
180 4, 182 4,
181 count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206')) 183 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '20121206'))
182 ); 184 );
183 } 185 }
184 186
@@ -189,28 +191,30 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
189 { 191 {
190 $this->assertEquals( 192 $this->assertEquals(
191 0, 193 0,
192 count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '19700101')) 194 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '19700101'))
193 ); 195 );
194 } 196 }
195 197
196 /** 198 /**
197 * Use an invalid date format 199 * Use an invalid date format
198 * @expectedException Exception
199 * @expectedExceptionMessageRegExp /Invalid date format/
200 */ 200 */
201 public function testFilterInvalidDayWithChars() 201 public function testFilterInvalidDayWithChars()
202 { 202 {
203 self::$linkFilter->filter(LinkFilter::$FILTER_DAY, 'Rainy day, dream away'); 203 $this->expectException(\Exception::class);
204 $this->expectExceptionMessageRegExp('/Invalid date format/');
205
206 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, 'Rainy day, dream away');
204 } 207 }
205 208
206 /** 209 /**
207 * Use an invalid date format 210 * Use an invalid date format
208 * @expectedException Exception
209 * @expectedExceptionMessageRegExp /Invalid date format/
210 */ 211 */
211 public function testFilterInvalidDayDigits() 212 public function testFilterInvalidDayDigits()
212 { 213 {
213 self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20'); 214 $this->expectException(\Exception::class);
215 $this->expectExceptionMessageRegExp('/Invalid date format/');
216
217 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '20');
214 } 218 }
215 219
216 /** 220 /**
@@ -218,7 +222,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
218 */ 222 */
219 public function testFilterSmallHash() 223 public function testFilterSmallHash()
220 { 224 {
221 $links = self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'IuWvgA'); 225 $links = self::$linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, 'IuWvgA');
222 226
223 $this->assertEquals( 227 $this->assertEquals(
224 1, 228 1,
@@ -233,12 +237,12 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
233 237
234 /** 238 /**
235 * No link for this hash 239 * No link for this hash
236 *
237 * @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException
238 */ 240 */
239 public function testFilterUnknownSmallHash() 241 public function testFilterUnknownSmallHash()
240 { 242 {
241 self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah'); 243 $this->expectException(\Shaarli\Bookmark\Exception\BookmarkNotFoundException::class);
244
245 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, 'Iblaah');
242 } 246 }
243 247
244 /** 248 /**
@@ -248,7 +252,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
248 { 252 {
249 $this->assertEquals( 253 $this->assertEquals(
250 0, 254 0,
251 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'azertyuiop')) 255 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'azertyuiop'))
252 ); 256 );
253 } 257 }
254 258
@@ -259,12 +263,12 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
259 { 263 {
260 $this->assertEquals( 264 $this->assertEquals(
261 2, 265 2,
262 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars.userfriendly.org')) 266 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'ars.userfriendly.org'))
263 ); 267 );
264 268
265 $this->assertEquals( 269 $this->assertEquals(
266 2, 270 2,
267 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars org')) 271 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'ars org'))
268 ); 272 );
269 } 273 }
270 274
@@ -276,21 +280,21 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
276 // use miscellaneous cases 280 // use miscellaneous cases
277 $this->assertEquals( 281 $this->assertEquals(
278 2, 282 2,
279 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'userfriendly -')) 283 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'userfriendly -'))
280 ); 284 );
281 $this->assertEquals( 285 $this->assertEquals(
282 2, 286 2,
283 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'UserFriendly -')) 287 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'UserFriendly -'))
284 ); 288 );
285 $this->assertEquals( 289 $this->assertEquals(
286 2, 290 2,
287 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -')) 291 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -'))
288 ); 292 );
289 293
290 // use miscellaneous case and offset 294 // use miscellaneous case and offset
291 $this->assertEquals( 295 $this->assertEquals(
292 2, 296 2,
293 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'RFrIendL')) 297 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'RFrIendL'))
294 ); 298 );
295 } 299 }
296 300
@@ -301,17 +305,17 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
301 { 305 {
302 $this->assertEquals( 306 $this->assertEquals(
303 1, 307 1,
304 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'publishing media')) 308 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'publishing media'))
305 ); 309 );
306 310
307 $this->assertEquals( 311 $this->assertEquals(
308 1, 312 1,
309 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'mercurial w3c')) 313 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'mercurial w3c'))
310 ); 314 );
311 315
312 $this->assertEquals( 316 $this->assertEquals(
313 3, 317 3,
314 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '"free software"')) 318 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, '"free software"'))
315 ); 319 );
316 } 320 }
317 321
@@ -322,29 +326,29 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
322 { 326 {
323 $this->assertEquals( 327 $this->assertEquals(
324 6, 328 6,
325 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web')) 329 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web'))
326 ); 330 );
327 331
328 $this->assertEquals( 332 $this->assertEquals(
329 6, 333 6,
330 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', 'all')) 334 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', 'all'))
331 ); 335 );
332 336
333 $this->assertEquals( 337 $this->assertEquals(
334 6, 338 6,
335 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', 'bla')) 339 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', 'bla'))
336 ); 340 );
337 341
338 // Private only. 342 // Private only.
339 $this->assertEquals( 343 $this->assertEquals(
340 1, 344 1,
341 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, 'private')) 345 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', false, 'private'))
342 ); 346 );
343 347
344 // Public only. 348 // Public only.
345 $this->assertEquals( 349 $this->assertEquals(
346 5, 350 5,
347 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, 'public')) 351 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', false, 'public'))
348 ); 352 );
349 } 353 }
350 354
@@ -355,7 +359,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
355 { 359 {
356 $this->assertEquals( 360 $this->assertEquals(
357 3, 361 3,
358 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software')) 362 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'free software'))
359 ); 363 );
360 } 364 }
361 365
@@ -366,12 +370,12 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
366 { 370 {
367 $this->assertEquals( 371 $this->assertEquals(
368 1, 372 1,
369 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free -gnu')) 373 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'free -gnu'))
370 ); 374 );
371 375
372 $this->assertEquals( 376 $this->assertEquals(
373 ReferenceLinkDB::$NB_LINKS_TOTAL - 1, 377 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
374 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution')) 378 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, '-revolution'))
375 ); 379 );
376 } 380 }
377 381
@@ -383,7 +387,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
383 $this->assertEquals( 387 $this->assertEquals(
384 2, 388 2,
385 count(self::$linkFilter->filter( 389 count(self::$linkFilter->filter(
386 LinkFilter::$FILTER_TEXT, 390 LegacyLinkFilter::$FILTER_TEXT,
387 '"Free Software " stallman "read this" @website stuff' 391 '"Free Software " stallman "read this" @website stuff'
388 )) 392 ))
389 ); 393 );
@@ -391,7 +395,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
391 $this->assertEquals( 395 $this->assertEquals(
392 1, 396 1,
393 count(self::$linkFilter->filter( 397 count(self::$linkFilter->filter(
394 LinkFilter::$FILTER_TEXT, 398 LegacyLinkFilter::$FILTER_TEXT,
395 '"free software " stallman "read this" -beard @website stuff' 399 '"free software " stallman "read this" -beard @website stuff'
396 )) 400 ))
397 ); 401 );
@@ -405,7 +409,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
405 $this->assertEquals( 409 $this->assertEquals(
406 0, 410 0,
407 count(self::$linkFilter->filter( 411 count(self::$linkFilter->filter(
408 LinkFilter::$FILTER_TEXT, 412 LegacyLinkFilter::$FILTER_TEXT,
409 '"designer naming"' 413 '"designer naming"'
410 )) 414 ))
411 ); 415 );
@@ -413,7 +417,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
413 $this->assertEquals( 417 $this->assertEquals(
414 0, 418 0,
415 count(self::$linkFilter->filter( 419 count(self::$linkFilter->filter(
416 LinkFilter::$FILTER_TEXT, 420 LegacyLinkFilter::$FILTER_TEXT,
417 '"designernaming"' 421 '"designernaming"'
418 )) 422 ))
419 ); 423 );
@@ -426,12 +430,12 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
426 { 430 {
427 $this->assertEquals( 431 $this->assertEquals(
428 1, 432 1,
429 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'gnu -free')) 433 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'gnu -free'))
430 ); 434 );
431 435
432 $this->assertEquals( 436 $this->assertEquals(
433 ReferenceLinkDB::$NB_LINKS_TOTAL - 1, 437 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
434 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free')) 438 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, '-free'))
435 ); 439 );
436 } 440 }
437 441
@@ -445,42 +449,42 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
445 $this->assertEquals( 449 $this->assertEquals(
446 1, 450 1,
447 count(self::$linkFilter->filter( 451 count(self::$linkFilter->filter(
448 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 452 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
449 array($tags, $terms) 453 array($tags, $terms)
450 )) 454 ))
451 ); 455 );
452 $this->assertEquals( 456 $this->assertEquals(
453 2, 457 2,
454 count(self::$linkFilter->filter( 458 count(self::$linkFilter->filter(
455 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 459 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
456 array('', $terms) 460 array('', $terms)
457 )) 461 ))
458 ); 462 );
459 $this->assertEquals( 463 $this->assertEquals(
460 1, 464 1,
461 count(self::$linkFilter->filter( 465 count(self::$linkFilter->filter(
462 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 466 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
463 array(false, 'PSR-2') 467 array(false, 'PSR-2')
464 )) 468 ))
465 ); 469 );
466 $this->assertEquals( 470 $this->assertEquals(
467 1, 471 1,
468 count(self::$linkFilter->filter( 472 count(self::$linkFilter->filter(
469 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 473 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
470 array($tags, '') 474 array($tags, '')
471 )) 475 ))
472 ); 476 );
473 $this->assertEquals( 477 $this->assertEquals(
474 ReferenceLinkDB::$NB_LINKS_TOTAL, 478 ReferenceLinkDB::$NB_LINKS_TOTAL,
475 count(self::$linkFilter->filter( 479 count(self::$linkFilter->filter(
476 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 480 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
477 '' 481 ''
478 )) 482 ))
479 ); 483 );
480 } 484 }
481 485
482 /** 486 /**
483 * Filter links by #hashtag. 487 * Filter bookmarks by #hashtag.
484 */ 488 */
485 public function testFilterByHashtag() 489 public function testFilterByHashtag()
486 { 490 {
@@ -488,7 +492,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
488 $this->assertEquals( 492 $this->assertEquals(
489 3, 493 3,
490 count(self::$linkFilter->filter( 494 count(self::$linkFilter->filter(
491 LinkFilter::$FILTER_TAG, 495 LegacyLinkFilter::$FILTER_TAG,
492 $hashtag 496 $hashtag
493 )) 497 ))
494 ); 498 );
@@ -497,7 +501,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
497 $this->assertEquals( 501 $this->assertEquals(
498 1, 502 1,
499 count(self::$linkFilter->filter( 503 count(self::$linkFilter->filter(
500 LinkFilter::$FILTER_TAG, 504 LegacyLinkFilter::$FILTER_TAG,
501 $hashtag, 505 $hashtag,
502 false, 506 false,
503 'private' 507 'private'
diff --git a/tests/legacy/LegacyUpdaterTest.php b/tests/legacy/LegacyUpdaterTest.php
new file mode 100644
index 00000000..f7391b86
--- /dev/null
+++ b/tests/legacy/LegacyUpdaterTest.php
@@ -0,0 +1,886 @@
1<?php
2namespace Shaarli\Updater;
3
4use DateTime;
5use Exception;
6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigJson;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Config\ConfigPhp;
10use Shaarli\Legacy\LegacyLinkDB;
11use Shaarli\Legacy\LegacyUpdater;
12use Shaarli\Thumbnailer;
13
14require_once 'application/updater/UpdaterUtils.php';
15require_once 'tests/updater/DummyUpdater.php';
16require_once 'tests/utils/ReferenceLinkDB.php';
17require_once 'inc/rain.tpl.class.php';
18
19/**
20 * Class UpdaterTest.
21 * Runs unit tests against the updater class.
22 */
23class LegacyUpdaterTest extends \Shaarli\TestCase
24{
25 /**
26 * @var string Path to test datastore.
27 */
28 protected static $testDatastore = 'sandbox/datastore.php';
29
30 /**
31 * @var string Config file path (without extension).
32 */
33 protected static $configFile = 'sandbox/config';
34
35 /**
36 * @var ConfigManager
37 */
38 protected $conf;
39
40 /**
41 * Executed before each test.
42 */
43 protected function setUp(): void
44 {
45 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
46 $this->conf = new ConfigManager(self::$configFile);
47 }
48
49 /**
50 * Test UpdaterUtils::read_updates_file with an empty/missing file.
51 */
52 public function testReadEmptyUpdatesFile()
53 {
54 $this->assertEquals(array(), UpdaterUtils::read_updates_file(''));
55 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
56 touch($updatesFile);
57 $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile));
58 unlink($updatesFile);
59 }
60
61 /**
62 * Test read/write updates file.
63 */
64 public function testReadWriteUpdatesFile()
65 {
66 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
67 $updatesMethods = array('m1', 'm2', 'm3');
68
69 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
70 $readMethods = UpdaterUtils::read_updates_file($updatesFile);
71 $this->assertEquals($readMethods, $updatesMethods);
72
73 // Update
74 $updatesMethods[] = 'm4';
75 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
76 $readMethods = UpdaterUtils::read_updates_file($updatesFile);
77 $this->assertEquals($readMethods, $updatesMethods);
78 unlink($updatesFile);
79 }
80
81 /**
82 * Test errors in UpdaterUtils::write_updates_file(): empty updates file.
83 */
84 public function testWriteEmptyUpdatesFile()
85 {
86 $this->expectException(\Exception::class);
87 $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/');
88
89 UpdaterUtils::write_updates_file('', array('test'));
90 }
91
92 /**
93 * Test errors in UpdaterUtils::write_updates_file(): not writable updates file.
94 */
95 public function testWriteUpdatesFileNotWritable()
96 {
97 $this->expectException(\Exception::class);
98 $this->expectExceptionMessageRegExp('/Unable to write(.*)/');
99
100 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
101 touch($updatesFile);
102 chmod($updatesFile, 0444);
103 try {
104 @UpdaterUtils::write_updates_file($updatesFile, array('test'));
105 } catch (Exception $e) {
106 unlink($updatesFile);
107 throw $e;
108 }
109 }
110
111 /**
112 * Test the update() method, with no update to run.
113 * 1. Everything already run.
114 * 2. User is logged out.
115 */
116 public function testNoUpdates()
117 {
118 $updates = array(
119 'updateMethodDummy1',
120 'updateMethodDummy2',
121 'updateMethodDummy3',
122 'updateMethodException',
123 );
124 $updater = new DummyUpdater($updates, array(), $this->conf, true);
125 $this->assertEquals(array(), $updater->update());
126
127 $updater = new DummyUpdater(array(), array(), $this->conf, false);
128 $this->assertEquals(array(), $updater->update());
129 }
130
131 /**
132 * Test the update() method, with all updates to run (except the failing one).
133 */
134 public function testUpdatesFirstTime()
135 {
136 $updates = array('updateMethodException',);
137 $expectedUpdates = array(
138 'updateMethodDummy1',
139 'updateMethodDummy2',
140 'updateMethodDummy3',
141 );
142 $updater = new DummyUpdater($updates, array(), $this->conf, true);
143 $this->assertEquals($expectedUpdates, $updater->update());
144 }
145
146 /**
147 * Test the update() method, only one update to run.
148 */
149 public function testOneUpdate()
150 {
151 $updates = array(
152 'updateMethodDummy1',
153 'updateMethodDummy3',
154 'updateMethodException',
155 );
156 $expectedUpdate = array('updateMethodDummy2');
157
158 $updater = new DummyUpdater($updates, array(), $this->conf, true);
159 $this->assertEquals($expectedUpdate, $updater->update());
160 }
161
162 /**
163 * Test Update failed.
164 */
165 public function testUpdateFailed()
166 {
167 $this->expectException(\Exception::class);
168
169 $updates = array(
170 'updateMethodDummy1',
171 'updateMethodDummy2',
172 'updateMethodDummy3',
173 );
174
175 $updater = new DummyUpdater($updates, array(), $this->conf, true);
176 $updater->update();
177 }
178
179 /**
180 * Test update mergeDeprecatedConfig:
181 * 1. init a config file.
182 * 2. init a options.php file with update value.
183 * 3. merge.
184 * 4. check updated value in config file.
185 */
186 public function testUpdateMergeDeprecatedConfig()
187 {
188 $this->conf->setConfigFile('tests/utils/config/configPhp');
189 $this->conf->reset();
190
191 $optionsFile = 'tests/updater/options.php';
192 $options = '<?php
193$GLOBALS[\'privateLinkByDefault\'] = true;';
194 file_put_contents($optionsFile, $options);
195
196 // tmp config file.
197 $this->conf->setConfigFile('tests/updater/config');
198
199 // merge configs
200 $updater = new LegacyUpdater(array(), array(), $this->conf, true);
201 // This writes a new config file in tests/updater/config.php
202 $updater->updateMethodMergeDeprecatedConfigFile();
203
204 // make sure updated field is changed
205 $this->conf->reload();
206 $this->assertTrue($this->conf->get('privacy.default_private_links'));
207 $this->assertFalse(is_file($optionsFile));
208 // Delete the generated file.
209 unlink($this->conf->getConfigFileExt());
210 }
211
212 /**
213 * Test mergeDeprecatedConfig in without options file.
214 */
215 public function testMergeDeprecatedConfigNoFile()
216 {
217 $updater = new LegacyUpdater(array(), array(), $this->conf, true);
218 $updater->updateMethodMergeDeprecatedConfigFile();
219
220 $this->assertEquals('root', $this->conf->get('credentials.login'));
221 }
222
223 /**
224 * Test renameDashTags update method.
225 */
226 public function testRenameDashTags()
227 {
228 $refDB = new \ReferenceLinkDB(true);
229 $refDB->write(self::$testDatastore);
230 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
231
232 $this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
233 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
234 $updater->updateMethodRenameDashTags();
235 $this->assertNotEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
236 }
237
238 /**
239 * Convert old PHP config file to JSON config.
240 */
241 public function testConfigToJson()
242 {
243 $configFile = 'tests/utils/config/configPhp';
244 $this->conf->setConfigFile($configFile);
245 $this->conf->reset();
246
247 // The ConfigIO is initialized with ConfigPhp.
248 $this->assertTrue($this->conf->getConfigIO() instanceof ConfigPhp);
249
250 $updater = new LegacyUpdater(array(), array(), $this->conf, false);
251 $done = $updater->updateMethodConfigToJson();
252 $this->assertTrue($done);
253
254 // The ConfigIO has been updated to ConfigJson.
255 $this->assertTrue($this->conf->getConfigIO() instanceof ConfigJson);
256 $this->assertTrue(file_exists($this->conf->getConfigFileExt()));
257
258 // Check JSON config data.
259 $this->conf->reload();
260 $this->assertEquals('root', $this->conf->get('credentials.login'));
261 $this->assertEquals('lala', $this->conf->get('redirector.url'));
262 $this->assertEquals('data/datastore.php', $this->conf->get('resource.datastore'));
263 $this->assertEquals('1', $this->conf->get('plugins.WALLABAG_VERSION'));
264
265 rename($configFile . '.save.php', $configFile . '.php');
266 unlink($this->conf->getConfigFileExt());
267 }
268
269 /**
270 * Launch config conversion update with an existing JSON file => nothing to do.
271 */
272 public function testConfigToJsonNothingToDo()
273 {
274 $filetime = filemtime($this->conf->getConfigFileExt());
275 $updater = new LegacyUpdater(array(), array(), $this->conf, false);
276 $done = $updater->updateMethodConfigToJson();
277 $this->assertTrue($done);
278 $expected = filemtime($this->conf->getConfigFileExt());
279 $this->assertEquals($expected, $filetime);
280 }
281
282 /**
283 * Test escapeUnescapedConfig with valid data.
284 */
285 public function testEscapeConfig()
286 {
287 $sandbox = 'sandbox/config';
288 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
289 $this->conf = new ConfigManager($sandbox);
290 $title = '<script>alert("title");</script>';
291 $headerLink = '<script>alert("header_link");</script>';
292 $this->conf->set('general.title', $title);
293 $this->conf->set('general.header_link', $headerLink);
294 $updater = new LegacyUpdater(array(), array(), $this->conf, true);
295 $done = $updater->updateMethodEscapeUnescapedConfig();
296 $this->assertTrue($done);
297 $this->conf->reload();
298 $this->assertEquals(escape($title), $this->conf->get('general.title'));
299 $this->assertEquals(escape($headerLink), $this->conf->get('general.header_link'));
300 unlink($sandbox . '.json.php');
301 }
302
303 /**
304 * Test updateMethodApiSettings(): create default settings for the API (enabled + secret).
305 */
306 public function testUpdateApiSettings()
307 {
308 $confFile = 'sandbox/config';
309 copy(self::$configFile .'.json.php', $confFile .'.json.php');
310 $conf = new ConfigManager($confFile);
311 $updater = new LegacyUpdater(array(), array(), $conf, true);
312
313 $this->assertFalse($conf->exists('api.enabled'));
314 $this->assertFalse($conf->exists('api.secret'));
315 $updater->updateMethodApiSettings();
316 $conf->reload();
317 $this->assertTrue($conf->get('api.enabled'));
318 $this->assertTrue($conf->exists('api.secret'));
319 unlink($confFile .'.json.php');
320 }
321
322 /**
323 * Test updateMethodApiSettings(): already set, do nothing.
324 */
325 public function testUpdateApiSettingsNothingToDo()
326 {
327 $confFile = 'sandbox/config';
328 copy(self::$configFile .'.json.php', $confFile .'.json.php');
329 $conf = new ConfigManager($confFile);
330 $conf->set('api.enabled', false);
331 $conf->set('api.secret', '');
332 $updater = new LegacyUpdater(array(), array(), $conf, true);
333 $updater->updateMethodApiSettings();
334 $this->assertFalse($conf->get('api.enabled'));
335 $this->assertEmpty($conf->get('api.secret'));
336 unlink($confFile .'.json.php');
337 }
338
339 /**
340 * Test updateMethodDatastoreIds().
341 */
342 public function testDatastoreIds()
343 {
344 $links = array(
345 '20121206_182539' => array(
346 'linkdate' => '20121206_182539',
347 'title' => 'Geek and Poke',
348 'url' => 'http://geek-and-poke.com/',
349 'description' => 'desc',
350 'tags' => 'dev cartoon tag1 tag2 tag3 tag4 ',
351 'updated' => '20121206_190301',
352 'private' => false,
353 ),
354 '20121206_172539' => array(
355 'linkdate' => '20121206_172539',
356 'title' => 'UserFriendly - Samba',
357 'url' => 'http://ars.userfriendly.org/cartoons/?id=20010306',
358 'description' => '',
359 'tags' => 'samba cartoon web',
360 'private' => false,
361 ),
362 '20121206_142300' => array(
363 'linkdate' => '20121206_142300',
364 'title' => 'UserFriendly - Web Designer',
365 'url' => 'http://ars.userfriendly.org/cartoons/?id=20121206',
366 'description' => 'Naming conventions... #private',
367 'tags' => 'samba cartoon web',
368 'private' => true,
369 ),
370 );
371 $refDB = new \ReferenceLinkDB(true);
372 $refDB->setLinks($links);
373 $refDB->write(self::$testDatastore);
374 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
375
376 $checksum = hash_file('sha1', self::$testDatastore);
377
378 $this->conf->set('resource.data_dir', 'sandbox');
379 $this->conf->set('resource.datastore', self::$testDatastore);
380
381 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
382 $this->assertTrue($updater->updateMethodDatastoreIds());
383
384 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
385
386 $backupFiles = glob($this->conf->get('resource.data_dir') . '/datastore.'. date('YmdH') .'*.php');
387 $backup = null;
388 foreach ($backupFiles as $backupFile) {
389 if (strpos($backupFile, '_1') === false) {
390 $backup = $backupFile;
391 }
392 }
393 $this->assertNotNull($backup);
394 $this->assertFileExists($backup);
395 $this->assertEquals($checksum, hash_file('sha1', $backup));
396 unlink($backup);
397
398 $this->assertEquals(3, count($linkDB));
399 $this->assertTrue(isset($linkDB[0]));
400 $this->assertFalse(isset($linkDB[0]['linkdate']));
401 $this->assertEquals(0, $linkDB[0]['id']);
402 $this->assertEquals('UserFriendly - Web Designer', $linkDB[0]['title']);
403 $this->assertEquals('http://ars.userfriendly.org/cartoons/?id=20121206', $linkDB[0]['url']);
404 $this->assertEquals('Naming conventions... #private', $linkDB[0]['description']);
405 $this->assertEquals('samba cartoon web', $linkDB[0]['tags']);
406 $this->assertTrue($linkDB[0]['private']);
407 $this->assertEquals(
408 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_142300'),
409 $linkDB[0]['created']
410 );
411
412 $this->assertTrue(isset($linkDB[1]));
413 $this->assertFalse(isset($linkDB[1]['linkdate']));
414 $this->assertEquals(1, $linkDB[1]['id']);
415 $this->assertEquals('UserFriendly - Samba', $linkDB[1]['title']);
416 $this->assertEquals(
417 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_172539'),
418 $linkDB[1]['created']
419 );
420
421 $this->assertTrue(isset($linkDB[2]));
422 $this->assertFalse(isset($linkDB[2]['linkdate']));
423 $this->assertEquals(2, $linkDB[2]['id']);
424 $this->assertEquals('Geek and Poke', $linkDB[2]['title']);
425 $this->assertEquals(
426 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_182539'),
427 $linkDB[2]['created']
428 );
429 $this->assertEquals(
430 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_190301'),
431 $linkDB[2]['updated']
432 );
433 }
434
435 /**
436 * Test updateMethodDatastoreIds() with the update already applied: nothing to do.
437 */
438 public function testDatastoreIdsNothingToDo()
439 {
440 $refDB = new \ReferenceLinkDB(true);
441 $refDB->write(self::$testDatastore);
442 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
443
444 $this->conf->set('resource.data_dir', 'sandbox');
445 $this->conf->set('resource.datastore', self::$testDatastore);
446
447 $checksum = hash_file('sha1', self::$testDatastore);
448 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
449 $this->assertTrue($updater->updateMethodDatastoreIds());
450 $this->assertEquals($checksum, hash_file('sha1', self::$testDatastore));
451 }
452
453 /**
454 * Test defaultTheme update with default settings: nothing to do.
455 */
456 public function testDefaultThemeWithDefaultSettings()
457 {
458 $sandbox = 'sandbox/config';
459 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
460 $this->conf = new ConfigManager($sandbox);
461 $updater = new LegacyUpdater([], [], $this->conf, true);
462 $this->assertTrue($updater->updateMethodDefaultTheme());
463
464 $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
465 $this->assertEquals('default', $this->conf->get('resource.theme'));
466 $this->conf = new ConfigManager($sandbox);
467 $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
468 $this->assertEquals('default', $this->conf->get('resource.theme'));
469 unlink($sandbox . '.json.php');
470 }
471
472 /**
473 * Test defaultTheme update with a custom theme in a subfolder
474 */
475 public function testDefaultThemeWithCustomTheme()
476 {
477 $theme = 'iamanartist';
478 $sandbox = 'sandbox/config';
479 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
480 $this->conf = new ConfigManager($sandbox);
481 mkdir('sandbox/'. $theme);
482 touch('sandbox/'. $theme .'/linklist.html');
483 $this->conf->set('resource.raintpl_tpl', 'sandbox/'. $theme .'/');
484 $updater = new LegacyUpdater([], [], $this->conf, true);
485 $this->assertTrue($updater->updateMethodDefaultTheme());
486
487 $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
488 $this->assertEquals($theme, $this->conf->get('resource.theme'));
489 $this->conf = new ConfigManager($sandbox);
490 $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
491 $this->assertEquals($theme, $this->conf->get('resource.theme'));
492 unlink($sandbox . '.json.php');
493 unlink('sandbox/'. $theme .'/linklist.html');
494 rmdir('sandbox/'. $theme);
495 }
496
497 /**
498 * Test updateMethodEscapeMarkdown with markdown plugin enabled
499 * => setting markdown_escape set to false.
500 */
501 public function testEscapeMarkdownSettingToFalse()
502 {
503 $sandboxConf = 'sandbox/config';
504 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
505 $this->conf = new ConfigManager($sandboxConf);
506
507 $this->conf->set('general.enabled_plugins', ['markdown']);
508 $updater = new LegacyUpdater([], [], $this->conf, true);
509 $this->assertTrue($updater->updateMethodEscapeMarkdown());
510 $this->assertFalse($this->conf->get('security.markdown_escape'));
511
512 // reload from file
513 $this->conf = new ConfigManager($sandboxConf);
514 $this->assertFalse($this->conf->get('security.markdown_escape'));
515 }
516
517
518 /**
519 * Test updateMethodEscapeMarkdown with markdown plugin disabled
520 * => setting markdown_escape set to true.
521 */
522 public function testEscapeMarkdownSettingToTrue()
523 {
524 $sandboxConf = 'sandbox/config';
525 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
526 $this->conf = new ConfigManager($sandboxConf);
527
528 $this->conf->set('general.enabled_plugins', []);
529 $updater = new LegacyUpdater([], [], $this->conf, true);
530 $this->assertTrue($updater->updateMethodEscapeMarkdown());
531 $this->assertTrue($this->conf->get('security.markdown_escape'));
532
533 // reload from file
534 $this->conf = new ConfigManager($sandboxConf);
535 $this->assertTrue($this->conf->get('security.markdown_escape'));
536 }
537
538 /**
539 * Test updateMethodEscapeMarkdown with nothing to do (setting already enabled)
540 */
541 public function testEscapeMarkdownSettingNothingToDoEnabled()
542 {
543 $sandboxConf = 'sandbox/config';
544 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
545 $this->conf = new ConfigManager($sandboxConf);
546 $this->conf->set('security.markdown_escape', true);
547 $updater = new LegacyUpdater([], [], $this->conf, true);
548 $this->assertTrue($updater->updateMethodEscapeMarkdown());
549 $this->assertTrue($this->conf->get('security.markdown_escape'));
550 }
551
552 /**
553 * Test updateMethodEscapeMarkdown with nothing to do (setting already disabled)
554 */
555 public function testEscapeMarkdownSettingNothingToDoDisabled()
556 {
557 $this->conf->set('security.markdown_escape', false);
558 $updater = new LegacyUpdater([], [], $this->conf, true);
559 $this->assertTrue($updater->updateMethodEscapeMarkdown());
560 $this->assertFalse($this->conf->get('security.markdown_escape'));
561 }
562
563 /**
564 * Test updateMethodPiwikUrl with valid data
565 */
566 public function testUpdatePiwikUrlValid()
567 {
568 $sandboxConf = 'sandbox/config';
569 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
570 $this->conf = new ConfigManager($sandboxConf);
571 $url = 'mypiwik.tld';
572 $this->conf->set('plugins.PIWIK_URL', $url);
573 $updater = new LegacyUpdater([], [], $this->conf, true);
574 $this->assertTrue($updater->updateMethodPiwikUrl());
575 $this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
576
577 // reload from file
578 $this->conf = new ConfigManager($sandboxConf);
579 $this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
580 }
581
582 /**
583 * Test updateMethodPiwikUrl without setting
584 */
585 public function testUpdatePiwikUrlEmpty()
586 {
587 $updater = new LegacyUpdater([], [], $this->conf, true);
588 $this->assertTrue($updater->updateMethodPiwikUrl());
589 $this->assertEmpty($this->conf->get('plugins.PIWIK_URL'));
590 }
591
592 /**
593 * Test updateMethodPiwikUrl: valid URL, nothing to do
594 */
595 public function testUpdatePiwikUrlNothingToDo()
596 {
597 $url = 'https://mypiwik.tld';
598 $this->conf->set('plugins.PIWIK_URL', $url);
599 $updater = new LegacyUpdater([], [], $this->conf, true);
600 $this->assertTrue($updater->updateMethodPiwikUrl());
601 $this->assertEquals($url, $this->conf->get('plugins.PIWIK_URL'));
602 }
603
604 /**
605 * Test updateMethodAtomDefault with show_atom set to false
606 * => update to true.
607 */
608 public function testUpdateMethodAtomDefault()
609 {
610 $sandboxConf = 'sandbox/config';
611 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
612 $this->conf = new ConfigManager($sandboxConf);
613 $this->conf->set('feed.show_atom', false);
614 $updater = new LegacyUpdater([], [], $this->conf, true);
615 $this->assertTrue($updater->updateMethodAtomDefault());
616 $this->assertTrue($this->conf->get('feed.show_atom'));
617 // reload from file
618 $this->conf = new ConfigManager($sandboxConf);
619 $this->assertTrue($this->conf->get('feed.show_atom'));
620 }
621 /**
622 * Test updateMethodAtomDefault with show_atom not set.
623 * => nothing to do
624 */
625 public function testUpdateMethodAtomDefaultNoExist()
626 {
627 $sandboxConf = 'sandbox/config';
628 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
629 $this->conf = new ConfigManager($sandboxConf);
630 $updater = new LegacyUpdater([], [], $this->conf, true);
631 $this->assertTrue($updater->updateMethodAtomDefault());
632 $this->assertTrue($this->conf->get('feed.show_atom'));
633 }
634 /**
635 * Test updateMethodAtomDefault with show_atom set to true.
636 * => nothing to do
637 */
638 public function testUpdateMethodAtomDefaultAlreadyTrue()
639 {
640 $sandboxConf = 'sandbox/config';
641 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
642 $this->conf = new ConfigManager($sandboxConf);
643 $this->conf->set('feed.show_atom', true);
644 $updater = new LegacyUpdater([], [], $this->conf, true);
645 $this->assertTrue($updater->updateMethodAtomDefault());
646 $this->assertTrue($this->conf->get('feed.show_atom'));
647 }
648
649 /**
650 * Test updateMethodDownloadSizeAndTimeoutConf, it should be set if none is already defined.
651 */
652 public function testUpdateMethodDownloadSizeAndTimeoutConf()
653 {
654 $sandboxConf = 'sandbox/config';
655 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
656 $this->conf = new ConfigManager($sandboxConf);
657 $updater = new LegacyUpdater([], [], $this->conf, true);
658 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
659 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
660 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
661
662 $this->conf = new ConfigManager($sandboxConf);
663 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
664 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
665 }
666
667 /**
668 * Test updateMethodDownloadSizeAndTimeoutConf, it shouldn't be set if it is already defined.
669 */
670 public function testUpdateMethodDownloadSizeAndTimeoutConfIgnore()
671 {
672 $sandboxConf = 'sandbox/config';
673 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
674 $this->conf = new ConfigManager($sandboxConf);
675 $this->conf->set('general.download_max_size', 38);
676 $this->conf->set('general.download_timeout', 70);
677 $updater = new LegacyUpdater([], [], $this->conf, true);
678 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
679 $this->assertEquals(38, $this->conf->get('general.download_max_size'));
680 $this->assertEquals(70, $this->conf->get('general.download_timeout'));
681 }
682
683 /**
684 * Test updateMethodDownloadSizeAndTimeoutConf, only the maz size should be set here.
685 */
686 public function testUpdateMethodDownloadSizeAndTimeoutConfOnlySize()
687 {
688 $sandboxConf = 'sandbox/config';
689 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
690 $this->conf = new ConfigManager($sandboxConf);
691 $this->conf->set('general.download_max_size', 38);
692 $updater = new LegacyUpdater([], [], $this->conf, true);
693 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
694 $this->assertEquals(38, $this->conf->get('general.download_max_size'));
695 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
696 }
697
698 /**
699 * Test updateMethodDownloadSizeAndTimeoutConf, only the time out should be set here.
700 */
701 public function testUpdateMethodDownloadSizeAndTimeoutConfOnlyTimeout()
702 {
703 $sandboxConf = 'sandbox/config';
704 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
705 $this->conf = new ConfigManager($sandboxConf);
706 $this->conf->set('general.download_timeout', 3);
707 $updater = new LegacyUpdater([], [], $this->conf, true);
708 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
709 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
710 $this->assertEquals(3, $this->conf->get('general.download_timeout'));
711 }
712
713 /**
714 * Test updateMethodWebThumbnailer with thumbnails enabled.
715 */
716 public function testUpdateMethodWebThumbnailerEnabled()
717 {
718 $this->conf->remove('thumbnails');
719 $this->conf->set('thumbnail.enable_thumbnails', true);
720 $updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
721 $this->assertTrue($updater->updateMethodWebThumbnailer());
722 $this->assertFalse($this->conf->exists('thumbnail'));
723 $this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode'));
724 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
725 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
726 $this->assertContainsPolyfill('You have enabled or changed thumbnails', $_SESSION['warnings'][0]);
727 }
728
729 /**
730 * Test updateMethodWebThumbnailer with thumbnails disabled.
731 */
732 public function testUpdateMethodWebThumbnailerDisabled()
733 {
734 if (isset($_SESSION['warnings'])) {
735 unset($_SESSION['warnings']);
736 }
737
738 $this->conf->remove('thumbnails');
739 $this->conf->set('thumbnail.enable_thumbnails', false);
740 $updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
741 $this->assertTrue($updater->updateMethodWebThumbnailer());
742 $this->assertFalse($this->conf->exists('thumbnail'));
743 $this->assertEquals(Thumbnailer::MODE_NONE, $this->conf->get('thumbnails.mode'));
744 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
745 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
746 $this->assertTrue(empty($_SESSION['warnings']));
747 }
748
749 /**
750 * Test updateMethodWebThumbnailer with thumbnails disabled.
751 */
752 public function testUpdateMethodWebThumbnailerNothingToDo()
753 {
754 if (isset($_SESSION['warnings'])) {
755 unset($_SESSION['warnings']);
756 }
757
758 $updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
759 $this->assertTrue($updater->updateMethodWebThumbnailer());
760 $this->assertFalse($this->conf->exists('thumbnail'));
761 $this->assertEquals(Thumbnailer::MODE_COMMON, $this->conf->get('thumbnails.mode'));
762 $this->assertEquals(90, $this->conf->get('thumbnails.width'));
763 $this->assertEquals(53, $this->conf->get('thumbnails.height'));
764 $this->assertTrue(empty($_SESSION['warnings']));
765 }
766
767 /**
768 * Test updateMethodSetSticky().
769 */
770 public function testUpdateStickyValid()
771 {
772 $blank = [
773 'id' => 1,
774 'url' => 'z',
775 'title' => '',
776 'description' => '',
777 'tags' => '',
778 'created' => new DateTime(),
779 ];
780 $links = [
781 1 => ['id' => 1] + $blank,
782 2 => ['id' => 2] + $blank,
783 ];
784 $refDB = new \ReferenceLinkDB(true);
785 $refDB->setLinks($links);
786 $refDB->write(self::$testDatastore);
787 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
788
789 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
790 $this->assertTrue($updater->updateMethodSetSticky());
791
792 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
793 foreach ($linkDB as $link) {
794 $this->assertFalse($link['sticky']);
795 }
796 }
797
798 /**
799 * Test updateMethodSetSticky().
800 */
801 public function testUpdateStickyNothingToDo()
802 {
803 $blank = [
804 'id' => 1,
805 'url' => 'z',
806 'title' => '',
807 'description' => '',
808 'tags' => '',
809 'created' => new DateTime(),
810 ];
811 $links = [
812 1 => ['id' => 1, 'sticky' => true] + $blank,
813 2 => ['id' => 2] + $blank,
814 ];
815 $refDB = new \ReferenceLinkDB(true);
816 $refDB->setLinks($links);
817 $refDB->write(self::$testDatastore);
818 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
819
820 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
821 $this->assertTrue($updater->updateMethodSetSticky());
822
823 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
824 $this->assertTrue($linkDB[1]['sticky']);
825 }
826
827 /**
828 * Test updateMethodRemoveRedirector().
829 */
830 public function testUpdateRemoveRedirector()
831 {
832 $sandboxConf = 'sandbox/config';
833 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
834 $this->conf = new ConfigManager($sandboxConf);
835 $updater = new LegacyUpdater([], null, $this->conf, true);
836 $this->assertTrue($updater->updateMethodRemoveRedirector());
837 $this->assertFalse($this->conf->exists('redirector'));
838 $this->conf = new ConfigManager($sandboxConf);
839 $this->assertFalse($this->conf->exists('redirector'));
840 }
841
842 /**
843 * Test updateMethodFormatterSetting()
844 */
845 public function testUpdateMethodFormatterSettingDefault()
846 {
847 $sandboxConf = 'sandbox/config';
848 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
849 $this->conf = new ConfigManager($sandboxConf);
850 $this->conf->set('formatter', 'default');
851 $updater = new LegacyUpdater([], null, $this->conf, true);
852 $enabledPlugins = $this->conf->get('general.enabled_plugins');
853 $this->assertFalse(in_array('markdown', $enabledPlugins));
854 $this->assertTrue($updater->updateMethodFormatterSetting());
855 $this->assertEquals('default', $this->conf->get('formatter'));
856 $this->assertEquals($enabledPlugins, $this->conf->get('general.enabled_plugins'));
857
858 $this->conf = new ConfigManager($sandboxConf);
859 $this->assertEquals('default', $this->conf->get('formatter'));
860 $this->assertEquals($enabledPlugins, $this->conf->get('general.enabled_plugins'));
861 }
862
863 /**
864 * Test updateMethodFormatterSetting()
865 */
866 public function testUpdateMethodFormatterSettingMarkdown()
867 {
868 $sandboxConf = 'sandbox/config';
869 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
870 $this->conf = new ConfigManager($sandboxConf);
871 $this->conf->set('formatter', 'default');
872 $updater = new LegacyUpdater([], null, $this->conf, true);
873 $enabledPlugins = $this->conf->get('general.enabled_plugins');
874 $enabledPlugins[] = 'markdown';
875 $this->conf->set('general.enabled_plugins', $enabledPlugins);
876
877 $this->assertTrue(in_array('markdown', $this->conf->get('general.enabled_plugins')));
878 $this->assertTrue($updater->updateMethodFormatterSetting());
879 $this->assertEquals('markdown', $this->conf->get('formatter'));
880 $this->assertFalse(in_array('markdown', $this->conf->get('general.enabled_plugins')));
881
882 $this->conf = new ConfigManager($sandboxConf);
883 $this->assertEquals('markdown', $this->conf->get('formatter'));
884 $this->assertFalse(in_array('markdown', $this->conf->get('general.enabled_plugins')));
885 }
886}
diff --git a/tests/netscape/BookmarkExportTest.php b/tests/netscape/BookmarkExportTest.php
index 6de9876d..9b95ccc9 100644
--- a/tests/netscape/BookmarkExportTest.php
+++ b/tests/netscape/BookmarkExportTest.php
@@ -1,14 +1,20 @@
1<?php 1<?php
2
2namespace Shaarli\Netscape; 3namespace Shaarli\Netscape;
3 4
4use Shaarli\Bookmark\LinkDB; 5use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Config\ConfigManager;
7use Shaarli\Formatter\BookmarkFormatter;
8use Shaarli\Formatter\FormatterFactory;
9use Shaarli\History;
10use Shaarli\TestCase;
5 11
6require_once 'tests/utils/ReferenceLinkDB.php'; 12require_once 'tests/utils/ReferenceLinkDB.php';
7 13
8/** 14/**
9 * Netscape bookmark export 15 * Netscape bookmark export
10 */ 16 */
11class BookmarkExportTest extends \PHPUnit\Framework\TestCase 17class BookmarkExportTest extends TestCase
12{ 18{
13 /** 19 /**
14 * @var string datastore to test write operations 20 * @var string datastore to test write operations
@@ -16,41 +22,86 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
16 protected static $testDatastore = 'sandbox/datastore.php'; 22 protected static $testDatastore = 'sandbox/datastore.php';
17 23
18 /** 24 /**
25 * @var ConfigManager instance.
26 */
27 protected static $conf;
28
29 /**
19 * @var \ReferenceLinkDB instance. 30 * @var \ReferenceLinkDB instance.
20 */ 31 */
21 protected static $refDb = null; 32 protected static $refDb = null;
22 33
23 /** 34 /**
24 * @var LinkDB private LinkDB instance. 35 * @var BookmarkFileService private instance.
36 */
37 protected static $bookmarkService = null;
38
39 /**
40 * @var BookmarkFormatter instance
41 */
42 protected static $formatter;
43
44 /**
45 * @var History instance
46 */
47 protected static $history;
48
49 /**
50 * @var NetscapeBookmarkUtils
25 */ 51 */
26 protected static $linkDb = null; 52 protected $netscapeBookmarkUtils;
27 53
28 /** 54 /**
29 * Instantiate reference data 55 * Instantiate reference data
30 */ 56 */
31 public static function setUpBeforeClass() 57 public static function setUpBeforeClass(): void
32 { 58 {
33 self::$refDb = new \ReferenceLinkDB(); 59 static::$conf = new ConfigManager('tests/utils/config/configJson');
34 self::$refDb->write(self::$testDatastore); 60 static::$conf->set('resource.datastore', static::$testDatastore);
35 self::$linkDb = new LinkDB(self::$testDatastore, true, false); 61 static::$refDb = new \ReferenceLinkDB();
62 static::$refDb->write(static::$testDatastore);
63 static::$history = new History('sandbox/history.php');
64 static::$bookmarkService = new BookmarkFileService(static::$conf, static::$history, true);
65 $factory = new FormatterFactory(static::$conf, true);
66 static::$formatter = $factory->getFormatter('raw');
67 }
68
69 public function setUp(): void
70 {
71 $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils(
72 static::$bookmarkService,
73 static::$conf,
74 static::$history
75 );
36 } 76 }
37 77
38 /** 78 /**
39 * Attempt to export an invalid link selection 79 * Attempt to export an invalid link selection
40 * @expectedException Exception
41 * @expectedExceptionMessageRegExp /Invalid export selection/
42 */ 80 */
43 public function testFilterAndFormatInvalid() 81 public function testFilterAndFormatInvalid()
44 { 82 {
45 NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'derp', false, ''); 83 $this->expectException(\Exception::class);
84 $this->expectExceptionMessageRegExp('/Invalid export selection/');
85
86 $this->netscapeBookmarkUtils->filterAndFormat(
87 self::$formatter,
88 'derp',
89 false,
90 ''
91 );
46 } 92 }
47 93
48 /** 94 /**
49 * Prepare all links for export 95 * Prepare all bookmarks for export
50 */ 96 */
51 public function testFilterAndFormatAll() 97 public function testFilterAndFormatAll()
52 { 98 {
53 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'all', false, ''); 99 $links = $this->netscapeBookmarkUtils->filterAndFormat(
100 self::$formatter,
101 'all',
102 false,
103 ''
104 );
54 $this->assertEquals(self::$refDb->countLinks(), sizeof($links)); 105 $this->assertEquals(self::$refDb->countLinks(), sizeof($links));
55 foreach ($links as $link) { 106 foreach ($links as $link) {
56 $date = $link['created']; 107 $date = $link['created'];
@@ -66,11 +117,16 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
66 } 117 }
67 118
68 /** 119 /**
69 * Prepare private links for export 120 * Prepare private bookmarks for export
70 */ 121 */
71 public function testFilterAndFormatPrivate() 122 public function testFilterAndFormatPrivate()
72 { 123 {
73 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'private', false, ''); 124 $links = $this->netscapeBookmarkUtils->filterAndFormat(
125 self::$formatter,
126 'private',
127 false,
128 ''
129 );
74 $this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links)); 130 $this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links));
75 foreach ($links as $link) { 131 foreach ($links as $link) {
76 $date = $link['created']; 132 $date = $link['created'];
@@ -86,11 +142,16 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
86 } 142 }
87 143
88 /** 144 /**
89 * Prepare public links for export 145 * Prepare public bookmarks for export
90 */ 146 */
91 public function testFilterAndFormatPublic() 147 public function testFilterAndFormatPublic()
92 { 148 {
93 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, ''); 149 $links = $this->netscapeBookmarkUtils->filterAndFormat(
150 self::$formatter,
151 'public',
152 false,
153 ''
154 );
94 $this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links)); 155 $this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links));
95 foreach ($links as $link) { 156 foreach ($links as $link) {
96 $date = $link['created']; 157 $date = $link['created'];
@@ -110,9 +171,14 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
110 */ 171 */
111 public function testFilterAndFormatDoNotPrependNoteUrl() 172 public function testFilterAndFormatDoNotPrependNoteUrl()
112 { 173 {
113 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, ''); 174 $links = $this->netscapeBookmarkUtils->filterAndFormat(
175 self::$formatter,
176 'public',
177 false,
178 ''
179 );
114 $this->assertEquals( 180 $this->assertEquals(
115 '?WDWyig', 181 '/shaare/WDWyig',
116 $links[2]['url'] 182 $links[2]['url']
117 ); 183 );
118 } 184 }
@@ -123,14 +189,14 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
123 public function testFilterAndFormatPrependNoteUrl() 189 public function testFilterAndFormatPrependNoteUrl()
124 { 190 {
125 $indexUrl = 'http://localhost:7469/shaarli/'; 191 $indexUrl = 'http://localhost:7469/shaarli/';
126 $links = NetscapeBookmarkUtils::filterAndFormat( 192 $links = $this->netscapeBookmarkUtils->filterAndFormat(
127 self::$linkDb, 193 self::$formatter,
128 'public', 194 'public',
129 true, 195 true,
130 $indexUrl 196 $indexUrl
131 ); 197 );
132 $this->assertEquals( 198 $this->assertEquals(
133 $indexUrl . '?WDWyig', 199 $indexUrl . 'shaare/WDWyig',
134 $links[2]['url'] 200 $links[2]['url']
135 ); 201 );
136 } 202 }
diff --git a/tests/netscape/BookmarkImportTest.php b/tests/netscape/BookmarkImportTest.php
index ccafc161..c1e49b5f 100644
--- a/tests/netscape/BookmarkImportTest.php
+++ b/tests/netscape/BookmarkImportTest.php
@@ -1,26 +1,31 @@
1<?php 1<?php
2
2namespace Shaarli\Netscape; 3namespace Shaarli\Netscape;
3 4
4use DateTime; 5use DateTime;
5use Shaarli\Bookmark\LinkDB; 6use Psr\Http\Message\UploadedFileInterface;
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\BookmarkFileService;
9use Shaarli\Bookmark\BookmarkFilter;
6use Shaarli\Config\ConfigManager; 10use Shaarli\Config\ConfigManager;
7use Shaarli\History; 11use Shaarli\History;
12use Shaarli\TestCase;
13use Slim\Http\UploadedFile;
8 14
9/** 15/**
10 * Utility function to load a file's metadata in a $_FILES-like array 16 * Utility function to load a file's metadata in a $_FILES-like array
11 * 17 *
12 * @param string $filename Basename of the file 18 * @param string $filename Basename of the file
13 * 19 *
14 * @return array A $_FILES-like array 20 * @return UploadedFileInterface Upload file in PSR-7 compatible object
15 */ 21 */
16function file2array($filename) 22function file2array($filename)
17{ 23{
18 return array( 24 return new UploadedFile(
19 'filetoupload' => array( 25 __DIR__ . '/input/' . $filename,
20 'name' => $filename, 26 $filename,
21 'tmp_name' => __DIR__ . '/input/' . $filename, 27 null,
22 'size' => filesize(__DIR__ . '/input/' . $filename) 28 filesize(__DIR__ . '/input/' . $filename)
23 )
24 ); 29 );
25} 30}
26 31
@@ -28,7 +33,7 @@ function file2array($filename)
28/** 33/**
29 * Netscape bookmark import 34 * Netscape bookmark import
30 */ 35 */
31class BookmarkImportTest extends \PHPUnit\Framework\TestCase 36class BookmarkImportTest extends TestCase
32{ 37{
33 /** 38 /**
34 * @var string datastore to test write operations 39 * @var string datastore to test write operations
@@ -41,9 +46,9 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
41 protected static $historyFilePath = 'sandbox/history.php'; 46 protected static $historyFilePath = 'sandbox/history.php';
42 47
43 /** 48 /**
44 * @var LinkDB private LinkDB instance 49 * @var BookmarkFileService private LinkDB instance
45 */ 50 */
46 protected $linkDb = null; 51 protected $bookmarkService = null;
47 52
48 /** 53 /**
49 * @var string Dummy page cache 54 * @var string Dummy page cache
@@ -61,11 +66,16 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
61 protected $history; 66 protected $history;
62 67
63 /** 68 /**
69 * @var NetscapeBookmarkUtils
70 */
71 protected $netscapeBookmarkUtils;
72
73 /**
64 * @var string Save the current timezone. 74 * @var string Save the current timezone.
65 */ 75 */
66 protected static $defaultTimeZone; 76 protected static $defaultTimeZone;
67 77
68 public static function setUpBeforeClass() 78 public static function setUpBeforeClass(): void
69 { 79 {
70 self::$defaultTimeZone = date_default_timezone_get(); 80 self::$defaultTimeZone = date_default_timezone_get();
71 // Timezone without DST for test consistency 81 // Timezone without DST for test consistency
@@ -75,28 +85,31 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
75 /** 85 /**
76 * Resets test data before each test 86 * Resets test data before each test
77 */ 87 */
78 protected function setUp() 88 protected function setUp(): void
79 { 89 {
80 if (file_exists(self::$testDatastore)) { 90 if (file_exists(self::$testDatastore)) {
81 unlink(self::$testDatastore); 91 unlink(self::$testDatastore);
82 } 92 }
83 // start with an empty datastore 93 // start with an empty datastore
84 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>'); 94 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>');
85 $this->linkDb = new LinkDB(self::$testDatastore, true, false); 95
86 $this->conf = new ConfigManager('tests/utils/config/configJson'); 96 $this->conf = new ConfigManager('tests/utils/config/configJson');
87 $this->conf->set('resource.page_cache', $this->pagecache); 97 $this->conf->set('resource.page_cache', $this->pagecache);
98 $this->conf->set('resource.datastore', self::$testDatastore);
88 $this->history = new History(self::$historyFilePath); 99 $this->history = new History(self::$historyFilePath);
100 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
101 $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history);
89 } 102 }
90 103
91 /** 104 /**
92 * Delete history file. 105 * Delete history file.
93 */ 106 */
94 public function tearDown() 107 protected function tearDown(): void
95 { 108 {
96 @unlink(self::$historyFilePath); 109 @unlink(self::$historyFilePath);
97 } 110 }
98 111
99 public static function tearDownAfterClass() 112 public static function tearDownAfterClass(): void
100 { 113 {
101 date_default_timezone_set(self::$defaultTimeZone); 114 date_default_timezone_set(self::$defaultTimeZone);
102 } 115 }
@@ -110,9 +123,9 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
110 $this->assertEquals( 123 $this->assertEquals(
111 'File empty.htm (0 bytes) has an unknown file format.' 124 'File empty.htm (0 bytes) has an unknown file format.'
112 .' Nothing was imported.', 125 .' Nothing was imported.',
113 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) 126 $this->netscapeBookmarkUtils->import(null, $files)
114 ); 127 );
115 $this->assertEquals(0, count($this->linkDb)); 128 $this->assertEquals(0, $this->bookmarkService->count());
116 } 129 }
117 130
118 /** 131 /**
@@ -123,9 +136,9 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
123 $files = file2array('no_doctype.htm'); 136 $files = file2array('no_doctype.htm');
124 $this->assertEquals( 137 $this->assertEquals(
125 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.', 138 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
126 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) 139 $this->netscapeBookmarkUtils->import(null, $files)
127 ); 140 );
128 $this->assertEquals(0, count($this->linkDb)); 141 $this->assertEquals(0, $this->bookmarkService->count());
129 } 142 }
130 143
131 /** 144 /**
@@ -136,10 +149,10 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
136 $files = file2array('lowercase_doctype.htm'); 149 $files = file2array('lowercase_doctype.htm');
137 $this->assertStringMatchesFormat( 150 $this->assertStringMatchesFormat(
138 'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:' 151 'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:'
139 .' 2 links imported, 0 links overwritten, 0 links skipped.', 152 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
140 NetscapeBookmarkUtils::import(null, $files, $this->linkDb, $this->conf, $this->history) 153 $this->netscapeBookmarkUtils->import(null, $files)
141 ); 154 );
142 $this->assertEquals(2, count($this->linkDb)); 155 $this->assertEquals(2, $this->bookmarkService->count());
143 } 156 }
144 157
145 158
@@ -151,25 +164,24 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
151 $files = file2array('internet_explorer_encoding.htm'); 164 $files = file2array('internet_explorer_encoding.htm');
152 $this->assertStringMatchesFormat( 165 $this->assertStringMatchesFormat(
153 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:' 166 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
154 .' 1 links imported, 0 links overwritten, 0 links skipped.', 167 .' 1 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
155 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) 168 $this->netscapeBookmarkUtils->import([], $files)
156 ); 169 );
157 $this->assertEquals(1, count($this->linkDb)); 170 $this->assertEquals(1, $this->bookmarkService->count());
158 $this->assertEquals(0, count_private($this->linkDb)); 171 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
159 172
173 $bookmark = $this->bookmarkService->findByUrl('http://hginit.com/');
174 $this->assertEquals(0, $bookmark->getId());
160 $this->assertEquals( 175 $this->assertEquals(
161 array( 176 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160618_203944'),
162 'id' => 0, 177 $bookmark->getCreated()
163 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160618_203944'),
164 'title' => 'Hg Init a Mercurial tutorial by Joel Spolsky',
165 'url' => 'http://hginit.com/',
166 'description' => '',
167 'private' => 0,
168 'tags' => '',
169 'shorturl' => 'La37cg',
170 ),
171 $this->linkDb->getLinkFromUrl('http://hginit.com/')
172 ); 178 );
179 $this->assertEquals('Hg Init a Mercurial tutorial by Joel Spolsky', $bookmark->getTitle());
180 $this->assertEquals('http://hginit.com/', $bookmark->getUrl());
181 $this->assertEquals('', $bookmark->getDescription());
182 $this->assertFalse($bookmark->isPrivate());
183 $this->assertEquals('', $bookmark->getTagsString());
184 $this->assertEquals('La37cg', $bookmark->getShortUrl());
173 } 185 }
174 186
175 /** 187 /**
@@ -180,116 +192,115 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
180 $files = file2array('netscape_nested.htm'); 192 $files = file2array('netscape_nested.htm');
181 $this->assertStringMatchesFormat( 193 $this->assertStringMatchesFormat(
182 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:' 194 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
183 .' 8 links imported, 0 links overwritten, 0 links skipped.', 195 .' 8 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
184 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) 196 $this->netscapeBookmarkUtils->import([], $files)
185 );
186 $this->assertEquals(8, count($this->linkDb));
187 $this->assertEquals(2, count_private($this->linkDb));
188
189 $this->assertEquals(
190 array(
191 'id' => 0,
192 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235541'),
193 'title' => 'Nested 1',
194 'url' => 'http://nest.ed/1',
195 'description' => '',
196 'private' => 0,
197 'tags' => 'tag1 tag2',
198 'shorturl' => 'KyDNKA',
199 ),
200 $this->linkDb->getLinkFromUrl('http://nest.ed/1')
201 );
202 $this->assertEquals(
203 array(
204 'id' => 1,
205 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235542'),
206 'title' => 'Nested 1-1',
207 'url' => 'http://nest.ed/1-1',
208 'description' => '',
209 'private' => 0,
210 'tags' => 'folder1 tag1 tag2',
211 'shorturl' => 'T2LnXg',
212 ),
213 $this->linkDb->getLinkFromUrl('http://nest.ed/1-1')
214 );
215 $this->assertEquals(
216 array(
217 'id' => 2,
218 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235547'),
219 'title' => 'Nested 1-2',
220 'url' => 'http://nest.ed/1-2',
221 'description' => '',
222 'private' => 0,
223 'tags' => 'folder1 tag3 tag4',
224 'shorturl' => '46SZxA',
225 ),
226 $this->linkDb->getLinkFromUrl('http://nest.ed/1-2')
227 );
228 $this->assertEquals(
229 array(
230 'id' => 3,
231 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'),
232 'title' => 'Nested 2-1',
233 'url' => 'http://nest.ed/2-1',
234 'description' => 'First link of the second section',
235 'private' => 1,
236 'tags' => 'folder2',
237 'shorturl' => '4UHOSw',
238 ),
239 $this->linkDb->getLinkFromUrl('http://nest.ed/2-1')
240 );
241 $this->assertEquals(
242 array(
243 'id' => 4,
244 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'),
245 'title' => 'Nested 2-2',
246 'url' => 'http://nest.ed/2-2',
247 'description' => 'Second link of the second section',
248 'private' => 1,
249 'tags' => 'folder2',
250 'shorturl' => 'yfzwbw',
251 ),
252 $this->linkDb->getLinkFromUrl('http://nest.ed/2-2')
253 );
254 $this->assertEquals(
255 array(
256 'id' => 5,
257 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'),
258 'title' => 'Nested 3-1',
259 'url' => 'http://nest.ed/3-1',
260 'description' => '',
261 'private' => 0,
262 'tags' => 'folder3 folder3-1 tag3',
263 'shorturl' => 'UwxIUQ',
264 ),
265 $this->linkDb->getLinkFromUrl('http://nest.ed/3-1')
266 );
267 $this->assertEquals(
268 array(
269 'id' => 6,
270 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'),
271 'title' => 'Nested 3-2',
272 'url' => 'http://nest.ed/3-2',
273 'description' => '',
274 'private' => 0,
275 'tags' => 'folder3 folder3-1',
276 'shorturl' => 'p8dyZg',
277 ),
278 $this->linkDb->getLinkFromUrl('http://nest.ed/3-2')
279 );
280 $this->assertEquals(
281 array(
282 'id' => 7,
283 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160229_111541'),
284 'title' => 'Nested 2',
285 'url' => 'http://nest.ed/2',
286 'description' => '',
287 'private' => 0,
288 'tags' => 'tag4',
289 'shorturl' => 'Gt3Uug',
290 ),
291 $this->linkDb->getLinkFromUrl('http://nest.ed/2')
292 ); 197 );
198 $this->assertEquals(8, $this->bookmarkService->count());
199 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
200
201 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/1');
202 $this->assertEquals(0, $bookmark->getId());
203 $this->assertEquals(
204 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235541'),
205 $bookmark->getCreated()
206 );
207 $this->assertEquals('Nested 1', $bookmark->getTitle());
208 $this->assertEquals('http://nest.ed/1', $bookmark->getUrl());
209 $this->assertEquals('', $bookmark->getDescription());
210 $this->assertFalse($bookmark->isPrivate());
211 $this->assertEquals('tag1 tag2', $bookmark->getTagsString());
212 $this->assertEquals('KyDNKA', $bookmark->getShortUrl());
213
214 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/1-1');
215 $this->assertEquals(1, $bookmark->getId());
216 $this->assertEquals(
217 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235542'),
218 $bookmark->getCreated()
219 );
220 $this->assertEquals('Nested 1-1', $bookmark->getTitle());
221 $this->assertEquals('http://nest.ed/1-1', $bookmark->getUrl());
222 $this->assertEquals('', $bookmark->getDescription());
223 $this->assertFalse($bookmark->isPrivate());
224 $this->assertEquals('folder1 tag1 tag2', $bookmark->getTagsString());
225 $this->assertEquals('T2LnXg', $bookmark->getShortUrl());
226
227 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/1-2');
228 $this->assertEquals(2, $bookmark->getId());
229 $this->assertEquals(
230 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235547'),
231 $bookmark->getCreated()
232 );
233 $this->assertEquals('Nested 1-2', $bookmark->getTitle());
234 $this->assertEquals('http://nest.ed/1-2', $bookmark->getUrl());
235 $this->assertEquals('', $bookmark->getDescription());
236 $this->assertFalse($bookmark->isPrivate());
237 $this->assertEquals('folder1 tag3 tag4', $bookmark->getTagsString());
238 $this->assertEquals('46SZxA', $bookmark->getShortUrl());
239
240 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/2-1');
241 $this->assertEquals(3, $bookmark->getId());
242 $this->assertEquals(
243 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160202_202222'),
244 $bookmark->getCreated()
245 );
246 $this->assertEquals('Nested 2-1', $bookmark->getTitle());
247 $this->assertEquals('http://nest.ed/2-1', $bookmark->getUrl());
248 $this->assertEquals('First link of the second section', $bookmark->getDescription());
249 $this->assertTrue($bookmark->isPrivate());
250 $this->assertEquals('folder2', $bookmark->getTagsString());
251 $this->assertEquals('4UHOSw', $bookmark->getShortUrl());
252
253 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/2-2');
254 $this->assertEquals(4, $bookmark->getId());
255 $this->assertEquals(
256 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160119_230227'),
257 $bookmark->getCreated()
258 );
259 $this->assertEquals('Nested 2-2', $bookmark->getTitle());
260 $this->assertEquals('http://nest.ed/2-2', $bookmark->getUrl());
261 $this->assertEquals('Second link of the second section', $bookmark->getDescription());
262 $this->assertTrue($bookmark->isPrivate());
263 $this->assertEquals('folder2', $bookmark->getTagsString());
264 $this->assertEquals('yfzwbw', $bookmark->getShortUrl());
265
266 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/3-1');
267 $this->assertEquals(5, $bookmark->getId());
268 $this->assertEquals(
269 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160202_202222'),
270 $bookmark->getCreated()
271 );
272 $this->assertEquals('Nested 3-1', $bookmark->getTitle());
273 $this->assertEquals('http://nest.ed/3-1', $bookmark->getUrl());
274 $this->assertEquals('', $bookmark->getDescription());
275 $this->assertFalse($bookmark->isPrivate());
276 $this->assertEquals('folder3 folder3-1 tag3', $bookmark->getTagsString());
277 $this->assertEquals('UwxIUQ', $bookmark->getShortUrl());
278
279 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/3-2');
280 $this->assertEquals(6, $bookmark->getId());
281 $this->assertEquals(
282 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160119_230227'),
283 $bookmark->getCreated()
284 );
285 $this->assertEquals('Nested 3-2', $bookmark->getTitle());
286 $this->assertEquals('http://nest.ed/3-2', $bookmark->getUrl());
287 $this->assertEquals('', $bookmark->getDescription());
288 $this->assertFalse($bookmark->isPrivate());
289 $this->assertEquals('folder3 folder3-1', $bookmark->getTagsString());
290 $this->assertEquals('p8dyZg', $bookmark->getShortUrl());
291
292 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/2');
293 $this->assertEquals(7, $bookmark->getId());
294 $this->assertEquals(
295 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160229_111541'),
296 $bookmark->getCreated()
297 );
298 $this->assertEquals('Nested 2', $bookmark->getTitle());
299 $this->assertEquals('http://nest.ed/2', $bookmark->getUrl());
300 $this->assertEquals('', $bookmark->getDescription());
301 $this->assertFalse($bookmark->isPrivate());
302 $this->assertEquals('tag4', $bookmark->getTagsString());
303 $this->assertEquals('Gt3Uug', $bookmark->getShortUrl());
293 } 304 }
294 305
295 /** 306 /**
@@ -302,40 +313,38 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
302 $files = file2array('netscape_basic.htm'); 313 $files = file2array('netscape_basic.htm');
303 $this->assertStringMatchesFormat( 314 $this->assertStringMatchesFormat(
304 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 315 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
305 .' 2 links imported, 0 links overwritten, 0 links skipped.', 316 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
306 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) 317 $this->netscapeBookmarkUtils->import([], $files)
307 ); 318 );
308 319
309 $this->assertEquals(2, count($this->linkDb)); 320 $this->assertEquals(2, $this->bookmarkService->count());
310 $this->assertEquals(1, count_private($this->linkDb)); 321 $this->assertEquals(1, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
311 322
323 $bookmark = $this->bookmarkService->findByUrl('https://private.tld');
324 $this->assertEquals(0, $bookmark->getId());
312 $this->assertEquals( 325 $this->assertEquals(
313 array( 326 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20001010_135536'),
314 'id' => 0, 327 $bookmark->getCreated()
315 // Old link - UTC+4 (note that TZ in the import file is ignored).
316 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20001010_135536'),
317 'title' => 'Secret stuff',
318 'url' => 'https://private.tld',
319 'description' => "Super-secret stuff you're not supposed to know about",
320 'private' => 1,
321 'tags' => 'private secret',
322 'shorturl' => 'EokDtA',
323 ),
324 $this->linkDb->getLinkFromUrl('https://private.tld')
325 ); 328 );
329 $this->assertEquals('Secret stuff', $bookmark->getTitle());
330 $this->assertEquals('https://private.tld', $bookmark->getUrl());
331 $this->assertEquals('Super-secret stuff you\'re not supposed to know about', $bookmark->getDescription());
332 $this->assertTrue($bookmark->isPrivate());
333 $this->assertEquals('private secret', $bookmark->getTagsString());
334 $this->assertEquals('EokDtA', $bookmark->getShortUrl());
335
336 $bookmark = $this->bookmarkService->findByUrl('http://public.tld');
337 $this->assertEquals(1, $bookmark->getId());
326 $this->assertEquals( 338 $this->assertEquals(
327 array( 339 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235548'),
328 'id' => 1, 340 $bookmark->getCreated()
329 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
330 'title' => 'Public stuff',
331 'url' => 'http://public.tld',
332 'description' => '',
333 'private' => 0,
334 'tags' => 'public hello world',
335 'shorturl' => 'Er9ddA',
336 ),
337 $this->linkDb->getLinkFromUrl('http://public.tld')
338 ); 341 );
342 $this->assertEquals('Public stuff', $bookmark->getTitle());
343 $this->assertEquals('http://public.tld', $bookmark->getUrl());
344 $this->assertEquals('', $bookmark->getDescription());
345 $this->assertFalse($bookmark->isPrivate());
346 $this->assertEquals('public hello world', $bookmark->getTagsString());
347 $this->assertEquals('Er9ddA', $bookmark->getShortUrl());
339 } 348 }
340 349
341 /** 350 /**
@@ -347,43 +356,42 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
347 $files = file2array('netscape_basic.htm'); 356 $files = file2array('netscape_basic.htm');
348 $this->assertStringMatchesFormat( 357 $this->assertStringMatchesFormat(
349 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 358 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
350 .' 2 links imported, 0 links overwritten, 0 links skipped.', 359 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
351 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 360 $this->netscapeBookmarkUtils->import($post, $files)
352 ); 361 );
353 $this->assertEquals(2, count($this->linkDb));
354 $this->assertEquals(1, count_private($this->linkDb));
355 362
363 $this->assertEquals(2, $this->bookmarkService->count());
364 $this->assertEquals(1, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
365
366 $bookmark = $this->bookmarkService->findByUrl('https://private.tld');
367 $this->assertEquals(0, $bookmark->getId());
356 $this->assertEquals( 368 $this->assertEquals(
357 array( 369 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20001010_135536'),
358 'id' => 0, 370 $bookmark->getCreated()
359 // Note that TZ in the import file is ignored.
360 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20001010_135536'),
361 'title' => 'Secret stuff',
362 'url' => 'https://private.tld',
363 'description' => "Super-secret stuff you're not supposed to know about",
364 'private' => 1,
365 'tags' => 'private secret',
366 'shorturl' => 'EokDtA',
367 ),
368 $this->linkDb->getLinkFromUrl('https://private.tld')
369 ); 371 );
372 $this->assertEquals('Secret stuff', $bookmark->getTitle());
373 $this->assertEquals('https://private.tld', $bookmark->getUrl());
374 $this->assertEquals('Super-secret stuff you\'re not supposed to know about', $bookmark->getDescription());
375 $this->assertTrue($bookmark->isPrivate());
376 $this->assertEquals('private secret', $bookmark->getTagsString());
377 $this->assertEquals('EokDtA', $bookmark->getShortUrl());
378
379 $bookmark = $this->bookmarkService->findByUrl('http://public.tld');
380 $this->assertEquals(1, $bookmark->getId());
370 $this->assertEquals( 381 $this->assertEquals(
371 array( 382 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235548'),
372 'id' => 1, 383 $bookmark->getCreated()
373 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
374 'title' => 'Public stuff',
375 'url' => 'http://public.tld',
376 'description' => '',
377 'private' => 0,
378 'tags' => 'public hello world',
379 'shorturl' => 'Er9ddA',
380 ),
381 $this->linkDb->getLinkFromUrl('http://public.tld')
382 ); 384 );
385 $this->assertEquals('Public stuff', $bookmark->getTitle());
386 $this->assertEquals('http://public.tld', $bookmark->getUrl());
387 $this->assertEquals('', $bookmark->getDescription());
388 $this->assertFalse($bookmark->isPrivate());
389 $this->assertEquals('public hello world', $bookmark->getTagsString());
390 $this->assertEquals('Er9ddA', $bookmark->getShortUrl());
383 } 391 }
384 392
385 /** 393 /**
386 * Import links as public 394 * Import bookmarks as public
387 */ 395 */
388 public function testImportAsPublic() 396 public function testImportAsPublic()
389 { 397 {
@@ -391,23 +399,17 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
391 $files = file2array('netscape_basic.htm'); 399 $files = file2array('netscape_basic.htm');
392 $this->assertStringMatchesFormat( 400 $this->assertStringMatchesFormat(
393 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 401 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
394 .' 2 links imported, 0 links overwritten, 0 links skipped.', 402 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
395 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 403 $this->netscapeBookmarkUtils->import($post, $files)
396 );
397 $this->assertEquals(2, count($this->linkDb));
398 $this->assertEquals(0, count_private($this->linkDb));
399 $this->assertEquals(
400 0,
401 $this->linkDb[0]['private']
402 );
403 $this->assertEquals(
404 0,
405 $this->linkDb[1]['private']
406 ); 404 );
405 $this->assertEquals(2, $this->bookmarkService->count());
406 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
407 $this->assertFalse($this->bookmarkService->get(0)->isPrivate());
408 $this->assertFalse($this->bookmarkService->get(1)->isPrivate());
407 } 409 }
408 410
409 /** 411 /**
410 * Import links as private 412 * Import bookmarks as private
411 */ 413 */
412 public function testImportAsPrivate() 414 public function testImportAsPrivate()
413 { 415 {
@@ -415,45 +417,34 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
415 $files = file2array('netscape_basic.htm'); 417 $files = file2array('netscape_basic.htm');
416 $this->assertStringMatchesFormat( 418 $this->assertStringMatchesFormat(
417 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 419 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
418 .' 2 links imported, 0 links overwritten, 0 links skipped.', 420 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
419 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 421 $this->netscapeBookmarkUtils->import($post, $files)
420 );
421 $this->assertEquals(2, count($this->linkDb));
422 $this->assertEquals(2, count_private($this->linkDb));
423 $this->assertEquals(
424 1,
425 $this->linkDb['0']['private']
426 );
427 $this->assertEquals(
428 1,
429 $this->linkDb['1']['private']
430 ); 422 );
423 $this->assertEquals(2, $this->bookmarkService->count());
424 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
425 $this->assertTrue($this->bookmarkService->get(0)->isPrivate());
426 $this->assertTrue($this->bookmarkService->get(1)->isPrivate());
431 } 427 }
432 428
433 /** 429 /**
434 * Overwrite private links so they become public 430 * Overwrite private bookmarks so they become public
435 */ 431 */
436 public function testOverwriteAsPublic() 432 public function testOverwriteAsPublic()
437 { 433 {
438 $files = file2array('netscape_basic.htm'); 434 $files = file2array('netscape_basic.htm');
439 435
440 // import links as private 436 // import bookmarks as private
441 $post = array('privacy' => 'private'); 437 $post = array('privacy' => 'private');
442 $this->assertStringMatchesFormat( 438 $this->assertStringMatchesFormat(
443 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 439 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
444 .' 2 links imported, 0 links overwritten, 0 links skipped.', 440 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
445 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 441 $this->netscapeBookmarkUtils->import($post, $files)
446 );
447 $this->assertEquals(2, count($this->linkDb));
448 $this->assertEquals(2, count_private($this->linkDb));
449 $this->assertEquals(
450 1,
451 $this->linkDb[0]['private']
452 );
453 $this->assertEquals(
454 1,
455 $this->linkDb[1]['private']
456 ); 442 );
443 $this->assertEquals(2, $this->bookmarkService->count());
444 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
445 $this->assertTrue($this->bookmarkService->get(0)->isPrivate());
446 $this->assertTrue($this->bookmarkService->get(1)->isPrivate());
447
457 // re-import as public, enable overwriting 448 // re-import as public, enable overwriting
458 $post = array( 449 $post = array(
459 'privacy' => 'public', 450 'privacy' => 'public',
@@ -461,45 +452,33 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
461 ); 452 );
462 $this->assertStringMatchesFormat( 453 $this->assertStringMatchesFormat(
463 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 454 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
464 .' 2 links imported, 2 links overwritten, 0 links skipped.', 455 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
465 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 456 $this->netscapeBookmarkUtils->import($post, $files)
466 );
467 $this->assertEquals(2, count($this->linkDb));
468 $this->assertEquals(0, count_private($this->linkDb));
469 $this->assertEquals(
470 0,
471 $this->linkDb[0]['private']
472 );
473 $this->assertEquals(
474 0,
475 $this->linkDb[1]['private']
476 ); 457 );
458 $this->assertEquals(2, $this->bookmarkService->count());
459 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
460 $this->assertFalse($this->bookmarkService->get(0)->isPrivate());
461 $this->assertFalse($this->bookmarkService->get(1)->isPrivate());
477 } 462 }
478 463
479 /** 464 /**
480 * Overwrite public links so they become private 465 * Overwrite public bookmarks so they become private
481 */ 466 */
482 public function testOverwriteAsPrivate() 467 public function testOverwriteAsPrivate()
483 { 468 {
484 $files = file2array('netscape_basic.htm'); 469 $files = file2array('netscape_basic.htm');
485 470
486 // import links as public 471 // import bookmarks as public
487 $post = array('privacy' => 'public'); 472 $post = array('privacy' => 'public');
488 $this->assertStringMatchesFormat( 473 $this->assertStringMatchesFormat(
489 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 474 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
490 .' 2 links imported, 0 links overwritten, 0 links skipped.', 475 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
491 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 476 $this->netscapeBookmarkUtils->import($post, $files)
492 );
493 $this->assertEquals(2, count($this->linkDb));
494 $this->assertEquals(0, count_private($this->linkDb));
495 $this->assertEquals(
496 0,
497 $this->linkDb['0']['private']
498 );
499 $this->assertEquals(
500 0,
501 $this->linkDb['1']['private']
502 ); 477 );
478 $this->assertEquals(2, $this->bookmarkService->count());
479 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
480 $this->assertFalse($this->bookmarkService->get(0)->isPrivate());
481 $this->assertFalse($this->bookmarkService->get(1)->isPrivate());
503 482
504 // re-import as private, enable overwriting 483 // re-import as private, enable overwriting
505 $post = array( 484 $post = array(
@@ -508,23 +487,17 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
508 ); 487 );
509 $this->assertStringMatchesFormat( 488 $this->assertStringMatchesFormat(
510 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 489 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
511 .' 2 links imported, 2 links overwritten, 0 links skipped.', 490 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
512 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 491 $this->netscapeBookmarkUtils->import($post, $files)
513 );
514 $this->assertEquals(2, count($this->linkDb));
515 $this->assertEquals(2, count_private($this->linkDb));
516 $this->assertEquals(
517 1,
518 $this->linkDb['0']['private']
519 );
520 $this->assertEquals(
521 1,
522 $this->linkDb['1']['private']
523 ); 492 );
493 $this->assertEquals(2, $this->bookmarkService->count());
494 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
495 $this->assertTrue($this->bookmarkService->get(0)->isPrivate());
496 $this->assertTrue($this->bookmarkService->get(1)->isPrivate());
524 } 497 }
525 498
526 /** 499 /**
527 * Attept to import the same links twice without enabling overwriting 500 * Attept to import the same bookmarks twice without enabling overwriting
528 */ 501 */
529 public function testSkipOverwrite() 502 public function testSkipOverwrite()
530 { 503 {
@@ -532,21 +505,21 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
532 $files = file2array('netscape_basic.htm'); 505 $files = file2array('netscape_basic.htm');
533 $this->assertStringMatchesFormat( 506 $this->assertStringMatchesFormat(
534 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 507 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
535 .' 2 links imported, 0 links overwritten, 0 links skipped.', 508 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
536 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 509 $this->netscapeBookmarkUtils->import($post, $files)
537 ); 510 );
538 $this->assertEquals(2, count($this->linkDb)); 511 $this->assertEquals(2, $this->bookmarkService->count());
539 $this->assertEquals(0, count_private($this->linkDb)); 512 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
540 513
541 // re-import as private, DO NOT enable overwriting 514 // re-import as private, DO NOT enable overwriting
542 $post = array('privacy' => 'private'); 515 $post = array('privacy' => 'private');
543 $this->assertStringMatchesFormat( 516 $this->assertStringMatchesFormat(
544 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 517 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
545 .' 0 links imported, 0 links overwritten, 2 links skipped.', 518 .' 0 bookmarks imported, 0 bookmarks overwritten, 2 bookmarks skipped.',
546 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 519 $this->netscapeBookmarkUtils->import($post, $files)
547 ); 520 );
548 $this->assertEquals(2, count($this->linkDb)); 521 $this->assertEquals(2, $this->bookmarkService->count());
549 $this->assertEquals(0, count_private($this->linkDb)); 522 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
550 } 523 }
551 524
552 /** 525 /**
@@ -561,19 +534,13 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
561 $files = file2array('netscape_basic.htm'); 534 $files = file2array('netscape_basic.htm');
562 $this->assertStringMatchesFormat( 535 $this->assertStringMatchesFormat(
563 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 536 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
564 .' 2 links imported, 0 links overwritten, 0 links skipped.', 537 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
565 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 538 $this->netscapeBookmarkUtils->import($post, $files)
566 );
567 $this->assertEquals(2, count($this->linkDb));
568 $this->assertEquals(0, count_private($this->linkDb));
569 $this->assertEquals(
570 'tag1 tag2 tag3 private secret',
571 $this->linkDb['0']['tags']
572 );
573 $this->assertEquals(
574 'tag1 tag2 tag3 public hello world',
575 $this->linkDb['1']['tags']
576 ); 539 );
540 $this->assertEquals(2, $this->bookmarkService->count());
541 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
542 $this->assertEquals('tag1 tag2 tag3 private secret', $this->bookmarkService->get(0)->getTagsString());
543 $this->assertEquals('tag1 tag2 tag3 public hello world', $this->bookmarkService->get(1)->getTagsString());
577 } 544 }
578 545
579 /** 546 /**
@@ -588,18 +555,18 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
588 $files = file2array('netscape_basic.htm'); 555 $files = file2array('netscape_basic.htm');
589 $this->assertStringMatchesFormat( 556 $this->assertStringMatchesFormat(
590 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 557 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
591 .' 2 links imported, 0 links overwritten, 0 links skipped.', 558 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
592 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 559 $this->netscapeBookmarkUtils->import($post, $files)
593 ); 560 );
594 $this->assertEquals(2, count($this->linkDb)); 561 $this->assertEquals(2, $this->bookmarkService->count());
595 $this->assertEquals(0, count_private($this->linkDb)); 562 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
596 $this->assertEquals( 563 $this->assertEquals(
597 'tag1&amp; tag2 &quot;tag3&quot; private secret', 564 'tag1&amp; tag2 &quot;tag3&quot; private secret',
598 $this->linkDb['0']['tags'] 565 $this->bookmarkService->get(0)->getTagsString()
599 ); 566 );
600 $this->assertEquals( 567 $this->assertEquals(
601 'tag1&amp; tag2 &quot;tag3&quot; public hello world', 568 'tag1&amp; tag2 &quot;tag3&quot; public hello world',
602 $this->linkDb['1']['tags'] 569 $this->bookmarkService->get(1)->getTagsString()
603 ); 570 );
604 } 571 }
605 572
@@ -613,23 +580,14 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
613 $files = file2array('same_date.htm'); 580 $files = file2array('same_date.htm');
614 $this->assertStringMatchesFormat( 581 $this->assertStringMatchesFormat(
615 'File same_date.htm (453 bytes) was successfully processed in %d seconds:' 582 'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
616 .' 3 links imported, 0 links overwritten, 0 links skipped.', 583 .' 3 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
617 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history) 584 $this->netscapeBookmarkUtils->import(array(), $files)
618 ); 585 );
619 $this->assertEquals(3, count($this->linkDb)); 586 $this->assertEquals(3, $this->bookmarkService->count());
620 $this->assertEquals(0, count_private($this->linkDb)); 587 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
621 $this->assertEquals( 588 $this->assertEquals(0, $this->bookmarkService->get(0)->getId());
622 0, 589 $this->assertEquals(1, $this->bookmarkService->get(1)->getId());
623 $this->linkDb[0]['id'] 590 $this->assertEquals(2, $this->bookmarkService->get(2)->getId());
624 );
625 $this->assertEquals(
626 1,
627 $this->linkDb[1]['id']
628 );
629 $this->assertEquals(
630 2,
631 $this->linkDb[2]['id']
632 );
633 } 591 }
634 592
635 public function testImportCreateUpdateHistory() 593 public function testImportCreateUpdateHistory()
@@ -639,14 +597,14 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
639 'overwrite' => 'true', 597 'overwrite' => 'true',
640 ]; 598 ];
641 $files = file2array('netscape_basic.htm'); 599 $files = file2array('netscape_basic.htm');
642 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); 600 $this->netscapeBookmarkUtils->import($post, $files);
643 $history = $this->history->getHistory(); 601 $history = $this->history->getHistory();
644 $this->assertEquals(1, count($history)); 602 $this->assertEquals(1, count($history));
645 $this->assertEquals(History::IMPORT, $history[0]['event']); 603 $this->assertEquals(History::IMPORT, $history[0]['event']);
646 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']); 604 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
647 605
648 // re-import as private, enable overwriting 606 // re-import as private, enable overwriting
649 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); 607 $this->netscapeBookmarkUtils->import($post, $files);
650 $history = $this->history->getHistory(); 608 $history = $this->history->getHistory();
651 $this->assertEquals(2, count($history)); 609 $this->assertEquals(2, count($history));
652 $this->assertEquals(History::IMPORT, $history[0]['event']); 610 $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 510288bb..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 links. 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
@@ -54,18 +71,18 @@ class PluginArchiveorgTest extends \PHPUnit\Framework\TestCase
54 } 71 }
55 72
56 /** 73 /**
57 * Test render_linklist hook on internal links. 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 bdfab439..16ecf357 100644
--- a/tests/plugins/PluginIssoTest.php
+++ b/tests/plugins/PluginIssoTest.php
@@ -2,9 +2,10 @@
2namespace Shaarli\Plugin\Isso; 2namespace Shaarli\Plugin\Isso;
3 3
4use DateTime; 4use DateTime;
5use Shaarli\Bookmark\LinkDB; 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');
@@ -60,7 +61,7 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
60 array( 61 array(
61 'id' => 12, 62 'id' => 12,
62 'url' => $str, 63 'url' => $str,
63 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date), 64 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date),
64 ) 65 )
65 ) 66 )
66 ); 67 );
@@ -85,9 +86,9 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
85 } 86 }
86 87
87 /** 88 /**
88 * Test isso plugin when multiple links 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');
@@ -102,27 +103,27 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
102 'id' => 12, 103 'id' => 12,
103 'url' => $str, 104 'url' => $str,
104 'shorturl' => $short1 = 'abcd', 105 'shorturl' => $short1 = 'abcd',
105 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date1), 106 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date1),
106 ), 107 ),
107 array( 108 array(
108 'id' => 13, 109 'id' => 13,
109 'url' => $str . '2', 110 'url' => $str . '2',
110 'shorturl' => $short2 = 'efgh', 111 'shorturl' => $short2 = 'efgh',
111 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date2), 112 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date2),
112 ), 113 ),
113 ) 114 )
114 ); 115 );
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');
@@ -136,7 +137,7 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
136 'id' => 12, 137 'id' => 12,
137 'url' => $str, 138 'url' => $str,
138 'shorturl' => $short1 = 'abcd', 139 'shorturl' => $short1 = 'abcd',
139 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date), 140 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date),
140 ) 141 )
141 ), 142 ),
142 'search_term' => $str 143 'search_term' => $str
@@ -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/PluginMarkdownTest.php b/tests/plugins/PluginMarkdownTest.php
deleted file mode 100644
index 9ddbc558..00000000
--- a/tests/plugins/PluginMarkdownTest.php
+++ /dev/null
@@ -1,306 +0,0 @@
1<?php
2namespace Shaarli\Plugin\Markdown;
3
4use Shaarli\Config\ConfigManager;
5use Shaarli\Plugin\PluginManager;
6
7/**
8 * PluginMarkdownTest.php
9 */
10
11require_once 'application/bookmark/LinkUtils.php';
12require_once 'application/Utils.php';
13require_once 'plugins/markdown/markdown.php';
14
15/**
16 * Class PluginMarkdownTest
17 * Unit test for the Markdown plugin
18 */
19class PluginMarkdownTest extends \PHPUnit\Framework\TestCase
20{
21 /**
22 * @var ConfigManager instance.
23 */
24 protected $conf;
25
26 /**
27 * Reset plugin path
28 */
29 public function setUp()
30 {
31 PluginManager::$PLUGINS_PATH = 'plugins';
32 $this->conf = new ConfigManager('tests/utils/config/configJson');
33 $this->conf->set('security.allowed_protocols', ['ftp', 'magnet']);
34 }
35
36 /**
37 * Test render_linklist hook.
38 * Only check that there is basic markdown rendering.
39 */
40 public function testMarkdownLinklist()
41 {
42 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
43 $data = array(
44 'links' => array(
45 0 => array(
46 'description' => $markdown,
47 ),
48 ),
49 );
50
51 $data = hook_markdown_render_linklist($data, $this->conf);
52 $this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
53 $this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
54
55 $this->assertEquals($markdown, $data['links'][0]['description_src']);
56 }
57
58 /**
59 * Test render_feed hook.
60 */
61 public function testMarkdownFeed()
62 {
63 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
64 $markdown .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
65 $data = array(
66 'links' => array(
67 0 => array(
68 'description' => $markdown,
69 ),
70 ),
71 );
72
73 $data = hook_markdown_render_feed($data, $this->conf);
74 $this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
75 $this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
76 $this->assertStringEndsWith(
77 '&#8212; <a href="http://domain.tld/?0oc_VQ">Permalien</a></p></div>',
78 $data['links'][0]['description']
79 );
80 }
81
82 /**
83 * Test render_daily hook.
84 * Only check that there is basic markdown rendering.
85 */
86 public function testMarkdownDaily()
87 {
88 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
89 $data = array(
90 // Columns data
91 'linksToDisplay' => array(
92 // nth link
93 0 => array(
94 'formatedDescription' => $markdown,
95 ),
96 ),
97 );
98
99 $data = hook_markdown_render_daily($data, $this->conf);
100 $this->assertNotFalse(strpos($data['linksToDisplay'][0]['formatedDescription'], '<h1>'));
101 $this->assertNotFalse(strpos($data['linksToDisplay'][0]['formatedDescription'], '<p>'));
102 }
103
104 /**
105 * Test reverse_text2clickable().
106 */
107 public function testReverseText2clickable()
108 {
109 $text = 'stuff http://hello.there/is=someone#here otherstuff';
110 $clickableText = text2clickable($text);
111 $reversedText = reverse_text2clickable($clickableText);
112 $this->assertEquals($text, $reversedText);
113 }
114
115 /**
116 * Test reverse_text2clickable().
117 */
118 public function testReverseText2clickableHashtags()
119 {
120 $text = file_get_contents('tests/plugins/resources/hashtags.raw');
121 $md = file_get_contents('tests/plugins/resources/hashtags.md');
122 $clickableText = hashtag_autolink($text);
123 $reversedText = reverse_text2clickable($clickableText);
124 $this->assertEquals($md, $reversedText);
125 }
126
127 /**
128 * Test reverse_nl2br().
129 */
130 public function testReverseNl2br()
131 {
132 $text = 'stuff' . PHP_EOL . 'otherstuff';
133 $processedText = nl2br($text);
134 $reversedText = reverse_nl2br($processedText);
135 $this->assertEquals($text, $reversedText);
136 }
137
138 /**
139 * Test reverse_space2nbsp().
140 */
141 public function testReverseSpace2nbsp()
142 {
143 $text = ' stuff' . PHP_EOL . ' otherstuff and another';
144 $processedText = space2nbsp($text);
145 $reversedText = reverse_space2nbsp($processedText);
146 $this->assertEquals($text, $reversedText);
147 }
148
149 public function testReverseFeedPermalink()
150 {
151 $text = 'Description... ';
152 $text .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
153 $expected = 'Description... &#8212; [Permalien](http://domain.tld/?0oc_VQ)';
154 $processedText = reverse_feed_permalink($text);
155
156 $this->assertEquals($expected, $processedText);
157 }
158
159 public function testReverseLastFeedPermalink()
160 {
161 $text = 'Description... ';
162 $text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
163 $expected = $text;
164 $text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
165 $expected .= '<br>&#8212; [Permalien](http://domain.tld/?0oc_VQ)';
166 $processedText = reverse_feed_permalink($text);
167
168 $this->assertEquals($expected, $processedText);
169 }
170
171 public function testReverseNoFeedPermalink()
172 {
173 $text = 'Hello! Where are you from?';
174 $expected = $text;
175 $processedText = reverse_feed_permalink($text);
176
177 $this->assertEquals($expected, $processedText);
178 }
179
180 /**
181 * Test sanitize_html().
182 */
183 public function testSanitizeHtml()
184 {
185 $input = '< script src="js.js"/>';
186 $input .= '< script attr>alert(\'xss\');</script>';
187 $input .= '<style> * { display: none }</style>';
188 $output = escape($input);
189 $input .= '<a href="#" onmouseHover="alert(\'xss\');" attr="tt">link</a>';
190 $output .= '<a href="#" attr="tt">link</a>';
191 $input .= '<a href="#" onmouseHover=alert(\'xss\'); attr="tt">link</a>';
192 $output .= '<a href="#" attr="tt">link</a>';
193 $this->assertEquals($output, sanitize_html($input));
194 // Do not touch escaped HTML.
195 $input = escape($input);
196 $this->assertEquals($input, sanitize_html($input));
197 }
198
199 /**
200 * Test the no markdown tag.
201 */
202 public function testNoMarkdownTag()
203 {
204 $str = 'All _work_ and `no play` makes Jack a *dull* boy.';
205 $data = array(
206 'links' => array(array(
207 'description' => $str,
208 'tags' => NO_MD_TAG,
209 'taglist' => array(NO_MD_TAG),
210 ))
211 );
212
213 $processed = hook_markdown_render_linklist($data, $this->conf);
214 $this->assertEquals($str, $processed['links'][0]['description']);
215
216 $processed = hook_markdown_render_feed($data, $this->conf);
217 $this->assertEquals($str, $processed['links'][0]['description']);
218
219 $data = array(
220 // Columns data
221 'linksToDisplay' => array(
222 // nth link
223 0 => array(
224 'formatedDescription' => $str,
225 'tags' => NO_MD_TAG,
226 'taglist' => array(),
227 ),
228 ),
229 );
230
231 $data = hook_markdown_render_daily($data, $this->conf);
232 $this->assertEquals($str, $data['linksToDisplay'][0]['formatedDescription']);
233 }
234
235 /**
236 * Test that a close value to nomarkdown is not understand as nomarkdown (previous value `.nomarkdown`).
237 */
238 public function testNoMarkdownNotExcactlyMatching()
239 {
240 $str = 'All _work_ and `no play` makes Jack a *dull* boy.';
241 $data = array(
242 'links' => array(array(
243 'description' => $str,
244 'tags' => '.' . NO_MD_TAG,
245 'taglist' => array('.'. NO_MD_TAG),
246 ))
247 );
248
249 $data = hook_markdown_render_feed($data, $this->conf);
250 $this->assertContains('<em>', $data['links'][0]['description']);
251 }
252
253 /**
254 * Make sure that the generated HTML match the reference HTML file.
255 */
256 public function testMarkdownGlobalProcessDescription()
257 {
258 $md = file_get_contents('tests/plugins/resources/markdown.md');
259 $md = format_description($md);
260 $html = file_get_contents('tests/plugins/resources/markdown.html');
261
262 $data = process_markdown(
263 $md,
264 $this->conf->get('security.markdown_escape', true),
265 $this->conf->get('security.allowed_protocols')
266 );
267 $this->assertEquals($html, $data . PHP_EOL);
268 }
269
270 /**
271 * Make sure that the HTML tags are escaped.
272 */
273 public function testMarkdownWithHtmlEscape()
274 {
275 $md = '**strong** <strong>strong</strong>';
276 $html = '<div class="markdown"><p><strong>strong</strong> &lt;strong&gt;strong&lt;/strong&gt;</p></div>';
277 $data = array(
278 'links' => array(
279 0 => array(
280 'description' => $md,
281 ),
282 ),
283 );
284 $data = hook_markdown_render_linklist($data, $this->conf);
285 $this->assertEquals($html, $data['links'][0]['description']);
286 }
287
288 /**
289 * Make sure that the HTML tags aren't escaped with the setting set to false.
290 */
291 public function testMarkdownWithHtmlNoEscape()
292 {
293 $this->conf->set('security.markdown_escape', false);
294 $md = '**strong** <strong>strong</strong>';
295 $html = '<div class="markdown"><p><strong>strong</strong> <strong>strong</strong></p></div>';
296 $data = array(
297 'links' => array(
298 0 => array(
299 'description' => $md,
300 ),
301 ),
302 );
303 $data = hook_markdown_render_linklist($data, $this->conf);
304 $this->assertEquals($html, $data['links'][0]['description']);
305 }
306}
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..698d3d10 100644
--- a/tests/security/BanManagerTest.php
+++ b/tests/security/BanManagerTest.php
@@ -3,8 +3,8 @@
3 3
4namespace Shaarli\Security; 4namespace Shaarli\Security;
5 5
6use PHPUnit\Framework\TestCase;
7use Shaarli\FileUtils; 6use Shaarli\FileUtils;
7use Shaarli\TestCase;
8 8
9/** 9/**
10 * Test coverage for BanManager 10 * Test coverage for BanManager
@@ -32,7 +32,7 @@ class BanManagerTest extends TestCase
32 /** 32 /**
33 * Prepare or reset test resources 33 * Prepare or reset test resources
34 */ 34 */
35 public function setUp() 35 protected function setUp(): void
36 { 36 {
37 if (file_exists($this->banFile)) { 37 if (file_exists($this->banFile)) {
38 unlink($this->banFile); 38 unlink($this->banFile);
diff --git a/tests/security/LoginManagerTest.php b/tests/security/LoginManagerTest.php
index eef0f22a..d302983d 100644
--- a/tests/security/LoginManagerTest.php
+++ b/tests/security/LoginManagerTest.php
@@ -1,9 +1,8 @@
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 Shaarli\TestCase;
7 6
8/** 7/**
9 * Test coverage for LoginManager 8 * Test coverage for LoginManager
@@ -58,10 +57,13 @@ class LoginManagerTest extends TestCase
58 /** @var string Salt used by hash functions */ 57 /** @var string Salt used by hash functions */
59 protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2'; 58 protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
60 59
60 /** @var CookieManager */
61 protected $cookieManager;
62
61 /** 63 /**
62 * Prepare or reset test resources 64 * Prepare or reset test resources
63 */ 65 */
64 public function setUp() 66 protected function setUp(): void
65 { 67 {
66 if (file_exists($this->banFile)) { 68 if (file_exists($this->banFile)) {
67 unlink($this->banFile); 69 unlink($this->banFile);
@@ -78,13 +80,18 @@ class LoginManagerTest extends TestCase
78 'security.ban_after' => 2, 80 'security.ban_after' => 2,
79 'security.ban_duration' => 3600, 81 'security.ban_duration' => 3600,
80 'security.trusted_proxies' => [$this->trustedProxy], 82 'security.trusted_proxies' => [$this->trustedProxy],
83 'ldap.host' => '',
81 ]); 84 ]);
82 85
83 $this->cookie = []; 86 $this->cookie = [];
84 $this->session = []; 87 $this->session = [];
85 88
86 $this->sessionManager = new SessionManager($this->session, $this->configManager); 89 $this->cookieManager = $this->createMock(CookieManager::class);
87 $this->loginManager = new LoginManager($this->configManager, $this->sessionManager); 90 $this->cookieManager->method('getCookieParameter')->willReturnCallback(function (string $key) {
91 return $this->cookie[$key] ?? null;
92 });
93 $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path');
94 $this->loginManager = new LoginManager($this->configManager, $this->sessionManager, $this->cookieManager);
88 $this->server['REMOTE_ADDR'] = $this->ipAddr; 95 $this->server['REMOTE_ADDR'] = $this->ipAddr;
89 } 96 }
90 97
@@ -192,8 +199,8 @@ class LoginManagerTest extends TestCase
192 $configManager = new \FakeConfigManager([ 199 $configManager = new \FakeConfigManager([
193 'resource.ban_file' => $this->banFile, 200 'resource.ban_file' => $this->banFile,
194 ]); 201 ]);
195 $loginManager = new LoginManager($configManager, null); 202 $loginManager = new LoginManager($configManager, null, $this->cookieManager);
196 $loginManager->checkLoginState([], ''); 203 $loginManager->checkLoginState('');
197 204
198 $this->assertFalse($loginManager->isLoggedIn()); 205 $this->assertFalse($loginManager->isLoggedIn());
199 } 206 }
@@ -209,9 +216,9 @@ class LoginManagerTest extends TestCase
209 'expires_on' => time() + 100, 216 'expires_on' => time() + 100,
210 ]; 217 ];
211 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 218 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
212 $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope'; 219 $this->cookie[CookieManager::STAY_SIGNED_IN] = 'nope';
213 220
214 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); 221 $this->loginManager->checkLoginState($this->clientIpAddress);
215 222
216 $this->assertTrue($this->loginManager->isLoggedIn()); 223 $this->assertTrue($this->loginManager->isLoggedIn());
217 $this->assertTrue(empty($this->session['username'])); 224 $this->assertTrue(empty($this->session['username']));
@@ -223,9 +230,9 @@ class LoginManagerTest extends TestCase
223 public function testCheckLoginStateStaySignedInWithValidToken() 230 public function testCheckLoginStateStaySignedInWithValidToken()
224 { 231 {
225 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 232 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
226 $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken(); 233 $this->cookie[CookieManager::STAY_SIGNED_IN] = $this->loginManager->getStaySignedInToken();
227 234
228 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); 235 $this->loginManager->checkLoginState($this->clientIpAddress);
229 236
230 $this->assertTrue($this->loginManager->isLoggedIn()); 237 $this->assertTrue($this->loginManager->isLoggedIn());
231 $this->assertEquals($this->login, $this->session['username']); 238 $this->assertEquals($this->login, $this->session['username']);
@@ -240,7 +247,7 @@ class LoginManagerTest extends TestCase
240 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 247 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
241 $this->session['expires_on'] = time() - 100; 248 $this->session['expires_on'] = time() - 100;
242 249
243 $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); 250 $this->loginManager->checkLoginState($this->clientIpAddress);
244 251
245 $this->assertFalse($this->loginManager->isLoggedIn()); 252 $this->assertFalse($this->loginManager->isLoggedIn());
246 } 253 }
@@ -252,7 +259,7 @@ class LoginManagerTest extends TestCase
252 { 259 {
253 $this->loginManager->generateStaySignedInToken($this->clientIpAddress); 260 $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
254 261
255 $this->loginManager->checkLoginState($this->cookie, '10.7.157.98'); 262 $this->loginManager->checkLoginState('10.7.157.98');
256 263
257 $this->assertFalse($this->loginManager->isLoggedIn()); 264 $this->assertFalse($this->loginManager->isLoggedIn());
258 } 265 }
@@ -296,4 +303,37 @@ class LoginManagerTest extends TestCase
296 $this->loginManager->checkCredentials('', '', $this->login, $this->password) 303 $this->loginManager->checkCredentials('', '', $this->login, $this->password)
297 ); 304 );
298 } 305 }
306
307 /**
308 * Check user credentials through LDAP - server unreachable
309 */
310 public function testCheckCredentialsFromUnreachableLdap()
311 {
312 $this->configManager->set('ldap.host', 'dummy');
313 $this->assertFalse(
314 $this->loginManager->checkCredentials('', '', $this->login, $this->password)
315 );
316 }
317
318 /**
319 * Check user credentials through LDAP - wrong login and password supplied
320 */
321 public function testCheckCredentialsFromLdapWrongLoginAndPassword()
322 {
323 $this->configManager->set('ldap.host', 'dummy');
324 $this->assertFalse(
325 $this->loginManager->checkCredentialsFromLdap($this->login, $this->password, function() { return null; }, function() { return false; })
326 );
327 }
328
329 /**
330 * Check user credentials through LDAP - correct login and password supplied
331 */
332 public function testCheckCredentialsFromLdapGoodLoginAndPassword()
333 {
334 $this->configManager->set('ldap.host', 'dummy');
335 $this->assertTrue(
336 $this->loginManager->checkCredentialsFromLdap($this->login, $this->password, function() { return null; }, function() { return true; })
337 );
338 }
299} 339}
diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php
index f264505e..3f9c3ef5 100644
--- a/tests/security/SessionManagerTest.php
+++ b/tests/security/SessionManagerTest.php
@@ -1,12 +1,8 @@
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\TestCase;
9use Shaarli\Security\SessionManager;
10 6
11/** 7/**
12 * Test coverage for SessionManager 8 * Test coverage for SessionManager
@@ -28,23 +24,23 @@ class SessionManagerTest extends TestCase
28 /** 24 /**
29 * Assign reference data 25 * Assign reference data
30 */ 26 */
31 public static function setUpBeforeClass() 27 public static function setUpBeforeClass(): void
32 { 28 {
33 self::$sidHashes = ReferenceSessionIdHashes::getHashes(); 29 self::$sidHashes = \ReferenceSessionIdHashes::getHashes();
34 } 30 }
35 31
36 /** 32 /**
37 * Initialize or reset test resources 33 * Initialize or reset test resources
38 */ 34 */
39 public function setUp() 35 protected function setUp(): void
40 { 36 {
41 $this->conf = new FakeConfigManager([ 37 $this->conf = new \FakeConfigManager([
42 'credentials.login' => 'johndoe', 38 'credentials.login' => 'johndoe',
43 'credentials.salt' => 'salt', 39 'credentials.salt' => 'salt',
44 'security.session_protection_disabled' => false, 40 'security.session_protection_disabled' => false,
45 ]); 41 ]);
46 $this->session = []; 42 $this->session = [];
47 $this->sessionManager = new SessionManager($this->session, $this->conf); 43 $this->sessionManager = new SessionManager($this->session, $this->conf, 'session_path');
48 } 44 }
49 45
50 /** 46 /**
@@ -69,7 +65,7 @@ class SessionManagerTest extends TestCase
69 $token => 1, 65 $token => 1,
70 ], 66 ],
71 ]; 67 ];
72 $sessionManager = new SessionManager($session, $this->conf); 68 $sessionManager = new SessionManager($session, $this->conf, 'session_path');
73 69
74 // check and destroy the token 70 // check and destroy the token
75 $this->assertTrue($sessionManager->checkToken($token)); 71 $this->assertTrue($sessionManager->checkToken($token));
@@ -211,15 +207,16 @@ class SessionManagerTest extends TestCase
211 'expires_on' => time() + 1000, 207 'expires_on' => time() + 1000,
212 'username' => 'johndoe', 208 'username' => 'johndoe',
213 'visibility' => 'public', 209 'visibility' => 'public',
214 'untaggedonly' => false, 210 'untaggedonly' => true,
215 ]; 211 ];
216 $this->sessionManager->logout(); 212 $this->sessionManager->logout();
217 213
218 $this->assertFalse(isset($this->session['ip'])); 214 $this->assertArrayNotHasKey('ip', $this->session);
219 $this->assertFalse(isset($this->session['expires_on'])); 215 $this->assertArrayNotHasKey('expires_on', $this->session);
220 $this->assertFalse(isset($this->session['username'])); 216 $this->assertArrayNotHasKey('username', $this->session);
221 $this->assertFalse(isset($this->session['visibility'])); 217 $this->assertArrayNotHasKey('visibility', $this->session);
222 $this->assertFalse(isset($this->session['untaggedonly'])); 218 $this->assertArrayHasKey('untaggedonly', $this->session);
219 $this->assertTrue($this->session['untaggedonly']);
223 } 220 }
224 221
225 /** 222 /**
@@ -269,4 +266,61 @@ class SessionManagerTest extends TestCase
269 $this->session['ip'] = 'ip_id_one'; 266 $this->session['ip'] = 'ip_id_one';
270 $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two')); 267 $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two'));
271 } 268 }
269
270 /**
271 * Test creating an entry in the session array
272 */
273 public function testSetSessionParameterCreate(): void
274 {
275 $this->sessionManager->setSessionParameter('abc', 'def');
276
277 static::assertSame('def', $this->session['abc']);
278 }
279
280 /**
281 * Test updating an entry in the session array
282 */
283 public function testSetSessionParameterUpdate(): void
284 {
285 $this->session['abc'] = 'ghi';
286
287 $this->sessionManager->setSessionParameter('abc', 'def');
288
289 static::assertSame('def', $this->session['abc']);
290 }
291
292 /**
293 * Test updating an entry in the session array with null value
294 */
295 public function testSetSessionParameterUpdateNull(): void
296 {
297 $this->session['abc'] = 'ghi';
298
299 $this->sessionManager->setSessionParameter('abc', null);
300
301 static::assertArrayHasKey('abc', $this->session);
302 static::assertNull($this->session['abc']);
303 }
304
305 /**
306 * Test deleting an existing entry in the session array
307 */
308 public function testDeleteSessionParameter(): void
309 {
310 $this->session['abc'] = 'def';
311
312 $this->sessionManager->deleteSessionParameter('abc');
313
314 static::assertArrayNotHasKey('abc', $this->session);
315 }
316
317 /**
318 * Test deleting a non existent entry in the session array
319 */
320 public function testDeleteSessionParameterNotExisting(): void
321 {
322 $this->sessionManager->deleteSessionParameter('abc');
323
324 static::assertArrayNotHasKey('abc', $this->session);
325 }
272} 326}
diff --git a/tests/updater/DummyUpdater.php b/tests/updater/DummyUpdater.php
index 9e866f1f..3403233f 100644
--- a/tests/updater/DummyUpdater.php
+++ b/tests/updater/DummyUpdater.php
@@ -4,6 +4,7 @@ namespace Shaarli\Updater;
4use Exception; 4use Exception;
5use ReflectionClass; 5use ReflectionClass;
6use ReflectionMethod; 6use ReflectionMethod;
7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\LinkDB; 8use Shaarli\Bookmark\LinkDB;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
9 10
@@ -16,14 +17,14 @@ class DummyUpdater extends Updater
16 /** 17 /**
17 * Object constructor. 18 * Object constructor.
18 * 19 *
19 * @param array $doneUpdates Updates which are already done. 20 * @param array $doneUpdates Updates which are already done.
20 * @param LinkDB $linkDB LinkDB instance. 21 * @param BookmarkFileService $bookmarkService LinkDB instance.
21 * @param ConfigManager $conf Configuration Manager instance. 22 * @param ConfigManager $conf Configuration Manager instance.
22 * @param boolean $isLoggedIn True if the user is logged in. 23 * @param boolean $isLoggedIn True if the user is logged in.
23 */ 24 */
24 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) 25 public function __construct($doneUpdates, $bookmarkService, $conf, $isLoggedIn)
25 { 26 {
26 parent::__construct($doneUpdates, $linkDB, $conf, $isLoggedIn); 27 parent::__construct($doneUpdates, $bookmarkService, $conf, $isLoggedIn);
27 28
28 // Retrieve all update methods. 29 // Retrieve all update methods.
29 // For unit test, only retrieve final methods, 30 // For unit test, only retrieve final methods,
@@ -36,7 +37,7 @@ class DummyUpdater extends Updater
36 * 37 *
37 * @return bool true. 38 * @return bool true.
38 */ 39 */
39 final private function updateMethodDummy1() 40 final protected function updateMethodDummy1()
40 { 41 {
41 return true; 42 return true;
42 } 43 }
@@ -46,7 +47,7 @@ class DummyUpdater extends Updater
46 * 47 *
47 * @return bool true. 48 * @return bool true.
48 */ 49 */
49 final private function updateMethodDummy2() 50 final protected function updateMethodDummy2()
50 { 51 {
51 return true; 52 return true;
52 } 53 }
@@ -56,7 +57,7 @@ class DummyUpdater extends Updater
56 * 57 *
57 * @return bool true. 58 * @return bool true.
58 */ 59 */
59 final private function updateMethodDummy3() 60 final protected function updateMethodDummy3()
60 { 61 {
61 return true; 62 return true;
62 } 63 }
@@ -66,7 +67,7 @@ class DummyUpdater extends Updater
66 * 67 *
67 * @throws Exception error. 68 * @throws Exception error.
68 */ 69 */
69 final private function updateMethodException() 70 final protected function updateMethodException()
70 { 71 {
71 throw new Exception('whatever'); 72 throw new Exception('whatever');
72 } 73 }
diff --git a/tests/updater/UpdaterTest.php b/tests/updater/UpdaterTest.php
index 93bc86c1..a6280b8c 100644
--- a/tests/updater/UpdaterTest.php
+++ b/tests/updater/UpdaterTest.php
@@ -1,24 +1,19 @@
1<?php 1<?php
2namespace Shaarli\Updater; 2namespace Shaarli\Updater;
3 3
4use DateTime;
5use Exception; 4use Exception;
6use Shaarli\Bookmark\LinkDB; 5use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Config\ConfigJson; 6use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
9use Shaarli\Config\ConfigPhp; 8use Shaarli\History;
10use Shaarli\Thumbnailer; 9use Shaarli\TestCase;
11 10
12require_once 'application/updater/UpdaterUtils.php';
13require_once 'tests/updater/DummyUpdater.php';
14require_once 'tests/utils/ReferenceLinkDB.php';
15require_once 'inc/rain.tpl.class.php';
16 11
17/** 12/**
18 * Class UpdaterTest. 13 * Class UpdaterTest.
19 * Runs unit tests against the updater class. 14 * Runs unit tests against the updater class.
20 */ 15 */
21class UpdaterTest extends \PHPUnit\Framework\TestCase 16class UpdaterTest extends TestCase
22{ 17{
23 /** 18 /**
24 * @var string Path to test datastore. 19 * @var string Path to test datastore.
@@ -35,24 +30,38 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
35 */ 30 */
36 protected $conf; 31 protected $conf;
37 32
33 /** @var BookmarkServiceInterface */
34 protected $bookmarkService;
35
36 /** @var \ReferenceLinkDB */
37 protected $refDB;
38
39 /** @var Updater */
40 protected $updater;
41
38 /** 42 /**
39 * Executed before each test. 43 * Executed before each test.
40 */ 44 */
41 public function setUp() 45 protected function setUp(): void
42 { 46 {
47 $this->refDB = new \ReferenceLinkDB();
48 $this->refDB->write(self::$testDatastore);
49
43 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); 50 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
44 $this->conf = new ConfigManager(self::$configFile); 51 $this->conf = new ConfigManager(self::$configFile);
52 $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true);
53 $this->updater = new Updater([], $this->bookmarkService, $this->conf, true);
45 } 54 }
46 55
47 /** 56 /**
48 * Test read_updates_file with an empty/missing file. 57 * Test UpdaterUtils::read_updates_file with an empty/missing file.
49 */ 58 */
50 public function testReadEmptyUpdatesFile() 59 public function testReadEmptyUpdatesFile()
51 { 60 {
52 $this->assertEquals(array(), read_updates_file('')); 61 $this->assertEquals(array(), UpdaterUtils::read_updates_file(''));
53 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 62 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
54 touch($updatesFile); 63 touch($updatesFile);
55 $this->assertEquals(array(), read_updates_file($updatesFile)); 64 $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile));
56 unlink($updatesFile); 65 unlink($updatesFile);
57 } 66 }
58 67
@@ -64,42 +73,42 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
64 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 73 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
65 $updatesMethods = array('m1', 'm2', 'm3'); 74 $updatesMethods = array('m1', 'm2', 'm3');
66 75
67 write_updates_file($updatesFile, $updatesMethods); 76 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
68 $readMethods = read_updates_file($updatesFile); 77 $readMethods = UpdaterUtils::read_updates_file($updatesFile);
69 $this->assertEquals($readMethods, $updatesMethods); 78 $this->assertEquals($readMethods, $updatesMethods);
70 79
71 // Update 80 // Update
72 $updatesMethods[] = 'm4'; 81 $updatesMethods[] = 'm4';
73 write_updates_file($updatesFile, $updatesMethods); 82 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
74 $readMethods = read_updates_file($updatesFile); 83 $readMethods = UpdaterUtils::read_updates_file($updatesFile);
75 $this->assertEquals($readMethods, $updatesMethods); 84 $this->assertEquals($readMethods, $updatesMethods);
76 unlink($updatesFile); 85 unlink($updatesFile);
77 } 86 }
78 87
79 /** 88 /**
80 * Test errors in write_updates_file(): empty updates file. 89 * Test errors in UpdaterUtils::write_updates_file(): empty updates file.
81 *
82 * @expectedException Exception
83 * @expectedExceptionMessageRegExp /Updates file path is not set(.*)/
84 */ 90 */
85 public function testWriteEmptyUpdatesFile() 91 public function testWriteEmptyUpdatesFile()
86 { 92 {
87 write_updates_file('', array('test')); 93 $this->expectException(\Exception::class);
94 $this->expectExceptionMessageRegExp('/Updates file path is not set(.*)/');
95
96 UpdaterUtils::write_updates_file('', array('test'));
88 } 97 }
89 98
90 /** 99 /**
91 * Test errors in write_updates_file(): not writable updates file. 100 * Test errors in UpdaterUtils::write_updates_file(): not writable updates file.
92 *
93 * @expectedException Exception
94 * @expectedExceptionMessageRegExp /Unable to write(.*)/
95 */ 101 */
96 public function testWriteUpdatesFileNotWritable() 102 public function testWriteUpdatesFileNotWritable()
97 { 103 {
104 $this->expectException(\Exception::class);
105 $this->expectExceptionMessageRegExp('/Unable to write(.*)/');
106
98 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 107 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
99 touch($updatesFile); 108 touch($updatesFile);
100 chmod($updatesFile, 0444); 109 chmod($updatesFile, 0444);
101 try { 110 try {
102 @write_updates_file($updatesFile, array('test')); 111 @UpdaterUtils::write_updates_file($updatesFile, array('test'));
103 } catch (Exception $e) { 112 } catch (Exception $e) {
104 unlink($updatesFile); 113 unlink($updatesFile);
105 throw $e; 114 throw $e;
@@ -159,11 +168,11 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
159 168
160 /** 169 /**
161 * Test Update failed. 170 * Test Update failed.
162 *
163 * @expectedException \Exception
164 */ 171 */
165 public function testUpdateFailed() 172 public function testUpdateFailed()
166 { 173 {
174 $this->expectException(\Exception::class);
175
167 $updates = array( 176 $updates = array(
168 'updateMethodDummy1', 177 'updateMethodDummy1',
169 'updateMethodDummy2', 178 'updateMethodDummy2',
@@ -174,653 +183,39 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
174 $updater->update(); 183 $updater->update();
175 } 184 }
176 185
177 /** 186 public function testUpdateMethodRelativeHomeLinkRename(): void
178 * Test update mergeDeprecatedConfig:
179 * 1. init a config file.
180 * 2. init a options.php file with update value.
181 * 3. merge.
182 * 4. check updated value in config file.
183 */
184 public function testUpdateMergeDeprecatedConfig()
185 {
186 $this->conf->setConfigFile('tests/utils/config/configPhp');
187 $this->conf->reset();
188
189 $optionsFile = 'tests/updater/options.php';
190 $options = '<?php
191$GLOBALS[\'privateLinkByDefault\'] = true;';
192 file_put_contents($optionsFile, $options);
193
194 // tmp config file.
195 $this->conf->setConfigFile('tests/updater/config');
196
197 // merge configs
198 $updater = new Updater(array(), array(), $this->conf, true);
199 // This writes a new config file in tests/updater/config.php
200 $updater->updateMethodMergeDeprecatedConfigFile();
201
202 // make sure updated field is changed
203 $this->conf->reload();
204 $this->assertTrue($this->conf->get('privacy.default_private_links'));
205 $this->assertFalse(is_file($optionsFile));
206 // Delete the generated file.
207 unlink($this->conf->getConfigFileExt());
208 }
209
210 /**
211 * Test mergeDeprecatedConfig in without options file.
212 */
213 public function testMergeDeprecatedConfigNoFile()
214 {
215 $updater = new Updater(array(), array(), $this->conf, true);
216 $updater->updateMethodMergeDeprecatedConfigFile();
217
218 $this->assertEquals('root', $this->conf->get('credentials.login'));
219 }
220
221 /**
222 * Test renameDashTags update method.
223 */
224 public function testRenameDashTags()
225 {
226 $refDB = new \ReferenceLinkDB();
227 $refDB->write(self::$testDatastore);
228 $linkDB = new LinkDB(self::$testDatastore, true, false);
229
230 $this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
231 $updater = new Updater(array(), $linkDB, $this->conf, true);
232 $updater->updateMethodRenameDashTags();
233 $this->assertNotEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
234 }
235
236 /**
237 * Convert old PHP config file to JSON config.
238 */
239 public function testConfigToJson()
240 {
241 $configFile = 'tests/utils/config/configPhp';
242 $this->conf->setConfigFile($configFile);
243 $this->conf->reset();
244
245 // The ConfigIO is initialized with ConfigPhp.
246 $this->assertTrue($this->conf->getConfigIO() instanceof ConfigPhp);
247
248 $updater = new Updater(array(), array(), $this->conf, false);
249 $done = $updater->updateMethodConfigToJson();
250 $this->assertTrue($done);
251
252 // The ConfigIO has been updated to ConfigJson.
253 $this->assertTrue($this->conf->getConfigIO() instanceof ConfigJson);
254 $this->assertTrue(file_exists($this->conf->getConfigFileExt()));
255
256 // Check JSON config data.
257 $this->conf->reload();
258 $this->assertEquals('root', $this->conf->get('credentials.login'));
259 $this->assertEquals('lala', $this->conf->get('redirector.url'));
260 $this->assertEquals('data/datastore.php', $this->conf->get('resource.datastore'));
261 $this->assertEquals('1', $this->conf->get('plugins.WALLABAG_VERSION'));
262
263 rename($configFile . '.save.php', $configFile . '.php');
264 unlink($this->conf->getConfigFileExt());
265 }
266
267 /**
268 * Launch config conversion update with an existing JSON file => nothing to do.
269 */
270 public function testConfigToJsonNothingToDo()
271 {
272 $filetime = filemtime($this->conf->getConfigFileExt());
273 $updater = new Updater(array(), array(), $this->conf, false);
274 $done = $updater->updateMethodConfigToJson();
275 $this->assertTrue($done);
276 $expected = filemtime($this->conf->getConfigFileExt());
277 $this->assertEquals($expected, $filetime);
278 }
279
280 /**
281 * Test escapeUnescapedConfig with valid data.
282 */
283 public function testEscapeConfig()
284 {
285 $sandbox = 'sandbox/config';
286 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
287 $this->conf = new ConfigManager($sandbox);
288 $title = '<script>alert("title");</script>';
289 $headerLink = '<script>alert("header_link");</script>';
290 $this->conf->set('general.title', $title);
291 $this->conf->set('general.header_link', $headerLink);
292 $updater = new Updater(array(), array(), $this->conf, true);
293 $done = $updater->updateMethodEscapeUnescapedConfig();
294 $this->assertTrue($done);
295 $this->conf->reload();
296 $this->assertEquals(escape($title), $this->conf->get('general.title'));
297 $this->assertEquals(escape($headerLink), $this->conf->get('general.header_link'));
298 unlink($sandbox . '.json.php');
299 }
300
301 /**
302 * Test updateMethodApiSettings(): create default settings for the API (enabled + secret).
303 */
304 public function testUpdateApiSettings()
305 {
306 $confFile = 'sandbox/config';
307 copy(self::$configFile .'.json.php', $confFile .'.json.php');
308 $conf = new ConfigManager($confFile);
309 $updater = new Updater(array(), array(), $conf, true);
310
311 $this->assertFalse($conf->exists('api.enabled'));
312 $this->assertFalse($conf->exists('api.secret'));
313 $updater->updateMethodApiSettings();
314 $conf->reload();
315 $this->assertTrue($conf->get('api.enabled'));
316 $this->assertTrue($conf->exists('api.secret'));
317 unlink($confFile .'.json.php');
318 }
319
320 /**
321 * Test updateMethodApiSettings(): already set, do nothing.
322 */
323 public function testUpdateApiSettingsNothingToDo()
324 { 187 {
325 $confFile = 'sandbox/config'; 188 $this->updater->setBasePath('/subfolder');
326 copy(self::$configFile .'.json.php', $confFile .'.json.php'); 189 $this->conf->set('general.header_link', '?');
327 $conf = new ConfigManager($confFile);
328 $conf->set('api.enabled', false);
329 $conf->set('api.secret', '');
330 $updater = new Updater(array(), array(), $conf, true);
331 $updater->updateMethodApiSettings();
332 $this->assertFalse($conf->get('api.enabled'));
333 $this->assertEmpty($conf->get('api.secret'));
334 unlink($confFile .'.json.php');
335 }
336 190
337 /** 191 $this->updater->updateMethodRelativeHomeLink();
338 * Test updateMethodDatastoreIds().
339 */
340 public function testDatastoreIds()
341 {
342 $links = array(
343 '20121206_182539' => array(
344 'linkdate' => '20121206_182539',
345 'title' => 'Geek and Poke',
346 'url' => 'http://geek-and-poke.com/',
347 'description' => 'desc',
348 'tags' => 'dev cartoon tag1 tag2 tag3 tag4 ',
349 'updated' => '20121206_190301',
350 'private' => false,
351 ),
352 '20121206_172539' => array(
353 'linkdate' => '20121206_172539',
354 'title' => 'UserFriendly - Samba',
355 'url' => 'http://ars.userfriendly.org/cartoons/?id=20010306',
356 'description' => '',
357 'tags' => 'samba cartoon web',
358 'private' => false,
359 ),
360 '20121206_142300' => array(
361 'linkdate' => '20121206_142300',
362 'title' => 'UserFriendly - Web Designer',
363 'url' => 'http://ars.userfriendly.org/cartoons/?id=20121206',
364 'description' => 'Naming conventions... #private',
365 'tags' => 'samba cartoon web',
366 'private' => true,
367 ),
368 );
369 $refDB = new \ReferenceLinkDB();
370 $refDB->setLinks($links);
371 $refDB->write(self::$testDatastore);
372 $linkDB = new LinkDB(self::$testDatastore, true, false);
373
374 $checksum = hash_file('sha1', self::$testDatastore);
375
376 $this->conf->set('resource.data_dir', 'sandbox');
377 $this->conf->set('resource.datastore', self::$testDatastore);
378
379 $updater = new Updater(array(), $linkDB, $this->conf, true);
380 $this->assertTrue($updater->updateMethodDatastoreIds());
381
382 $linkDB = new LinkDB(self::$testDatastore, true, false);
383
384 $backup = glob($this->conf->get('resource.data_dir') . '/datastore.'. date('YmdH') .'*.php');
385 $backup = $backup[0];
386
387 $this->assertFileExists($backup);
388 $this->assertEquals($checksum, hash_file('sha1', $backup));
389 unlink($backup);
390
391 $this->assertEquals(3, count($linkDB));
392 $this->assertTrue(isset($linkDB[0]));
393 $this->assertFalse(isset($linkDB[0]['linkdate']));
394 $this->assertEquals(0, $linkDB[0]['id']);
395 $this->assertEquals('UserFriendly - Web Designer', $linkDB[0]['title']);
396 $this->assertEquals('http://ars.userfriendly.org/cartoons/?id=20121206', $linkDB[0]['url']);
397 $this->assertEquals('Naming conventions... #private', $linkDB[0]['description']);
398 $this->assertEquals('samba cartoon web', $linkDB[0]['tags']);
399 $this->assertTrue($linkDB[0]['private']);
400 $this->assertEquals(
401 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'),
402 $linkDB[0]['created']
403 );
404 192
405 $this->assertTrue(isset($linkDB[1])); 193 static::assertSame('/subfolder/', $this->conf->get('general.header_link'));
406 $this->assertFalse(isset($linkDB[1]['linkdate']));
407 $this->assertEquals(1, $linkDB[1]['id']);
408 $this->assertEquals('UserFriendly - Samba', $linkDB[1]['title']);
409 $this->assertEquals(
410 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'),
411 $linkDB[1]['created']
412 );
413
414 $this->assertTrue(isset($linkDB[2]));
415 $this->assertFalse(isset($linkDB[2]['linkdate']));
416 $this->assertEquals(2, $linkDB[2]['id']);
417 $this->assertEquals('Geek and Poke', $linkDB[2]['title']);
418 $this->assertEquals(
419 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'),
420 $linkDB[2]['created']
421 );
422 $this->assertEquals(
423 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_190301'),
424 $linkDB[2]['updated']
425 );
426 } 194 }
427 195
428 /** 196 public function testUpdateMethodRelativeHomeLinkDoNotRename(): void
429 * Test updateMethodDatastoreIds() with the update already applied: nothing to do.
430 */
431 public function testDatastoreIdsNothingToDo()
432 { 197 {
433 $refDB = new \ReferenceLinkDB(); 198 $this->conf->set('general.header_link', '~/my-blog');
434 $refDB->write(self::$testDatastore);
435 $linkDB = new LinkDB(self::$testDatastore, true, false);
436 199
437 $this->conf->set('resource.data_dir', 'sandbox'); 200 $this->updater->updateMethodRelativeHomeLink();
438 $this->conf->set('resource.datastore', self::$testDatastore);
439 201
440 $checksum = hash_file('sha1', self::$testDatastore); 202 static::assertSame('~/my-blog', $this->conf->get('general.header_link'));
441 $updater = new Updater(array(), $linkDB, $this->conf, true);
442 $this->assertTrue($updater->updateMethodDatastoreIds());
443 $this->assertEquals($checksum, hash_file('sha1', self::$testDatastore));
444 } 203 }
445 204
446 /** 205 public function testUpdateMethodMigrateExistingNotesUrl(): void
447 * Test defaultTheme update with default settings: nothing to do.
448 */
449 public function testDefaultThemeWithDefaultSettings()
450 { 206 {
451 $sandbox = 'sandbox/config'; 207 $this->updater->updateMethodMigrateExistingNotesUrl();
452 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
453 $this->conf = new ConfigManager($sandbox);
454 $updater = new Updater([], [], $this->conf, true);
455 $this->assertTrue($updater->updateMethodDefaultTheme());
456
457 $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
458 $this->assertEquals('default', $this->conf->get('resource.theme'));
459 $this->conf = new ConfigManager($sandbox);
460 $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
461 $this->assertEquals('default', $this->conf->get('resource.theme'));
462 unlink($sandbox . '.json.php');
463 }
464 208
465 /** 209 static::assertSame($this->refDB->getLinks()[0]->getUrl(), $this->bookmarkService->get(0)->getUrl());
466 * Test defaultTheme update with a custom theme in a subfolder 210 static::assertSame($this->refDB->getLinks()[1]->getUrl(), $this->bookmarkService->get(1)->getUrl());
467 */ 211 static::assertSame($this->refDB->getLinks()[4]->getUrl(), $this->bookmarkService->get(4)->getUrl());
468 public function testDefaultThemeWithCustomTheme() 212 static::assertSame($this->refDB->getLinks()[6]->getUrl(), $this->bookmarkService->get(6)->getUrl());
469 { 213 static::assertSame($this->refDB->getLinks()[7]->getUrl(), $this->bookmarkService->get(7)->getUrl());
470 $theme = 'iamanartist'; 214 static::assertSame($this->refDB->getLinks()[8]->getUrl(), $this->bookmarkService->get(8)->getUrl());
471 $sandbox = 'sandbox/config'; 215 static::assertSame($this->refDB->getLinks()[9]->getUrl(), $this->bookmarkService->get(9)->getUrl());
472 copy(self::$configFile . '.json.php', $sandbox . '.json.php'); 216 static::assertSame('/shaare/WDWyig', $this->bookmarkService->get(42)->getUrl());
473 $this->conf = new ConfigManager($sandbox); 217 static::assertSame('/shaare/WDWyig', $this->bookmarkService->get(41)->getUrl());
474 mkdir('sandbox/'. $theme); 218 static::assertSame('/shaare/0gCTjQ', $this->bookmarkService->get(10)->getUrl());
475 touch('sandbox/'. $theme .'/linklist.html'); 219 static::assertSame('/shaare/PCRizQ', $this->bookmarkService->get(11)->getUrl());
476 $this->conf->set('resource.raintpl_tpl', 'sandbox/'. $theme .'/');
477 $updater = new Updater([], [], $this->conf, true);
478 $this->assertTrue($updater->updateMethodDefaultTheme());
479
480 $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
481 $this->assertEquals($theme, $this->conf->get('resource.theme'));
482 $this->conf = new ConfigManager($sandbox);
483 $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
484 $this->assertEquals($theme, $this->conf->get('resource.theme'));
485 unlink($sandbox . '.json.php');
486 unlink('sandbox/'. $theme .'/linklist.html');
487 rmdir('sandbox/'. $theme);
488 }
489
490 /**
491 * Test updateMethodEscapeMarkdown with markdown plugin enabled
492 * => setting markdown_escape set to false.
493 */
494 public function testEscapeMarkdownSettingToFalse()
495 {
496 $sandboxConf = 'sandbox/config';
497 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
498 $this->conf = new ConfigManager($sandboxConf);
499
500 $this->conf->set('general.enabled_plugins', ['markdown']);
501 $updater = new Updater([], [], $this->conf, true);
502 $this->assertTrue($updater->updateMethodEscapeMarkdown());
503 $this->assertFalse($this->conf->get('security.markdown_escape'));
504
505 // reload from file
506 $this->conf = new ConfigManager($sandboxConf);
507 $this->assertFalse($this->conf->get('security.markdown_escape'));
508 }
509
510
511 /**
512 * Test updateMethodEscapeMarkdown with markdown plugin disabled
513 * => setting markdown_escape set to true.
514 */
515 public function testEscapeMarkdownSettingToTrue()
516 {
517 $sandboxConf = 'sandbox/config';
518 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
519 $this->conf = new ConfigManager($sandboxConf);
520
521 $this->conf->set('general.enabled_plugins', []);
522 $updater = new Updater([], [], $this->conf, true);
523 $this->assertTrue($updater->updateMethodEscapeMarkdown());
524 $this->assertTrue($this->conf->get('security.markdown_escape'));
525
526 // reload from file
527 $this->conf = new ConfigManager($sandboxConf);
528 $this->assertTrue($this->conf->get('security.markdown_escape'));
529 }
530
531 /**
532 * Test updateMethodEscapeMarkdown with nothing to do (setting already enabled)
533 */
534 public function testEscapeMarkdownSettingNothingToDoEnabled()
535 {
536 $sandboxConf = 'sandbox/config';
537 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
538 $this->conf = new ConfigManager($sandboxConf);
539 $this->conf->set('security.markdown_escape', true);
540 $updater = new Updater([], [], $this->conf, true);
541 $this->assertTrue($updater->updateMethodEscapeMarkdown());
542 $this->assertTrue($this->conf->get('security.markdown_escape'));
543 }
544
545 /**
546 * Test updateMethodEscapeMarkdown with nothing to do (setting already disabled)
547 */
548 public function testEscapeMarkdownSettingNothingToDoDisabled()
549 {
550 $this->conf->set('security.markdown_escape', false);
551 $updater = new Updater([], [], $this->conf, true);
552 $this->assertTrue($updater->updateMethodEscapeMarkdown());
553 $this->assertFalse($this->conf->get('security.markdown_escape'));
554 }
555
556 /**
557 * Test updateMethodPiwikUrl with valid data
558 */
559 public function testUpdatePiwikUrlValid()
560 {
561 $sandboxConf = 'sandbox/config';
562 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
563 $this->conf = new ConfigManager($sandboxConf);
564 $url = 'mypiwik.tld';
565 $this->conf->set('plugins.PIWIK_URL', $url);
566 $updater = new Updater([], [], $this->conf, true);
567 $this->assertTrue($updater->updateMethodPiwikUrl());
568 $this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
569
570 // reload from file
571 $this->conf = new ConfigManager($sandboxConf);
572 $this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
573 }
574
575 /**
576 * Test updateMethodPiwikUrl without setting
577 */
578 public function testUpdatePiwikUrlEmpty()
579 {
580 $updater = new Updater([], [], $this->conf, true);
581 $this->assertTrue($updater->updateMethodPiwikUrl());
582 $this->assertEmpty($this->conf->get('plugins.PIWIK_URL'));
583 }
584
585 /**
586 * Test updateMethodPiwikUrl: valid URL, nothing to do
587 */
588 public function testUpdatePiwikUrlNothingToDo()
589 {
590 $url = 'https://mypiwik.tld';
591 $this->conf->set('plugins.PIWIK_URL', $url);
592 $updater = new Updater([], [], $this->conf, true);
593 $this->assertTrue($updater->updateMethodPiwikUrl());
594 $this->assertEquals($url, $this->conf->get('plugins.PIWIK_URL'));
595 }
596
597 /**
598 * Test updateMethodAtomDefault with show_atom set to false
599 * => update to true.
600 */
601 public function testUpdateMethodAtomDefault()
602 {
603 $sandboxConf = 'sandbox/config';
604 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
605 $this->conf = new ConfigManager($sandboxConf);
606 $this->conf->set('feed.show_atom', false);
607 $updater = new Updater([], [], $this->conf, true);
608 $this->assertTrue($updater->updateMethodAtomDefault());
609 $this->assertTrue($this->conf->get('feed.show_atom'));
610 // reload from file
611 $this->conf = new ConfigManager($sandboxConf);
612 $this->assertTrue($this->conf->get('feed.show_atom'));
613 }
614 /**
615 * Test updateMethodAtomDefault with show_atom not set.
616 * => nothing to do
617 */
618 public function testUpdateMethodAtomDefaultNoExist()
619 {
620 $sandboxConf = 'sandbox/config';
621 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
622 $this->conf = new ConfigManager($sandboxConf);
623 $updater = new Updater([], [], $this->conf, true);
624 $this->assertTrue($updater->updateMethodAtomDefault());
625 $this->assertTrue($this->conf->get('feed.show_atom'));
626 }
627 /**
628 * Test updateMethodAtomDefault with show_atom set to true.
629 * => nothing to do
630 */
631 public function testUpdateMethodAtomDefaultAlreadyTrue()
632 {
633 $sandboxConf = 'sandbox/config';
634 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
635 $this->conf = new ConfigManager($sandboxConf);
636 $this->conf->set('feed.show_atom', true);
637 $updater = new Updater([], [], $this->conf, true);
638 $this->assertTrue($updater->updateMethodAtomDefault());
639 $this->assertTrue($this->conf->get('feed.show_atom'));
640 }
641
642 /**
643 * Test updateMethodDownloadSizeAndTimeoutConf, it should be set if none is already defined.
644 */
645 public function testUpdateMethodDownloadSizeAndTimeoutConf()
646 {
647 $sandboxConf = 'sandbox/config';
648 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
649 $this->conf = new ConfigManager($sandboxConf);
650 $updater = new Updater([], [], $this->conf, true);
651 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
652 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
653 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
654
655 $this->conf = new ConfigManager($sandboxConf);
656 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
657 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
658 }
659
660 /**
661 * Test updateMethodDownloadSizeAndTimeoutConf, it shouldn't be set if it is already defined.
662 */
663 public function testUpdateMethodDownloadSizeAndTimeoutConfIgnore()
664 {
665 $sandboxConf = 'sandbox/config';
666 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
667 $this->conf = new ConfigManager($sandboxConf);
668 $this->conf->set('general.download_max_size', 38);
669 $this->conf->set('general.download_timeout', 70);
670 $updater = new Updater([], [], $this->conf, true);
671 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
672 $this->assertEquals(38, $this->conf->get('general.download_max_size'));
673 $this->assertEquals(70, $this->conf->get('general.download_timeout'));
674 }
675
676 /**
677 * Test updateMethodDownloadSizeAndTimeoutConf, only the maz size should be set here.
678 */
679 public function testUpdateMethodDownloadSizeAndTimeoutConfOnlySize()
680 {
681 $sandboxConf = 'sandbox/config';
682 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
683 $this->conf = new ConfigManager($sandboxConf);
684 $this->conf->set('general.download_max_size', 38);
685 $updater = new Updater([], [], $this->conf, true);
686 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
687 $this->assertEquals(38, $this->conf->get('general.download_max_size'));
688 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
689 }
690
691 /**
692 * Test updateMethodDownloadSizeAndTimeoutConf, only the time out should be set here.
693 */
694 public function testUpdateMethodDownloadSizeAndTimeoutConfOnlyTimeout()
695 {
696 $sandboxConf = 'sandbox/config';
697 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
698 $this->conf = new ConfigManager($sandboxConf);
699 $this->conf->set('general.download_timeout', 3);
700 $updater = new Updater([], [], $this->conf, true);
701 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
702 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
703 $this->assertEquals(3, $this->conf->get('general.download_timeout'));
704 }
705
706 /**
707 * Test updateMethodWebThumbnailer with thumbnails enabled.
708 */
709 public function testUpdateMethodWebThumbnailerEnabled()
710 {
711 $this->conf->remove('thumbnails');
712 $this->conf->set('thumbnail.enable_thumbnails', true);
713 $updater = new Updater([], [], $this->conf, true, $_SESSION);
714 $this->assertTrue($updater->updateMethodWebThumbnailer());
715 $this->assertFalse($this->conf->exists('thumbnail'));
716 $this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode'));
717 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
718 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
719 $this->assertContains('You have enabled or changed thumbnails', $_SESSION['warnings'][0]);
720 }
721
722 /**
723 * Test updateMethodWebThumbnailer with thumbnails disabled.
724 */
725 public function testUpdateMethodWebThumbnailerDisabled()
726 {
727 $this->conf->remove('thumbnails');
728 $this->conf->set('thumbnail.enable_thumbnails', false);
729 $updater = new Updater([], [], $this->conf, true, $_SESSION);
730 $this->assertTrue($updater->updateMethodWebThumbnailer());
731 $this->assertFalse($this->conf->exists('thumbnail'));
732 $this->assertEquals(Thumbnailer::MODE_NONE, $this->conf->get('thumbnails.mode'));
733 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
734 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
735 $this->assertTrue(empty($_SESSION['warnings']));
736 }
737
738 /**
739 * Test updateMethodWebThumbnailer with thumbnails disabled.
740 */
741 public function testUpdateMethodWebThumbnailerNothingToDo()
742 {
743 $updater = new Updater([], [], $this->conf, true, $_SESSION);
744 $this->assertTrue($updater->updateMethodWebThumbnailer());
745 $this->assertFalse($this->conf->exists('thumbnail'));
746 $this->assertEquals(Thumbnailer::MODE_COMMON, $this->conf->get('thumbnails.mode'));
747 $this->assertEquals(90, $this->conf->get('thumbnails.width'));
748 $this->assertEquals(53, $this->conf->get('thumbnails.height'));
749 $this->assertTrue(empty($_SESSION['warnings']));
750 }
751
752 /**
753 * Test updateMethodSetSticky().
754 */
755 public function testUpdateStickyValid()
756 {
757 $blank = [
758 'id' => 1,
759 'url' => 'z',
760 'title' => '',
761 'description' => '',
762 'tags' => '',
763 'created' => new DateTime(),
764 ];
765 $links = [
766 1 => ['id' => 1] + $blank,
767 2 => ['id' => 2] + $blank,
768 ];
769 $refDB = new \ReferenceLinkDB();
770 $refDB->setLinks($links);
771 $refDB->write(self::$testDatastore);
772 $linkDB = new LinkDB(self::$testDatastore, true, false);
773
774 $updater = new Updater(array(), $linkDB, $this->conf, true);
775 $this->assertTrue($updater->updateMethodSetSticky());
776
777 $linkDB = new LinkDB(self::$testDatastore, true, false);
778 foreach ($linkDB as $link) {
779 $this->assertFalse($link['sticky']);
780 }
781 }
782
783 /**
784 * Test updateMethodSetSticky().
785 */
786 public function testUpdateStickyNothingToDo()
787 {
788 $blank = [
789 'id' => 1,
790 'url' => 'z',
791 'title' => '',
792 'description' => '',
793 'tags' => '',
794 'created' => new DateTime(),
795 ];
796 $links = [
797 1 => ['id' => 1, 'sticky' => true] + $blank,
798 2 => ['id' => 2] + $blank,
799 ];
800 $refDB = new \ReferenceLinkDB();
801 $refDB->setLinks($links);
802 $refDB->write(self::$testDatastore);
803 $linkDB = new LinkDB(self::$testDatastore, true, false);
804
805 $updater = new Updater(array(), $linkDB, $this->conf, true);
806 $this->assertTrue($updater->updateMethodSetSticky());
807
808 $linkDB = new LinkDB(self::$testDatastore, true, false);
809 $this->assertTrue($linkDB[1]['sticky']);
810 }
811
812 /**
813 * Test updateMethodRemoveRedirector().
814 */
815 public function testUpdateRemoveRedirector()
816 {
817 $sandboxConf = 'sandbox/config';
818 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
819 $this->conf = new ConfigManager($sandboxConf);
820 $updater = new Updater([], null, $this->conf, true);
821 $this->assertTrue($updater->updateMethodRemoveRedirector());
822 $this->assertFalse($this->conf->exists('redirector'));
823 $this->conf = new ConfigManager($sandboxConf);
824 $this->assertFalse($this->conf->exists('redirector'));
825 } 220 }
826} 221}
diff --git a/tests/utils/FakeBookmarkService.php b/tests/utils/FakeBookmarkService.php
new file mode 100644
index 00000000..1ec5bc3d
--- /dev/null
+++ b/tests/utils/FakeBookmarkService.php
@@ -0,0 +1,18 @@
1<?php
2
3
4use Shaarli\Bookmark\BookmarkArray;
5use Shaarli\Bookmark\BookmarkFilter;
6use Shaarli\Bookmark\BookmarkIO;
7use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Bookmark\Exception\EmptyDataStoreException;
9use Shaarli\Config\ConfigManager;
10use Shaarli\History;
11
12class FakeBookmarkService extends BookmarkFileService
13{
14 public function getBookmarks()
15 {
16 return $this->bookmarks;
17 }
18}
diff --git a/tests/utils/ReferenceHistory.php b/tests/utils/ReferenceHistory.php
index e411c417..516c9f51 100644
--- a/tests/utils/ReferenceHistory.php
+++ b/tests/utils/ReferenceHistory.php
@@ -76,7 +76,7 @@ class ReferenceHistory
76 } 76 }
77 77
78 /** 78 /**
79 * Returns the number of links in the reference data 79 * Returns the number of bookmarks in the reference data
80 */ 80 */
81 public function count() 81 public function count()
82 { 82 {
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index c12bcb67..fc3cb109 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -1,30 +1,39 @@
1<?php 1<?php
2 2
3use Shaarli\Bookmark\LinkDB; 3use Shaarli\Bookmark\Bookmark;
4use Shaarli\Bookmark\BookmarkArray;
4 5
5/** 6/**
6 * Populates a reference datastore to test LinkDB 7 * Populates a reference datastore to test Bookmark
7 */ 8 */
8class ReferenceLinkDB 9class ReferenceLinkDB
9{ 10{
10 public static $NB_LINKS_TOTAL = 11; 11 public static $NB_LINKS_TOTAL = 11;
11 12
12 private $_links = array(); 13 private $bookmarks = array();
13 private $_publicCount = 0; 14 private $_publicCount = 0;
14 private $_privateCount = 0; 15 private $_privateCount = 0;
15 16
17 private $isLegacy;
18
16 /** 19 /**
17 * Populates the test DB with reference data 20 * Populates the test DB with reference data
21 *
22 * @param bool $isLegacy Use links as array instead of Bookmark object
18 */ 23 */
19 public function __construct() 24 public function __construct($isLegacy = false)
20 { 25 {
26 $this->isLegacy = $isLegacy;
27 if (! $this->isLegacy) {
28 $this->bookmarks = new BookmarkArray();
29 }
21 $this->addLink( 30 $this->addLink(
22 11, 31 11,
23 'Pined older', 32 'Pined older',
24 '?PCRizQ', 33 '/shaare/PCRizQ',
25 'This is an older pinned link', 34 'This is an older pinned link',
26 0, 35 0,
27 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20100309_101010'), 36 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100309_101010'),
28 '', 37 '',
29 null, 38 null,
30 'PCRizQ', 39 'PCRizQ',
@@ -34,10 +43,10 @@ class ReferenceLinkDB
34 $this->addLink( 43 $this->addLink(
35 10, 44 10,
36 'Pined', 45 'Pined',
37 '?0gCTjQ', 46 '/shaare/0gCTjQ',
38 'This is a pinned link', 47 'This is a pinned link',
39 0, 48 0,
40 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121207_152312'), 49 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121207_152312'),
41 '', 50 '',
42 null, 51 null,
43 '0gCTjQ', 52 '0gCTjQ',
@@ -47,10 +56,10 @@ class ReferenceLinkDB
47 $this->addLink( 56 $this->addLink(
48 41, 57 41,
49 'Link title: @website', 58 'Link title: @website',
50 '?WDWyig', 59 '/shaare/WDWyig',
51 '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',
52 0, 61 0,
53 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), 62 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'),
54 'sTuff', 63 'sTuff',
55 null, 64 null,
56 'WDWyig' 65 'WDWyig'
@@ -59,10 +68,10 @@ class ReferenceLinkDB
59 $this->addLink( 68 $this->addLink(
60 42, 69 42,
61 'Note: I have a big ID but an old date', 70 'Note: I have a big ID but an old date',
62 '?WDWyig', 71 '/shaare/WDWyig',
63 'Used to test links reordering.', 72 'Used to test bookmarks reordering.',
64 0, 73 0,
65 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20100310_101010'), 74 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
66 'ut' 75 'ut'
67 ); 76 );
68 77
@@ -72,7 +81,7 @@ class ReferenceLinkDB
72 'http://www.php-fig.org/psr/psr-2/', 81 'http://www.php-fig.org/psr/psr-2/',
73 '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.',
74 0, 83 0,
75 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_152312'), 84 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'),
76 '' 85 ''
77 ); 86 );
78 87
@@ -82,9 +91,9 @@ class ReferenceLinkDB
82 'https://static.fsf.org/nosvn/faif-2.0.pdf', 91 'https://static.fsf.org/nosvn/faif-2.0.pdf',
83 'Richard Stallman and the Free Software Revolution. Read this. #hashtag', 92 'Richard Stallman and the Free Software Revolution. Read this. #hashtag',
84 0, 93 0,
85 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'), 94 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'),
86 'free gnu software stallman -exclude stuff hashtag', 95 'free gnu software stallman -exclude stuff hashtag',
87 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160803_093033') 96 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160803_093033')
88 ); 97 );
89 98
90 $this->addLink( 99 $this->addLink(
@@ -93,9 +102,9 @@ class ReferenceLinkDB
93 'http://mediagoblin.org/', 102 'http://mediagoblin.org/',
94 'A free software media publishing platform #hashtagOther', 103 'A free software media publishing platform #hashtagOther',
95 0, 104 0,
96 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 105 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130614_184135'),
97 'gnu media web .hidden hashtag', 106 'gnu media web .hidden hashtag',
98 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130615_184230'), 107 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130615_184230'),
99 'IuWvgA' 108 'IuWvgA'
100 ); 109 );
101 110
@@ -105,7 +114,7 @@ class ReferenceLinkDB
105 'https://dvcs.w3.org/hg/markup-validator/summary', 114 'https://dvcs.w3.org/hg/markup-validator/summary',
106 'Mercurial repository for the W3C Validator #private', 115 'Mercurial repository for the W3C Validator #private',
107 1, 116 1,
108 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20141125_084734'), 117 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20141125_084734'),
109 'css html w3c web Mercurial' 118 'css html w3c web Mercurial'
110 ); 119 );
111 120
@@ -115,7 +124,7 @@ class ReferenceLinkDB
115 'http://ars.userfriendly.org/cartoons/?id=20121206', 124 'http://ars.userfriendly.org/cartoons/?id=20121206',
116 'Naming conventions... #private', 125 'Naming conventions... #private',
117 0, 126 0,
118 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'), 127 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_142300'),
119 'dev cartoon web' 128 'dev cartoon web'
120 ); 129 );
121 130
@@ -125,7 +134,7 @@ class ReferenceLinkDB
125 'http://ars.userfriendly.org/cartoons/?id=20010306', 134 'http://ars.userfriendly.org/cartoons/?id=20010306',
126 'Tropical printing', 135 'Tropical printing',
127 0, 136 0,
128 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'), 137 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_172539'),
129 'samba cartoon web' 138 'samba cartoon web'
130 ); 139 );
131 140
@@ -135,7 +144,7 @@ class ReferenceLinkDB
135 'http://geek-and-poke.com/', 144 'http://geek-and-poke.com/',
136 '', 145 '',
137 1, 146 1,
138 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'), 147 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_182539'),
139 'dev cartoon tag1 tag2 tag3 tag4 ' 148 'dev cartoon tag1 tag2 tag3 tag4 '
140 ); 149 );
141 } 150 }
@@ -164,10 +173,15 @@ class ReferenceLinkDB
164 'tags' => $tags, 173 'tags' => $tags,
165 'created' => $date, 174 'created' => $date,
166 'updated' => $updated, 175 'updated' => $updated,
167 'shorturl' => $shorturl ? $shorturl : smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id), 176 'shorturl' => $shorturl ? $shorturl : smallHash($date->format(Bookmark::LINK_DATE_FORMAT) . $id),
168 'sticky' => $pinned 177 'sticky' => $pinned
169 ); 178 );
170 $this->_links[$id] = $link; 179 if (! $this->isLegacy) {
180 $bookmark = new Bookmark();
181 $this->bookmarks[$id] = $bookmark->fromArray($link);
182 } else {
183 $this->bookmarks[$id] = $link;
184 }
171 185
172 if ($private) { 186 if ($private) {
173 $this->_privateCount++; 187 $this->_privateCount++;
@@ -184,37 +198,38 @@ class ReferenceLinkDB
184 $this->reorder(); 198 $this->reorder();
185 file_put_contents( 199 file_put_contents(
186 $filename, 200 $filename,
187 '<?php /* '.base64_encode(gzdeflate(serialize($this->_links))).' */ ?>' 201 '<?php /* '.base64_encode(gzdeflate(serialize($this->bookmarks))).' */ ?>'
188 ); 202 );
189 } 203 }
190 204
191 /** 205 /**
192 * Reorder links by creation date (newest first). 206 * Reorder links by creation date (newest first).
193 * 207 *
194 * Also update the urls and ids mapping arrays.
195 *
196 * @param string $order ASC|DESC 208 * @param string $order ASC|DESC
197 */ 209 */
198 public function reorder($order = 'DESC') 210 public function reorder($order = 'DESC')
199 { 211 {
200 // backward compatibility: ignore reorder if the the `created` field doesn't exist 212 if (! $this->isLegacy) {
201 if (! isset(array_values($this->_links)[0]['created'])) { 213 $this->bookmarks->reorder($order);
202 return; 214 } else {
203 } 215 $order = $order === 'ASC' ? -1 : 1;
204 216 // backward compatibility: ignore reorder if the the `created` field doesn't exist
205 $order = $order === 'ASC' ? -1 : 1; 217 if (! isset(array_values($this->bookmarks)[0]['created'])) {
206 // Reorder array by dates. 218 return;
207 usort($this->_links, function ($a, $b) use ($order) {
208 if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
209 return $a['sticky'] ? -1 : 1;
210 } 219 }
211 220
212 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; 221 usort($this->bookmarks, function ($a, $b) use ($order) {
213 }); 222 if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
223 return $a['sticky'] ? -1 : 1;
224 }
225
226 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
227 });
228 }
214 } 229 }
215 230
216 /** 231 /**
217 * Returns the number of links in the reference data 232 * Returns the number of bookmarks in the reference data
218 */ 233 */
219 public function countLinks() 234 public function countLinks()
220 { 235 {
@@ -222,7 +237,7 @@ class ReferenceLinkDB
222 } 237 }
223 238
224 /** 239 /**
225 * Returns the number of public links in the reference data 240 * Returns the number of public bookmarks in the reference data
226 */ 241 */
227 public function countPublicLinks() 242 public function countPublicLinks()
228 { 243 {
@@ -230,7 +245,7 @@ class ReferenceLinkDB
230 } 245 }
231 246
232 /** 247 /**
233 * Returns the number of private links in the reference data 248 * Returns the number of private bookmarks in the reference data
234 */ 249 */
235 public function countPrivateLinks() 250 public function countPrivateLinks()
236 { 251 {
@@ -238,14 +253,20 @@ class ReferenceLinkDB
238 } 253 }
239 254
240 /** 255 /**
241 * Returns the number of links without tag 256 * Returns the number of bookmarks without tag
242 */ 257 */
243 public function countUntaggedLinks() 258 public function countUntaggedLinks()
244 { 259 {
245 $cpt = 0; 260 $cpt = 0;
246 foreach ($this->_links as $link) { 261 foreach ($this->bookmarks as $link) {
247 if (empty($link['tags'])) { 262 if (! $this->isLegacy) {
248 ++$cpt; 263 if (empty($link->getTags())) {
264 ++$cpt;
265 }
266 } else {
267 if (empty($link['tags'])) {
268 ++$cpt;
269 }
249 } 270 }
250 } 271 }
251 return $cpt; 272 return $cpt;
@@ -254,16 +275,16 @@ class ReferenceLinkDB
254 public function getLinks() 275 public function getLinks()
255 { 276 {
256 $this->reorder(); 277 $this->reorder();
257 return $this->_links; 278 return $this->bookmarks;
258 } 279 }
259 280
260 /** 281 /**
261 * Setter to override link creation. 282 * Setter to override link creation.
262 * 283 *
263 * @param array $links List of links. 284 * @param array $links List of bookmarks.
264 */ 285 */
265 public function setLinks($links) 286 public function setLinks($links)
266 { 287 {
267 $this->_links = $links; 288 $this->bookmarks = $links;
268 } 289 }
269} 290}
diff --git a/tests/utils/config/configJson.json.php b/tests/utils/config/configJson.json.php
index 1549ddfc..b04dc303 100644
--- a/tests/utils/config/configJson.json.php
+++ b/tests/utils/config/configJson.json.php
@@ -41,12 +41,12 @@
41 "foo": "bar" 41 "foo": "bar"
42 }, 42 },
43 "resource": { 43 "resource": {
44 "datastore": "tests\/utils\/config\/datastore.php", 44 "datastore": "sandbox/datastore.php",
45 "data_dir": "sandbox\/", 45 "data_dir": "sandbox\/",
46 "raintpl_tpl": "tpl\/", 46 "raintpl_tpl": "tpl\/",
47 "config": "data\/config.php", 47 "config": "data\/config.php",
48 "ban_file": "data\/ipbans.php", 48 "ban_file": "data\/ipbans.php",
49 "updates": "data\/updates.txt", 49 "updates": "sandbox/updates.txt",
50 "log": "data\/log.txt", 50 "log": "data\/log.txt",
51 "update_check": "data\/lastupdatecheck.txt", 51 "update_check": "data\/lastupdatecheck.txt",
52 "history": "data\/history.php", 52 "history": "data\/history.php",
@@ -59,7 +59,7 @@
59 "WALLABAG_VERSION": 1 59 "WALLABAG_VERSION": 1
60 }, 60 },
61 "dev": { 61 "dev": {
62 "debug": true 62 "debug": false
63 }, 63 },
64 "updates": { 64 "updates": {
65 "check_updates": false, 65 "check_updates": false,
diff --git a/tpl/default/404.html b/tpl/default/404.html
index 472566a6..7b696e4c 100644
--- a/tpl/default/404.html
+++ b/tpl/default/404.html
@@ -6,9 +6,9 @@
6<body> 6<body>
7<div id="pageheader"> 7<div id="pageheader">
8 {include="page.header"} 8 {include="page.header"}
9<div class="center" id="page404" class="page404-container"> 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..67d3ebd1 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">
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..16c55896 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">
@@ -32,7 +32,7 @@
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 c1a6a6bc..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>
@@ -68,6 +68,28 @@
68 </select> 68 </select>
69 </div> 69 </div>
70 </div> 70 </div>
71 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
72 <div class="form-label">
73 <label for="formatter">
74 <span class="label-name">{'Description formatter'|t}</span>
75 </label>
76 </div>
77 </div>
78 <div class="pure-u-lg-{$ratioInput} pure-u-1">
79 <div class="form-input">
80 <select name="formatter" id="formatter" class="align">
81 {loop="$formatter_available"}
82 <option value="{$value}"
83 {if="$value===$formatter"}
84 selected="selected"
85 {/if}
86 >
87 {$value|ucfirst}
88 </option>
89 {/loop}
90 </select>
91 </div>
92 </div>
71 </div> 93 </div>
72 <div class="pure-g"> 94 <div class="pure-g">
73 <div class="pure-u-lg-{$ratioLabel} pure-u-1"> 95 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
@@ -267,7 +289,7 @@
267 {if="! $gd_enabled"} 289 {if="! $gd_enabled"}
268 {'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}
269 {elseif="$thumbnails_enabled"} 291 {elseif="$thumbnails_enabled"}
270 <a href="?do=thumbs_update">{'Synchronize thumbnails'|t}</a> 292 <a href="{$base_path}/admin/thumbnails">{'Synchronize thumbnails'|t}</a>
271 {/if} 293 {/if}
272 </span> 294 </span>
273 </label> 295 </label>
diff --git a/tpl/default/daily.html b/tpl/default/daily.html
index 6b5103a4..3ab8053f 100644
--- a/tpl/default/daily.html
+++ b/tpl/default/daily.html
@@ -11,7 +11,7 @@
11 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily"> 11 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor" id="daily">
12 <h2 class="window-title"> 12 <h2 class="window-title">
13 {'The Daily Shaarli'|t} 13 {'The Daily Shaarli'|t}
14 <a href="?do=dailyrss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a> 14 <a href="{$base_path}/daily-rss" title="{'1 RSS entry per day'|t}"><i class="fa fa-rss"></i></a>
15 </h2> 15 </h2>
16 16
17 <div id="plugin_zone_start_daily" class="plugin_zone"> 17 <div id="plugin_zone_start_daily" class="plugin_zone">
@@ -25,7 +25,7 @@
25 <div class="pure-g"> 25 <div class="pure-g">
26 <div class="pure-u-lg-1-3 pure-u-1 center"> 26 <div class="pure-u-lg-1-3 pure-u-1 center">
27 {if="$previousday"} 27 {if="$previousday"}
28 <a href="?do=daily&amp;day={$previousday}"> 28 <a href="{$base_path}/daily?day={$previousday}">
29 <i class="fa fa-arrow-left"></i> 29 <i class="fa fa-arrow-left"></i>
30 {'Previous day'|t} 30 {'Previous day'|t}
31 </a> 31 </a>
@@ -36,7 +36,7 @@
36 </div> 36 </div>
37 <div class="pure-u-lg-1-3 pure-u-1 center"> 37 <div class="pure-u-lg-1-3 pure-u-1 center">
38 {if="$nextday"} 38 {if="$nextday"}
39 <a href="?do=daily&amp;day={$nextday}"> 39 <a href="{$base_path}/daily?day={$nextday}">
40 {'Next day'|t} 40 {'Next day'|t}
41 <i class="fa fa-arrow-right"></i> 41 <i class="fa fa-arrow-right"></i>
42 </a> 42 </a>
@@ -69,7 +69,7 @@
69 {$link=$value} 69 {$link=$value}
70 <div class="daily-entry"> 70 <div class="daily-entry">
71 <div class="daily-entry-title center"> 71 <div class="daily-entry-title center">
72 <a href="?{$link.shorturl}" title="{'Permalink'|t}"> 72 <a href="{$base_path}/?{$link.shorturl}" title="{'Permalink'|t}">
73 <i class="fa fa-link"></i> 73 <i class="fa fa-link"></i>
74 </a> 74 </a>
75 <a href="{$link.real_url}">{$link.title}</a> 75 <a href="{$link.real_url}">{$link.title}</a>
@@ -85,7 +85,7 @@
85 {if="$link.tags"} 85 {if="$link.tags"}
86 <div class="daily-entry-tags center"> 86 <div class="daily-entry-tags center">
87 {loop="link.taglist"} 87 {loop="link.taglist"}
88 <span class="label label-tag" title="Add tag"> 88 <span class="label label-tag">
89 {$value} 89 {$value}
90 </span> 90 </span>
91 {/loop} 91 {/loop}
@@ -116,7 +116,7 @@
116 </div> 116 </div>
117</div> 117</div>
118{include="page.footer"} 118{include="page.footer"}
119<script src="js/thumbnails.min.js?v={$version_hash}"></script> 119<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
120</body> 120</body>
121</html> 121</html>
122 122
diff --git a/tpl/default/dailyrss.html b/tpl/default/dailyrss.html
index f589b06e..d40d9496 100644
--- a/tpl/default/dailyrss.html
+++ b/tpl/default/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 {/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} - {/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>
27 {/loop}
28 ]]></description>
29 </item>
30 {/loop}
31 </channel>
32</rss><!-- Cached version of {$page_url} -->
diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html
index df14535d..568545bd 100644
--- a/tpl/default/editlink.html
+++ b/tpl/default/editlink.html
@@ -7,11 +7,14 @@
7 {include="page.header"} 7 {include="page.header"}
8 <div id="editlinkform" class="edit-link-container" class="pure-g"> 8 <div id="editlinkform" class="edit-link-container" class="pure-g">
9 <div class="pure-u-lg-1-5 pure-u-1-24"></div> 9 <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"> 10 <form method="post"
11 name="linkform"
12 action="{$base_path}/admin/shaare"
13 class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"
14 >
11 <h2 class="window-title"> 15 <h2 class="window-title">
12 {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} 16 {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
13 </h2> 17 </h2>
14 <input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
15 {if="isset($link.id)"} 18 {if="isset($link.id)"}
16 <input type="hidden" name="lf_id" value="{$link.id}"> 19 <input type="hidden" name="lf_id" value="{$link.id}">
17 {/if} 20 {/if}
@@ -20,7 +23,7 @@
20 <label for="lf_url">{'URL'|t}</label> 23 <label for="lf_url">{'URL'|t}</label>
21 </div> 24 </div>
22 <div> 25 <div>
23 <input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input autofocus"> 26 <input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input">
24 </div> 27 </div>
25 <div> 28 <div>
26 <label for="lf_title">{'Title'|t}</label> 29 <label for="lf_title">{'Title'|t}</label>
@@ -50,6 +53,15 @@
50 &nbsp;<label for="lf_private">{'Private'|t}</label> 53 &nbsp;<label for="lf_private">{'Private'|t}</label>
51 </div> 54 </div>
52 55
56 {if="$formatter==='markdown'"}
57 <div class="md_help">
58 {'Description will be rendered with'|t}
59 <a href="http://daringfireball.net/projects/markdown/syntax" title="{'Markdown syntax documentation'|t}">
60 {'Markdown syntax'|t}
61 </a>.
62 </div>
63 {/if}
64
53 <div id="editlink-plugins"> 65 <div id="editlink-plugins">
54 {loop="$edit_link_plugin"} 66 {loop="$edit_link_plugin"}
55 {$value} 67 {$value}
@@ -61,7 +73,7 @@
61 <input type="submit" name="save_edit" class="" id="button-save-edit" 73 <input type="submit" name="save_edit" class="" id="button-save-edit"
62 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}"> 74 value="{if="$link_is_new"}{'Save'|t}{else}{'Apply Changes'|t}{/if}">
63 {if="!$link_is_new"} 75 {if="!$link_is_new"}
64 <a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}" 76 <a href="{$base_path}/admin/shaare/delete?id={$link.id}&amp;token={$token}"
65 title="" name="delete_link" class="button button-red confirm-delete"> 77 title="" name="delete_link" class="button button-red confirm-delete">
66 {'Delete'|t} 78 {'Delete'|t}
67 </a> 79 </a>
@@ -69,6 +81,7 @@
69 </div> 81 </div>
70 82
71 <input type="hidden" name="token" value="{$token}"> 83 <input type="hidden" name="token" value="{$token}">
84 <input type="hidden" name="source" value="{$source}">
72 {if="$http_referer"} 85 {if="$http_referer"}
73 <input type="hidden" name="returnurl" value="{$http_referer}"> 86 <input type="hidden" name="returnurl" value="{$http_referer}">
74 {/if} 87 {/if}
diff --git a/tpl/default/error.html b/tpl/default/error.html
new file mode 100644
index 00000000..c3e0c3c1
--- /dev/null
+++ b/tpl/default/error.html
@@ -0,0 +1,22 @@
1<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head>
4 {include="includes"}
5</head>
6<body>
7<div id="pageheader">
8 {include="page.header"}
9<div id="pageError" class="page-error-container center">
10 <h2>{$message}</h2>
11
12 {if="!empty($stacktrace)"}
13 <pre>
14 {$stacktrace}
15 </pre>
16 {/if}
17
18 <img src="{$asset_path}/img/sad_star.png#" alt="">
19</div>
20{include="page.footer"}
21</body>
22</html>
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 29187505..dd58bd1e 100644
--- a/tpl/default/feed.atom.html
+++ b/tpl/default/feed.atom.html
@@ -6,11 +6,13 @@
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}
12 <author> 14 <author>
13 <name>{$index_url}</name> 15 <name>{$pagetitle}</name>
14 <uri>{$index_url}</uri> 16 <uri>{$index_url}</uri>
15 </author> 17 </author>
16 <id>{$index_url}</id> 18 <id>{$index_url}</id>
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 6c30d1bf..227f9b52 100644
--- a/tpl/default/includes.html
+++ b/tpl/default/includes.html
@@ -3,18 +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'"}
12 <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" />
13{/if}
11{loop="$plugins_includes.css_files"} 14{loop="$plugins_includes.css_files"}
12 <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/> 15 <link type="text/css" rel="stylesheet" href="{$base_path}/{$value}?v={$version_hash}#"/>
13{/loop} 16{/loop}
14{if="is_file('data/user.css')"} 17{if="is_file('data/user.css')"}
15 <link type="text/css" rel="stylesheet" href="data/user.css#" /> 18 <link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />
16{/if} 19{/if}
17<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}" />
18{if="! empty($links) && count($links) === 1"} 22{if="! empty($links) && count($links) === 1"}
19 {$link=reset($links)} 23 {$link=reset($links)}
20 <meta property="og:title" content="{$link.title}" /> 24 <meta property="og:title" content="{$link.title}" />
@@ -22,12 +26,12 @@
22 <meta property="og:url" content="{$index_url}?{$link.shorturl}" /> 26 <meta property="og:url" content="{$index_url}?{$link.shorturl}" />
23 {$ogDescription=isset($link.description_src) ? $link.description_src : $link.description} 27 {$ogDescription=isset($link.description_src) ? $link.description_src : $link.description}
24 <meta property="og:description" content="{function="substr(strip_tags($ogDescription), 0, 300)"}" /> 28 <meta property="og:description" content="{function="substr(strip_tags($ogDescription), 0, 300)"}" />
25 {if="$link.thumbnail"} 29 {if="!empty($link.thumbnail)"}
26 <meta property="og:image" content="{$index_url}{$link.thumbnail}" /> 30 <meta property="og:image" content="{$index_url}{$link.thumbnail}" />
27 {/if} 31 {/if}
28 {if="!$hide_timestamps || $is_logged_in"} 32 {if="!$hide_timestamps || $is_logged_in"}
29 <meta property="article:published_time" content="{$link.created->format(DateTime::ATOM)}" /> 33 <meta property="article:published_time" content="{$link.created->format(DateTime::ATOM)}" />
30 {if="$link.updated"} 34 {if="!empty($link.updated)"}
31 <meta property="article:modified_time" content="{$link.updated->format(DateTime::ATOM)}" /> 35 <meta property="article:modified_time" content="{$link.updated->format(DateTime::ATOM)}" />
32 {/if} 36 {/if}
33 {/if} 37 {/if}
diff --git a/tpl/default/install.html b/tpl/default/install.html
index c6f501f0..a506a2eb 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">
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index ffc236c7..b08773d8 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}
@@ -138,7 +140,7 @@
138 <div class="thumbnail"> 140 <div class="thumbnail">
139 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} 141 {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"> 142 <a href="{$value.real_url}" aria-hidden="true" tabindex="-1">
141 <img data-src="{$value.thumbnail}#" class="b-lazy" 143 <img data-src="{$base_path}/{$value.thumbnail}#" class="b-lazy"
142 src="" 144 src=""
143 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" /> 145 alt="" width="{$thumbnails_width}" height="{$thumbnails_height}" />
144 </a> 146 </a>
@@ -181,7 +183,7 @@
181 {$tag_counter=count($value.taglist)} 183 {$tag_counter=count($value.taglist)}
182 {loop="value.taglist"} 184 {loop="value.taglist"}
183 <span class="label label-tag" title="{$strAddTag}"> 185 <span class="label label-tag" title="{$strAddTag}">
184 <a href="?addtag={$value|urlencode}">{$value}</a> 186 <a href="{$base_path}/add-tag/{$value1.urlencoded_taglist.$key2}">{$value}</a>
185 </span> 187 </span>
186 {if="$tag_counter - 1 != $counter"}&middot;{/if} 188 {if="$tag_counter - 1 != $counter"}&middot;{/if}
187 {/loop} 189 {/loop}
@@ -196,16 +198,16 @@
196 <input type="checkbox" class="link-checkbox" value="{$value.id}"> 198 <input type="checkbox" class="link-checkbox" value="{$value.id}">
197 </span> 199 </span>
198 <span class="linklist-item-infos-controls-item ctrl-edit"> 200 <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> 201 <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> 202 </span>
201 <span class="linklist-item-infos-controls-item ctrl-delete"> 203 <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}" 204 <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"> 205 title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
204 <i class="fa fa-trash" aria-hidden="true"></i> 206 <i class="fa fa-trash" aria-hidden="true"></i>
205 </a> 207 </a>
206 </span> 208 </span>
207 <span class="linklist-item-infos-controls-item ctrl-pin"> 209 <span class="linklist-item-infos-controls-item ctrl-pin">
208 <a href="?do=pin&amp;id={$value.id}&amp;token={$token}" 210 <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"> 211 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> 212 <i class="fa fa-thumb-tack" aria-hidden="true"></i>
211 </a> 213 </a>
@@ -222,7 +224,7 @@
222 </div> 224 </div>
223 {/if} 225 {/if}
224 {/if} 226 {/if}
225 <a href="?{$value.shorturl}" title="{$strPermalink}"> 227 <a href="{$base_path}/shaare/{$value.shorturl}" title="{$strPermalink}">
226 {if="!$hide_timestamps || $is_logged_in"} 228 {if="!$hide_timestamps || $is_logged_in"}
227 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink} 229 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
228 <span class="linkdate" title="{$updated}"> 230 <span class="linkdate" title="{$updated}">
@@ -265,12 +267,22 @@
265 {/if} 267 {/if}
266 {if="$is_logged_in"} 268 {if="$is_logged_in"}
267 &middot; 269 &middot;
268 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" aria-label="{$strDelete}" 270 <a href="{$base_path}/admin/shaare/delete?id={$value.id}&amp;token={$token}" aria-label="{$strDelete}"
269 title="{$strDelete}" class="delete-link confirm-delete"> 271 title="{$strDelete}" class="delete-link confirm-delete">
270 <i class="fa fa-trash" aria-hidden="true"></i> 272 <i class="fa fa-trash" aria-hidden="true"></i>
271 </a> 273 </a>
272 &middot; 274 &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> 275 <a href="{$base_path}/admin/shaare/{$value.id}" aria-label="{$strEdit}" title="{$strEdit}">
276 <i class="fa fa-pencil-square-o edit-link" aria-hidden="true"></i>
277 </a>
278 &middot;
279 <a href="{$base_path}/admin/shaare/{$value.id}/pin?token={$token}"
280 aria-label="{$strToggleSticky}"
281 title="{$strToggleSticky}"
282 class="pin-link {if="$value.sticky"}pinned-link{/if}"
283 >
284 <i class="fa fa-thumb-tack" aria-hidden="true"></i>
285 </a>
274 {/if} 286 {/if}
275 </div> 287 </div>
276 </div> 288 </div>
@@ -295,6 +307,6 @@
295</div> 307</div>
296 308
297{include="page.footer"} 309{include="page.footer"}
298<script src="js/thumbnails.min.js?v={$version_hash}"></script> 310<script src="{$asset_path}/js/thumbnails.min.js?v={$version_hash}#"></script>
299</body> 311</body>
300</html> 312</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/loginform.html b/tpl/default/loginform.html
index 761aec0c..90c2b2b6 100644
--- a/tpl/default/loginform.html
+++ b/tpl/default/loginform.html
@@ -5,44 +5,32 @@
5</head> 5</head>
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8{if="!$user_can_login"} 8<div class="pure-g">
9<div class="pure-g pure-alert pure-alert-error pure-alert-closable center"> 9 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
10 <div class="pure-u-2-24"></div> 10 <div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24 login-form-container">
11 <div class="pure-u-20-24"> 11 <form method="post" name="loginform">
12 <p>{'You have been banned after too many failed login attempts. Try again later.'|t}</p> 12 <h2 class="window-title">{'Login'|t}</h2>
13 </div> 13 <div>
14 <div class="pure-u-2-24"> 14 <input type="text" name="login" aria-label="{'Username'|t}" placeholder="{'Username'|t}"
15 <i class="fa fa-times pure-alert-close"></i> 15 {if="!empty($username)"}value="{$username}"{/if} class="autofocus">
16 </div>
17 <div>
18 <input type="password" name="password" aria-label="{'Password'|t}" placeholder="{'Password'|t}" class="autofocus">
19 </div>
20 <div class="remember-me">
21 <input type="checkbox" name="longlastingsession" id="longlastingsessionform"
22 {if="$remember_user_default"}checked="checked"{/if}>
23 <label for="longlastingsessionform">{'Remember me'|t}</label>
24 </div>
25 <div>
26 <input type="submit" value="{'Login'|t}" class="bigbutton">
27 </div>
28 <input type="hidden" name="token" value="{$token}">
29 {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
30 </form>
16 </div> 31 </div>
32 <div class="pure-u-lg-1-3 pure-u-1-8"></div>
17</div> 33</div>
18{else}
19 <div class="pure-g">
20 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
21 <div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24 login-form-container">
22 <form method="post" name="loginform">
23 <h2 class="window-title">{'Login'|t}</h2>
24 <div>
25 <input type="text" name="login" aria-label="{'Username'|t}" placeholder="{'Username'|t}"
26 {if="!empty($username)"}value="{$username}"{/if} class="autofocus">
27 </div>
28 <div>
29 <input type="password" name="password" aria-label="{'Password'|t}" placeholder="{'Password'|t}" class="autofocus">
30 </div>
31 <div class="remember-me">
32 <input type="checkbox" name="longlastingsession" id="longlastingsessionform"
33 {if="$remember_user_default"}checked="checked"{/if}>
34 <label for="longlastingsessionform">{'Remember me'|t}</label>
35 </div>
36 <div>
37 <input type="submit" value="{'Login'|t}" class="bigbutton">
38 </div>
39 <input type="hidden" name="token" value="{$token}">
40 {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
41 </form>
42 </div>
43 <div class="pure-u-lg-1-3 pure-u-1-8"></div>
44 </div>
45{/if}
46 34
47{include="page.footer"} 35{include="page.footer"}
48</body> 36</body>
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..51bdb2f0 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="{$base_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="{$base_path}/{$value}#"></script>
29{/loop} 29{/loop}
30 30
31<div id="js-translations" class="hidden"> 31<div id="js-translations" class="hidden">
@@ -39,4 +39,5 @@
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 4f063dc3..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="?do=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="?do=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..b7a56c89 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="{$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..05d13556 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">
@@ -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/tag.cloud.html b/tpl/default/tag.cloud.html
index b9c0b162..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>
@@ -32,6 +32,7 @@
32 {/if} 32 {/if}
33 autocomplete="off" data-multiple data-autofirst data-minChars="1" 33 autocomplete="off" data-multiple data-autofirst data-minChars="1"
34 data-list="{loop="$tags"}{$key}, {/loop}" 34 data-list="{loop="$tags"}{$key}, {/loop}"
35 class="autofocus"
35 > 36 >
36 <button type="submit" class="search-button" aria-label="{'Search'|t}"><i class="fa fa-search" aria-hidden="true"></i></button> 37 <button type="submit" class="search-button" aria-label="{'Search'|t}"><i class="fa fa-search" aria-hidden="true"></i></button>
37 </form> 38 </form>
@@ -47,8 +48,8 @@
47 48
48 <div id="cloudtag" class="cloudtag-container"> 49 <div id="cloudtag" class="cloudtag-container">
49 {loop="tags"} 50 {loop="tags"}
50 <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
51 ><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>
52 {loop="$value.tag_plugin"} 53 {loop="$value.tag_plugin"}
53 {$value} 54 {$value}
54 {/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 f1939798..504644ca 100644
--- a/tpl/default/thumbnails.html
+++ b/tpl/default/thumbnails.html
@@ -38,11 +38,11 @@
38 </div> 38 </div>
39 </div> 39 </div>
40 40
41 <input type="hidden" name="ids" value="{function="implode($ids, ',')"}" /> 41 <input type="hidden" name="ids" value="{function="implode(',', $ids)"}" />
42 </div> 42 </div>
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..2cb08e38 100644
--- a/tpl/default/tools.html
+++ b/tpl/default/tools.html
@@ -11,35 +11,35 @@
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 {if="!$openshaarli"} 23 {if="!$openshaarli"}
24 <div class="tools-item"> 24 <div class="tools-item">
25 <a href="?do=changepasswd" title="{'Change your password'|t}"> 25 <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> 26 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Change password'|t}</span>
27 </a> 27 </a>
28 </div> 28 </div>
29 {/if} 29 {/if}
30 <div class="tools-item"> 30 <div class="tools-item">
31 <a href="?do=changetag" title="{'Rename or delete a tag in all links'|t}"> 31 <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> 32 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Manage tags'|t}</span>
33 </a> 33 </a>
34 </div> 34 </div>
35 <div class="tools-item"> 35 <div class="tools-item">
36 <a href="?do=import" 36 <a href="{$base_path}/admin/import"
37 title="{'Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, delicious...)'|t}"> 37 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> 38 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Import links'|t}</span>
39 </a> 39 </a>
40 </div> 40 </div>
41 <div class="tools-item"> 41 <div class="tools-item">
42 <a href="?do=export" 42 <a href="{$base_path}/admin/export"
43 title="{'Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)'|t}"> 43 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> 44 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Export database'|t}</span>
45 </a> 45 </a>
@@ -47,7 +47,7 @@
47 47
48 {if="$thumbnails_enabled"} 48 {if="$thumbnails_enabled"}
49 <div class="tools-item"> 49 <div class="tools-item">
50 <a href="?do=thumbs_update" title="{'Synchronize all link thumbnails'|t}"> 50 <a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
51 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span> 51 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
52 </a> 52 </a>
53 </div> 53 </div>
@@ -86,7 +86,7 @@
86 alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}'); 86 alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}');
87 } 87 }
88 window.open( 88 window.open(
89 '{$pageabsaddr}?post='%20+%20encodeURIComponent(url)+ 89 '{$pageabsaddr}admin/shaare?post='%20+%20encodeURIComponent(url)+
90 '&amp;title='%20+%20encodeURIComponent(title)+ 90 '&amp;title='%20+%20encodeURIComponent(title)+
91 '&amp;description='%20+%20encodeURIComponent(desc)+ 91 '&amp;description='%20+%20encodeURIComponent(desc)+
92 '&amp;source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1' 92 '&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 160286a5..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>
@@ -33,6 +33,19 @@
33 </tr> 33 </tr>
34 34
35 <tr> 35 <tr>
36 <td><b>Description formatter:</b></td>
37 <td>
38 <select name="formatter" id="formatter">
39 {loop="$formatter_available"}
40 <option value="{$value}" {if="$value===$formatter"}selected{/if}>
41 {$value|ucfirst}
42 </option>
43 {/loop}
44 </select>
45 </td>
46 </tr>
47
48 <tr>
36 <td><b>Timezone:</b></td> 49 <td><b>Timezone:</b></td>
37 <td> 50 <td>
38 <select id="continent" name="continent"> 51 <select id="continent" name="continent">
@@ -146,7 +159,7 @@
146 {if="! $gd_enabled"} 159 {if="! $gd_enabled"}
147 {'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}
148 {elseif="$thumbnails_enabled"} 161 {elseif="$thumbnails_enabled"}
149 <a href="?do=thumbs_update">{'Synchonize thumbnails'|t}</a> 162 <a href="{$base_path}/admin/thumbnails">{'Synchonize thumbnails'|t}</a>
150 {/if} 163 {/if}
151 </label> 164 </label>
152 </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 5fa7d194..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}
@@ -26,7 +21,16 @@
26 <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input" 21 <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input"
27 data-list="{loop="$tags"}{$key}, {/loop}" data-multiple autocomplete="off" ><br> 22 data-list="{loop="$tags"}{$key}, {/loop}" data-multiple autocomplete="off" ><br>
28 23
29 {loop="$edit_link_plugin"} 24 {if="$formatter==='markdown'"}
25 <div class="md_help">
26 {'Description will be rendered with'|t}
27 <a href="http://daringfireball.net/projects/markdown/syntax" title="{'Markdown syntax documentation'|t}">
28 {'Markdown syntax'|t}
29 </a>.
30 </div>
31 {/if}
32
33 {loop="$edit_link_plugin"}
30 {$value} 34 {$value}
31 {/loop} 35 {/loop}
32 36
@@ -38,21 +42,19 @@
38 &nbsp;<label for="lf_private"><i>Private</i></label><br><br> 42 &nbsp;<label for="lf_private"><i>Private</i></label><br><br>
39 {/if} 43 {/if}
40 <input type="submit" value="Save" name="save_edit" class="bigbutton"> 44 <input type="submit" value="Save" name="save_edit" class="bigbutton">
41 <input type="submit" value="Cancel" name="cancel_edit" class="bigbutton">
42 {if="!$link_is_new && isset($link.id)"} 45 {if="!$link_is_new && isset($link.id)"}
43 <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}"
44 name="delete_link" class="bigbutton" 47 name="delete_link" class="bigbutton"
45 onClick="return confirmDeleteLink();"> 48 onClick="return confirmDeleteLink();">
46 {'Delete'|t} 49 {'Delete'|t}
47 </a> 50 </a>
48 {/if} 51 {/if}
49 <input type="hidden" name="token" value="{$token}"> 52 <input type="hidden" name="token" value="{$token}">
53 <input type="hidden" name="source" value="{$source}">
50 {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}
51 </form> 55 </form>
52 </div> 56 </div>
53</div> 57</div>
54{if="$source !== 'firefoxsocialapi'"}
55{include="page.footer"} 58{include="page.footer"}
56{/if}
57</body> 59</body>
58</html> 60</html>
diff --git a/tpl/vintage/error.html b/tpl/vintage/error.html
new file mode 100644
index 00000000..64f54cd2
--- /dev/null
+++ b/tpl/vintage/error.html
@@ -0,0 +1,25 @@
1<!DOCTYPE html>
2<html>
3<head>
4 {include="includes"}
5</head>
6<body>
7<div id="pageheader">
8 {include="page.header"}
9</div>
10<div class="error-container">
11 <h1>Error</h1>
12 <p>{$message}</p>
13
14 {if="!empty($stacktrace)"}
15 <br>
16 <pre>
17 {$stacktrace}
18 </pre>
19 {/if}
20
21 <p>Would you mind <a href="{$base_path}/">clicking here</a>?</p>
22</div>
23{include="page.footer"}
24</body>
25</html>
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 49798e85..5919bb49 100644
--- a/tpl/vintage/feed.atom.html
+++ b/tpl/vintage/feed.atom.html
@@ -6,13 +6,13 @@
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}
14 <author> 14 <author>
15 <name>{$index_url}</name> 15 <name>{$pagetitle}</name>
16 <uri>{$index_url}</uri> 16 <uri>{$index_url}</uri>
17 </author> 17 </author>
18 <id>{$index_url}</id> 18 <id>{$index_url}</id>
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 2efb6b10..eac05701 100644
--- a/tpl/vintage/includes.html
+++ b/tpl/vintage/includes.html
@@ -3,15 +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'"}
11 <link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" />
12{/if}
10{loop="$plugins_includes.css_files"} 13{loop="$plugins_includes.css_files"}
11<link type="text/css" rel="stylesheet" href="{$value}#"/> 14<link type="text/css" rel="stylesheet" href="{$base_path}/{$value}#"/>
12{/loop} 15{/loop}
13{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}
14<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}" />
15{if="! empty($links) && count($links) === 1"} 19{if="! empty($links) && count($links) === 1"}
16 {$link=reset($links)} 20 {$link=reset($links)}
17 <meta property="og:title" content="{$link.title}" /> 21 <meta property="og:title" content="{$link.title}" />
@@ -19,12 +23,12 @@
19 <meta property="og:url" content="{$index_url}?{$link.shorturl}" /> 23 <meta property="og:url" content="{$index_url}?{$link.shorturl}" />
20 {$ogDescription=isset($link.description_src) ? $link.description_src : $link.description} 24 {$ogDescription=isset($link.description_src) ? $link.description_src : $link.description}
21 <meta property="og:description" content="{function="mb_substr(strip_tags($ogDescription), 0, 300)"}" /> 25 <meta property="og:description" content="{function="mb_substr(strip_tags($ogDescription), 0, 300)"}" />
22 {if="$link.thumbnail"} 26 {if="!empty($link.thumbnail)"}
23 <meta property="og:image" content="{$index_url}{$link.thumbnail}" /> 27 <meta property="og:image" content="{$index_url}{$link.thumbnail}" />
24 {/if} 28 {/if}
25 {if="!$hide_timestamps || $is_logged_in"} 29 {if="!$hide_timestamps || $is_logged_in"}
26 <meta property="article:published_time" content="{$link.created->format(DateTime::ATOM)}" /> 30 <meta property="article:published_time" content="{$link.created->format(DateTime::ATOM)}" />
27 {if="$link.updated"} 31 {if="!empty($link.updated)"}
28 <meta property="article:modified_time" content="{$link.updated->format(DateTime::ATOM)}" /> 32 <meta property="article:modified_time" content="{$link.updated->format(DateTime::ATOM)}" />
29 {/if} 33 {/if}
30 {/if} 34 {/if}
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..00896eb5 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,12 +65,12 @@
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>
@@ -84,7 +83,7 @@
84 <div class="thumbnail"> 83 <div class="thumbnail">
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,7 @@
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>
158 156
159</body> 157</body>
160</html> 158</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 0f7d6387..6aa20ab1 100644
--- a/tpl/vintage/loginform.html
+++ b/tpl/vintage/loginform.html
@@ -2,36 +2,30 @@
2<html> 2<html>
3<head>{include="includes"}</head> 3<head>{include="includes"}</head>
4<body 4<body
5{if="$user_can_login"} 5{if="empty($username)"}
6 {if="empty($username)"} 6 onload="document.loginform.login.focus();"
7 onload="document.loginform.login.focus();" 7{else}
8 {else} 8 onload="document.loginform.password.focus();"
9 onload="document.loginform.password.focus();"
10 {/if}
11{/if}> 9{/if}>
12<div id="pageheader"> 10<div id="pageheader">
13 {include="page.header"} 11 {include="page.header"}
14 12
15 <div id="headerform"> 13 <div id="headerform">
16 {if="!$user_can_login"} 14 <form method="post" name="loginform" action="{$base_path}/login">
17 You have been banned from login after too many failed attempts. Try later. 15 <label for="login">Login: <input type="text" id="login" name="login" tabindex="1"
18 {else} 16 {if="!empty($username)"}value="{$username}"{/if}>
19 <form method="post" name="loginform"> 17 </label>
20 <label for="login">Login: <input type="text" id="login" name="login" tabindex="1" 18 <label for="password">Password: <input type="password" id="password" name="password" tabindex="2">
21 {if="!empty($username)"}value="{$username}"{/if}> 19 </label>
22 </label> 20 <input type="submit" value="Login" class="bigbutton" tabindex="4">
23 <label for="password">Password: <input type="password" id="password" name="password" tabindex="2"> 21 <label for="longlastingsession">
24 </label> 22 <input type="checkbox" name="longlastingsession"
25 <input type="submit" value="Login" class="bigbutton" tabindex="4"> 23 id="longlastingsession" tabindex="3"
26 <label for="longlastingsession"> 24 {if="$remember_user_default"}checked="checked"{/if}>
27 <input type="checkbox" name="longlastingsession" 25 Stay signed in (Do not check on public computers)</label>
28 id="longlastingsession" tabindex="3" 26 <input type="hidden" name="token" value="{$token}">
29 {if="$remember_user_default"}checked="checked"{/if}> 27 {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
30 Stay signed in (Do not check on public computers)</label> 28 </form>
31 <input type="hidden" name="token" value="{$token}">
32 {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
33 </form>
34 {/if}
35 </div> 29 </div>
36</div> 30</div>
37 31
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 40c53e5b..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="?do=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 79aebf8d..18f296f7 100644
--- a/tpl/vintage/thumbnails.html
+++ b/tpl/vintage/thumbnails.html
@@ -20,9 +20,9 @@
20 </div> 20 </div>
21</div> 21</div>
22 22
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 ed548c73..a73758cc 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -2,26 +2,21 @@ 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: {
26 thumbnails: './assets/common/js/thumbnails.js', 21 thumbnails: './assets/common/js/thumbnails.js',
27 thumbnails_update: './assets/common/js/thumbnails-update.js', 22 thumbnails_update: './assets/common/js/thumbnails-update.js',
@@ -30,6 +25,7 @@ module.exports = [
30 './assets/default/js/base.js', 25 './assets/default/js/base.js',
31 './assets/default/scss/shaarli.scss', 26 './assets/default/scss/shaarli.scss',
32 ].concat(glob.sync('./assets/default/img/*')), 27 ].concat(glob.sync('./assets/default/img/*')),
28 markdown: './assets/common/css/markdown.css',
33 }, 29 },
34 output: { 30 output: {
35 filename: '[name].min.js', 31 filename: '[name].min.js',
@@ -44,23 +40,23 @@ module.exports = [
44 loader: 'babel-loader', 40 loader: 'babel-loader',
45 options: { 41 options: {
46 presets: [ 42 presets: [
47 'babel-preset-env', 43 '@babel/preset-env',
48 ] 44 ]
49 } 45 }
50 } 46 }
51 }, 47 },
52 { 48 {
53 test: /\.scss/, 49 test: /\.s?css/,
54 use: extractCssDefault.extract({ 50 use: [
55 use: [{ 51 {
56 loader: "css-loader", 52 loader: MiniCssExtractPlugin.loader,
57 options: { 53 options: {
58 minimize: true, 54 publicPath: 'tpl/default/css/',
59 } 55 },
60 }, { 56 },
61 loader: "sass-loader" 57 'css-loader',
62 }], 58 'sass-loader',
63 }) 59 ],
64 }, 60 },
65 { 61 {
66 test: /\.(gif|png|jpe?g|svg|ico)$/i, 62 test: /\.(gif|png|jpe?g|svg|ico)$/i,
@@ -80,23 +76,28 @@ module.exports = [
80 options: { 76 options: {
81 name: '../fonts/[name].[ext]', 77 name: '../fonts/[name].[ext]',
82 // do not add a publicPath here because it's already handled by CSS's publicPath 78 // do not add a publicPath here because it's already handled by CSS's publicPath
83 publicPath: '', 79 publicPath: '../default/',
84 } 80 }
85 }, 81 },
86 ], 82 ],
87 }, 83 },
84 optimization: {
85 minimize: true,
86 minimizer: [new TerserPlugin()],
87 },
88 plugins: [ 88 plugins: [
89 new MinifyPlugin(), 89 extractCss,
90 extractCssDefault,
91 ], 90 ],
92 }, 91 },
93 { 92 {
93 mode: 'production',
94 entry: { 94 entry: {
95 shaarli: [ 95 shaarli: [
96 './assets/vintage/js/base.js', 96 './assets/vintage/js/base.js',
97 './assets/vintage/css/reset.css', 97 './assets/vintage/css/reset.css',
98 './assets/vintage/css/shaarli.css', 98 './assets/vintage/css/shaarli.css',
99 ].concat(glob.sync('./assets/vintage/img/*')), 99 ].concat(glob.sync('./assets/vintage/img/*')),
100 markdown: './assets/common/css/markdown.css',
100 thumbnails: './assets/common/js/thumbnails.js', 101 thumbnails: './assets/common/js/thumbnails.js',
101 thumbnails_update: './assets/common/js/thumbnails-update.js', 102 thumbnails_update: './assets/common/js/thumbnails-update.js',
102 }, 103 },
@@ -113,21 +114,23 @@ module.exports = [
113 loader: 'babel-loader', 114 loader: 'babel-loader',
114 options: { 115 options: {
115 presets: [ 116 presets: [
116 'babel-preset-env', 117 '@babel/preset-env',
117 ] 118 ]
118 } 119 }
119 } 120 }
120 }, 121 },
121 { 122 {
122 test: /\.css$/, 123 test: /\.css$/,
123 use: extractCssVintage.extract({ 124 use: [
124 use: [{ 125 {
125 loader: "css-loader", 126 loader: MiniCssExtractPlugin.loader,
126 options: { 127 options: {
127 minimize: true, 128 publicPath: 'tpl/vintage/css/',
128 } 129 },
129 }], 130 },
130 }) 131 'css-loader',
132 'sass-loader',
133 ],
131 }, 134 },
132 { 135 {
133 test: /\.(gif|png|jpe?g|svg|ico)$/i, 136 test: /\.(gif|png|jpe?g|svg|ico)$/i,
@@ -143,9 +146,12 @@ module.exports = [
143 }, 146 },
144 ], 147 ],
145 }, 148 },
149 optimization: {
150 minimize: true,
151 minimizer: [new TerserPlugin()],
152 },
146 plugins: [ 153 plugins: [
147 new MinifyPlugin(), 154 extractCss,
148 extractCssVintage,
149 ], 155 ],
150 }, 156 },
151]; 157];
diff --git a/yarn.lock b/yarn.lock
index 1aa94f42..0a12820c 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"
@@ -2753,100 +2921,107 @@ hmac-drbg@^1.0.0:
2753 minimalistic-assert "^1.0.0" 2921 minimalistic-assert "^1.0.0"
2754 minimalistic-crypto-utils "^1.0.1" 2922 minimalistic-crypto-utils "^1.0.1"
2755 2923
2756home-or-tmp@^2.0.0: 2924homedir-polyfill@^1.0.1:
2757 version "2.0.0" 2925 version "1.0.3"
2758 resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" 2926 resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
2759 integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= 2927 integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
2760 dependencies: 2928 dependencies:
2761 os-homedir "^1.0.0" 2929 parse-passwd "^1.0.0"
2762 os-tmpdir "^1.0.1"
2763 2930
2764hosted-git-info@^2.1.4: 2931hosted-git-info@^2.1.4:
2765 version "2.7.1" 2932 version "2.8.8"
2766 resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" 2933 resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
2767 integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== 2934 integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
2768 2935
2769html-comment-regex@^1.1.0: 2936html-tags@^3.1.0:
2770 version "1.1.2" 2937 version "3.1.0"
2771 resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" 2938 resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
2772 integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== 2939 integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
2773 2940
2774http-signature@~1.2.0: 2941htmlparser2@^3.10.0:
2775 version "1.2.0" 2942 version "3.10.1"
2776 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 2943 resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
2777 integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= 2944 integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
2778 dependencies: 2945 dependencies:
2779 assert-plus "^1.0.0" 2946 domelementtype "^1.3.1"
2780 jsprim "^1.2.2" 2947 domhandler "^2.3.0"
2781 sshpk "^1.7.0" 2948 domutils "^1.5.1"
2949 entities "^1.1.1"
2950 inherits "^2.0.1"
2951 readable-stream "^3.1.1"
2782 2952
2783https-browserify@^1.0.0: 2953https-browserify@^1.0.0:
2784 version "1.0.0" 2954 version "1.0.0"
2785 resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" 2955 resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
2786 integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= 2956 integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
2787 2957
2788iconv-lite@^0.4.17, iconv-lite@^0.4.4: 2958icss-utils@^4.0.0, icss-utils@^4.1.1:
2789 version "0.4.24" 2959 version "4.1.1"
2790 resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 2960 resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
2791 integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 2961 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: 2962 dependencies:
2805 postcss "^6.0.1" 2963 postcss "^7.0.14"
2806 2964
2807ieee754@^1.1.4: 2965ieee754@^1.1.4:
2808 version "1.1.13" 2966 version "1.1.13"
2809 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" 2967 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
2810 integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== 2968 integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
2811 2969
2812ignore-walk@^3.0.1: 2970iferr@^0.1.5:
2813 version "3.0.1" 2971 version "0.1.5"
2814 resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" 2972 resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
2815 integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== 2973 integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
2974
2975ignore@^4.0.6:
2976 version "4.0.6"
2977 resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
2978 integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
2979
2980ignore@^5.1.4, ignore@^5.1.8:
2981 version "5.1.8"
2982 resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
2983 integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
2984
2985import-fresh@^3.0.0, import-fresh@^3.2.1:
2986 version "3.2.1"
2987 resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
2988 integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
2816 dependencies: 2989 dependencies:
2817 minimatch "^3.0.4" 2990 parent-module "^1.0.0"
2991 resolve-from "^4.0.0"
2818 2992
2819ignore@^3.1.2, ignore@^3.3.3: 2993import-lazy@^4.0.0:
2820 version "3.3.10" 2994 version "4.0.0"
2821 resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" 2995 resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153"
2822 integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== 2996 integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==
2997
2998import-local@^2.0.0:
2999 version "2.0.0"
3000 resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
3001 integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
3002 dependencies:
3003 pkg-dir "^3.0.0"
3004 resolve-cwd "^2.0.0"
2823 3005
2824imurmurhash@^0.1.4: 3006imurmurhash@^0.1.4:
2825 version "0.1.4" 3007 version "0.1.4"
2826 resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 3008 resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
2827 integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= 3009 integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
2828 3010
2829in-publish@^2.0.0: 3011indent-string@^4.0.0:
2830 version "2.0.0" 3012 version "4.0.0"
2831 resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" 3013 resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
2832 integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= 3014 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 3015
2841indexes-of@^1.0.1: 3016indexes-of@^1.0.1:
2842 version "1.0.1" 3017 version "1.0.1"
2843 resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" 3018 resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
2844 integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= 3019 integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
2845 3020
2846indexof@0.0.1: 3021infer-owner@^1.0.3, infer-owner@^1.0.4:
2847 version "0.0.1" 3022 version "1.0.4"
2848 resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 3023 resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
2849 integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= 3024 integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
2850 3025
2851inflight@^1.0.4: 3026inflight@^1.0.4:
2852 version "1.0.6" 3027 version "1.0.6"
@@ -2856,82 +3031,38 @@ inflight@^1.0.4:
2856 once "^1.3.0" 3031 once "^1.3.0"
2857 wrappy "1" 3032 wrappy "1"
2858 3033
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: 3034inherits@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" 3035 version "2.0.4"
2861 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 3036 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
2862 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 3037 integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
2863 3038
2864inherits@2.0.1: 3039inherits@2.0.1:
2865 version "2.0.1" 3040 version "2.0.1"
2866 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" 3041 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
2867 integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= 3042 integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
2868 3043
2869ini@~1.3.0: 3044inherits@2.0.3:
3045 version "2.0.3"
3046 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
3047 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
3048
3049ini@^1.3.4, ini@^1.3.5:
2870 version "1.3.5" 3050 version "1.3.5"
2871 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" 3051 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
2872 integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== 3052 integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
2873 3053
2874inquirer@^0.12.0: 3054interpret@^1.4.0:
2875 version "0.12.0" 3055 version "1.4.0"
2876 resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" 3056 resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
2877 integrity sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34= 3057 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 3058
2918invariant@^2.2.2: 3059invariant@^2.2.2, invariant@^2.2.4:
2919 version "2.2.4" 3060 version "2.2.4"
2920 resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" 3061 resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
2921 integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== 3062 integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
2922 dependencies: 3063 dependencies:
2923 loose-envify "^1.0.0" 3064 loose-envify "^1.0.0"
2924 3065
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: 3066is-accessor-descriptor@^0.1.6:
2936 version "0.1.6" 3067 version "0.1.6"
2937 resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" 3068 resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
@@ -2946,6 +3077,24 @@ is-accessor-descriptor@^1.0.0:
2946 dependencies: 3077 dependencies:
2947 kind-of "^6.0.0" 3078 kind-of "^6.0.0"
2948 3079
3080is-alphabetical@^1.0.0:
3081 version "1.0.4"
3082 resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
3083 integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
3084
3085is-alphanumeric@^1.0.0:
3086 version "1.0.0"
3087 resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4"
3088 integrity sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=
3089
3090is-alphanumerical@^1.0.0:
3091 version "1.0.4"
3092 resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf"
3093 integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==
3094 dependencies:
3095 is-alphabetical "^1.0.0"
3096 is-decimal "^1.0.0"
3097
2949is-arrayish@^0.2.1: 3098is-arrayish@^0.2.1:
2950 version "0.2.1" 3099 version "0.2.1"
2951 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 3100 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -2958,15 +3107,27 @@ is-binary-path@^1.0.0:
2958 dependencies: 3107 dependencies:
2959 binary-extensions "^1.0.0" 3108 binary-extensions "^1.0.0"
2960 3109
3110is-binary-path@~2.1.0:
3111 version "2.1.0"
3112 resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
3113 integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
3114 dependencies:
3115 binary-extensions "^2.0.0"
3116
2961is-buffer@^1.1.5: 3117is-buffer@^1.1.5:
2962 version "1.1.6" 3118 version "1.1.6"
2963 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 3119 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
2964 integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== 3120 integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
2965 3121
2966is-callable@^1.1.4: 3122is-buffer@^2.0.0:
2967 version "1.1.4" 3123 version "2.0.4"
2968 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" 3124 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
2969 integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== 3125 integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
3126
3127is-callable@^1.1.4, is-callable@^1.2.0:
3128 version "1.2.2"
3129 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
3130 integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
2970 3131
2971is-data-descriptor@^0.1.4: 3132is-data-descriptor@^0.1.4:
2972 version "0.1.4" 3133 version "0.1.4"
@@ -2983,9 +3144,14 @@ is-data-descriptor@^1.0.0:
2983 kind-of "^6.0.0" 3144 kind-of "^6.0.0"
2984 3145
2985is-date-object@^1.0.1: 3146is-date-object@^1.0.1:
2986 version "1.0.1" 3147 version "1.0.2"
2987 resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" 3148 resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
2988 integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= 3149 integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
3150
3151is-decimal@^1.0.0, is-decimal@^1.0.2:
3152 version "1.0.4"
3153 resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
3154 integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
2989 3155
2990is-descriptor@^0.1.0: 3156is-descriptor@^0.1.0:
2991 version "0.1.6" 3157 version "0.1.6"
@@ -3022,25 +3188,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" 3188 resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
3023 integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 3189 integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
3024 3190
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: 3191is-fullwidth-code-point@^2.0.0:
3040 version "2.0.0" 3192 version "2.0.0"
3041 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 3193 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
3042 integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 3194 integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
3043 3195
3196is-fullwidth-code-point@^3.0.0:
3197 version "3.0.0"
3198 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
3199 integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
3200
3044is-glob@^3.1.0: 3201is-glob@^3.1.0:
3045 version "3.1.0" 3202 version "3.1.0"
3046 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" 3203 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
@@ -3048,28 +3205,22 @@ is-glob@^3.1.0:
3048 dependencies: 3205 dependencies:
3049 is-extglob "^2.1.0" 3206 is-extglob "^2.1.0"
3050 3207
3051is-glob@^4.0.0: 3208is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
3052 version "4.0.1" 3209 version "4.0.1"
3053 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" 3210 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
3054 integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== 3211 integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
3055 dependencies: 3212 dependencies:
3056 is-extglob "^2.1.1" 3213 is-extglob "^2.1.1"
3057 3214
3058is-my-ip-valid@^1.0.0: 3215is-hexadecimal@^1.0.0:
3059 version "1.0.0" 3216 version "1.0.4"
3060 resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" 3217 resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
3061 integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== 3218 integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
3062 3219
3063is-my-json-valid@^2.10.0: 3220is-negative-zero@^2.0.0:
3064 version "2.20.0" 3221 version "2.0.0"
3065 resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.0.tgz#1345a6fca3e8daefc10d0fa77067f54cedafd59a" 3222 resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
3066 integrity sha512-XTHBZSIIxNsIsZXg7XB5l8z/OBFosl1Wao4tXLpeC7eKU4Vm/kdop2azkPqULwnfGQjmeDIyey9g7afMMtdWAA== 3223 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 3224
3074is-number@^3.0.0: 3225is-number@^3.0.0:
3075 version "3.0.0" 3226 version "3.0.0"
@@ -3078,74 +3229,77 @@ is-number@^3.0.0:
3078 dependencies: 3229 dependencies:
3079 kind-of "^3.0.2" 3230 kind-of "^3.0.2"
3080 3231
3081is-plain-obj@^1.0.0: 3232is-number@^7.0.0:
3233 version "7.0.0"
3234 resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
3235 integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
3236
3237is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
3082 version "1.1.0" 3238 version "1.1.0"
3083 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" 3239 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
3084 integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= 3240 integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
3085 3241
3086is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: 3242is-plain-obj@^2.0.0:
3243 version "2.1.0"
3244 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
3245 integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
3246
3247is-plain-object@^2.0.3, is-plain-object@^2.0.4:
3087 version "2.0.4" 3248 version "2.0.4"
3088 resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" 3249 resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
3089 integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== 3250 integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
3090 dependencies: 3251 dependencies:
3091 isobject "^3.0.1" 3252 isobject "^3.0.1"
3092 3253
3093is-promise@^2.1.0: 3254is-regex@^1.1.0, is-regex@^1.1.1:
3094 version "2.1.0" 3255 version "1.1.1"
3095 resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" 3256 resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
3096 integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= 3257 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: 3258 dependencies:
3108 has "^1.0.1" 3259 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
3115is-stream@^1.1.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 3260
3120is-svg@^2.0.0: 3261is-regexp@^2.0.0:
3121 version "2.1.0" 3262 version "2.1.0"
3122 resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" 3263 resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
3123 integrity sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk= 3264 integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==
3124 dependencies: 3265
3125 html-comment-regex "^1.1.0" 3266is-string@^1.0.5:
3267 version "1.0.5"
3268 resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
3269 integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
3126 3270
3127is-symbol@^1.0.2: 3271is-symbol@^1.0.2:
3128 version "1.0.2" 3272 version "1.0.3"
3129 resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" 3273 resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
3130 integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== 3274 integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
3131 dependencies: 3275 dependencies:
3132 has-symbols "^1.0.0" 3276 has-symbols "^1.0.1"
3133 3277
3134is-typedarray@~1.0.0: 3278is-typedarray@^1.0.0:
3135 version "1.0.0" 3279 version "1.0.0"
3136 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 3280 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
3137 integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 3281 integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
3138 3282
3139is-utf8@^0.2.0: 3283is-whitespace-character@^1.0.0:
3140 version "0.2.1" 3284 version "1.0.4"
3141 resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" 3285 resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7"
3142 integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= 3286 integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==
3143 3287
3144is-windows@^1.0.2: 3288is-windows@^1.0.1, is-windows@^1.0.2:
3145 version "1.0.2" 3289 version "1.0.2"
3146 resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" 3290 resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
3147 integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== 3291 integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
3148 3292
3293is-word-character@^1.0.0:
3294 version "1.0.4"
3295 resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230"
3296 integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==
3297
3298is-wsl@^1.1.0:
3299 version "1.1.0"
3300 resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
3301 integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
3302
3149isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: 3303isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
3150 version "1.0.0" 3304 version "1.0.0"
3151 resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 3305 resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -3168,99 +3322,58 @@ isobject@^3.0.0, isobject@^3.0.1:
3168 resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" 3322 resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
3169 integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= 3323 integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
3170 3324
3171isstream@~0.1.2: 3325jest-worker@^26.3.0:
3172 version "0.1.2" 3326 version "26.3.0"
3173 resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 3327 resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f"
3174 integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= 3328 integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw==
3175 3329 dependencies:
3176js-base64@^2.1.8, js-base64@^2.1.9: 3330 "@types/node" "*"
3177 version "2.5.1" 3331 merge-stream "^2.0.0"
3178 resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" 3332 supports-color "^7.0.0"
3179 integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==
3180 3333
3181"js-tokens@^3.0.0 || ^4.0.0": 3334"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
3182 version "4.0.0" 3335 version "4.0.0"
3183 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 3336 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
3184 integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 3337 integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
3185 3338
3186js-tokens@^3.0.2: 3339js-yaml@^3.13.1:
3187 version "3.0.2" 3340 version "3.14.0"
3188 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" 3341 resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
3189 integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= 3342 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: 3343 dependencies:
3196 argparse "^1.0.7" 3344 argparse "^1.0.7"
3197 esprima "^4.0.0" 3345 esprima "^4.0.0"
3198 3346
3199js-yaml@~3.7.0: 3347jsesc@^2.5.1:
3200 version "3.7.0" 3348 version "2.5.2"
3201 resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" 3349 resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
3202 integrity sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A= 3350 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 3351
3217jsesc@~0.5.0: 3352jsesc@~0.5.0:
3218 version "0.5.0" 3353 version "0.5.0"
3219 resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" 3354 resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
3220 integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= 3355 integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
3221 3356
3222json-loader@^0.5.4: 3357json-parse-better-errors@^1.0.2:
3223 version "0.5.7" 3358 version "1.0.2"
3224 resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" 3359 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== 3360 integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
3226 3361
3227json-schema-traverse@^0.3.0: 3362json-parse-even-better-errors@^2.3.0:
3228 version "0.3.1" 3363 version "2.3.1"
3229 resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" 3364 resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
3230 integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= 3365 integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
3231 3366
3232json-schema-traverse@^0.4.1: 3367json-schema-traverse@^0.4.1:
3233 version "0.4.1" 3368 version "0.4.1"
3234 resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 3369 resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
3235 integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 3370 integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
3236 3371
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: 3372json-stable-stringify-without-jsonify@^1.0.1:
3243 version "1.0.1" 3373 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" 3374 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= 3375 integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
3246 3376
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: 3377json5@^1.0.1:
3265 version "1.0.1" 3378 version "1.0.1"
3266 resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" 3379 resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
@@ -3268,32 +3381,12 @@ json5@^1.0.1:
3268 dependencies: 3381 dependencies:
3269 minimist "^1.2.0" 3382 minimist "^1.2.0"
3270 3383
3271jsonfile@^3.0.0: 3384json5@^2.1.2:
3272 version "3.0.1" 3385 version "2.1.3"
3273 resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" 3386 resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
3274 integrity sha1-pezG9l9T9mLEQVx2daAzHQmS7GY= 3387 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: 3388 dependencies:
3293 assert-plus "1.0.0" 3389 minimist "^1.2.5"
3294 extsprintf "1.3.0"
3295 json-schema "0.2.3"
3296 verror "1.10.0"
3297 3390
3298kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: 3391kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
3299 version "3.2.2" 3392 version "3.2.2"
@@ -3314,46 +3407,45 @@ kind-of@^5.0.0:
3314 resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" 3407 resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
3315 integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== 3408 integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
3316 3409
3317kind-of@^6.0.0, kind-of@^6.0.2: 3410kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
3318 version "6.0.2" 3411 version "6.0.3"
3319 resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" 3412 resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
3320 integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== 3413 integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
3321 3414
3322known-css-properties@^0.3.0: 3415klona@^2.0.3:
3323 version "0.3.0" 3416 version "2.0.4"
3324 resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.3.0.tgz#a3d135bbfc60ee8c6eacf2f7e7e6f2d4755e49a4" 3417 resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
3325 integrity sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ== 3418 integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
3326 3419
3327lazy-cache@^1.0.3: 3420known-css-properties@^0.19.0:
3328 version "1.0.4" 3421 version "0.19.0"
3329 resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" 3422 resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.19.0.tgz#5d92b7fa16c72d971bda9b7fe295bdf61836ee5b"
3330 integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= 3423 integrity sha512-eYboRV94Vco725nKMlpkn3nV2+96p9c3gKXRsYqAJSswSENvBhN7n5L+uDhY58xQa0UukWsDMTGELzmD8Q+wTA==
3331 3424
3332lcid@^1.0.0: 3425leven@^3.1.0:
3333 version "1.0.0" 3426 version "3.1.0"
3334 resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" 3427 resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
3335 integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= 3428 integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
3336 dependencies:
3337 invert-kv "^1.0.0"
3338 3429
3339levn@^0.3.0, levn@~0.3.0: 3430levenary@^1.1.1:
3340 version "0.3.0" 3431 version "1.1.1"
3341 resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" 3432 resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77"
3342 integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= 3433 integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==
3343 dependencies: 3434 dependencies:
3344 prelude-ls "~1.1.2" 3435 leven "^3.1.0"
3345 type-check "~0.3.2"
3346 3436
3347load-json-file@^1.0.0: 3437levn@^0.4.1:
3348 version "1.1.0" 3438 version "0.4.1"
3349 resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" 3439 resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
3350 integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= 3440 integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
3351 dependencies: 3441 dependencies:
3352 graceful-fs "^4.1.2" 3442 prelude-ls "^1.2.1"
3353 parse-json "^2.2.0" 3443 type-check "~0.4.0"
3354 pify "^2.0.0" 3444
3355 pinkie-promise "^2.0.0" 3445lines-and-columns@^1.1.6:
3356 strip-bom "^2.0.0" 3446 version "1.1.6"
3447 resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
3448 integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
3357 3449
3358load-json-file@^2.0.0: 3450load-json-file@^2.0.0:
3359 version "2.0.0" 3451 version "2.0.0"
@@ -3365,20 +3457,29 @@ load-json-file@^2.0.0:
3365 pify "^2.0.0" 3457 pify "^2.0.0"
3366 strip-bom "^3.0.0" 3458 strip-bom "^3.0.0"
3367 3459
3368loader-runner@^2.3.0: 3460loader-runner@^2.4.0:
3369 version "2.4.0" 3461 version "2.4.0"
3370 resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" 3462 resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
3371 integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== 3463 integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
3372 3464
3373loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: 3465loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0:
3374 version "1.2.3" 3466 version "1.4.0"
3375 resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" 3467 resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
3376 integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== 3468 integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
3377 dependencies: 3469 dependencies:
3378 big.js "^5.2.2" 3470 big.js "^5.2.2"
3379 emojis-list "^2.0.0" 3471 emojis-list "^3.0.0"
3380 json5 "^1.0.1" 3472 json5 "^1.0.1"
3381 3473
3474loader-utils@^2.0.0:
3475 version "2.0.0"
3476 resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
3477 integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
3478 dependencies:
3479 big.js "^5.2.2"
3480 emojis-list "^3.0.0"
3481 json5 "^2.1.2"
3482
3382locate-path@^2.0.0: 3483locate-path@^2.0.0:
3383 version "2.0.0" 3484 version "2.0.0"
3384 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" 3485 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -3387,55 +3488,37 @@ locate-path@^2.0.0:
3387 p-locate "^2.0.0" 3488 p-locate "^2.0.0"
3388 path-exists "^3.0.0" 3489 path-exists "^3.0.0"
3389 3490
3390lodash.camelcase@^4.3.0: 3491locate-path@^3.0.0:
3391 version "4.3.0" 3492 version "3.0.0"
3392 resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" 3493 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
3393 integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= 3494 integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
3394 3495 dependencies:
3395lodash.capitalize@^4.1.0: 3496 p-locate "^3.0.0"
3396 version "4.2.1" 3497 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 3498
3420lodash.tail@^4.1.1: 3499locate-path@^5.0.0:
3421 version "4.1.1" 3500 version "5.0.0"
3422 resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" 3501 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
3423 integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= 3502 integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
3503 dependencies:
3504 p-locate "^4.1.0"
3424 3505
3425lodash.uniq@^4.5.0: 3506lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
3426 version "4.5.0" 3507 version "4.17.20"
3427 resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" 3508 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
3428 integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= 3509 integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
3429 3510
3430lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.3.0, lodash@~4.17.10: 3511log-symbols@^4.0.0:
3431 version "4.17.11" 3512 version "4.0.0"
3432 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" 3513 resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
3433 integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== 3514 integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
3515 dependencies:
3516 chalk "^4.0.0"
3434 3517
3435longest@^1.0.1: 3518longest-streak@^2.0.1:
3436 version "1.0.1" 3519 version "2.0.4"
3437 resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" 3520 resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
3438 integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= 3521 integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
3439 3522
3440loose-envify@^1.0.0: 3523loose-envify@^1.0.0:
3441 version "1.4.0" 3524 version "1.4.0"
@@ -3444,39 +3527,50 @@ loose-envify@^1.0.0:
3444 dependencies: 3527 dependencies:
3445 js-tokens "^3.0.0 || ^4.0.0" 3528 js-tokens "^3.0.0 || ^4.0.0"
3446 3529
3447loud-rejection@^1.0.0: 3530lru-cache@^5.1.1:
3448 version "1.6.0" 3531 version "5.1.1"
3449 resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" 3532 resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
3450 integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= 3533 integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
3534 dependencies:
3535 yallist "^3.0.2"
3536
3537lru-cache@^6.0.0:
3538 version "6.0.0"
3539 resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
3540 integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
3451 dependencies: 3541 dependencies:
3452 currently-unhandled "^0.4.1" 3542 yallist "^4.0.0"
3453 signal-exit "^3.0.0"
3454 3543
3455lru-cache@^4.0.1: 3544make-dir@^2.0.0:
3456 version "4.1.5" 3545 version "2.1.0"
3457 resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" 3546 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
3458 integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== 3547 integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
3459 dependencies: 3548 dependencies:
3460 pseudomap "^1.0.2" 3549 pify "^4.0.1"
3461 yallist "^2.1.2" 3550 semver "^5.6.0"
3462 3551
3463make-dir@^1.0.0: 3552make-dir@^3.0.2:
3464 version "1.3.0" 3553 version "3.1.0"
3465 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" 3554 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
3466 integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== 3555 integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
3467 dependencies: 3556 dependencies:
3468 pify "^3.0.0" 3557 semver "^6.0.0"
3469 3558
3470map-cache@^0.2.2: 3559map-cache@^0.2.2:
3471 version "0.2.2" 3560 version "0.2.2"
3472 resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" 3561 resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
3473 integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= 3562 integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
3474 3563
3475map-obj@^1.0.0, map-obj@^1.0.1: 3564map-obj@^1.0.0:
3476 version "1.0.1" 3565 version "1.0.1"
3477 resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" 3566 resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
3478 integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= 3567 integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
3479 3568
3569map-obj@^4.0.0:
3570 version "4.1.0"
3571 resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5"
3572 integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==
3573
3480map-visit@^1.0.0: 3574map-visit@^1.0.0:
3481 version "1.0.0" 3575 version "1.0.0"
3482 resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" 3576 resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -3484,10 +3578,22 @@ map-visit@^1.0.0:
3484 dependencies: 3578 dependencies:
3485 object-visit "^1.0.0" 3579 object-visit "^1.0.0"
3486 3580
3487math-expression-evaluator@^1.2.14: 3581markdown-escapes@^1.0.0:
3488 version "1.2.17" 3582 version "1.0.4"
3489 resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" 3583 resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
3490 integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw= 3584 integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
3585
3586markdown-table@^2.0.0:
3587 version "2.0.0"
3588 resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b"
3589 integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==
3590 dependencies:
3591 repeat-string "^1.0.0"
3592
3593mathml-tag-names@^2.1.3:
3594 version "2.1.3"
3595 resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
3596 integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
3491 3597
3492md5.js@^1.3.4: 3598md5.js@^1.3.4:
3493 version "1.3.5" 3599 version "1.3.5"
@@ -3498,14 +3604,14 @@ md5.js@^1.3.4:
3498 inherits "^2.0.1" 3604 inherits "^2.0.1"
3499 safe-buffer "^5.1.2" 3605 safe-buffer "^5.1.2"
3500 3606
3501mem@^1.1.0: 3607mdast-util-compact@^2.0.0:
3502 version "1.1.0" 3608 version "2.0.1"
3503 resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" 3609 resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz#cabc69a2f43103628326f35b1acf735d55c99490"
3504 integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y= 3610 integrity sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA==
3505 dependencies: 3611 dependencies:
3506 mimic-fn "^1.0.0" 3612 unist-util-visit "^2.0.0"
3507 3613
3508memory-fs@^0.4.0, memory-fs@~0.4.1: 3614memory-fs@^0.4.1:
3509 version "0.4.1" 3615 version "0.4.1"
3510 resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" 3616 resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
3511 integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= 3617 integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
@@ -3513,28 +3619,42 @@ memory-fs@^0.4.0, memory-fs@~0.4.1:
3513 errno "^0.1.3" 3619 errno "^0.1.3"
3514 readable-stream "^2.0.1" 3620 readable-stream "^2.0.1"
3515 3621
3516meow@^3.7.0: 3622memory-fs@^0.5.0:
3517 version "3.7.0" 3623 version "0.5.0"
3518 resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" 3624 resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c"
3519 integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= 3625 integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==
3520 dependencies: 3626 dependencies:
3521 camelcase-keys "^2.0.0" 3627 errno "^0.1.3"
3522 decamelize "^1.1.2" 3628 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 3629
3532merge@^1.2.0: 3630meow@^7.1.1:
3533 version "1.2.1" 3631 version "7.1.1"
3534 resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" 3632 resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306"
3535 integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== 3633 integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==
3634 dependencies:
3635 "@types/minimist" "^1.2.0"
3636 camelcase-keys "^6.2.2"
3637 decamelize-keys "^1.1.0"
3638 hard-rejection "^2.1.0"
3639 minimist-options "4.1.0"
3640 normalize-package-data "^2.5.0"
3641 read-pkg-up "^7.0.1"
3642 redent "^3.0.0"
3643 trim-newlines "^3.0.0"
3644 type-fest "^0.13.1"
3645 yargs-parser "^18.1.3"
3646
3647merge-stream@^2.0.0:
3648 version "2.0.0"
3649 resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
3650 integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
3536 3651
3537micromatch@^3.1.10, micromatch@^3.1.4: 3652merge2@^1.3.0:
3653 version "1.4.1"
3654 resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
3655 integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
3656
3657micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
3538 version "3.1.10" 3658 version "3.1.10"
3539 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" 3659 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
3540 integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== 3660 integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
@@ -3553,6 +3673,14 @@ micromatch@^3.1.10, micromatch@^3.1.4:
3553 snapdragon "^0.8.1" 3673 snapdragon "^0.8.1"
3554 to-regex "^3.0.2" 3674 to-regex "^3.0.2"
3555 3675
3676micromatch@^4.0.2:
3677 version "4.0.2"
3678 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
3679 integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
3680 dependencies:
3681 braces "^3.0.1"
3682 picomatch "^2.0.5"
3683
3556miller-rabin@^4.0.0: 3684miller-rabin@^4.0.0:
3557 version "4.0.1" 3685 version "4.0.1"
3558 resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" 3686 resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -3561,27 +3689,20 @@ miller-rabin@^4.0.0:
3561 bn.js "^4.0.0" 3689 bn.js "^4.0.0"
3562 brorand "^1.0.1" 3690 brorand "^1.0.1"
3563 3691
3564mime-db@1.40.0: 3692min-indent@^1.0.0:
3565 version "1.40.0" 3693 version "1.0.1"
3566 resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" 3694 resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
3567 integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== 3695 integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
3568 3696
3569mime-types@^2.1.12, mime-types@~2.1.19: 3697mini-css-extract-plugin@^0.11.2:
3570 version "2.1.24" 3698 version "0.11.2"
3571 resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" 3699 resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.2.tgz#e3af4d5e04fbcaaf11838ab230510073060b37bf"
3572 integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== 3700 integrity sha512-h2LknfX4U1kScXxH8xE9LCOqT5B+068EAj36qicMb8l4dqdJoyHcmWmpd+ueyZfgu/POvIn+teoUnTtei2ikug==
3573 dependencies: 3701 dependencies:
3574 mime-db "1.40.0" 3702 loader-utils "^1.1.0"
3575 3703 normalize-url "1.9.1"
3576mime@^1.4.1: 3704 schema-utils "^1.0.0"
3577 version "1.6.0" 3705 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 3706
3586minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: 3707minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
3587 version "1.0.1" 3708 version "1.0.1"
@@ -3593,90 +3714,125 @@ 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" 3714 resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
3594 integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= 3715 integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
3595 3716
3596minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: 3717minimatch@^3.0.4:
3597 version "3.0.4" 3718 version "3.0.4"
3598 resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 3719 resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
3599 integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 3720 integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
3600 dependencies: 3721 dependencies:
3601 brace-expansion "^1.1.7" 3722 brace-expansion "^1.1.7"
3602 3723
3603minimist@0.0.8: 3724minimist-options@4.1.0:
3604 version "0.0.8" 3725 version "4.1.0"
3605 resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 3726 resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
3606 integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 3727 integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
3728 dependencies:
3729 arrify "^1.0.1"
3730 is-plain-obj "^1.1.0"
3731 kind-of "^6.0.3"
3607 3732
3608minimist@1.1.x: 3733minimist@^1.2.0, minimist@^1.2.5:
3609 version "1.1.3" 3734 version "1.2.5"
3610 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" 3735 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
3611 integrity sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag= 3736 integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
3612 3737
3613minimist@^1.1.3, minimist@^1.2.0: 3738minipass-collect@^1.0.2:
3614 version "1.2.0" 3739 version "1.0.2"
3615 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 3740 resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
3616 integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= 3741 integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
3742 dependencies:
3743 minipass "^3.0.0"
3617 3744
3618minipass@^2.2.1, minipass@^2.3.4: 3745minipass-flush@^1.0.5:
3619 version "2.3.5" 3746 version "1.0.5"
3620 resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" 3747 resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
3621 integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== 3748 integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
3622 dependencies: 3749 dependencies:
3623 safe-buffer "^5.1.2" 3750 minipass "^3.0.0"
3624 yallist "^3.0.0"
3625 3751
3626minizlib@^1.1.1: 3752minipass-pipeline@^1.2.2:
3627 version "1.2.1" 3753 version "1.2.4"
3628 resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" 3754 resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
3629 integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== 3755 integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
3756 dependencies:
3757 minipass "^3.0.0"
3758
3759minipass@^3.0.0, minipass@^3.1.1:
3760 version "3.1.3"
3761 resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
3762 integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
3763 dependencies:
3764 yallist "^4.0.0"
3765
3766minizlib@^2.1.1:
3767 version "2.1.2"
3768 resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
3769 integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
3630 dependencies: 3770 dependencies:
3631 minipass "^2.2.1" 3771 minipass "^3.0.0"
3772 yallist "^4.0.0"
3773
3774mississippi@^3.0.0:
3775 version "3.0.0"
3776 resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
3777 integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
3778 dependencies:
3779 concat-stream "^1.5.0"
3780 duplexify "^3.4.2"
3781 end-of-stream "^1.1.0"
3782 flush-write-stream "^1.0.0"
3783 from2 "^2.1.0"
3784 parallel-transform "^1.1.0"
3785 pump "^3.0.0"
3786 pumpify "^1.3.3"
3787 stream-each "^1.1.0"
3788 through2 "^2.0.0"
3632 3789
3633mixin-deep@^1.2.0: 3790mixin-deep@^1.2.0:
3634 version "1.3.1" 3791 version "1.3.2"
3635 resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" 3792 resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
3636 integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== 3793 integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
3637 dependencies: 3794 dependencies:
3638 for-in "^1.0.2" 3795 for-in "^1.0.2"
3639 is-extendable "^1.0.1" 3796 is-extendable "^1.0.1"
3640 3797
3641mixin-object@^2.0.1: 3798mkdirp@^0.5.1, mkdirp@^0.5.3:
3642 version "2.0.1" 3799 version "0.5.5"
3643 resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" 3800 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
3644 integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= 3801 integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
3645 dependencies: 3802 dependencies:
3646 for-in "^0.1.3" 3803 minimist "^1.2.5"
3647 is-extendable "^0.1.1"
3648 3804
3649"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: 3805mkdirp@^1.0.3, mkdirp@^1.0.4:
3650 version "0.5.1" 3806 version "1.0.4"
3651 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 3807 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
3652 integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= 3808 integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
3809
3810move-concurrently@^1.0.1:
3811 version "1.0.1"
3812 resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
3813 integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
3653 dependencies: 3814 dependencies:
3654 minimist "0.0.8" 3815 aproba "^1.1.1"
3816 copy-concurrently "^1.0.0"
3817 fs-write-stream-atomic "^1.0.8"
3818 mkdirp "^0.5.1"
3819 rimraf "^2.5.4"
3820 run-queue "^1.0.3"
3655 3821
3656ms@2.0.0: 3822ms@2.0.0:
3657 version "2.0.0" 3823 version "2.0.0"
3658 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 3824 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
3659 integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 3825 integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
3660 3826
3661ms@^2.1.1: 3827ms@2.1.2:
3662 version "2.1.1" 3828 version "2.1.2"
3663 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 3829 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
3664 integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 3830 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 3831
3676nan@^2.12.1, nan@^2.13.2: 3832nan@^2.12.1:
3677 version "2.14.0" 3833 version "2.14.1"
3678 resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" 3834 resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
3679 integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== 3835 integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
3680 3836
3681nanomatch@^1.2.9: 3837nanomatch@^1.2.9:
3682 version "1.2.13" 3838 version "1.2.13"
@@ -3700,47 +3856,20 @@ natural-compare@^1.4.0:
3700 resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" 3856 resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
3701 integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= 3857 integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
3702 3858
3703needle@^2.2.1: 3859neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2:
3704 version "2.4.0" 3860 version "2.6.2"
3705 resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" 3861 resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
3706 integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== 3862 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 3863
3717next-tick@^1.0.0: 3864nice-try@^1.0.4:
3718 version "1.0.0" 3865 version "1.0.5"
3719 resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" 3866 resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
3720 integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= 3867 integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
3721 3868
3722node-gyp@^3.8.0: 3869node-libs-browser@^2.2.1:
3723 version "3.8.0" 3870 version "2.2.1"
3724 resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" 3871 resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
3725 integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== 3872 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: 3873 dependencies:
3745 assert "^1.1.1" 3874 assert "^1.1.1"
3746 browserify-zlib "^0.2.0" 3875 browserify-zlib "^0.2.0"
@@ -3752,7 +3881,7 @@ node-libs-browser@^2.0.0:
3752 events "^3.0.0" 3881 events "^3.0.0"
3753 https-browserify "^1.0.0" 3882 https-browserify "^1.0.0"
3754 os-browserify "^0.3.0" 3883 os-browserify "^0.3.0"
3755 path-browserify "0.0.0" 3884 path-browserify "0.0.1"
3756 process "^0.11.10" 3885 process "^0.11.10"
3757 punycode "^1.2.4" 3886 punycode "^1.2.4"
3758 querystring-es3 "^0.2.0" 3887 querystring-es3 "^0.2.0"
@@ -3764,63 +3893,14 @@ node-libs-browser@^2.0.0:
3764 tty-browserify "0.0.0" 3893 tty-browserify "0.0.0"
3765 url "^0.11.0" 3894 url "^0.11.0"
3766 util "^0.11.0" 3895 util "^0.11.0"
3767 vm-browserify "0.0.4" 3896 vm-browserify "^1.0.1"
3768 3897
3769node-pre-gyp@^0.12.0: 3898node-releases@^1.1.61:
3770 version "0.12.0" 3899 version "1.1.61"
3771 resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" 3900 resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e"
3772 integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== 3901 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 3902
3815nopt@^4.0.1: 3903normalize-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" 3904 version "2.5.0"
3825 resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" 3905 resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
3826 integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== 3906 integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
@@ -3837,7 +3917,7 @@ normalize-path@^2.1.1:
3837 dependencies: 3917 dependencies:
3838 remove-trailing-separator "^1.0.1" 3918 remove-trailing-separator "^1.0.1"
3839 3919
3840normalize-path@^3.0.0: 3920normalize-path@^3.0.0, normalize-path@~3.0.0:
3841 version "3.0.0" 3921 version "3.0.0"
3842 resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 3922 resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
3843 integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 3923 integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@@ -3847,7 +3927,12 @@ normalize-range@^0.1.2:
3847 resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" 3927 resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
3848 integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= 3928 integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
3849 3929
3850normalize-url@^1.4.0: 3930normalize-selector@^0.2.0:
3931 version "0.2.0"
3932 resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03"
3933 integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=
3934
3935normalize-url@1.9.1:
3851 version "1.9.1" 3936 version "1.9.1"
3852 resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" 3937 resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
3853 integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= 3938 integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=
@@ -3857,51 +3942,11 @@ normalize-url@^1.4.0:
3857 query-string "^4.1.0" 3942 query-string "^4.1.0"
3858 sort-keys "^1.0.0" 3943 sort-keys "^1.0.0"
3859 3944
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: 3945num2fraction@^1.2.2:
3891 version "1.2.2" 3946 version "1.2.2"
3892 resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" 3947 resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
3893 integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= 3948 integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
3894 3949
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: 3950object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
3906 version "4.1.1" 3951 version "4.1.1"
3907 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 3952 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -3916,7 +3961,12 @@ object-copy@^0.1.0:
3916 define-property "^0.2.5" 3961 define-property "^0.2.5"
3917 kind-of "^3.0.3" 3962 kind-of "^3.0.3"
3918 3963
3919object-keys@^1.0.12: 3964object-inspect@^1.7.0, object-inspect@^1.8.0:
3965 version "1.8.0"
3966 resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
3967 integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
3968
3969object-keys@^1.0.12, object-keys@^1.1.1:
3920 version "1.1.1" 3970 version "1.1.1"
3921 resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" 3971 resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
3922 integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== 3972 integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@@ -3928,6 +3978,25 @@ object-visit@^1.0.0:
3928 dependencies: 3978 dependencies:
3929 isobject "^3.0.0" 3979 isobject "^3.0.0"
3930 3980
3981object.assign@^4.1.0:
3982 version "4.1.1"
3983 resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd"
3984 integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==
3985 dependencies:
3986 define-properties "^1.1.3"
3987 es-abstract "^1.18.0-next.0"
3988 has-symbols "^1.0.1"
3989 object-keys "^1.1.1"
3990
3991object.entries@^1.1.2:
3992 version "1.1.2"
3993 resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add"
3994 integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==
3995 dependencies:
3996 define-properties "^1.1.3"
3997 es-abstract "^1.17.5"
3998 has "^1.0.3"
3999
3931object.pick@^1.3.0: 4000object.pick@^1.3.0:
3932 version "1.3.0" 4001 version "1.3.0"
3933 resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" 4002 resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
@@ -3935,81 +4004,40 @@ object.pick@^1.3.0:
3935 dependencies: 4004 dependencies:
3936 isobject "^3.0.1" 4005 isobject "^3.0.1"
3937 4006
3938once@^1.3.0: 4007object.values@^1.1.1:
4008 version "1.1.1"
4009 resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
4010 integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
4011 dependencies:
4012 define-properties "^1.1.3"
4013 es-abstract "^1.17.0-next.1"
4014 function-bind "^1.1.1"
4015 has "^1.0.3"
4016
4017once@^1.3.0, once@^1.3.1, once@^1.4.0:
3939 version "1.4.0" 4018 version "1.4.0"
3940 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 4019 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
3941 integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 4020 integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
3942 dependencies: 4021 dependencies:
3943 wrappy "1" 4022 wrappy "1"
3944 4023
3945onetime@^1.0.0: 4024optionator@^0.9.1:
3946 version "1.1.0" 4025 version "0.9.1"
3947 resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" 4026 resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
3948 integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= 4027 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: 4028 dependencies:
3955 mimic-fn "^1.0.0" 4029 deep-is "^0.1.3"
3956 4030 fast-levenshtein "^2.0.6"
3957optionator@^0.8.1, optionator@^0.8.2: 4031 levn "^0.4.1"
3958 version "0.8.2" 4032 prelude-ls "^1.2.1"
3959 resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" 4033 type-check "^0.4.0"
3960 integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= 4034 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 4035
3969os-browserify@^0.3.0: 4036os-browserify@^0.3.0:
3970 version "0.3.0" 4037 version "0.3.0"
3971 resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" 4038 resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
3972 integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= 4039 integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
3973 4040
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: 4041p-limit@^1.1.0:
4014 version "1.3.0" 4042 version "1.3.0"
4015 resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" 4043 resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@@ -4017,6 +4045,20 @@ p-limit@^1.1.0:
4017 dependencies: 4045 dependencies:
4018 p-try "^1.0.0" 4046 p-try "^1.0.0"
4019 4047
4048p-limit@^2.0.0, p-limit@^2.2.0:
4049 version "2.3.0"
4050 resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
4051 integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
4052 dependencies:
4053 p-try "^2.0.0"
4054
4055p-limit@^3.0.2:
4056 version "3.0.2"
4057 resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe"
4058 integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==
4059 dependencies:
4060 p-try "^2.0.0"
4061
4020p-locate@^2.0.0: 4062p-locate@^2.0.0:
4021 version "2.0.0" 4063 version "2.0.0"
4022 resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" 4064 resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
@@ -4024,28 +4066,81 @@ p-locate@^2.0.0:
4024 dependencies: 4066 dependencies:
4025 p-limit "^1.1.0" 4067 p-limit "^1.1.0"
4026 4068
4069p-locate@^3.0.0:
4070 version "3.0.0"
4071 resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
4072 integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
4073 dependencies:
4074 p-limit "^2.0.0"
4075
4076p-locate@^4.1.0:
4077 version "4.1.0"
4078 resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
4079 integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
4080 dependencies:
4081 p-limit "^2.2.0"
4082
4083p-map@^4.0.0:
4084 version "4.0.0"
4085 resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
4086 integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
4087 dependencies:
4088 aggregate-error "^3.0.0"
4089
4027p-try@^1.0.0: 4090p-try@^1.0.0:
4028 version "1.0.0" 4091 version "1.0.0"
4029 resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" 4092 resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
4030 integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= 4093 integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
4031 4094
4095p-try@^2.0.0:
4096 version "2.2.0"
4097 resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
4098 integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
4099
4032pako@~1.0.5: 4100pako@~1.0.5:
4033 version "1.0.10" 4101 version "1.0.11"
4034 resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" 4102 resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
4035 integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== 4103 integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
4036 4104
4037parse-asn1@^5.0.0: 4105parallel-transform@^1.1.0:
4038 version "5.1.4" 4106 version "1.2.0"
4039 resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" 4107 resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
4040 integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== 4108 integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==
4109 dependencies:
4110 cyclist "^1.0.1"
4111 inherits "^2.0.3"
4112 readable-stream "^2.1.5"
4113
4114parent-module@^1.0.0:
4115 version "1.0.1"
4116 resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
4117 integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
4118 dependencies:
4119 callsites "^3.0.0"
4120
4121parse-asn1@^5.0.0, parse-asn1@^5.1.5:
4122 version "5.1.6"
4123 resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
4124 integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==
4041 dependencies: 4125 dependencies:
4042 asn1.js "^4.0.0" 4126 asn1.js "^5.2.0"
4043 browserify-aes "^1.0.0" 4127 browserify-aes "^1.0.0"
4044 create-hash "^1.1.0"
4045 evp_bytestokey "^1.0.0" 4128 evp_bytestokey "^1.0.0"
4046 pbkdf2 "^3.0.3" 4129 pbkdf2 "^3.0.3"
4047 safe-buffer "^5.1.1" 4130 safe-buffer "^5.1.1"
4048 4131
4132parse-entities@^2.0.0:
4133 version "2.0.0"
4134 resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
4135 integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
4136 dependencies:
4137 character-entities "^1.0.0"
4138 character-entities-legacy "^1.0.0"
4139 character-reference-invalid "^1.0.0"
4140 is-alphanumerical "^1.0.0"
4141 is-decimal "^1.0.0"
4142 is-hexadecimal "^1.0.0"
4143
4049parse-json@^2.2.0: 4144parse-json@^2.2.0:
4050 version "2.2.0" 4145 version "2.2.0"
4051 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" 4146 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
@@ -4053,62 +4148,66 @@ parse-json@^2.2.0:
4053 dependencies: 4148 dependencies:
4054 error-ex "^1.2.0" 4149 error-ex "^1.2.0"
4055 4150
4151parse-json@^5.0.0:
4152 version "5.1.0"
4153 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646"
4154 integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==
4155 dependencies:
4156 "@babel/code-frame" "^7.0.0"
4157 error-ex "^1.3.1"
4158 json-parse-even-better-errors "^2.3.0"
4159 lines-and-columns "^1.1.6"
4160
4161parse-passwd@^1.0.0:
4162 version "1.0.0"
4163 resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
4164 integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
4165
4056pascalcase@^0.1.1: 4166pascalcase@^0.1.1:
4057 version "0.1.1" 4167 version "0.1.1"
4058 resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" 4168 resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
4059 integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= 4169 integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
4060 4170
4061path-browserify@0.0.0: 4171path-browserify@0.0.1:
4062 version "0.0.0" 4172 version "0.0.1"
4063 resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" 4173 resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
4064 integrity sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo= 4174 integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==
4065 4175
4066path-dirname@^1.0.0: 4176path-dirname@^1.0.0:
4067 version "1.0.2" 4177 version "1.0.2"
4068 resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" 4178 resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
4069 integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= 4179 integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
4070 4180
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: 4181path-exists@^3.0.0:
4079 version "3.0.0" 4182 version "3.0.0"
4080 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 4183 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
4081 integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= 4184 integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
4082 4185
4083path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: 4186path-exists@^4.0.0:
4187 version "4.0.0"
4188 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
4189 integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
4190
4191path-is-absolute@^1.0.0:
4084 version "1.0.1" 4192 version "1.0.1"
4085 resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 4193 resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
4086 integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 4194 integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
4087 4195
4088path-is-inside@^1.0.1, path-is-inside@^1.0.2: 4196path-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" 4197 version "2.0.1"
4095 resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 4198 resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
4096 integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= 4199 integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
4097 4200
4201path-key@^3.1.0:
4202 version "3.1.1"
4203 resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
4204 integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
4205
4098path-parse@^1.0.6: 4206path-parse@^1.0.6:
4099 version "1.0.6" 4207 version "1.0.6"
4100 resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 4208 resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
4101 integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 4209 integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
4102 4210
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: 4211path-type@^2.0.0:
4113 version "2.0.0" 4212 version "2.0.0"
4114 resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" 4213 resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@@ -4116,10 +4215,15 @@ path-type@^2.0.0:
4116 dependencies: 4215 dependencies:
4117 pify "^2.0.0" 4216 pify "^2.0.0"
4118 4217
4218path-type@^4.0.0:
4219 version "4.0.0"
4220 resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
4221 integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
4222
4119pbkdf2@^3.0.3: 4223pbkdf2@^3.0.3:
4120 version "3.0.17" 4224 version "3.1.1"
4121 resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" 4225 resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94"
4122 integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== 4226 integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==
4123 dependencies: 4227 dependencies:
4124 create-hash "^1.1.2" 4228 create-hash "^1.1.2"
4125 create-hmac "^1.1.4" 4229 create-hmac "^1.1.4"
@@ -4127,32 +4231,20 @@ pbkdf2@^3.0.3:
4127 safe-buffer "^5.0.1" 4231 safe-buffer "^5.0.1"
4128 sha.js "^2.4.8" 4232 sha.js "^2.4.8"
4129 4233
4130performance-now@^2.1.0: 4234picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
4131 version "2.1.0" 4235 version "2.2.2"
4132 resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 4236 resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
4133 integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= 4237 integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
4134 4238
4135pify@^2.0.0: 4239pify@^2.0.0:
4136 version "2.3.0" 4240 version "2.3.0"
4137 resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 4241 resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
4138 integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= 4242 integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
4139 4243
4140pify@^3.0.0: 4244pify@^4.0.1:
4141 version "3.0.0" 4245 version "4.0.1"
4142 resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" 4246 resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
4143 integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= 4247 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 4248
4157pkg-dir@^2.0.0: 4249pkg-dir@^2.0.0:
4158 version "2.0.0" 4250 version "2.0.0"
@@ -4161,350 +4253,168 @@ pkg-dir@^2.0.0:
4161 dependencies: 4253 dependencies:
4162 find-up "^2.1.0" 4254 find-up "^2.1.0"
4163 4255
4164pluralize@^1.2.1: 4256pkg-dir@^3.0.0:
4165 version "1.2.1" 4257 version "3.0.0"
4166 resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" 4258 resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
4167 integrity sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU= 4259 integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
4260 dependencies:
4261 find-up "^3.0.0"
4168 4262
4169pluralize@^7.0.0: 4263pkg-dir@^4.1.0:
4170 version "7.0.0" 4264 version "4.2.0"
4171 resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" 4265 resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
4172 integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== 4266 integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
4267 dependencies:
4268 find-up "^4.0.0"
4173 4269
4174posix-character-classes@^0.1.0: 4270posix-character-classes@^0.1.0:
4175 version "0.1.1" 4271 version "0.1.1"
4176 resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" 4272 resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
4177 integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= 4273 integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
4178 4274
4179postcss-calc@^5.2.0: 4275postcss-html@^0.36.0:
4180 version "5.3.1" 4276 version "0.36.0"
4181 resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" 4277 resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.36.0.tgz#b40913f94eaacc2453fd30a1327ad6ee1f88b204"
4182 integrity sha1-d7rnypKK2FcW4v2kLyYb98HWW14= 4278 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: 4279 dependencies:
4202 postcss "^5.0.11" 4280 htmlparser2 "^3.10.0"
4203 postcss-value-parser "^3.1.2"
4204 4281
4205postcss-discard-comments@^2.0.4: 4282postcss-less@^3.1.4:
4206 version "2.0.4" 4283 version "3.1.4"
4207 resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" 4284 resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.1.4.tgz#369f58642b5928ef898ffbc1a6e93c958304c5ad"
4208 integrity sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0= 4285 integrity sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==
4209 dependencies: 4286 dependencies:
4210 postcss "^5.0.14" 4287 postcss "^7.0.14"
4211 4288
4212postcss-discard-duplicates@^2.0.1: 4289postcss-media-query-parser@^0.2.3:
4213 version "2.1.0" 4290 version "0.2.3"
4214 resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" 4291 resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
4215 integrity sha1-uavye4isGIFYpesSq8riAmO5GTI= 4292 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 4293
4275postcss-message-helpers@^2.0.0: 4294postcss-modules-extract-imports@^2.0.0:
4276 version "2.0.0" 4295 version "2.0.0"
4277 resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" 4296 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= 4297 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: 4298 dependencies:
4322 postcss "^6.0.1" 4299 postcss "^7.0.5"
4323 4300
4324postcss-modules-local-by-default@^1.2.0: 4301postcss-modules-local-by-default@^3.0.3:
4325 version "1.2.0" 4302 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" 4303 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= 4304 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: 4305 dependencies:
4345 icss-replace-symbols "^1.1.0" 4306 icss-utils "^4.1.1"
4346 postcss "^6.0.1" 4307 postcss "^7.0.32"
4308 postcss-selector-parser "^6.0.2"
4309 postcss-value-parser "^4.1.0"
4347 4310
4348postcss-normalize-charset@^1.1.0: 4311postcss-modules-scope@^2.2.0:
4349 version "1.1.1" 4312 version "2.2.0"
4350 resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" 4313 resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
4351 integrity sha1-757nEhLX/nWceO0WL2HtYrXLk/E= 4314 integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
4352 dependencies: 4315 dependencies:
4353 postcss "^5.0.5" 4316 postcss "^7.0.6"
4317 postcss-selector-parser "^6.0.0"
4354 4318
4355postcss-normalize-url@^3.0.7: 4319postcss-modules-values@^3.0.0:
4356 version "3.0.8" 4320 version "3.0.0"
4357 resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" 4321 resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
4358 integrity sha1-EI90s/L82viRov+j6kWSJ5/HgiI= 4322 integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
4359 dependencies: 4323 dependencies:
4360 is-absolute-url "^2.0.0" 4324 icss-utils "^4.0.0"
4361 normalize-url "^1.4.0" 4325 postcss "^7.0.6"
4362 postcss "^5.0.14"
4363 postcss-value-parser "^3.2.3"
4364 4326
4365postcss-ordered-values@^2.1.0: 4327postcss-resolve-nested-selector@^0.1.1:
4366 version "2.2.3" 4328 version "0.1.1"
4367 resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" 4329 resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e"
4368 integrity sha1-7sbCpntsQSqNsgQud/6NpD+VwR0= 4330 integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=
4369 dependencies:
4370 postcss "^5.0.4"
4371 postcss-value-parser "^3.0.1"
4372 4331
4373postcss-reduce-idents@^2.2.2: 4332postcss-safe-parser@^4.0.2:
4374 version "2.4.0" 4333 version "4.0.2"
4375 resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" 4334 resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96"
4376 integrity sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM= 4335 integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==
4377 dependencies: 4336 dependencies:
4378 postcss "^5.0.4" 4337 postcss "^7.0.26"
4379 postcss-value-parser "^3.0.2"
4380 4338
4381postcss-reduce-initial@^1.0.0: 4339postcss-sass@^0.4.4:
4382 version "1.0.1" 4340 version "0.4.4"
4383 resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" 4341 resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.4.4.tgz#91f0f3447b45ce373227a98b61f8d8f0785285a3"
4384 integrity sha1-aPgGlfBF0IJjqHmtJA343WT2ROo= 4342 integrity sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==
4385 dependencies: 4343 dependencies:
4386 postcss "^5.0.4" 4344 gonzales-pe "^4.3.0"
4345 postcss "^7.0.21"
4387 4346
4388postcss-reduce-transforms@^1.0.3: 4347postcss-scss@^2.1.1:
4389 version "1.0.4" 4348 version "2.1.1"
4390 resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" 4349 resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383"
4391 integrity sha1-/3b02CEkN7McKYpC0uFEQCV3GuE= 4350 integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==
4392 dependencies: 4351 dependencies:
4393 has "^1.0.1" 4352 postcss "^7.0.6"
4394 postcss "^5.0.8"
4395 postcss-value-parser "^3.0.1"
4396 4353
4397postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: 4354postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
4398 version "2.2.3" 4355 version "6.0.3"
4399 resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" 4356 resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.3.tgz#766d77728728817cc140fa1ac6da5e77f9fada98"
4400 integrity sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A= 4357 integrity sha512-0ClFaY4X1ra21LRqbW6y3rUbWcxnSVkDFG57R7Nxus9J9myPFlv+jYDMohzpkBx0RrjjiqjtycpchQ+PLGmZ9w==
4401 dependencies: 4358 dependencies:
4402 flatten "^1.0.2" 4359 cssesc "^3.0.0"
4403 indexes-of "^1.0.1" 4360 indexes-of "^1.0.1"
4404 uniq "^1.0.1" 4361 uniq "^1.0.1"
4362 util-deprecate "^1.0.2"
4405 4363
4406postcss-svgo@^2.1.1: 4364postcss-syntax@^0.36.2:
4407 version "2.1.6" 4365 version "0.36.2"
4408 resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" 4366 resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c"
4409 integrity sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0= 4367 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 4368
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: 4369postcss-value-parser@^4.1.0:
4426 version "3.3.1" 4370 version "4.1.0"
4427 resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" 4371 resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
4428 integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== 4372 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 4373
4449postcss@^6.0.1: 4374postcss@^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" 4375 version "7.0.34"
4451 resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" 4376 resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.34.tgz#f2baf57c36010df7de4009940f21532c16d65c20"
4452 integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== 4377 integrity sha512-H/7V2VeNScX9KE83GDrDZNiGT1m2H+UTnlinIzhjlLX9hfMUn1mHNnGeX81a1c8JSBdBvqk7c2ZOG6ZPn5itGw==
4453 dependencies: 4378 dependencies:
4454 chalk "^2.4.1" 4379 chalk "^2.4.2"
4455 source-map "^0.6.1" 4380 source-map "^0.6.1"
4456 supports-color "^5.4.0" 4381 supports-color "^6.1.0"
4457 4382
4458prelude-ls@~1.1.2: 4383prelude-ls@^1.2.1:
4459 version "1.1.2" 4384 version "1.2.1"
4460 resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" 4385 resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
4461 integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= 4386 integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
4462 4387
4463prepend-http@^1.0.0: 4388prepend-http@^1.0.0:
4464 version "1.0.4" 4389 version "1.0.4"
4465 resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" 4390 resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
4466 integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= 4391 integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
4467 4392
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: 4393process-nextick-args@~2.0.0:
4474 version "2.0.0" 4394 version "2.0.1"
4475 resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" 4395 resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
4476 integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== 4396 integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
4477 4397
4478process@^0.11.10: 4398process@^0.11.10:
4479 version "0.11.10" 4399 version "0.11.10"
4480 resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 4400 resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
4481 integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= 4401 integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
4482 4402
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: 4403progress@^2.0.0:
4489 version "2.0.3" 4404 version "2.0.3"
4490 resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" 4405 resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
4491 integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== 4406 integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
4492 4407
4408promise-inflight@^1.0.1:
4409 version "1.0.1"
4410 resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
4411 integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
4412
4493prr@~1.0.1: 4413prr@~1.0.1:
4494 version "1.0.1" 4414 version "1.0.1"
4495 resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" 4415 resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
4496 integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= 4416 integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
4497 4417
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: 4418public-encrypt@^4.0.0:
4509 version "4.0.3" 4419 version "4.0.3"
4510 resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" 4420 resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@@ -4517,12 +4427,37 @@ public-encrypt@^4.0.0:
4517 randombytes "^2.0.1" 4427 randombytes "^2.0.1"
4518 safe-buffer "^5.1.2" 4428 safe-buffer "^5.1.2"
4519 4429
4430pump@^2.0.0:
4431 version "2.0.1"
4432 resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
4433 integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
4434 dependencies:
4435 end-of-stream "^1.1.0"
4436 once "^1.3.1"
4437
4438pump@^3.0.0:
4439 version "3.0.0"
4440 resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
4441 integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
4442 dependencies:
4443 end-of-stream "^1.1.0"
4444 once "^1.3.1"
4445
4446pumpify@^1.3.3:
4447 version "1.5.1"
4448 resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
4449 integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
4450 dependencies:
4451 duplexify "^3.6.0"
4452 inherits "^2.0.3"
4453 pump "^2.0.0"
4454
4520punycode@1.3.2: 4455punycode@1.3.2:
4521 version "1.3.2" 4456 version "1.3.2"
4522 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" 4457 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
4523 integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= 4458 integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
4524 4459
4525punycode@^1.2.4, punycode@^1.4.1: 4460punycode@^1.2.4:
4526 version "1.4.1" 4461 version "1.4.1"
4527 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 4462 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
4528 integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= 4463 integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
@@ -4538,19 +4473,9 @@ pure-extras@^1.0.0:
4538 integrity sha1-N+PMNZDLqFCYFFTNpdso4npjhxo= 4473 integrity sha1-N+PMNZDLqFCYFFTNpdso4npjhxo=
4539 4474
4540purecss@^1.0.0: 4475purecss@^1.0.0:
4541 version "1.0.0" 4476 version "1.0.1"
4542 resolved "https://registry.yarnpkg.com/purecss/-/purecss-1.0.0.tgz#3dbcd9e2a7592448a69acb705cce16311bf4b785" 4477 resolved "https://registry.yarnpkg.com/purecss/-/purecss-1.0.1.tgz#c83d84326a10beb5c3b36d20c0254e946e5568a7"
4543 integrity sha512-gfC78WCOWNnfkzulx9aoWwcl+0JflhwKeJ+k9s/ZyIawfYNA4bqBmt0DtfgtQK9iuYMtGfbdE8R2AQMjSWR2VQ== 4478 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 4479
4555query-string@^4.1.0: 4480query-string@^4.1.0:
4556 version "4.3.4" 4481 version "4.3.4"
@@ -4570,7 +4495,12 @@ querystring@0.2.0:
4570 resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" 4495 resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
4571 integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= 4496 integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
4572 4497
4573randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: 4498quick-lru@^4.0.1:
4499 version "4.0.1"
4500 resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
4501 integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
4502
4503randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
4574 version "2.1.0" 4504 version "2.1.0"
4575 resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" 4505 resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
4576 integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 4506 integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
@@ -4585,24 +4515,6 @@ randomfill@^1.0.3:
4585 randombytes "^2.0.5" 4515 randombytes "^2.0.5"
4586 safe-buffer "^5.1.0" 4516 safe-buffer "^5.1.0"
4587 4517
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: 4518read-pkg-up@^2.0.0:
4607 version "2.0.0" 4519 version "2.0.0"
4608 resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" 4520 resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
@@ -4611,14 +4523,14 @@ read-pkg-up@^2.0.0:
4611 find-up "^2.0.0" 4523 find-up "^2.0.0"
4612 read-pkg "^2.0.0" 4524 read-pkg "^2.0.0"
4613 4525
4614read-pkg@^1.0.0: 4526read-pkg-up@^7.0.1:
4615 version "1.1.0" 4527 version "7.0.1"
4616 resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" 4528 resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
4617 integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= 4529 integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
4618 dependencies: 4530 dependencies:
4619 load-json-file "^1.0.0" 4531 find-up "^4.1.0"
4620 normalize-package-data "^2.3.2" 4532 read-pkg "^5.2.0"
4621 path-type "^1.0.0" 4533 type-fest "^0.8.1"
4622 4534
4623read-pkg@^2.0.0: 4535read-pkg@^2.0.0:
4624 version "2.0.0" 4536 version "2.0.0"
@@ -4629,10 +4541,20 @@ read-pkg@^2.0.0:
4629 normalize-package-data "^2.3.2" 4541 normalize-package-data "^2.3.2"
4630 path-type "^2.0.0" 4542 path-type "^2.0.0"
4631 4543
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: 4544read-pkg@^5.2.0:
4633 version "2.3.6" 4545 version "5.2.0"
4634 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 4546 resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
4635 integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== 4547 integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
4548 dependencies:
4549 "@types/normalize-package-data" "^2.4.0"
4550 normalize-package-data "^2.5.0"
4551 parse-json "^5.0.0"
4552 type-fest "^0.6.0"
4553
4554"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:
4555 version "2.3.7"
4556 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
4557 integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
4636 dependencies: 4558 dependencies:
4637 core-util-is "~1.0.0" 4559 core-util-is "~1.0.0"
4638 inherits "~2.0.3" 4560 inherits "~2.0.3"
@@ -4642,6 +4564,15 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable
4642 string_decoder "~1.1.1" 4564 string_decoder "~1.1.1"
4643 util-deprecate "~1.0.1" 4565 util-deprecate "~1.0.1"
4644 4566
4567readable-stream@^3.1.1, readable-stream@^3.6.0:
4568 version "3.6.0"
4569 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
4570 integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
4571 dependencies:
4572 inherits "^2.0.3"
4573 string_decoder "^1.1.1"
4574 util-deprecate "^1.0.1"
4575
4645readdirp@^2.2.1: 4576readdirp@^2.2.1:
4646 version "2.2.1" 4577 version "2.2.1"
4647 resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" 4578 resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@@ -4651,57 +4582,44 @@ readdirp@^2.2.1:
4651 micromatch "^3.1.10" 4582 micromatch "^3.1.10"
4652 readable-stream "^2.0.2" 4583 readable-stream "^2.0.2"
4653 4584
4654readline2@^1.0.1: 4585readdirp@~3.4.0:
4655 version "1.0.1" 4586 version "3.4.0"
4656 resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" 4587 resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
4657 integrity sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU= 4588 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: 4589 dependencies:
4668 indent-string "^2.1.0" 4590 picomatch "^2.2.1"
4669 strip-indent "^1.0.1"
4670 4591
4671reduce-css-calc@^1.2.6: 4592redent@^3.0.0:
4672 version "1.3.0" 4593 version "3.0.0"
4673 resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" 4594 resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
4674 integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY= 4595 integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
4675 dependencies: 4596 dependencies:
4676 balanced-match "^0.4.2" 4597 indent-string "^4.0.0"
4677 math-expression-evaluator "^1.2.14" 4598 strip-indent "^3.0.0"
4678 reduce-function-call "^1.0.1"
4679 4599
4680reduce-function-call@^1.0.1: 4600regenerate-unicode-properties@^8.2.0:
4681 version "1.0.2" 4601 version "8.2.0"
4682 resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" 4602 resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
4683 integrity sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk= 4603 integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
4684 dependencies: 4604 dependencies:
4685 balanced-match "^0.4.2" 4605 regenerate "^1.4.0"
4686 4606
4687regenerate@^1.2.1: 4607regenerate@^1.4.0:
4688 version "1.4.0" 4608 version "1.4.1"
4689 resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" 4609 resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f"
4690 integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== 4610 integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==
4691 4611
4692regenerator-runtime@^0.11.0: 4612regenerator-runtime@^0.13.4:
4693 version "0.11.1" 4613 version "0.13.7"
4694 resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" 4614 resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
4695 integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== 4615 integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
4696 4616
4697regenerator-transform@^0.10.0: 4617regenerator-transform@^0.14.2:
4698 version "0.10.1" 4618 version "0.14.5"
4699 resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" 4619 resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
4700 integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q== 4620 integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==
4701 dependencies: 4621 dependencies:
4702 babel-runtime "^6.18.0" 4622 "@babel/runtime" "^7.8.4"
4703 babel-types "^6.19.0"
4704 private "^0.1.6"
4705 4623
4706regex-not@^1.0.0, regex-not@^1.0.2: 4624regex-not@^1.0.0, regex-not@^1.0.2:
4707 version "1.0.2" 4625 version "1.0.2"
@@ -4711,41 +4629,86 @@ regex-not@^1.0.0, regex-not@^1.0.2:
4711 extend-shallow "^3.0.2" 4629 extend-shallow "^3.0.2"
4712 safe-regex "^1.1.0" 4630 safe-regex "^1.1.0"
4713 4631
4714regexpp@^1.0.1: 4632regexpp@^3.1.0:
4715 version "1.1.0" 4633 version "3.1.0"
4716 resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" 4634 resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
4717 integrity sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw== 4635 integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
4718 4636
4719regexpu-core@^1.0.0: 4637regexpu-core@^4.7.0:
4720 version "1.0.0" 4638 version "4.7.1"
4721 resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" 4639 resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
4722 integrity sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs= 4640 integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
4723 dependencies: 4641 dependencies:
4724 regenerate "^1.2.1" 4642 regenerate "^1.4.0"
4725 regjsgen "^0.2.0" 4643 regenerate-unicode-properties "^8.2.0"
4726 regjsparser "^0.1.4" 4644 regjsgen "^0.5.1"
4727 4645 regjsparser "^0.6.4"
4728regexpu-core@^2.0.0: 4646 unicode-match-property-ecmascript "^1.0.4"
4729 version "2.0.0" 4647 unicode-match-property-value-ecmascript "^1.2.0"
4730 resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" 4648
4731 integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA= 4649regjsgen@^0.5.1:
4732 dependencies: 4650 version "0.5.2"
4733 regenerate "^1.2.1" 4651 resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
4734 regjsgen "^0.2.0" 4652 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 4653
4742regjsparser@^0.1.4: 4654regjsparser@^0.6.4:
4743 version "0.1.5" 4655 version "0.6.4"
4744 resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" 4656 resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
4745 integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw= 4657 integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==
4746 dependencies: 4658 dependencies:
4747 jsesc "~0.5.0" 4659 jsesc "~0.5.0"
4748 4660
4661remark-parse@^8.0.0:
4662 version "8.0.3"
4663 resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1"
4664 integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==
4665 dependencies:
4666 ccount "^1.0.0"
4667 collapse-white-space "^1.0.2"
4668 is-alphabetical "^1.0.0"
4669 is-decimal "^1.0.0"
4670 is-whitespace-character "^1.0.0"
4671 is-word-character "^1.0.0"
4672 markdown-escapes "^1.0.0"
4673 parse-entities "^2.0.0"
4674 repeat-string "^1.5.4"
4675 state-toggle "^1.0.0"
4676 trim "0.0.1"
4677 trim-trailing-lines "^1.0.0"
4678 unherit "^1.0.4"
4679 unist-util-remove-position "^2.0.0"
4680 vfile-location "^3.0.0"
4681 xtend "^4.0.1"
4682
4683remark-stringify@^8.0.0:
4684 version "8.1.1"
4685 resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-8.1.1.tgz#e2a9dc7a7bf44e46a155ec78996db896780d8ce5"
4686 integrity sha512-q4EyPZT3PcA3Eq7vPpT6bIdokXzFGp9i85igjmhRyXWmPs0Y6/d2FYwUNotKAWyLch7g0ASZJn/KHHcHZQ163A==
4687 dependencies:
4688 ccount "^1.0.0"
4689 is-alphanumeric "^1.0.0"
4690 is-decimal "^1.0.0"
4691 is-whitespace-character "^1.0.0"
4692 longest-streak "^2.0.1"
4693 markdown-escapes "^1.0.0"
4694 markdown-table "^2.0.0"
4695 mdast-util-compact "^2.0.0"
4696 parse-entities "^2.0.0"
4697 repeat-string "^1.5.4"
4698 state-toggle "^1.0.0"
4699 stringify-entities "^3.0.0"
4700 unherit "^1.0.4"
4701 xtend "^4.0.1"
4702
4703remark@^12.0.0:
4704 version "12.0.1"
4705 resolved "https://registry.yarnpkg.com/remark/-/remark-12.0.1.tgz#f1ddf68db7be71ca2bad0a33cd3678b86b9c709f"
4706 integrity sha512-gS7HDonkdIaHmmP/+shCPejCEEW+liMp/t/QwmF0Xt47Rpuhl32lLtDV1uKWvGoq+kxr5jSgg5oAIpGuyULjUw==
4707 dependencies:
4708 remark-parse "^8.0.0"
4709 remark-stringify "^8.0.0"
4710 unified "^9.0.0"
4711
4749remove-trailing-separator@^1.0.1: 4712remove-trailing-separator@^1.0.1:
4750 version "1.1.0" 4713 version "1.1.0"
4751 resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" 4714 resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -4756,114 +4719,99 @@ repeat-element@^1.1.2:
4756 resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" 4719 resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
4757 integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== 4720 integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
4758 4721
4759repeat-string@^1.5.2, repeat-string@^1.6.1: 4722repeat-string@^1.0.0, repeat-string@^1.5.4, repeat-string@^1.6.1:
4760 version "1.6.1" 4723 version "1.6.1"
4761 resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" 4724 resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
4762 integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= 4725 integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
4763 4726
4764repeating@^2.0.0: 4727replace-ext@1.0.0:
4765 version "2.0.1" 4728 version "1.0.0"
4766 resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" 4729 resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
4767 integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= 4730 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 4731
4797require-directory@^2.1.1: 4732require-directory@^2.1.1:
4798 version "2.1.1" 4733 version "2.1.1"
4799 resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 4734 resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
4800 integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 4735 integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
4801 4736
4802require-main-filename@^1.0.1: 4737require-main-filename@^2.0.0:
4803 version "1.0.1" 4738 version "2.0.0"
4804 resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" 4739 resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
4805 integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= 4740 integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
4806 4741
4807require-uncached@^1.0.2, require-uncached@^1.0.3: 4742resolve-cwd@^2.0.0:
4808 version "1.0.3" 4743 version "2.0.0"
4809 resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" 4744 resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
4810 integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= 4745 integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
4811 dependencies: 4746 dependencies:
4812 caller-path "^0.1.0" 4747 resolve-from "^3.0.0"
4813 resolve-from "^1.0.0"
4814 4748
4815resolve-from@^1.0.0: 4749resolve-dir@^1.0.0, resolve-dir@^1.0.1:
4816 version "1.0.1" 4750 version "1.0.1"
4817 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" 4751 resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
4818 integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= 4752 integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
4753 dependencies:
4754 expand-tilde "^2.0.0"
4755 global-modules "^1.0.0"
4756
4757resolve-from@^3.0.0:
4758 version "3.0.0"
4759 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
4760 integrity sha1-six699nWiBvItuZTM17rywoYh0g=
4761
4762resolve-from@^4.0.0:
4763 version "4.0.0"
4764 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
4765 integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
4766
4767resolve-from@^5.0.0:
4768 version "5.0.0"
4769 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
4770 integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
4819 4771
4820resolve-url@^0.2.1: 4772resolve-url@^0.2.1:
4821 version "0.2.1" 4773 version "0.2.1"
4822 resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" 4774 resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
4823 integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= 4775 integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
4824 4776
4825resolve@^1.10.0, resolve@^1.5.0: 4777resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2:
4826 version "1.11.0" 4778 version "1.17.0"
4827 resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232" 4779 resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
4828 integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw== 4780 integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
4829 dependencies: 4781 dependencies:
4830 path-parse "^1.0.6" 4782 path-parse "^1.0.6"
4831 4783
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: 4784ret@~0.1.10:
4849 version "0.1.15" 4785 version "0.1.15"
4850 resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" 4786 resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
4851 integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== 4787 integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
4852 4788
4853right-align@^0.1.1: 4789reusify@^1.0.4:
4854 version "0.1.3" 4790 version "1.0.4"
4855 resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" 4791 resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
4856 integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= 4792 integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
4857 dependencies:
4858 align-text "^0.1.1"
4859 4793
4860rimraf@2, rimraf@^2.6.1, rimraf@~2.6.2: 4794rimraf@2.6.3:
4861 version "2.6.3" 4795 version "2.6.3"
4862 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" 4796 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
4863 integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== 4797 integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
4864 dependencies: 4798 dependencies:
4865 glob "^7.1.3" 4799 glob "^7.1.3"
4866 4800
4801rimraf@^2.5.4, rimraf@^2.6.3:
4802 version "2.7.1"
4803 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
4804 integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
4805 dependencies:
4806 glob "^7.1.3"
4807
4808rimraf@^3.0.2:
4809 version "3.0.2"
4810 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
4811 integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
4812 dependencies:
4813 glob "^7.1.3"
4814
4867ripemd160@^2.0.0, ripemd160@^2.0.1: 4815ripemd160@^2.0.0, ripemd160@^2.0.1:
4868 version "2.0.2" 4816 version "2.0.2"
4869 resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" 4817 resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -4872,38 +4820,24 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
4872 hash-base "^3.0.0" 4820 hash-base "^3.0.0"
4873 inherits "^2.0.1" 4821 inherits "^2.0.1"
4874 4822
4875run-async@^0.1.0: 4823run-parallel@^1.1.9:
4876 version "0.1.0" 4824 version "1.1.9"
4877 resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" 4825 resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
4878 integrity sha1-yK1KXhEGYeQCp9IbUw4AnyX444k= 4826 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 4827
4889rx-lite-aggregates@^4.0.8: 4828run-queue@^1.0.0, run-queue@^1.0.3:
4890 version "4.0.8" 4829 version "1.0.3"
4891 resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" 4830 resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
4892 integrity sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74= 4831 integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
4893 dependencies: 4832 dependencies:
4894 rx-lite "*" 4833 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 4834
4901rx-lite@^3.1.2: 4835safe-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" 4836 version "5.2.1"
4903 resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" 4837 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
4904 integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= 4838 integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
4905 4839
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: 4840safe-buffer@~5.1.0, safe-buffer@~5.1.1:
4907 version "5.1.2" 4841 version "5.1.2"
4908 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 4842 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
4909 integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 4843 integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
@@ -4915,63 +4849,28 @@ safe-regex@^1.1.0:
4915 dependencies: 4849 dependencies:
4916 ret "~0.1.10" 4850 ret "~0.1.10"
4917 4851
4918"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: 4852safer-buffer@^2.1.0:
4919 version "2.1.2" 4853 version "2.1.2"
4920 resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 4854 resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
4921 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 4855 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
4922 4856
4923sass-graph@^2.2.4: 4857sass-loader@^10.0.2:
4924 version "2.2.4" 4858 version "10.0.2"
4925 resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" 4859 resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.2.tgz#c7b73010848b264792dd45372eea0b87cba4401e"
4926 integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= 4860 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: 4861 dependencies:
4958 clone-deep "^2.0.1" 4862 klona "^2.0.3"
4959 loader-utils "^1.0.1" 4863 loader-utils "^2.0.0"
4960 lodash.tail "^4.1.1" 4864 neo-async "^2.6.2"
4961 neo-async "^2.5.0" 4865 schema-utils "^2.7.1"
4962 pify "^3.0.0" 4866 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 4867
4969schema-utils@^0.3.0: 4868sass@^1.26.11:
4970 version "0.3.0" 4869 version "1.26.11"
4971 resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" 4870 resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.11.tgz#0f22cc4ab2ba27dad1d4ca30837beb350b709847"
4972 integrity sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8= 4871 integrity sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw==
4973 dependencies: 4872 dependencies:
4974 ajv "^5.0.0" 4873 chokidar ">=2.0.0 <4.0.0"
4975 4874
4976schema-utils@^0.4.5: 4875schema-utils@^0.4.5:
4977 version "0.4.7" 4876 version "0.4.7"
@@ -4981,43 +4880,67 @@ schema-utils@^0.4.5:
4981 ajv "^6.1.0" 4880 ajv "^6.1.0"
4982 ajv-keywords "^3.1.0" 4881 ajv-keywords "^3.1.0"
4983 4882
4984scss-tokenizer@^0.2.3: 4883schema-utils@^1.0.0:
4985 version "0.2.3" 4884 version "1.0.0"
4986 resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" 4885 resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
4987 integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= 4886 integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==
4988 dependencies: 4887 dependencies:
4989 js-base64 "^2.1.8" 4888 ajv "^6.1.0"
4990 source-map "^0.4.2" 4889 ajv-errors "^1.0.0"
4890 ajv-keywords "^3.1.0"
4991 4891
4992"semver@2 || 3 || 4 || 5", semver@^5.3.0: 4892schema-utils@^2.6.5, schema-utils@^2.7.1:
4993 version "5.7.0" 4893 version "2.7.1"
4994 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" 4894 resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
4995 integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== 4895 integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
4896 dependencies:
4897 "@types/json-schema" "^7.0.5"
4898 ajv "^6.12.4"
4899 ajv-keywords "^3.5.2"
4996 4900
4997semver@~5.3.0: 4901"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
4998 version "5.3.0" 4902 version "5.7.1"
4999 resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" 4903 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
5000 integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= 4904 integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
5001 4905
5002set-blocking@^2.0.0, set-blocking@~2.0.0: 4906semver@7.0.0:
5003 version "2.0.0" 4907 version "7.0.0"
5004 resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 4908 resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
5005 integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= 4909 integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
4910
4911semver@^6.0.0:
4912 version "6.3.0"
4913 resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
4914 integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
5006 4915
5007set-value@^0.4.3: 4916semver@^7.2.1, semver@^7.3.2:
5008 version "0.4.3" 4917 version "7.3.2"
5009 resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" 4918 resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
5010 integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= 4919 integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
4920
4921serialize-javascript@^4.0.0:
4922 version "4.0.0"
4923 resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
4924 integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
5011 dependencies: 4925 dependencies:
5012 extend-shallow "^2.0.1" 4926 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 4927
5017set-value@^2.0.0: 4928serialize-javascript@^5.0.1:
4929 version "5.0.1"
4930 resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
4931 integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
4932 dependencies:
4933 randombytes "^2.1.0"
4934
4935set-blocking@^2.0.0:
5018 version "2.0.0" 4936 version "2.0.0"
5019 resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" 4937 resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
5020 integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg== 4938 integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
4939
4940set-value@^2.0.0, set-value@^2.0.1:
4941 version "2.0.1"
4942 resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
4943 integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
5021 dependencies: 4944 dependencies:
5022 extend-shallow "^2.0.1" 4945 extend-shallow "^2.0.1"
5023 is-extendable "^0.1.1" 4946 is-extendable "^0.1.1"
@@ -5037,15 +4960,6 @@ sha.js@^2.4.0, sha.js@^2.4.8:
5037 inherits "^2.0.1" 4960 inherits "^2.0.1"
5038 safe-buffer "^5.0.1" 4961 safe-buffer "^5.0.1"
5039 4962
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: 4963shebang-command@^1.2.0:
5050 version "1.2.0" 4964 version "1.2.0"
5051 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 4965 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -5053,38 +4967,51 @@ shebang-command@^1.2.0:
5053 dependencies: 4967 dependencies:
5054 shebang-regex "^1.0.0" 4968 shebang-regex "^1.0.0"
5055 4969
4970shebang-command@^2.0.0:
4971 version "2.0.0"
4972 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
4973 integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
4974 dependencies:
4975 shebang-regex "^3.0.0"
4976
5056shebang-regex@^1.0.0: 4977shebang-regex@^1.0.0:
5057 version "1.0.0" 4978 version "1.0.0"
5058 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 4979 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
5059 integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= 4980 integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
5060 4981
5061shelljs@^0.6.0: 4982shebang-regex@^3.0.0:
5062 version "0.6.1" 4983 version "3.0.0"
5063 resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" 4984 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
5064 integrity sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg= 4985 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 4986
5071slash@^1.0.0: 4987signal-exit@^3.0.2:
5072 version "1.0.0" 4988 version "3.0.3"
5073 resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 4989 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
5074 integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= 4990 integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
5075 4991
5076slice-ansi@0.0.4: 4992slash@^3.0.0:
5077 version "0.0.4" 4993 version "3.0.0"
5078 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" 4994 resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
5079 integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= 4995 integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
5080 4996
5081slice-ansi@1.0.0: 4997slice-ansi@^2.1.0:
5082 version "1.0.0" 4998 version "2.1.0"
5083 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" 4999 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
5084 integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg== 5000 integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
5085 dependencies: 5001 dependencies:
5002 ansi-styles "^3.2.0"
5003 astral-regex "^1.0.0"
5086 is-fullwidth-code-point "^2.0.0" 5004 is-fullwidth-code-point "^2.0.0"
5087 5005
5006slice-ansi@^4.0.0:
5007 version "4.0.0"
5008 resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
5009 integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
5010 dependencies:
5011 ansi-styles "^4.0.0"
5012 astral-regex "^2.0.0"
5013 is-fullwidth-code-point "^3.0.0"
5014
5088snapdragon-node@^2.0.1: 5015snapdragon-node@^2.0.1:
5089 version "2.1.1" 5016 version "2.1.1"
5090 resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" 5017 resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -5128,70 +5055,69 @@ source-list-map@^2.0.0:
5128 integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== 5055 integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
5129 5056
5130source-map-resolve@^0.5.0: 5057source-map-resolve@^0.5.0:
5131 version "0.5.2" 5058 version "0.5.3"
5132 resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" 5059 resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
5133 integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== 5060 integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
5134 dependencies: 5061 dependencies:
5135 atob "^2.1.1" 5062 atob "^2.1.2"
5136 decode-uri-component "^0.2.0" 5063 decode-uri-component "^0.2.0"
5137 resolve-url "^0.2.1" 5064 resolve-url "^0.2.1"
5138 source-map-url "^0.4.0" 5065 source-map-url "^0.4.0"
5139 urix "^0.1.0" 5066 urix "^0.1.0"
5140 5067
5141source-map-support@^0.4.15: 5068source-map-support@~0.5.12:
5142 version "0.4.18" 5069 version "0.5.19"
5143 resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" 5070 resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
5144 integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== 5071 integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
5145 dependencies: 5072 dependencies:
5146 source-map "^0.5.6" 5073 buffer-from "^1.0.0"
5074 source-map "^0.6.0"
5147 5075
5148source-map-url@^0.4.0: 5076source-map-url@^0.4.0:
5149 version "0.4.0" 5077 version "0.4.0"
5150 resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" 5078 resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
5151 integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= 5079 integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
5152 5080
5153source-map@^0.4.2: 5081source-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" 5082 version "0.5.7"
5162 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" 5083 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
5163 integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= 5084 integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
5164 5085
5165source-map@^0.6.1, source-map@~0.6.1: 5086source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
5166 version "0.6.1" 5087 version "0.6.1"
5167 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 5088 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
5168 integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 5089 integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
5169 5090
5170spdx-correct@^3.0.0: 5091spdx-correct@^3.0.0:
5171 version "3.1.0" 5092 version "3.1.1"
5172 resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" 5093 resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
5173 integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== 5094 integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
5174 dependencies: 5095 dependencies:
5175 spdx-expression-parse "^3.0.0" 5096 spdx-expression-parse "^3.0.0"
5176 spdx-license-ids "^3.0.0" 5097 spdx-license-ids "^3.0.0"
5177 5098
5178spdx-exceptions@^2.1.0: 5099spdx-exceptions@^2.1.0:
5179 version "2.2.0" 5100 version "2.3.0"
5180 resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" 5101 resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
5181 integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== 5102 integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
5182 5103
5183spdx-expression-parse@^3.0.0: 5104spdx-expression-parse@^3.0.0:
5184 version "3.0.0" 5105 version "3.0.1"
5185 resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" 5106 resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
5186 integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== 5107 integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
5187 dependencies: 5108 dependencies:
5188 spdx-exceptions "^2.1.0" 5109 spdx-exceptions "^2.1.0"
5189 spdx-license-ids "^3.0.0" 5110 spdx-license-ids "^3.0.0"
5190 5111
5191spdx-license-ids@^3.0.0: 5112spdx-license-ids@^3.0.0:
5192 version "3.0.4" 5113 version "3.0.6"
5193 resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1" 5114 resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
5194 integrity sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA== 5115 integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==
5116
5117specificity@^0.4.1:
5118 version "0.4.1"
5119 resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019"
5120 integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==
5195 5121
5196split-string@^3.0.1, split-string@^3.0.2: 5122split-string@^3.0.1, split-string@^3.0.2:
5197 version "3.1.0" 5123 version "3.1.0"
@@ -5205,20 +5131,24 @@ sprintf-js@~1.0.2:
5205 resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 5131 resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
5206 integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= 5132 integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
5207 5133
5208sshpk@^1.7.0: 5134ssri@^6.0.1:
5209 version "1.16.1" 5135 version "6.0.1"
5210 resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" 5136 resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
5211 integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== 5137 integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
5212 dependencies: 5138 dependencies:
5213 asn1 "~0.2.3" 5139 figgy-pudding "^3.5.1"
5214 assert-plus "^1.0.0" 5140
5215 bcrypt-pbkdf "^1.0.0" 5141ssri@^8.0.0:
5216 dashdash "^1.12.0" 5142 version "8.0.0"
5217 ecc-jsbn "~0.1.1" 5143 resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808"
5218 getpass "^0.1.1" 5144 integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==
5219 jsbn "~0.1.0" 5145 dependencies:
5220 safer-buffer "^2.0.2" 5146 minipass "^3.1.1"
5221 tweetnacl "~0.14.0" 5147
5148state-toggle@^1.0.0:
5149 version "1.0.3"
5150 resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"
5151 integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==
5222 5152
5223static-extend@^0.1.1: 5153static-extend@^0.1.1:
5224 version "0.1.2" 5154 version "0.1.2"
@@ -5228,13 +5158,6 @@ static-extend@^0.1.1:
5228 define-property "^0.2.5" 5158 define-property "^0.2.5"
5229 object-copy "^0.1.0" 5159 object-copy "^0.1.0"
5230 5160
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: 5161stream-browserify@^2.0.1:
5239 version "2.0.2" 5162 version "2.0.2"
5240 resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" 5163 resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
@@ -5243,6 +5166,14 @@ stream-browserify@^2.0.1:
5243 inherits "~2.0.1" 5166 inherits "~2.0.1"
5244 readable-stream "^2.0.2" 5167 readable-stream "^2.0.2"
5245 5168
5169stream-each@^1.1.0:
5170 version "1.2.3"
5171 resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
5172 integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==
5173 dependencies:
5174 end-of-stream "^1.1.0"
5175 stream-shift "^1.0.0"
5176
5246stream-http@^2.7.2: 5177stream-http@^2.7.2:
5247 version "2.8.3" 5178 version "2.8.3"
5248 resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" 5179 resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
@@ -5254,34 +5185,56 @@ stream-http@^2.7.2:
5254 to-arraybuffer "^1.0.0" 5185 to-arraybuffer "^1.0.0"
5255 xtend "^4.0.0" 5186 xtend "^4.0.0"
5256 5187
5188stream-shift@^1.0.0:
5189 version "1.0.1"
5190 resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
5191 integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
5192
5257strict-uri-encode@^1.0.0: 5193strict-uri-encode@^1.0.0:
5258 version "1.1.0" 5194 version "1.1.0"
5259 resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" 5195 resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
5260 integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= 5196 integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
5261 5197
5262string-width@^1.0.1, string-width@^1.0.2: 5198string-width@^3.0.0, string-width@^3.1.0:
5263 version "1.0.2" 5199 version "3.1.0"
5264 resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 5200 resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
5265 integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= 5201 integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
5266 dependencies: 5202 dependencies:
5267 code-point-at "^1.0.0" 5203 emoji-regex "^7.0.1"
5268 is-fullwidth-code-point "^1.0.0" 5204 is-fullwidth-code-point "^2.0.0"
5269 strip-ansi "^3.0.0" 5205 strip-ansi "^5.1.0"
5270 5206
5271"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: 5207string-width@^4.2.0:
5272 version "2.1.1" 5208 version "4.2.0"
5273 resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 5209 resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
5274 integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 5210 integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
5275 dependencies: 5211 dependencies:
5276 is-fullwidth-code-point "^2.0.0" 5212 emoji-regex "^8.0.0"
5277 strip-ansi "^4.0.0" 5213 is-fullwidth-code-point "^3.0.0"
5214 strip-ansi "^6.0.0"
5278 5215
5279string_decoder@^1.0.0: 5216string.prototype.trimend@^1.0.1:
5280 version "1.2.0" 5217 version "1.0.1"
5281 resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" 5218 resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
5282 integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== 5219 integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
5283 dependencies: 5220 dependencies:
5284 safe-buffer "~5.1.0" 5221 define-properties "^1.1.3"
5222 es-abstract "^1.17.5"
5223
5224string.prototype.trimstart@^1.0.1:
5225 version "1.0.1"
5226 resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
5227 integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
5228 dependencies:
5229 define-properties "^1.1.3"
5230 es-abstract "^1.17.5"
5231
5232string_decoder@^1.0.0, string_decoder@^1.1.1:
5233 version "1.3.0"
5234 resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
5235 integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
5236 dependencies:
5237 safe-buffer "~5.2.0"
5285 5238
5286string_decoder@~1.1.1: 5239string_decoder@~1.1.1:
5287 version "1.1.1" 5240 version "1.1.1"
@@ -5290,185 +5243,277 @@ string_decoder@~1.1.1:
5290 dependencies: 5243 dependencies:
5291 safe-buffer "~5.1.0" 5244 safe-buffer "~5.1.0"
5292 5245
5293strip-ansi@^3.0.0, strip-ansi@^3.0.1: 5246stringify-entities@^3.0.0:
5294 version "3.0.1" 5247 version "3.0.1"
5295 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 5248 resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-3.0.1.tgz#32154b91286ab0869ab2c07696223bd23b6dbfc0"
5296 integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= 5249 integrity sha512-Lsk3ISA2++eJYqBMPKcr/8eby1I6L0gP0NlxF8Zja6c05yr/yCYyb2c9PwXjd08Ib3If1vn1rbs1H5ZtVuOfvQ==
5297 dependencies: 5250 dependencies:
5298 ansi-regex "^2.0.0" 5251 character-entities-html4 "^1.0.0"
5252 character-entities-legacy "^1.0.0"
5253 is-alphanumerical "^1.0.0"
5254 is-decimal "^1.0.2"
5255 is-hexadecimal "^1.0.0"
5299 5256
5300strip-ansi@^4.0.0: 5257strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
5301 version "4.0.0" 5258 version "5.2.0"
5302 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 5259 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
5303 integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= 5260 integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
5304 dependencies: 5261 dependencies:
5305 ansi-regex "^3.0.0" 5262 ansi-regex "^4.1.0"
5306 5263
5307strip-bom@^2.0.0: 5264strip-ansi@^6.0.0:
5308 version "2.0.0" 5265 version "6.0.0"
5309 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" 5266 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
5310 integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= 5267 integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
5311 dependencies: 5268 dependencies:
5312 is-utf8 "^0.2.0" 5269 ansi-regex "^5.0.0"
5313 5270
5314strip-bom@^3.0.0: 5271strip-bom@^3.0.0:
5315 version "3.0.0" 5272 version "3.0.0"
5316 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" 5273 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
5317 integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= 5274 integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
5318 5275
5319strip-eof@^1.0.0: 5276strip-indent@^3.0.0:
5320 version "1.0.0" 5277 version "3.0.0"
5321 resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" 5278 resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
5322 integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= 5279 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: 5280 dependencies:
5329 get-stdin "^4.0.1" 5281 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 5282
5336strip-json-comments@~2.0.1: 5283strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
5337 version "2.0.1" 5284 version "3.1.1"
5338 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 5285 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
5339 integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= 5286 integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
5340 5287
5341style-loader@^0.19.1: 5288style-search@^0.1.0:
5342 version "0.19.1" 5289 version "0.1.0"
5343 resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.19.1.tgz#591ffc80bcefe268b77c5d9ebc0505d772619f85" 5290 resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
5344 integrity sha512-IRE+ijgojrygQi3rsqT0U4dd+UcPCqcVvauZpCnQrGAlEe+FUIyrK93bUDScamesjP08JlQNsFJU+KmPedP5Og== 5291 integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=
5345 dependencies:
5346 loader-utils "^1.0.2"
5347 schema-utils "^0.3.0"
5348 5292
5349supports-color@^2.0.0: 5293stylelint-config-recommended@^3.0.0:
5294 version "3.0.0"
5295 resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz#e0e547434016c5539fe2650afd58049a2fd1d657"
5296 integrity sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ==
5297
5298stylelint-config-standard@^20.0.0:
5299 version "20.0.0"
5300 resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-20.0.0.tgz#06135090c9e064befee3d594289f50e295b5e20d"
5301 integrity sha512-IB2iFdzOTA/zS4jSVav6z+wGtin08qfj+YyExHB3LF9lnouQht//YyB0KZq9gGz5HNPkddHOzcY8HsUey6ZUlA==
5302 dependencies:
5303 stylelint-config-recommended "^3.0.0"
5304
5305stylelint-scss@^3.18.0:
5306 version "3.18.0"
5307 resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.18.0.tgz#8f06371c223909bf3f62e839548af1badeed31e9"
5308 integrity sha512-LD7+hv/6/ApNGt7+nR/50ft7cezKP2HM5rI8avIdGaUWre3xlHfV4jKO/DRZhscfuN+Ewy9FMhcTq0CcS0C/SA==
5309 dependencies:
5310 lodash "^4.17.15"
5311 postcss-media-query-parser "^0.2.3"
5312 postcss-resolve-nested-selector "^0.1.1"
5313 postcss-selector-parser "^6.0.2"
5314 postcss-value-parser "^4.1.0"
5315
5316stylelint@^13.7.1:
5317 version "13.7.1"
5318 resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.7.1.tgz#bee97ee78d778a3f1dbe3f7397b76414973e263e"
5319 integrity sha512-qzqazcyRxrSRdmFuO0/SZOJ+LyCxYy0pwcvaOBBnl8/2VfHSMrtNIE+AnyJoyq6uKb+mt+hlgmVrvVi6G6XHfQ==
5320 dependencies:
5321 "@stylelint/postcss-css-in-js" "^0.37.2"
5322 "@stylelint/postcss-markdown" "^0.36.1"
5323 autoprefixer "^9.8.6"
5324 balanced-match "^1.0.0"
5325 chalk "^4.1.0"
5326 cosmiconfig "^7.0.0"
5327 debug "^4.1.1"
5328 execall "^2.0.0"
5329 fast-glob "^3.2.4"
5330 fastest-levenshtein "^1.0.12"
5331 file-entry-cache "^5.0.1"
5332 get-stdin "^8.0.0"
5333 global-modules "^2.0.0"
5334 globby "^11.0.1"
5335 globjoin "^0.1.4"
5336 html-tags "^3.1.0"
5337 ignore "^5.1.8"
5338 import-lazy "^4.0.0"
5339 imurmurhash "^0.1.4"
5340 known-css-properties "^0.19.0"
5341 lodash "^4.17.20"
5342 log-symbols "^4.0.0"
5343 mathml-tag-names "^2.1.3"
5344 meow "^7.1.1"
5345 micromatch "^4.0.2"
5346 normalize-selector "^0.2.0"
5347 postcss "^7.0.32"
5348 postcss-html "^0.36.0"
5349 postcss-less "^3.1.4"
5350 postcss-media-query-parser "^0.2.3"
5351 postcss-resolve-nested-selector "^0.1.1"
5352 postcss-safe-parser "^4.0.2"
5353 postcss-sass "^0.4.4"
5354 postcss-scss "^2.1.1"
5355 postcss-selector-parser "^6.0.2"
5356 postcss-syntax "^0.36.2"
5357 postcss-value-parser "^4.1.0"
5358 resolve-from "^5.0.0"
5359 slash "^3.0.0"
5360 specificity "^0.4.1"
5361 string-width "^4.2.0"
5362 strip-ansi "^6.0.0"
5363 style-search "^0.1.0"
5364 sugarss "^2.0.0"
5365 svg-tags "^1.0.0"
5366 table "^6.0.1"
5367 v8-compile-cache "^2.1.1"
5368 write-file-atomic "^3.0.3"
5369
5370sugarss@^2.0.0:
5350 version "2.0.0" 5371 version "2.0.0"
5351 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 5372 resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d"
5352 integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= 5373 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: 5374 dependencies:
5359 has-flag "^1.0.0" 5375 postcss "^7.0.2"
5360 5376
5361supports-color@^4.2.1: 5377supports-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" 5378 version "5.5.0"
5370 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 5379 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
5371 integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 5380 integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
5372 dependencies: 5381 dependencies:
5373 has-flag "^3.0.0" 5382 has-flag "^3.0.0"
5374 5383
5375svgo@^0.7.0: 5384supports-color@^6.1.0:
5376 version "0.7.2" 5385 version "6.1.0"
5377 resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" 5386 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
5378 integrity sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U= 5387 integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
5379 dependencies: 5388 dependencies:
5380 coa "~1.0.1" 5389 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 5390
5388table@4.0.2: 5391supports-color@^7.0.0, supports-color@^7.1.0:
5389 version "4.0.2" 5392 version "7.2.0"
5390 resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" 5393 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
5391 integrity sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA== 5394 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: 5395 dependencies:
5422 block-stream "*" 5396 has-flag "^4.0.0"
5423 fstream "^1.0.12"
5424 inherits "2"
5425 5397
5426tar@^4: 5398svg-tags@^1.0.0:
5427 version "4.4.8" 5399 version "1.0.0"
5428 resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" 5400 resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
5429 integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== 5401 integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
5402
5403table@^5.2.3:
5404 version "5.4.6"
5405 resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
5406 integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
5407 dependencies:
5408 ajv "^6.10.2"
5409 lodash "^4.17.14"
5410 slice-ansi "^2.1.0"
5411 string-width "^3.0.0"
5412
5413table@^6.0.1:
5414 version "6.0.3"
5415 resolved "https://registry.yarnpkg.com/table/-/table-6.0.3.tgz#e5b8a834e37e27ad06de2e0fda42b55cfd8a0123"
5416 integrity sha512-8321ZMcf1B9HvVX/btKv8mMZahCjn2aYrDlpqHaBFCfnox64edeH9kEid0vTLTRR8gWR2A20aDgeuTTea4sVtw==
5417 dependencies:
5418 ajv "^6.12.4"
5419 lodash "^4.17.20"
5420 slice-ansi "^4.0.0"
5421 string-width "^4.2.0"
5422
5423tapable@^1.0.0, tapable@^1.1.3:
5424 version "1.1.3"
5425 resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
5426 integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
5427
5428tar@^6.0.2:
5429 version "6.0.5"
5430 resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
5431 integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
5432 dependencies:
5433 chownr "^2.0.0"
5434 fs-minipass "^2.0.0"
5435 minipass "^3.0.0"
5436 minizlib "^2.1.1"
5437 mkdirp "^1.0.3"
5438 yallist "^4.0.0"
5439
5440terser-webpack-plugin@^1.4.3:
5441 version "1.4.5"
5442 resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b"
5443 integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==
5444 dependencies:
5445 cacache "^12.0.2"
5446 find-cache-dir "^2.1.0"
5447 is-wsl "^1.1.0"
5448 schema-utils "^1.0.0"
5449 serialize-javascript "^4.0.0"
5450 source-map "^0.6.1"
5451 terser "^4.1.2"
5452 webpack-sources "^1.4.0"
5453 worker-farm "^1.7.0"
5454
5455terser-webpack-plugin@^4.2.2:
5456 version "4.2.2"
5457 resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.2.2.tgz#d86200c700053bba637913fe4310ba1bdeb5568e"
5458 integrity sha512-3qAQpykRTD5DReLu5/cwpsg7EZFzP3Q0Hp2XUWJUw2mpq2jfgOKTZr8IZKKnNieRVVo1UauROTdhbQJZveGKtQ==
5459 dependencies:
5460 cacache "^15.0.5"
5461 find-cache-dir "^3.3.1"
5462 jest-worker "^26.3.0"
5463 p-limit "^3.0.2"
5464 schema-utils "^2.7.1"
5465 serialize-javascript "^5.0.1"
5466 source-map "^0.6.1"
5467 terser "^5.3.2"
5468 webpack-sources "^1.4.3"
5469
5470terser@^4.1.2:
5471 version "4.8.0"
5472 resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
5473 integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
5430 dependencies: 5474 dependencies:
5431 chownr "^1.1.1" 5475 commander "^2.20.0"
5432 fs-minipass "^1.2.5" 5476 source-map "~0.6.1"
5433 minipass "^2.3.4" 5477 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 5478
5439text-table@~0.2.0: 5479terser@^5.3.2:
5480 version "5.3.2"
5481 resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.2.tgz#f4bea90eb92945b2a028ceef79181b9bb586e7af"
5482 integrity sha512-H67sydwBz5jCUA32ZRL319ULu+Su1cAoZnnc+lXnenGRYWyLE3Scgkt8mNoAsMx0h5kdo758zdoS0LG9rYZXDQ==
5483 dependencies:
5484 commander "^2.20.0"
5485 source-map "~0.6.1"
5486 source-map-support "~0.5.12"
5487
5488text-table@^0.2.0:
5440 version "0.2.0" 5489 version "0.2.0"
5441 resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" 5490 resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
5442 integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= 5491 integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
5443 5492
5444through@^2.3.6: 5493through2@^2.0.0:
5445 version "2.3.8" 5494 version "2.0.5"
5446 resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 5495 resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
5447 integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= 5496 integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
5497 dependencies:
5498 readable-stream "~2.3.6"
5499 xtend "~4.0.1"
5448 5500
5449timers-browserify@^2.0.4: 5501timers-browserify@^2.0.4:
5450 version "2.0.10" 5502 version "2.0.11"
5451 resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" 5503 resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"
5452 integrity sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg== 5504 integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==
5453 dependencies: 5505 dependencies:
5454 setimmediate "^1.0.4" 5506 setimmediate "^1.0.4"
5455 5507
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: 5508to-arraybuffer@^1.0.0:
5464 version "1.0.1" 5509 version "1.0.1"
5465 resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" 5510 resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
5466 integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= 5511 integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
5467 5512
5468to-fast-properties@^1.0.3: 5513to-fast-properties@^2.0.0:
5469 version "1.0.3" 5514 version "2.0.0"
5470 resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" 5515 resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
5471 integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= 5516 integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
5472 5517
5473to-object-path@^0.3.0: 5518to-object-path@^0.3.0:
5474 version "0.3.0" 5519 version "0.3.0"
@@ -5485,6 +5530,13 @@ to-regex-range@^2.1.0:
5485 is-number "^3.0.0" 5530 is-number "^3.0.0"
5486 repeat-string "^1.6.1" 5531 repeat-string "^1.6.1"
5487 5532
5533to-regex-range@^5.0.1:
5534 version "5.0.1"
5535 resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
5536 integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
5537 dependencies:
5538 is-number "^7.0.0"
5539
5488to-regex@^3.0.1, to-regex@^3.0.2: 5540to-regex@^3.0.1, to-regex@^3.0.2:
5489 version "3.0.2" 5541 version "3.0.2"
5490 resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" 5542 resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -5495,108 +5547,194 @@ to-regex@^3.0.1, to-regex@^3.0.2:
5495 regex-not "^1.0.2" 5547 regex-not "^1.0.2"
5496 safe-regex "^1.1.0" 5548 safe-regex "^1.1.0"
5497 5549
5498tough-cookie@~2.4.3: 5550trim-newlines@^3.0.0:
5499 version "2.4.3" 5551 version "3.0.0"
5500 resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" 5552 resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30"
5501 integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== 5553 integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==
5502 dependencies:
5503 psl "^1.1.24"
5504 punycode "^1.4.1"
5505 5554
5506trim-newlines@^1.0.0: 5555trim-trailing-lines@^1.0.0:
5507 version "1.0.0" 5556 version "1.1.3"
5508 resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" 5557 resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz#7f0739881ff76657b7776e10874128004b625a94"
5509 integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= 5558 integrity sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==
5510 5559
5511trim-right@^1.0.1: 5560trim@0.0.1:
5512 version "1.0.1" 5561 version "0.0.1"
5513 resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" 5562 resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
5514 integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= 5563 integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0=
5515 5564
5516"true-case-path@^1.0.2": 5565trough@^1.0.0:
5517 version "1.0.3" 5566 version "1.0.5"
5518 resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" 5567 resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
5519 integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== 5568 integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
5569
5570tsconfig-paths@^3.9.0:
5571 version "3.9.0"
5572 resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
5573 integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==
5520 dependencies: 5574 dependencies:
5521 glob "^7.1.2" 5575 "@types/json5" "^0.0.29"
5576 json5 "^1.0.1"
5577 minimist "^1.2.0"
5578 strip-bom "^3.0.0"
5579
5580tslib@^1.9.0:
5581 version "1.13.0"
5582 resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
5583 integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
5522 5584
5523tty-browserify@0.0.0: 5585tty-browserify@0.0.0:
5524 version "0.0.0" 5586 version "0.0.0"
5525 resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" 5587 resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
5526 integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= 5588 integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
5527 5589
5528tunnel-agent@^0.6.0: 5590type-check@^0.4.0, type-check@~0.4.0:
5529 version "0.6.0" 5591 version "0.4.0"
5530 resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 5592 resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
5531 integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= 5593 integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
5532 dependencies: 5594 dependencies:
5533 safe-buffer "^5.0.1" 5595 prelude-ls "^1.2.1"
5534 5596
5535tweetnacl@^0.14.3, tweetnacl@~0.14.0: 5597type-fest@^0.13.1:
5536 version "0.14.5" 5598 version "0.13.1"
5537 resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 5599 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
5538 integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= 5600 integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
5539 5601
5540type-check@~0.3.2: 5602type-fest@^0.6.0:
5541 version "0.3.2" 5603 version "0.6.0"
5542 resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" 5604 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
5543 integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= 5605 integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
5606
5607type-fest@^0.8.1:
5608 version "0.8.1"
5609 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
5610 integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
5611
5612typedarray-to-buffer@^3.1.5:
5613 version "3.1.5"
5614 resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
5615 integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
5544 dependencies: 5616 dependencies:
5545 prelude-ls "~1.1.2" 5617 is-typedarray "^1.0.0"
5546 5618
5547typedarray@^0.0.6: 5619typedarray@^0.0.6:
5548 version "0.0.6" 5620 version "0.0.6"
5549 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 5621 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
5550 integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= 5622 integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
5551 5623
5552uglify-js@^2.8.29: 5624unherit@^1.0.4:
5553 version "2.8.29" 5625 version "1.1.3"
5554 resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" 5626 resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22"
5555 integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= 5627 integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==
5556 dependencies: 5628 dependencies:
5557 source-map "~0.5.1" 5629 inherits "^2.0.0"
5558 yargs "~3.10.0" 5630 xtend "^4.0.0"
5559 optionalDependencies:
5560 uglify-to-browserify "~1.0.0"
5561 5631
5562uglify-to-browserify@~1.0.0: 5632unicode-canonical-property-names-ecmascript@^1.0.4:
5563 version "1.0.2" 5633 version "1.0.4"
5564 resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" 5634 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= 5635 integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
5566 5636
5567uglifyjs-webpack-plugin@^0.4.6: 5637unicode-match-property-ecmascript@^1.0.4:
5568 version "0.4.6" 5638 version "1.0.4"
5569 resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" 5639 resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
5570 integrity sha1-uVH0q7a9YX5m9j64kUmOORdj4wk= 5640 integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
5571 dependencies: 5641 dependencies:
5572 source-map "^0.5.6" 5642 unicode-canonical-property-names-ecmascript "^1.0.4"
5573 uglify-js "^2.8.29" 5643 unicode-property-aliases-ecmascript "^1.0.4"
5574 webpack-sources "^1.0.1" 5644
5645unicode-match-property-value-ecmascript@^1.2.0:
5646 version "1.2.0"
5647 resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
5648 integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
5649
5650unicode-property-aliases-ecmascript@^1.0.4:
5651 version "1.1.0"
5652 resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
5653 integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
5654
5655unified@^9.0.0:
5656 version "9.2.0"
5657 resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8"
5658 integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==
5659 dependencies:
5660 bail "^1.0.0"
5661 extend "^3.0.0"
5662 is-buffer "^2.0.0"
5663 is-plain-obj "^2.0.0"
5664 trough "^1.0.0"
5665 vfile "^4.0.0"
5575 5666
5576union-value@^1.0.0: 5667union-value@^1.0.0:
5577 version "1.0.0" 5668 version "1.0.1"
5578 resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" 5669 resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
5579 integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ= 5670 integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
5580 dependencies: 5671 dependencies:
5581 arr-union "^3.1.0" 5672 arr-union "^3.1.0"
5582 get-value "^2.0.6" 5673 get-value "^2.0.6"
5583 is-extendable "^0.1.1" 5674 is-extendable "^0.1.1"
5584 set-value "^0.4.3" 5675 set-value "^2.0.1"
5585 5676
5586uniq@^1.0.1: 5677uniq@^1.0.1:
5587 version "1.0.1" 5678 version "1.0.1"
5588 resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" 5679 resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
5589 integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= 5680 integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
5590 5681
5591uniqs@^2.0.0: 5682unique-filename@^1.1.1:
5592 version "2.0.0" 5683 version "1.1.1"
5593 resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" 5684 resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
5594 integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= 5685 integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
5686 dependencies:
5687 unique-slug "^2.0.0"
5595 5688
5596universalify@^0.1.0: 5689unique-slug@^2.0.0:
5597 version "0.1.2" 5690 version "2.0.2"
5598 resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" 5691 resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
5599 integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== 5692 integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
5693 dependencies:
5694 imurmurhash "^0.1.4"
5695
5696unist-util-find-all-after@^3.0.1:
5697 version "3.0.1"
5698 resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-3.0.1.tgz#95cc62f48812d879b4685a0512bf1b838da50e9a"
5699 integrity sha512-0GICgc++sRJesLwEYDjFVJPJttBpVQaTNgc6Jw0Jhzvfs+jtKePEMu+uD+PqkRUrAvGQqwhpDwLGWo1PK8PDEw==
5700 dependencies:
5701 unist-util-is "^4.0.0"
5702
5703unist-util-is@^4.0.0:
5704 version "4.0.2"
5705 resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.2.tgz#c7d1341188aa9ce5b3cff538958de9895f14a5de"
5706 integrity sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ==
5707
5708unist-util-remove-position@^2.0.0:
5709 version "2.0.1"
5710 resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc"
5711 integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==
5712 dependencies:
5713 unist-util-visit "^2.0.0"
5714
5715unist-util-stringify-position@^2.0.0:
5716 version "2.0.3"
5717 resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da"
5718 integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==
5719 dependencies:
5720 "@types/unist" "^2.0.2"
5721
5722unist-util-visit-parents@^3.0.0:
5723 version "3.1.0"
5724 resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.0.tgz#4dd262fb9dcfe44f297d53e882fc6ff3421173d5"
5725 integrity sha512-0g4wbluTF93npyPrp/ymd3tCDTMnP0yo2akFD2FIBAYXq/Sga3lwaU1D8OYKbtpioaI6CkDcQ6fsMnmtzt7htw==
5726 dependencies:
5727 "@types/unist" "^2.0.0"
5728 unist-util-is "^4.0.0"
5729
5730unist-util-visit@^2.0.0:
5731 version "2.0.3"
5732 resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
5733 integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==
5734 dependencies:
5735 "@types/unist" "^2.0.0"
5736 unist-util-is "^4.0.0"
5737 unist-util-visit-parents "^3.0.0"
5600 5738
5601unset-value@^1.0.0: 5739unset-value@^1.0.0:
5602 version "1.0.0" 5740 version "1.0.0"
@@ -5607,14 +5745,14 @@ unset-value@^1.0.0:
5607 isobject "^3.0.0" 5745 isobject "^3.0.0"
5608 5746
5609upath@^1.1.1: 5747upath@^1.1.1:
5610 version "1.1.2" 5748 version "1.2.0"
5611 resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" 5749 resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
5612 integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== 5750 integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
5613 5751
5614uri-js@^4.2.2: 5752uri-js@^4.2.2:
5615 version "4.2.2" 5753 version "4.4.0"
5616 resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" 5754 resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602"
5617 integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== 5755 integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==
5618 dependencies: 5756 dependencies:
5619 punycode "^2.1.0" 5757 punycode "^2.1.0"
5620 5758
@@ -5623,15 +5761,6 @@ urix@^0.1.0:
5623 resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" 5761 resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
5624 integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= 5762 integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
5625 5763
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: 5764url@^0.11.0:
5636 version "0.11.0" 5765 version "0.11.0"
5637 resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" 5766 resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
@@ -5645,14 +5774,7 @@ use@^3.1.0:
5645 resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" 5774 resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
5646 integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== 5775 integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
5647 5776
5648user-home@^2.0.0: 5777util-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" 5778 version "1.0.2"
5657 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 5779 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
5658 integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 5780 integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
@@ -5664,13 +5786,6 @@ util@0.10.3:
5664 dependencies: 5786 dependencies:
5665 inherits "2.0.1" 5787 inherits "2.0.1"
5666 5788
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: 5789util@^0.11.0:
5675 version "0.11.1" 5790 version "0.11.1"
5676 resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" 5791 resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
@@ -5678,10 +5793,10 @@ util@^0.11.0:
5678 dependencies: 5793 dependencies:
5679 inherits "2.0.3" 5794 inherits "2.0.3"
5680 5795
5681uuid@^3.3.2: 5796v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1:
5682 version "3.3.2" 5797 version "2.1.1"
5683 resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" 5798 resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
5684 integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== 5799 integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
5685 5800
5686validate-npm-package-license@^3.0.1: 5801validate-npm-package-license@^3.0.1:
5687 version "3.0.4" 5802 version "3.0.4"
@@ -5691,214 +5806,222 @@ validate-npm-package-license@^3.0.1:
5691 spdx-correct "^3.0.0" 5806 spdx-correct "^3.0.0"
5692 spdx-expression-parse "^3.0.0" 5807 spdx-expression-parse "^3.0.0"
5693 5808
5694vendors@^1.0.0: 5809vfile-location@^3.0.0:
5695 version "1.0.3" 5810 version "3.1.0"
5696 resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.3.tgz#a6467781abd366217c050f8202e7e50cc9eef8c0" 5811 resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.1.0.tgz#81cd8a04b0ac935185f4fce16f270503fc2f692f"
5697 integrity sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw== 5812 integrity sha512-FCZ4AN9xMcjFIG1oGmZKo61PjwJHRVA+0/tPUP2ul4uIwjGGndIxavEMRpWn5p4xwm/ZsdXp9YNygf1ZyE4x8g==
5698 5813
5699verror@1.10.0: 5814vfile-message@^2.0.0:
5700 version "1.10.0" 5815 version "2.0.4"
5701 resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 5816 resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a"
5702 integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= 5817 integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==
5703 dependencies: 5818 dependencies:
5704 assert-plus "^1.0.0" 5819 "@types/unist" "^2.0.0"
5705 core-util-is "1.0.2" 5820 unist-util-stringify-position "^2.0.0"
5706 extsprintf "^1.2.0" 5821
5822vfile@^4.0.0:
5823 version "4.2.0"
5824 resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.0.tgz#26c78ac92eb70816b01d4565e003b7e65a2a0e01"
5825 integrity sha512-a/alcwCvtuc8OX92rqqo7PflxiCgXRFjdyoGVuYV+qbgCb0GgZJRvIgCD4+U/Kl1yhaRsaTwksF88xbPyGsgpw==
5826 dependencies:
5827 "@types/unist" "^2.0.0"
5828 is-buffer "^2.0.0"
5829 replace-ext "1.0.0"
5830 unist-util-stringify-position "^2.0.0"
5831 vfile-message "^2.0.0"
5832
5833vm-browserify@^1.0.1:
5834 version "1.1.2"
5835 resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
5836 integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
5707 5837
5708vm-browserify@0.0.4: 5838watchpack-chokidar2@^2.0.0:
5709 version "0.0.4" 5839 version "2.0.0"
5710 resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" 5840 resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
5711 integrity sha1-XX6kW7755Kb/ZflUOOCofDV9WnM= 5841 integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==
5712 dependencies: 5842 dependencies:
5713 indexof "0.0.1" 5843 chokidar "^2.1.8"
5714 5844
5715watchpack@^1.4.0: 5845watchpack@^1.7.4:
5716 version "1.6.0" 5846 version "1.7.4"
5717 resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" 5847 resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b"
5718 integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== 5848 integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==
5719 dependencies: 5849 dependencies:
5720 chokidar "^2.0.2"
5721 graceful-fs "^4.1.2" 5850 graceful-fs "^4.1.2"
5722 neo-async "^2.5.0" 5851 neo-async "^2.5.0"
5723 5852 optionalDependencies:
5724webpack-sources@^1.0.1: 5853 chokidar "^3.4.1"
5725 version "1.3.0" 5854 watchpack-chokidar2 "^2.0.0"
5726 resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" 5855
5727 integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== 5856webpack-cli@^3.3.12:
5857 version "3.3.12"
5858 resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a"
5859 integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==
5860 dependencies:
5861 chalk "^2.4.2"
5862 cross-spawn "^6.0.5"
5863 enhanced-resolve "^4.1.1"
5864 findup-sync "^3.0.0"
5865 global-modules "^2.0.0"
5866 import-local "^2.0.0"
5867 interpret "^1.4.0"
5868 loader-utils "^1.4.0"
5869 supports-color "^6.1.0"
5870 v8-compile-cache "^2.1.1"
5871 yargs "^13.3.2"
5872
5873webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
5874 version "1.4.3"
5875 resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
5876 integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
5728 dependencies: 5877 dependencies:
5729 source-list-map "^2.0.0" 5878 source-list-map "^2.0.0"
5730 source-map "~0.6.1" 5879 source-map "~0.6.1"
5731 5880
5732webpack@^3.10.0: 5881webpack@^4.44.2:
5733 version "3.12.0" 5882 version "4.44.2"
5734 resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.12.0.tgz#3f9e34360370602fcf639e97939db486f4ec0d74" 5883 resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72"
5735 integrity sha512-Sw7MdIIOv/nkzPzee4o0EdvCuPmxT98+vVpIvwtcwcF1Q4SDSNp92vwcKc4REe7NItH9f1S4ra9FuQ7yuYZ8bQ== 5884 integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==
5736 dependencies: 5885 dependencies:
5737 acorn "^5.0.0" 5886 "@webassemblyjs/ast" "1.9.0"
5738 acorn-dynamic-import "^2.0.0" 5887 "@webassemblyjs/helper-module-context" "1.9.0"
5739 ajv "^6.1.0" 5888 "@webassemblyjs/wasm-edit" "1.9.0"
5740 ajv-keywords "^3.1.0" 5889 "@webassemblyjs/wasm-parser" "1.9.0"
5741 async "^2.1.2" 5890 acorn "^6.4.1"
5742 enhanced-resolve "^3.4.0" 5891 ajv "^6.10.2"
5743 escope "^3.6.0" 5892 ajv-keywords "^3.4.1"
5744 interpret "^1.0.0" 5893 chrome-trace-event "^1.0.2"
5745 json-loader "^0.5.4" 5894 enhanced-resolve "^4.3.0"
5746 json5 "^0.5.1" 5895 eslint-scope "^4.0.3"
5747 loader-runner "^2.3.0" 5896 json-parse-better-errors "^1.0.2"
5748 loader-utils "^1.1.0" 5897 loader-runner "^2.4.0"
5749 memory-fs "~0.4.1" 5898 loader-utils "^1.2.3"
5750 mkdirp "~0.5.0" 5899 memory-fs "^0.4.1"
5751 node-libs-browser "^2.0.0" 5900 micromatch "^3.1.10"
5752 source-map "^0.5.3" 5901 mkdirp "^0.5.3"
5753 supports-color "^4.2.1" 5902 neo-async "^2.6.1"
5754 tapable "^0.2.7" 5903 node-libs-browser "^2.2.1"
5755 uglifyjs-webpack-plugin "^0.4.6" 5904 schema-utils "^1.0.0"
5756 watchpack "^1.4.0" 5905 tapable "^1.1.3"
5757 webpack-sources "^1.0.1" 5906 terser-webpack-plugin "^1.4.3"
5758 yargs "^8.0.2" 5907 watchpack "^1.7.4"
5759 5908 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 5909
5770which-module@^2.0.0: 5910which-module@^2.0.0:
5771 version "2.0.0" 5911 version "2.0.0"
5772 resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 5912 resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
5773 integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= 5913 integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
5774 5914
5775which@1, which@^1.2.9: 5915which@^1.2.14, which@^1.2.9, which@^1.3.1:
5776 version "1.3.1" 5916 version "1.3.1"
5777 resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 5917 resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
5778 integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== 5918 integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
5779 dependencies: 5919 dependencies:
5780 isexe "^2.0.0" 5920 isexe "^2.0.0"
5781 5921
5782wide-align@^1.1.0: 5922which@^2.0.1:
5783 version "1.1.3" 5923 version "2.0.2"
5784 resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" 5924 resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
5785 integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== 5925 integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
5786 dependencies: 5926 dependencies:
5787 string-width "^1.0.2 || 2" 5927 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 5928
5794wordwrap@0.0.2: 5929word-wrap@^1.2.3:
5795 version "0.0.2" 5930 version "1.2.3"
5796 resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" 5931 resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
5797 integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= 5932 integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
5798 5933
5799wordwrap@~1.0.0: 5934worker-farm@^1.7.0:
5800 version "1.0.0" 5935 version "1.7.0"
5801 resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" 5936 resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
5802 integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= 5937 integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==
5938 dependencies:
5939 errno "~0.1.7"
5803 5940
5804wrap-ansi@^2.0.0: 5941wrap-ansi@^5.1.0:
5805 version "2.1.0" 5942 version "5.1.0"
5806 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" 5943 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
5807 integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= 5944 integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
5808 dependencies: 5945 dependencies:
5809 string-width "^1.0.1" 5946 ansi-styles "^3.2.0"
5810 strip-ansi "^3.0.1" 5947 string-width "^3.0.0"
5948 strip-ansi "^5.0.0"
5811 5949
5812wrappy@1: 5950wrappy@1:
5813 version "1.0.2" 5951 version "1.0.2"
5814 resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 5952 resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
5815 integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 5953 integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
5816 5954
5817write@^0.2.1: 5955write-file-atomic@^3.0.3:
5818 version "0.2.1" 5956 version "3.0.3"
5819 resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" 5957 resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
5820 integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= 5958 integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
5959 dependencies:
5960 imurmurhash "^0.1.4"
5961 is-typedarray "^1.0.0"
5962 signal-exit "^3.0.2"
5963 typedarray-to-buffer "^3.1.5"
5964
5965write@1.0.3:
5966 version "1.0.3"
5967 resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
5968 integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
5821 dependencies: 5969 dependencies:
5822 mkdirp "^0.5.1" 5970 mkdirp "^0.5.1"
5823 5971
5824xtend@^4.0.0: 5972xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
5825 version "4.0.1" 5973 version "4.0.2"
5826 resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 5974 resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
5827 integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= 5975 integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
5828 5976
5829y18n@^3.2.1: 5977y18n@^4.0.0:
5830 version "3.2.1" 5978 version "4.0.0"
5831 resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" 5979 resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
5832 integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= 5980 integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
5833 5981
5834yallist@^2.1.2: 5982yallist@^3.0.2:
5835 version "2.1.2" 5983 version "3.1.1"
5836 resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 5984 resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
5837 integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= 5985 integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
5838 5986
5839yallist@^3.0.0, yallist@^3.0.2: 5987yallist@^4.0.0:
5840 version "3.0.3" 5988 version "4.0.0"
5841 resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" 5989 resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
5842 integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== 5990 integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
5843 5991
5844yargs-parser@^5.0.0: 5992yaml@^1.10.0:
5845 version "5.0.0" 5993 version "1.10.0"
5846 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" 5994 resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
5847 integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= 5995 integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
5996
5997yargs-parser@^13.1.2:
5998 version "13.1.2"
5999 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
6000 integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
5848 dependencies: 6001 dependencies:
5849 camelcase "^3.0.0" 6002 camelcase "^5.0.0"
6003 decamelize "^1.2.0"
5850 6004
5851yargs-parser@^7.0.0: 6005yargs-parser@^18.1.3:
5852 version "7.0.0" 6006 version "18.1.3"
5853 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" 6007 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
5854 integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k= 6008 integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
5855 dependencies: 6009 dependencies:
5856 camelcase "^4.1.0" 6010 camelcase "^5.0.0"
5857 6011 decamelize "^1.2.0"
5858yargs@^7.0.0: 6012
5859 version "7.1.0" 6013yargs@^13.3.2:
5860 resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" 6014 version "13.3.2"
5861 integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= 6015 resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
5862 dependencies: 6016 integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
5863 camelcase "^3.0.0" 6017 dependencies:
5864 cliui "^3.2.0" 6018 cliui "^5.0.0"
5865 decamelize "^1.1.1" 6019 find-up "^3.0.0"
5866 get-caller-file "^1.0.1" 6020 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" 6021 require-directory "^2.1.1"
5889 require-main-filename "^1.0.1" 6022 require-main-filename "^2.0.0"
5890 set-blocking "^2.0.0" 6023 set-blocking "^2.0.0"
5891 string-width "^2.0.0" 6024 string-width "^3.0.0"
5892 which-module "^2.0.0" 6025 which-module "^2.0.0"
5893 y18n "^3.2.1" 6026 y18n "^4.0.0"
5894 yargs-parser "^7.0.0" 6027 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"