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
This commit is contained in:
Knah Tsaeb 2018-10-01 15:51:33 +02:00
commit 94716fb2ba
220 changed files with 14665 additions and 12494 deletions

12
.dev/.eslintrc.js Normal file
View 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
View 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

View File

@ -4,6 +4,9 @@
.github .github
tests tests
# Docker Compose resources
docker-compose.yml
# Shaarli runtime resources # Shaarli runtime resources
cache/* cache/*
data/* data/*
@ -35,10 +38,17 @@ phpmd.html
# User plugin configuration # User plugin configuration
plugins/*/config.php plugins/*/config.php
# HTML documentation
doc/html/
# 3rd party themes # 3rd party themes
tpl/* tpl/*
!tpl/default !tpl/default
!tpl/vintage !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

View File

@ -10,7 +10,7 @@ trim_trailing_whitespace = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[*.{htaccess,html,xml}] [*.{htaccess,html,scss,js,json,xml,yml}]
indent_size = 2 indent_size = 2
[*.php] [*.php]

33
.gitattributes vendored
View File

@ -25,18 +25,21 @@ Dockerfile text
*.mo binary *.mo binary
# Exclude from Git archives # Exclude from Git archives
.editorconfig export-ignore .editorconfig export-ignore
.gitattributes export-ignore .dev export-ignore
.github export-ignore .gitattributes export-ignore
.gitignore export-ignore .github export-ignore
.travis.yml export-ignore .gitignore export-ignore
doc/**/*.json export-ignore .travis.yml export-ignore
doc/**/*.md export-ignore doc/**/*.json export-ignore
.docker/ export-ignore doc/**/*.md export-ignore
.dockerignore export-ignore .docker/ export-ignore
Dockerfile* export-ignore .dockerignore export-ignore
Doxyfile export-ignore docker-compose.* export-ignore
Makefile export-ignore Dockerfile* export-ignore
mkdocs.yml export-ignore Doxyfile export-ignore
phpunit.xml export-ignore Makefile export-ignore
tests/ export-ignore node_modules/ export-ignore
mkdocs.yml export-ignore
phpunit.xml export-ignore
tests/ export-ignore

10
.gitignore vendored
View File

@ -40,3 +40,13 @@ tpl/*
contact.php contact.php
formStyle.css 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

View File

@ -14,3 +14,35 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L] 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>

View File

@ -1,20 +1,53 @@
sudo: false sudo: false
dist: trusty 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: cache:
directories: directories:
- $HOME/.composer/cache - $HOME/.composer/cache
php:
- 7.1
- 7.0
- 5.6
- 5.5
install: install:
- composer self-update
- composer install --prefer-dist - composer install --prefer-dist
- locale -a
before_script: before_script:
- PATH=${PATH//:\.\/node_modules\/\.bin/} - PATH=${PATH//:\.\/node_modules\/\.bin/}
script: script:
- make clean - make clean
- make check_permissions - make check_permissions

20
AUTHORS
View File

@ -1,6 +1,6 @@
588 ArthurHoaro <arthur@hoa.ro> 687 ArthurHoaro <arthur@hoa.ro>
283 VirtualTam <virtualtam@flibidi.net> 355 VirtualTam <virtualtam@flibidi.net>
179 nodiscc <nodiscc@gmail.com> 195 nodiscc <nodiscc@gmail.com>
56 Sébastien Sauvage <sebsauvage@sebsauvage.net> 56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
15 Florian Eula <eula.florian@gmail.com> 15 Florian Eula <eula.florian@gmail.com>
13 Emilien Klein <emilien@klein.st> 13 Emilien Klein <emilien@klein.st>
@ -9,12 +9,15 @@
8 Christophe HENRY <christophe.henry@sbgodin.fr> 8 Christophe HENRY <christophe.henry@sbgodin.fr>
6 B. van Berkum <dev@dotmpe.com> 6 B. van Berkum <dev@dotmpe.com>
5 Lucas Cimon <lucas.cimon@gmail.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 Alexandre Alapetite <alexandre@alapetite.fr>
4 David Sferruzza <david.sferruzza@gmail.com> 4 David Sferruzza <david.sferruzza@gmail.com>
4 Immánuel Fodor <immanuelfactor+github@gmail.com> 4 Immánuel Fodor <immanuelfactor+github@gmail.com>
4 kalvn <kalvnthereal@gmail.com>
3 Teromene <teromene@teromene.fr> 3 Teromene <teromene@teromene.fr>
3 llune <llune@users.noreply.github.com>
2 Chris Kuethe <chris.kuethe@gmail.com> 2 Chris Kuethe <chris.kuethe@gmail.com>
2 Felix Bartels <felix@host-consultants.de>
2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org> 2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
2 Mathieu Chabanon <git@matchab.fr> 2 Mathieu Chabanon <git@matchab.fr>
2 Miloš Jovanović <mjovanovic@gmail.com> 2 Miloš Jovanović <mjovanovic@gmail.com>
@ -23,20 +26,26 @@
2 Timo Van Neerden <fire@lehollandaisvolant.net> 2 Timo Van Neerden <fire@lehollandaisvolant.net>
2 julienCXX <software@chmodplusx.eu> 2 julienCXX <software@chmodplusx.eu>
2 philipp-r <philipp-r@users.noreply.github.com> 2 philipp-r <philipp-r@users.noreply.github.com>
2 pips <pips@e5150.fr>
1 Adrien Oliva <adrien.oliva@yapbreak.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 Alexis J <alexis@effingo.be>
1 Angristan <angristan@users.noreply.github.com>
1 BoboTiG <bobotig@gmail.com> 1 BoboTiG <bobotig@gmail.com>
1 Bronco <bronco@warriordudimanche.net> 1 Bronco <bronco@warriordudimanche.net>
1 Buster One <37770318+buster-one@users.noreply.github.com>
1 D Low <daniellowtw@gmail.com> 1 D Low <daniellowtw@gmail.com>
1 Daniel Jakots <vigdis@chown.me> 1 Daniel Jakots <vigdis@chown.me>
1 Dennis Verspuij <dennisverspuij@users.noreply.github.com>
1 Dimtion <zizou.xena@gmail.com> 1 Dimtion <zizou.xena@gmail.com>
1 Fanch <fanch-github@qth.fr> 1 Fanch <fanch-github@qth.fr>
1 Felix Bartels <felix@host-consultants.de>
1 Felix Kästner <github.com-fpunktk@fpunktk.de> 1 Felix Kästner <github.com-fpunktk@fpunktk.de>
1 Florian Voigt <flvoigt@me.com> 1 Florian Voigt <flvoigt@me.com>
1 Franck Kerbiriou <FranckKe@users.noreply.github.com> 1 Franck Kerbiriou <FranckKe@users.noreply.github.com>
1 Gary Marigliano <gmarigliano93@gmail.com> 1 Gary Marigliano <gmarigliano93@gmail.com>
1 Guillaume Virlet <github@virlet.org> 1 Guillaume Virlet <github@virlet.org>
1 Jonathan Amiez <jonathan.amiez@gmail.com>
1 Jonathan Druart <jonathan.druart@gmail.com> 1 Jonathan Druart <jonathan.druart@gmail.com>
1 Julien Pivotto <roidelapluie@inuits.eu> 1 Julien Pivotto <roidelapluie@inuits.eu>
1 Kevin Canévet <kevin@streamroot.io> 1 Kevin Canévet <kevin@streamroot.io>
@ -49,3 +58,4 @@
1 TsT <tst2005@gmail.com> 1 TsT <tst2005@gmail.com>
1 dimtion <zizou.xena@gmail.com> 1 dimtion <zizou.xena@gmail.com>
1 durcheinandr <jochen@durcheinandr.de> 1 durcheinandr <jochen@durcheinandr.de>
1 lapineige <lapineige@users.noreply.github.com>

View File

@ -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/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). 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 ## [v0.9.7](https://github.com/shaarli/Shaarli/releases/tag/v0.9.7) - 2018-06-20
### Changed ### Changed
- Build the Docker images from the local Git sources - 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. - 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 ## [v0.8.5](https://github.com/shaarli/Shaarli/releases/tag/v0.8.5) - 2018-01-04
**XSS vulnerability fixed. Please update.** **XSS vulnerability fixed. Please update.**

38
COPYING
View File

@ -1,55 +1,57 @@
Files: * Files: *
License: zlib/libpng License: zlib/libpng
Copyright: (c) 2011-2015 Sébastien SAUVAGE <sebsauvage@sebsauvage.net> 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) License: BSD (http://opensource.org/licenses/BSD-3-Clause)
Copyright: (c) 2010, Yahoo! Inc. 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/) License: CC-BY (http://creativecommons.org/licenses/by/3.0/)
Copyright: (c) 2014 Yusuke Kamiyamane Copyright: (c) 2014 Yusuke Kamiyamane
Source: http://p.yusukekamiyamane.com/ 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/) License: CC-BY (http://creativecommons.org/licenses/by/3.0/)
Copyright: (c) 2014 Designmodo Copyright: (c) 2014 Designmodo
Source: http://designmodo.com/linecons-free/ 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 Licence: Public Domain
Source: https://openclipart.org/people/j4p4n/j4p4n_ornimental_bookend_-_left.svg 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 Licence: Public Domain
Source: http://bashcorpo.deviantart.com/art/Grungy-paper-texture-v-5-22966998 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 License: zlib/libpng
Copyright: (c) 2011-2014 idleman idleman@idleman.fr 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) 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 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> Copyright: 2011-2012, Federico Ulfo <rainelemental@gmail.com>
2011-2012, The Rain Team <hello@raintm.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 Files: plugins/wallabag/wallabag.png
License: MIT License (http://opensource.org/licenses/MIT) License: MIT License (http://opensource.org/licenses/MIT)
Copyright: (C) 2015 Nicolas Lœuillet - https://github.com/wallabag/wallabag 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 ZLIB/LIBPNG LICENSE

View File

@ -5,7 +5,7 @@ FROM python:3-alpine as docs
ADD . /usr/src/app/shaarli ADD . /usr/src/app/shaarli
RUN cd /usr/src/app/shaarli \ RUN cd /usr/src/app/shaarli \
&& pip install --no-cache-dir mkdocs \ && pip install --no-cache-dir mkdocs \
&& mkdocs build && mkdocs build --clean
# Stage 2: # Stage 2:
# - Resolve PHP dependencies with Composer # - Resolve PHP dependencies with Composer
@ -15,8 +15,17 @@ RUN cd shaarli \
&& composer --prefer-dist --no-dev install && composer --prefer-dist --no-dev install
# Stage 3: # 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 # - Shaarli image
FROM alpine:3.6 FROM alpine:3.8
LABEL maintainer="Shaarli Community" LABEL maintainer="Shaarli Community"
RUN apk --update --no-cache add \ RUN apk --update --no-cache add \
@ -47,12 +56,13 @@ RUN rm -rf /etc/php7/php-fpm.d/www.conf \
WORKDIR /var/www WORKDIR /var/www
COPY --from=composer /app/shaarli shaarli COPY --from=node /shaarli shaarli
RUN chown -R nginx:nginx . \ RUN chown -R nginx:nginx . \
&& ln -sf /dev/stdout /var/log/nginx/shaarli.access.log \ && ln -sf /dev/stdout /var/log/nginx/shaarli.access.log \
&& ln -sf /dev/stderr /var/log/nginx/shaarli.error.log && ln -sf /dev/stderr /var/log/nginx/shaarli.error.log
VOLUME /var/www/shaarli/cache
VOLUME /var/www/shaarli/data VOLUME /var/www/shaarli/data
EXPOSE 80 EXPOSE 80

View File

@ -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 # pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
# the logo to the output directory. # 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 # 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 # into which the generated documentation will be written. If a relative path is
@ -804,6 +804,7 @@ RECURSIVE = YES
# run. # run.
EXCLUDE = vendor \ EXCLUDE = vendor \
data \
tpl \ tpl \
inc \ inc \
doc \ doc \

View File

@ -157,21 +157,32 @@ composer_dependencies: clean
composer install --no-dev --prefer-dist composer install --no-dev --prefer-dist
find vendor/ -name ".git" -type d -exec rm -rf {} + 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 ### 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 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|^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|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^tpl|$(ARCHIVE_PREFIX)tpl|" tpl/
gzip $(ARCHIVE_VERSION).tar gzip $(ARCHIVE_VERSION).tar
### generate a release zip and include 3rd-party dependencies and translations ### 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 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor} mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/ rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)doc/ zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)doc/
rsync -a vendor/ $(ARCHIVE_PREFIX)vendor/ rsync -a vendor/ $(ARCHIVE_PREFIX)vendor/
zip -r $(ARCHIVE_VERSION).zip $(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) rm -rf $(ARCHIVE_PREFIX)
## ##
@ -192,18 +203,27 @@ authors:
### generate Doxygen documentation ### generate Doxygen documentation
doxygen: clean doxygen: clean
@rm -rf doxygen @rm -rf doxygen
@( cat Doxyfile ; echo "PROJECT_NUMBER=`git describe`" ) | doxygen - @doxygen Doxyfile
### generate HTML documentation from Markdown pages with MkDocs ### generate HTML documentation from Markdown pages with MkDocs
htmldoc: htmldoc:
python3 -m venv venv/ python3 -m venv venv/
bash -c 'source venv/bin/activate; \ bash -c 'source venv/bin/activate; \
pip install mkdocs; \ pip install mkdocs; \
mkdocs build' mkdocs build --clean'
find doc/html/ -type f -exec chmod a-x '{}' \; find doc/html/ -type f -exec chmod a-x '{}' \;
rm -r venv rm -r venv
### Generate Shaarli's translation compiled file (.mo) ### Generate Shaarli's translation compiled file (.mo)
translate: 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

View File

@ -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._ _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._ _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/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
&bull; &bull;
[![](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/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
&bull; &bull;
[![](https://img.shields.io/badge/master-v0.10.x-blue.svg)](https://github.com/shaarli/Shaarli) [![](https://img.shields.io/badge/master-v0.10.x-blue.svg)](https://github.com/shaarli/Shaarli)

View File

@ -37,7 +37,7 @@ class FileUtils
if (is_file($file) && !is_writeable($file)) { if (is_file($file) && !is_writeable($file)) {
// The datastore exists but is not writeable // The datastore exists but is not writeable
throw new IOException($file); 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 // The datastore does not exist and its parent directory is not writeable
throw new IOException(dirname($file)); throw new IOException(dirname($file));
} }

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* GET an HTTP URL to retrieve its content * 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 string $url URL to get (http://...)
* @param int $timeout network timeout (in seconds) * @param int $timeout network timeout (in seconds)
@ -415,6 +415,37 @@ function getIpAddressFromProxy($server, $trustedIps)
return array_pop($ips); 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. * Returns true if Shaarli's currently browsed in HTTPS.
* Supports reverse proxies (if the headers are correctly set). * Supports reverse proxies (if the headers are correctly set).

View File

@ -98,6 +98,12 @@ class Languages
$this->translator->setLanguage($this->language); $this->translator->setLanguage($this->language);
$this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages'); $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
// Default extension translation from the current theme
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $this->conf->get('theme') .'/language';
if (is_dir($themeTransFolder)) {
$this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false);
}
foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) { foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
if ($domain !== self::DEFAULT_DOMAIN) { if ($domain !== self::DEFAULT_DOMAIN) {
$this->translator->loadDomain($domain, $translationPath, false); $this->translator->loadDomain($domain, $translationPath, false);
@ -116,12 +122,23 @@ class Languages
$translations = new Translations(); $translations = new Translations();
// Core translations // Core translations
try { try {
/** @var Translations $translations */
$translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po'); $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
$translations->setDomain('shaarli'); $translations->setDomain('shaarli');
$this->translator->loadTranslations($translations); $this->translator->loadTranslations($translations);
} catch (\InvalidArgumentException $e) {} } 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.). // Extension translations (plugins, themes, etc.).
foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) { foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
@ -130,7 +147,6 @@ class Languages
} }
try { try {
/** @var Translations $extension */
$extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po'); $extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
$extension->setDomain($domain); $extension->setDomain($domain);
$this->translator->loadTranslations($extension); $this->translator->loadTranslations($extension);
@ -161,6 +177,7 @@ class Languages
'auto' => t('Automatic'), 'auto' => t('Automatic'),
'en' => t('English'), 'en' => t('English'),
'fr' => t('French'), 'fr' => t('French'),
'de' => t('German'),
]; ];
} }
} }

View File

@ -436,15 +436,17 @@ You use the community supported version of the original Shaarli project, by Seba
/** /**
* Returns the list tags appearing in the links with the given tags * 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 * @param array $filteringTags tags selecting the links to consider
* @return: a tag=>linksCount array * @param string $visibility process only all/private/public links
*
* @return array tag => linksCount
*/ */
public function linksCountPerTag($filteringTags = [], $visibility = 'all') public function linksCountPerTag($filteringTags = [], $visibility = 'all')
{ {
$links = empty($filteringTags) ? $this->links : $this->filterSearch(['searchtags' => $filteringTags], false, $visibility); $links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
$tags = array(); $tags = [];
$caseMapping = array(); $caseMapping = [];
foreach ($links as $link) { foreach ($links as $link) {
foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) { foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
if (empty($tag)) { if (empty($tag)) {
@ -458,8 +460,19 @@ You use the community supported version of the original Shaarli project, by Seba
$tags[$caseMapping[strtolower($tag)]]++; $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; return $tags;
} }

View File

@ -117,7 +117,7 @@ class LinkFilter
foreach ($this->links as $key => $value) { foreach ($this->links as $key => $value) {
if ($value['private'] && $visibility === 'private') { if ($value['private'] && $visibility === 'private') {
$out[$key] = $value; $out[$key] = $value;
} else if (! $value['private'] && $visibility === 'public') { } elseif (! $value['private'] && $visibility === 'public') {
$out[$key] = $value; $out[$key] = $value;
} }
} }
@ -210,7 +210,7 @@ class LinkFilter
if ($visibility !== 'all') { if ($visibility !== 'all') {
if (! $link['private'] && $visibility === 'private') { if (! $link['private'] && $visibility === 'private') {
continue; continue;
} else if ($link['private'] && $visibility === 'public') { } elseif ($link['private'] && $visibility === 'public') {
continue; continue;
} }
} }
@ -337,7 +337,7 @@ class LinkFilter
if ($visibility !== 'all') { if ($visibility !== 'all') {
if (! $link['private'] && $visibility === 'private') { if (! $link['private'] && $visibility === 'private') {
continue; continue;
} else if ($link['private'] && $visibility === 'public') { } elseif ($link['private'] && $visibility === 'public') {
continue; continue;
} }
} }
@ -380,7 +380,7 @@ class LinkFilter
if ($visibility !== 'all') { if ($visibility !== 'all') {
if (! $link['private'] && $visibility === 'private') { if (! $link['private'] && $visibility === 'private') {
continue; continue;
} else if ($link['private'] && $visibility === 'public') { } elseif ($link['private'] && $visibility === 'public') {
continue; continue;
} }
} }

View File

@ -11,6 +11,7 @@
*/ */
function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo') function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo')
{ {
$isRedirected = false;
/** /**
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download). * 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 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); $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; 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) { if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
return false; return false;
} }
if (empty($charset)) { if (!empty($contentType) && empty($charset)) {
$charset = header_extract_charset($contentType); $charset = header_extract_charset($contentType);
} }
if (empty($charset)) { if (empty($charset)) {

View File

@ -108,7 +108,7 @@ class NetscapeBookmarkUtils
$filesize = $files['filetoupload']['size']; $filesize = $files['filetoupload']['size'];
$data = file_get_contents($files['filetoupload']['tmp_name']); $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); return self::importStatus($filename, $filesize);
} }
@ -154,13 +154,13 @@ class NetscapeBookmarkUtils
if (empty($post['privacy']) || $post['privacy'] == 'default') { if (empty($post['privacy']) || $post['privacy'] == 'default') {
// use value from the imported file // use value from the imported file
$private = $bkm['pub'] == '1' ? 0 : 1; $private = $bkm['pub'] == '1' ? 0 : 1;
} else if ($post['privacy'] == 'private') { } elseif ($post['privacy'] == 'private') {
// all imported links are private // all imported links are private
$private = 1; $private = 1;
} else if ($post['privacy'] == 'public') { } elseif ($post['privacy'] == 'public') {
// all imported links are public // all imported links are public
$private = 0; $private = 0;
} }
$newLink = array( $newLink = array(
'title' => $bkm['title'], 'title' => $bkm['title'],

View File

@ -1,6 +1,7 @@
<?php <?php
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Thumbnailer;
/** /**
* This class is in charge of building the final page. * This class is in charge of building the final page.
@ -21,25 +22,42 @@ class PageBuilder
*/ */
protected $conf; protected $conf;
/**
* @var array $_SESSION
*/
protected $session;
/** /**
* @var LinkDB $linkDB instance. * @var LinkDB $linkDB instance.
*/ */
protected $linkDB; protected $linkDB;
/**
* @var null|string XSRF token
*/
protected $token;
/** @var bool $isLoggedIn Whether the user is logged in **/
protected $isLoggedIn = false;
/** /**
* PageBuilder constructor. * PageBuilder constructor.
* $tpl is initialized at false for lazy loading. * $tpl is initialized at false for lazy loading.
* *
* @param ConfigManager $conf Configuration Manager instance (reference). * @param ConfigManager $conf Configuration Manager instance (reference).
* @param LinkDB $linkDB instance. * @param array $session $_SESSION array
* @param string $token Session token * @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->tpl = false;
$this->conf = $conf; $this->conf = $conf;
$this->session = $session;
$this->linkDB = $linkDB; $this->linkDB = $linkDB;
$this->token = $token; $this->token = $token;
$this->isLoggedIn = $isLoggedIn;
} }
/** /**
@ -55,7 +73,7 @@ class PageBuilder
$this->conf->get('resource.update_check'), $this->conf->get('resource.update_check'),
$this->conf->get('updates.check_updates_interval'), $this->conf->get('updates.check_updates_interval'),
$this->conf->get('updates.check_updates'), $this->conf->get('updates.check_updates'),
isLoggedIn(), $this->isLoggedIn,
$this->conf->get('updates.check_updates_branch') $this->conf->get('updates.check_updates_branch')
); );
$this->tpl->assign('newVersion', escape($version)); $this->tpl->assign('newVersion', escape($version));
@ -67,6 +85,7 @@ class PageBuilder
$this->tpl->assign('versionError', escape($exc->getMessage())); $this->tpl->assign('versionError', escape($exc->getMessage()));
} }
$this->tpl->assign('is_logged_in', $this->isLoggedIn);
$this->tpl->assign('feedurl', escape(index_url($_SERVER))); $this->tpl->assign('feedurl', escape(index_url($_SERVER)));
$searchcrits = ''; // Search criteria $searchcrits = ''; // Search criteria
if (!empty($_GET['searchtags'])) { if (!empty($_GET['searchtags'])) {
@ -83,7 +102,8 @@ class PageBuilder
ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt')) ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt'))
); );
$this->tpl->assign('scripturl', index_url($_SERVER)); $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('untaggedonly', !empty($_SESSION['untaggedonly']));
$this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli')); $this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli'));
if ($this->conf->exists('general.header_link')) { if ($this->conf->exists('general.header_link')) {
@ -99,6 +119,19 @@ class PageBuilder
if ($this->linkDB !== null) { if ($this->linkDB !== null) {
$this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); $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. // To be removed with a proper theme configuration.
$this->tpl->assign('conf', $this->conf); $this->tpl->assign('conf', $this->conf);
} }

View File

@ -7,6 +7,8 @@
*/ */
class Router class Router
{ {
public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
public static $PAGE_LOGIN = 'login'; public static $PAGE_LOGIN = 'login';
public static $PAGE_PICWALL = 'picwall'; public static $PAGE_PICWALL = 'picwall';
@ -47,6 +49,8 @@ class Router
public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin'; public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
public static $PAGE_THUMBS_UPDATE = 'thumbs_update';
public static $GET_TOKEN = 'token'; public static $GET_TOKEN = 'token';
/** /**
@ -101,6 +105,14 @@ class Router
return self::$PAGE_FEED_RSS; 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. // At this point, only loggedin pages.
if (!$loggedIn) { if (!$loggedIn) {
return self::$PAGE_LINKLIST; return self::$PAGE_LINKLIST;

View File

@ -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
View 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');
}
}

View File

@ -2,6 +2,7 @@
use Shaarli\Config\ConfigJson; use Shaarli\Config\ConfigJson;
use Shaarli\Config\ConfigPhp; use Shaarli\Config\ConfigPhp;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Thumbnailer;
/** /**
* Class Updater. * Class Updater.
@ -30,6 +31,11 @@ class Updater
*/ */
protected $isLoggedIn; protected $isLoggedIn;
/**
* @var array $_SESSION
*/
protected $session;
/** /**
* @var ReflectionMethod[] List of current class methods. * @var ReflectionMethod[] List of current class methods.
*/ */
@ -42,13 +48,17 @@ class Updater
* @param LinkDB $linkDB LinkDB instance. * @param LinkDB $linkDB LinkDB instance.
* @param ConfigManager $conf Configuration Manager instance. * @param ConfigManager $conf Configuration Manager instance.
* @param boolean $isLoggedIn True if the user is logged in. * @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->doneUpdates = $doneUpdates;
$this->linkDB = $linkDB; $this->linkDB = $linkDB;
$this->conf = $conf; $this->conf = $conf;
$this->isLoggedIn = $isLoggedIn; $this->isLoggedIn = $isLoggedIn;
$this->session = &$session;
// Retrieve all update methods. // Retrieve all update methods.
$class = new ReflectionClass($this); $class = new ReflectionClass($this);
@ -445,6 +455,68 @@ class Updater
$this->linkDB->save($this->conf->get('resource.page_cache')); $this->linkDB->save($this->conf->get('resource.page_cache'));
return true; 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;
}
} }
/** /**

View File

@ -81,7 +81,7 @@ function whitelist_protocols($url, $protocols)
// Protocol not allowed: we remove it and replace it with http // Protocol not allowed: we remove it and replace it with http
if ($protocol === 1 && ! in_array($match[1], $protocols)) { if ($protocol === 1 && ! in_array($match[1], $protocols)) {
$url = str_replace($match[0], 'http://', $url); $url = str_replace($match[0], 'http://', $url);
} else if ($protocol !== 1) { } elseif ($protocol !== 1) {
$url = 'http://' . $url; $url = 'http://' . $url;
} }
return $url; return $url;
@ -260,7 +260,7 @@ class Url
if (! function_exists('idn_to_ascii') || ! isset($this->parts['host'])) { if (! function_exists('idn_to_ascii') || ! isset($this->parts['host'])) {
return $out; 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); return str_replace($this->parts['host'], $asciiHost, $out);
} }

View File

@ -134,4 +134,20 @@ class ApiUtils
return $oldLink; 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,
];
}
} }

View File

@ -36,7 +36,7 @@ class History extends ApiController
if (empty($offset)) { if (empty($offset)) {
$offset = 0; $offset = 0;
} }
else if (ctype_digit($offset)) { elseif (ctype_digit($offset)) {
$offset = (int) $offset; $offset = (int) $offset;
} else { } else {
throw new ApiBadParametersException('Invalid offset'); throw new ApiBadParametersException('Invalid offset');
@ -46,7 +46,7 @@ class History extends ApiController
$limit = $request->getParam('limit'); $limit = $request->getParam('limit');
if (empty($limit)) { if (empty($limit)) {
$limit = count($history); $limit = count($history);
} else if (ctype_digit($limit)) { } elseif (ctype_digit($limit)) {
$limit = (int) $limit; $limit = (int) $limit;
} else { } else {
throw new ApiBadParametersException('Invalid limit'); throw new ApiBadParametersException('Invalid limit');

View File

@ -59,25 +59,25 @@ class Links extends ApiController
$limit = $request->getParam('limit'); $limit = $request->getParam('limit');
if (empty($limit)) { if (empty($limit)) {
$limit = self::$DEFAULT_LIMIT; $limit = self::$DEFAULT_LIMIT;
} else if (ctype_digit($limit)) { } elseif (ctype_digit($limit)) {
$limit = intval($limit); $limit = intval($limit);
} else if ($limit === 'all') { } elseif ($limit === 'all') {
$limit = count($links); $limit = count($links);
} else { } else {
throw new ApiBadParametersException('Invalid limit'); throw new ApiBadParametersException('Invalid limit');
} }
// 'environment' is set by Slim and encapsulate $_SERVER. // 'environment' is set by Slim and encapsulate $_SERVER.
$index = index_url($this->ci['environment']); $indexUrl = index_url($this->ci['environment']);
$out = []; $out = [];
$cpt = 0; $index = 0;
foreach ($links as $link) { foreach ($links as $link) {
if (count($out) >= $limit) { if (count($out) >= $limit) {
break; break;
} }
if ($cpt++ >= $offset) { if ($index++ >= $offset) {
$out[] = ApiUtils::formatLink($link, $index); $out[] = ApiUtils::formatLink($link, $indexUrl);
} }
} }

View 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);
}
}

View 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);
}
}

View File

@ -123,7 +123,7 @@ class ConfigManager
* Supports nested settings with dot separated keys. * Supports nested settings with dot separated keys.
* *
* @param string $setting Asked setting, keys separated with dots. * @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 $write Write the new setting in the config file, default false.
* @param bool $isLoggedIn User login state, default false. * @param bool $isLoggedIn User login state, default false.
* *
@ -147,6 +147,33 @@ class ConfigManager
} }
} }
/**
* 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. * Check if a settings exists.
* *
@ -272,7 +299,7 @@ class ConfigManager
* *
* @param array $settings Ordered array which contains keys to find. * @param array $settings Ordered array which contains keys to find.
* @param mixed $value * @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. * @return mixed Found setting or NOT_FOUND flag.
*/ */
@ -289,6 +316,27 @@ class ConfigManager
$conf[$setting] = $value; $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. * Set a bunch of default values allowing Shaarli to start without a config file.
*/ */
@ -333,12 +381,12 @@ class ConfigManager
// default state of the 'remember me' checkbox of the login form // default state of the 'remember me' checkbox of the login form
$this->setEmpty('privacy.remember_user_default', true); $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.url', '');
$this->setEmpty('redirector.encode_url', true); $this->setEmpty('redirector.encode_url', true);
$this->setEmpty('thumbnails.width', '125');
$this->setEmpty('thumbnails.height', '90');
$this->setEmpty('translation.language', 'auto'); $this->setEmpty('translation.language', 'auto');
$this->setEmpty('translation.mode', 'php'); $this->setEmpty('translation.mode', 'php');
$this->setEmpty('translation.extensions', []); $this->setEmpty('translation.extensions', []);

View 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;
}
}

View 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
View 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>

View 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);
})();

View File

@ -0,0 +1,7 @@
import Blazy from 'blazy';
(() => {
// Suppress ESLint error because that's how bLazy works
/* eslint-disable no-new */
new Blazy();
})();

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 530 B

After

Width:  |  Height:  |  Size: 530 B

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

573
assets/default/js/base.js Normal file
View 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));
});
})();

View 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));
}
});
});
})();

File diff suppressed because it is too large Load Diff

View File

@ -113,7 +113,7 @@ a.bigbutton, #pageheader a.bigbutton {
} }
#pageheader #logo { #pageheader #logo {
background-image: url('../../../images/logo.png'); background-image: url('../img/logo.png');
background-repeat: no-repeat; background-repeat: no-repeat;
float: left; float: left;
margin: 0 10px 0 10px; margin: 0 10px 0 10px;
@ -433,7 +433,7 @@ a.bigbutton, #pageheader a.bigbutton {
} }
#linklist li.private { #linklist li.private {
background: url('../images/private.png') no-repeat 4px center; background: url('../img/private.png') no-repeat 4px center;
padding-left: 30px; padding-left: 30px;
} }
@ -465,7 +465,7 @@ a.bigbutton, #pageheader a.bigbutton {
} }
.linkdate a { .linkdate a {
background-image: url('../images/calendar.png'); background-image: url('../img/calendar.png');
padding: 2px 0 3px 20px; padding: 2px 0 3px 20px;
background-repeat: no-repeat; background-repeat: no-repeat;
text-decoration: none; text-decoration: none;
@ -516,7 +516,7 @@ a.bigbutton, #pageheader a.bigbutton {
height: 20px; height: 20px;
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
background-image: url('../images/tag_blue.png'); background-image: url('../img/tag_blue.png');
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 3px center; background-position: 3px center;
background-color: #ffffff; background-color: #ffffff;
@ -701,8 +701,8 @@ a.bigbutton, #pageheader a.bigbutton {
position: relative; position: relative;
display: table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
width: 90px; width: 120px;
height: 90px; height: 120px;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
float: left; float: left;
@ -739,9 +739,9 @@ a.bigbutton, #pageheader a.bigbutton {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 90px; width: 120px;
font-weight: bold; font-weight: bold;
font-size: 8pt; font-size: 9pt;
color: #fff; color: #fff;
text-align: left; text-align: left;
background-color: transparent; background-color: transparent;
@ -762,7 +762,7 @@ div.daily {
/* Background paper texture by BashCorpo: /* Background paper texture by BashCorpo:
http://www.bashcorpo.dk/textures.php http://www.bashcorpo.dk/textures.php
http://bashcorpo.deviantart.com/art/Grungy-paper-texture-v-5-22966998 */ 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; -webkit-background-size: cover;
-moz-background-size: cover; -moz-background-size: cover;
-o-background-size: cover; -o-background-size: cover;
@ -860,7 +860,7 @@ div.dailyEntryThumbnail {
width: 100%; width: 100%;
text-align: center; text-align: center;
background-color: rgb(128, 128, 128); background-color: rgb(128, 128, 128);
background: url(../images/50pc_transparent.png); background: url(../img/50pc_transparent.png);
padding: 4px 0px 2px 0px; padding: 4px 0px 2px 0px;
} }
@ -1210,3 +1210,43 @@ ul.errors {
width: 13px; width: 13px;
height: 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;
}

View File

Before

Width:  |  Height:  |  Size: 599 B

After

Width:  |  Height:  |  Size: 599 B

View File

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

Before

Width:  |  Height:  |  Size: 650 B

After

Width:  |  Height:  |  Size: 650 B

View File

Before

Width:  |  Height:  |  Size: 302 B

After

Width:  |  Height:  |  Size: 302 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 684 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 658 B

After

Width:  |  Height:  |  Size: 658 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 932 B

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 369 B

After

Width:  |  Height:  |  Size: 369 B

View File

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 813 B

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

Before

Width:  |  Height:  |  Size: 648 B

After

Width:  |  Height:  |  Size: 648 B

View File

Before

Width:  |  Height:  |  Size: 720 B

After

Width:  |  Height:  |  Size: 720 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 714 B

30
assets/vintage/js/base.js Normal file
View 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;
});
});
})();

View File

@ -11,20 +11,21 @@
"keywords": ["bookmark", "link", "share", "web"], "keywords": ["bookmark", "link", "share", "web"],
"config": { "config": {
"platform": { "platform": {
"php": "5.5.38" "php": "5.6.31"
} }
}, },
"require": { "require": {
"php": ">=5.5", "php": ">=5.6",
"shaarli/netscape-bookmark-parser": "^2.0", "shaarli/netscape-bookmark-parser": "^2.0",
"erusev/parsedown": "1.6", "erusev/parsedown": "^1.6",
"slim/slim": "^3.0", "slim/slim": "^3.0",
"arthurhoaro/web-thumbnailer": "^1.1",
"pubsubhubbub/publisher": "dev-master", "pubsubhubbub/publisher": "dev-master",
"gettext/gettext": "^4.4" "gettext/gettext": "^4.4"
}, },
"require-dev": { "require-dev": {
"phpmd/phpmd" : "@stable", "phpmd/phpmd" : "@stable",
"phpunit/phpunit": "4.8.*", "phpunit/phpunit": "^5.0",
"sebastian/phpcpd": "*", "sebastian/phpcpd": "*",
"squizlabs/php_codesniffer": "2.*", "squizlabs/php_codesniffer": "2.*",
"phpunit/phpcov": "*" "phpunit/phpcov": "*"
@ -36,7 +37,8 @@
"Shaarli\\Api\\Controllers\\": "application/api/controllers", "Shaarli\\Api\\Controllers\\": "application/api/controllers",
"Shaarli\\Api\\Exceptions\\": "application/api/exceptions", "Shaarli\\Api\\Exceptions\\": "application/api/exceptions",
"Shaarli\\Config\\": "application/config/", "Shaarli\\Config\\": "application/config/",
"Shaarli\\Config\\Exception\\": "application/config/exception" "Shaarli\\Config\\Exception\\": "application/config/exception",
"Shaarli\\Security\\": "application/security"
} }
} }
} }

852
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,21 @@
## CSS ## CSS
- Yahoo UI [CSS Reset](http://yuilibrary.com/yui/docs/cssreset/)
- resets default CSS properties for all HTML elements (overriding browsers' default values) - Yahoo UI [CSS Reset](http://yuilibrary.com/yui/docs/cssreset/) - standardize cross-browser rendering
- ensures custom CSS stylessheets will provide the same results on all browsers
## Javascript ## Javascript
- [Awesomeplete](https://leaverou.github.io/awesomplete/) ([GitHub](https://github.com/LeaVerou/awesomplete)) - autocompletion in input forms - [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 - [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 - [qr.js](http://neocotic.com/qr.js/) ([GitHub](https://github.com/neocotic/qr.js)) - QR code generation
## PHP ## 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 - [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

View File

@ -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

View File

@ -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 - [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 - [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 - [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 ### Mobile Apps
- [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension. - [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 - [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 - [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 ### 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 ### Server apps
- [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content - [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content

View File

@ -2,8 +2,8 @@
A [`Makefile`](https://github.com/shaarli/Shaarli/blob/master/Makefile) is available to perform project-related operations: 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 - Documentation - generate a local HTML copy of the GitHub wiki
- [Static analysis](Static analysis) - check that the code is compliant to PHP conventions - [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 - [Unit tests](Unit-tests) - ensure there are no regressions introduced by new commits
## Automatic builds ## Automatic builds
[Travis CI](http://docs.travis-ci.com/) is a Continuous Integration build server, that runs a build: [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 - updates Composer
- installs 3rd-party test dependencies with 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: After all jobs have finished, Travis returns the results to GitHub:

View File

@ -3,8 +3,11 @@
Please have a look at the following pages: Please have a look at the following pages:
- [Contributing to Shaarli](https://github.com/shaarli/Shaarli/tree/master/CONTRIBUTING.md) - [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-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 - [PSR-2](http://www.php-fig.org/psr/psr-2/) - Coding Style Guide
- [Unit tests](Unit tests) - [Unit tests](Unit-tests)
- [GnuPG signature](GnuPG signature) for tags/releases - 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

View File

@ -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: 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 index.php # Main program
application/ # Shaarli classes application/ # Shaarli classes
├── LinkDB.php ├── LinkDB.php
...
└── Utils.php └── Utils.php
tests/ # Shaarli unitary & functional tests tests/ # Shaarli unitary & functional tests
├── LinkDBTest.php ├── LinkDBTest.php
├── utils # utilities to ease testing
...
├── utils # utilities to ease testing
│ └── ReferenceLinkDB.php │ └── ReferenceLinkDB.php
└── UtilsTest.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 COPYING # Shaarli license
inc/ # static assets and 3rd party libraries inc/ # static assets and 3rd party libraries
├── awesomplete.* # tags autocompletion library └── rain.tpl.class.php # RainTPL templating 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.
images/ # Images and icons used in Shaarli images/ # Images and icons used in Shaarli
data/ # data storage: bookmark database, configuration, logs, banlist… data/ # data storage: bookmark database, configuration, logs, banlist...
├── config.php # Shaarli configuration (login, password, timezone, title) ├── config.json.php # Shaarli configuration (login, password, timezone, title...)
├── datastore.php # Your link database (compressed). ├── datastore.php # Your link database (compressed).
├── ipban.php # IP address ban system data ├── ipban.php # IP address ban system data
├── lastupdatecheck.txt # Update check timestamp file ├── 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 cache/ # thumbnails cache
# This directory is automatically created. You can erase it anytime you want. # This directory is automatically created. You can erase it anytime you want.
tmp/ # Temporary directory for compiled RainTPL templates. tmp/ # Temporary directory for compiled RainTPL templates.
# This directory is automatically created. You can erase it anytime you want. # This directory is automatically created. You can erase it anytime you want.
vendor/ # Third-party dependencies. This directory is created by Composer
``` ```

View File

@ -1,8 +1,7 @@
To install Shaarli, simply place the files in a directory under your webserver's To install Shaarli, simply place the files in a directory under your webserver's
Document Root (or directly at the document root). Document Root (or directly at the document root).
Also, please make sure your server meets the [requirements](Server-requirements) Also, please make sure your server is properly [configured](Server-configuration).
and is properly [configured](Server-configuration).
Multiple releases branches are available: Multiple releases branches are available:
@ -23,13 +22,13 @@ Using one of the following methods:
### Download as an archive ### 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 ```bash
$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.3/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.3-full.zip $ unzip shaarli-v0.9.7-full.zip
$ mv Shaarli /path/to/shaarli/ $ 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: 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. * Install [python3-virtualenv](https://pypi.python.org/pypi/virtualenv) to build the local HTML documentation.
``` ```
$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/ $ mkdir -p /path/to/shaarli && cd /path/to/shaarli/
$ git clone -b latest https://github.com/shaarli/Shaarli.git . $ git clone -b latest https://github.com/shaarli/Shaarli.git .
$ composer install --no-dev --prefer-dist $ composer install --no-dev --prefer-dist
$ make build_frontend
$ make translate $ make translate
$ make htmldoc $ make htmldoc
``` ```
@ -91,7 +92,9 @@ $ composer install --no-dev --prefer-dist
_Use at your own risk!_ _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: 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 # install/update third-party dependencies
$ cd /path/to/shaarli $ cd /path/to/shaarli
$ composer install --no-dev --prefer-dist $ composer install --no-dev --prefer-dist
$ make build_frontend
$ make translate $ make translate
$ make htmldoc $ make htmldoc
``` ```

View File

@ -22,7 +22,9 @@ With Shaarli:
Shaarli stands for _shaaring_ your _links_. Shaarli stands for _shaaring_ your _links_.
### My Shaarli is broken! ### 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: If everything looks right but the issue(s) remain(s), please:

View File

@ -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
View 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.

View File

@ -37,7 +37,7 @@ This is important in case plugins are depending on each other. Read plugins READ
## File mode ## 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 ```php
$GLOBALS['config']['ENABLED_PLUGINS'] $GLOBALS['config']['ENABLED_PLUGINS']
@ -48,7 +48,7 @@ Example:
```php ```php
$GLOBALS['config']['ENABLED_PLUGINS'] = array( $GLOBALS['config']['ENABLED_PLUGINS'] = array(
'qrcode', 'qrcode',
'archiveorg', 'archiveorg',
'wallabag', 'wallabag',
'markdown', 'markdown',

View File

@ -3,8 +3,9 @@
See the [REST API documentation](http://shaarli.github.io/api-documentation/) See the [REST API documentation](http://shaarli.github.io/api-documentation/)
for a list of available endpoints and parameters. for a list of available endpoints and parameters.
Please ensure that your server meets the [requirements](Server-requirements) Please ensure that your server meets the
and is properly [configured](Server-configuration): [requirements](Server-configuration#prerequisites) and is properly
[configured](Server-configuration):
- URL rewriting is enabled (see specific Apache and Nginx sections) - URL rewriting is enabled (see specific Apache and Nginx sections)
- the server's timezone is properly defined - 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 - [Documentation](http://python-shaarli-client.readthedocs.io/en/latest/) on ReadTheDocs
- [python-shaarli-client](https://github.com/shaarli/python-shaarli-client) on Github - [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
}
}
```

View File

@ -1,139 +1,130 @@
*Example virtual host configurations for popular web servers*
- [Prerequisites](#prerequisistes)
- [Apache](#apache) - [Apache](#apache)
- [Nginx](#nginx) - [Nginx](#nginx)
- [Proxies](#proxies)
- [See also](#see-also)
## Prerequisites ## Prerequisites
### Shaarli ### 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 - A web server and PHP interpreter module/service have been installed.
Related guides: - 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) Version | Status | Shaarli compatibility
- [How do I create my own Certificate Authority?](https://workaround.org/certificate-authority) :---:|:---:|:---:
- Generate a self-signed certificate (will trigger browser warnings) with apache2: 7.2 | Supported | Yes
`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` 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 - The following PHP extensions are installed on the server:
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` Extension | Required? | Usage
- `X-Forwarded-Host` ---|:---:|---
- `X-Forwarded-For` [`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 ## 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) In `/etc/apache2/sites-available/shaarli.conf`:
- [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).
```apache ```apache
<VirtualHost *:443> <VirtualHost *:443>
ServerName shaarli.my-domain.org ServerName shaarli.my-domain.org
DocumentRoot /absolute/path/to/shaarli/ 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 SSLEngine on
SSLCertificateFile /absolute/path/to/the/website/certificate.pem SSLCertificateFile /etc/letsencrypt/live/yourdomain.example.com/fullchain.pem
SSLCertificateKeyFile /absolute/path/to/the/website/key.key 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/> <Directory /absolute/path/to/shaarli/>
#Required for .htaccess support
AllowOverride All AllowOverride All
Options Indexes FollowSymLinks MultiViews
Order allow,deny 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> </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> </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. _Note: Apache module `mod_rewrite` must be enabled to use the REST API._
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.
## LightHttpd
## Nginx ## 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) <!--- TODO refactor everything below this point --->
- [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)
### Common setup ### Common setup
Once Nginx and PHP-FPM are installed, we need to ensure: 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/)

View File

@ -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
View 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.

View File

@ -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`. 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 ### Extend Shaarli's translation
If you're writing a custom theme, or a non official plugin, you might want to use the translation system, If you're writing a custom theme, or a non official plugin, you might want to use the translation system,

View File

@ -63,7 +63,7 @@ Related threads:
### I forgot my password! ### 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 ### I'm locked out - Login bruteforce protection
@ -97,7 +97,7 @@ php56 1
```php ```php
//list($status,$headers,$data) = getHTTP($url,4); // Short timeout to keep the application responsive. //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); //if (strpos($status,'200 OK')) $title=html_extract_title($data);
``` ```
@ -106,11 +106,11 @@ php56 1
### Dates are not properly formatted ### 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 :-( ) 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
### Problems on CentOS servers (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
On **CentOS**/RedHat derivatives, you may need to install the `php-mbstring` package. (it's the case on sebsauvage.net :-( )
### My session expires! I can't stay logged in ### 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 ## 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)). 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`

View File

@ -8,7 +8,7 @@ Read first:
### Docker test images ### 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 and can be used to build Docker images to run Shaarli test suites under common
Linux environments. Linux environments.
@ -27,7 +27,7 @@ What's behind the curtains:
- test PHP dependencies (OS packages) - test PHP dependencies (OS packages)
- Composer - Composer
- the local workspace is mapped to the container's `/shaarli/` directory, - 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) (running tests as `root` would bypass permission checks and may hide issues)
- the tests are run inside the container. - the tests are run inside the container.
@ -36,7 +36,7 @@ What's behind the curtains:
```bash ```bash
# build the Debian 9 Docker image # build the Debian 9 Docker image
$ cd /path/to/shaarli $ cd /path/to/shaarli
$ cd docker/test/debian9 $ cd tests/docker/debian9
$ docker build -t shaarli-test:debian9 . $ docker build -t shaarli-test:debian9 .
``` ```

Some files were not shown because too many files have changed in this diff Show More