Merge latest 0.12.2

This commit is contained in:
Knah Tsaeb 2023-05-24 11:35:15 +02:00
parent 984073a980
commit 23a5fc1eef
232 changed files with 27850 additions and 10113 deletions

View file

@ -7,31 +7,20 @@ RewriteEngine On
RewriteRule ^(.git|doxygen|vendor) - [F]
# Forward the "Authorization" HTTP header
# fixes JWT token not correctly forwarded on some Apache/FastCGI setups
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
# Alternative (if the 2 lines above don't work)
# SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
# REST API
# Slim URL Redirection
# Ionos Hosting needs RewriteBase /
# RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
<Limit GET POST PUT DELETE OPTIONS>
<IfModule version_module>
<IfVersion >= 2.4>
Require all granted
</IfVersion>
<IfVersion < 2.4>
Allow from all
Deny from none
</IfVersion>
</IfModule>
<IfModule !version_module>
Require all granted
</IfModule>
</Limit>
<LimitExcept GET POST PUT DELETE OPTIONS>
<LimitExcept GET POST PUT DELETE PATCH OPTIONS>
<IfModule version_module>
<IfVersion >= 2.4>
Require all denied

72
AUTHORS
View file

@ -1,47 +1,73 @@
769 ArthurHoaro <arthur@hoa.ro>
401 VirtualTam <virtualtam@flibidi.net>
216 nodiscc <nodiscc@gmail.com>
1206 ArthurHoaro <arthur@hoa.ro>
405 VirtualTam <virtualtam@flibidi.net>
384 nodiscc <nodiscc@gmail.com>
56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
23 dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
19 Keith Carangelo <mail@kcaran.com>
16 Luce Carević <lcarevic@access42.net>
15 Florian Eula <eula.florian@gmail.com>
13 Emilien Klein <emilien@klein.st>
13 Luce Carević <lcarevic@access42.net>
14 Emilien Klein <emilien@klein.st>
12 Nicolas Danelon <hi@nicolasmd.com.ar>
9 Lucas Cimon <lucas.cimon@gmail.com>
9 Willi Eggeling <thewilli@gmail.com>
8 Christophe HENRY <christophe.henry@sbgodin.fr>
6 Immánuel Fodor <immanuelfactor+github@gmail.com>
6 YFdyh000 <yfdyh000@gmail.com>
6 kalvn <kalvnthereal@gmail.com>
6 B. van Berkum <dev@dotmpe.com>
6 llune <llune@users.noreply.github.com>
5 Lucas Cimon <lucas.cimon@gmail.com>
5 Mark Schmitz <kramred@gmail.com>
5 kalvn <kalvnthereal@gmail.com>
5 Sébastien NOBILI <code@pipoprods.org>
4 Alexandre Alapetite <alexandre@alapetite.fr>
4 yude <yudesleepy@gmail.com>
4 David Sferruzza <david.sferruzza@gmail.com>
4 Immánuel Fodor <immanuelfactor+github@gmail.com>
3 Agurato <mail.vmonot@gmail.com>
3 Teromene <teromene@teromene.fr>
2 Alexandre G.-Raymond <alex@ndre.gr>
2 Chris Kuethe <chris.kuethe@gmail.com>
3 yudete <yu@yude.moe>
3 Agurato <mail.vmonot@gmail.com>
3 Olivier <bourreauolivier@gmail.com>
3 Christoph Stoettner <christoph.stoettner@stoeps.de>
2 Felix Bartels <felix@host-consultants.de>
2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
2 Luce Carević <lcarevic@access42.net>
2 Mathieu Chabanon <git@matchab.fr>
2 Miloš Jovanović <mjovanovic@gmail.com>
2 Neros <contact@neros.fr>
2 Alexandre G.-Raymond <alex@ndre.gr>
2 Qwerty <champlywood@free.fr>
2 Guillaume Virlet <github@virlet.org>
2 Sebastien Wains <sebw@users.noreply.github.com>
2 Stephen Muth <smuth4@gmail.com>
2 Timo Van Neerden <fire@lehollandaisvolant.net>
2 Alexander Railean <alexandr.railean@arculus.de>
2 Doug Breaux <25640850+dougbreaux@users.noreply.github.com>
2 flow.gunso <flow.gunso@gmail.com>
2 Chris Kuethe <chris.kuethe@gmail.com>
2 Ganesh Kandu <kanduganesh@gmail.com>
2 julienCXX <software@chmodplusx.eu>
2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
2 philipp-r <philipp-r@users.noreply.github.com>
2 pips <pips@e5150.fr>
2 prog-it <pash.vld@gmail.com>
2 trailjeep <trailjeep@gmail.com>
1 Adrien Oliva <adrien.oliva@yapbreak.fr>
1 leyrer <gitlab@leyrer.priv.at>
1 locness3 <37651007+locness3@users.noreply.github.com>
1 owen bell <66233223+xfnw@users.noreply.github.com>
1 philipp <philipp@philipp.PC.Ubuntu>
1 rfolo9li <50079896+rfolo9li@users.noreply.github.com>
1 sprak3000 <sprak3000+github@gmail.com>
1 yudejp <i@yude.jp>
1 Rajat Hans <rajathans9@gmail.com>
1 Adrien le Maire <adrien@alemaire.be>
1 Ajabep <ajabep@users.noreply.github.com>
1 Alexis J <alexis@effingo.be>
1 Angristan <angristan@users.noreply.github.com>
1 Bish Erbas <42714627+bisherbas@users.noreply.github.com>
1 BoboTiG <bobotig@gmail.com>
1 Brendan M. Sleight <bms.git@barwap.com>
1 Bronco <bronco@warriordudimanche.net>
1 Buster One <37770318+buster-one@users.noreply.github.com>
1 D Low <daniellowtw@gmail.com>
1 Daniel Jakots <vigdis@chown.me>
1 David Foucher <dev@tyjak.net>
1 Denis Renning <denis@devtty.de>
1 Dennis Verspuij <dennisverspuij@users.noreply.github.com>
1 Dimtion <zizou.xena@gmail.com>
1 Fanch <fanch-github@qth.fr>
@ -49,19 +75,31 @@
1 Florian Voigt <flvoigt@me.com>
1 Franck Kerbiriou <FranckKe@users.noreply.github.com>
1 Gary Marigliano <gmarigliano93@gmail.com>
1 Guillaume Virlet <github@virlet.org>
1 Gregory <gregory@nosheep.fr>
1 Hazhar Galeh <78073762+hazhargaleh@users.noreply.github.com>
1 Hg <dev@indigo.re>
1 Jens Kubieziel <github@kubieziel.de>
1 Jonathan Amiez <jonathan.amiez@gmail.com>
1 Jonathan Druart <jonathan.druart@gmail.com>
1 Julien Pivotto <roidelapluie@inuits.eu>
1 Kevin Canévet <kevin@streamroot.io>
1 Kevin Masson <kevin.masson@methodinthemadness.eu>
1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
1 Lionel Martin <renarddesmers@gmail.com>
1 Loïc Carr <zizou.xena@gmail.com>
1 Mark Gerarts <mark.gerarts@gmail.com>
1 Marsup <marsup@gmail.com>
1 Neros <contact@neros.fr>
1 Rajat Hans <rajathans9@gmail.com>
1 Nicolas Friedli <nicolas@theologique.ch>
1 Paul van den Burg <github@paulvandenburg.nl>
1 Adrien Oliva <adrien.oliva@yapbreak.fr>
1 Sbgodin <Sbgodin@users.noreply.github.com>
1 ToM <tom@leloop.org>
1 TsT <tst2005@gmail.com>
1 agentcobra <agentcobra@free.fr>
1 aguy <aguytech@users.noreply.github.com>
1 bschwede <gummibando@gmx.net>
1 dimtion <zizou.xena@gmail.com>
1 durcheinandr <jochen@durcheinandr.de>
1 heimpogo <hypertexthome@googlemail.com>
1 jalr <mail@jalr.de>
1 lapineige <lapineige@users.noreply.github.com>

View file

@ -4,44 +4,234 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
<<<<<<< HEAD
## [v0.10.4](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4) - 2019-04-16
## [v0.12.2](https://github.com/shaarli/Shaarli/releases/tag/v0.12.2) - 2023-03-18
### Fixed
- Fix thumbnails disabling if PHP GD is not installed
- Fix a warning if links sticky status isn't set
> Docker: use `ghcr.io/shaarli/shaarli` as Docker image instead of `shaarli/shaarli`.
> The `:master` Docker image has been removed, please use `:latest` instead.
> The `:stable` Docker image has been removed, please use `:release` instead.
## Added
- Bulk action: add or delete tag to multiple bookmarks
- New Core Plugin: ReadItLater
- Plugin system: allow plugins to provide custom routes
- Support search highlights when matching URL content
- Support for OR (~) and optional AND (+) operators for tag search
- Russian translation
- Chinese translation
- Export:
- Export: set a bookmark's LAST_MODIFIED attribute to its update timestamp
- Export: set a bookmark's PRIVATE attribute using an integer value
- Add an additional free disk space check before saving the datastore
- curl: support HTTP/2 response code header
- CI:
- Build and push Docker images through Github Actions
- push container images to github registry in addition to dockerhub
- Documentation:
- Add '206 not acceptable' to the Troubleshooting section
- Add mention to Shaarli Archiver
- doc: add note to adjust proxy timeouts or PHP max execution time
- doc: shaarli configuration: mention file:/// URIs
- add "formatter" key to example config.json.php
## Changed
- docker latest: replace dev in shaarli_version.php with the latest commit
- Daily RSS Cache: invalidate cache base on the date
- Update Japanese translations
- Update German translations
- Templates: Inject current template name
- format_date: include timezone in IntlDateFormatter object
- Handle pagination through BookmarkService
- autocapitalize off for username input
- More intuitive label for plugin checkboxes
- Simple and uniform localized website title
- Use rewrited version of Netscape Bookmark Parser
- tests/makefile: rewrite translate target to be compatible with busybox
- PubSubHub Plugin: make 1 external call per request
- Docker:
- newer alpine (for newer PHP) and apk upgrade
- Dockerfile.armhf: upgrade python2 -> python3
- Dockerfile: add php8-gettext package
- update s6 service definition to use php-fpm8
- install php8-ldap in Docker images
- CI:
- use Github Action instead of Travis CI
- use the yarnpkg command instead of yarn
- tools: github actions: fix PHP 8.0 tests
- github actions: add tests for PHP 8.2
- Documentation:
- apache: explicitely ste index.php as DirectoryIndex
- bookmarklet is now working on github.com
- LDAP login support, update php requirements list
- installation/tests: clarify build tools installation procedure
- doc: PHP extensions are also required for development
- doc: move OCI images hosting to ghcr.io
## Fixed
- Error handling if the datastore mutex is not working
- Synchronous metadata retrieval is failing in strict mode
- Improve metadata extraction
- Typo: 'Authentication' ->
- default_colors plugin: update CSS file on color change
- API: POST/PUT Link - properly parse tags string
- Error when using bulk shaare with a single URL
- Bulk Shaare:
- use unique HTML ID
- error with a single URL
- redirection with ending slash
- Bug when trying to access ATOM feed without bookmarks
- Documentation build
- pubsubhubbub hub link in RSS / Atom.
- Monthly views previous/next month links during month
- Resolve PHP 8.1 deprecation warnings
- Fix PHP 8 incompatibility with debug mode enabled
- Fixed Roboto-Regular and Roboto-Bold font declarations
- template/vintage: fix typo in visibility selection link
- Do not display deprecated warnings by default
- Fix a bug when using '/' as a tag separator
- Fix Logger exception: gracefully handle permission issue
- Documentation:
- plugins.md: fix link casing
## Removed
- Daily RSS: Remove relative description (today, yesterday)
- Documentation:
- remove the markdown plugin from the plugins list
- remove duplicate "general" key in example config.php.json
## [v0.12.1](https://github.com/shaarli/Shaarli/releases/tag/v0.12.1) - 2020-11-12
> nginx ([#1628](https://github.com/shaarli/Shaarli/pull/1628)) and Apache ([#1630](https://github.com/shaarli/Shaarli/pull/1630)) configurations have been reviewed. It is recommended that you
> update yours using [the documentation](https://shaarli.readthedocs.io/en/master/Server-configuration/).
> Users using official Docker image will receive updated configuration automatically.
## [v0.10.3](https://github.com/shaarli/Shaarli/releases/tag/v0.10.3) - 2019-02-23
### Added
- Add OpenGraph metadata tags on permalink page
- Add CORS headers to REST API reponses
- Add a button to toggle checkboxes of displayed links
- Add an icon to the link list when the Isso plugin is enabled
- Add noindex, nofollow to documentation pages
- Document usage of robots.txt
- Add a button to set links as sticky
- Bulk creation of bookmarks
- Server administration tool page (and install page requirements)
- Support any tag separator, not just whitespaces
- Share a private bookmark using a URL with a token
- Add a setting to retrieve bookmark metadata asynchronously (enabled by default)
- Highlight fulltext search results
- Weekly and monthly view/RSS feed for daily page
- MarkdownExtra formatter
- Default formatter: add a setting to disable auto-linkification
- Add mutex on datastore I/O operations to prevent data loss
- PHP 8.0 support
- REST API: allow override of creation and update dates
- Add strict types for bookmarks management
### Changed
- Update French translation
- Refactor the documentation homepage
- Bump netscape-bookmark-parser
- Update session_start condition
- Improve accessibility
- Cleanup and refactor lint tooling
- Improve regex and performances to extract HTML metadata (title, description, etc.)
- Support using Shaarli without URL rewriting (prefix URL with `/index.php/`)
- Improve the "Manage tags" tools page
- Use PSR-3 logger for login attempts
- Move utils classes to Shaarli\Helper namespace and folder
- Include php-simplexml in Docker image
- Raise 404 error instead of 500 if permalink access is denied
- Display error details even with dev.debug set to false
- Reviewed nginx configuration
- Reviewed Apache configuration
- Replace vimeo link in demo bookmarks due to IP ban on the demo instance
- Apply PSR-12 on code base, and add CI check using PHPCS
### Fixed
- Fix input size for dropdown search form
- Fix history for bulk link deletion
- Fix thumbnail requests
- Fix hashtag rendering when markdown escaping is enabled
- Fix AJAX tag deletion
- Fix lint errors and improve PSR-1 and PSR-2 compliance
- Compatiliby issue on login with PHP 7.1
- Japanese translations update
- Redirect to referrer after bookmark deletion
- Inject ROOT_PATH in plugin instead of regenerating it everywhere
- Wallabag plugin: minor improvements
- REST API postLink: change relative path to absolute path
- Webpack: fix vintage theme images include
- Docker-compose: fix SSL certificate + add parameter for Docker tag
### Removed
- Remove Firefox Share documentation
- `config.json.php` new lines in prefix/suffix to prevent issues with Windows PHP
## [v0.12.0](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-10-13
**Save you `data/` folder before updating!**
### Added
- Thumbnailer: add soundcloud.com to list of common media domains
- Markdown rendering is now integrated into Shaarli core
- Add autofocus on tag cloud filter input
- Japanese translations
- Japanese translation: add language to admin configuration page
- Support for PHP 8.0
- Support for local anchor URL (starting with `#`)
- LDAP authentication
- Encapsulated PageCacheManager
- Docs:
- add screenshots of all pages
- section about mkdocs
- Ulauncher extension
- CI: run against PHP 7.4
- Added $links_per_page variable to template and display on default
- Inject BookmarkServiceInterface in plugins data
- Add manual configuration for root URL
- Added PATCH to the allowed Apache request methods.
- REST API: compatibility with ionos Apache's headers
### Changed
- Introduce Bookmark object and Service layer
- Save bookmark as objects in the datastore
- Handle bookmark as objects across the whole codebase (except templates and plugins)
- Process all Shaarli page through Slim controller, with proper URL rewriting (see #1516)
- Docs: the entire documentation has been reviewed, updated and improved, thanks to @nodiscc!
- ATOM feed: use instance name as author name instead of URL
- Updated French translation
- Default colors plugin: generate CSS file during initialization
- Improve default bookmarks after install
- Upgrade all front end dependencies and webpack build
- Default theme: Make tag cloud/list views buttons more obvious
### Fixed
- Undefined index: thumbnail in daily page
- Undefined index: thumbnail on OpenGraph headers
- Undefined index: updated on linklist
- Make sure that bookmark sort is consistent, even with equal timestamps
- Code PHP version check as requirement bumped to PHP 7.1
- Thumbnail images lazy loading
- Markdown plugin: fix RSS feed direct link reverse
- Fix RSS permalink included in Markdown bloc
- Demo plugin: multiple typos
- Makefile target for releases
- Makefile target for html documentation
- Session cookie setting being set while session is active
- Deprecated use of implode
- Division by zero in tag cloud
- CI: deprecated linux distribution and sudo directive
- Docker build: gcc is no longer included in python alpine image
- Default template: display pin button in mobile view
- Pinned bookmarks are not longer displayed first in ATOM/RSS feeds
- Docs:
- Outdated Docker documentation for stable branch
- Outdated links
- Plugin description in meta files
- docker-compose.yml: pin traefik image to 1.7-alpine
### Removed
- Markdown plugin
- Docs:
- emojione & twemoji removed
- Makefile: remove static_analysis_summary from all: target
- doc/Makefile: remove references to composer update
## [v0.11.1](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) - 2019-08-03
Release to fix broken Docker build on the latest version.
### Fixed
- Fixed Docker build
- Fixed a few documentation broken links
- Fixed broken label in configuration page
### Added
- More accessibility improvements
||||||| merged common ancestors
=======
## [v0.11.0](https://github.com/shaarli/Shaarli/releases/tag/v0.11.0) - 2019-07-27
**Shaarli no longer officially support PHP 5.6 and PHP 7.0 as they've reached end of life.**
@ -122,7 +312,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Removed
- Remove Firefox Share documentation
>>>>>>> v0.11.0
## [v0.10.2](https://github.com/shaarli/Shaarli/releases/tag/v0.10.2) - 2018-08-11
### Fixed
@ -366,7 +555,7 @@ configuration to enable URL rewriting, see:
- `/api/v1/info`: get general information on the Shaarli instance
- `/api/v1/links`: get a list of shaared links
- `/api/v1/history`: get a list of latest actions
Theming:
- Theming:
- Introduce a new theme
- Allow selecting themes/templates from the configuration page
- New/Edit link form can be submitted using CTRL+Enter in the textarea
@ -425,22 +614,6 @@ Theming:
### Security
- Markdown plugin: escape HTML entities by default
## [v0.8.4](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) - 2017-03-04
### Security
- Markdown plugin: escape HTML entities by default
## [v0.8.3](https://github.com/shaarli/Shaarli/releases/tag/v0.8.3) - 2017-01-20
### Fixed
- PHP 7.1 compatibility: add ConfigManager parameter to anti-bruteforce function call in login template.
## [v0.8.2](https://github.com/shaarli/Shaarli/releases/tag/v0.8.2) - 2016-12-15
### Fixed
- Editing a link created before the new ID system would change its permalink.
## [v0.8.7](https://github.com/shaarli/Shaarli/releases/tag/v0.8.7) - 2018-06-20
### Changed

View file

@ -6,18 +6,13 @@ _Do you want to share the links you discover?_
_Shaarli is a minimalist link sharing service that you can install on your own server._
_It is designed to be personal (single-user), fast and handy._
[![](https://img.shields.io/badge/stable-v0.9.7-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.7)
[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
&bull;
[![](https://img.shields.io/badge/latest-v0.10.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4)
[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
&bull;
[![](https://img.shields.io/badge/master-v0.11.x-blue.svg)](https://github.com/shaarli/Shaarli)
[![](https://img.shields.io/travis/shaarli/Shaarli.svg?label=master)](https://travis-ci.org/shaarli/Shaarli)
[![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1)
[![](https://img.shields.io/badge/latest-v0.12.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.1)
[![](https://img.shields.io/badge/master-v0.12.x-blue.svg)](https://github.com/shaarli/Shaarli)
[![](https://github.com/shaarli/Shaarli/actions/workflows/ci.yml/badge.svg)](https://github.com/shaarli/Shaarli/actions)
[![Join the chat at https://gitter.im/shaarli/Shaarli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/shaarli/Shaarli)
[![Bountysource](https://www.bountysource.com/badge/team?team_id=19583&style=bounties_received)](https://www.bountysource.com/teams/shaarli/issues)
[![Docker repository](https://img.shields.io/docker/pulls/shaarli/shaarli.svg)](https://hub.docker.com/r/shaarli/shaarli/)
[![Docker repository](https://img.shields.io/docker/pulls/shaarli/shaarli.svg)](https://github.com/shaarli/Shaarli/pkgs/container/shaarli)
## Quickstart

View file

@ -1,8 +1,11 @@
<?php
namespace Shaarli;
use DateTime;
use Exception;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Helper\FileUtils;
/**
* Class History
@ -20,7 +23,7 @@ use Exception;
* - UPDATED: link updated
* - DELETED: link deleted
* - SETTINGS: the settings have been updated through the UI.
* - IMPORT: bulk links import
* - IMPORT: bulk bookmarks import
*
* Note: new events are put at the beginning of the file and history array.
*/
@ -29,27 +32,27 @@ class History
/**
* @var string Action key: a new link has been created.
*/
const CREATED = 'CREATED';
public const CREATED = 'CREATED';
/**
* @var string Action key: a link has been updated.
*/
const UPDATED = 'UPDATED';
public const UPDATED = 'UPDATED';
/**
* @var string Action key: a link has been deleted.
*/
const DELETED = 'DELETED';
public const DELETED = 'DELETED';
/**
* @var string Action key: settings have been updated.
*/
const SETTINGS = 'SETTINGS';
public const SETTINGS = 'SETTINGS';
/**
* @var string Action key: a bulk import has been processed.
*/
const IMPORT = 'IMPORT';
public const IMPORT = 'IMPORT';
/**
* @var string History file path.
@ -96,31 +99,31 @@ class History
/**
* Add Event: new link.
*
* @param array $link Link data.
* @param Bookmark $link Link data.
*/
public function addLink($link)
{
$this->addEvent(self::CREATED, $link['id']);
$this->addEvent(self::CREATED, $link->getId());
}
/**
* Add Event: update existing link.
*
* @param array $link Link data.
* @param Bookmark $link Link data.
*/
public function updateLink($link)
{
$this->addEvent(self::UPDATED, $link['id']);
$this->addEvent(self::UPDATED, $link->getId());
}
/**
* Add Event: delete existing link.
*
* @param array $link Link data.
* @param Bookmark $link Link data.
*/
public function deleteLink($link)
{
$this->addEvent(self::DELETED, $link['id']);
$this->addEvent(self::DELETED, $link->getId());
}
/**
@ -134,7 +137,7 @@ class History
/**
* Add Event: bulk import.
*
* Note: we don't store links add/update one by one since it can have a huge impact on performances.
* Note: we don't store bookmarks add/update one by one since it can have a huge impact on performances.
*/
public function importLinks()
{

View file

@ -41,7 +41,7 @@ class Languages
/**
* Core translations domain
*/
const DEFAULT_DOMAIN = 'shaarli';
public const DEFAULT_DOMAIN = 'shaarli';
/**
* @var TranslatorInterface
@ -76,7 +76,8 @@ class Languages
$this->language = $confLanguage;
}
if (! extension_loaded('gettext')
if (
! extension_loaded('gettext')
|| in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
) {
$this->initPhpTranslator();
@ -98,7 +99,7 @@ class Languages
$this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
// Default extension translation from the current theme
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $this->conf->get('theme') .'/language';
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $this->conf->get('theme') . '/language';
if (is_dir($themeTransFolder)) {
$this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false);
}
@ -121,7 +122,9 @@ class Languages
$translations = new Translations();
// Core translations
try {
$translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
$translations = $translations->addFromPoFile(
'inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po'
);
$translations->setDomain('shaarli');
$this->translator->loadTranslations($translations);
} catch (\InvalidArgumentException $e) {
@ -129,11 +132,11 @@ class Languages
// Default extension translation from the current theme
$theme = $this->conf->get('theme');
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $theme .'/language';
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $theme . '/language';
if (is_dir($themeTransFolder)) {
try {
$translations = Translations::fromPoFile(
$themeTransFolder .'/'. $this->language .'/LC_MESSAGES/'. $theme .'.po'
$themeTransFolder . '/' . $this->language . '/LC_MESSAGES/' . $theme . '.po'
);
$translations->setDomain($theme);
$this->translator->loadTranslations($translations);
@ -149,7 +152,7 @@ class Languages
try {
$extension = Translations::fromPoFile(
$translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po'
$translationPath . $this->language . '/LC_MESSAGES/' . $domain . '.po'
);
$extension->setDomain($domain);
$this->translator->loadTranslations($extension);
@ -179,9 +182,12 @@ class Languages
{
return [
'auto' => t('Automatic'),
'de' => t('German'),
'en' => t('English'),
'fr' => t('French'),
'de' => t('German'),
'jp' => t('Japanese'),
'ru' => t('Russian'),
'zh_CN' => t('Chinese (Simplified)'),
];
}
}

View file

@ -4,7 +4,6 @@ namespace Shaarli;
use Shaarli\Config\ConfigManager;
use WebThumbnailer\Application\ConfigManager as WTConfigManager;
use WebThumbnailer\Exception\WebThumbnailerException;
use WebThumbnailer\WebThumbnailer;
/**
@ -14,7 +13,7 @@ use WebThumbnailer\WebThumbnailer;
*/
class Thumbnailer
{
const COMMON_MEDIA_DOMAINS = [
protected const COMMON_MEDIA_DOMAINS = [
'imgur.com',
'flickr.com',
'youtube.com',
@ -27,13 +26,14 @@ class Thumbnailer
'instagram.com',
'pinterest.com',
'pinterest.fr',
'soundcloud.com',
'tumblr.com',
'deviantart.com',
];
const MODE_ALL = 'all';
const MODE_COMMON = 'common';
const MODE_NONE = 'none';
public const MODE_ALL = 'all';
public const MODE_COMMON = 'common';
public const MODE_NONE = 'none';
/**
* @var WebThumbnailer instance.
@ -60,7 +60,7 @@ class Thumbnailer
// TODO: create a proper error handling system able to catch exceptions...
die(t(
'php-gd extension must be loaded to use thumbnails. '
.'Thumbnails are now disabled. Please reload the page.'
. 'Thumbnails are now disabled. Please reload the page.'
));
}
@ -81,7 +81,8 @@ class Thumbnailer
*/
public function get($url)
{
if ($this->conf->get('thumbnails.mode') === self::MODE_COMMON
if (
$this->conf->get('thumbnails.mode') === self::MODE_COMMON
&& ! $this->isCommonMediaOrImage($url)
) {
return false;
@ -89,7 +90,7 @@ class Thumbnailer
try {
return $this->wt->thumbnail($url);
} catch (WebThumbnailerException $e) {
} catch (\Throwable $e) {
// Exceptions are only thrown in debug mode.
error_log(get_class($e) . ': ' . $e->getMessage());
}

View file

@ -1,4 +1,5 @@
<?php
/**
* Generates a list of available timezone continents and cities.
*
@ -43,7 +44,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
// Try to split the provided timezone
$spos = strpos($preselectedTimezone, '/');
$pcontinent = substr($preselectedTimezone, 0, $spos);
$pcity = substr($preselectedTimezone, $spos+1);
$pcity = substr($preselectedTimezone, $spos + 1);
}
$continents = [];
@ -60,7 +61,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
}
$continent = substr($tz, 0, $spos);
$city = substr($tz, $spos+1);
$city = substr($tz, $spos + 1);
$cities[] = ['continent' => $continent, 'city' => $city];
$continents[$continent] = true;
}
@ -85,7 +86,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
function isTimeZoneValid($continent, $city)
{
return in_array(
$continent.'/'.$city,
$continent . '/' . $city,
timezone_identifiers_list()
);
}

View file

@ -1,24 +1,27 @@
<?php
/**
* Shaarli utilities
*/
/**
* Logs a message to a text file
* Format log using provided data.
*
* The log format is compatible with fail2ban.
* @param string $message the message to log
* @param string|null $clientIp the client's remote IPv4/IPv6 address
*
* @param string $logFile where to write the logs
* @param string $clientIp the client's remote IPv4/IPv6 address
* @param string $message the message to log
* @return string Formatted message to log
*/
function logm($logFile, $clientIp, $message)
function format_log(string $message, string $clientIp = null): string
{
file_put_contents(
$logFile,
date('Y/m/d H:i:s').' - '.$clientIp.' - '.strval($message).PHP_EOL,
FILE_APPEND
);
$out = $message;
if (!empty($clientIp)) {
// Note: we keep the first dash to avoid breaking fail2ban configs
$out = '- ' . $clientIp . ' - ' . $out;
}
return $out;
}
/**
@ -58,6 +61,7 @@ function smallHash($text)
*/
function startsWith($haystack, $needle, $case = true)
{
$needle = $needle ?? '';
if ($case) {
return (strcmp(substr($haystack, 0, strlen($needle)), $needle) === 0);
}
@ -87,18 +91,22 @@ function endsWith($haystack, $needle, $case = true)
*
* @param mixed $input Data to escape: a single string or an array of strings.
*
* @return string escaped.
* @return string|array escaped.
*/
function escape($input)
{
if (is_bool($input)) {
if (null === $input) {
return null;
}
if (is_bool($input) || is_int($input) || is_float($input) || $input instanceof DateTimeInterface) {
return $input;
}
if (is_array($input)) {
$out = array();
$out = [];
foreach ($input as $key => $value) {
$out[$key] = escape($value);
$out[escape($key)] = escape($value);
}
return $out;
}
@ -157,12 +165,12 @@ function checkDateFormat($format, $string)
*
* @return string $referer - final referer.
*/
function generateLocation($referer, $host, $loopTerms = array())
function generateLocation($referer, $host, $loopTerms = [])
{
$finalReferer = '?';
$finalReferer = './?';
// No referer if it contains any value in $loopCriteria.
foreach ($loopTerms as $value) {
foreach (array_filter($loopTerms) as $value) {
if (strpos($referer, $value) !== false) {
return $finalReferer;
}
@ -173,7 +181,7 @@ function generateLocation($referer, $host, $loopTerms = array())
$host = substr($host, 0, $pos);
}
$refererHost = parse_url($referer, PHP_URL_HOST);
$refererHost = parse_url($referer, PHP_URL_HOST) ?? '';
if (!empty($referer) && (strpos($refererHost, $host) !== false || startsWith('?', $refererHost))) {
$finalReferer = $referer;
}
@ -190,7 +198,7 @@ function generateLocation($referer, $host, $loopTerms = array())
function autoLocale($headerLocale)
{
// Default if browser does not send HTTP_ACCEPT_LANGUAGE
$locales = array('en_US', 'en_US.utf8', 'en_US.UTF-8');
$locales = ['en_US.UTF-8', 'en_US.utf8', 'en_US'];
if (! empty($headerLocale)) {
if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) {
$attempts = [];
@ -285,7 +293,7 @@ function generate_api_secret($username, $salt)
*/
function normalize_spaces($string)
{
return preg_replace('/\s{2,}/', ' ', trim($string));
return preg_replace('/\s{2,}/', ' ', trim($string ?? ''));
}
/**
@ -294,32 +302,52 @@ function normalize_spaces($string)
* Requires php-intl to display international datetimes,
* otherwise default format '%c' will be returned.
*
* @param DateTime $date to format.
* @param bool $time Displays time if true.
* @param bool $intl Use international format if true.
* @param DateTimeInterface $date to format.
* @param bool $time Displays time if true.
* @param bool $intl Use international format if true.
*
* @return bool|string Formatted date, or false if the input is invalid.
*/
function format_date($date, $time = true, $intl = true)
{
if (! $date instanceof DateTime) {
if (! $date instanceof DateTimeInterface) {
return false;
}
if (! $intl || ! class_exists('IntlDateFormatter')) {
$format = $time ? '%c' : '%x';
return strftime($format, $date->getTimestamp());
$format = 'F j, Y';
if ($time) {
$format .= ' h:i:s A \G\M\TP';
}
return $date->format($format);
}
$formatter = new IntlDateFormatter(
setlocale(LC_TIME, 0),
IntlDateFormatter::LONG,
$time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE
);
$formatter->setTimeZone($date->getTimezone());
return $formatter->format($date);
}
/**
* Format the date month according to the locale.
*
* @param DateTimeInterface $date to format.
*
* @return bool|string Formatted date, or false if the input is invalid.
*/
function format_month(DateTimeInterface $date)
{
if (! $date instanceof DateTimeInterface) {
return false;
}
return strftime('%B', $date->getTimestamp());
}
/**
* Check if the input is an integer, no matter its real type.
*
@ -353,13 +381,15 @@ function return_bytes($val)
return $val;
}
$val = trim($val);
$last = strtolower($val[strlen($val)-1]);
$last = strtolower($val[strlen($val) - 1]);
$val = intval(substr($val, 0, -1));
switch ($last) {
case 'g':
$val *= 1024;
// do no break in order 1024^2 for each unit
case 'm':
$val *= 1024;
// do no break in order 1024^2 for each unit
case 'k':
$val *= 1024;
}
@ -448,14 +478,28 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
* Wrapper function for translation which match the API
* of gettext()/_() and ngettext().
*
* @param string $text Text to translate.
* @param string $nText The plural message ID.
* @param int $nb The number of items for plural forms.
* @param string $domain The domain where the translation is stored (default: shaarli).
* @param string $text Text to translate.
* @param string $nText The plural message ID.
* @param int $nb The number of items for plural forms.
* @param string $domain The domain where the translation is stored (default: shaarli).
* @param array $variables Associative array of variables to replace in translated text.
* @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables.
*
* @return string Text translated.
*/
function t($text, $nText = '', $nb = 1, $domain = 'shaarli')
function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false)
{
return dn__($domain, $text, $nText, $nb);
$postFunction = $fixCase ? 'ucfirst' : function ($input) {
return $input;
};
return $postFunction(dn__($domain, $text, $nText, $nb, $variables));
}
/**
* Converts an exception into a printable stack trace string.
*/
function exception2text(Throwable $e): string
{
return $e->getMessage() . PHP_EOL . $e->getFile() . $e->getLine() . PHP_EOL . $e->getTraceAsString();
}

View file

@ -1,8 +1,11 @@
<?php
namespace Shaarli\Api;
use malkusch\lock\mutex\FlockMutex;
use Shaarli\Api\Exceptions\ApiAuthorizationException;
use Shaarli\Api\Exceptions\ApiException;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager;
use Slim\Container;
use Slim\Http\Request;
@ -70,7 +73,14 @@ class ApiMiddleware
$response = $e->getApiResponse();
}
return $response;
return $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader(
'Access-Control-Allow-Headers',
'X-Requested-With, Content-Type, Accept, Origin, Authorization'
)
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
;
}
/**
@ -99,7 +109,10 @@ class ApiMiddleware
*/
protected function checkToken($request)
{
if (! $request->hasHeader('Authorization')) {
if (
!$request->hasHeader('Authorization')
&& !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])
) {
throw new ApiAuthorizationException('JWT token not provided');
}
@ -107,7 +120,11 @@ class ApiMiddleware
throw new ApiAuthorizationException('Token secret must be set in Shaarli\'s administration');
}
$authorization = $request->getHeaderLine('Authorization');
if (isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])) {
$authorization = $this->container->environment['REDIRECT_HTTP_AUTHORIZATION'];
} else {
$authorization = $request->getHeaderLine('Authorization');
}
if (! preg_match('/^Bearer (.*)/i', $authorization, $matches)) {
throw new ApiAuthorizationException('Invalid JWT header');
@ -117,7 +134,7 @@ class ApiMiddleware
}
/**
* Instantiate a new LinkDB including private links,
* Instantiate a new LinkDB including private bookmarks,
* and load in the Slim container.
*
* FIXME! LinkDB could use a refactoring to avoid this trick.
@ -126,10 +143,12 @@ class ApiMiddleware
*/
protected function setLinkDb($conf)
{
$linkDb = new \Shaarli\Bookmark\LinkDB(
$conf->get('resource.datastore'),
true,
$conf->get('privacy.hide_public_links')
$linkDb = new BookmarkFileService(
$conf,
$this->container->get('pluginManager'),
$this->container->get('history'),
new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
true
);
$this->container['db'] = $linkDb;
}

View file

@ -1,7 +1,9 @@
<?php
namespace Shaarli\Api;
use Shaarli\Api\Exceptions\ApiAuthorizationException;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Http\Base64Url;
/**
@ -15,6 +17,8 @@ class ApiUtils
* @param string $token JWT token extracted from the headers.
* @param string $secret API secret set in the settings.
*
* @return bool true on success
*
* @throws ApiAuthorizationException the token is not valid.
*/
public static function validateJwtToken($token, $secret)
@ -24,7 +28,7 @@ class ApiUtils
throw new ApiAuthorizationException('Malformed JWT token');
}
$genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] .'.'. $parts[1], $secret, true));
$genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] . '.' . $parts[1], $secret, true));
if ($parts[2] != $genSign) {
throw new ApiAuthorizationException('Invalid JWT signature');
}
@ -39,39 +43,42 @@ class ApiUtils
throw new ApiAuthorizationException('Invalid JWT payload');
}
if (empty($payload->iat)
if (
empty($payload->iat)
|| $payload->iat > time()
|| time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION
) {
throw new ApiAuthorizationException('Invalid JWT issued time');
}
return true;
}
/**
* Format a Link for the REST API.
*
* @param array $link Link data read from the datastore.
* @param string $indexUrl Shaarli's index URL (used for relative URL).
* @param Bookmark $bookmark Bookmark data read from the datastore.
* @param string $indexUrl Shaarli's index URL (used for relative URL).
*
* @return array Link data formatted for the REST API.
*/
public static function formatLink($link, $indexUrl)
public static function formatLink($bookmark, $indexUrl)
{
$out['id'] = $link['id'];
$out['id'] = $bookmark->getId();
// Not an internal link
if (! is_note($link['url'])) {
$out['url'] = $link['url'];
if (! $bookmark->isNote()) {
$out['url'] = $bookmark->getUrl();
} else {
$out['url'] = $indexUrl . $link['url'];
$out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/');
}
$out['shorturl'] = $link['shorturl'];
$out['title'] = $link['title'];
$out['description'] = $link['description'];
$out['tags'] = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
$out['private'] = $link['private'] == true;
$out['created'] = $link['created']->format(\DateTime::ATOM);
if (! empty($link['updated'])) {
$out['updated'] = $link['updated']->format(\DateTime::ATOM);
$out['shorturl'] = $bookmark->getShortUrl();
$out['title'] = $bookmark->getTitle();
$out['description'] = $bookmark->getDescription();
$out['tags'] = $bookmark->getTags();
$out['private'] = $bookmark->isPrivate();
$out['created'] = $bookmark->getCreated()->format(\DateTime::ATOM);
if (! empty($bookmark->getUpdated())) {
$out['updated'] = $bookmark->getUpdated()->format(\DateTime::ATOM);
} else {
$out['updated'] = '';
}
@ -79,58 +86,72 @@ class ApiUtils
}
/**
* Convert a link given through a request, to a valid link for LinkDB.
* Convert a link given through a request, to a valid Bookmark for the datastore.
*
* If no URL is provided, it will generate a local note URL.
* If no title is provided, it will use the URL as title.
*
* @param array $input Request Link.
* @param bool $defaultPrivate Request Link.
* @param array|null $input Request Link.
* @param bool $defaultPrivate Setting defined if a bookmark is private by default.
* @param string $tagsSeparator Tags separator loaded from the config file.
*
* @return array Formatted link.
* @return Bookmark instance.
*/
public static function buildLinkFromRequest($input, $defaultPrivate)
{
$input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : '';
public static function buildBookmarkFromRequest(
?array $input,
bool $defaultPrivate,
string $tagsSeparator
): Bookmark {
$bookmark = new Bookmark();
$url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
if (isset($input['private'])) {
$private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
} else {
$private = $defaultPrivate;
}
$link = [
'title' => ! empty($input['title']) ? $input['title'] : $input['url'],
'url' => $input['url'],
'description' => ! empty($input['description']) ? $input['description'] : '',
'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '',
'private' => $private,
'created' => new \DateTime(),
];
return $link;
$bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
$bookmark->setUrl($url);
$bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
// Be permissive with provided tags format
if (is_string($input['tags'] ?? null)) {
$input['tags'] = tags_str2array($input['tags'], $tagsSeparator);
}
if (is_array($input['tags'] ?? null) && count($input['tags']) === 1 && is_string($input['tags'][0])) {
$input['tags'] = tags_str2array($input['tags'][0], $tagsSeparator);
}
$bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
$bookmark->setPrivate($private);
$created = \DateTime::createFromFormat(\DateTime::ATOM, $input['created'] ?? '');
if ($created instanceof \DateTimeInterface) {
$bookmark->setCreated($created);
}
$updated = \DateTime::createFromFormat(\DateTime::ATOM, $input['updated'] ?? '');
if ($updated instanceof \DateTimeInterface) {
$bookmark->setUpdated($updated);
}
return $bookmark;
}
/**
* Update link fields using an updated link object.
*
* @param array $oldLink data
* @param array $newLink data
* @param Bookmark $oldLink data
* @param Bookmark $newLink data
*
* @return array $oldLink updated with $newLink values
* @return Bookmark $oldLink updated with $newLink values
*/
public static function updateLink($oldLink, $newLink)
{
foreach (['title', 'url', 'description', 'tags', 'private'] as $field) {
$oldLink[$field] = $newLink[$field];
}
$oldLink['updated'] = new \DateTime();
if (empty($oldLink['url'])) {
$oldLink['url'] = '?' . $oldLink['shorturl'];
}
if (empty($oldLink['title'])) {
$oldLink['title'] = $oldLink['url'];
}
$oldLink->setTitle($newLink->getTitle());
$oldLink->setUrl($newLink->getUrl());
$oldLink->setDescription($newLink->getDescription());
$oldLink->setTags($newLink->getTags());
$oldLink->setPrivate($newLink->isPrivate());
return $oldLink;
}
@ -139,7 +160,7 @@ class ApiUtils
* Format a Tag for the REST API.
*
* @param string $tag Tag name
* @param int $occurrences Number of links using this tag
* @param int $occurrences Number of bookmarks using this tag
*
* @return array Link data formatted for the REST API.
*/

View file

@ -2,8 +2,9 @@
namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\LinkDB;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Slim\Container;
/**
@ -26,12 +27,12 @@ abstract class ApiController
protected $conf;
/**
* @var LinkDB
* @var BookmarkServiceInterface
*/
protected $linkDb;
protected $bookmarkService;
/**
* @var HistoryController
* @var History
*/
protected $history;
@ -51,7 +52,7 @@ abstract class ApiController
{
$this->ci = $ci;
$this->conf = $ci->get('conf');
$this->linkDb = $ci->get('db');
$this->bookmarkService = $ci->get('db');
$this->history = $ci->get('history');
if ($this->conf->get('dev.debug', false)) {
$this->jsonStyle = JSON_PRETTY_PRINT;

View file

@ -1,6 +1,5 @@
<?php
namespace Shaarli\Api\Controllers;
use Shaarli\Api\Exceptions\ApiBadParametersException;
@ -31,7 +30,7 @@ class HistoryController extends ApiController
$history = $this->history->getHistory();
// Return history operations from the {offset}th, starting from {since}.
$since = \DateTime::createFromFormat(\DateTime::ATOM, $request->getParam('since'));
$since = \DateTime::createFromFormat(\DateTime::ATOM, $request->getParam('since', ''));
$offset = $request->getParam('offset');
if (empty($offset)) {
$offset = 0;
@ -41,7 +40,7 @@ class HistoryController extends ApiController
throw new ApiBadParametersException('Invalid offset');
}
// limit parameter is either a number of links or 'all' for everything.
// limit parameter is either a number of bookmarks or 'all' for everything.
$limit = $request->getParam('limit');
if (empty($limit)) {
$limit = count($history);

View file

@ -2,6 +2,7 @@
namespace Shaarli\Api\Controllers;
use Shaarli\Bookmark\BookmarkFilter;
use Slim\Http\Request;
use Slim\Http\Response;
@ -26,15 +27,15 @@ class Info extends ApiController
public function getInfo($request, $response)
{
$info = [
'global_counter' => count($this->linkDb),
'private_counter' => count_private($this->linkDb),
'settings' => array(
'global_counter' => $this->bookmarkService->count(),
'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE),
'settings' => [
'title' => $this->conf->get('general.title', 'Shaarli'),
'header_link' => $this->conf->get('general.header_link', '?'),