Merge latest 0.12.2
This commit is contained in:
parent
984073a980
commit
23a5fc1eef
232 changed files with 27850 additions and 10113 deletions
25
.htaccess
25
.htaccess
|
@ -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
72
AUTHORS
|
@ -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>
|
||||
|
|
265
CHANGELOG.md
265
CHANGELOG.md
|
@ -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
|
||||
|
|
15
README.md
15
README.md
|
@ -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://github.com/shaarli/Shaarli/releases/tag/v0.9.7)
|
||||
[](https://travis-ci.org/shaarli/Shaarli)
|
||||
•
|
||||
[](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4)
|
||||
[](https://travis-ci.org/shaarli/Shaarli)
|
||||
•
|
||||
[](https://github.com/shaarli/Shaarli)
|
||||
[](https://travis-ci.org/shaarli/Shaarli)
|
||||
|
||||
[](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1)
|
||||
[](https://github.com/shaarli/Shaarli/releases/tag/v0.12.1)
|
||||
[](https://github.com/shaarli/Shaarli)
|
||||
[](https://github.com/shaarli/Shaarli/actions)
|
||||
[](https://gitter.im/shaarli/Shaarli)
|
||||
[](https://www.bountysource.com/teams/shaarli/issues)
|
||||
[](https://hub.docker.com/r/shaarli/shaarli/)
|
||||
[](https://github.com/shaarli/Shaarli/pkgs/container/shaarli)
|
||||
|
||||
## Quickstart
|
||||
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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)'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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', '?'),
|
||||