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

View file

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

33
.gitattributes vendored
View file

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

10
.gitignore vendored
View file

@ -40,3 +40,13 @@ tpl/*
contact.php
formStyle.css
# Front end
node_modules
tpl/default/js
tpl/default/css
tpl/default/fonts
tpl/default/img
tpl/vintage/js
tpl/vintage/css
tpl/vintage/img

View file

@ -14,3 +14,35 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
<Limit GET POST PUT DELETE OPTIONS>
<IfModule version_module>
<IfVersion >= 2.4>
Require all granted
</IfVersion>
<IfVersion < 2.4>
Allow from all
Deny from none
</IfVersion>
</IfModule>
<IfModule !version_module>
Require all granted
</IfModule>
</Limit>
<LimitExcept GET POST PUT DELETE OPTIONS>
<IfModule version_module>
<IfVersion >= 2.4>
Require all denied
</IfVersion>
<IfVersion < 2.4>
Allow from none
Deny from all
</IfVersion>
</IfModule>
<IfModule !version_module>
Require all denied
</IfModule>
</LimitExcept>

View file

@ -1,20 +1,53 @@
sudo: false
dist: trusty
language: php
matrix:
include:
- language: php
php: 7.2
- language: php
php: 7.1
- language: php
php: 7.0
- language: php
php: 5.6
- language: node_js
node_js: 8
cache:
yarn: true
directories:
- $HOME/.cache/yarn
install:
- yarn install
before_script:
- PATH=${PATH//:\.\/node_modules\/\.bin/}
script:
- yarn run build # Just to be sure that the build isn't broken
- make eslint
- make sasslint
- language: python
python: 3.6
cache:
directories:
- $HOME/.cache/pip
install:
- pip install mkdocs
script:
- mkdocs build --clean
cache:
directories:
- $HOME/.composer/cache
php:
- 7.1
- 7.0
- 5.6
- 5.5
install:
- composer self-update
- composer install --prefer-dist
- locale -a
before_script:
- PATH=${PATH//:\.\/node_modules\/\.bin/}
script:
- make clean
- make check_permissions

20
AUTHORS
View file

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

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/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [v0.10.2](https://github.com/shaarli/Shaarli/releases/tag/v0.10.2) - 2018-08-11
### Fixed
- Docker build
## [v0.10.1](https://github.com/shaarli/Shaarli/releases/tag/v0.10.1) - 2018-08-11
### Changed
- Accessibility:
- Remove alt text on the logo
- Remove redundant title in tools page
### Fixed
- Fixed an error on the daily page and daily RSS
- Fixed an issue causing 'You are not authorized to add a link' error while logged out
- Fixed thumbnail path when Shaarli's path uses symbolic links
- Add a `mod_version` check in Shaarli's root `.htaccess` file for Apache 2.2 syntax
- Include assets in the release Makefile target
### Removed
- Firefox Social API shaare has been removed
## [v0.10.0](https://github.com/shaarli/Shaarli/releases/tag/v0.10.0) - 2018-07-28
**PHP 5.5 compatibility has been dropped.** Shaarli now requires at least PHP 5.6.
### Added
- Add a filter to display public links only
- Add PHP 7.2 support
- Add German translation
- Resolve front-end dependencies from NPM
- Build front-end bundles with Yarn and Webpack
- Lint Javascript code with ESLint
- Lint SASS code with SASSLint
- Support redirection in cURL download callback
- Introduce multi-stage builds for Docker images
- Use Travis matrix and stages to run Javascript tests in a dedicated environment
- Add tag endpoint in the REST API
- Build the documentation in Travis builds
- Provide a Docker Compose example
### Changed
- Use web-thumbnailer to retrieve thumbnails (see #687)
- Use a specific page title in all pages
- Daily: run hooks before creating the columns
- Load theme translations files automatically
- Make max download size and timeout configurable
- Make Nginx logs accessible as stdout/stderr for Docker images
- Update buttons used to toggle link visibility filters
- Rewrite Javascript code for ES6 compliance
- Refactor IP ban management
- Refactor user login management
- Refactor server-side session management
- Update Doxygen configuration
- Update Parsedown
- Improve documentation
- Docker: build the images from the local sources
- Docker: bump alpine version to 3.7
- Docker: expose a volume for the thumbnail cache
### Removed
- Drop support for PHP 5.5
- Remove vendored front-end libraries
- Remove environment specific .gitignore entries
### Fixed
- Ignore the case while checking DOCTYPE during the file import
- Fix removal of on=... attributes from html generated from Markdown
- httpd: always forward the 'Authorization' header
- Ensure user-specific CSS file is loaded
- Fix feed permalink rendering when Markdown escaping is enabled
- Fix order of tags with the same number of occurrences
- Fixed the referrer meta tag in default template
- Disable MkDocs' strict mode for ReadTheDocs builds to pass
- fix and simplify Dockerfile for armhf
### Security
- Update `.htaccess` to prevent accessing Git metadata when using a Git-based installation
## [v0.9.7](https://github.com/shaarli/Shaarli/releases/tag/v0.9.7) - 2018-06-20
### Changed
- Build the Docker images from the local Git sources
@ -240,6 +323,19 @@ Theming:
- Editing a link created before the new ID system would change its permalink.
## [v0.8.7](https://github.com/shaarli/Shaarli/releases/tag/v0.8.7) - 2018-06-20
### Changed
- Build the Docker image from the local Git sources
### Removed
- Disable PHP 5.3 Travis build (unsupported)
## [v0.8.6](https://github.com/shaarli/Shaarli/releases/tag/v0.8.6) - 2018-02-19
### Changed
- Run version check tests against the 'stable' branch
## [v0.8.5](https://github.com/shaarli/Shaarli/releases/tag/v0.8.5) - 2018-01-04
**XSS vulnerability fixed. Please update.**

38
COPYING
View file

@ -1,55 +1,57 @@
Files: *
License: zlib/libpng
Copyright: (c) 2011-2015 Sébastien SAUVAGE <sebsauvage@sebsauvage.net>
(c) 2011-2017 The Shaarli Community, see AUTHORS
(c) 2011-2018 The Shaarli Community, see AUTHORS
Files: inc/reset.css
Files: assets/vintage/css/reset.css
License: BSD (http://opensource.org/licenses/BSD-3-Clause)
Copyright: (c) 2010, Yahoo! Inc.
Files: images/calendar.png, images/edit_icon.png, images/feed-icon-14x14.png, images/private.png, images/private_16x16.png, images/private_16x16_active.png, images/tag_blue.png
Files: assets/vintage/img/calendar.png
assets/vintage/img/edit_icon.png
assets/vintage/img/feed-icon-14x14.png
assets/vintage/img/private.png
assets/vintage/img/private_16x16.png
assets/vintage/img/private_16x16_active.png
assets/vintage/img/tag_blue.png
License: CC-BY (http://creativecommons.org/licenses/by/3.0/)
Copyright: (c) 2014 Yusuke Kamiyamane
Source: http://p.yusukekamiyamane.com/
Files: images/delete_icon.png
Files: assets/vintage/img/delete_icon.png
License: CC-BY (http://creativecommons.org/licenses/by/3.0/)
Copyright: (c) 2014 Designmodo
Source: http://designmodo.com/linecons-free/
Files: images/floral_left.png, images/floral_right.png, images/squiggle.png, images/squiggle_closing.png
Files: assets/vintage/img/floral_left.png
assets/vintage/img/floral_right.png
assets/vintage/img/squiggle.png
assets/vintage/img/squiggle_closing.png
Licence: Public Domain
Source: https://openclipart.org/people/j4p4n/j4p4n_ornimental_bookend_-_left.svg
Files: images/Paper_texture_v5_by_bashcorpo_w1000.jpg
Files: assets/vintage/img/Paper_texture_v5_by_bashcorpo_w1000.jpg
Licence: Public Domain
Source: http://bashcorpo.deviantart.com/art/Grungy-paper-texture-v-5-22966998
Files: images/logo.png
Files: assets/vintage/img/logo.png
assets/vintage/img/logo.png
License: zlib/libpng
Copyright: (c) 2011-2014 idleman idleman@idleman.fr
Files: inc/blazy*.js
Files: assets/default/img/sad_star.png
License: MIT License (http://opensource.org/licenses/MIT)
Copyright: (C) Bjoern Klinggaard - @bklinggaard - http://dinbror.dk/blazy
Copyright: (C) 2015 kalvn - https://github.com/kalvn/Shaarli-Material
Files: inc/rain.tpl.class.php
License: LGPL-3+ (https://www.gnu.org/licenses/lgpl-3.0.txt)
Copyright: 2011-2012, Federico Ulfo <rainelemental@gmail.com>
2011-2012, The Rain Team <hello@raintm.com>
License: LGPL-3+ (https://www.gnu.org/licenses/lgpl-3.0.txt)
Files: inc/awesomplete*
License: MIT License (http://opensource.org/licenses/MIT)
Copyright: (C) 2015 Lea Verou - https://github.com/LeaVerou/awesomplete
Files: plugins/wallabag/wallabag.png
License: MIT License (http://opensource.org/licenses/MIT)
Copyright: (C) 2015 Nicolas Lœuillet - https://github.com/wallabag/wallabag
Files: tpl/default/sad_star.png
License: MIT License (http://opensource.org/licenses/MIT)
Copyright: (C) 2015 kalvn - https://github.com/kalvn/Shaarli-Material
----------------------------------------------------
ZLIB/LIBPNG LICENSE

View file

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

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
# the logo to the output directory.
PROJECT_LOGO = images/logo.png
PROJECT_LOGO = doc/md/images/logo.png
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
# into which the generated documentation will be written. If a relative path is
@ -804,6 +804,7 @@ RECURSIVE = YES
# run.
EXCLUDE = vendor \
data \
tpl \
inc \
doc \

View file

@ -157,21 +157,32 @@ composer_dependencies: clean
composer install --no-dev --prefer-dist
find vendor/ -name ".git" -type d -exec rm -rf {} +
### download 3rd-party frontend libraries
frontend_dependencies:
yarn install
### Build frontend dependencies
build_frontend: frontend_dependencies
yarn run build
### generate a release tarball and include 3rd-party dependencies and translations
release_tar: composer_dependencies htmldoc translate
release_tar: composer_dependencies htmldoc translate build_frontend
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^tpl|$(ARCHIVE_PREFIX)tpl|" tpl/
gzip $(ARCHIVE_VERSION).tar
### generate a release zip and include 3rd-party dependencies and translations
release_zip: composer_dependencies htmldoc translate
release_zip: composer_dependencies htmldoc translate build_frontend
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)doc/
rsync -a vendor/ $(ARCHIVE_PREFIX)vendor/
zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)vendor/
rsync -a tpl/ $(ARCHIVE_PREFIX)tpl/
zip -r $(ARCHIVE_VERSION).zip $(ARCHIVE_PREFIX)tpl/
rm -rf $(ARCHIVE_PREFIX)
##
@ -192,18 +203,27 @@ authors:
### generate Doxygen documentation
doxygen: clean
@rm -rf doxygen
@( cat Doxyfile ; echo "PROJECT_NUMBER=`git describe`" ) | doxygen -
@doxygen Doxyfile
### generate HTML documentation from Markdown pages with MkDocs
htmldoc:
python3 -m venv venv/
bash -c 'source venv/bin/activate; \
pip install mkdocs; \
mkdocs build'
mkdocs build --clean'
find doc/html/ -type f -exec chmod a-x '{}' \;
rm -r venv
### Generate Shaarli's translation compiled file (.mo)
translate:
@find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \;
@find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \;
### Run ESLint check against Shaarli's JS files
eslint:
@yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/
@yarn run eslint -c .dev/.eslintrc.js assets/default/js/
### Run CSSLint check against Shaarli's SCSS files
sasslint:
@yarn run sass-lint -c .dev/.sasslintrc 'assets/default/scss/*.scss' -v -q

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._
_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)
&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)
&bull;
[![](https://img.shields.io/badge/master-v0.10.x-blue.svg)](https://github.com/shaarli/Shaarli)

View file

@ -37,7 +37,7 @@ public static function writeFlatDB($file, $content)
if (is_file($file) && !is_writeable($file)) {
// The datastore exists but is not writeable
throw new IOException($file);
} else if (!is_file($file) && !is_writeable(dirname($file))) {
} elseif (!is_file($file) && !is_writeable(dirname($file))) {
// The datastore does not exist and its parent directory is not writeable
throw new IOException(dirname($file));
}

View file

@ -1,7 +1,7 @@
<?php
/**
* GET an HTTP URL to retrieve its content
* Uses the cURL library or a fallback method
* Uses the cURL library or a fallback method
*
* @param string $url URL to get (http://...)
* @param int $timeout network timeout (in seconds)
@ -415,6 +415,37 @@ function getIpAddressFromProxy($server, $trustedIps)
return array_pop($ips);
}
/**
* Return an identifier based on the advertised client IP address(es)
*
* This aims at preventing session hijacking from users behind the same proxy
* by relying on HTTP headers.
*
* See:
* - https://secure.php.net/manual/en/reserved.variables.server.php
* - https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php
* - https://stackoverflow.com/questions/12233406/preventing-session-hijacking
* - https://stackoverflow.com/questions/21354859/trusting-x-forwarded-for-to-identify-a-visitor
*
* @param array $server The $_SERVER array
*
* @return string An identifier based on client IP address information
*/
function client_ip_id($server)
{
$ip = $server['REMOTE_ADDR'];
if (isset($server['HTTP_X_FORWARDED_FOR'])) {
$ip = $ip . '_' . $server['HTTP_X_FORWARDED_FOR'];
}
if (isset($server['HTTP_CLIENT_IP'])) {
$ip = $ip . '_' . $server['HTTP_CLIENT_IP'];
}
return $ip;
}
/**
* Returns true if Shaarli's currently browsed in HTTPS.
* Supports reverse proxies (if the headers are correctly set).

View file

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

View file

@ -436,15 +436,17 @@ public function filterSearch($filterRequest = array(), $casesensitive = false, $
/**
* Returns the list tags appearing in the links with the given tags
* @param $filteringTags: tags selecting the links to consider
* @param $visibility: process only all/private/public links
* @return: a tag=>linksCount array
*
* @param array $filteringTags tags selecting the links to consider
* @param string $visibility process only all/private/public links
*
* @return array tag => linksCount
*/
public function linksCountPerTag($filteringTags = [], $visibility = 'all')
{
$links = empty($filteringTags) ? $this->links : $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
$tags = array();
$caseMapping = array();
$links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
$tags = [];
$caseMapping = [];
foreach ($links as $link) {
foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
if (empty($tag)) {
@ -458,8 +460,19 @@ public function linksCountPerTag($filteringTags = [], $visibility = 'all')
$tags[$caseMapping[strtolower($tag)]]++;
}
}
// Sort tags by usage (most used tag first)
arsort($tags);
/*
* Formerly used arsort(), which doesn't define the sort behaviour for equal values.
* Also, this function doesn't produce the same result between PHP 5.6 and 7.
*
* So we now use array_multisort() to sort tags by DESC occurrences,
* then ASC alphabetically for equal values.
*
* @see https://github.com/shaarli/Shaarli/issues/1142
*/
$keys = array_keys($tags);
$tmpTags = array_combine($keys, $keys);
array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
return $tags;
}

View file

@ -117,7 +117,7 @@ private function noFilter($visibility = 'all')
foreach ($this->links as $key => $value) {
if ($value['private'] && $visibility === 'private') {
$out[$key] = $value;
} else if (! $value['private'] && $visibility === 'public') {
} elseif (! $value['private'] && $visibility === 'public') {
$out[$key] = $value;
}
}
@ -210,7 +210,7 @@ private function filterFulltext($searchterms, $visibility = 'all')
if ($visibility !== 'all') {
if (! $link['private'] && $visibility === 'private') {
continue;
} else if ($link['private'] && $visibility === 'public') {
} elseif ($link['private'] && $visibility === 'public') {
continue;
}
}
@ -337,7 +337,7 @@ public function filterTags($tags, $casesensitive = false, $visibility = 'all')
if ($visibility !== 'all') {
if (! $link['private'] && $visibility === 'private') {
continue;
} else if ($link['private'] && $visibility === 'public') {
} elseif ($link['private'] && $visibility === 'public') {
continue;
}
}
@ -380,7 +380,7 @@ public function filterUntagged($visibility)
if ($visibility !== 'all') {
if (! $link['private'] && $visibility === 'private') {
continue;
} else if ($link['private'] && $visibility === 'public') {
} elseif ($link['private'] && $visibility === 'public') {
continue;
}
}

View file

@ -11,6 +11,7 @@
*/
function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo')
{
$isRedirected = false;
/**
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
*
@ -22,16 +23,24 @@ function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_get
*
* @return int|bool length of $data or false if we need to stop the download
*/
return function(&$ch, $data) use ($curlGetInfo, &$charset, &$title) {
return function(&$ch, $data) use ($curlGetInfo, &$charset, &$title, &$isRedirected) {
$responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
if (!empty($responseCode) && $responseCode != 200) {
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
$isRedirected = true;
return strlen($data);
}
if (!empty($responseCode) && $responseCode !== 200) {
return false;
}
$contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
// After a redirection, the content type will keep the previous request value
// until it finds the next content-type header.
if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
$contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
}
if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
return false;
}
if (empty($charset)) {
if (!empty($contentType) && empty($charset)) {
$charset = header_extract_charset($contentType);
}
if (empty($charset)) {

View file

@ -108,7 +108,7 @@ public static function import($post, $files, $linkDb, $conf, $history)
$filesize = $files['filetoupload']['size'];
$data = file_get_contents($files['filetoupload']['tmp_name']);
if (strpos($data, '<!DOCTYPE NETSCAPE-Bookmark-file-1>') === false) {
if (preg_match('/<!DOCTYPE NETSCAPE-Bookmark-file-1>/i', $data) === 0) {
return self::importStatus($filename, $filesize);
}
@ -154,13 +154,13 @@ public static function import($post, $files, $linkDb, $conf, $history)
if (empty($post['privacy']) || $post['privacy'] == 'default') {
// use value from the imported file
$private = $bkm['pub'] == '1' ? 0 : 1;
} else if ($post['privacy'] == 'private') {
} elseif ($post['privacy'] == 'private') {
// all imported links are private
$private = 1;
} else if ($post['privacy'] == 'public') {
} elseif ($post['privacy'] == 'public') {
// all imported links are public
$private = 0;
}
}
$newLink = array(
'title' => $bkm['title'],

View file

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

View file

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

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\ConfigPhp;
use Shaarli\Config\ConfigManager;
use Shaarli\Thumbnailer;
/**
* Class Updater.
@ -30,6 +31,11 @@ class Updater
*/
protected $isLoggedIn;
/**
* @var array $_SESSION
*/
protected $session;
/**
* @var ReflectionMethod[] List of current class methods.
*/
@ -42,13 +48,17 @@ class Updater
* @param LinkDB $linkDB LinkDB instance.
* @param ConfigManager $conf Configuration Manager instance.
* @param boolean $isLoggedIn True if the user is logged in.
* @param array $session $_SESSION (by reference)
*
* @throws ReflectionException
*/
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
{
$this->doneUpdates = $doneUpdates;
$this->linkDB = $linkDB;
$this->conf = $conf;
$this->isLoggedIn = $isLoggedIn;
$this->session = &$session;
// Retrieve all update methods.
$class = new ReflectionClass($this);
@ -445,6 +455,68 @@ public function updateMethodReorderDatastore()
$this->linkDB->save($this->conf->get('resource.page_cache'));
return true;
}
/**
* Change privateonly session key to visibility.
*/
public function updateMethodVisibilitySession()
{
if (isset($_SESSION['privateonly'])) {
unset($_SESSION['privateonly']);
$_SESSION['visibility'] = 'private';
}
return true;
}
/**
* Add download size and timeout to the configuration file
*
* @return bool true if the update is successful, false otherwise.
*/
public function updateMethodDownloadSizeAndTimeoutConf()
{
if ($this->conf->exists('general.download_max_size')
&& $this->conf->exists('general.download_timeout')
) {
return true;
}
if (! $this->conf->exists('general.download_max_size')) {
$this->conf->set('general.download_max_size', 1024*1024*4);
}
if (! $this->conf->exists('general.download_timeout')) {
$this->conf->set('general.download_timeout', 30);
}
$this->conf->write($this->isLoggedIn);
return true;
}
/**
* * Move thumbnails management to WebThumbnailer, coming with new settings.
*/
public function updateMethodWebThumbnailer()
{
if ($this->conf->exists('thumbnails.mode')) {
return true;
}
$thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
$this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
$this->conf->set('thumbnails.width', 125);
$this->conf->set('thumbnails.height', 90);
$this->conf->remove('thumbnail');
$this->conf->write(true);
if ($thumbnailsEnabled) {
$this->session['warnings'][] = t(
'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
);
}
return true;
}
}
/**

View file

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

View file

@ -134,4 +134,20 @@ public static function updateLink($oldLink, $newLink)
return $oldLink;
}
/**
* Format a Tag for the REST API.
*
* @param string $tag Tag name
* @param int $occurrences Number of links using this tag
*
* @return array Link data formatted for the REST API.
*/
public static function formatTag($tag, $occurences)
{
return [
'name' => $tag,
'occurrences' => $occurences,
];
}
}

View file

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

View file

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

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 @@ public function get($setting, $default = '')
* Supports nested settings with dot separated keys.
*
* @param string $setting Asked setting, keys separated with dots.
* @param string $value Value to set.
* @param mixed $value Value to set.
* @param bool $write Write the new setting in the config file, default false.
* @param bool $isLoggedIn User login state, default false.
*
@ -147,6 +147,33 @@ public function set($setting, $value, $write = false, $isLoggedIn = false)
}
}
/**
* Remove a config element from the config file.
*
* @param string $setting Asked setting, keys separated with dots.
* @param bool $write Write the new setting in the config file, default false.
* @param bool $isLoggedIn User login state, default false.
*
* @throws \Exception Invalid
*/
public function remove($setting, $write = false, $isLoggedIn = false)
{
if (empty($setting) || ! is_string($setting)) {
throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
}
// During the ConfigIO transition, map legacy settings to the new ones.
if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
$setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
}
$settings = explode('.', $setting);
self::removeConfig($settings, $this->loadedConfig);
if ($write) {
$this->write($isLoggedIn);
}
}
/**
* Check if a settings exists.
*
@ -272,7 +299,7 @@ protected static function getConfig($settings, $conf)
*
* @param array $settings Ordered array which contains keys to find.
* @param mixed $value
* @param array $conf Loaded settings, then sub-array.
* @param array $conf Loaded settings, then sub-array.
*
* @return mixed Found setting or NOT_FOUND flag.
*/
@ -289,6 +316,27 @@ protected static function setConfig($settings, $value, &$conf)
$conf[$setting] = $value;
}
/**
* Recursive function which find asked setting in the loaded config and deletes it.
*
* @param array $settings Ordered array which contains keys to find.
* @param array $conf Loaded settings, then sub-array.
*
* @return mixed Found setting or NOT_FOUND flag.
*/
protected static function removeConfig($settings, &$conf)
{
if (!is_array($settings) || count($settings) == 0) {
return self::$NOT_FOUND;
}
$setting = array_shift($settings);
if (count($settings) > 0) {
return self::removeConfig($settings, $conf[$setting]);
}
unset($conf[$setting]);
}
/**
* Set a bunch of default values allowing Shaarli to start without a config file.
*/
@ -333,12 +381,12 @@ protected function setDefaultValues()
// default state of the 'remember me' checkbox of the login form
$this->setEmpty('privacy.remember_user_default', true);
$this->setEmpty('thumbnail.enable_thumbnails', true);
$this->setEmpty('thumbnail.enable_localcache', true);
$this->setEmpty('redirector.url', '');
$this->setEmpty('redirector.encode_url', true);
$this->setEmpty('thumbnails.width', '125');
$this->setEmpty('thumbnails.height', '90');
$this->setEmpty('translation.language', 'auto');
$this->setEmpty('translation.mode', 'php');
$this->setEmpty('translation.extensions', []);

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

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 KiB

After

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

852
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,21 @@
## CSS
- Yahoo UI [CSS Reset](http://yuilibrary.com/yui/docs/cssreset/)
- resets default CSS properties for all HTML elements (overriding browsers' default values)
- ensures custom CSS stylessheets will provide the same results on all browsers
- Yahoo UI [CSS Reset](http://yuilibrary.com/yui/docs/cssreset/) - standardize cross-browser rendering
## Javascript
- [Awesomeplete](https://leaverou.github.io/awesomplete/) ([GitHub](https://github.com/LeaVerou/awesomplete)) - autocompletion in input forms
- [bLazy](http://dinbror.dk/blazy/) ([GitHub](https://github.com/dinbror/blazy)) - lazy loading for thumbnails
- [qr.js](http://neocotic.com/qr.js/) ([GitHub](https://github.com/neocotic/qr.js)) - QR code generation
## PHP
- [shaarli/netscape-bookmark-parser](https://github.com/shaarli/netscape-bookmark-parser) - Netscape bookmark parser
- [RainTPL](https://github.com/rainphp/raintpl) - HTML templating for PHP
### Composer
Library | Usage
---|---
[`shaarli/netscape-bookmark-parser`](https://packagist.org/packages/shaarli/netscape-bookmark-parser) | Import bookmarks from Netscape files
[`erusev/parsedown`](https://packagist.org/packages/erusev/parsedown) | Parse MarkDown syntax for the MarkDown plugin
[`slim/slim`](https://packagist.org/packages/slim/slim) | Handle routes and middleware for the REST API

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
- [octopress-shaarli](https://github.com/ahmet2mir/octopress-shaarli) - Octopress plugin to retrieve Shaarli links on the sidebar
- [Scuttle to Shaarli](https://github.com/q2apro/scuttle-to-shaarli) - Import bookmarks from Scuttle
- [Shaarli app for Cloudron](https://git.cloudron.io/cloudron/shaarli-app) - Effortlessly run Shaarli with the help of [Cloudron](https://cloudron.io/) [![Install](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=com.github.shaarli)
- [Shaarli_ynh](https://github.com/YunoHost-Apps/shaarli_ynh) - Shaarli is available as a [Yunohost](https://yunohost.org) app [![Install Shaarli with YunoHost](https://install-app.yunohost.org/install-with-yunohost.png)](https://install-app.yunohost.org/?app=shaarli)
### Mobile Apps
- [ShaarliOS](https://github.com/mro/ShaarliOS) - Apple iOS share extension.
- [Shaarli for Android](http://sebsauvage.net/links/?ZAyDzg) - Android application that adds Shaarli as a sharing provider
- [Shaarlier for Android](https://github.com/dimtion/Shaarlier) - Android application to simply add links directly into your Shaarli
- [Stakali for Android](https://stakali.toneiv.eu) - Stakali is a personal bookmark manager which synchronizes with Shaarli
### Browser addons
* [Shaarli Web Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli.
- [Shaarli Firefox Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli.
- [Shaarli Chrome Extension](https://github.com/octplane/Shiny-Shaarli) - toolbar button to share your current tab with Shaarli.
### Server apps
- [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content

View file

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

View file

@ -3,8 +3,11 @@
Please have a look at the following pages:
- [Contributing to Shaarli](https://github.com/shaarli/Shaarli/tree/master/CONTRIBUTING.md)
- [Static analysis](Static analysis) - patches should try to stick to the [PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), especially:
- [Static analysis](Static-analysis) - patches should try to stick to the
[PHP Standard Recommendations](http://www.php-fig.org/psr/) (PSR), especially:
- [PSR-1](http://www.php-fig.org/psr/psr-1/) - Basic Coding Standard
- [PSR-2](http://www.php-fig.org/psr/psr-2/) - Coding Style Guide
- [Unit tests](Unit tests)
- [GnuPG signature](GnuPG signature) for tags/releases
- [Unit tests](Unit-tests)
- Javascript linting - Shaarli uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript).
Run `make eslint` to check JS style.
- [GnuPG signature](GnuPG-signature) for tags/releases

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:
@ -6,29 +6,49 @@ Here is the directory structure of Shaarli and the purpose of the different file
index.php # Main program
application/ # Shaarli classes
├── LinkDB.php
...
└── Utils.php
tests/ # Shaarli unitary & functional tests
tests/ # Shaarli unitary & functional tests
├── LinkDBTest.php
├── utils # utilities to ease testing
...
├── utils # utilities to ease testing
│ └── ReferenceLinkDB.php
└── UtilsTest.php
assets/
├── common/ # Assets shared by multiple themes
├── ...
├── default/ # Assets for the default template, before compilation
├── fonts/ # Font files
├── img/ # Images used by the default theme
├── js/ # JavaScript files in ES6 syntax
├── scss/ # SASS files
└── vintage/ # Assets for the vintage template, before compilation
└── ...
COPYING # Shaarli license
inc/ # static assets and 3rd party libraries
├── awesomplete.* # tags autocompletion library
├── blazy.* # picture wall lazy image loading library
├── shaarli.css, reset.css # Shaarli stylesheet.
├── qr.* # qr code generation library
└──rain.tpl.class.php # RainTPL templating library
tpl/ # RainTPL templates for Shaarli. They are used to build the pages.
└── rain.tpl.class.php # RainTPL templating library
images/ # Images and icons used in Shaarli
data/ # data storage: bookmark database, configuration, logs, banlist…
├── config.php # Shaarli configuration (login, password, timezone, title…)
data/ # data storage: bookmark database, configuration, logs, banlist...
├── config.json.php # Shaarli configuration (login, password, timezone, title...)
├── datastore.php # Your link database (compressed).
├── ipban.php # IP address ban system data
├── lastupdatecheck.txt # Update check timestamp file
└──log.txt # login/IPban log.
└── log.txt # login/IPban log.
tpl/ # RainTPL templates for Shaarli. They are used to build the pages.
├── default/ # Default Shaarli theme
├── fonts/ # Font files
├── img/ # Images
├── js/ # JavaScript files compiled by Babel and compatible with all browsers
├── css/ # CSS files compiled with SASS
└── vintage/ # Legacy Shaarli theme
└── ...
cache/ # thumbnails cache
# This directory is automatically created. You can erase it anytime you want.
tmp/ # Temporary directory for compiled RainTPL templates.
# This directory is automatically created. You can erase it anytime you want.
vendor/ # Third-party dependencies. This directory is created by Composer
```

View file

@ -1,8 +1,7 @@
To install Shaarli, simply place the files in a directory under your webserver's
Document Root (or directly at the document root).
Also, please make sure your server meets the [requirements](Server-requirements)
and is properly [configured](Server-configuration).
Also, please make sure your server is properly [configured](Server-configuration).
Multiple releases branches are available:
@ -23,13 +22,13 @@ Using one of the following methods:
### Download as an archive
In most cases, you should download the latest Shaarli release from the [releases](https://github.com/shaarli/Shaarli/releases) page. **Download our *shaarli-full* archive** to include dependencies.
In most cases, you should download the latest Shaarli release from the [releases](https://github.com/shaarli/Shaarli/releases) page. Download our **shaarli-full** archive to include dependencies.
The current latest released version is `v0.9.3`
The current latest released version is `v0.9.7`
```bash
$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.3/shaarli-v0.9.3-full.zip
$ unzip shaarli-v0.9.3-full.zip
$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.7/shaarli-v0.9.7-full.zip
$ unzip shaarli-v0.9.7-full.zip
$ mv Shaarli /path/to/shaarli/
```
@ -37,13 +36,15 @@ $ mv Shaarli /path/to/shaarli/
Cloning using `git` or downloading Github branches as zip files requires additional steps:
* Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
* Install [Composer](Unit-tests.md#install_composer) to manage third-party [PHP dependencies](3rd-party-libraries.md#composer).
* Install [yarn](https://yarnpkg.com/lang/en/docs/install/) to build the frontend dependencies.
* Install [python3-virtualenv](https://pypi.python.org/pypi/virtualenv) to build the local HTML documentation.
```
$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/
$ git clone -b latest https://github.com/shaarli/Shaarli.git .
$ composer install --no-dev --prefer-dist
$ make build_frontend
$ make translate
$ make htmldoc
```
@ -91,7 +92,9 @@ $ composer install --no-dev --prefer-dist
_Use at your own risk!_
Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
Install [Composer](Unit-tests.md#install_composer) to manage Shaarli PHP dependencies,
and [yarn](https://yarnpkg.com/lang/en/docs/install/)
for front-end dependencies.
To get the latest changes from the `master` branch:
@ -101,6 +104,7 @@ $ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
# install/update third-party dependencies
$ cd /path/to/shaarli
$ composer install --no-dev --prefer-dist
$ make build_frontend
$ make translate
$ make htmldoc
```

View file

@ -22,7 +22,9 @@ With Shaarli:
Shaarli stands for _shaaring_ your _links_.
### My Shaarli is broken!
First of all, ensure that both the [web server](Server-configuration) and [Shaarli](Shaarli-configuration) are correctly configured, and that your installation is [supported](Server-requirements).
First of all, ensure that both the [web server](Server-configuration) and
[Shaarli](Shaarli-configuration) are correctly configured, and that your
installation is [supported](Server-configuration).
If everything looks right but the issue(s) remain(s), please:

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
Enabled plugin are stored in your `config.php` parameters file, under the `array`:
Enabled plugin are stored in your `config.json.php` parameters file, under the `array`:
```php
$GLOBALS['config']['ENABLED_PLUGINS']
@ -48,7 +48,7 @@ Example:
```php
$GLOBALS['config']['ENABLED_PLUGINS'] = array(
'qrcode',
'qrcode',
'archiveorg',
'wallabag',
'markdown',

View file

@ -3,8 +3,9 @@
See the [REST API documentation](http://shaarli.github.io/api-documentation/)
for a list of available endpoints and parameters.
Please ensure that your server meets the [requirements](Server-requirements)
and is properly [configured](Server-configuration):
Please ensure that your server meets the
[requirements](Server-configuration#prerequisites) and is properly
[configured](Server-configuration):
- URL rewriting is enabled (see specific Apache and Nginx sections)
- the server's timezone is properly defined
@ -151,3 +152,22 @@ See the reference API client:
- [Documentation](http://python-shaarli-client.readthedocs.io/en/latest/) on ReadTheDocs
- [python-shaarli-client](https://github.com/shaarli/python-shaarli-client) on Github
## Troubleshooting
### Debug mode
> This should never be used in a production environment.
For security reasons, authentication issues will always return an `HTTP 401` error code without any detail.
It is possible to enable the debug mode in `config.json.php`
to get the actual error message in the HTTP response body with:
```json
{
"dev": {
"debug": true
}
}
```

View file

@ -1,139 +1,130 @@
*Example virtual host configurations for popular web servers*
- [Prerequisites](#prerequisistes)
- [Apache](#apache)
- [Nginx](#nginx)
- [Proxies](#proxies)
- [See also](#see-also)
## Prerequisites
### Shaarli
- Shaarli is installed in a directory readable/writeable by the user
- the correct read/write permissions have been granted to the web server _user and/or group_
- for HTTPS / SSL:
- a key pair (public, private) and a certificate have been generated
- the appropriate server SSL extension is installed and active
### HTTPS, TLS and self-signed certificates
Related guides:
- A web server and PHP interpreter module/service have been installed.
- You have write access to the Shaarli installation directory.
- The correct read/write permissions have been granted to the web server user and group.
- Your PHP interpreter is compatible with supported PHP versions:
- [How to Create Self-Signed SSL Certificates with OpenSSL](http://www.xenocafe.com/tutorials/linux/centos/openssl/self_signed_certificates/index.php)
- [How do I create my own Certificate Authority?](https://workaround.org/certificate-authority)
- Generate a self-signed certificate (will trigger browser warnings) with apache2:
`make-ssl-cert generate-default-snakeoil --force-overwrite` will create `/etc/ssl/certs/ssl-cert-snakeoil.pem` and `/etc/ssl/private/ssl-cert-snakeoil.key`
Version | Status | Shaarli compatibility
:---:|:---:|:---:
7.2 | Supported | Yes
7.1 | Supported | Yes
7.0 | Supported | Yes
5.6 | Supported | Yes
5.5 | EOL: 2016-07-10 | Yes
5.4 | EOL: 2015-09-14 | Yes (up to Shaarli 0.8.x)
5.3 | EOL: 2014-08-14 | Yes (up to Shaarli 0.8.x)
### Proxies
If Shaarli is served behind a proxy (i.e. there is a proxy server between clients and the web server hosting Shaarli), please refer to the proxy server documentation for proper configuration. In particular, you have to ensure that the following server variables are properly set:
- The following PHP extensions are installed on the server:
- `X-Forwarded-Proto`
- `X-Forwarded-Host`
- `X-Forwarded-For`
Extension | Required? | Usage
---|:---:|---
[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS
[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
[`php-gd`](http://php.net/manual/en/book.image.php) | optional | required to use thumbnails
[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)
See also [proxy-related](https://github.com/shaarli/Shaarli/issues?utf8=%E2%9C%93&q=label%3Aproxy+) issues.
--------------------------------------------------------------------------------
### SSL/TLS configuration
To setup HTTPS / SSL on your webserver (recommended), you must generate a public/private **key pair** and a **certificate**, and install, configure and activate the appropriate **webserver SSL extension**.
#### Let's Encrypt
[Let's Encrypt](https://en.wikipedia.org/wiki/Let%27s_Encrypt) is a certificate authority that provides free TLS/X.509 certificates via an automated process.
* Install `certbot` using the appropriate method described on https://certbot.eff.org/.
Location of the `certbot` program and template configuration files may vary depending on which installation method was used. Change the file paths below accordingly. Here is an easy way to create a signed certificate using `certbot`, it assumes `certbot` was installed through APT on a Debian-based distribution:
* Stop the apache2/nginx service.
* Run `certbot --agree-tos --standalone --preferred-challenges tls-sni --email "youremail@example.com" --domain yourdomain.example.com`
* For the Apache webserver, copy `/usr/lib/python2.7/dist-packages/certbot_apache/options-ssl-apache.conf` to `/etc/letsencrypt/options-ssl-apache.conf` (paths may vary depending on installation method)
* For Nginx: TODO
* Setup your webserver as described below
* Restart the apache2/nginx service.
#### Self-signed certificates
If you don't want to request a certificate from Let's Encrypt, or are unable to (for example, webserver on a LAN, or domain name not registered in the public DNS system), you can generate a self-signed certificate. This certificate will trigger security warnings in web browsers, unless you add it to the browser's SSL store manually.
* Apache: run `make-ssl-cert generate-default-snakeoil --force-overwrite`
* Nginx: TODO
--------------------------------------------------------------------------------
## Apache
### Minimal
```apache
<VirtualHost *:80>
ServerName shaarli.my-domain.org
DocumentRoot /absolute/path/to/shaarli/
</VirtualHost>
```
### Debug - Log all the things!
This configuration will log both Apache and PHP errors, which may prove useful to identify server configuration errors.
See:
Here is a basic configuration example for the Apache web server with `mod_php`.
- [Apache/PHP - error log per VirtualHost](http://stackoverflow.com/q/176) (StackOverflow)
- [PHP: php_value vs php_admin_value and the use of php_flag explained](https://ma.ttias.be/php-php_value-vs-php_admin_value-and-the-use-of-php_flag-explained/)
```apache
<VirtualHost *:80>
ServerName shaarli.my-domain.org
DocumentRoot /absolute/path/to/shaarli/
LogLevel warn
ErrorLog /var/log/apache2/shaarli-error.log
CustomLog /var/log/apache2/shaarli-access.log combined
php_flag log_errors on
php_flag display_errors on
php_value error_reporting 2147483647
php_value error_log /var/log/apache2/shaarli-php-error.log
</VirtualHost>
```
### Standard - Keep access and error logs
```apache
<VirtualHost *:80>
ServerName shaarli.my-domain.org
DocumentRoot /absolute/path/to/shaarli/
LogLevel warn
ErrorLog /var/log/apache2/shaarli-error.log
CustomLog /var/log/apache2/shaarli-access.log combined
</VirtualHost>
```
### Paranoid - Redirect HTTP (:80) to HTTPS (:443)
See [Server-side TLS](https://wiki.mozilla.org/Security/Server_Side_TLS#Apache) (Mozilla).
In `/etc/apache2/sites-available/shaarli.conf`:
```apache
<VirtualHost *:443>
ServerName shaarli.my-domain.org
DocumentRoot /absolute/path/to/shaarli/
# Logging
# Possible values include: debug, info, notice, warn, error, crit, alert, emerg.
LogLevel warn
ErrorLog /var/log/apache2/shaarli-error.log
CustomLog /var/log/apache2/shaarli-access.log combined
# Let's Encrypt SSL configuration (recommended)
SSLEngine on
SSLCertificateFile /absolute/path/to/the/website/certificate.pem
SSLCertificateKeyFile /absolute/path/to/the/website/key.key
SSLCertificateFile /etc/letsencrypt/live/yourdomain.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.example.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
# Self-signed SSL cert configuration
#SSLEngine on
#SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
#SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
# Optional, log PHP errors, useful for debugging
#php_flag log_errors on
#php_flag display_errors on
#php_value error_reporting 2147483647
#php_value error_log /var/log/apache2/shaarli-php-error.log
<Directory /absolute/path/to/shaarli/>
#Required for .htaccess support
AllowOverride All
Options Indexes FollowSymLinks MultiViews
Order allow,deny
allow from all
Allow from all
Options Indexes FollowSymLinks MultiViews #TODO is Indexes/Multiviews required?
# Optional - required for playvideos plugin
#Header set Content-Security-Policy "script-src 'self' 'unsafe-inline' https://www.youtube.com https://s.ytimg.com 'unsafe-eval'"
</Directory>
LogLevel warn
ErrorLog /var/log/apache2/shaarli-error.log
CustomLog /var/log/apache2/shaarli-access.log combined
</VirtualHost>
<VirtualHost *:80>
ServerName shaarli.my-domain.org
Redirect 301 / https://shaarli.my-domain.org
LogLevel warn
ErrorLog /var/log/apache2/shaarli-error.log
CustomLog /var/log/apache2/shaarli-access.log combined
</VirtualHost>
```
### .htaccess
Enable this configuration with `sudo a2ensite shaarli`
Shaarli use `.htaccess` Apache files to deny access to files that shouldn't be directly accessed (datastore, config, etc.). You need the directive `AllowOverride All` in your virtual host configuration for them to work.
_Note: If you use Apache 2.2 or lower, you need [mod_version](https://httpd.apache.org/docs/current/mod/mod_version.html) to be installed and enabled._
**Warning**: If you use Apache 2.2 or lower, you need [mod_version](https://httpd.apache.org/docs/current/mod/mod_version.html) to be installed and enabled.
Apache module `mod_rewrite` **must** be enabled to use the REST API. URL rewriting rules for the Slim microframework are stated in the root `.htaccess` file.
_Note: Apache module `mod_rewrite` must be enabled to use the REST API._
## LightHttpd
## Nginx
### Foreword
Nginx does not natively interpret PHP scripts; to this effect, we will run a [FastCGI](https://en.wikipedia.org/wiki/FastCGI) service, to which Nginx's FastCGI module will proxy all requests to PHP resources.
Required packages:
Here is a basic configuration example for the Nginx web server, using the [php-fpm](http://php-fpm.org) PHP FastCGI Process Manager, and Nginx's [FastCGI](https://en.wikipedia.org/wiki/FastCGI) module.
- [nginx](http://nginx.org)
- [php-fpm](http://php-fpm.org) - PHP FastCGI Process Manager
Official documentation:
- [Beginner's guide](http://nginx.org/en/docs/beginners_guide.html)
- [ngx_http_fastcgi_module](http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html)
- [Pitfalls](http://wiki.nginx.org/Pitfalls)
Community resources:
- [Server-side TLS (Nginx)](https://wiki.mozilla.org/Security/Server_Side_TLS#Nginx) (Mozilla)
- [PHP configuration examples](http://kbeezie.com/nginx-configuration-examples/) (Karl Blessing)
<!--- TODO refactor everything below this point --->
### Common setup
Once Nginx and PHP-FPM are installed, we need to ensure:
@ -404,3 +395,39 @@ http {
}
}
```
## Proxies
If Shaarli is served behind a proxy (i.e. there is a proxy server between clients and the web server hosting Shaarli), please refer to the proxy server documentation for proper configuration. In particular, you have to ensure that the following server variables are properly set:
- `X-Forwarded-Proto`
- `X-Forwarded-Host`
- `X-Forwarded-For`
See also [proxy-related](https://github.com/shaarli/Shaarli/issues?utf8=%E2%9C%93&q=label%3Aproxy+) issues.
## See also
* [Server security](Server-security.md)
#### Webservers
- [Apache/PHP - error log per VirtualHost](http://stackoverflow.com/q/176) (StackOverflow)
- [Apache - PHP: php_value vs php_admin_value and the use of php_flag explained](https://ma.ttias.be/php-php_value-vs-php_admin_value-and-the-use-of-php_flag-explained/)
- [Server-side TLS (Apache)](https://wiki.mozilla.org/Security/Server_Side_TLS#Apache) (Mozilla)
- [Nginx Beginner's guide](http://nginx.org/en/docs/beginners_guide.html)
- [Nginx ngx_http_fastcgi_module](http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html)
- [Nginx Pitfalls](http://wiki.nginx.org/Pitfalls)
- [Nginx PHP configuration examples](http://kbeezie.com/nginx-configuration-examples/) (Karl Blessing)
- [Server-side TLS (Nginx)](https://wiki.mozilla.org/Security/Server_Side_TLS#Nginx) (Mozilla)
- [How to Create Self-Signed SSL Certificates with OpenSSL](http://www.xenocafe.com/tutorials/linux/centos/openssl/self_signed_certificates/index.php)
- [How do I create my own Certificate Authority?](https://workaround.org/certificate-authority)
#### PHP
- [Travis configuration](https://github.com/shaarli/Shaarli/blob/master/.travis.yml)
- [PHP: Supported versions](http://php.net/supported-versions.php)
- [PHP: Unsupported versions](http://php.net/eol.php) _(EOL - End Of Life)_
- [PHP 7 Changelog](http://php.net/ChangeLog-7.php)
- [PHP 5 Changelog](http://php.net/ChangeLog-5.php)
- [PHP: Bugs](https://bugs.php.net/)

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`.
### Theme translations
Theme translation extensions are loaded automatically if they're present.
As a theme developer, all you have to do is to add the `.po` and `.mo` compiled file like this:
tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.po
tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.mo
Where `<lang>` is the ISO 3166-1 alpha-2 language code.
Read the following section "Extend Shaarli's translation" to learn how to generate those files.
### Extend Shaarli's translation
If you're writing a custom theme, or a non official plugin, you might want to use the translation system,

View file

@ -63,7 +63,7 @@ Related threads:
### I forgot my password!
Delete the file `data/config.php` and display the page again. You will be asked for a new login/password.
Delete the file `data/config.json.php` and display the page again. You will be asked for a new login/password.
### I'm locked out - Login bruteforce protection
@ -97,7 +97,7 @@ php56 1
```php
//list($status,$headers,$data) = getHTTP($url,4); // Short timeout to keep the application responsive.
// FIXME: Decode charset according to charset specified in either 1) HTTP response headers or 2) <head> in html
// FIXME: Decode charset according to charset specified in either 1) HTTP response headers or 2) <head> in html
//if (strpos($status,'200 OK')) $title=html_extract_title($data);
```
@ -106,11 +106,11 @@ php56 1
### Dates are not properly formatted
Shaarli tries to sniff the language of the browser (using HTTP_ACCEPT_LANGUAGE headers) and choose a date format accordingly. But Shaarli can only use the date formats (and more generaly speaking, the locales) provided by the webserver. So even if you have a browser in French, you may end up with dates in US format (it's the case on sebsauvage.net :-( )
### Problems on CentOS servers
On **CentOS**/RedHat derivatives, you may need to install the `php-mbstring` package.
Shaarli tries to sniff the language of the browser (using `HTTP_ACCEPT_LANGUAGE` headers)
and choose a date format accordingly. But Shaarli can only use the date formats
(and more generally speaking, the locales) provided by the webserver.
So even if you have a browser in French, you may end up with dates in US format
(it's the case on sebsauvage.net :-( )
### My session expires! I can't stay logged in
@ -126,7 +126,3 @@ This can be caused by several things:
## Sessions do not seem to work correctly on your server
Follow the instructions in the error message. Make sure you are accessing shaarli via a direct IP address or a proper hostname. If you have **no dots** in the hostname (e.g. `localhost` or `http://my-webserver/shaarli/`), some browsers will not store cookies at all (this respects the [HTTP cookie specification](http://curl.haxx.se/rfc/cookie_spec.html)).
### pubsubhubbub support
Download [publisher.php](https://pubsubhubbub.googlecode.com/git/publisher_clients/php/library/publisher.php) at the root of your Shaarli installation and set `$GLOBALS['config']['PUBSUBHUB_URL']` in your `config.php`

View file

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

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