Release v0.10.2
-----BEGIN PGP SIGNATURE----- iQIzBAABCAAdFiEEWe5LuNiFNDXAgI8BOzJIyqqwgW4FAltu2K0ACgkQOzJIyqqw gW4fpg/+MfXOj0d4sR3QMgafKHAVtiVmrOydVwqFOjVe+BOjpxHliDtOqo++cquF umZ3Ln9D8R3Wocw5cdLOn0/WbS+xMqyLmJWkGb1sn2NS8NWINXwCw6A8QuYF789p NmfmhYnXCW8OoX3TWLT1RR/0UL0V2ZJsMYTWfngxM4EVSPkaZc8C7Sjqs4hL/m4w uPcHgsCziZjxtGmdFUKLEEoFwxWKIvZTnYNTVegD6uHGb7jNZGXz1kizIpsXHC3p LffOpx1bamTbPoNhM0PyTTRAvNF3qBWsWY58Haldv9R60KsxJ7Fxc9PXgt02vUfw dGLMuMEd98iArAlovqQCy4/f+r1JhqJUsfj2IDJM5QSTiYWJL6zShHyHoWWifU07 4eZCOZce3kskRd8kl/0TRqdFKBB1RxIDtEZRBbmIhnkUt8E2fZG+7XPvZiIeTZSc 9/8y0KAxBnOuWtLny/NE6kS6yNUSlYooTU6kkDZ4lvsJFpHlQKwwuoFDcsD6oY0k yZ7lCAJht645pEQAF9b7WaB+qiE55suWFUcXM/uHqRdvl+DhEJE5C/BD7orW2mi9 CVfjmqEz5UFkalG7cZpb/NB1Rtcm1YT1NlY0h1YMRtT6ZILkgUNZLWb6tuZ2e0CS sPvVzSNzyJmw5vRC6MtwAJzRRkqa1cFJ58vnQB1n8N65n/mAFNA= =+fbH -----END PGP SIGNATURE----- Merge tag 'v0.10.2' into myShaarli_commu Release v0.10.2
12
.dev/.eslintrc.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
module.exports = {
|
||||
"extends": "airbnb-base",
|
||||
"env": {
|
||||
"browser": true,
|
||||
},
|
||||
"rules": {
|
||||
"no-param-reassign": 0, // manipulate DOM style properties
|
||||
"no-restricted-globals": 0, // currently Shaarli uses alert/confirm, could be be improved later
|
||||
"no-alert": 0, // currently Shaarli uses alert/confirm, could be be improved later
|
||||
"no-cond-assign": [2, "except-parens"], // assignment in while loops is readable and avoid assignment duplication
|
||||
}
|
||||
};
|
15
.dev/.sasslintrc
Normal file
|
@ -0,0 +1,15 @@
|
|||
options:
|
||||
max-warnings: 0
|
||||
rules:
|
||||
property-sort-order:
|
||||
- 1
|
||||
-
|
||||
order: 'concentric'
|
||||
no-important:
|
||||
- 0
|
||||
no-vendor-prefixes:
|
||||
- 0 # this will be fixed with v2: see https://github.com/sasstools/sass-lint/pull/1137
|
||||
nesting-depth:
|
||||
- 1
|
||||
-
|
||||
max-depth: 4
|
|
@ -4,6 +4,9 @@
|
|||
.github
|
||||
tests
|
||||
|
||||
# Docker Compose resources
|
||||
docker-compose.yml
|
||||
|
||||
# Shaarli runtime resources
|
||||
cache/*
|
||||
data/*
|
||||
|
@ -35,10 +38,17 @@ phpmd.html
|
|||
# User plugin configuration
|
||||
plugins/*/config.php
|
||||
|
||||
# HTML documentation
|
||||
doc/html/
|
||||
|
||||
# 3rd party themes
|
||||
tpl/*
|
||||
!tpl/default
|
||||
!tpl/vintage
|
||||
|
||||
# Front end
|
||||
node_modules
|
||||
tpl/default/js
|
||||
tpl/default/css
|
||||
tpl/default/fonts
|
||||
tpl/default/img
|
||||
tpl/vintage/js
|
||||
tpl/vintage/css
|
||||
tpl/vintage/img
|
||||
|
|
|
@ -10,7 +10,7 @@ trim_trailing_whitespace = true
|
|||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{htaccess,html,xml}]
|
||||
[*.{htaccess,html,scss,js,json,xml,yml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.php]
|
||||
|
|
33
.gitattributes
vendored
|
@ -25,18 +25,21 @@ Dockerfile text
|
|||
*.mo binary
|
||||
|
||||
# Exclude from Git archives
|
||||
.editorconfig export-ignore
|
||||
.gitattributes export-ignore
|
||||
.github export-ignore
|
||||
.gitignore export-ignore
|
||||
.travis.yml export-ignore
|
||||
doc/**/*.json export-ignore
|
||||
doc/**/*.md export-ignore
|
||||
.docker/ export-ignore
|
||||
.dockerignore export-ignore
|
||||
Dockerfile* export-ignore
|
||||
Doxyfile export-ignore
|
||||
Makefile export-ignore
|
||||
mkdocs.yml export-ignore
|
||||
phpunit.xml export-ignore
|
||||
tests/ export-ignore
|
||||
.editorconfig export-ignore
|
||||
.dev export-ignore
|
||||
.gitattributes export-ignore
|
||||
.github export-ignore
|
||||
.gitignore export-ignore
|
||||
.travis.yml export-ignore
|
||||
doc/**/*.json export-ignore
|
||||
doc/**/*.md export-ignore
|
||||
.docker/ export-ignore
|
||||
.dockerignore export-ignore
|
||||
docker-compose.* export-ignore
|
||||
Dockerfile* export-ignore
|
||||
Doxyfile export-ignore
|
||||
Makefile export-ignore
|
||||
node_modules/ export-ignore
|
||||
mkdocs.yml export-ignore
|
||||
phpunit.xml export-ignore
|
||||
tests/ export-ignore
|
||||
|
|
10
.gitignore
vendored
|
@ -40,3 +40,13 @@ tpl/*
|
|||
|
||||
contact.php
|
||||
formStyle.css
|
||||
|
||||
# Front end
|
||||
node_modules
|
||||
tpl/default/js
|
||||
tpl/default/css
|
||||
tpl/default/fonts
|
||||
tpl/default/img
|
||||
tpl/vintage/js
|
||||
tpl/vintage/css
|
||||
tpl/vintage/img
|
||||
|
|
32
.htaccess
|
@ -14,3 +14,35 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
|
|||
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>
|
||||
<IfModule version_module>
|
||||
<IfVersion >= 2.4>
|
||||
Require all denied
|
||||
</IfVersion>
|
||||
<IfVersion < 2.4>
|
||||
Allow from none
|
||||
Deny from all
|
||||
</IfVersion>
|
||||
</IfModule>
|
||||
|
||||
<IfModule !version_module>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
</LimitExcept>
|
||||
|
|
49
.travis.yml
|
@ -1,20 +1,53 @@
|
|||
sudo: false
|
||||
dist: trusty
|
||||
language: php
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- language: php
|
||||
php: 7.2
|
||||
- language: php
|
||||
php: 7.1
|
||||
- language: php
|
||||
php: 7.0
|
||||
- language: php
|
||||
php: 5.6
|
||||
- language: node_js
|
||||
node_js: 8
|
||||
cache:
|
||||
yarn: true
|
||||
directories:
|
||||
- $HOME/.cache/yarn
|
||||
|
||||
install:
|
||||
- yarn install
|
||||
|
||||
before_script:
|
||||
- PATH=${PATH//:\.\/node_modules\/\.bin/}
|
||||
|
||||
script:
|
||||
- yarn run build # Just to be sure that the build isn't broken
|
||||
- make eslint
|
||||
- make sasslint
|
||||
- language: python
|
||||
python: 3.6
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
install:
|
||||
- pip install mkdocs
|
||||
script:
|
||||
- mkdocs build --clean
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
php:
|
||||
- 7.1
|
||||
- 7.0
|
||||
- 5.6
|
||||
- 5.5
|
||||
|
||||
install:
|
||||
- composer self-update
|
||||
- composer install --prefer-dist
|
||||
- locale -a
|
||||
|
||||
before_script:
|
||||
- PATH=${PATH//:\.\/node_modules\/\.bin/}
|
||||
|
||||
script:
|
||||
- make clean
|
||||
- make check_permissions
|
||||
|
|
20
AUTHORS
|
@ -1,6 +1,6 @@
|
|||
588 ArthurHoaro <arthur@hoa.ro>
|
||||
283 VirtualTam <virtualtam@flibidi.net>
|
||||
179 nodiscc <nodiscc@gmail.com>
|
||||
687 ArthurHoaro <arthur@hoa.ro>
|
||||
355 VirtualTam <virtualtam@flibidi.net>
|
||||
195 nodiscc <nodiscc@gmail.com>
|
||||
56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
|
||||
15 Florian Eula <eula.florian@gmail.com>
|
||||
13 Emilien Klein <emilien@klein.st>
|
||||
|
@ -9,12 +9,15 @@
|
|||
8 Christophe HENRY <christophe.henry@sbgodin.fr>
|
||||
6 B. van Berkum <dev@dotmpe.com>
|
||||
5 Lucas Cimon <lucas.cimon@gmail.com>
|
||||
5 Mark Schmitz <kramred@gmail.com>
|
||||
5 kalvn <kalvnthereal@gmail.com>
|
||||
4 Alexandre Alapetite <alexandre@alapetite.fr>
|
||||
4 David Sferruzza <david.sferruzza@gmail.com>
|
||||
4 Immánuel Fodor <immanuelfactor+github@gmail.com>
|
||||
4 kalvn <kalvnthereal@gmail.com>
|
||||
3 Teromene <teromene@teromene.fr>
|
||||
3 llune <llune@users.noreply.github.com>
|
||||
2 Chris Kuethe <chris.kuethe@gmail.com>
|
||||
2 Felix Bartels <felix@host-consultants.de>
|
||||
2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
|
||||
2 Mathieu Chabanon <git@matchab.fr>
|
||||
2 Miloš Jovanović <mjovanovic@gmail.com>
|
||||
|
@ -23,20 +26,26 @@
|
|||
2 Timo Van Neerden <fire@lehollandaisvolant.net>
|
||||
2 julienCXX <software@chmodplusx.eu>
|
||||
2 philipp-r <philipp-r@users.noreply.github.com>
|
||||
2 pips <pips@e5150.fr>
|
||||
1 Adrien Oliva <adrien.oliva@yapbreak.fr>
|
||||
1 Adrien le Maire <adrien@alemaire.be>
|
||||
1 Alexandre G.-Raymond <alex@ndre.gr>
|
||||
1 Alexis J <alexis@effingo.be>
|
||||
1 Angristan <angristan@users.noreply.github.com>
|
||||
1 BoboTiG <bobotig@gmail.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 Dennis Verspuij <dennisverspuij@users.noreply.github.com>
|
||||
1 Dimtion <zizou.xena@gmail.com>
|
||||
1 Fanch <fanch-github@qth.fr>
|
||||
1 Felix Bartels <felix@host-consultants.de>
|
||||
1 Felix Kästner <github.com-fpunktk@fpunktk.de>
|
||||
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 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>
|
||||
|
@ -49,3 +58,4 @@
|
|||
1 TsT <tst2005@gmail.com>
|
||||
1 dimtion <zizou.xena@gmail.com>
|
||||
1 durcheinandr <jochen@durcheinandr.de>
|
||||
1 lapineige <lapineige@users.noreply.github.com>
|
||||
|
|
96
CHANGELOG.md
|
@ -4,6 +4,89 @@ 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/).
|
||||
|
||||
## [v0.10.2](https://github.com/shaarli/Shaarli/releases/tag/v0.10.2) - 2018-08-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Docker build
|
||||
|
||||
## [v0.10.1](https://github.com/shaarli/Shaarli/releases/tag/v0.10.1) - 2018-08-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Accessibility:
|
||||
- Remove alt text on the logo
|
||||
- Remove redundant title in tools page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an error on the daily page and daily RSS
|
||||
- Fixed an issue causing 'You are not authorized to add a link' error while logged out
|
||||
- Fixed thumbnail path when Shaarli's path uses symbolic links
|
||||
- Add a `mod_version` check in Shaarli's root `.htaccess` file for Apache 2.2 syntax
|
||||
- Include assets in the release Makefile target
|
||||
|
||||
### Removed
|
||||
|
||||
- Firefox Social API shaare has been removed
|
||||
|
||||
## [v0.10.0](https://github.com/shaarli/Shaarli/releases/tag/v0.10.0) - 2018-07-28
|
||||
**PHP 5.5 compatibility has been dropped.** Shaarli now requires at least PHP 5.6.
|
||||
|
||||
### Added
|
||||
- Add a filter to display public links only
|
||||
- Add PHP 7.2 support
|
||||
- Add German translation
|
||||
- Resolve front-end dependencies from NPM
|
||||
- Build front-end bundles with Yarn and Webpack
|
||||
- Lint Javascript code with ESLint
|
||||
- Lint SASS code with SASSLint
|
||||
- Support redirection in cURL download callback
|
||||
- Introduce multi-stage builds for Docker images
|
||||
- Use Travis matrix and stages to run Javascript tests in a dedicated environment
|
||||
- Add tag endpoint in the REST API
|
||||
- Build the documentation in Travis builds
|
||||
- Provide a Docker Compose example
|
||||
|
||||
### Changed
|
||||
- Use web-thumbnailer to retrieve thumbnails (see #687)
|
||||
- Use a specific page title in all pages
|
||||
- Daily: run hooks before creating the columns
|
||||
- Load theme translations files automatically
|
||||
- Make max download size and timeout configurable
|
||||
- Make Nginx logs accessible as stdout/stderr for Docker images
|
||||
- Update buttons used to toggle link visibility filters
|
||||
- Rewrite Javascript code for ES6 compliance
|
||||
- Refactor IP ban management
|
||||
- Refactor user login management
|
||||
- Refactor server-side session management
|
||||
- Update Doxygen configuration
|
||||
- Update Parsedown
|
||||
- Improve documentation
|
||||
- Docker: build the images from the local sources
|
||||
- Docker: bump alpine version to 3.7
|
||||
- Docker: expose a volume for the thumbnail cache
|
||||
|
||||
### Removed
|
||||
- Drop support for PHP 5.5
|
||||
- Remove vendored front-end libraries
|
||||
- Remove environment specific .gitignore entries
|
||||
|
||||
### Fixed
|
||||
- Ignore the case while checking DOCTYPE during the file import
|
||||
- Fix removal of on=... attributes from html generated from Markdown
|
||||
- httpd: always forward the 'Authorization' header
|
||||
- Ensure user-specific CSS file is loaded
|
||||
- Fix feed permalink rendering when Markdown escaping is enabled
|
||||
- Fix order of tags with the same number of occurrences
|
||||
- Fixed the referrer meta tag in default template
|
||||
- Disable MkDocs' strict mode for ReadTheDocs builds to pass
|
||||
- fix and simplify Dockerfile for armhf
|
||||
|
||||
### Security
|
||||
- Update `.htaccess` to prevent accessing Git metadata when using a Git-based installation
|
||||
|
||||
|
||||
## [v0.9.7](https://github.com/shaarli/Shaarli/releases/tag/v0.9.7) - 2018-06-20
|
||||
### Changed
|
||||
- Build the Docker images from the local Git sources
|
||||
|
@ -240,6 +323,19 @@ Theming:
|
|||
|
||||
- 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
|
||||
- Build the Docker image from the local Git sources
|
||||
|
||||
### Removed
|
||||
- Disable PHP 5.3 Travis build (unsupported)
|
||||
|
||||
|
||||
## [v0.8.6](https://github.com/shaarli/Shaarli/releases/tag/v0.8.6) - 2018-02-19
|
||||
### Changed
|
||||
- Run version check tests against the 'stable' branch
|
||||
|
||||
|
||||
## [v0.8.5](https://github.com/shaarli/Shaarli/releases/tag/v0.8.5) - 2018-01-04
|
||||
**XSS vulnerability fixed. Please update.**
|
||||
|
||||
|
|
38
COPYING
|
@ -1,55 +1,57 @@
|
|||
Files: *
|
||||
License: zlib/libpng
|
||||
Copyright: (c) 2011-2015 Sébastien SAUVAGE <sebsauvage@sebsauvage.net>
|
||||
(c) 2011-2017 The Shaarli Community, see AUTHORS
|
||||
(c) 2011-2018 The Shaarli Community, see AUTHORS
|
||||
|
||||
Files: inc/reset.css
|
||||
Files: assets/vintage/css/reset.css
|
||||
License: BSD (http://opensource.org/licenses/BSD-3-Clause)
|
||||
Copyright: (c) 2010, Yahoo! Inc.
|
||||
|
||||
Files: images/calendar.png, images/edit_icon.png, images/feed-icon-14x14.png, images/private.png, images/private_16x16.png, images/private_16x16_active.png, images/tag_blue.png
|
||||
Files: assets/vintage/img/calendar.png
|
||||
assets/vintage/img/edit_icon.png
|
||||
assets/vintage/img/feed-icon-14x14.png
|
||||
assets/vintage/img/private.png
|
||||
assets/vintage/img/private_16x16.png
|
||||
assets/vintage/img/private_16x16_active.png
|
||||
assets/vintage/img/tag_blue.png
|
||||
License: CC-BY (http://creativecommons.org/licenses/by/3.0/)
|
||||
Copyright: (c) 2014 Yusuke Kamiyamane
|
||||
Source: http://p.yusukekamiyamane.com/
|
||||
|
||||
Files: images/delete_icon.png
|
||||
Files: assets/vintage/img/delete_icon.png
|
||||
License: CC-BY (http://creativecommons.org/licenses/by/3.0/)
|
||||
Copyright: (c) 2014 Designmodo
|
||||
Source: http://designmodo.com/linecons-free/
|
||||
|
||||
Files: images/floral_left.png, images/floral_right.png, images/squiggle.png, images/squiggle_closing.png
|
||||
Files: assets/vintage/img/floral_left.png
|
||||
assets/vintage/img/floral_right.png
|
||||
assets/vintage/img/squiggle.png
|
||||
assets/vintage/img/squiggle_closing.png
|
||||
Licence: Public Domain
|
||||
Source: https://openclipart.org/people/j4p4n/j4p4n_ornimental_bookend_-_left.svg
|
||||
|
||||
Files: images/Paper_texture_v5_by_bashcorpo_w1000.jpg
|
||||
Files: assets/vintage/img/Paper_texture_v5_by_bashcorpo_w1000.jpg
|
||||
Licence: Public Domain
|
||||
Source: http://bashcorpo.deviantart.com/art/Grungy-paper-texture-v-5-22966998
|
||||
|
||||
Files: images/logo.png
|
||||
Files: assets/vintage/img/logo.png
|
||||
assets/vintage/img/logo.png
|
||||
License: zlib/libpng
|
||||
Copyright: (c) 2011-2014 idleman idleman@idleman.fr
|
||||
|
||||
Files: inc/blazy*.js
|
||||
Files: assets/default/img/sad_star.png
|
||||
License: MIT License (http://opensource.org/licenses/MIT)
|
||||
Copyright: (C) Bjoern Klinggaard - @bklinggaard - http://dinbror.dk/blazy
|
||||
Copyright: (C) 2015 kalvn - https://github.com/kalvn/Shaarli-Material
|
||||
|
||||
Files: inc/rain.tpl.class.php
|
||||
License: LGPL-3+ (https://www.gnu.org/licenses/lgpl-3.0.txt)
|
||||
Copyright: 2011-2012, Federico Ulfo <rainelemental@gmail.com>
|
||||
2011-2012, The Rain Team <hello@raintm.com>
|
||||
License: LGPL-3+ (https://www.gnu.org/licenses/lgpl-3.0.txt)
|
||||
|
||||
Files: inc/awesomplete*
|
||||
License: MIT License (http://opensource.org/licenses/MIT)
|
||||
Copyright: (C) 2015 Lea Verou - https://github.com/LeaVerou/awesomplete
|
||||
|
||||
Files: plugins/wallabag/wallabag.png
|
||||
License: MIT License (http://opensource.org/licenses/MIT)
|
||||
Copyright: (C) 2015 Nicolas Lœuillet - https://github.com/wallabag/wallabag
|
||||
|
||||
Files: tpl/default/sad_star.png
|
||||
License: MIT License (http://opensource.org/licenses/MIT)
|
||||
Copyright: (C) 2015 kalvn - https://github.com/kalvn/Shaarli-Material
|
||||
|
||||
----------------------------------------------------
|
||||
ZLIB/LIBPNG LICENSE
|
||||
|
||||
|
|
16
Dockerfile
|
@ -5,7 +5,7 @@ FROM python:3-alpine as docs
|
|||
ADD . /usr/src/app/shaarli
|
||||
RUN cd /usr/src/app/shaarli \
|
||||
&& pip install --no-cache-dir mkdocs \
|
||||
&& mkdocs build
|
||||
&& mkdocs build --clean
|
||||
|
||||
# Stage 2:
|
||||
# - Resolve PHP dependencies with Composer
|
||||
|
@ -15,8 +15,17 @@ RUN cd shaarli \
|
|||
&& composer --prefer-dist --no-dev install
|
||||
|
||||
# Stage 3:
|
||||
# - Frontend dependencies
|
||||
FROM node:9.9-alpine as node
|
||||
COPY --from=composer /app/shaarli shaarli
|
||||
RUN cd shaarli \
|
||||
&& yarn install \
|
||||
&& yarn run build \
|
||||
&& rm -rf node_modules
|
||||
|
||||
# Stage 4:
|
||||
# - Shaarli image
|
||||
FROM alpine:3.6
|
||||
FROM alpine:3.8
|
||||
LABEL maintainer="Shaarli Community"
|
||||
|
||||
RUN apk --update --no-cache add \
|
||||
|
@ -47,12 +56,13 @@ RUN rm -rf /etc/php7/php-fpm.d/www.conf \
|
|||
|
||||
|
||||
WORKDIR /var/www
|
||||
COPY --from=composer /app/shaarli shaarli
|
||||
COPY --from=node /shaarli shaarli
|
||||
|
||||
RUN chown -R nginx:nginx . \
|
||||
&& ln -sf /dev/stdout /var/log/nginx/shaarli.access.log \
|
||||
&& ln -sf /dev/stderr /var/log/nginx/shaarli.error.log
|
||||
|
||||
VOLUME /var/www/shaarli/cache
|
||||
VOLUME /var/www/shaarli/data
|
||||
|
||||
EXPOSE 80
|
||||
|
|
3
Doxyfile
|
@ -51,7 +51,7 @@ PROJECT_BRIEF = "The personal, minimalist, super-fast, no-database deli
|
|||
# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
|
||||
# the logo to the output directory.
|
||||
|
||||
PROJECT_LOGO = images/logo.png
|
||||
PROJECT_LOGO = doc/md/images/logo.png
|
||||
|
||||
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
|
||||
# into which the generated documentation will be written. If a relative path is
|
||||
|
@ -804,6 +804,7 @@ RECURSIVE = YES
|
|||
# run.
|
||||
|
||||
EXCLUDE = vendor \
|
||||
data \
|
||||
tpl \
|
||||
inc \
|
||||
doc \
|
||||
|
|
30
Makefile
|
@ -157,21 +157,32 @@ composer_dependencies: clean
|
|||
composer install --no-dev --prefer-dist
|
||||
find vendor/ -name ".git" -type d -exec rm -rf {} +
|
||||
|
||||
### download 3rd-party frontend libraries
|
||||
frontend_dependencies:
|
||||
yarn install
|
||||
|
||||
### Build frontend dependencies
|
||||
build_frontend: frontend_dependencies
|
||||
yarn run build
|
||||
|
||||
### generate a release tarball and include 3rd-party dependencies and translations
|
||||
release_tar: composer_dependencies htmldoc translate
|
||||
release_tar: composer_dependencies htmldoc translate build_frontend
|
||||
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD
|
||||
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/
|
||||
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
|
||||
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^tpl|$(ARCHIVE_PREFIX)tpl|" tpl/
|
||||
gzip $(ARCHIVE_VERSION).tar
|
||||
|
||||
### generate a release zip and include 3rd-party dependencies and translations
|
||||
release_zip: composer_dependencies htmldoc translate
|
||||
release_zip: composer_dependencies htmldoc translate build_frontend
|
||||
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
|
||||
mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
|
||||
rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
|
||||
zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)doc/
|
||||
rsync -a vendor/ $(ARCHIVE_PREFIX)vendor/
|
||||
zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)vendor/
|
||||
rsync -a tpl/ $(ARCHIVE_PREFIX)tpl/
|
||||
zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)tpl/
|
||||
rm -rf $(ARCHIVE_PREFIX)
|
||||
|
||||
##
|
||||
|
@ -192,18 +203,27 @@ authors:
|
|||
### generate Doxygen documentation
|
||||
doxygen: clean
|
||||
@rm -rf doxygen
|
||||
@( cat Doxyfile ; echo "PROJECT_NUMBER=`git describe`" ) | doxygen -
|
||||
@doxygen Doxyfile
|
||||
|
||||
### generate HTML documentation from Markdown pages with MkDocs
|
||||
htmldoc:
|
||||
python3 -m venv venv/
|
||||
bash -c 'source venv/bin/activate; \
|
||||
pip install mkdocs; \
|
||||
mkdocs build'
|
||||
mkdocs build --clean'
|
||||
find doc/html/ -type f -exec chmod a-x '{}' \;
|
||||
rm -r venv
|
||||
|
||||
|
||||
### Generate Shaarli's translation compiled file (.mo)
|
||||
translate:
|
||||
@find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \;
|
||||
@find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \;
|
||||
|
||||
### Run ESLint check against Shaarli's JS files
|
||||
eslint:
|
||||
@yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/
|
||||
@yarn run eslint -c .dev/.eslintrc.js assets/default/js/
|
||||
|
||||
### Run CSSLint check against Shaarli's SCSS files
|
||||
sasslint:
|
||||
@yarn run sass-lint -c .dev/.sasslintrc 'assets/default/scss/*.scss' -v -q
|
||||
|
|
|
@ -6,10 +6,10 @@ _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.8.5-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.5)
|
||||
[![](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)
|
||||
•
|
||||
[![](https://img.shields.io/badge/latest-v0.9.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.4)
|
||||
[![](https://img.shields.io/badge/latest-v0.10.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.10.1)
|
||||
[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
|
||||
•
|
||||
[![](https://img.shields.io/badge/master-v0.10.x-blue.svg)](https://github.com/shaarli/Shaarli)
|
||||
|
|
|
@ -37,7 +37,7 @@ public static function writeFlatDB($file, $content)
|
|||
if (is_file($file) && !is_writeable($file)) {
|
||||
// The datastore exists but is not writeable
|
||||
throw new IOException($file);
|
||||
} else if (!is_file($file) && !is_writeable(dirname($file))) {
|
||||
} elseif (!is_file($file) && !is_writeable(dirname($file))) {
|
||||
// The datastore does not exist and its parent directory is not writeable
|
||||
throw new IOException(dirname($file));
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
/**
|
||||
* GET an HTTP URL to retrieve its content
|
||||
* Uses the cURL library or a fallback method
|
||||
* Uses the cURL library or a fallback method
|
||||
*
|
||||
* @param string $url URL to get (http://...)
|
||||
* @param int $timeout network timeout (in seconds)
|
||||
|
@ -415,6 +415,37 @@ function getIpAddressFromProxy($server, $trustedIps)
|
|||
return array_pop($ips);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return an identifier based on the advertised client IP address(es)
|
||||
*
|
||||
* This aims at preventing session hijacking from users behind the same proxy
|
||||
* by relying on HTTP headers.
|
||||
*
|
||||
* See:
|
||||
* - https://secure.php.net/manual/en/reserved.variables.server.php
|
||||
* - https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php
|
||||
* - https://stackoverflow.com/questions/12233406/preventing-session-hijacking
|
||||
* - https://stackoverflow.com/questions/21354859/trusting-x-forwarded-for-to-identify-a-visitor
|
||||
*
|
||||
* @param array $server The $_SERVER array
|
||||
*
|
||||
* @return string An identifier based on client IP address information
|
||||
*/
|
||||
function client_ip_id($server)
|
||||
{
|
||||
$ip = $server['REMOTE_ADDR'];
|
||||
|
||||
if (isset($server['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $ip . '_' . $server['HTTP_X_FORWARDED_FOR'];
|
||||
}
|
||||
if (isset($server['HTTP_CLIENT_IP'])) {
|
||||
$ip = $ip . '_' . $server['HTTP_CLIENT_IP'];
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if Shaarli's currently browsed in HTTPS.
|
||||
* Supports reverse proxies (if the headers are correctly set).
|
||||
|
|
|
@ -98,6 +98,12 @@ protected function initGettextTranslator ()
|
|||
$this->translator->setLanguage($this->language);
|
||||
$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';
|
||||
if (is_dir($themeTransFolder)) {
|
||||
$this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false);
|
||||
}
|
||||
|
||||
foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
|
||||
if ($domain !== self::DEFAULT_DOMAIN) {
|
||||
$this->translator->loadDomain($domain, $translationPath, false);
|
||||
|
@ -116,12 +122,23 @@ protected function initPhpTranslator()
|
|||
$translations = new Translations();
|
||||
// Core translations
|
||||
try {
|
||||
/** @var Translations $translations */
|
||||
$translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
|
||||
$translations->setDomain('shaarli');
|
||||
$this->translator->loadTranslations($translations);
|
||||
} catch (\InvalidArgumentException $e) {}
|
||||
|
||||
// Default extension translation from the current theme
|
||||
$theme = $this->conf->get('theme');
|
||||
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $theme .'/language';
|
||||
if (is_dir($themeTransFolder)) {
|
||||
try {
|
||||
$translations = Translations::fromPoFile(
|
||||
$themeTransFolder .'/'. $this->language .'/LC_MESSAGES/'. $theme .'.po'
|
||||
);
|
||||
$translations->setDomain($theme);
|
||||
$this->translator->loadTranslations($translations);
|
||||
} catch (\InvalidArgumentException $e) {}
|
||||
}
|
||||
|
||||
// Extension translations (plugins, themes, etc.).
|
||||
foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
|
||||
|
@ -130,7 +147,6 @@ protected function initPhpTranslator()
|
|||
}
|
||||
|
||||
try {
|
||||
/** @var Translations $extension */
|
||||
$extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
|
||||
$extension->setDomain($domain);
|
||||
$this->translator->loadTranslations($extension);
|
||||
|
@ -161,6 +177,7 @@ public static function getAvailableLanguages()
|
|||
'auto' => t('Automatic'),
|
||||
'en' => t('English'),
|
||||
'fr' => t('French'),
|
||||
'de' => t('German'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -436,15 +436,17 @@ public function filterSearch($filterRequest = array(), $casesensitive = false, $
|
|||
|
||||
/**
|
||||
* Returns the list tags appearing in the links with the given tags
|
||||
* @param $filteringTags: tags selecting the links to consider
|
||||
* @param $visibility: process only all/private/public links
|
||||
* @return: a tag=>linksCount array
|
||||
*
|
||||
* @param array $filteringTags tags selecting the links to consider
|
||||
* @param string $visibility process only all/private/public links
|
||||
*
|
||||
* @return array tag => linksCount
|
||||
*/
|
||||
public function linksCountPerTag($filteringTags = [], $visibility = 'all')
|
||||
{
|
||||
$links = empty($filteringTags) ? $this->links : $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
|
||||
$tags = array();
|
||||
$caseMapping = array();
|
||||
$links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
|
||||
$tags = [];
|
||||
$caseMapping = [];
|
||||
foreach ($links as $link) {
|
||||
foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
|
||||
if (empty($tag)) {
|
||||
|
@ -458,8 +460,19 @@ public function linksCountPerTag($filteringTags = [], $visibility = 'all')
|
|||
$tags[$caseMapping[strtolower($tag)]]++;
|
||||
}
|
||||
}
|
||||
// Sort tags by usage (most used tag first)
|
||||
arsort($tags);
|
||||
|
||||
/*
|
||||
* Formerly used arsort(), which doesn't define the sort behaviour for equal values.
|
||||
* Also, this function doesn't produce the same result between PHP 5.6 and 7.
|
||||
*
|
||||
* So we now use array_multisort() to sort tags by DESC occurrences,
|
||||
* then ASC alphabetically for equal values.
|
||||
*
|
||||
* @see https://github.com/shaarli/Shaarli/issues/1142
|
||||
*/
|
||||
$keys = array_keys($tags);
|
||||
$tmpTags = array_combine($keys, $keys);
|
||||
array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
|
||||
return $tags;
|
||||
}
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ private function noFilter($visibility = 'all')
|
|||
foreach ($this->links as $key => $value) {
|
||||
if ($value['private'] && $visibility === 'private') {
|
||||
$out[$key] = $value;
|
||||
} else if (! $value['private'] && $visibility === 'public') {
|
||||
} elseif (! $value['private'] && $visibility === 'public') {
|
||||
$out[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +210,7 @@ private function filterFulltext($searchterms, $visibility = 'all')
|
|||
if ($visibility !== 'all') {
|
||||
if (! $link['private'] && $visibility === 'private') {
|
||||
continue;
|
||||
} else if ($link['private'] && $visibility === 'public') {
|
||||
} elseif ($link['private'] && $visibility === 'public') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -337,7 +337,7 @@ public function filterTags($tags, $casesensitive = false, $visibility = 'all')
|
|||
if ($visibility !== 'all') {
|
||||
if (! $link['private'] && $visibility === 'private') {
|
||||
continue;
|
||||
} else if ($link['private'] && $visibility === 'public') {
|
||||
} elseif ($link['private'] && $visibility === 'public') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -380,7 +380,7 @@ public function filterUntagged($visibility)
|
|||
if ($visibility !== 'all') {
|
||||
if (! $link['private'] && $visibility === 'private') {
|
||||
continue;
|
||||
} else if ($link['private'] && $visibility === 'public') {
|
||||
} elseif ($link['private'] && $visibility === 'public') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
*/
|
||||
function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo')
|
||||
{
|
||||
$isRedirected = false;
|
||||
/**
|
||||
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
|
||||
*
|
||||
|
@ -22,16 +23,24 @@ function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_get
|
|||
*
|
||||
* @return int|bool length of $data or false if we need to stop the download
|
||||
*/
|
||||
return function(&$ch, $data) use ($curlGetInfo, &$charset, &$title) {
|
||||
return function(&$ch, $data) use ($curlGetInfo, &$charset, &$title, &$isRedirected) {
|
||||
$responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
if (!empty($responseCode) && $responseCode != 200) {
|
||||
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
|
||||
$isRedirected = true;
|
||||
return strlen($data);
|
||||
}
|
||||
if (!empty($responseCode) && $responseCode !== 200) {
|
||||
return false;
|
||||
}
|
||||
$contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
// After a redirection, the content type will keep the previous request value
|
||||
// until it finds the next content-type header.
|
||||
if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
|
||||
$contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
}
|
||||
if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
|
||||
return false;
|
||||
}
|
||||
if (empty($charset)) {
|
||||
if (!empty($contentType) && empty($charset)) {
|
||||
$charset = header_extract_charset($contentType);
|
||||
}
|
||||
if (empty($charset)) {
|
||||
|
|
|
@ -108,7 +108,7 @@ public static function import($post, $files, $linkDb, $conf, $history)
|
|||
$filesize = $files['filetoupload']['size'];
|
||||
$data = file_get_contents($files['filetoupload']['tmp_name']);
|
||||
|
||||
if (strpos($data, '<!DOCTYPE NETSCAPE-Bookmark-file-1>') === false) {
|
||||
if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) {
|
||||
return self::importStatus($filename, $filesize);
|
||||
}
|
||||
|
||||
|
@ -154,13 +154,13 @@ public static function import($post, $files, $linkDb, $conf, $history)
|
|||
if (empty($post['privacy']) || $post['privacy'] == 'default') {
|
||||
// use value from the imported file
|
||||
$private = $bkm['pub'] == '1' ? 0 : 1;
|
||||
} else if ($post['privacy'] == 'private') {
|
||||
} elseif ($post['privacy'] == 'private') {
|
||||
// all imported links are private
|
||||
$private = 1;
|
||||
} else if ($post['privacy'] == 'public') {
|
||||
} elseif ($post['privacy'] == 'public') {
|
||||
// all imported links are public
|
||||
$private = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$newLink = array(
|
||||
'title' => $bkm['title'],
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Thumbnailer;
|
||||
|
||||
/**
|
||||
* This class is in charge of building the final page.
|
||||
|
@ -21,25 +22,42 @@ class PageBuilder
|
|||
*/
|
||||
protected $conf;
|
||||
|
||||
/**
|
||||
* @var array $_SESSION
|
||||
*/
|
||||
protected $session;
|
||||
|
||||
/**
|
||||
* @var LinkDB $linkDB instance.
|
||||
*/
|
||||
protected $linkDB;
|
||||
|
||||
/**
|
||||
* @var null|string XSRF token
|
||||
*/
|
||||
protected $token;
|
||||
|
||||
/** @var bool $isLoggedIn Whether the user is logged in **/
|
||||
protected $isLoggedIn = false;
|
||||
|
||||
/**
|
||||
* PageBuilder constructor.
|
||||
* $tpl is initialized at false for lazy loading.
|
||||
*
|
||||
* @param ConfigManager $conf Configuration Manager instance (reference).
|
||||
* @param LinkDB $linkDB instance.
|
||||
* @param string $token Session token
|
||||
* @param ConfigManager $conf Configuration Manager instance (reference).
|
||||
* @param array $session $_SESSION array
|
||||
* @param LinkDB $linkDB instance.
|
||||
* @param string $token Session token
|
||||
* @param bool $isLoggedIn
|
||||
*/
|
||||
public function __construct(&$conf, $linkDB = null, $token = null)
|
||||
public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
|
||||
{
|
||||
$this->tpl = false;
|
||||
$this->conf = $conf;
|
||||
$this->session = $session;
|
||||
$this->linkDB = $linkDB;
|
||||
$this->token = $token;
|
||||
$this->isLoggedIn = $isLoggedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,7 +73,7 @@ private function initialize()
|
|||
$this->conf->get('resource.update_check'),
|
||||
$this->conf->get('updates.check_updates_interval'),
|
||||
$this->conf->get('updates.check_updates'),
|
||||
isLoggedIn(),
|
||||
$this->isLoggedIn,
|
||||
$this->conf->get('updates.check_updates_branch')
|
||||
);
|
||||
$this->tpl->assign('newVersion', escape($version));
|
||||
|
@ -67,6 +85,7 @@ private function initialize()
|
|||
$this->tpl->assign('versionError', escape($exc->getMessage()));
|
||||
}
|
||||
|
||||
$this->tpl->assign('is_logged_in', $this->isLoggedIn);
|
||||
$this->tpl->assign('feedurl', escape(index_url($_SERVER)));
|
||||
$searchcrits = ''; // Search criteria
|
||||
if (!empty($_GET['searchtags'])) {
|
||||
|
@ -83,7 +102,8 @@ private function initialize()
|
|||
ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt'))
|
||||
);
|
||||
$this->tpl->assign('scripturl', index_url($_SERVER));
|
||||
$this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links?
|
||||
$visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
|
||||
$this->tpl->assign('visibility', $visibility);
|
||||
$this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly']));
|
||||
$this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli'));
|
||||
if ($this->conf->exists('general.header_link')) {
|
||||
|
@ -99,6 +119,19 @@ private function initialize()
|
|||
if ($this->linkDB !== null) {
|
||||
$this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
|
||||
}
|
||||
|
||||
$this->tpl->assign(
|
||||
'thumbnails_enabled',
|
||||
$this->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
|
||||
);
|
||||
$this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
|
||||
$this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
|
||||
|
||||
if (! empty($_SESSION['warnings'])) {
|
||||
$this->tpl->assign('global_warnings', $_SESSION['warnings']);
|
||||
unset($_SESSION['warnings']);
|
||||
}
|
||||
|
||||
// To be removed with a proper theme configuration.
|
||||
$this->tpl->assign('conf', $this->conf);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
*/
|
||||
class Router
|
||||
{
|
||||
public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
|
||||
|
||||
public static $PAGE_LOGIN = 'login';
|
||||
|
||||
public static $PAGE_PICWALL = 'picwall';
|
||||
|
@ -47,6 +49,8 @@ class Router
|
|||
|
||||
public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
|
||||
|
||||
public static $PAGE_THUMBS_UPDATE = 'thumbs_update';
|
||||
|
||||
public static $GET_TOKEN = 'token';
|
||||
|
||||
/**
|
||||
|
@ -101,6 +105,14 @@ public static function findPage($query, $get, $loggedIn)
|
|||
return self::$PAGE_FEED_RSS;
|
||||
}
|
||||
|
||||
if (startsWith($query, 'do='. self::$PAGE_THUMBS_UPDATE)) {
|
||||
return self::$PAGE_THUMBS_UPDATE;
|
||||
}
|
||||
|
||||
if (startsWith($query, 'do='. self::$AJAX_THUMB_UPDATE)) {
|
||||
return self::$AJAX_THUMB_UPDATE;
|
||||
}
|
||||
|
||||
// At this point, only loggedin pages.
|
||||
if (!$loggedIn) {
|
||||
return self::$PAGE_LINKLIST;
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
<?php
|
||||
namespace Shaarli;
|
||||
|
||||
/**
|
||||
* Manages the server-side session
|
||||
*/
|
||||
class SessionManager
|
||||
{
|
||||
protected $session = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $session The $_SESSION array (reference)
|
||||
* @param ConfigManager $conf ConfigManager instance
|
||||
*/
|
||||
public function __construct(& $session, $conf)
|
||||
{
|
||||
$this->session = &$session;
|
||||
$this->conf = $conf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a session token
|
||||
*
|
||||
* @return string token
|
||||
*/
|
||||
public function generateToken()
|
||||
{
|
||||
$token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
|
||||
$this->session['tokens'][$token] = 1;
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the validity of a session token, and destroys it afterwards
|
||||
*
|
||||
* @param string $token The token to check
|
||||
*
|
||||
* @return bool true if the token is valid, else false
|
||||
*/
|
||||
public function checkToken($token)
|
||||
{
|
||||
if (! isset($this->session['tokens'][$token])) {
|
||||
// the token is wrong, or has already been used
|
||||
return false;
|
||||
}
|
||||
|
||||
// destroy the token to prevent future use
|
||||
unset($this->session['tokens'][$token]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate session ID to prevent Full Path Disclosure.
|
||||
*
|
||||
* See #298.
|
||||
* The session ID's format depends on the hash algorithm set in PHP settings
|
||||
*
|
||||
* @param string $sessionId Session ID
|
||||
*
|
||||
* @return true if valid, false otherwise.
|
||||
*
|
||||
* @see http://php.net/manual/en/function.hash-algos.php
|
||||
* @see http://php.net/manual/en/session.configuration.php
|
||||
*/
|
||||
public static function checkId($sessionId)
|
||||
{
|
||||
if (empty($sessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$sessionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
127
application/Thumbnailer.php
Normal file
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace Shaarli;
|
||||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use WebThumbnailer\Exception\WebThumbnailerException;
|
||||
use WebThumbnailer\WebThumbnailer;
|
||||
use WebThumbnailer\Application\ConfigManager as WTConfigManager;
|
||||
|
||||
/**
|
||||
* Class Thumbnailer
|
||||
*
|
||||
* Utility class used to retrieve thumbnails using web-thumbnailer dependency.
|
||||
*/
|
||||
class Thumbnailer
|
||||
{
|
||||
const COMMON_MEDIA_DOMAINS = [
|
||||
'imgur.com',
|
||||
'flickr.com',
|
||||
'youtube.com',
|
||||
'wikimedia.org',
|
||||
'redd.it',
|
||||
'gfycat.com',
|
||||
'media.giphy.com',
|
||||
'twitter.com',
|
||||
'twimg.com',
|
||||
'instagram.com',
|
||||
'pinterest.com',
|
||||
'pinterest.fr',
|
||||
'tumblr.com',
|
||||
'deviantart.com',
|
||||
];
|
||||
|
||||
const MODE_ALL = 'all';
|
||||
const MODE_COMMON = 'common';
|
||||
const MODE_NONE = 'none';
|
||||
|
||||
/**
|
||||
* @var WebThumbnailer instance.
|
||||
*/
|
||||
protected $wt;
|
||||
|
||||
/**
|
||||
* @var ConfigManager instance.
|
||||
*/
|
||||
protected $conf;
|
||||
|
||||
/**
|
||||
* Thumbnailer constructor.
|
||||
*
|
||||
* @param ConfigManager $conf instance.
|
||||
*/
|
||||
public function __construct($conf)
|
||||
{
|
||||
$this->conf = $conf;
|
||||
|
||||
if (! $this->checkRequirements()) {
|
||||
$this->conf->set('thumbnails.enabled', false);
|
||||
$this->conf->write(true);
|
||||
// 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.'));
|
||||
}
|
||||
|
||||
$this->wt = new WebThumbnailer();
|
||||
WTConfigManager::addFile('inc/web-thumbnailer.json');
|
||||
$this->wt->maxWidth($this->conf->get('thumbnails.width'))
|
||||
->maxHeight($this->conf->get('thumbnails.height'))
|
||||
->crop(true)
|
||||
->debug($this->conf->get('dev.debug', false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a thumbnail for given URL
|
||||
*
|
||||
* @param string $url where to look for a thumbnail.
|
||||
*
|
||||
* @return bool|string The thumbnail relative cache file path, or false if none has been found.
|
||||
*/
|
||||
public function get($url)
|
||||
{
|
||||
if ($this->conf->get('thumbnails.mode') === self::MODE_COMMON
|
||||
&& ! $this->isCommonMediaOrImage($url)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->wt->thumbnail($url);
|
||||
} catch (WebThumbnailerException $e) {
|
||||
// Exceptions are only thrown in debug mode.
|
||||
error_log(get_class($e) . ': ' . $e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* We check weather the given URL is from a common media domain,
|
||||
* or if the file extension is an image.
|
||||
*
|
||||
* @param string $url to check
|
||||
*
|
||||
* @return bool true if it's an image or from a common media domain, false otherwise.
|
||||
*/
|
||||
public function isCommonMediaOrImage($url)
|
||||
{
|
||||
foreach (self::COMMON_MEDIA_DOMAINS as $domain) {
|
||||
if (strpos($url, $domain) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (endsWith($url, '.jpg') || endsWith($url, '.png') || endsWith($url, '.jpeg')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that requirements are match to use thumbnails:
|
||||
* - php-gd is loaded
|
||||
*/
|
||||
protected function checkRequirements()
|
||||
{
|
||||
return extension_loaded('gd');
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
use Shaarli\Config\ConfigJson;
|
||||
use Shaarli\Config\ConfigPhp;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Thumbnailer;
|
||||
|
||||
/**
|
||||
* Class Updater.
|
||||
|
@ -30,6 +31,11 @@ class Updater
|
|||
*/
|
||||
protected $isLoggedIn;
|
||||
|
||||
/**
|
||||
* @var array $_SESSION
|
||||
*/
|
||||
protected $session;
|
||||
|
||||
/**
|
||||
* @var ReflectionMethod[] List of current class methods.
|
||||
*/
|
||||
|
@ -42,13 +48,17 @@ class Updater
|
|||
* @param LinkDB $linkDB LinkDB instance.
|
||||
* @param ConfigManager $conf Configuration Manager instance.
|
||||
* @param boolean $isLoggedIn True if the user is logged in.
|
||||
* @param array $session $_SESSION (by reference)
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
|
||||
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
|
||||
{
|
||||
$this->doneUpdates = $doneUpdates;
|
||||
$this->linkDB = $linkDB;
|
||||
$this->conf = $conf;
|
||||
$this->isLoggedIn = $isLoggedIn;
|
||||
$this->session = &$session;
|
||||
|
||||
// Retrieve all update methods.
|
||||
$class = new ReflectionClass($this);
|
||||
|
@ -445,6 +455,68 @@ public function updateMethodReorderDatastore()
|
|||
$this->linkDB->save($this->conf->get('resource.page_cache'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change privateonly session key to visibility.
|
||||
*/
|
||||
public function updateMethodVisibilitySession()
|
||||
{
|
||||
if (isset($_SESSION['privateonly'])) {
|
||||
unset($_SESSION['privateonly']);
|
||||
$_SESSION['visibility'] = 'private';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add download size and timeout to the configuration file
|
||||
*
|
||||
* @return bool true if the update is successful, false otherwise.
|
||||
*/
|
||||
public function updateMethodDownloadSizeAndTimeoutConf()
|
||||
{
|
||||
if ($this->conf->exists('general.download_max_size')
|
||||
&& $this->conf->exists('general.download_timeout')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $this->conf->exists('general.download_max_size')) {
|
||||
$this->conf->set('general.download_max_size', 1024*1024*4);
|
||||
}
|
||||
|
||||
if (! $this->conf->exists('general.download_timeout')) {
|
||||
$this->conf->set('general.download_timeout', 30);
|
||||
}
|
||||
|
||||
$this->conf->write($this->isLoggedIn);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* * Move thumbnails management to WebThumbnailer, coming with new settings.
|
||||
*/
|
||||
public function updateMethodWebThumbnailer()
|
||||
{
|
||||
if ($this->conf->exists('thumbnails.mode')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
|
||||
$this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
|
||||
$this->conf->set('thumbnails.width', 125);
|
||||
$this->conf->set('thumbnails.height', 90);
|
||||
$this->conf->remove('thumbnail');
|
||||
$this->conf->write(true);
|
||||
|
||||
if ($thumbnailsEnabled) {
|
||||
$this->session['warnings'][] = t(
|
||||
'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -81,7 +81,7 @@ function whitelist_protocols($url, $protocols)
|
|||
// Protocol not allowed: we remove it and replace it with http
|
||||
if ($protocol === 1 && ! in_array($match[1], $protocols)) {
|
||||
$url = str_replace($match[0], 'http://', $url);
|
||||
} else if ($protocol !== 1) {
|
||||
} elseif ($protocol !== 1) {
|
||||
$url = 'http://' . $url;
|
||||
}
|
||||
return $url;
|
||||
|
@ -260,7 +260,7 @@ public function idnToAscii()
|
|||
if (! function_exists('idn_to_ascii') || ! isset($this->parts['host'])) {
|
||||
return $out;
|
||||
}
|
||||
$asciiHost = idn_to_ascii($this->parts['host']);
|
||||
$asciiHost = idn_to_ascii($this->parts['host'], 0, INTL_IDNA_VARIANT_UTS46);
|
||||
return str_replace($this->parts['host'], $asciiHost, $out);
|
||||
}
|
||||
|
||||
|
|
|
@ -134,4 +134,20 @@ public static function updateLink($oldLink, $newLink)
|
|||
|
||||
return $oldLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Tag for the REST API.
|
||||
*
|
||||
* @param string $tag Tag name
|
||||
* @param int $occurrences Number of links using this tag
|
||||
*
|
||||
* @return array Link data formatted for the REST API.
|
||||
*/
|
||||
public static function formatTag($tag, $occurences)
|
||||
{
|
||||
return [
|
||||
'name' => $tag,
|
||||
'occurrences' => $occurences,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ public function getHistory($request, $response)
|
|||
if (empty($offset)) {
|
||||
$offset = 0;
|
||||
}
|
||||
else if (ctype_digit($offset)) {
|
||||
elseif (ctype_digit($offset)) {
|
||||
$offset = (int) $offset;
|
||||
} else {
|
||||
throw new ApiBadParametersException('Invalid offset');
|
||||
|
@ -46,7 +46,7 @@ public function getHistory($request, $response)
|
|||
$limit = $request->getParam('limit');
|
||||
if (empty($limit)) {
|
||||
$limit = count($history);
|
||||
} else if (ctype_digit($limit)) {
|
||||
} elseif (ctype_digit($limit)) {
|
||||
$limit = (int) $limit;
|
||||
} else {
|
||||
throw new ApiBadParametersException('Invalid limit');
|
||||
|
|
|
@ -59,25 +59,25 @@ public function getLinks($request, $response)
|
|||
$limit = $request->getParam('limit');
|
||||
if (empty($limit)) {
|
||||
$limit = self::$DEFAULT_LIMIT;
|
||||
} else if (ctype_digit($limit)) {
|
||||
} elseif (ctype_digit($limit)) {
|
||||
$limit = intval($limit);
|
||||
} else if ($limit === 'all') {
|
||||
} elseif ($limit === 'all') {
|
||||
$limit = count($links);
|
||||
} else {
|
||||
throw new ApiBadParametersException('Invalid limit');
|
||||
}
|
||||
|
||||
// 'environment' is set by Slim and encapsulate $_SERVER.
|
||||
$index = index_url($this->ci['environment']);
|
||||
$indexUrl = index_url($this->ci['environment']);
|
||||
|
||||
$out = [];
|
||||
$cpt = 0;
|
||||
$index = 0;
|
||||
foreach ($links as $link) {
|
||||
if (count($out) >= $limit) {
|
||||
break;
|
||||
}
|
||||
if ($cpt++ >= $offset) {
|
||||
$out[] = ApiUtils::formatLink($link, $index);
|
||||
if ($index++ >= $offset) {
|
||||
$out[] = ApiUtils::formatLink($link, $indexUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
161
application/api/controllers/Tags.php
Normal file
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace Shaarli\Api\Controllers;
|
||||
|
||||
use Shaarli\Api\ApiUtils;
|
||||
use Shaarli\Api\Exceptions\ApiBadParametersException;
|
||||
use Shaarli\Api\Exceptions\ApiLinkNotFoundException;
|
||||
use Shaarli\Api\Exceptions\ApiTagNotFoundException;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
/**
|
||||
* Class Tags
|
||||
*
|
||||
* REST API Controller: all services related to tags collection.
|
||||
*
|
||||
* @package Api\Controllers
|
||||
*/
|
||||
class Tags extends ApiController
|
||||
{
|
||||
/**
|
||||
* @var int Number of links returned if no limit is provided.
|
||||
*/
|
||||
public static $DEFAULT_LIMIT = 'all';
|
||||
|
||||
/**
|
||||
* Retrieve a list of tags, allowing different filters.
|
||||
*
|
||||
* @param Request $request Slim request.
|
||||
* @param Response $response Slim response.
|
||||
*
|
||||
* @return Response response.
|
||||
*
|
||||
* @throws ApiBadParametersException Invalid parameters.
|
||||
*/
|
||||
public function getTags($request, $response)
|
||||
{
|
||||
$visibility = $request->getParam('visibility');
|
||||
$tags = $this->linkDb->linksCountPerTag([], $visibility);
|
||||
|
||||
// Return tags from the {offset}th tag, starting from 0.
|
||||
$offset = $request->getParam('offset');
|
||||
if (! empty($offset) && ! ctype_digit($offset)) {
|
||||
throw new ApiBadParametersException('Invalid offset');
|
||||
}
|
||||
$offset = ! empty($offset) ? intval($offset) : 0;
|
||||
if ($offset > count($tags)) {
|
||||
return $response->withJson([], 200, $this->jsonStyle);
|
||||
}
|
||||
|
||||
// limit parameter is either a number of links or 'all' for everything.
|
||||
$limit = $request->getParam('limit');
|
||||
if (empty($limit)) {
|
||||
$limit = self::$DEFAULT_LIMIT;
|
||||
}
|
||||
if (ctype_digit($limit)) {
|
||||
$limit = intval($limit);
|
||||
} elseif ($limit === 'all') {
|
||||
$limit = count($tags);
|
||||
} else {
|
||||
throw new ApiBadParametersException('Invalid limit');
|
||||
}
|
||||
|
||||
$out = [];
|
||||
$index = 0;
|
||||
foreach ($tags as $tag => $occurrences) {
|
||||
if (count($out) >= $limit) {
|
||||
break;
|
||||
}
|
||||
if ($index++ >= $offset) {
|
||||
$out[] = ApiUtils::formatTag($tag, $occurrences);
|
||||
}
|
||||
}
|
||||
|
||||
return $response->withJson($out, 200, $this->jsonStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single formatted tag by its name.
|
||||
*
|
||||
* @param Request $request Slim request.
|
||||
* @param Response $response Slim response.
|
||||
* @param array $args Path parameters. including the tag name.
|
||||
*
|
||||
* @return Response containing the link array.
|
||||
*
|
||||
* @throws ApiTagNotFoundException generating a 404 error.
|
||||
*/
|
||||
public function getTag($request, $response, $args)
|
||||
{
|
||||
$tags = $this->linkDb->linksCountPerTag();
|
||||
if (!isset($tags[$args['tagName']])) {
|
||||
throw new ApiTagNotFoundException();
|
||||
}
|
||||
$out = ApiUtils::formatTag($args['tagName'], $tags[$args['tagName']]);
|
||||
|
||||
return $response->withJson($out, 200, $this->jsonStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a tag from the given name.
|
||||
* If the new name provided matches an existing tag, they will be merged.
|
||||
*
|
||||
* @param Request $request Slim request.
|
||||
* @param Response $response Slim response.
|
||||
* @param array $args Path parameters. including the tag name.
|
||||
*
|
||||
* @return Response response.
|
||||
*
|
||||
* @throws ApiTagNotFoundException generating a 404 error.
|
||||
* @throws ApiBadParametersException new tag name not provided
|
||||
*/
|
||||
public function putTag($request, $response, $args)
|
||||
{
|
||||
$tags = $this->linkDb->linksCountPerTag();
|
||||
if (! isset($tags[$args['tagName']])) {
|
||||
throw new ApiTagNotFoundException();
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
if (empty($data['name'])) {
|
||||
throw new ApiBadParametersException('New tag name is required in the request body');
|
||||
}
|
||||
|
||||
$updated = $this->linkDb->renameTag($args['tagName'], $data['name']);
|
||||
$this->linkDb->save($this->conf->get('resource.page_cache'));
|
||||
foreach ($updated as $link) {
|
||||
$this->history->updateLink($link);
|
||||
}
|
||||
|
||||
$tags = $this->linkDb->linksCountPerTag();
|
||||
$out = ApiUtils::formatTag($data['name'], $tags[$data['name']]);
|
||||
return $response->withJson($out, 200, $this->jsonStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing tag by its name.
|
||||
*
|
||||
* @param Request $request Slim request.
|
||||
* @param Response $response Slim response.
|
||||
* @param array $args Path parameters. including the tag name.
|
||||
*
|
||||
* @return Response response.
|
||||
*
|
||||
* @throws ApiTagNotFoundException generating a 404 error.
|
||||
*/
|
||||
public function deleteTag($request, $response, $args)
|
||||
{
|
||||
$tags = $this->linkDb->linksCountPerTag();
|
||||
if (! isset($tags[$args['tagName']])) {
|
||||
throw new ApiTagNotFoundException();
|
||||
}
|
||||
$updated = $this->linkDb->renameTag($args['tagName'], null);
|
||||
$this->linkDb->save($this->conf->get('resource.page_cache'));
|
||||
foreach ($updated as $link) {
|
||||
$this->history->updateLink($link);
|
||||
}
|
||||
|
||||
return $response->withStatus(204);
|
||||
}
|
||||
}
|
32
application/api/exceptions/ApiTagNotFoundException.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Shaarli\Api\Exceptions;
|
||||
|
||||
|
||||
use Slim\Http\Response;
|
||||
|
||||
/**
|
||||
* Class ApiTagNotFoundException
|
||||
*
|
||||
* Tag selected by name couldn't be found in the datastore, results in a 404 error.
|
||||
*
|
||||
* @package Shaarli\Api\Exceptions
|
||||
*/
|
||||
class ApiTagNotFoundException extends ApiException
|
||||
{
|
||||
/**
|
||||
* ApiLinkNotFoundException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->message = 'Tag not found';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getApiResponse()
|
||||
{
|
||||
return $this->buildApiResponse(404);
|
||||
}
|
||||
}
|
|
@ -123,7 +123,7 @@ public function get($setting, $default = '')
|
|||
* Supports nested settings with dot separated keys.
|
||||
*
|
||||
* @param string $setting Asked setting, keys separated with dots.
|
||||
* @param string $value Value to set.
|
||||
* @param mixed $value Value to set.
|
||||
* @param bool $write Write the new setting in the config file, default false.
|
||||
* @param bool $isLoggedIn User login state, default false.
|
||||
*
|
||||
|
@ -147,6 +147,33 @@ public function set($setting, $value, $write = false, $isLoggedIn = false)
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a config element from the config file.
|
||||
*
|
||||
* @param string $setting Asked setting, keys separated with dots.
|
||||
* @param bool $write Write the new setting in the config file, default false.
|
||||
* @param bool $isLoggedIn User login state, default false.
|
||||
*
|
||||
* @throws \Exception Invalid
|
||||
*/
|
||||
public function remove($setting, $write = false, $isLoggedIn = false)
|
||||
{
|
||||
if (empty($setting) || ! is_string($setting)) {
|
||||
throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
|
||||
}
|
||||
|
||||
// During the ConfigIO transition, map legacy settings to the new ones.
|
||||
if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
|
||||
$setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
|
||||
}
|
||||
|
||||
$settings = explode('.', $setting);
|
||||
self::removeConfig($settings, $this->loadedConfig);
|
||||
if ($write) {
|
||||
$this->write($isLoggedIn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a settings exists.
|
||||
*
|
||||
|
@ -272,7 +299,7 @@ protected static function getConfig($settings, $conf)
|
|||
*
|
||||
* @param array $settings Ordered array which contains keys to find.
|
||||
* @param mixed $value
|
||||
* @param array $conf Loaded settings, then sub-array.
|
||||
* @param array $conf Loaded settings, then sub-array.
|
||||
*
|
||||
* @return mixed Found setting or NOT_FOUND flag.
|
||||
*/
|
||||
|
@ -289,6 +316,27 @@ protected static function setConfig($settings, $value, &$conf)
|
|||
$conf[$setting] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive function which find asked setting in the loaded config and deletes it.
|
||||
*
|
||||
* @param array $settings Ordered array which contains keys to find.
|
||||
* @param array $conf Loaded settings, then sub-array.
|
||||
*
|
||||
* @return mixed Found setting or NOT_FOUND flag.
|
||||
*/
|
||||
protected static function removeConfig($settings, &$conf)
|
||||
{
|
||||
if (!is_array($settings) || count($settings) == 0) {
|
||||
return self::$NOT_FOUND;
|
||||
}
|
||||
|
||||
$setting = array_shift($settings);
|
||||
if (count($settings) > 0) {
|
||||
return self::removeConfig($settings, $conf[$setting]);
|
||||
}
|
||||
unset($conf[$setting]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a bunch of default values allowing Shaarli to start without a config file.
|
||||
*/
|
||||
|
@ -333,12 +381,12 @@ protected function setDefaultValues()
|
|||
// default state of the 'remember me' checkbox of the login form
|
||||
$this->setEmpty('privacy.remember_user_default', true);
|
||||
|
||||
$this->setEmpty('thumbnail.enable_thumbnails', true);
|
||||
$this->setEmpty('thumbnail.enable_localcache', true);
|
||||
|
||||
$this->setEmpty('redirector.url', '');
|
||||
$this->setEmpty('redirector.encode_url', true);
|
||||
|
||||
$this->setEmpty('thumbnails.width', '125');
|
||||
$this->setEmpty('thumbnails.height', '90');
|
||||
|
||||
$this->setEmpty('translation.language', 'auto');
|
||||
$this->setEmpty('translation.mode', 'php');
|
||||
$this->setEmpty('translation.extensions', []);
|
||||
|
|
265
application/security/LoginManager.php
Normal file
|
@ -0,0 +1,265 @@
|
|||
<?php
|
||||
namespace Shaarli\Security;
|
||||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
|
||||
/**
|
||||
* User login management
|
||||
*/
|
||||
class LoginManager
|
||||
{
|
||||
/** @var string Name of the cookie set after logging in **/
|
||||
public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
|
||||
|
||||
/** @var array A reference to the $_GLOBALS array */
|
||||
protected $globals = [];
|
||||
|
||||
/** @var ConfigManager Configuration Manager instance **/
|
||||
protected $configManager = null;
|
||||
|
||||
/** @var SessionManager Session Manager instance **/
|
||||
protected $sessionManager = null;
|
||||
|
||||
/** @var string Path to the file containing IP bans */
|
||||
protected $banFile = '';
|
||||
|
||||
/** @var bool Whether the user is logged in **/
|
||||
protected $isLoggedIn = false;
|
||||
|
||||
/** @var bool Whether the Shaarli instance is open to public edition **/
|
||||
protected $openShaarli = false;
|
||||
|
||||
/** @var string User sign-in token depending on remote IP and credentials */
|
||||
protected $staySignedInToken = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $globals The $GLOBALS array (reference)
|
||||
* @param ConfigManager $configManager Configuration Manager instance
|
||||
* @param SessionManager $sessionManager SessionManager instance
|
||||
*/
|
||||
public function __construct(& $globals, $configManager, $sessionManager)
|
||||
{
|
||||
$this->globals = &$globals;
|
||||
$this->configManager = $configManager;
|
||||
$this->sessionManager = $sessionManager;
|
||||
$this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php');
|
||||
$this->readBanFile();
|
||||
if ($this->configManager->get('security.open_shaarli') === true) {
|
||||
$this->openShaarli = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a token depending on deployment salt, user password and client IP
|
||||
*
|
||||
* @param string $clientIpAddress The remote client IP address
|
||||
*/
|
||||
public function generateStaySignedInToken($clientIpAddress)
|
||||
{
|
||||
$this->staySignedInToken = sha1(
|
||||
$this->configManager->get('credentials.hash')
|
||||
. $clientIpAddress
|
||||
. $this->configManager->get('credentials.salt')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user's client stay-signed-in token
|
||||
*
|
||||
* @return string User's client stay-signed-in token
|
||||
*/
|
||||
public function getStaySignedInToken()
|
||||
{
|
||||
return $this->staySignedInToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check user session state and validity (expiration)
|
||||
*
|
||||
* @param array $cookie The $_COOKIE array
|
||||
* @param string $clientIpId Client IP address identifier
|
||||
*/
|
||||
public function checkLoginState($cookie, $clientIpId)
|
||||
{
|
||||
if (! $this->configManager->exists('credentials.login')) {
|
||||
// Shaarli is not configured yet
|
||||
$this->isLoggedIn = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE])
|
||||
&& $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
|
||||
) {
|
||||
// The user client has a valid stay-signed-in cookie
|
||||
// Session information is updated with the current client information
|
||||
$this->sessionManager->storeLoginInfo($clientIpId);
|
||||
|
||||
} elseif ($this->sessionManager->hasSessionExpired()
|
||||
|| $this->sessionManager->hasClientIpChanged($clientIpId)
|
||||
) {
|
||||
$this->sessionManager->logout();
|
||||
$this->isLoggedIn = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isLoggedIn = true;
|
||||
$this->sessionManager->extendSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the user is currently logged in
|
||||
*
|
||||
* @return true when the user is logged in, false otherwise
|
||||
*/
|
||||
public function isLoggedIn()
|
||||
{
|
||||
if ($this->openShaarli) {
|
||||
return true;
|
||||
}
|
||||
return $this->isLoggedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check user credentials are valid
|
||||
*
|
||||
* @param string $remoteIp Remote client IP address
|
||||
* @param string $clientIpId Client IP address identifier
|
||||
* @param string $login Username
|
||||
* @param string $password Password
|
||||
*
|
||||
* @return bool true if the provided credentials are valid, false otherwise
|
||||
*/
|
||||
public function checkCredentials($remoteIp, $clientIpId, $login, $password)
|
||||
{
|
||||
$hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
|
||||
|
||||
if ($login != $this->configManager->get('credentials.login')
|
||||
|| $hash != $this->configManager->get('credentials.hash')
|
||||
) {
|
||||
logm(
|
||||
$this->configManager->get('resource.log'),
|
||||
$remoteIp,
|
||||
'Login failed for user ' . $login
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->sessionManager->storeLoginInfo($clientIpId);
|
||||
logm(
|
||||
$this->configManager->get('resource.log'),
|
||||
$remoteIp,
|
||||
'Login successful'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file containing banned IPs
|
||||
*/
|
||||
protected function readBanFile()
|
||||
{
|
||||
if (! file_exists($this->banFile)) {
|
||||
return;
|
||||
}
|
||||
include $this->banFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the banned IPs to a file
|
||||
*/
|
||||
protected function writeBanFile()
|
||||
{
|
||||
if (! array_key_exists('IPBANS', $this->globals)) {
|
||||
return;
|
||||
}
|
||||
file_put_contents(
|
||||
$this->banFile,
|
||||
"<?php\n\$GLOBALS['IPBANS']=" . var_export($this->globals['IPBANS'], true) . ";\n?>"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a failed login and ban the IP after too many failed attempts
|
||||
*
|
||||
* @param array $server The $_SERVER array
|
||||
*/
|
||||
public function handleFailedLogin($server)
|
||||
{
|
||||
$ip = $server['REMOTE_ADDR'];
|
||||
$trusted = $this->configManager->get('security.trusted_proxies', []);
|
||||
|
||||
if (in_array($ip, $trusted)) {
|
||||
$ip = getIpAddressFromProxy($server, $trusted);
|
||||
if (! $ip) {
|
||||
// the IP is behind a trusted forward proxy, but is not forwarded
|
||||
// in the HTTP headers, so we do nothing
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// increment the fail count for this IP
|
||||
if (isset($this->globals['IPBANS']['FAILURES'][$ip])) {
|
||||
$this->globals['IPBANS']['FAILURES'][$ip]++;
|
||||
} else {
|
||||
$this->globals['IPBANS']['FAILURES'][$ip] = 1;
|
||||
}
|
||||
|
||||
if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) {
|
||||
$this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800);
|
||||
logm(
|
||||
$this->configManager->get('resource.log'),
|
||||
$server['REMOTE_ADDR'],
|
||||
'IP address banned from login'
|
||||
);
|
||||
}
|
||||
$this->writeBanFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a successful login
|
||||
*
|
||||
* @param array $server The $_SERVER array
|
||||
*/
|
||||
public function handleSuccessfulLogin($server)
|
||||
{
|
||||
$ip = $server['REMOTE_ADDR'];
|
||||
// FIXME unban when behind a trusted proxy?
|
||||
|
||||
unset($this->globals['IPBANS']['FAILURES'][$ip]);
|
||||
unset($this->globals['IPBANS']['BANS'][$ip]);
|
||||
|
||||
$this->writeBanFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user can login from this IP
|
||||
*
|
||||
* @param array $server The $_SERVER array
|
||||
*
|
||||
* @return bool true if the user is allowed to login
|
||||
*/
|
||||
public function canLogin($server)
|
||||
{
|
||||
$ip = $server['REMOTE_ADDR'];
|
||||
|
||||
if (! isset($this->globals['IPBANS']['BANS'][$ip])) {
|
||||
// the user is not banned
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->globals['IPBANS']['BANS'][$ip] > time()) {
|
||||
// the user is still banned
|
||||
return false;
|
||||
}
|
||||
|
||||
// the ban has expired, the user can attempt to log in again
|
||||
logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.');
|
||||
unset($this->globals['IPBANS']['FAILURES'][$ip]);
|
||||
unset($this->globals['IPBANS']['BANS'][$ip]);
|
||||
|
||||
$this->writeBanFile();
|
||||
return true;
|
||||
}
|
||||
}
|
199
application/security/SessionManager.php
Normal file
|
@ -0,0 +1,199 @@
|
|||
<?php
|
||||
namespace Shaarli\Security;
|
||||
|
||||
use Shaarli\Config\ConfigManager;
|
||||
|
||||
/**
|
||||
* Manages the server-side session
|
||||
*/
|
||||
class SessionManager
|
||||
{
|
||||
/** @var int Session expiration timeout, in seconds */
|
||||
public static $SHORT_TIMEOUT = 3600; // 1 hour
|
||||
|
||||
/** @var int Session expiration timeout, in seconds */
|
||||
public static $LONG_TIMEOUT = 31536000; // 1 year
|
||||
|
||||
/** @var array Local reference to the global $_SESSION array */
|
||||
protected $session = [];
|
||||
|
||||
/** @var ConfigManager Configuration Manager instance **/
|
||||
protected $conf = null;
|
||||
|
||||
/** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
|
||||
protected $staySignedIn = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $session The $_SESSION array (reference)
|
||||
* @param ConfigManager $conf ConfigManager instance
|
||||
*/
|
||||
public function __construct(& $session, $conf)
|
||||
{
|
||||
$this->session = &$session;
|
||||
$this->conf = $conf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define whether the user should stay signed in across browser sessions
|
||||
*
|
||||
* @param bool $staySignedIn Keep the user signed in
|
||||
*/
|
||||
public function setStaySignedIn($staySignedIn)
|
||||
{
|
||||
$this->staySignedIn = $staySignedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a session token
|
||||
*
|
||||
* @return string token
|
||||
*/
|
||||
public function generateToken()
|
||||
{
|
||||
$token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
|
||||
$this->session['tokens'][$token] = 1;
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the validity of a session token, and destroys it afterwards
|
||||
*
|
||||
* @param string $token The token to check
|
||||
*
|
||||
* @return bool true if the token is valid, else false
|
||||
*/
|
||||
public function checkToken($token)
|
||||
{
|
||||
if (! isset($this->session['tokens'][$token])) {
|
||||
// the token is wrong, or has already been used
|
||||
return false;
|
||||
}
|
||||
|
||||
// destroy the token to prevent future use
|
||||
unset($this->session['tokens'][$token]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate session ID to prevent Full Path Disclosure.
|
||||
*
|
||||
* See #298.
|
||||
* The session ID's format depends on the hash algorithm set in PHP settings
|
||||
*
|
||||
* @param string $sessionId Session ID
|
||||
*
|
||||
* @return true if valid, false otherwise.
|
||||
*
|
||||
* @see http://php.net/manual/en/function.hash-algos.php
|
||||
* @see http://php.net/manual/en/session.configuration.php
|
||||
*/
|
||||
public static function checkId($sessionId)
|
||||
{
|
||||
if (empty($sessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$sessionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store user login information after a successful login
|
||||
*
|
||||
* @param string $clientIpId Client IP address identifier
|
||||
*/
|
||||
public function storeLoginInfo($clientIpId)
|
||||
{
|
||||
$this->session['ip'] = $clientIpId;
|
||||
$this->session['username'] = $this->conf->get('credentials.login');
|
||||
$this->extendTimeValidityBy(self::$SHORT_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend session validity
|
||||
*/
|
||||
public function extendSession()
|
||||
{
|
||||
if ($this->staySignedIn) {
|
||||
return $this->extendTimeValidityBy(self::$LONG_TIMEOUT);
|
||||
}
|
||||
return $this->extendTimeValidityBy(self::$SHORT_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend expiration time
|
||||
*
|
||||
* @param int $duration Expiration time extension (seconds)
|
||||
*
|
||||
* @return int New session expiration time
|
||||
*/
|
||||
protected function extendTimeValidityBy($duration)
|
||||
{
|
||||
$expirationTime = time() + $duration;
|
||||
$this->session['expires_on'] = $expirationTime;
|
||||
return $expirationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout a user by unsetting all login information
|
||||
*
|
||||
* See:
|
||||
* - https://secure.php.net/manual/en/function.setcookie.php
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
if (isset($this->session)) {
|
||||
unset($this->session['ip']);
|
||||
unset($this->session['expires_on']);
|
||||
unset($this->session['username']);
|
||||
unset($this->session['visibility']);
|
||||
unset($this->session['untaggedonly']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the session has expired
|
||||
*
|
||||
* @param string $clientIpId Client IP address identifier
|
||||
*
|
||||
* @return bool true if the session has expired, false otherwise
|
||||
*/
|
||||
public function hasSessionExpired()
|
||||
{
|
||||
if (empty($this->session['expires_on'])) {
|
||||
return true;
|
||||
}
|
||||
if (time() >= $this->session['expires_on']) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the client IP address has changed
|
||||
*
|
||||
* @param string $clientIpId Client IP address identifier
|
||||
*
|
||||
* @return bool true if the IP has changed, false if it has not, or
|
||||
* if session protection has been disabled
|
||||
*/
|
||||
public function hasClientIpChanged($clientIpId)
|
||||
{
|
||||
if ($this->conf->get('security.session_protection_disabled') === true) {
|
||||
return false;
|
||||
}
|
||||
if (isset($this->session['ip']) && $this->session['ip'] === $clientIpId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
13
assets/.htaccess
Normal file
|
@ -0,0 +1,13 @@
|
|||
<IfModule version_module>
|
||||
<IfVersion >= 2.4>
|
||||
Require all denied
|
||||
</IfVersion>
|
||||
<IfVersion < 2.4>
|
||||
Allow from none
|
||||
Deny from all
|
||||
</IfVersion>
|
||||
</IfModule>
|
||||
|
||||
<IfModule !version_module>
|
||||
Require all denied
|
||||
</IfModule>
|
51
assets/common/js/thumbnails-update.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Script used in the thumbnails update page.
|
||||
*
|
||||
* It retrieves the list of link IDs to update, and execute AJAX requests
|
||||
* to update their thumbnails, while updating the progress bar.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update the thumbnail of the link with the current i index in ids.
|
||||
* It contains a recursive call to retrieve the thumb of the next link when it succeed.
|
||||
* It also update the progress bar and other visual feedback elements.
|
||||
*
|
||||
* @param {array} ids List of LinkID to update
|
||||
* @param {int} i Current index in ids
|
||||
* @param {object} elements List of DOM element to avoid retrieving them at each iteration
|
||||
*/
|
||||
function updateThumb(ids, i, elements) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '?do=ajax_thumb_update');
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = () => {
|
||||
if (xhr.status !== 200) {
|
||||
alert(`An error occurred. Return code: ${xhr.status}`);
|
||||
} else {
|
||||
const { response } = xhr;
|
||||
i += 1;
|
||||
elements.progressBar.style.width = `${(i * 100) / ids.length}%`;
|
||||
elements.current.innerHTML = i;
|
||||
elements.title.innerHTML = response.title;
|
||||
if (response.thumbnail !== false) {
|
||||
elements.thumbnail.innerHTML = `<img src="${response.thumbnail}">`;
|
||||
}
|
||||
if (i < ids.length) {
|
||||
updateThumb(ids, i, elements);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(`id=${ids[i]}`);
|
||||
}
|
||||
|
||||
(() => {
|
||||
const ids = document.getElementsByName('ids')[0].value.split(',');
|
||||
const elements = {
|
||||
progressBar: document.querySelector('.progressbar > div'),
|
||||
current: document.querySelector('.progress-current'),
|
||||
thumbnail: document.querySelector('.thumbnail-placeholder'),
|
||||
title: document.querySelector('.thumbnail-link-title'),
|
||||
};
|
||||
updateThumb(ids, 0, elements);
|
||||
})();
|
7
assets/common/js/thumbnails.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Blazy from 'blazy';
|
||||
|
||||
(() => {
|
||||
// Suppress ESLint error because that's how bLazy works
|
||||
/* eslint-disable no-new */
|
||||
new Blazy();
|
||||
})();
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 530 B After Width: | Height: | Size: 530 B |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
573
assets/default/js/base.js
Normal file
|
@ -0,0 +1,573 @@
|
|||
import Awesomplete from 'awesomplete';
|
||||
|
||||
/**
|
||||
* Find a parent element according to its tag and its attributes
|
||||
*
|
||||
* @param element Element where to start the search
|
||||
* @param tagName Expected parent tag name
|
||||
* @param attributes Associative array of expected attributes (name=>value).
|
||||
*
|
||||
* @returns Found element or null.
|
||||
*/
|
||||
function findParent(element, tagName, attributes) {
|
||||
const parentMatch = key => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1;
|
||||
while (element) {
|
||||
if (element.tagName.toLowerCase() === tagName) {
|
||||
if (Object.keys(attributes).find(parentMatch)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax request to refresh the CSRF token.
|
||||
*/
|
||||
function refreshToken() {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '?do=token');
|
||||
xhr.onload = () => {
|
||||
const token = document.getElementById('token');
|
||||
token.setAttribute('value', xhr.responseText);
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function createAwesompleteInstance(element, tags = []) {
|
||||
const awesome = new Awesomplete(Awesomplete.$(element));
|
||||
// Tags are separated by a space
|
||||
awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]);
|
||||
// Insert new selected tag in the input
|
||||
awesome.replace = (text) => {
|
||||
const before = awesome.input.value.match(/^.+ \s*|/)[0];
|
||||
awesome.input.value = `${before}${text} `;
|
||||
};
|
||||
// Highlight found items
|
||||
awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(/[^ ]*$/)[0]);
|
||||
// Don't display already selected items
|
||||
const reg = /(\w+) /g;
|
||||
let match;
|
||||
awesome.data = (item, input) => {
|
||||
while ((match = reg.exec(input))) {
|
||||
if (item === match[1]) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return item;
|
||||
};
|
||||
awesome.minChars = 1;
|
||||
if (tags.length) {
|
||||
awesome.list = tags;
|
||||
}
|
||||
|
||||
return awesome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update awesomplete list of tag for all elements matching the given selector
|
||||
*
|
||||
* @param selector CSS selector
|
||||
* @param tags Array of tags
|
||||
* @param instances List of existing awesomplete instances
|
||||
*/
|
||||
function updateAwesompleteList(selector, tags, instances) {
|
||||
if (instances.length === 0) {
|
||||
// First load: create Awesomplete instances
|
||||
const elements = document.querySelectorAll(selector);
|
||||
[...elements].forEach((element) => {
|
||||
instances.push(createAwesompleteInstance(element, tags));
|
||||
});
|
||||
} else {
|
||||
// Update awesomplete tag list
|
||||
instances.map((item) => {
|
||||
item.list = tags;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* html_entities in JS
|
||||
*
|
||||
* @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
|
||||
*/
|
||||
function htmlEntities(str) {
|
||||
return str.replace(/[\u00A0-\u9999<>&]/gim, i => `&#${i.charCodeAt(0)};`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the class 'hidden' to city options not attached to the current selected continent.
|
||||
*
|
||||
* @param cities List of <option> elements
|
||||
* @param currentContinent Current selected continent
|
||||
* @param reset Set to true to reset the selected value
|
||||
*/
|
||||
function hideTimezoneCities(cities, currentContinent, reset = null) {
|
||||
let first = true;
|
||||
if (reset == null) {
|
||||
reset = false;
|
||||
}
|
||||
[...cities].forEach((option) => {
|
||||
if (option.getAttribute('data-continent') !== currentContinent) {
|
||||
option.className = 'hidden';
|
||||
} else {
|
||||
option.className = '';
|
||||
if (reset === true && first === true) {
|
||||
option.setAttribute('selected', 'selected');
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an element up in the tree from its class name.
|
||||
*/
|
||||
function getParentByClass(el, className) {
|
||||
const p = el.parentNode;
|
||||
if (p == null || p.classList.contains(className)) {
|
||||
return p;
|
||||
}
|
||||
return getParentByClass(p, className);
|
||||
}
|
||||
|
||||
function toggleHorizontal() {
|
||||
[...document.getElementById('shaarli-menu').querySelectorAll('.menu-transform')].forEach((el) => {
|
||||
el.classList.toggle('pure-menu-horizontal');
|
||||
});
|
||||
}
|
||||
|
||||
function toggleMenu(menu) {
|
||||
// set timeout so that the panel has a chance to roll up
|
||||
// before the menu switches states
|
||||
if (menu.classList.contains('open')) {
|
||||
setTimeout(toggleHorizontal, 500);
|
||||
} else {
|
||||
toggleHorizontal();
|
||||
}
|
||||
menu.classList.toggle('open');
|
||||
document.getElementById('menu-toggle').classList.toggle('x');
|
||||
}
|
||||
|
||||
function closeMenu(menu) {
|
||||
if (menu.classList.contains('open')) {
|
||||
toggleMenu(menu);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFold(button, description, thumb) {
|
||||
// Switch fold/expand - up = fold
|
||||
if (button.classList.contains('fa-chevron-up')) {
|
||||
button.title = document.getElementById('translation-expand').innerHTML;
|
||||
if (description != null) {
|
||||
description.style.display = 'none';
|
||||
}
|
||||
if (thumb != null) {
|
||||
thumb.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
button.title = document.getElementById('translation-fold').innerHTML;
|
||||
if (description != null) {
|
||||
description.style.display = 'block';
|
||||
}
|
||||
if (thumb != null) {
|
||||
thumb.style.display = 'block';
|
||||
}
|
||||
}
|
||||
button.classList.toggle('fa-chevron-down');
|
||||
button.classList.toggle('fa-chevron-up');
|
||||
}
|
||||
|
||||
function removeClass(element, classname) {
|
||||
element.className = element.className.replace(new RegExp(`(?:^|\\s)${classname}(?:\\s|$)`), ' ');
|
||||
}
|
||||
|
||||
function init(description) {
|
||||
function resize() {
|
||||
/* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
|
||||
const scrollTop = window.pageYOffset ||
|
||||
(document.documentElement || document.body.parentNode || document.body).scrollTop;
|
||||
|
||||
description.style.height = 'auto';
|
||||
description.style.height = `${description.scrollHeight + 10}px`;
|
||||
|
||||
window.scrollTo(0, scrollTop);
|
||||
}
|
||||
|
||||
/* 0-timeout to get the already changed text */
|
||||
function delayedResize() {
|
||||
window.setTimeout(resize, 0);
|
||||
}
|
||||
|
||||
const observe = (element, event, handler) => {
|
||||
element.addEventListener(event, handler, false);
|
||||
};
|
||||
observe(description, 'change', resize);
|
||||
observe(description, 'cut', delayedResize);
|
||||
observe(description, 'paste', delayedResize);
|
||||
observe(description, 'drop', delayedResize);
|
||||
observe(description, 'keydown', delayedResize);
|
||||
|
||||
resize();
|
||||
}
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* Handle responsive menu.
|
||||
* Source: http://purecss.io/layouts/tucked-menu-vertical/
|
||||
*/
|
||||
const menu = document.getElementById('shaarli-menu');
|
||||
const WINDOW_CHANGE_EVENT = ('onorientationchange' in window) ? 'orientationchange' : 'resize';
|
||||
|
||||
const menuToggle = document.getElementById('menu-toggle');
|
||||
if (menuToggle != null) {
|
||||
menuToggle.addEventListener('click', () => toggleMenu(menu));
|
||||
}
|
||||
|
||||
window.addEventListener(WINDOW_CHANGE_EVENT, () => closeMenu(menu));
|
||||
|
||||
/**
|
||||
* Fold/Expand shaares description and thumbnail.
|
||||
*/
|
||||
const foldAllButtons = document.getElementsByClassName('fold-all');
|
||||
const foldButtons = document.getElementsByClassName('fold-button');
|
||||
|
||||
[...foldButtons].forEach((foldButton) => {
|
||||
// Retrieve description
|
||||
let description = null;
|
||||
let thumbnail = null;
|
||||
const linklistItem = getParentByClass(foldButton, 'linklist-item');
|
||||
if (linklistItem != null) {
|
||||
description = linklistItem.querySelector('.linklist-item-description');
|
||||
thumbnail = linklistItem.querySelector('.linklist-item-thumbnail');
|
||||
if (description != null || thumbnail != null) {
|
||||
foldButton.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
foldButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
toggleFold(event.target, description, thumbnail);
|
||||
});
|
||||
});
|
||||
|
||||
if (foldAllButtons != null) {
|
||||
[].forEach.call(foldAllButtons, (foldAllButton) => {
|
||||
foldAllButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const state = foldAllButton.firstElementChild.getAttribute('class').indexOf('down') !== -1 ? 'down' : 'up';
|
||||
[].forEach.call(foldButtons, (foldButton) => {
|
||||
if ((foldButton.firstElementChild.classList.contains('fa-chevron-up') && state === 'down')
|
||||
|| (foldButton.firstElementChild.classList.contains('fa-chevron-down') && state === 'up')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Retrieve description
|
||||
let description = null;
|
||||
let thumbnail = null;
|
||||
const linklistItem = getParentByClass(foldButton, 'linklist-item');
|
||||
if (linklistItem != null) {
|
||||
description = linklistItem.querySelector('.linklist-item-description');
|
||||
thumbnail = linklistItem.querySelector('.linklist-item-thumbnail');
|
||||
if (description != null || thumbnail != null) {
|
||||
foldButton.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
toggleFold(foldButton.firstElementChild, description, thumbnail);
|
||||
});
|
||||
foldAllButton.firstElementChild.classList.toggle('fa-chevron-down');
|
||||
foldAllButton.firstElementChild.classList.toggle('fa-chevron-up');
|
||||
foldAllButton.title = state === 'down'
|
||||
? document.getElementById('translation-fold-all').innerHTML
|
||||
: document.getElementById('translation-expand-all').innerHTML;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation message before deletion.
|
||||
*/
|
||||
const deleteLinks = document.querySelectorAll('.confirm-delete');
|
||||
[...deleteLinks].forEach((deleteLink) => {
|
||||
deleteLink.addEventListener('click', (event) => {
|
||||
if (!confirm(document.getElementById('translation-delete-link').innerHTML)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Close alerts
|
||||
*/
|
||||
const closeLinks = document.querySelectorAll('.pure-alert-close');
|
||||
[...closeLinks].forEach((closeLink) => {
|
||||
closeLink.addEventListener('click', (event) => {
|
||||
const alert = getParentByClass(event.target, 'pure-alert-closable');
|
||||
alert.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* New version dismiss.
|
||||
* Hide the message for one week using localStorage.
|
||||
*/
|
||||
const newVersionDismiss = document.getElementById('new-version-dismiss');
|
||||
const newVersionMessage = document.querySelector('.new-version-message');
|
||||
if (newVersionMessage != null
|
||||
&& localStorage.getItem('newVersionDismiss') != null
|
||||
&& parseInt(localStorage.getItem('newVersionDismiss'), 10) + (7 * 24 * 60 * 60 * 1000) > (new Date()).getTime()
|
||||
) {
|
||||
newVersionMessage.style.display = 'none';
|
||||
}
|
||||
if (newVersionDismiss != null) {
|
||||
newVersionDismiss.addEventListener('click', () => {
|
||||
localStorage.setItem('newVersionDismiss', (new Date()).getTime().toString());
|
||||
});
|
||||
}
|
||||
|
||||
const hiddenReturnurl = document.getElementsByName('returnurl');
|
||||
if (hiddenReturnurl != null) {
|
||||
hiddenReturnurl.value = window.location.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofocus text fields
|
||||
*/
|
||||
const autofocusElements = document.querySelectorAll('.autofocus');
|
||||
let breakLoop = false;
|
||||
[].forEach.call(autofocusElements, (autofocusElement) => {
|
||||
if (autofocusElement.value === '' && !breakLoop) {
|
||||
autofocusElement.focus();
|
||||
breakLoop = true;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle sub menus/forms
|
||||
*/
|
||||
const openers = document.getElementsByClassName('subheader-opener');
|
||||
if (openers != null) {
|
||||
[...openers].forEach((opener) => {
|
||||
opener.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const id = opener.getAttribute('data-open-id');
|
||||
const sub = document.getElementById(id);
|
||||
|
||||
if (sub != null) {
|
||||
[...document.getElementsByClassName('subheader-form')].forEach((element) => {
|
||||
if (element !== sub) {
|
||||
removeClass(element, 'open');
|
||||
}
|
||||
});
|
||||
|
||||
sub.classList.toggle('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove CSS target padding (for fixed bar)
|
||||
*/
|
||||
if (location.hash !== '') {
|
||||
const anchor = document.getElementById(location.hash.substr(1));
|
||||
if (anchor != null) {
|
||||
const padsize = anchor.clientHeight;
|
||||
window.scroll(0, window.scrollY - padsize);
|
||||
anchor.style.paddingTop = '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text area resizer
|
||||
*/
|
||||
const description = document.getElementById('lf_description');
|
||||
|
||||
if (description != null) {
|
||||
init(description);
|
||||
// Submit editlink form with CTRL + Enter in the text area.
|
||||
description.addEventListener('keydown', (event) => {
|
||||
if (event.ctrlKey && event.keyCode === 13) {
|
||||
document.getElementById('button-save-edit').click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmarklet alert
|
||||
*/
|
||||
const bookmarkletLinks = document.querySelectorAll('.bookmarklet-link');
|
||||
const bkmMessage = document.getElementById('bookmarklet-alert');
|
||||
[].forEach.call(bookmarkletLinks, (link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
alert(bkmMessage.value);
|
||||
});
|
||||
});
|
||||
|
||||
const continent = document.getElementById('continent');
|
||||
const city = document.getElementById('city');
|
||||
if (continent != null && city != null) {
|
||||
continent.addEventListener('change', () => {
|
||||
hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
|
||||
});
|
||||
hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk actions
|
||||
*/
|
||||
const linkCheckboxes = document.querySelectorAll('.delete-checkbox');
|
||||
const bar = document.getElementById('actions');
|
||||
[...linkCheckboxes].forEach((checkbox) => {
|
||||
checkbox.style.display = 'inline-block';
|
||||
checkbox.addEventListener('click', () => {
|
||||
const linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
|
||||
const count = [...linkCheckedCheckboxes].length;
|
||||
if (count === 0 && bar.classList.contains('open')) {
|
||||
bar.classList.toggle('open');
|
||||
} else if (count > 0 && !bar.classList.contains('open')) {
|
||||
bar.classList.toggle('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deleteButton = document.getElementById('actions-delete');
|
||||
const token = document.getElementById('token');
|
||||
if (deleteButton != null && token != null) {
|
||||
deleteButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const links = [];
|
||||
const linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
|
||||
[...linkCheckedCheckboxes].forEach((checkbox) => {
|
||||
links.push({
|
||||
id: checkbox.value,
|
||||
title: document.querySelector(`.linklist-item[data-id="${checkbox.value}"] .linklist-link`).innerHTML,
|
||||
});
|
||||
});
|
||||
|
||||
let message = `Are you sure you want to delete ${links.length} links?\n`;
|
||||
message += 'This action is IRREVERSIBLE!\n\nTitles:\n';
|
||||
const ids = [];
|
||||
links.forEach((item) => {
|
||||
message += ` - ${item.title}\n`;
|
||||
ids.push(item.id);
|
||||
});
|
||||
|
||||
if (window.confirm(message)) {
|
||||
window.location = `?delete_link&lf_linkdate=${ids.join('+')}&token=${token.value}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag list operations
|
||||
*
|
||||
* TODO: support error code in the backend for AJAX requests
|
||||
*/
|
||||
const tagList = document.querySelector('input[name="taglist"]');
|
||||
let existingTags = tagList ? tagList.value.split(' ') : [];
|
||||
let awesomepletes = [];
|
||||
|
||||
// Display/Hide rename form
|
||||
const renameTagButtons = document.querySelectorAll('.rename-tag');
|
||||
[...renameTagButtons].forEach((rename) => {
|
||||
rename.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
|
||||
const form = block.querySelector('.rename-tag-form');
|
||||
if (form.style.display === 'none' || form.style.display === '') {
|
||||
form.style.display = 'block';
|
||||
} else {
|
||||
form.style.display = 'none';
|
||||
}
|
||||
block.querySelector('input').focus();
|
||||
});
|
||||
});
|
||||
|
||||
// Rename a tag with an AJAX request
|
||||
const renameTagSubmits = document.querySelectorAll('.validate-rename-tag');
|
||||
[...renameTagSubmits].forEach((rename) => {
|
||||
rename.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
|
||||
const input = block.querySelector('.rename-tag-input');
|
||||
const totag = input.value.replace('/"/g', '\\"');
|
||||
if (totag.trim() === '') {
|
||||
return;
|
||||
}
|
||||
const refreshedToken = document.getElementById('token').value;
|
||||
const fromtag = block.getAttribute('data-tag');
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '?do=changetag');
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = () => {
|
||||
if (xhr.status !== 200) {
|
||||
alert(`An error occurred. Return code: ${xhr.status}`);
|
||||
location.reload();
|
||||
} else {
|
||||
block.setAttribute('data-tag', totag);
|
||||
input.setAttribute('name', totag);
|
||||
input.setAttribute('value', totag);
|
||||
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
|
||||
block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
|
||||
block.querySelector('a.tag-link').setAttribute('href', `?searchtags=${encodeURIComponent(totag)}`);
|
||||
block.querySelector('a.rename-tag').setAttribute('href', `?do=changetag&fromtag=${encodeURIComponent(totag)}`);
|
||||
|
||||
// Refresh awesomplete values
|
||||
existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag));
|
||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
|
||||
}
|
||||
};
|
||||
xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
|
||||
refreshToken();
|
||||
});
|
||||
});
|
||||
|
||||
// Validate input with enter key
|
||||
const renameTagInputs = document.querySelectorAll('.rename-tag-input');
|
||||
[...renameTagInputs].forEach((rename) => {
|
||||
rename.addEventListener('keypress', (event) => {
|
||||
if (event.keyCode === 13) { // enter
|
||||
findParent(event.target, 'div', { class: 'tag-list-item' }).querySelector('.validate-rename-tag').click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete a tag with an AJAX query (alert popup confirmation)
|
||||
const deleteTagButtons = document.querySelectorAll('.delete-tag');
|
||||
[...deleteTagButtons].forEach((rename) => {
|
||||
rename.style.display = 'inline';
|
||||
rename.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
|
||||
const tag = block.getAttribute('data-tag');
|
||||
const refreshedToken = document.getElementById('token');
|
||||
|
||||
if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '?do=changetag');
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = () => {
|
||||
block.remove();
|
||||
};
|
||||
xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`));
|
||||
refreshToken();
|
||||
|
||||
existingTags = existingTags.filter(tagItem => tagItem !== tag);
|
||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const autocompleteFields = document.querySelectorAll('input[data-multiple]');
|
||||
[...autocompleteFields].forEach((autocompleteField) => {
|
||||
awesomepletes.push(createAwesompleteInstance(autocompleteField));
|
||||
});
|
||||
})();
|
81
assets/default/js/plugins-admin.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Change the position counter of a row.
|
||||
*
|
||||
* @param elem Element Node to change.
|
||||
* @param toPos int New position.
|
||||
*/
|
||||
function changePos(elem, toPos) {
|
||||
const elemName = elem.getAttribute('data-line');
|
||||
elem.setAttribute('data-order', toPos);
|
||||
const hiddenInput = document.querySelector(`[name="order_${elemName}"]`);
|
||||
hiddenInput.setAttribute('value', toPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a row up or down.
|
||||
*
|
||||
* @param pos Element Node to move.
|
||||
* @param move int Move: +1 (down) or -1 (up)
|
||||
*/
|
||||
function changeOrder(pos, move) {
|
||||
const newpos = parseInt(pos, 10) + move;
|
||||
let lines = document.querySelectorAll(`[data-order="${pos}"]`);
|
||||
const changelines = document.querySelectorAll(`[data-order="${newpos}"]`);
|
||||
|
||||
// If we go down reverse lines to preserve the rows order
|
||||
if (move > 0) {
|
||||
lines = [].slice.call(lines).reverse();
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const parent = changelines[0].parentNode;
|
||||
changePos(lines[i], newpos);
|
||||
changePos(changelines[i], parseInt(pos, 10));
|
||||
const changeItem = move < 0 ? changelines[0] : changelines[changelines.length - 1].nextSibling;
|
||||
parent.insertBefore(lines[i], changeItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a row up in the table.
|
||||
*
|
||||
* @param pos int row counter.
|
||||
*
|
||||
* @return false
|
||||
*/
|
||||
function orderUp(pos) {
|
||||
if (pos !== 0) {
|
||||
changeOrder(pos, -1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a row down in the table.
|
||||
*
|
||||
* @param pos int row counter.
|
||||
*
|
||||
* @returns false
|
||||
*/
|
||||
function orderDown(pos) {
|
||||
const lastpos = parseInt(document.querySelector('[data-order]:last-child').getAttribute('data-order'), 10);
|
||||
if (pos !== lastpos) {
|
||||
changeOrder(pos, 1);
|
||||
}
|
||||
}
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* Plugin admin order
|
||||
*/
|
||||
const orderPA = document.querySelectorAll('.order');
|
||||
[...orderPA].forEach((link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
if (event.target.classList.contains('order-up')) {
|
||||
orderUp(parseInt(event.target.parentNode.parentNode.getAttribute('data-order'), 10));
|
||||
} else if (event.target.classList.contains('order-down')) {
|
||||
orderDown(parseInt(event.target.parentNode.parentNode.getAttribute('data-order'), 10));
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
1592
assets/default/scss/shaarli.scss
Normal file
|
@ -113,7 +113,7 @@ a.bigbutton, #pageheader a.bigbutton {
|
|||
}
|
||||
|
||||
#pageheader #logo {
|
||||
background-image: url('../../../images/logo.png');
|
||||
background-image: url('../img/logo.png');
|
||||
background-repeat: no-repeat;
|
||||
float: left;
|
||||
margin: 0 10px 0 10px;
|
||||
|
@ -433,7 +433,7 @@ a.bigbutton, #pageheader a.bigbutton {
|
|||
}
|
||||
|
||||
#linklist li.private {
|
||||
background: url('../images/private.png') no-repeat 4px center;
|
||||
background: url('../img/private.png') no-repeat 4px center;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
|
@ -465,7 +465,7 @@ a.bigbutton, #pageheader a.bigbutton {
|
|||
}
|
||||
|
||||
.linkdate a {
|
||||
background-image: url('../images/calendar.png');
|
||||
background-image: url('../img/calendar.png');
|
||||
padding: 2px 0 3px 20px;
|
||||
background-repeat: no-repeat;
|
||||
text-decoration: none;
|
||||
|
@ -516,7 +516,7 @@ a.bigbutton, #pageheader a.bigbutton {
|
|||
height: 20px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
background-image: url('../images/tag_blue.png');
|
||||
background-image: url('../img/tag_blue.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 3px center;
|
||||
background-color: #ffffff;
|
||||
|
@ -701,8 +701,8 @@ a.bigbutton, #pageheader a.bigbutton {
|
|||
position: relative;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
float: left;
|
||||
|
@ -739,9 +739,9 @@ a.bigbutton, #pageheader a.bigbutton {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 90px;
|
||||
width: 120px;
|
||||
font-weight: bold;
|
||||
font-size: 8pt;
|
||||
font-size: 9pt;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
|
@ -762,7 +762,7 @@ div.daily {
|
|||
/* Background paper texture by BashCorpo:
|
||||
http://www.bashcorpo.dk/textures.php
|
||||
http://bashcorpo.deviantart.com/art/Grungy-paper-texture-v-5-22966998 */
|
||||
background-image: url("../images/Paper_texture_v5_by_bashcorpo_w1000.jpg");
|
||||
background-image: url("../img/Paper_texture_v5_by_bashcorpo_w1000.jpg");
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
|
@ -860,7 +860,7 @@ div.dailyEntryThumbnail {
|
|||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: rgb(128, 128, 128);
|
||||
background: url(../images/50pc_transparent.png);
|
||||
background: url(../img/50pc_transparent.png);
|
||||
padding: 4px 0px 2px 0px;
|
||||
}
|
||||
|
||||
|
@ -1210,3 +1210,43 @@ ul.errors {
|
|||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.thumbnails-update-container {
|
||||
padding: 20px 0;
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.thumbnails-update-container .thumbnail-placeholder {
|
||||
background: grey;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.thumbnails-update-container .thumbnail-link-title {
|
||||
width: 75%;
|
||||
margin: auto;
|
||||
|
||||
padding-bottom: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progressbar {
|
||||
border-radius: 6px;
|
||||
background-color: #111;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.progressbar > div {
|
||||
border-radius: 10px;
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#f5f5f5,
|
||||
#f5f5f5 6px,
|
||||
#d0d0d0 6px,
|
||||
#d0d0d0 12px
|
||||
);
|
||||
width: 0%;
|
||||
height: 10px;
|
||||
}
|
Before Width: | Height: | Size: 599 B After Width: | Height: | Size: 599 B |
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 650 B After Width: | Height: | Size: 650 B |
Before Width: | Height: | Size: 302 B After Width: | Height: | Size: 302 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 684 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 658 B After Width: | Height: | Size: 658 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 932 B |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 369 B After Width: | Height: | Size: 369 B |
Before Width: | Height: | Size: 813 B After Width: | Height: | Size: 813 B |
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 679 B |
Before Width: | Height: | Size: 648 B After Width: | Height: | Size: 648 B |
Before Width: | Height: | Size: 720 B After Width: | Height: | Size: 720 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 714 B |
30
assets/vintage/js/base.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import Awesomplete from 'awesomplete';
|
||||
import 'awesomplete/awesomplete.css';
|
||||
|
||||
(() => {
|
||||
const awp = Awesomplete.$;
|
||||
const autocompleteFields = document.querySelectorAll('input[data-multiple]');
|
||||
[...autocompleteFields].forEach((autocompleteField) => {
|
||||
const awesomplete = new Awesomplete(awp(autocompleteField));
|
||||
awesomplete.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]);
|
||||
awesomplete.replace = (text) => {
|
||||
const before = awesomplete.input.value.match(/^.+ \s*|/)[0];
|
||||
awesomplete.input.value = `${before}${text} `;
|
||||
};
|
||||
awesomplete.minChars = 1;
|
||||
|
||||
autocompleteField.addEventListener('input', () => {
|
||||
const proposedTags = autocompleteField.getAttribute('data-list').replace(/,/g, '').split(' ');
|
||||
const reg = /(\w+) /g;
|
||||
let match;
|
||||
while ((match = reg.exec(autocompleteField.value)) !== null) {
|
||||
const id = proposedTags.indexOf(match[1]);
|
||||
if (id !== -1) {
|
||||
proposedTags.splice(id, 1);
|
||||
}
|
||||
}
|
||||
|
||||
awesomplete.list = proposedTags;
|
||||
});
|
||||
});
|
||||
})();
|
|
@ -11,20 +11,21 @@
|
|||
"keywords": ["bookmark", "link", "share", "web"],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "5.5.38"
|
||||
"php": "5.6.31"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5",
|
||||
"php": ">=5.6",
|
||||
"shaarli/netscape-bookmark-parser": "^2.0",
|
||||
"erusev/parsedown": "1.6",
|
||||
"erusev/parsedown": "^1.6",
|
||||
"slim/slim": "^3.0",
|
||||
"arthurhoaro/web-thumbnailer": "^1.1",
|
||||
"pubsubhubbub/publisher": "dev-master",
|
||||
"gettext/gettext": "^4.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpmd/phpmd" : "@stable",
|
||||
"phpunit/phpunit": "4.8.*",
|
||||
"phpunit/phpunit": "^5.0",
|
||||
"sebastian/phpcpd": "*",
|
||||
"squizlabs/php_codesniffer": "2.*",
|
||||
"phpunit/phpcov": "*"
|
||||
|
@ -36,7 +37,8 @@
|
|||
"Shaarli\\Api\\Controllers\\": "application/api/controllers",
|
||||
"Shaarli\\Api\\Exceptions\\": "application/api/exceptions",
|
||||
"Shaarli\\Config\\": "application/config/",
|
||||
"Shaarli\\Config\\Exception\\": "application/config/exception"
|
||||
"Shaarli\\Config\\Exception\\": "application/config/exception",
|
||||
"Shaarli\\Security\\": "application/security"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
852
composer.lock
generated
|
@ -1,13 +1,21 @@
|
|||
## CSS
|
||||
- Yahoo UI [CSS Reset](http://yuilibrary.com/yui/docs/cssreset/)
|
||||
- resets default CSS properties for all HTML elements (overriding browsers' default values)
|
||||
- ensures custom CSS stylessheets will provide the same results on all browsers
|
||||
|
||||
- Yahoo UI [CSS Reset](http://yuilibrary.com/yui/docs/cssreset/) - standardize cross-browser rendering
|
||||
|
||||
## Javascript
|
||||
|
||||
- [Awesomeplete](https://leaverou.github.io/awesomplete/) ([GitHub](https://github.com/LeaVerou/awesomplete)) - autocompletion in input forms
|
||||
- [bLazy](http://dinbror.dk/blazy/) ([GitHub](https://github.com/dinbror/blazy)) - lazy loading for thumbnails
|
||||
- [qr.js](http://neocotic.com/qr.js/) ([GitHub](https://github.com/neocotic/qr.js)) - QR code generation
|
||||
|
||||
## PHP
|
||||
- [shaarli/netscape-bookmark-parser](https://github.com/shaarli/netscape-bookmark-parser) - Netscape bookmark parser
|
||||
|
||||
- [RainTPL](https://github.com/rainphp/raintpl) - HTML templating for PHP
|
||||
|
||||
### Composer
|
||||
|
||||
Library | Usage
|
||||
---|---
|
||||
[`shaarli/netscape-bookmark-parser`](https://packagist.org/packages/shaarli/netscape-bookmark-parser) | Import bookmarks from Netscape files
|
||||
[`erusev/parsedown`](https://packagist.org/packages/erusev/parsedown) | Parse MarkDown syntax for the MarkDown plugin
|
||||
[`slim/slim`](https://packagist.org/packages/slim/slim) | Handle routes and middleware for the REST API
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
## Add the sharing button (_bookmarklet_) to your browser
|
||||
|
||||
- Open your Shaarli and `Login`
|
||||
- Click the `Tools` button in the top bar
|
||||
- Drag the **`✚Shaare link` button**, and drop it to your browser's bookmarks bar.
|
||||
|
||||
_This bookmarklet button is compatible with Firefox, Opera, Chrome and Safari. Under Opera, you can't drag'n drop the button: You have to right-click on it and add a bookmark to your personal toolbar._
|
||||
|
||||
![](images/bookmarklet.png)
|
||||
|
||||
## Share links using the _bookmarklet_
|
||||
|
||||
- When you are visiting a webpage you would like to share with Shaarli, click the _bookmarklet_ you just added.
|
||||
- A window opens.
|
||||
- You can freely edit title, description, tags... to find it later using the text search or tag filtering.
|
||||
- You will be able to edit this link later using the ![](https://raw.githubusercontent.com/shaarli/Shaarli/master/images/edit_icon.png) edit button.
|
||||
- You can also check the “Private” box so that the link is saved but only visible to you.
|
||||
- Click `Save`.**Voilà! Your link is now shared.**
|
||||
|
||||
## Troubleshooting: The bookmarklet doesn't work with a few websites (e.g. Github.com)
|
||||
|
||||
Websites which enforce Content Security Policy (CSP), such as github.com, disallow usage of bookmarklets. Unfortunatly, there is nothing Shaarli can do about it.
|
||||
|
||||
See [#196](https://github.com/shaarli/Shaarli/issues/196).
|
||||
|
||||
There is an open bug for both Firefox and Chromium:
|
||||
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=866522
|
||||
- https://code.google.com/p/chromium/issues/detail?id=233903
|
|
@ -32,15 +32,18 @@ See [Theming](Theming) for a list of community-contributed themes, and an instal
|
|||
- [tt-rss-shaarli](https://github.com/jcsaaddupuy/tt-rss-shaarli) - [Tiny-Tiny RSS](http://tt-rss.org/) plugin that adds support for sharing articles with Shaarli
|
||||
- [octopress-shaarli](https://github.com/ahmet2mir/octopress-shaarli) - Octopress plugin to retrieve Shaarli links on the sidebar
|
||||
- [Scuttle to Shaarli](https://github.com/q2apro/scuttle-to-shaarli) - Import bookmarks from Scuttle
|
||||
|
||||
- [Shaarli app for Cloudron](https://git.cloudron.io/cloudron/shaarli-app) - Effortlessly run Shaarli with the help of [Cloudron](https://cloudron.io/) [![Install](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=com.github.shaarli)
|
||||
- [Shaarli_ynh](https://github.com/YunoHost-Apps/shaarli_ynh) - Shaarli is available as a [Yunohost](https://yunohost.org) app [![Install Shaarli with YunoHost](https://install-app.yunohost.org/install-with-yunohost.png)](https://install-app.yunohost.org/?app=shaarli)
|
||||
|
||||
### Mobile Apps
|
||||
- [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension.
|
||||
- [Shaarli for Android](http://sebsauvage.net/links/?ZAyDzg) - Android application that adds Shaarli as a sharing provider
|
||||
- [Shaarlier for Android](https://github.com/dimtion/Shaarlier) - Android application to simply add links directly into your Shaarli
|
||||
- [Stakali for Android](https://stakali.toneiv.eu) - Stakali is a personal bookmark manager which synchronizes with Shaarli
|
||||
|
||||
### Browser addons
|
||||
* [Shaarli Web Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli.
|
||||
- [Shaarli Firefox Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli.
|
||||
- [Shaarli Chrome Extension](https://github.com/octplane/Shiny-Shaarli) - toolbar button to share your current tab with Shaarli.
|
||||
|
||||
### Server apps
|
||||
- [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
A [`Makefile`](https://github.com/shaarli/Shaarli/blob/master/Makefile) is available to perform project-related operations:
|
||||
|
||||
- Documentation - generate a local HTML copy of the GitHub wiki
|
||||
- [Static analysis](Static analysis) - check that the code is compliant to PHP conventions
|
||||
- [Unit tests](Unit tests) - ensure there are no regressions introduced by new commits
|
||||
- [Static analysis](Static-analysis) - check that the code is compliant to PHP conventions
|
||||
- [Unit tests](Unit-tests) - ensure there are no regressions introduced by new commits
|
||||
|
||||
## Automatic builds
|
||||
[Travis CI](http://docs.travis-ci.com/) is a Continuous Integration build server, that runs a build:
|
||||
|
@ -17,7 +17,8 @@ Each build job:
|
|||
|
||||
- updates Composer
|
||||
- installs 3rd-party test dependencies with Composer
|
||||
- runs [Unit tests](Unit tests)
|
||||
- runs [Unit tests](Unit-tests)
|
||||
- runs ESLint check
|
||||
|
||||
After all jobs have finished, Travis returns the results to GitHub:
|
||||
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
Please have a look at the following pages:
|
||||
|
||||
- [Contributing to Shaarli](https://github.com/shaarli/Shaarli/tree/master/CONTRIBUTING.md)
|
||||
- [Static analysis](Static analysis) - patches should try to stick to the [PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), especially:
|
||||
- [Static analysis](Static-analysis) - patches should try to stick to the
|
||||
[PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), especially:
|
||||
- [PSR-1](http://www.php-fig.org/psr/psr-1/) - Basic Coding Standard
|
||||
- [PSR-2](http://www.php-fig.org/psr/psr-2/) - Coding Style Guide
|
||||
- [Unit tests](Unit tests)
|
||||
- [GnuPG signature](GnuPG signature) for tags/releases
|
||||
- [Unit tests](Unit-tests)
|
||||
- Javascript linting - Shaarli uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript).
|
||||
Run `make eslint` to check JS style.
|
||||
- [GnuPG signature](GnuPG-signature) for tags/releases
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
TODO: This page is out of date
|
||||
## Directory structure
|
||||
|
||||
Here is the directory structure of Shaarli and the purpose of the different files:
|
||||
|
||||
|
@ -6,29 +6,49 @@ Here is the directory structure of Shaarli and the purpose of the different file
|
|||
index.php # Main program
|
||||
application/ # Shaarli classes
|
||||
├── LinkDB.php
|
||||
|
||||
...
|
||||
|
||||
└── Utils.php
|
||||
tests/ # Shaarli unitary & functional tests
|
||||
tests/ # Shaarli unitary & functional tests
|
||||
├── LinkDBTest.php
|
||||
├── utils # utilities to ease testing
|
||||
|
||||
...
|
||||
|
||||
├── utils # utilities to ease testing
|
||||
│ └── ReferenceLinkDB.php
|
||||
└── UtilsTest.php
|
||||
assets/
|
||||
├── common/ # Assets shared by multiple themes
|
||||
├── ...
|
||||
├── default/ # Assets for the default template, before compilation
|
||||
├── fonts/ # Font files
|
||||
├── img/ # Images used by the default theme
|
||||
├── js/ # JavaScript files in ES6 syntax
|
||||
├── scss/ # SASS files
|
||||
└── vintage/ # Assets for the vintage template, before compilation
|
||||
└── ...
|
||||
COPYING # Shaarli license
|
||||
inc/ # static assets and 3rd party libraries
|
||||
├── awesomplete.* # tags autocompletion library
|
||||
├── blazy.* # picture wall lazy image loading library
|
||||
├── shaarli.css, reset.css # Shaarli stylesheet.
|
||||
├── qr.* # qr code generation library
|
||||
└──rain.tpl.class.php # RainTPL templating library
|
||||
tpl/ # RainTPL templates for Shaarli. They are used to build the pages.
|
||||
└── rain.tpl.class.php # RainTPL templating library
|
||||
images/ # Images and icons used in Shaarli
|
||||
data/ # data storage: bookmark database, configuration, logs, banlist…
|
||||
├── config.php # Shaarli configuration (login, password, timezone, title…)
|
||||
data/ # data storage: bookmark database, configuration, logs, banlist...
|
||||
├── config.json.php # Shaarli configuration (login, password, timezone, title...)
|
||||
├── datastore.php # Your link database (compressed).
|
||||
├── ipban.php # IP address ban system data
|
||||
├── lastupdatecheck.txt # Update check timestamp file
|
||||
└──log.txt # login/IPban log.
|
||||
└── log.txt # login/IPban log.
|
||||
tpl/ # RainTPL templates for Shaarli. They are used to build the pages.
|
||||
├── default/ # Default Shaarli theme
|
||||
├── fonts/ # Font files
|
||||
├── img/ # Images
|
||||
├── js/ # JavaScript files compiled by Babel and compatible with all browsers
|
||||
├── css/ # CSS files compiled with SASS
|
||||
└── vintage/ # Legacy Shaarli theme
|
||||
└── ...
|
||||
cache/ # thumbnails cache
|
||||
# This directory is automatically created. You can erase it anytime you want.
|
||||
tmp/ # Temporary directory for compiled RainTPL templates.
|
||||
# This directory is automatically created. You can erase it anytime you want.
|
||||
vendor/ # Third-party dependencies. This directory is created by Composer
|
||||
```
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
To install Shaarli, simply place the files in a directory under your webserver's
|
||||
Document Root (or directly at the document root).
|
||||
|
||||
Also, please make sure your server meets the [requirements](Server-requirements)
|
||||
and is properly [configured](Server-configuration).
|
||||
Also, please make sure your server is properly [configured](Server-configuration).
|
||||
|
||||
Multiple releases branches are available:
|
||||
|
||||
|
@ -23,13 +22,13 @@ Using one of the following methods:
|
|||
|
||||
### Download as an archive
|
||||
|
||||
In most cases, you should download the latest Shaarli release from the [releases](https://github.com/shaarli/Shaarli/releases) page. **Download our *shaarli-full* archive** to include dependencies.
|
||||
In most cases, you should download the latest Shaarli release from the [releases](https://github.com/shaarli/Shaarli/releases) page. Download our **shaarli-full** archive to include dependencies.
|
||||
|
||||
The current latest released version is `v0.9.3`
|
||||
The current latest released version is `v0.9.7`
|
||||
|
||||
```bash
|
||||
$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.3/shaarli-v0.9.3-full.zip
|
||||
$ unzip shaarli-v0.9.3-full.zip
|
||||
$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.7/shaarli-v0.9.7-full.zip
|
||||
$ unzip shaarli-v0.9.7-full.zip
|
||||
$ mv Shaarli /path/to/shaarli/
|
||||
```
|
||||
|
||||
|
@ -37,13 +36,15 @@ $ mv Shaarli /path/to/shaarli/
|
|||
|
||||
Cloning using `git` or downloading Github branches as zip files requires additional steps:
|
||||
|
||||
* Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
|
||||
* Install [Composer](Unit-tests.md#install_composer) to manage third-party [PHP dependencies](3rd-party-libraries.md#composer).
|
||||
* Install [yarn](https://yarnpkg.com/lang/en/docs/install/) to build the frontend dependencies.
|
||||
* Install [python3-virtualenv](https://pypi.python.org/pypi/virtualenv) to build the local HTML documentation.
|
||||
|
||||
```
|
||||
$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/
|
||||
$ git clone -b latest https://github.com/shaarli/Shaarli.git .
|
||||
$ composer install --no-dev --prefer-dist
|
||||
$ make build_frontend
|
||||
$ make translate
|
||||
$ make htmldoc
|
||||
```
|
||||
|
@ -91,7 +92,9 @@ $ composer install --no-dev --prefer-dist
|
|||
|
||||
_Use at your own risk!_
|
||||
|
||||
Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
|
||||
Install [Composer](Unit-tests.md#install_composer) to manage Shaarli PHP dependencies,
|
||||
and [yarn](https://yarnpkg.com/lang/en/docs/install/)
|
||||
for front-end dependencies.
|
||||
|
||||
To get the latest changes from the `master` branch:
|
||||
|
||||
|
@ -101,6 +104,7 @@ $ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
|
|||
# install/update third-party dependencies
|
||||
$ cd /path/to/shaarli
|
||||
$ composer install --no-dev --prefer-dist
|
||||
$ make build_frontend
|
||||
$ make translate
|
||||
$ make htmldoc
|
||||
```
|
||||
|
|
|
@ -22,7 +22,9 @@ With Shaarli:
|
|||
Shaarli stands for _shaaring_ your _links_.
|
||||
|
||||
### My Shaarli is broken!
|
||||
First of all, ensure that both the [web server](Server-configuration) and [Shaarli](Shaarli-configuration) are correctly configured, and that your installation is [supported](Server-requirements).
|
||||
First of all, ensure that both the [web server](Server-configuration) and
|
||||
[Shaarli](Shaarli-configuration) are correctly configured, and that your
|
||||
installation is [supported](Server-configuration).
|
||||
|
||||
If everything looks right but the issue(s) remain(s), please:
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
| Note | Firefox Share is no longer available for Firefox 57 and later versions. |
|
||||
|---------|---------|
|
||||
|
||||
### Add Shaarli as a sharing service to Firefox
|
||||
|
||||
- Open your Shaarli and `Login`
|
||||
- Click the `Tools` button in the top bar
|
||||
- Click the `✚Add to Firefox social` button and accept the activation.
|
||||
|
||||
|
||||
### Sharing links using Firefox share
|
||||
|
||||
- Add the sharing service as described above
|
||||
- When you are visiting a webpage you would like to share with Shaarli,
|
||||
click the Firefox _Share_ button [images/firefoxshare.png](images/firefoxshare.png)
|
||||
- You can edit your link before and after saving, just like the bookmarklet above.
|
||||
|
||||
_Your Shaarli instance must be hosted on an HTTPS (SSL/TLS secure connection)
|
||||
enabled server for Firefox Share to work. Firefox Share will not work over
|
||||
plain HTTP connections._
|
18
doc/md/Link-structure.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
## Link structure
|
||||
|
||||
Every link available through the `LinkDB` object is represented as an array
|
||||
containing the following fields:
|
||||
|
||||
* `id` (integer): Unique identifier.
|
||||
* `title` (string): Title of the link.
|
||||
* `url` (string): URL of the link. Used for displayable links (without redirector, url encoding, etc.).
|
||||
Can be absolute or relative for Notes.
|
||||
* `real_url` (string): Real destination URL, can be redirected, encoded, etc.
|
||||
* `shorturl` (string): Permalink small hash.
|
||||
* `description` (string): Link text description.
|
||||
* `private` (boolean): whether the link is private or not.
|
||||
* `tags` (string): all link tags separated by a single space
|
||||
* `thumbnail` (string|boolean): relative path of the thumbnail cache file, or false if there isn't any.
|
||||
* `created` (DateTime): link creation date time.
|
||||
* `updated` (DateTime): last modification date time.
|
||||
|
|
@ -37,7 +37,7 @@ This is important in case plugins are depending on each other. Read plugins READ
|
|||
|
||||
## File mode
|
||||
|
||||
Enabled plugin are stored in your `config.php` parameters file, under the `array`:
|
||||
Enabled plugin are stored in your `config.json.php` parameters file, under the `array`:
|
||||
|
||||
```php
|
||||
$GLOBALS['config']['ENABLED_PLUGINS']
|
||||
|
@ -48,7 +48,7 @@ Example:
|
|||
|
||||
```php
|
||||
$GLOBALS['config']['ENABLED_PLUGINS'] = array(
|
||||
'qrcode',
|
||||
'qrcode',
|
||||
'archiveorg',
|
||||
'wallabag',
|
||||
'markdown',
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
See the [REST API documentation](http://shaarli.github.io/api-documentation/)
|
||||
for a list of available endpoints and parameters.
|
||||
|
||||
Please ensure that your server meets the [requirements](Server-requirements)
|
||||
and is properly [configured](Server-configuration):
|
||||
Please ensure that your server meets the
|
||||
[requirements](Server-configuration#prerequisites) and is properly
|
||||
[configured](Server-configuration):
|
||||
|
||||
- URL rewriting is enabled (see specific Apache and Nginx sections)
|
||||
- the server's timezone is properly defined
|
||||
|
@ -151,3 +152,22 @@ See the reference API client:
|
|||
|
||||
- [Documentation](http://python-shaarli-client.readthedocs.io/en/latest/) on ReadTheDocs
|
||||
- [python-shaarli-client](https://github.com/shaarli/python-shaarli-client) on Github
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Debug mode
|
||||
|
||||
> This should never be used in a production environment.
|
||||
|
||||
For security reasons, authentication issues will always return an `HTTP 401` error code without any detail.
|
||||
|
||||
It is possible to enable the debug mode in `config.json.php`
|
||||
to get the actual error message in the HTTP response body with:
|
||||
|
||||
```json
|
||||
{
|
||||
"dev": {
|
||||
"debug": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,139 +1,130 @@
|
|||
*Example virtual host configurations for popular web servers*
|
||||
|
||||
- [Prerequisites](#prerequisistes)
|
||||
- [Apache](#apache)
|
||||
- [Nginx](#nginx)
|
||||
- [Proxies](#proxies)
|
||||
- [See also](#see-also)
|
||||
|
||||
## Prerequisites
|
||||
### Shaarli
|
||||
- Shaarli is installed in a directory readable/writeable by the user
|
||||
- the correct read/write permissions have been granted to the web server _user and/or group_
|
||||
- for HTTPS / SSL:
|
||||
- a key pair (public, private) and a certificate have been generated
|
||||
- the appropriate server SSL extension is installed and active
|
||||
|
||||
### HTTPS, TLS and self-signed certificates
|
||||
Related guides:
|
||||
- A web server and PHP interpreter module/service have been installed.
|
||||
- You have write access to the Shaarli installation directory.
|
||||
- The correct read/write permissions have been granted to the web server user and group.
|
||||
- Your PHP interpreter is compatible with supported PHP versions:
|
||||
|
||||
- [How to Create Self-Signed SSL Certificates with OpenSSL](http://www.xenocafe.com/tutorials/linux/centos/openssl/self_signed_certificates/index.php)
|
||||
- [How do I create my own Certificate Authority?](https://workaround.org/certificate-authority)
|
||||
- Generate a self-signed certificate (will trigger browser warnings) with apache2:
|
||||
`make-ssl-cert generate-default-snakeoil --force-overwrite` will create `/etc/ssl/certs/ssl-cert-snakeoil.pem` and `/etc/ssl/private/ssl-cert-snakeoil.key`
|
||||
Version | Status | Shaarli compatibility
|
||||
:---:|:---:|:---:
|
||||
7.2 | Supported | Yes
|
||||
7.1 | Supported | Yes
|
||||
7.0 | Supported | Yes
|
||||
5.6 | Supported | Yes
|
||||
5.5 | EOL: 2016-07-10 | Yes
|
||||
5.4 | EOL: 2015-09-14 | Yes (up to Shaarli 0.8.x)
|
||||
5.3 | EOL: 2014-08-14 | Yes (up to Shaarli 0.8.x)
|
||||
|
||||
### Proxies
|
||||
If Shaarli is served behind a proxy (i.e. there is a proxy server between clients and the web server hosting Shaarli), please refer to the proxy server documentation for proper configuration. In particular, you have to ensure that the following server variables are properly set:
|
||||
- The following PHP extensions are installed on the server:
|
||||
|
||||
- `X-Forwarded-Proto`
|
||||
- `X-Forwarded-Host`
|
||||
- `X-Forwarded-For`
|
||||
Extension | Required? | Usage
|
||||
---|:---:|---
|
||||
[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS
|
||||
[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
|
||||
[`php-gd`](http://php.net/manual/en/book.image.php) | optional | required to use thumbnails
|
||||
[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
|
||||
[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
|
||||
[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)
|
||||
|
||||
See also [proxy-related](https://github.com/shaarli/Shaarli/issues?utf8=%E2%9C%93&q=label%3Aproxy+) issues.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### SSL/TLS configuration
|
||||
|
||||
To setup HTTPS / SSL on your webserver (recommended), you must generate a public/private **key pair** and a **certificate**, and install, configure and activate the appropriate **webserver SSL extension**.
|
||||
|
||||
#### Let's Encrypt
|
||||
|
||||
[Let's Encrypt](https://en.wikipedia.org/wiki/Let%27s_Encrypt) is a certificate authority that provides free TLS/X.509 certificates via an automated process.
|
||||
|
||||
* Install `certbot` using the appropriate method described on https://certbot.eff.org/.
|
||||
|
||||
Location of the `certbot` program and template configuration files may vary depending on which installation method was used. Change the file paths below accordingly. Here is an easy way to create a signed certificate using `certbot`, it assumes `certbot` was installed through APT on a Debian-based distribution:
|
||||
|
||||
* Stop the apache2/nginx service.
|
||||
* Run `certbot --agree-tos --standalone --preferred-challenges tls-sni --email "youremail@example.com" --domain yourdomain.example.com`
|
||||
* For the Apache webserver, copy `/usr/lib/python2.7/dist-packages/certbot_apache/options-ssl-apache.conf` to `/etc/letsencrypt/options-ssl-apache.conf` (paths may vary depending on installation method)
|
||||
* For Nginx: TODO
|
||||
* Setup your webserver as described below
|
||||
* Restart the apache2/nginx service.
|
||||
|
||||
#### Self-signed certificates
|
||||
|
||||
If you don't want to request a certificate from Let's Encrypt, or are unable to (for example, webserver on a LAN, or domain name not registered in the public DNS system), you can generate a self-signed certificate. This certificate will trigger security warnings in web browsers, unless you add it to the browser's SSL store manually.
|
||||
|
||||
* Apache: run `make-ssl-cert generate-default-snakeoil --force-overwrite`
|
||||
* Nginx: TODO
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## Apache
|
||||
### Minimal
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName shaarli.my-domain.org
|
||||
DocumentRoot /absolute/path/to/shaarli/
|
||||
</VirtualHost>
|
||||
```
|
||||
### Debug - Log all the things!
|
||||
This configuration will log both Apache and PHP errors, which may prove useful to identify server configuration errors.
|
||||
|
||||
See:
|
||||
Here is a basic configuration example for the Apache web server with `mod_php`.
|
||||
|
||||
- [Apache/PHP - error log per VirtualHost](http://stackoverflow.com/q/176) (StackOverflow)
|
||||
- [PHP: php_value vs php_admin_value and the use of php_flag explained](https://ma.ttias.be/php-php_value-vs-php_admin_value-and-the-use-of-php_flag-explained/)
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName shaarli.my-domain.org
|
||||
DocumentRoot /absolute/path/to/shaarli/
|
||||
|
||||
LogLevel warn
|
||||
ErrorLog /var/log/apache2/shaarli-error.log
|
||||
CustomLog /var/log/apache2/shaarli-access.log combined
|
||||
|
||||
php_flag log_errors on
|
||||
php_flag display_errors on
|
||||
php_value error_reporting 2147483647
|
||||
php_value error_log /var/log/apache2/shaarli-php-error.log
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### Standard - Keep access and error logs
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName shaarli.my-domain.org
|
||||
DocumentRoot /absolute/path/to/shaarli/
|
||||
|
||||
LogLevel warn
|
||||
ErrorLog /var/log/apache2/shaarli-error.log
|
||||
CustomLog /var/log/apache2/shaarli-access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### Paranoid - Redirect HTTP (:80) to HTTPS (:443)
|
||||
See [Server-side TLS](https://wiki.mozilla.org/Security/Server_Side_TLS#Apache) (Mozilla).
|
||||
In `/etc/apache2/sites-available/shaarli.conf`:
|
||||
|
||||
```apache
|
||||
<VirtualHost *:443>
|
||||
ServerName shaarli.my-domain.org
|
||||
DocumentRoot /absolute/path/to/shaarli/
|
||||
|
||||
# Logging
|
||||
# Possible values include: debug, info, notice, warn, error, crit, alert, emerg.
|
||||
LogLevel warn
|
||||
ErrorLog /var/log/apache2/shaarli-error.log
|
||||
CustomLog /var/log/apache2/shaarli-access.log combined
|
||||
|
||||
# Let's Encrypt SSL configuration (recommended)
|
||||
SSLEngine on
|
||||
SSLCertificateFile /absolute/path/to/the/website/certificate.pem
|
||||
SSLCertificateKeyFile /absolute/path/to/the/website/key.key
|
||||
SSLCertificateFile /etc/letsencrypt/live/yourdomain.example.com/fullchain.pem
|
||||
SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.example.com/privkey.pem
|
||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||
|
||||
# Self-signed SSL cert configuration
|
||||
#SSLEngine on
|
||||
#SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
|
||||
#SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
|
||||
|
||||
# Optional, log PHP errors, useful for debugging
|
||||
#php_flag log_errors on
|
||||
#php_flag display_errors on
|
||||
#php_value error_reporting 2147483647
|
||||
#php_value error_log /var/log/apache2/shaarli-php-error.log
|
||||
|
||||
<Directory /absolute/path/to/shaarli/>
|
||||
#Required for .htaccess support
|
||||
AllowOverride All
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
Order allow,deny
|
||||
allow from all
|
||||
Allow from all
|
||||
|
||||
Options Indexes FollowSymLinks MultiViews #TODO is Indexes/Multiviews required?
|
||||
|
||||
# Optional - required for playvideos plugin
|
||||
#Header set Content-Security-Policy "script-src 'self' 'unsafe-inline' https://www.youtube.com https://s.ytimg.com 'unsafe-eval'"
|
||||
</Directory>
|
||||
|
||||
LogLevel warn
|
||||
ErrorLog /var/log/apache2/shaarli-error.log
|
||||
CustomLog /var/log/apache2/shaarli-access.log combined
|
||||
</VirtualHost>
|
||||
<VirtualHost *:80>
|
||||
ServerName shaarli.my-domain.org
|
||||
Redirect 301 / https://shaarli.my-domain.org
|
||||
|
||||
LogLevel warn
|
||||
ErrorLog /var/log/apache2/shaarli-error.log
|
||||
CustomLog /var/log/apache2/shaarli-access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### .htaccess
|
||||
Enable this configuration with `sudo a2ensite shaarli`
|
||||
|
||||
Shaarli use `.htaccess` Apache files to deny access to files that shouldn't be directly accessed (datastore, config, etc.). You need the directive `AllowOverride All` in your virtual host configuration for them to work.
|
||||
_Note: If you use Apache 2.2 or lower, you need [mod_version](https://httpd.apache.org/docs/current/mod/mod_version.html) to be installed and enabled._
|
||||
|
||||
**Warning**: If you use Apache 2.2 or lower, you need [mod_version](https://httpd.apache.org/docs/current/mod/mod_version.html) to be installed and enabled.
|
||||
|
||||
Apache module `mod_rewrite` **must** be enabled to use the REST API. URL rewriting rules for the Slim microframework are stated in the root `.htaccess` file.
|
||||
_Note: Apache module `mod_rewrite` must be enabled to use the REST API._
|
||||
|
||||
## LightHttpd
|
||||
|
||||
## Nginx
|
||||
### Foreword
|
||||
Nginx does not natively interpret PHP scripts; to this effect, we will run a [FastCGI](https://en.wikipedia.org/wiki/FastCGI) service, to which Nginx's FastCGI module will proxy all requests to PHP resources.
|
||||
|
||||
Required packages:
|
||||
Here is a basic configuration example for the Nginx web server, using the [php-fpm](http://php-fpm.org) PHP FastCGI Process Manager, and Nginx's [FastCGI](https://en.wikipedia.org/wiki/FastCGI) module.
|
||||
|
||||
- [nginx](http://nginx.org)
|
||||
- [php-fpm](http://php-fpm.org) - PHP FastCGI Process Manager
|
||||
|
||||
Official documentation:
|
||||
|
||||
- [Beginner's guide](http://nginx.org/en/docs/beginners_guide.html)
|
||||
- [ngx_http_fastcgi_module](http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html)
|
||||
- [Pitfalls](http://wiki.nginx.org/Pitfalls)
|
||||
|
||||
Community resources:
|
||||
|
||||
- [Server-side TLS (Nginx)](https://wiki.mozilla.org/Security/Server_Side_TLS#Nginx) (Mozilla)
|
||||
- [PHP configuration examples](http://kbeezie.com/nginx-configuration-examples/) (Karl Blessing)
|
||||
<!--- TODO refactor everything below this point --->
|
||||
|
||||
### Common setup
|
||||
Once Nginx and PHP-FPM are installed, we need to ensure:
|
||||
|
@ -404,3 +395,39 @@ http {
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Proxies
|
||||
If Shaarli is served behind a proxy (i.e. there is a proxy server between clients and the web server hosting Shaarli), please refer to the proxy server documentation for proper configuration. In particular, you have to ensure that the following server variables are properly set:
|
||||
|
||||
- `X-Forwarded-Proto`
|
||||
- `X-Forwarded-Host`
|
||||
- `X-Forwarded-For`
|
||||
|
||||
See also [proxy-related](https://github.com/shaarli/Shaarli/issues?utf8=%E2%9C%93&q=label%3Aproxy+) issues.
|
||||
|
||||
|
||||
## See also
|
||||
|
||||
* [Server security](Server-security.md)
|
||||
|
||||
#### Webservers
|
||||
|
||||
- [Apache/PHP - error log per VirtualHost](http://stackoverflow.com/q/176) (StackOverflow)
|
||||
- [Apache - PHP: php_value vs php_admin_value and the use of php_flag explained](https://ma.ttias.be/php-php_value-vs-php_admin_value-and-the-use-of-php_flag-explained/)
|
||||
- [Server-side TLS (Apache)](https://wiki.mozilla.org/Security/Server_Side_TLS#Apache) (Mozilla)
|
||||
- [Nginx Beginner's guide](http://nginx.org/en/docs/beginners_guide.html)
|
||||
- [Nginx ngx_http_fastcgi_module](http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html)
|
||||
- [Nginx Pitfalls](http://wiki.nginx.org/Pitfalls)
|
||||
- [Nginx PHP configuration examples](http://kbeezie.com/nginx-configuration-examples/) (Karl Blessing)
|
||||
- [Server-side TLS (Nginx)](https://wiki.mozilla.org/Security/Server_Side_TLS#Nginx) (Mozilla)
|
||||
- [How to Create Self-Signed SSL Certificates with OpenSSL](http://www.xenocafe.com/tutorials/linux/centos/openssl/self_signed_certificates/index.php)
|
||||
- [How do I create my own Certificate Authority?](https://workaround.org/certificate-authority)
|
||||
|
||||
#### PHP
|
||||
|
||||
- [Travis configuration](https://github.com/shaarli/Shaarli/blob/master/.travis.yml)
|
||||
- [PHP: Supported versions](http://php.net/supported-versions.php)
|
||||
- [PHP: Unsupported versions](http://php.net/eol.php) _(EOL - End Of Life)_
|
||||
- [PHP 7 Changelog](http://php.net/ChangeLog-7.php)
|
||||
- [PHP 5 Changelog](http://php.net/ChangeLog-5.php)
|
||||
- [PHP: Bugs](https://bugs.php.net/)
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
## PHP
|
||||
|
||||
### Release information
|
||||
- [PHP: Supported versions](http://php.net/supported-versions.php)
|
||||
- [PHP: Unsupported versions](http://php.net/eol.php) _(EOL - End Of Life)_
|
||||
- [PHP 7 Changelog](http://php.net/ChangeLog-7.php)
|
||||
- [PHP 5 Changelog](http://php.net/ChangeLog-5.php)
|
||||
- [PHP: Bugs](https://bugs.php.net/)
|
||||
|
||||
### Supported versions
|
||||
Version | Status | Shaarli compatibility
|
||||
:---:|:---:|:---:
|
||||
7.1 | Supported (v0.9.x) | Yes
|
||||
7.0 | Supported | Yes
|
||||
5.6 | Supported | Yes
|
||||
5.5 | EOL: 2016-07-10 | Yes
|
||||
5.4 | EOL: 2015-09-14 | Yes (up to Shaarli 0.8.x)
|
||||
5.3 | EOL: 2014-08-14 | Yes (up to Shaarli 0.8.x)
|
||||
|
||||
See also:
|
||||
|
||||
- [Travis configuration](https://github.com/shaarli/Shaarli/blob/master/.travis.yml)
|
||||
|
||||
### Dependency management
|
||||
Starting with Shaarli `v0.8.x`, [Composer](https://getcomposer.org/) is used to resolve,
|
||||
download and install third-party PHP dependencies.
|
||||
|
||||
Library | Required? | Usage
|
||||
---|:---:|---
|
||||
[`shaarli/netscape-bookmark-parser`](https://packagist.org/packages/shaarli/netscape-bookmark-parser) | All | Import bookmarks from Netscape files
|
||||
[`erusev/parsedown`](https://packagist.org/packages/erusev/parsedown) | All | Parse MarkDown syntax for the MarkDown plugin
|
||||
[`slim/slim`](https://packagist.org/packages/slim/slim) | All | Handle routes and middleware for the REST API
|
||||
|
||||
### Extensions
|
||||
Extension | Required? | Usage
|
||||
---|:---:|---
|
||||
[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS
|
||||
[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
|
||||
[`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing
|
||||
[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
|
||||
[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
|
||||
[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)
|
88
doc/md/Sharing-content.md
Normal file
|
@ -0,0 +1,88 @@
|
|||
Content posted to Shaarli is separated in items called _Shaares_. For each Shaare,
|
||||
you can customize the following aspects:
|
||||
|
||||
* URL to link to
|
||||
* Title
|
||||
* Free-text description
|
||||
* Tags
|
||||
* Public/private status
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## Adding new Shaares
|
||||
|
||||
While logged in to your Shaarli, you can add new Shaares in several ways:
|
||||
|
||||
* [+Shaare button](#shaare-button)
|
||||
* [Bookmarklet](#bookmarklet)
|
||||
* [Firefox Share](#firefox-share)
|
||||
* Third-party [apps and browser addons](Community-&-Related-software.md#mobile-apps)
|
||||
* [REST API](https://shaarli.github.io/api-documentation/)
|
||||
|
||||
### +Shaare button
|
||||
|
||||
* While logged in to your Shaarli, click the **`+Shaare`** button located in the toolbar.
|
||||
* Enter the URL of a link you want to share.
|
||||
* Click `Add link`
|
||||
* The `New Shaare` dialog appears, allowing you to fill in the details of your Shaare.
|
||||
* The Description, Title, and Tags will help you find your Shaare later using tags or full-text search.
|
||||
* You can also check the “Private” box so that the link is saved but only visible to you (the logged-in user).
|
||||
* Click `Save`.
|
||||
|
||||
<!-- TODO Add screenshot of add/edit link dialog -->
|
||||
|
||||
### Bookmarklet
|
||||
|
||||
The _Bookmarklet_ \[[1](https://en.wikipedia.org/wiki/Bookmarklet)\] is a special
|
||||
browser bookmark you can use to add new content to your Shaarli. This bookmarklet is
|
||||
compatible with Firefox, Opera, Chrome and Safari. To set it up:
|
||||
|
||||
* Access the `Tools` page from the button in the toolbar.
|
||||
* Drag the **`✚Shaare link` button** to your browser's bookmarks bar.
|
||||
|
||||
Once this is done, you can shaare any URL you are visiting simply by clicking the
|
||||
bookmarklet in your browser! The same `New Shaare` dialog as above is displayed.
|
||||
|
||||
| Note | Websites which enforce Content Security Policy (CSP), such as github.com, disallow usage of bookmarklets. Unfortunately, there is nothing Shaarli can do about it. \[[1](https://github.com/shaarli/Shaarli/issues/196)]\ \[[2](https://bugzilla.mozilla.org/show_bug.cgi?id=866522)]\ \[[3](https://code.google.com/p/chromium/issues/detail?id=233903)]\ |
|
||||
|---------|---------|
|
||||
|
||||
| Note | Under Opera, you can't drag'n drop the button: You have to right-click on it and add a bookmark to your personal toolbar. |
|
||||
|---------|---------|
|
||||
|
||||
![](images/bookmarklet.png)
|
||||
|
||||
|
||||
### Firefox Share
|
||||
|
||||
Before using Firefox Share, you must first add Shaarli as a sharing provider:
|
||||
|
||||
- Click the `Tools` button in the top bar
|
||||
- Click the `✚Add to Firefox social` button and accept the activation.
|
||||
|
||||
Once this is done, you can share any URL you are visiting by clicking the Firefox
|
||||
_Share_ button ![images/firefoxshare.png](images/firefoxshare.png)
|
||||
|
||||
| Note | Firefox Share is no longer available for Firefox 57 and later versions. |
|
||||
|---------|---------|
|
||||
|
||||
| Note | Your Shaarli instance must be hosted on an HTTPS (SSL/TLS secure connection) enabled server for Firefox Share to work. Firefox Share will not work over plaintext HTTP connections. |
|
||||
|---------|---------|
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## Editing Shaares
|
||||
|
||||
Any Shaare can edited by clicking its ![](images/edit_icon.png) `Edit` button.
|
||||
|
||||
Editing a Shaare will not change it's permalink, each permalink always points to the
|
||||
latest revision of a Shaare.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## Using shaarli as a blog, notepad, pastebin...
|
||||
|
||||
While adding or editing a link, leave the URL field blank to create a text-only
|
||||
("note") post. This allows you to post any kind of text content, such as blog
|
||||
articles, private or public notes, snippets... There is no character limit! You can
|
||||
access your Shaare from its permalink.
|
||||
|
|
@ -76,6 +76,18 @@ Then click on the "Update" button, and you can start to translate every availabl
|
|||
|
||||
Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
|
||||
|
||||
### Theme translations
|
||||
|
||||
Theme translation extensions are loaded automatically if they're present.
|
||||
|
||||
As a theme developer, all you have to do is to add the `.po` and `.mo` compiled file like this:
|
||||
|
||||
tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.po
|
||||
tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.mo
|
||||
|
||||
Where `<lang>` is the ISO 3166-1 alpha-2 language code.
|
||||
Read the following section "Extend Shaarli's translation" to learn how to generate those files.
|
||||
|
||||
### Extend Shaarli's translation
|
||||
|
||||
If you're writing a custom theme, or a non official plugin, you might want to use the translation system,
|
||||
|
|
|
@ -63,7 +63,7 @@ Related threads:
|
|||
|
||||
### I forgot my password!
|
||||
|
||||
Delete the file `data/config.php` and display the page again. You will be asked for a new login/password.
|
||||
Delete the file `data/config.json.php` and display the page again. You will be asked for a new login/password.
|
||||
|
||||
### I'm locked out - Login bruteforce protection
|
||||
|
||||
|
@ -97,7 +97,7 @@ php56 1
|
|||
|
||||
```php
|
||||
//list($status,$headers,$data) = getHTTP($url,4); // Short timeout to keep the application responsive.
|
||||
// FIXME: Decode charset according to charset specified in either 1) HTTP response headers or 2) <head> in html
|
||||
// FIXME: Decode charset according to charset specified in either 1) HTTP response headers or 2) <head> in html
|
||||
//if (strpos($status,'200 OK')) $title=html_extract_title($data);
|
||||
```
|
||||
|
||||
|
@ -106,11 +106,11 @@ php56 1
|
|||
|
||||
### Dates are not properly formatted
|
||||
|
||||
Shaarli tries to sniff the language of the browser (using HTTP_ACCEPT_LANGUAGE headers) and choose a date format accordingly. But Shaarli can only use the date formats (and more generaly speaking, the locales) provided by the webserver. So even if you have a browser in French, you may end up with dates in US format (it's the case on sebsauvage.net :-( )
|
||||
|
||||
### Problems on CentOS servers
|
||||
|
||||
On **CentOS**/RedHat derivatives, you may need to install the `php-mbstring` package.
|
||||
Shaarli tries to sniff the language of the browser (using `HTTP_ACCEPT_LANGUAGE` headers)
|
||||
and choose a date format accordingly. But Shaarli can only use the date formats
|
||||
(and more generally speaking, the locales) provided by the webserver.
|
||||
So even if you have a browser in French, you may end up with dates in US format
|
||||
(it's the case on sebsauvage.net :-( )
|
||||
|
||||
### My session expires! I can't stay logged in
|
||||
|
||||
|
@ -126,7 +126,3 @@ This can be caused by several things:
|
|||
## Sessions do not seem to work correctly on your server
|
||||
|
||||
Follow the instructions in the error message. Make sure you are accessing shaarli via a direct IP address or a proper hostname. If you have **no dots** in the hostname (e.g. `localhost` or `http://my-webserver/shaarli/`), some browsers will not store cookies at all (this respects the [HTTP cookie specification](http://curl.haxx.se/rfc/cookie_spec.html)).
|
||||
|
||||
### pubsubhubbub support
|
||||
|
||||
Download [publisher.php](https://pubsubhubbub.googlecode.com/git/publisher_clients/php/library/publisher.php) at the root of your Shaarli installation and set `$GLOBALS['config']['PUBSUBHUB_URL']` in your `config.php`
|
||||
|
|
|
@ -8,7 +8,7 @@ Read first:
|
|||
|
||||
### Docker test images
|
||||
|
||||
Test Dockerfiles are located under `docker/tests/<distribution>/Dockerfile`,
|
||||
Test Dockerfiles are located under `tests/docker/<distribution>/Dockerfile`,
|
||||
and can be used to build Docker images to run Shaarli test suites under common
|
||||
Linux environments.
|
||||
|
||||
|
@ -27,7 +27,7 @@ What's behind the curtains:
|
|||
- test PHP dependencies (OS packages)
|
||||
- Composer
|
||||
- the local workspace is mapped to the container's `/shaarli/` directory,
|
||||
- the files are rsync'd to so tests are run using a standard Linux user account
|
||||
- the files are rsync'd so tests are run using a standard Linux user account
|
||||
(running tests as `root` would bypass permission checks and may hide issues)
|
||||
- the tests are run inside the container.
|
||||
|
||||
|
@ -36,7 +36,7 @@ What's behind the curtains:
|
|||
```bash
|
||||
# build the Debian 9 Docker image
|
||||
$ cd /path/to/shaarli
|
||||
$ cd docker/test/debian9
|
||||
$ cd tests/docker/debian9
|
||||
$ docker build -t shaarli-test:debian9 .
|
||||
```
|
||||
|
||||
|
|