Release v0.9.4

-----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEWe5LuNiFNDXAgI8BOzJIyqqwgW4FAlpws1AACgkQOzJIyqqw
 gW5FRw//YU1dW5CUwKjL9LxvQWWZmgm+iwuJP4sohCrySAG/2ZKxCRlJtdD1WGU3
 jF1HufmdDdx0fHiAAKSz5GK+9XVnI1MuGYzTWSTS+pZ1XO5v0nJMskSd+PSkHrs1
 5DaTzFnvwKflN7mKKbFOi9aBo7fIOYp8hmPHOHyDC458MJw7vraSiFjWXih10UW4
 3m3442UQ14Hfwe7uN6kOfxYrNmkyisa1VJshBYs5gs1qP0L4IGMoDIAuDzVCxbcA
 u/olrxfSaScrV9+yFUmUlcBHGq8ejQl20MsfK7QhErbZu6Y3FlcucySGWdzVV5Nr
 39sLFTjgoMhIk8oPt0N0szKH1uaqcNGbgOoo16unVFM/Kkd7kbLRoltTZIaNKyOs
 akqRczDkh8sd6RITsE7JwPEYloJPOLnNUPhTPqLTq9kFlCB8uGzy1VFnVUfSrqHU
 j6b/6xaoZUZ3hynBRLzwaN0wYQXH0jXWBHVbn2aZPSp0tTxhsnudCpPZ0STFu9As
 fv8NwGNejPr4I9hjoiys6ICu0NV+v88SdA347lUoXa2233Wg3EdIv8eAnZeANpkr
 ij0KfFhg7qiHQB8TftZjY9S9ehomw1jxShUkf2xwk7PQUngaKce/1xZAizn10jqj
 kLNTzPRUyVFUhEwYIeSCSOFJ22g7p8GvU+HxCIjystsxGDH3Q8s=
 =N7I1
 -----END PGP SIGNATURE-----

Merge tag 'v0.9.4' into latest

Release v0.9.4
This commit is contained in:
ArthurHoaro 2018-01-30 19:15:30 +01:00
commit a74184e1b0
118 changed files with 4292 additions and 878 deletions

23
.editorconfig Normal file
View file

@ -0,0 +1,23 @@
# EditorConfig: http://EditorConfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{htaccess,html,xml}]
indent_size = 2
[*.php]
max_line_length = 100
[Dockerfile]
max_line_length = 80
[Makefile]
indent_style = tab

2
.gitattributes vendored
View file

@ -22,8 +22,10 @@ Dockerfile text
*.ttf binary
*.min.css binary
*.min.js binary
*.mo binary
# Exclude from Git archives
.editorconfig export-ignore
.gitattributes export-ignore
.github export-ignore
.gitignore export-ignore

2
.github/mailmap vendored
View file

@ -1,6 +1,8 @@
ArthurHoaro <arthur@hoa.ro>
Florian Eula <eula.florian@gmail.com> feula
Florian Eula <eula.florian@gmail.com> <mr.pikzen@gmail.com>
Immánuel Fodor <immanuelfactor+github@gmail.com>
kalvn <kalvnthereal@gmail.com> <kalvn@users.noreply.github.com>
Nicolas Danelon <hi@nicolasmd.com.ar> nicolasm
Nicolas Danelon <hi@nicolasmd.com.ar> <nda@3818.com.ar>
Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@gmail.com>

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ vendor/
# Release archives
*.tar.gz
*.zip
inc/languages/*/LC_MESSAGES/shaarli.mo
# Development and test resources
coverage

View file

@ -13,6 +13,8 @@ install:
- composer self-update
- composer install --prefer-dist
- locale -a
before_script:
- PATH=${PATH//:\.\/node_modules\/\.bin/}
script:
- make clean
- make check_permissions

13
AUTHORS
View file

@ -1,6 +1,6 @@
542 ArthurHoaro <arthur@hoa.ro>
255 VirtualTam <virtualtam@flibidi.net>
148 nodiscc <nodiscc@gmail.com>
577 ArthurHoaro <arthur@hoa.ro>
283 VirtualTam <virtualtam@flibidi.net>
179 nodiscc <nodiscc@gmail.com>
56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
15 Florian Eula <eula.florian@gmail.com>
13 Emilien Klein <emilien@klein.st>
@ -11,8 +11,9 @@
5 Lucas Cimon <lucas.cimon@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 kalvn <kalvnthereal@gmail.com>
2 Chris Kuethe <chris.kuethe@gmail.com>
2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
2 Mathieu Chabanon <git@matchab.fr>
@ -27,11 +28,13 @@
1 BoboTiG <bobotig@gmail.com>
1 Bronco <bronco@warriordudimanche.net>
1 D Low <daniellowtw@gmail.com>
1 Daniel Jakots <vigdis@chown.me>
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 Druart <jonathan.druart@gmail.com>
@ -41,6 +44,8 @@
1 Lionel Martin <renarddesmers@gmail.com>
1 Mark Gerarts <mark.gerarts@gmail.com>
1 Marsup <marsup@gmail.com>
1 Neros <contact@neros.fr>
1 Sbgodin <Sbgodin@users.noreply.github.com>
1 TsT <tst2005@gmail.com>
1 dimtion <zizou.xena@gmail.com>
1 durcheinandr <jochen@durcheinandr.de>

View file

@ -4,12 +4,42 @@ 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.9.3](https://github.com/shaarli/Shaarli/releases/tag/v0.9.3) - 2018-01-04
## [v0.10.0](https://github.com/shaarli/Shaarli/releases/tag/v0.10.0) - UNPUBLISHED
## [v0.9.4](https://github.com/shaarli/Shaarli/releases/tag/v0.9.4) - 2018-01-30
### Added
- Enable translations: Shaarli is now also available in French. Other language translations are welcome!
- Add EditorConfig configuration
- Add favicons for mobile devices
- Add Alpine Linux arm32v7 Dockerfiles (master, latest)
### Changed
- Do not write bookmark edition history during file imports (performance)
- Migrate Docker images (master, latest) to Alpine Linux
- Improve unitary tests and code coverage
- Improve thumbnail display
- Improve theme ergonomics
- Improve messages if there is no plugin or parameter available in the admin page
- Increase buffer size for cURL download
- Force HTTPS if the original port is 443 behind a reverse proxy (workaround)
- Improve page title retrieval performances
### Removed
- Remove redirector setting from Configure page
### Fixed
- Fix broken links in the documentation
- Enable access to `data/user.css` (Apache 2.2 & 2.4)
- Don't URL encode description links if parameter `redirector.encode_url` is set to false
- Fix an issue preventing the Save button to appear for plugin parameters
## [v0.9.3](https://github.com/shaarli/Shaarli/releases/tag/v0.9.3) - 2018-01-04
**XSS vulnerability fixed. Please update.**
### Security
- Fix an XSS (cross-site-scripting) vulnerability in `index.php`
## Security
- Fix an XSS (cross-site-scripting) vulnerability in `index.php` -
[CVE-2018-5249](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-5249)
## [v0.9.2](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) - 2017-10-07
@ -48,7 +78,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Security
- Vulnerability introduced in v0.9.1 fixed.
- Fixed reflected XSS vulnerability introduced in v0.9.1, discovered by @chb9 ([CVE-2017-15215](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15215)).
## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23
@ -195,6 +226,13 @@ Theming:
- Editing a link created before the new ID system would change its permalink.
## [v0.8.5](https://github.com/shaarli/Shaarli/releases/tag/v0.8.5) - 2018-01-04
**XSS vulnerability fixed. Please update.**
## Security
- Fix an XSS (cross-site-scripting) vulnerability in `index.php` -
[CVE-2018-5249](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-5249)
## [v0.8.4](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) - 2017-03-04
### Security
- Markdown plugin: escape HTML entities by default

View file

@ -1,17 +1,6 @@
# The personal, minimalist, super-fast, database free, bookmarking service.
# Makefile for PHP code analysis & testing, documentation and release generation
# Prerequisites:
# - install Composer, either:
# - from your distro's package manager;
# - from the official website (https://getcomposer.org/download/);
# - install/update test dependencies:
# $ composer install # 1st setup
# $ composer update
# - install Xdebug for PHPUnit code coverage reports:
# - see http://xdebug.org/docs/install
# - enable in php.ini
BIN = vendor/bin
PHP_SOURCE = index.php application tests plugins
PHP_COMMA_SOURCE = index.php,application,tests,plugins
@ -115,7 +104,7 @@ check_permissions:
@echo "----------------------"
@echo "Check file permissions"
@echo "----------------------"
@for file in `git ls-files`; do \
@for file in `git ls-files | grep -v docker`; do \
if [ -x $$file ]; then \
errors=true; \
echo "$${file} is executable"; \
@ -130,12 +119,12 @@ check_permissions:
# See phpunit.xml for configuration
# https://phpunit.de/manual/current/en/appendixes.configuration.html
##
test:
test: translate
@echo "-------"
@echo "PHPUNIT"
@echo "-------"
@mkdir -p sandbox coverage
@$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests
@$(BIN)/phpunit --coverage-php coverage/main.cov --bootstrap tests/bootstrap.php --testsuite unit-tests
locale_test_%:
@UT_LOCALE=$*.utf8 \
@ -168,15 +157,15 @@ composer_dependencies: clean
composer install --no-dev --prefer-dist
find vendor/ -name ".git" -type d -exec rm -rf {} +
### generate a release tarball and include 3rd-party dependencies
release_tar: composer_dependencies htmldoc
### generate a release tarball and include 3rd-party dependencies and translations
release_tar: composer_dependencies htmldoc translate
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/
gzip $(ARCHIVE_VERSION).tar
### generate a release zip and include 3rd-party dependencies
release_zip: composer_dependencies htmldoc
### generate a release zip and include 3rd-party dependencies and translations
release_zip: composer_dependencies htmldoc translate
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/
@ -213,3 +202,8 @@ htmldoc:
mkdocs build'
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 \;

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.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4)
[![](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/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
&bull;
[![](https://img.shields.io/badge/latest-v0.9.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1)
[![](https://img.shields.io/badge/latest-v0.9.3-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.3)
[![](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.9.x-blue.svg)](https://github.com/shaarli/Shaarli)

View file

@ -149,12 +149,13 @@ public static function checkUpdate($currentVersion,
public static function checkPHPVersion($minVersion, $curVersion)
{
if (version_compare($curVersion, $minVersion) < 0) {
throw new Exception(
$msg = t(
'Your PHP version is obsolete!'
.' Shaarli requires at least PHP '.$minVersion.', and thus cannot run.'
. ' Shaarli requires at least PHP %s, and thus cannot run.'
. ' Your PHP version has known security vulnerabilities and should be'
. ' updated as soon as possible.'
);
throw new Exception(sprintf($msg, $minVersion));
}
}
@ -179,7 +180,7 @@ public static function checkResourcePermissions($conf)
$rainTplDir.'/'.$conf->get('resource.theme'),
) as $path) {
if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not readable';
$errors[] = '"'.$path.'" '. t('directory is not readable');
}
}
@ -191,10 +192,10 @@ public static function checkResourcePermissions($conf)
$conf->get('resource.raintpl_tmp'),
) as $path) {
if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not readable';
$errors[] = '"'.$path.'" '. t('directory is not readable');
}
if (! is_writable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not writable';
$errors[] = '"'.$path.'" '. t('directory is not writable');
}
}
@ -212,10 +213,10 @@ public static function checkResourcePermissions($conf)
}
if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" file is not readable';
$errors[] = '"'.$path.'" '. t('file is not readable');
}
if (! is_writable(realpath($path))) {
$errors[] = '"'.$path.'" file is not writable';
$errors[] = '"'.$path.'" '. t('file is not writable');
}
}

View file

@ -13,7 +13,7 @@
function purgeCachedPages($pageCacheDir)
{
if (! is_dir($pageCacheDir)) {
$error = 'Cannot purge '.$pageCacheDir.': no directory';
$error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
error_log($error);
return $error;
}

View file

@ -148,11 +148,11 @@ protected function buildItem($link, $pageaddr)
$link['url'] = $pageaddr . $link['url'];
}
if ($this->usePermalinks === true) {
$permalink = '<a href="'. $link['url'] .'" title="Direct link">Direct link</a>';
$permalink = '<a href="'. $link['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
} else {
$permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>';
$permalink = '<a href="'. $link['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
}
$link['description'] = format_description($link['description'], '', $pageaddr);
$link['description'] = format_description($link['description'], '', false, $pageaddr);
$link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
$pubDate = $link['created'];

View file

@ -16,6 +16,7 @@
* - UPDATED: link updated
* - DELETED: link deleted
* - SETTINGS: the settings have been updated through the UI.
* - IMPORT: bulk links import
*
* Note: new events are put at the beginning of the file and history array.
*/
@ -41,6 +42,11 @@ class History
*/
const SETTINGS = 'SETTINGS';
/**
* @var string Action key: a bulk import has been processed.
*/
const IMPORT = 'IMPORT';
/**
* @var string History file path.
*/
@ -121,6 +127,16 @@ public function updateSettings()
$this->addEvent(self::SETTINGS);
}
/**
* Add Event: bulk import.
*
* Note: we don't store links add/update one by one since it can have a huge impact on performances.
*/
public function importLinks()
{
$this->addEvent(self::IMPORT);
}
/**
* Save a new event and write it in the history file.
*
@ -155,7 +171,7 @@ protected function check()
}
if (! is_writable($this->historyFilePath)) {
throw new Exception('History file isn\'t readable or writable');
throw new Exception(t('History file isn\'t readable or writable'));
}
}
@ -166,7 +182,7 @@ protected function read()
{
$this->history = FileUtils::readFlatDB($this->historyFilePath, []);
if ($this->history === false) {
throw new Exception('Could not parse history file');
throw new Exception(t('Could not parse history file'));
}
}

View file

@ -6,6 +6,8 @@
* @param string $url URL to get (http://...)
* @param int $timeout network timeout (in seconds)
* @param int $maxBytes maximum downloaded bytes (default: 4 MiB)
* @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION).
* Can be used to add download conditions on the headers (response code, content type, etc.).
*
* @return array HTTP response headers, downloaded content
*
@ -29,7 +31,7 @@
* @see http://stackoverflow.com/q/9183178
* @see http://stackoverflow.com/q/1462720
*/
function get_http_response($url, $timeout = 30, $maxBytes = 4194304)
function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
{
$urlObj = new Url($url);
$cleanUrl = $urlObj->idnToAscii();
@ -75,8 +77,12 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304)
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
if (is_callable($curlWriteFunction)) {
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction);
}
// Max download size management
curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes)
@ -302,6 +308,13 @@ function server_url($server)
$port = $server['HTTP_X_FORWARDED_PORT'];
}
// This is a workaround for proxies that don't forward the scheme properly.
// Connecting over port 443 has to be in HTTPS.
// See https://github.com/shaarli/Shaarli/issues/1022
if ($port == '443') {
$scheme = 'https';
}
if (($scheme == 'http' && $port != '80')
|| ($scheme == 'https' && $port != '443')
) {

View file

@ -1,21 +1,164 @@
<?php
namespace Shaarli;
use Gettext\GettextTranslator;
use Gettext\Merge;
use Gettext\Translations;
use Gettext\Translator;
use Gettext\TranslatorInterface;
use Shaarli\Config\ConfigManager;
/**
* Wrapper function for translation which match the API
* of gettext()/_() and ngettext().
* Class Languages
*
* Not doing translation for now.
* Load Shaarli translations using 'gettext/gettext'.
* This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
* with a fixed language, or dynamically using autoLocale().
*
* @param string $text Text to translate.
* @param string $nText The plural message ID.
* @param int $nb The number of items for plural forms.
* Translation files PO/MO files follow gettext standard and must be placed under:
* <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
*
* @return String Text translated.
* Pros/cons:
* - gettext extension is faster
* - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
*
* Settings:
* - translation.mode:
* - auto: use default setting (PHP implementation)
* - php: use PHP implementation
* - gettext: use gettext wrapper
* - translation.language:
* - auto: use autoLocale() and the language change according to user HTTP headers
* - fixed language: e.g. 'fr'
* - translation.extensions:
* - domain => translation_path: allow plugins and themes to extend the defaut extension
* The domain must be unique, and translation path must be relative, and contains the tree mentioned above.
*
* @package Shaarli
*/
function t($text, $nText = '', $nb = 0) {
if (empty($nText)) {
return $text;
class Languages
{
/**
* Core translations domain
*/
const DEFAULT_DOMAIN = 'shaarli';
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* @var string
*/
protected $language;
/**
* @var ConfigManager
*/
protected $conf;
/**
* Languages constructor.
*
* @param string $language lang determined by autoLocale(), can be overridden.
* @param ConfigManager $conf instance.
*/
public function __construct($language, $conf)
{
$this->conf = $conf;
$confLanguage = $this->conf->get('translation.language', 'auto');
if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) {
$this->language = substr($language, 0, 5);
} else {
$this->language = $confLanguage;
}
if (! extension_loaded('gettext')
|| in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
) {
$this->initPhpTranslator();
} else {
$this->initGettextTranslator();
}
// Register default functions (e.g. '__()') to use our Translator
$this->translator->register();
}
/**
* Initialize the translator using php gettext extension (gettext dependency act as a wrapper).
*/
protected function initGettextTranslator ()
{
$this->translator = new GettextTranslator();
$this->translator->setLanguage($this->language);
$this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
if ($domain !== self::DEFAULT_DOMAIN) {
$this->translator->loadDomain($domain, $translationPath, false);
}
}
}
/**
* Initialize the translator using a PHP implementation of gettext.
*
* Note that if language po file doesn't exist, errors are ignored (e.g. not installed language).
*/
protected function initPhpTranslator()
{
$this->translator = new Translator();
$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) {}
// Extension translations (plugins, themes, etc.).
foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
if ($domain === self::DEFAULT_DOMAIN) {
continue;
}
try {
/** @var Translations $extension */
$extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
$extension->setDomain($domain);
$this->translator->loadTranslations($extension);
} catch (\InvalidArgumentException $e) {}
}
}
/**
* Checks if a language string is valid.
*
* @param string $language e.g. 'fr' or 'en_US'
*
* @return bool true if valid, false otherwise
*/
protected function isValidLanguage($language)
{
return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1;
}
/**
* Get the list of available languages for Shaarli.
*
* @return array List of available languages, with their label.
*/
public static function getAvailableLanguages()
{
return [
'auto' => t('Automatic'),
'en' => t('English'),
'fr' => t('French'),
];
}
$actualForm = $nb > 1 ? $nText : $text;
return sprintf($actualForm, $nb);
}

View file

@ -133,16 +133,16 @@ public function offsetSet($offset, $value)
{
// TODO: use exceptions instead of "die"
if (!$this->loggedIn) {
die('You are not authorized to add a link.');
die(t('You are not authorized to add a link.'));
}
if (!isset($value['id']) || empty($value['url'])) {
die('Internal Error: A link should always have an id and URL.');
die(t('Internal Error: A link should always have an id and URL.'));
}
if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
die('You must specify an integer as a key.');
die(t('You must specify an integer as a key.'));
}
if ($offset !== null && $offset !== $value['id']) {
die('Array offset and link ID must be equal.');
die(t('Array offset and link ID must be equal.'));
}
// If the link exists, we reuse the real offset, otherwise new entry
@ -248,13 +248,13 @@ private function check()
$this->links = array();
$link = array(
'id' => 1,
'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
'url'=>'https://shaarli.readthedocs.io',
'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
'description'=>t('Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page.
To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
You use the community supported version of the original Shaarli project, by Sebastien Sauvage.',
You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'),
'private'=>0,
'created'=> new DateTime(),
'tags'=>'opensource software'
@ -264,9 +264,9 @@ private function check()
$link = array(
'id' => 0,
'title'=>'My secret stuff... - Pastebin.com',
'title'=> t('My secret stuff... - Pastebin.com'),
'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
'private'=>1,
'created'=> new DateTime('1 minute ago'),
'tags'=>'secretstuff',
@ -289,13 +289,15 @@ private function read()
return;
}
$this->urls = [];
$this->ids = [];
$this->links = FileUtils::readFlatDB($this->datastore, []);
$toremove = array();
foreach ($this->links as $key => &$link) {
if (! $this->loggedIn && $link['private'] != 0) {
// Transition for not upgraded databases.
$toremove[] = $key;
unset($this->links[$key]);
continue;
}
@ -329,14 +331,10 @@ private function read()
}
$link['shorturl'] = smallHash($link['linkdate']);
}
}
// If user is not logged in, filter private links.
foreach ($toremove as $offset) {
unset($this->links[$offset]);
$this->urls[$link['url']] = $key;
$this->ids[$link['id']] = $key;
}
$this->reorder();
}
/**
@ -346,6 +344,7 @@ private function read()
*/
private function write()
{
$this->reorder();
FileUtils::writeFlatDB($this->datastore, $this->links);
}
@ -528,8 +527,8 @@ public function reorder($order = 'DESC')
return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
});
$this->urls = array();
$this->ids = array();
$this->urls = [];
$this->ids = [];
foreach ($this->links as $key => $link) {
$this->urls[$link['url']] = $key;
$this->ids[$link['id']] = $key;

View file

@ -444,5 +444,11 @@ public static function tagsStrToArray($tags, $casesensitive)
class LinkNotFoundException extends Exception
{
protected $message = 'The link you are trying to reach does not exist or has been deleted.';
/**
* LinkNotFoundException constructor.
*/
public function __construct()
{
$this->message = t('The link you are trying to reach does not exist or has been deleted.');
}
}

View file

@ -1,5 +1,54 @@
<?php
/**
* Get cURL callback function for CURLOPT_WRITEFUNCTION
*
* @param string $charset to extract from the downloaded page (reference)
* @param string $title to extract from the downloaded page (reference)
* @param string $curlGetInfo Optionnaly overrides curl_getinfo function
*
* @return Closure
*/
function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo')
{
/**
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
*
* While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
* Then we extract the title and the charset and stop the download when it's done.
*
* @param resource $ch cURL resource
* @param string $data chunk of data being downloaded
*
* @return int|bool length of $data or false if we need to stop the download
*/
return function(&$ch, $data) use ($curlGetInfo, &$charset, &$title) {
$responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
if (!empty($responseCode) && $responseCode != 200) {
return false;
}
$contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
return false;
}
if (empty($charset)) {
$charset = header_extract_charset($contentType);
}
if (empty($charset)) {
$charset = html_extract_charset($data);
}
if (empty($title)) {
$title = html_extract_title($data);
}
// We got everything we want, stop the download.
if (!empty($responseCode) && !empty($contentType) && !empty($charset) && !empty($title)) {
return false;
}
return strlen($data);
};
}
/**
* Extract title from an HTML document.
*
@ -16,46 +65,18 @@ function html_extract_title($html)
}
/**
* Determine charset from downloaded page.
* Priority:
* 1. HTTP headers (Content type).
* 2. HTML content page (tag <meta charset>).
* 3. Use a default charset (default: UTF-8).
* Extract charset from HTTP header if it's defined.
*
* @param array $headers HTTP headers array.
* @param string $htmlContent HTML content where to look for charset.
* @param string $defaultCharset Default charset to apply if other methods failed.
*
* @return string Determined charset.
*/
function get_charset($headers, $htmlContent, $defaultCharset = 'utf-8')
{
if ($charset = headers_extract_charset($headers)) {
return $charset;
}
if ($charset = html_extract_charset($htmlContent)) {
return $charset;
}
return $defaultCharset;
}
/**
* Extract charset from HTTP headers if it's defined.
*
* @param array $headers HTTP headers array.
* @param string $header HTTP header Content-Type line.
*
* @return bool|string Charset string if found (lowercase), false otherwise.
*/
function headers_extract_charset($headers)
function header_extract_charset($header)
{
if (! empty($headers['Content-Type']) && strpos($headers['Content-Type'], 'charset=') !== false) {
preg_match('/charset="?([^; ]+)/i', $headers['Content-Type'], $match);
preg_match('/charset="?([^; ]+)/i', $header, $match);
if (! empty($match[1])) {
return strtolower(trim($match[1]));
}
}
return false;
}
@ -102,12 +123,13 @@ function count_private($links)
*
* @param string $text input string.
* @param string $redirector if a redirector is set, use it to gerenate links.
* @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
*
* @return string returns $text with all links converted to HTML links.
*
* @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
*/
function text2clickable($text, $redirector = '')
function text2clickable($text, $redirector = '', $urlEncode = true)
{
$regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
@ -117,8 +139,9 @@ function text2clickable($text, $redirector = '')
// Redirector is set, urlencode the final URL.
return preg_replace_callback(
$regex,
function ($matches) use ($redirector) {
return '<a href="' . $redirector . urlencode($matches[1]) .'">'. $matches[1] .'</a>';
function ($matches) use ($redirector, $urlEncode) {
$url = $urlEncode ? urlencode($matches[1]) : $matches[1];
return '<a href="' . $redirector . $url .'">'. $matches[1] .'</a>';
},
$text
);
@ -164,12 +187,13 @@ function space2nbsp($text)
*
* @param string $description shaare's description.
* @param string $redirector if a redirector is set, use it to gerenate links.
* @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
* @param string $indexUrl URL to Shaarli's index.
*
* @return string formatted description.
*/
function format_description($description, $redirector = '', $indexUrl = '') {
return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl)));
function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '') {
return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector, $urlEncode), $indexUrl)));
}
/**

View file

@ -32,11 +32,10 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in
{
// see tpl/export.html for possible values
if (! in_array($selection, array('all', 'public', 'private'))) {
throw new Exception('Invalid export selection: "'.$selection.'"');
throw new Exception(t('Invalid export selection:') .' "'.$selection.'"');
}
$bookmarkLinks = array();
foreach ($linkDb as $link) {
if ($link['private'] != 0 && $selection == 'public') {
continue;
@ -66,6 +65,7 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in
* @param int $importCount how many links were imported
* @param int $overwriteCount how many links were overwritten
* @param int $skipCount how many links were skipped
* @param int $duration how many seconds did the import take
*
* @return string Summary of the bookmark import status
*/
@ -74,16 +74,18 @@ private static function importStatus(
$filesize,
$importCount=0,
$overwriteCount=0,
$skipCount=0
$skipCount=0,
$duration=0
)
{
$status = 'File '.$filename.' ('.$filesize.' bytes) ';
$status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
$status .= 'has an unknown file format. Nothing was imported.';
$status .= t('has an unknown file format. Nothing was imported.');
} else {
$status .= 'was successfully processed: '.$importCount.' links imported, ';
$status .= $overwriteCount.' links overwritten, ';
$status .= $skipCount.' links skipped.';
$status .= vsprintf(
t('was successfully processed in %d seconds: %d links imported, %d links overwritten, %d links skipped.'),
[$duration, $importCount, $overwriteCount, $skipCount]
);
}
return $status;
}
@ -101,6 +103,7 @@ private static function importStatus(
*/
public static function import($post, $files, $linkDb, $conf, $history)
{
$start = time();
$filename = $files['filetoupload']['name'];
$filesize = $files['filetoupload']['size'];
$data = file_get_contents($files['filetoupload']['tmp_name']);
@ -184,7 +187,6 @@ public static function import($post, $files, $linkDb, $conf, $history)
$linkDb[$existingLink['id']] = $newLink;
$importCount++;
$overwriteCount++;
$history->updateLink($newLink);
continue;
}
@ -196,16 +198,19 @@ public static function import($post, $files, $linkDb, $conf, $history)
$newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
$linkDb[$newLink['id']] = $newLink;
$importCount++;
$history->addLink($newLink);
}
$linkDb->save($conf->get('resource.page_cache'));
$history->importLinks();
$duration = time() - $start;
return self::importStatus(
$filename,
$filesize,
$importCount,
$overwriteCount,
$skipCount
$skipCount,
$duration
);
}
}

View file

@ -32,12 +32,14 @@ class PageBuilder
*
* @param ConfigManager $conf Configuration Manager instance (reference).
* @param LinkDB $linkDB instance.
* @param string $token Session token
*/
public function __construct(&$conf, $linkDB = null)
public function __construct(&$conf, $linkDB = null, $token = null)
{
$this->tpl = false;
$this->conf = $conf;
$this->linkDB = $linkDB;
$this->token = $token;
}
/**
@ -92,7 +94,7 @@ private function initialize()
$this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true));
$this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss');
$this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false));
$this->tpl->assign('token', getToken($this->conf));
$this->tpl->assign('token', $this->token);
if ($this->linkDB !== null) {
$this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
@ -159,9 +161,12 @@ public function renderPage($page)
*
* @param string $message A messate to display what is not found
*/
public function render404($message = 'The page you are trying to reach does not exist or has been deleted.')
public function render404($message = '')
{
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
if (empty($message)) {
$message = t('The page you are trying to reach does not exist or has been deleted.');
}
header($_SERVER['SERVER_PROTOCOL'] .' '. t('404 Not Found'));
$this->tpl->assign('error_message', $message);
$this->renderPage('404');
}

View file

@ -188,6 +188,9 @@ public function getPluginsMeta()
$metaData[$plugin] = parse_ini_file($metaFile);
$metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
if (isset($metaData[$plugin]['description'])) {
$metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
}
// Read parameters and format them into an array.
if (isset($metaData[$plugin]['parameters'])) {
$params = explode(';', $metaData[$plugin]['parameters']);
@ -203,7 +206,7 @@ public function getPluginsMeta()
$metaData[$plugin]['parameters'][$param]['value'] = '';
// Optional parameter description in parameter.PARAM_NAME=
if (isset($metaData[$plugin]['parameter.'. $param])) {
$metaData[$plugin]['parameters'][$param]['desc'] = $metaData[$plugin]['parameter.'. $param];
$metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]);
}
}
}
@ -237,6 +240,6 @@ class PluginFileNotFoundException extends Exception
*/
public function __construct($pluginName)
{
$this->message = 'Plugin "'. $pluginName .'" files not found.';
$this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
}
}

View file

@ -0,0 +1,83 @@
<?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;
}
}

View file

@ -73,7 +73,7 @@ public function update()
}
if ($this->methods === null) {
throw new UpdaterException('Couldn\'t retrieve Updater class methods.');
throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.'));
}
foreach ($this->methods as $method) {
@ -436,6 +436,15 @@ public function updateMethodResetHistoryFile()
}
return true;
}
/**
* Save the datastore -> the link order is now applied when links are saved.
*/
public function updateMethodReorderDatastore()
{
$this->linkDB->save($this->conf->get('resource.page_cache'));
return true;
}
}
/**
@ -482,7 +491,7 @@ private function buildMessage($message)
}
if (! empty($this->method)) {
$out .= 'An error occurred while running the update '. $this->method . PHP_EOL;
$out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
}
if (! empty($this->previous)) {
@ -522,11 +531,11 @@ function read_updates_file($updatesFilepath)
function write_updates_file($updatesFilepath, $updates)
{
if (empty($updatesFilepath)) {
throw new Exception('Updates file path is not set, can\'t write updates.');
throw new Exception(t('Updates file path is not set, can\'t write updates.'));
}
$res = file_put_contents($updatesFilepath, implode(';', $updates));
if ($res === false) {
throw new Exception('Unable to write updates in '. $updatesFilepath . '.');
throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
}
}

View file

@ -181,36 +181,6 @@ function generateLocation($referer, $host, $loopTerms = array())
return $finalReferer;
}
/**
* 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
*/
function is_session_id_valid($sessionId)
{
if (empty($sessionId)) {
return false;
}
if (!$sessionId) {
return false;
}
if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
return false;
}
return true;
}
/**
* Sniff browser language to set the locale automatically.
* Note that is may not work on your server if the corresponding locale is not installed.
@ -470,3 +440,18 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
usort($data, $callback);
}
}
/**
* Wrapper function for translation which match the API
* of gettext()/_() and ngettext().
*
* @param string $text Text to translate.
* @param string $nText The plural message ID.
* @param int $nb The number of items for plural forms.
* @param string $domain The domain where the translation is stored (default: shaarli).
*
* @return string Text translated.
*/
function t($text, $nText = '', $nb = 1, $domain = 'shaarli') {
return dn__($domain, $text, $nText, $nb);
}

View file

@ -22,10 +22,15 @@ public function read($filepath)
$data = json_decode($data, true);
if ($data === null) {
$errorCode = json_last_error();
$error = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #';
$error .= $errorCode. '<br>➜ <code>' . json_last_error_msg() .'</code>';
$error = sprintf(
'An error occurred while parsing JSON configuration file (%s): error code #%d',
$filepath,
$errorCode
);
$error .= '<br>➜ <code>' . json_last_error_msg() .'</code>';
if ($errorCode === JSON_ERROR_SYNTAX) {
$error .= '<br>Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
$error .= '<br>';
$error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
$error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.';
}
throw new \Exception($error);
@ -44,8 +49,8 @@ public function write($filepath, $conf)
if (!file_put_contents($filepath, $data)) {
throw new \IOException(
$filepath,
'Shaarli could not create the config file.
Please make sure Shaarli has the right to write in the folder is it installed in.'
t('Shaarli could not create the config file. '.
'Please make sure Shaarli has the right to write in the folder is it installed in.')
);
}
}

View file

@ -132,7 +132,7 @@ public function get($setting, $default = '')
public function set($setting, $value, $write = false, $isLoggedIn = false)
{
if (empty($setting) || ! is_string($setting)) {
throw new \Exception('Invalid setting key parameter. String expected, got: '. gettype($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.
@ -339,6 +339,10 @@ protected function setDefaultValues()
$this->setEmpty('redirector.url', '');
$this->setEmpty('redirector.encode_url', true);
$this->setEmpty('translation.language', 'auto');
$this->setEmpty('translation.mode', 'php');
$this->setEmpty('translation.extensions', []);
$this->setEmpty('plugins', array());
}

View file

@ -118,8 +118,8 @@ public function write($filepath, $conf)
) {
throw new \IOException(
$filepath,
'Shaarli could not create the config file.
Please make sure Shaarli has the right to write in the folder is it installed in.'
t('Shaarli could not create the config file. '.
'Please make sure Shaarli has the right to write in the folder is it installed in.')
);
}
}

View file

@ -18,6 +18,6 @@ class MissingFieldConfigException extends \Exception
public function __construct($field)
{
$this->field = $field;
$this->message = 'Configuration value is required for '. $this->field;
$this->message = sprintf(t('Configuration value is required for %s'), $this->field);
}
}

View file

@ -12,6 +12,6 @@ class PluginConfigOrderException extends \Exception
*/
public function __construct()
{
$this->message = 'An error occurred while trying to save plugins loading order.';
$this->message = t('An error occurred while trying to save plugins loading order.');
}
}

View file

@ -13,6 +13,6 @@ class UnauthorizedConfigException extends \Exception
*/
public function __construct()
{
$this->message = 'You are not authorized to alter config.';
$this->message = t('You are not authorized to alter config.');
}
}

View file

@ -16,7 +16,7 @@ class IOException extends Exception
public function __construct($path, $message = '')
{
$this->path = $path;
$this->message = empty($message) ? 'Error accessing' : $message;
$this->message = empty($message) ? t('Error accessing') : $message;
$this->message .= ' "' . $this->path .'"';
}
}

View file

@ -19,7 +19,8 @@
"shaarli/netscape-bookmark-parser": "^2.0",
"erusev/parsedown": "1.6",
"slim/slim": "^3.0",
"pubsubhubbub/publisher": "dev-master"
"pubsubhubbub/publisher": "dev-master",
"gettext/gettext": "^4.4"
},
"require-dev": {
"phpmd/phpmd" : "@stable",

249
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "68beedbfa104c788029b079800cfd6e8",
"content-hash": "13b7e1e474fe9264b098ba86face0feb",
"packages": [
{
"name": "container-interop/container-interop",
@ -76,6 +76,129 @@
],
"time": "2015-10-04T16:44:32+00:00"
},
{
"name": "gettext/gettext",
"version": "v4.4.3",
"source": {
"type": "git",
"url": "https://github.com/oscarotero/Gettext.git",
"reference": "4f57f004635cc6311a20815ebfdc0757cb337113"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/oscarotero/Gettext/zipball/4f57f004635cc6311a20815ebfdc0757cb337113",
"reference": "4f57f004635cc6311a20815ebfdc0757cb337113",
"shasum": ""
},
"require": {
"gettext/languages": "^2.3",
"php": ">=5.4.0"
},
"require-dev": {
"illuminate/view": "*",
"phpunit/phpunit": "^4.8|^5.7",
"squizlabs/php_codesniffer": "^3.0",
"symfony/yaml": "~2",
"twig/extensions": "*",
"twig/twig": "^1.31|^2.0"
},
"suggest": {
"illuminate/view": "Is necessary if you want to use the Blade extractor",
"symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator",
"twig/extensions": "Is necessary if you want to use the Twig extractor",
"twig/twig": "Is necessary if you want to use the Twig extractor"
},
"type": "library",
"autoload": {
"psr-4": {
"Gettext\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oscar Otero",
"email": "oom@oscarotero.com",
"homepage": "http://oscarotero.com",
"role": "Developer"
}
],
"description": "PHP gettext manager",
"homepage": "https://github.com/oscarotero/Gettext",
"keywords": [
"JS",
"gettext",
"i18n",
"mo",
"po",
"translation"
],
"time": "2017-08-09T16:59:46+00:00"
},
{
"name": "gettext/languages",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git",
"reference": "49c39e51569963cc917a924b489e7025bfb9d8c7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/49c39e51569963cc917a924b489e7025bfb9d8c7",
"reference": "49c39e51569963cc917a924b489e7025bfb9d8c7",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "^4"
},
"bin": [
"bin/export-plural-rules",
"bin/export-plural-rules.php"
],
"type": "library",
"autoload": {
"psr-4": {
"Gettext\\Languages\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michele Locati",
"email": "mlocati@gmail.com",
"role": "Developer"
}
],
"description": "gettext languages with plural rules",
"homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules",
"keywords": [
"cldr",
"i18n",
"internationalization",
"l10n",
"language",
"languages",
"localization",
"php",
"plural",
"plural rules",
"plurals",
"translate",
"translations",
"unicode"
],
"time": "2017-03-23T17:02:28+00:00"
},
{
"name": "katzgrau/klogger",
"version": "1.2.1",
@ -371,12 +494,12 @@
"source": {
"type": "git",
"url": "https://github.com/pubsubhubbub/php-publisher.git",
"reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7"
"reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/a5d6a0e1cc9d49101c3904480e5b06cbb8addba7",
"reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7",
"url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/0d224daebd504ab61c22fee4db58f8d1fc18945f",
"reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f",
"shasum": ""
},
"require": {
@ -406,7 +529,7 @@
"publishers",
"pubsubhubbub"
],
"time": "2016-11-15T06:24:01+00:00"
"time": "2017-10-08T10:59:41+00:00"
},
{
"name": "shaarli/netscape-bookmark-parser",
@ -632,16 +755,16 @@
},
{
"name": "phpdocumentor/reflection-common",
"version": "1.0",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c"
"reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
"reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
"reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
"shasum": ""
},
"require": {
@ -682,20 +805,20 @@
"reflection",
"static analysis"
],
"time": "2015-12-27T11:43:31+00:00"
"time": "2017-09-11T18:02:19+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "3.2.1",
"version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "183824db76118b9dddffc7e522b91fa175f75119"
"reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/183824db76118b9dddffc7e522b91fa175f75119",
"reference": "183824db76118b9dddffc7e522b91fa175f75119",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157",
"reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157",
"shasum": ""
},
"require": {
@ -727,7 +850,7 @@
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"time": "2017-08-04T20:55:59+00:00"
"time": "2017-08-08T06:39:58+00:00"
},
{
"name": "phpdocumentor/type-resolver",
@ -844,22 +967,22 @@
},
{
"name": "phpspec/prophecy",
"version": "v1.7.0",
"version": "v1.7.2",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "93d39f1f7f9326d746203c7c056f300f7f126073"
"reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073",
"reference": "93d39f1f7f9326d746203c7c056f300f7f126073",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
"reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
"sebastian/comparator": "^1.1|^2.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
},
@ -870,7 +993,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6.x-dev"
"dev-master": "1.7.x-dev"
}
},
"autoload": {
@ -903,7 +1026,7 @@
"spy",
"stub"
],
"time": "2017-03-02T20:05:34+00:00"
"time": "2017-09-04T11:05:03+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -1875,20 +1998,20 @@
},
{
"name": "symfony/config",
"version": "v3.3.6",
"version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297"
"reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297",
"reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297",
"url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
"reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
"shasum": ""
},
"require": {
"php": ">=5.5.9",
"php": "^5.5.9|>=7.0.8",
"symfony/filesystem": "~2.8|~3.0"
},
"conflict": {
@ -1933,20 +2056,20 @@
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
"time": "2017-07-19T07:37:29+00:00"
"time": "2017-10-04T18:56:58+00:00"
},
{
"name": "symfony/console",
"version": "v2.8.26",
"version": "v2.8.28",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd"
"reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/32a3c6b3398de5db8ed381f4ef92970c59c2fcdd",
"reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd",
"url": "https://api.github.com/repos/symfony/console/zipball/f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
"reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
"shasum": ""
},
"require": {
@ -1994,7 +2117,7 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2017-07-29T21:26:04+00:00"
"time": "2017-10-01T21:00:16+00:00"
},
{
"name": "symfony/debug",
@ -2055,20 +2178,20 @@
},
{
"name": "symfony/dependency-injection",
"version": "v3.3.6",
"version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
"reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0"
"reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8d70987f991481e809c63681ffe8ce3f3fde68a0",
"reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
"reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
"shasum": ""
},
"require": {
"php": ">=5.5.9",
"php": "^5.5.9|>=7.0.8",
"psr/container": "^1.0"
},
"conflict": {
@ -2121,24 +2244,24 @@
],
"description": "Symfony DependencyInjection Component",
"homepage": "https://symfony.com",
"time": "2017-07-28T15:27:31+00:00"
"time": "2017-10-04T17:15:30+00:00"
},
{
"name": "symfony/filesystem",
"version": "v3.3.6",
"version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "427987eb4eed764c3b6e38d52a0f87989e010676"
"reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676",
"reference": "427987eb4eed764c3b6e38d52a0f87989e010676",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
"reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
"php": "^5.5.9|>=7.0.8"
},
"type": "library",
"extra": {
@ -2170,24 +2293,24 @@
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
"time": "2017-07-11T07:17:58+00:00"
"time": "2017-10-03T13:33:10+00:00"
},
{
"name": "symfony/finder",
"version": "v3.3.6",
"version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
"reference": "773e19a491d97926f236942484cb541560ce862d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
"url": "https://api.github.com/repos/symfony/finder/zipball/773e19a491d97926f236942484cb541560ce862d",
"reference": "773e19a491d97926f236942484cb541560ce862d",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
"php": "^5.5.9|>=7.0.8"
},
"type": "library",
"extra": {
@ -2219,20 +2342,20 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2017-06-01T21:01:25+00:00"
"time": "2017-10-02T06:42:24+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.4.0",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "f29dca382a6485c3cbe6379f0c61230167681937"
"reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937",
"reference": "f29dca382a6485c3cbe6379f0c61230167681937",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
"reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
"shasum": ""
},
"require": {
@ -2244,7 +2367,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
"dev-master": "1.6-dev"
}
},
"autoload": {
@ -2278,24 +2401,24 @@
"portable",
"shim"
],
"time": "2017-06-09T14:24:12+00:00"
"time": "2017-10-11T12:05:26+00:00"
},
{
"name": "symfony/yaml",
"version": "v3.3.6",
"version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed"
"reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
"url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
"reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
"php": "^5.5.9|>=7.0.8"
},
"require-dev": {
"symfony/console": "~2.8|~3.0"
@ -2333,7 +2456,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2017-07-23T12:43:26+00:00"
"time": "2017-10-05T14:43:42+00:00"
},
{
"name": "theseer/fdomdocument",

View file

@ -1,10 +1,16 @@
<IfModule version_module>
<IfVersion >= 2.4>
Require all denied
<Files "user.css">
Require all granted
</Files>
</IfVersion>
<IfVersion < 2.4>
Allow from none
Deny from all
<Files "user.css">
Allow from all
</Files>
</IfVersion>
</IfModule>

View file

@ -45,6 +45,10 @@ Shaarli cannot import data directly from [Scuttle](https://github.com/scronide/s
However, you can use the third-party [scuttle-to-shaarli](https://github.com/q2apro/scuttle-to-shaarli)
tool to export the Scuttle database to the Netscape HTML format compatible with the Shaarli importer.
### Refind
You can use the third-party tool [Derefind](https://github.com/ShawnPConroy/Derefind) to convert refind.com bookmark exports to a format that can be imported into Shaarli.
## Import Shaarli links to Firefox
- Export your Shaarli links as described above.

View file

@ -21,7 +21,7 @@ _This bookmarklet button is compatible with Firefox, Opera, Chrome and Safari. U
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#196).
See [#196](https://github.com/shaarli/Shaarli/issues/196).
There is an open bug for both Firefox and Chromium:

View file

@ -14,10 +14,24 @@ Use the `Filter by tags` field to restrict displayed links to entries tagged wit
**Hidden tags:** Tags starting with a dot `.` (example `.secret`) are private. They can only be seen and searched when logged in.
Alternatively you can use the `Tag cloud` to discover all tags and click on any of them to display related links.
### Tag cloud
To search for links that are not tagged, enter `""` in the tag search field.
The `Tag cloud` page diplays a "cloud" view of all tags in your Shaarli.
* The most frequently used tags are displayed with a bigger font size.
* When sorting by `Most used` or `Alphabetical`, tags are displayed as a _list_, along with counters and edit/delete buttons for each tag.
* Clicking on any tag will display a list of all Shaares matching this tag.
* Clicking on the counter next to a tag `example`, will filter the tag cloud to only display tags found in Shaares tagged `example`. Repeat this any number of times to further filter the tag cloud. Click `List all links with those tags` to display Shaares matching your current tag filter.
## Filtering RSS feeds/Picture wall
RSS feeds can also be restricted to only return items matching a text/tag search: see [RSS feeds](RSS feeds).
RSS feeds can also be restricted to only return items matching a text/tag search: see [RSS feeds](RSS-feeds).
## Filter buttons
Filter buttons can be found at the top left of the link list. They allow you to apply different filters to the list:
* **Private links:** When this toggle button is enabled, only shaares set to `private` will be shown.
* **Untagged links:** When the this toggle button is enabled (top left of the link list), only shaares _without any tags_ will be shown in the link list.
Filter buttons are only available when logged in.

View file

@ -1,6 +1,59 @@
_Unofficial but related work on Shaarli. If you maintain one of these,
please get in touch with us to help us find a way to adapt your work to our fork._
## Related software
### REST API clients
See [REST API](REST-API) for a list of official and community clients.
### Third party plugins
- [autosave](https://github.com/kalvn/shaarli-plugin-autosave) by [@kalvn](https://github.com/kalvn): Automatically saves data when editing a link to avoid any loss in case of crash or unexpected shutdown.
- [Code Coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter.
- [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli.
- [emojione](https://github.com/NerosTie/emojione) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli.
- [twemoji](https://github.com/NerosTie/twemoji) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli (Twemoji version)
- [google analytics](https://github.com/ericjuden/Shaarli-Google-Analytics-Plugin) by [@ericjuden](http://github.com/ericjuden): Adds Google Analytics tracking support
- [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.
- [markdown-toolbar](https://github.com/immanuelfodor/shaarli-markdown-toolbar) by [@immanuelfodor](https://github.com/immanuelfodor) - Easily insert markdown syntax into the Description field when editing a link.
- [related](https://github.com/ilesinge/shaarli-related) by [@ilesinge](https://github.com/ilesinge) - Show related links based on the number of identical tags.
- [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks.
- [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli
- [shaarli2mastodon](https://github.com/kalvn/shaarli2mastodon) by [@kalvn](https://github.com/kalvn) - This Shaarli plugin allows you to automatically publish links you post on your Mastodon timeline.
- [shaarli-descriptor](https://github.com/immanuelfodor/shaarli-descriptor) by [@immanuelfodor](https://github.com/immanuelfodor) - Customize the default height/number of rows of the Description field when editing a link.
### Third-party themes
See [Theming](Theming) for a list of community-contributed themes, and an installation guide.
### Integration with other platforms
- [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
### 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
### Browser addons
* [Shaarli Web Extension](https://github.com/ikipatang/shaarli-web-extension) - toolbar button to share your current tab with Shaarli.
### Server apps
- [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content
- [shaarli-river](https://github.com/mknexen/shaarli-river) - An aggregator for shaarlis with many features
- [Shaarlo](https://github.com/DMeloni/shaarlo) - An aggregator for shaarlis with many features (a very popular running instance among French shaarliers: [shaarli.fr](http://shaarli.fr/))
- [Shaarlimages](https://github.com/BoboTiG/shaarlimages) - An image-oriented aggregator for Shaarlis
- [mknexen/shaarli-api](https://github.com/mknexen/shaarli-api) - A REST API for Shaarli
- [Self dead link](https://github.com/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php) - Detect dead links on shaarli. This version use the database of shaarli. [Another version](https://github.com/qwertygc/shaarli-dev-code/blob/master/dead-link.php), can be used for other shaarli instances (but is more resource consuming).
- [Bookmark Archiver](https://github.com/pirate/bookmark-archiver) - Save an archived copy of all websites starred using browser bookmarks/Shaarli/Delicious/Instapaper/Unmark.it/Pocket/Pinboard. Outputs browseable html.
## Alternatives to Shaarli
See [awesome-selfhosted: bookmarks & link sharing](https://github.com/Kickball/awesome-selfhosted/#bookmarks--link-sharing).
## Community
- [Liens en vrac de sebsauvage](http://sebsauvage.net/links/) - the original Shaarli
- [A large list of Shaarlis](http://porneia.free.fr/pub/links/ou-est-shaarli.html)
@ -12,56 +65,8 @@ please get in touch with us to help us find a way to adapt your work to our fork
- [Original revisions history](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)
- [Shaarli.fr/my](https://www.shaarli.fr/my.php) - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of [DMeloni](https://github.com/DMeloni)
### Articles and social media discussions
- 2016-09-22 - Hacker News - https://news.ycombinator.com/item?id=12552176
- 2015-08-15 - Reddit - [Question about migrating from WordPress to Shaarli.](https://www.reddit.com/r/selfhosted/comments/3h3zwh/question_about_migrating_from_wordpress_to_shaarli/)
- 2015-06-22 - Hacker News - https://news.ycombinator.com/item?id=9755366
- 2015-05-12 - Reddit - [shaarli - Self hosted Bookmarking / Delicious (PHP, MySQL)](https://www.reddit.com/r/selfhosted/comments/35pkkc/shaarli_self_hosted_bookmarking_delicious_php/)
### REST API clients
See [REST API](REST-API) for a list of official and community clients.
### Third party plugins
- [autosave](https://github.com/kalvn/shaarli-plugin-autosave) by [@kalvn](https://github.com/kalvn): Automatically saves data when editing a link to avoid any loss in case of crash or unexpected shutdown.
- [Code Coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter.
- [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli.
- [emojione](https://github.com/NerosTie/emojione) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli.
- [google analytics](https://github.com/ericjuden/Shaarli-Google-Analytics-Plugin) by [@ericjuden](http://github.com/ericjuden): Adds Google Analytics tracking support
- [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.
- [related](https://github.com/ilesinge/shaarli-related) by [@ilesinge](https://github.com/ilesinge) - Show related links based on the number of identical tags.
- [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks.
- [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli
### Third-party themes
See [Theming](Theming) for a list of community-contributed themes, and an installation guide.
## Integration with other platforms
- [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
### Mobile Apps
- [ShaarliOS](https://github.com/mro/ShaarliOS) iOS share extension - see [#308](https://github.com/shaarli/Shaarli/issues/308#issuecomment-184592070) for some promo codes,
- [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
### Server apps
- [shaarchiver](https://github.com/nodiscc/shaarchiver) - Archive your Shaarli bookmarks and their content
- [shaarli-river](https://github.com/mknexen/shaarli-river) - An aggregator for shaarlis with many features
- [Shaarlo](https://github.com/DMeloni/shaarlo) - An aggregator for shaarlis with many features (a very popular running instance among French shaarliers: [shaarli.fr](http://shaarli.fr/))
- [Shaarlimages](https://github.com/BoboTiG/shaarlimages) - An image-oriented aggregator for Shaarlis
- [mknexen/shaarli-api](https://github.com/mknexen/shaarli-api) - A REST API for Shaarli
- [Self dead link](https://github.com/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php) - Detect dead links on shaarli. This version use the database of shaarli. [Another version](https://github.com/qwertygc/shaarli-dev-code/blob/master/dead-link.php), can be used for other shaarli instances (but is more resource consuming).
- [Bookmark Archiver](https://github.com/pirate/bookmark-archiver) - Save an archived copy of all websites starred using browser bookmarks/Shaarli/Delicious/Instapaper/Unmark.it/Pocket/Pinboard. Outputs browseable html.
## Alternatives to Shaarli
See the [bookmarks & link sharing](https://github.com/Kickball/awesome-selfhosted/#bookmarks--link-sharing)
section on [awesome-selfhosted](https://github.com/Kickball/awesome-selfhosted/).

View file

@ -4,44 +4,57 @@ 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).
Several releases are available:
Multiple releases branches are available:
- latest (last release)
- stable (previous major release)
- master (development)
Using one of the following methods:
- by downloading full release archives including all dependencies
- by downloading Github archives
- by cloning the Git repository
- using Docker: [see the documentation](docker/shaarli-images.md)
---
--------------------------------------------------------------------------------
## Latest release (recommended)
### Download as an archive
Get the latest released version 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.1`
Or in command lines:
The current latest released version is `v0.9.3`
```bash
$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.1/shaarli-v0.9.1-full.zip
$ unzip shaarli-v0.9.1-full.zip
$ wget https://github.com/shaarli/Shaarli/releases/download/v0.9.3/shaarli-v0.9.3-full.zip
$ unzip shaarli-v0.9.3-full.zip
$ mv Shaarli /path/to/shaarli/
```
In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page. Cloning using `git` or downloading Github branches as zip files requires additional steps (see below).|
### Using git
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 [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 v0.9 https://github.com/shaarli/Shaarli.git .
$ git clone -b latest https://github.com/shaarli/Shaarli.git .
$ composer install --no-dev --prefer-dist
$ make translate
$ make htmldoc
```
--------------------------------------------------------------------------------
## Stable version
The stable version has been experienced by Shaarli users, and will receive security updates.
### Download as an archive
As a .zip archive:
@ -60,9 +73,9 @@ $ tar xvf stable.tar.gz
$ mv Shaarli-stable /path/to/shaarli/
```
### Clone with Git
### Using git
[Composer](https://getcomposer.org/) is required to build a functional Shaarli installation when pulling from git.
Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
```bash
$ git clone https://github.com/shaarli/Shaarli.git -b stable /path/to/shaarli/
@ -71,10 +84,15 @@ $ cd /path/to/shaarli/
$ composer install --no-dev --prefer-dist
```
--------------------------------------------------------------------------------
## Development version (mainline)
_Use at your own risk!_
Install [Composer](Unit-tests.md#install_composer) to manage Shaarli dependencies.
To get the latest changes from the `master` branch:
```bash
@ -83,13 +101,17 @@ $ 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 translate
$ make htmldoc
```
-------------------------------------------------------------------------------
## Finish Installation
Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.
![install screenshot](http://i.imgur.com/wuMpDSN.png)
![install screenshot](images/install-shaarli.png)
Setup your Shaarli installation, and it's ready to use!

View file

@ -1,25 +0,0 @@
### Main features
Shaarli is intended:
- to share, comment and save interesting links and news
- to bookmark useful/frequent personal links (as private links) and share them between computers
- as a minimal blog/microblog/writing platform (no character limit)
- as a read-it-later list (for example items tagged `readlater`)
- to draft and save articles/ideas
- to keep code snippets
- to keep notes and documentation
- as a shared clipboard between machines
- as a todo list
- to store playlists (e.g. with the `music` or `video` tags)
- to keep extracts/comments from webpages that may disappear
- to keep track of ongoing discussions (for example items tagged `discussion`)
- [to feed RSS aggregators](http://shaarli.chassegnouf.net/?9Efeiw) (planets) with specific tags
- to feed other social networks, blogs... using RSS feeds and external services (dlvr.it, ifttt.com ...)
### Using Shaarli as a blog, notepad, pastebin...
- Go to your Shaarli setup and log in
- Click the `Add Link` button
- To share text only, do not enter any URL in the corresponding input field and click `Add Link`
- Pick a title and enter your article, or note, in the description field; add a few tags; optionally check `Private` then click `Save`
- Voilà! Your article is now published (privately if you selected that option) and accessible using its permalink.

View file

@ -1,3 +1,6 @@
| 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`

View file

@ -35,7 +35,8 @@ Library | Required? | Usage
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 | multibyte (Unicode) string support
[`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)

View file

@ -81,6 +81,20 @@ _These settings should not be edited_
- **page_cache**: Shaarli's internal cache directory.
- **ban_file**: Banned IP file path.
### Translation
- **language**: translation language (also see [Translations](Translations))
- **auto** (default): The translation language is chosen from the browser locale.
It means that the language can be different for 2 different visitors depending on their locale.
- **en**: Use the English translation.
- **fr**: Use the French translation.
- **mode**:
- **auto** or **php** (default): Use the PHP implementation of gettext (slower)
- **gettext**: Use PHP builtin gettext extension
(faster, but requires `php-gettext` to be installed and to reload the web server on update)
- **extension**: Translation extensions for custom themes or plugins.
Must be an associative array: `translation domain => translation path`.
### Updates
- **check_updates**: Enable or disable update check to the git repository.
@ -211,6 +225,13 @@ _These settings should not be edited_
"plugins": {
"WALLABAG_URL": "http://demo.wallabag.org",
"WALLABAG_VERSION": "1"
},
"translation": {
"language": "fr",
"mode": "php",
"extensions": {
"demo": "plugins/demo_plugin/languages/"
}
}
} ?>
```

152
doc/md/Translations.md Normal file
View file

@ -0,0 +1,152 @@
## Translations
Shaarli supports [gettext](https://www.gnu.org/software/gettext/manual/gettext.html) translations
since `>= v0.9.2`.
Note that only the `default` theme supports translations.
### Contributing
We encourage the community to contribute to Shaarli's translation either by improving existing
translations or submitting a new language.
Contributing to the translation does not require development skill.
Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`)
is not stored on the repository, and is generated during the release process.
### How to
First, install [Poedit](https://poedit.net/) tool.
Poedit will extract strings to translate from the PHP source code.
**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract
every translatable string.
You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended)
or visit every template page in your browser to generate cache files, while logged in.
Here is a list :
```
http://<replace_domain>/
http://<replace_domain>/?nonope
http://<replace_domain>/?do=addlink
http://<replace_domain>/?do=changepasswd
http://<replace_domain>/?do=changetag
http://<replace_domain>/?do=configure
http://<replace_domain>/?do=tools
http://<replace_domain>/?do=daily
http://<replace_domain>/?post
http://<replace_domain>/?do=export
http://<replace_domain>/?do=import
http://<replace_domain>/?do=login
http://<replace_domain>/?do=picwall
http://<replace_domain>/?do=pluginadmin
http://<replace_domain>/?do=tagcloud
http://<replace_domain>/?do=taglist
```
#### Improve existing translation
In Poedit, click on "Edit a Translation", and from Shaarli's directory open
`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
The existing list of translatable strings should have been loaded, then click on the "Update" button.
You can start editing the translation.
![poedit-screenshot](images/poedit-1.jpg)
Save when you're done, then you can submit a pull request containing the updated `shaarli.po`.
#### Add a new language
Open Poedit and select "Create New Translation", then from Shaarli's directory open
`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
Then select the language you want to create.
Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`.
`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2)
format in lowercase (e.g. `de` for German).
Then click on the "Update" button, and you can start to translate every available string.
Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
### Extend Shaarli's translation
If you're writing a custom theme, or a non official plugin, you might want to use the translation system,
but you won't be able to able to override Shaarli's translation.
However, you can add your own translation domain which extends the main translation list.
> Note that you can find a live example of translation extension in the `demo_plugin`.
First, create your translation files tree directory:
```
<your_module>/languages/<ISO 3166-1 alpha-2 language code>/LC_MESSAGES/
```
Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
`my_theme.po`.
Users have to register your extension in their configuration with the parameter
`translation.extensions.<domain>: <translation files path>`.
Example:
```php
if (! $conf->exists('translation.extensions.my_theme')) {
$conf->set('translation.extensions.my_theme', '<your_module>/languages/');
$conf->write(true);
}
```
> Note that the page needs to be reloaded after the registration.
It is then recommended to create a custom translation function which will call the `t()` function with your domain.
For example :
```php
function my_theme_t($text, $nText = '', $nb = 1)
{
return t($text, $nText, $nb, 'my_theme'); // the last parameter is your translation domain.
}
```
All strings which can be translated should be processed through your function:
```php
my_theme_t('Comment');
my_theme_t('Comment', 'Comments', 2);
```
Or in templates:
```php
{'Comment'|my_theme_t}
{function="my_theme_t('Comment', 'Comments', 2)"}
```
> Note than in template, you need to visit your page at least once to generate a cache file.
When you're done, open Poedit and load translation strings from sources:
1. `File > New`
2. Choose your language
3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
4. Go to `Catalog > Properties...`
5. Fill the `Translation Properties` tab
6. Add your source path in the `Sources Paths` tab
7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
```
my_theme_t
my_theme_t:1,2
```
Click on the "Update" button and you're free to start your translations!

View file

@ -2,12 +2,12 @@
The framework used is [PHPUnit](https://phpunit.de/); it can be installed with [Composer](https://getcomposer.org/), which is a dependency management tool.
Regarding Composer, you can either use:
### Install composer
You can either use:
- a system-wide version, e.g. installed through your distro's package manager
- a local version, downloadable [here](https://getcomposer.org/download/)
#### Sample usage
- a local version, downloadable [here](https://getcomposer.org/download/).
```bash
# system-wide version
@ -29,6 +29,8 @@ $ composer update
#### Install and enable Xdebug to generate PHPUnit coverage reports
See http://xdebug.org/docs/install
For Debian-based distros:
```bash
$ aptitude install php5-xdebug

View file

@ -14,7 +14,7 @@ Shaarli stores all user data under the `data` directory:
- `data/ipbans.php` - banned IP addresses
- `data/updates.txt` - contains all automatic update to the configuration and datastore files already run
See [Shaarli configuration](Shaarli configuration) for more information about Shaarli resources.
See [Shaarli configuration](Shaarli-configuration) for more information about Shaarli resources.
It is recommended to backup this repository _before_ starting updating/upgrading Shaarli:
@ -27,7 +27,7 @@ As all user data is kept under `data`, this is the only directory you need to wo
- backup the `data` directory
- install or update Shaarli:
- fresh installation - see [Download and installation](Download and installation)
- fresh installation - see [Download and installation](Download-and-installation)
- update - see the following sections
- check or restore the `data` directory
@ -35,10 +35,13 @@ As all user data is kept under `data`, this is the only directory you need to wo
All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page.
We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download and installation) for `git` complete instructions.
We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation) for `git` complete instructions.
Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
If you use translations in gettext mode - meaning you manually changed the default mode -,
reload your web server.
After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details).
## Upgrading with Git
@ -72,6 +75,14 @@ Updating dependencies
Downloading: 100%
```
Shaarli >= `v0.9.2` supports translations:
```bash
$ make translate
```
If you use translations in gettext mode, reload your web server.
### Migrating and upgrading from Sebsauvage's repository
If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy.
@ -151,6 +162,14 @@ Updating dependencies
Downloading: 100%
```
Shaarli >= `v0.9.2` supports translations:
```bash
$ make translate
```
If you use translations in gettext mode, reload your web server.
Optionally, you can delete information related to the legacy version:
```bash
@ -173,7 +192,7 @@ Total 3317 (delta 2050), reused 3301 (delta 2034)to
#### Step 3: configuration
After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli configuration) for more details).
After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration) for more details).
## Troubleshooting

View file

@ -1,6 +1,120 @@
## Foreword
This guide assumes that:
- Shaarli runs in a Docker container
- The host's `10080` port is mapped to the container's `80` port
- Shaarli's Fully Qualified Domain Name (FQDN) is `shaarli.domain.tld`
- HTTP traffic is redirected to HTTPS
## Apache
- [Apache 2.4 documentation](https://httpd.apache.org/docs/2.4/)
- [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
- [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)
The following HTTP headers are set by using the `ProxyPass` directive:
- `X-Forwarded-For`
- `X-Forwarded-Host`
- `X-Forwarded-Server`
```apache
<VirtualHost *:80>
ServerName shaarli.domain.tld
Redirect permanent / https://shaarli.domain.tld
</VirtualHost>
<VirtualHost *:443>
ServerName shaarli.domain.tld
SSLEngine on
SSLCertificateFile /path/to/cert
SSLCertificateKeyFile /path/to/certkey
LogLevel warn
ErrorLog /var/log/apache2/shaarli-error.log
CustomLog /var/log/apache2/shaarli-access.log combined
RequestHeader set X-Forwarded-Proto "https"
ProxyPass / http://127.0.0.1:10080/
ProxyPassReverse / http://127.0.0.1:10080/
</VirtualHost>
```
TODO, see https://github.com/shaarli/Shaarli/issues/888
## HAProxy
- [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/)
```conf
global
[...]
defaults
[...]
frontend http-in
bind :80
redirect scheme https code 301 if !{ ssl_fc }
bind :443 ssl crt /path/to/cert.pem
default_backend shaarli
backend shaarli
mode http
option http-server-close
option forwardfor
reqadd X-Forwarded-Proto: https
server shaarli1 127.0.0.1:10080
```
## Nginx
- [Nginx documentation](https://nginx.org/en/docs/)
```nginx
http {
[...]
index index.html index.php;
root /home/john/web;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
server {
listen 80;
server_name shaarli.domain.tld;
return 301 https://shaarli.domain.tld$request_uri;
}
server {
listen 443 ssl http2;
server_name shaarli.domain.tld;
ssl_certificate /path/to/cert
ssl_certificate_key /path/to/certkey
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_pass http://localhost:10080/;
proxy_set_header Host $host;
proxy_connect_timeout 30s;
proxy_read_timeout 120s;
access_log /var/log/nginx/shaarli.access.log;
error_log /var/log/nginx/shaarli.error.log;
}
}
}
```

View file

@ -1,3 +1,6 @@
A brief guide on getting starting using docker is given in [Docker 101](docker-101.md).
To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](../Upgrade-and-migration.md).
## Get and run a Shaarli image
### DockerHub repository
@ -5,14 +8,24 @@ The images can be found in the [`shaarli/shaarli`](https://hub.docker.com/r/shaa
repository.
### Available image tags
- `latest`: master branch (tarball release)
- `latest`: latest branch (tarball release)
- `master`: master branch (tarball release)
- `stable`: stable branch (tarball release)
All images rely on:
The `latest` and `master` images rely on:
- [Alpine Linux](https://www.alpinelinux.org/)
- [PHP7-FPM](http://php-fpm.org/)
- [Nginx](http://nginx.org/)
The `stable` image relies on:
- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
- [PHP5-FPM](http://php-fpm.org/)
- [Nginx](http://nginx.org/)
Additional [Dockerfiles](https://github.com/shaarli/Shaarli/tree/master/docker) are provided for the `arm32v7` platform, relying on [Linuxserver.io Alpine armhf images](https://hub.docker.com/r/lsiobase/alpine.armhf/). These images must be built using [`docker build`](https://docs.docker.com/engine/reference/commandline/build/) on an `arm32v7` machine or using an emulator such as [qemu](https://resin.io/blog/building-arm-containers-on-any-x86-machine-even-dockerhub/).
### Download from DockerHub
```bash
$ docker pull shaarli/shaarli
@ -69,3 +82,14 @@ backstabbing_galileo
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
### Automatic builds
Docker users can start a personal instance from an [autobuild image](https://hub.docker.com/r/shaarli/shaarli/). For example to start a temporary Shaarli at ``localhost:8000``, and keep session data (config, storage):
```
MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
docker run -ti --rm \
-p 8000:80 \
-v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
shaarli/shaarli
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
doc/md/images/poedit-1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -22,20 +22,25 @@ It runs the latest development version of Shaarli and is updated/reset daily.
Login: `demo`; Password: `demo`
Docker users can start a personal instance from an [autobuild image](https://hub.docker.com/r/shaarli/shaarli/). For example to start a temporary Shaarli at ``localhost:8000``, and keep session data (config, storage):
```
MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
docker run -ti --rm \
-p 8000:80 \
-v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
shaarli/shaarli
```
A brief guide on getting starting using docker is given in [Docker 101](docker/docker-101).
To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](Upgrade-and-migration) documentation.
## Features
Shaarli can be used:
- to share, comment and save interesting links and news.
- to bookmark useful/frequent personal links (as private links) and share them between computers.
- as a minimal blog/microblog/writing platform (no character limit).
- as a read-it-later list (for example items tagged `readlater`).
- to draft and save articles/posts/ideas.
- to keep code snippets.
- to keep notes and documentation.
- as a shared clipboard/notepad/pastebin between machines.
- as a todo list.
- to store playlists (e.g. with the `music` or `video` tags).
- to keep extracts/comments from webpages that may disappear.
- to keep track of ongoing discussions (for example items tagged `discussion`).
- [to feed RSS aggregators](http://shaarli.chassegnouf.net/?9Efeiw) (planets) with specific tags.
- to feed other social networks, blogs... using RSS feeds and external services (dlvr.it, ifttt.com ...).
### Interface
- minimalist design (simple is beautiful)
- FAST
@ -89,14 +94,12 @@ Easily extensible by any client using the REST API exposed by Shaarli.
See the [API documentation](http://shaarli.github.io/api-documentation/).
### Other usages
Though Shaarli is primarily a bookmarking application, it can serve other purposes
(see [Features](Features)):
- micro-blogging
- pastebin
- online notepad
- snippet archive
### Using Shaarli as a blog, notepad, pastebin...
- Go to your Shaarli setup and log in
- Click the `Add Link` button
- To share text only, do not enter any URL in the corresponding input field and click `Add Link`
- Pick a title and enter your article, or note, in the description field; add a few tags; optionally check `Private` then click `Save`
- Voilà! Your article is now published (privately if you selected that option) and accessible using its permalink.
## About
### Shaarli community fork

View file

@ -0,0 +1,47 @@
FROM lsiobase/alpine.armhf:3.6
MAINTAINER Shaarli Community
RUN apk --update --no-cache add \
ca-certificates \
curl \
nginx \
php7 \
php7-ctype \
php7-curl \
php7-fpm \
php7-gd \
php7-iconv \
php7-intl \
php7-json \
php7-mbstring \
php7-openssl \
php7-phar \
php7-session \
php7-xml \
php7-zlib \
s6
COPY nginx.conf /etc/nginx/nginx.conf
COPY php-fpm.conf /etc/php7/php-fpm.conf
COPY services.d /etc/services.d
RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
&& rm -rf /etc/php7/php-fpm.d/www.conf \
&& sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
&& sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
WORKDIR /var/www
RUN curl -L https://github.com/shaarli/Shaarli/archive/latest.tar.gz | tar xzf - \
&& mv Shaarli-latest shaarli \
&& cd shaarli \
&& composer --prefer-dist --no-dev install \
&& rm -rf ~/.composer \
&& chown -R nginx:nginx .
VOLUME /var/www/shaarli/data
EXPOSE 80
ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
CMD []

View file

@ -0,0 +1,47 @@
FROM lsiobase/alpine.armhf:3.6
MAINTAINER Shaarli Community
RUN apk --update --no-cache add \
ca-certificates \
curl \
nginx \
php7 \
php7-ctype \
php7-curl \
php7-fpm \
php7-gd \
php7-iconv \
php7-intl \
php7-json \
php7-mbstring \
php7-openssl \
php7-phar \
php7-session \
php7-xml \
php7-zlib \
s6
COPY nginx.conf /etc/nginx/nginx.conf
COPY php-fpm.conf /etc/php7/php-fpm.conf
COPY services.d /etc/services.d
RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
&& rm -rf /etc/php7/php-fpm.d/www.conf \
&& sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
&& sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
WORKDIR /var/www
RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
&& mv Shaarli-master shaarli \
&& cd shaarli \
&& composer --prefer-dist --no-dev install \
&& rm -rf ~/.composer \
&& chown -R nginx:nginx .
VOLUME /var/www/shaarli/data
EXPOSE 80
ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
CMD []

View file

@ -0,0 +1,47 @@
FROM alpine:3.6
MAINTAINER Shaarli Community
RUN apk --update --no-cache add \
ca-certificates \
curl \
nginx \
php7 \
php7-ctype \
php7-curl \
php7-fpm \
php7-gd \
php7-iconv \
php7-intl \
php7-json \
php7-mbstring \
php7-openssl \
php7-phar \
php7-session \
php7-xml \
php7-zlib \
s6
COPY nginx.conf /etc/nginx/nginx.conf
COPY php-fpm.conf /etc/php7/php-fpm.conf
COPY services.d /etc/services.d
RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
&& rm -rf /etc/php7/php-fpm.d/www.conf \
&& sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
&& sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
WORKDIR /var/www
RUN curl -L https://github.com/shaarli/Shaarli/archive/latest.tar.gz | tar xzf - \
&& mv Shaarli-latest shaarli \
&& cd shaarli \
&& composer --prefer-dist --no-dev install \
&& rm -rf ~/.composer \
&& chown -R nginx:nginx .
VOLUME /var/www/shaarli/data
EXPOSE 80
ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
CMD []

View file

@ -0,0 +1,47 @@
FROM alpine:3.6
MAINTAINER Shaarli Community
RUN apk --update --no-cache add \
ca-certificates \
curl \
nginx \
php7 \
php7-ctype \
php7-curl \
php7-fpm \
php7-gd \
php7-iconv \
php7-intl \
php7-json \
php7-mbstring \
php7-openssl \
php7-phar \
php7-session \
php7-xml \
php7-zlib \
s6
COPY nginx.conf /etc/nginx/nginx.conf
COPY php-fpm.conf /etc/php7/php-fpm.conf
COPY services.d /etc/services.d
RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
&& rm -rf /etc/php7/php-fpm.d/www.conf \
&& sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
&& sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
WORKDIR /var/www
RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
&& mv Shaarli-master shaarli \
&& cd shaarli \
&& composer --prefer-dist --no-dev install \
&& rm -rf ~/.composer \
&& chown -R nginx:nginx .
VOLUME /var/www/shaarli/data
EXPOSE 80
ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
CMD []

10
docker/alpine/IMAGE.md Normal file
View file

@ -0,0 +1,10 @@
## Alpine images
- [Alpine Linux](https://www.alpinelinux.org/)
- [PHP-FPM](http://php-fpm.org/)
- [Nginx](http://nginx.org/)
### `shaarli/shaarli:latest`
- [Shaarli](https://github.com/shaarli/Shaarli), `latest` branch
### `shaarli/shaarli:master`
- [Shaarli](https://github.com/shaarli/Shaarli), `master` branch

View file

@ -1,6 +1,7 @@
user www-data www-data;
user nginx nginx;
daemon off;
worker_processes 4;
pid /var/run/nginx.pid;
events {
worker_connections 768;
@ -59,7 +60,7 @@ http {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# filter and proxy PHP requests to PHP-FPM
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
}

View file

@ -0,0 +1,16 @@
[global]
daemonize = no
[www]
user = nginx
group = nginx
listen.owner = nginx
listen.group = nginx
catch_workers_output = yes
listen = /var/run/php-fpm.sock
pm = dynamic
pm.max_children = 20
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 2048

View file

@ -0,0 +1,2 @@
#!/bin/sh
/bin/true

View file

@ -0,0 +1,2 @@
#!/bin/execlineb -P
nginx

View file

@ -0,0 +1,2 @@
#!/bin/execlineb -P
php-fpm7 -F

View file

@ -1,37 +0,0 @@
FROM debian:jessie
MAINTAINER Shaarli Community
ENV TERM dumb
RUN apt-get update \
&& apt-get install --no-install-recommends -y \
ca-certificates \
curl \
nginx-light \
php5-curl \
php5-fpm \
php5-gd \
php5-intl \
supervisor \
&& apt-get clean
RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini
RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini
COPY nginx.conf /etc/nginx/nginx.conf
COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
RUN chmod 755 /usr/local/bin/composer
WORKDIR /var/www
RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
&& mv Shaarli-master shaarli \
&& cd shaarli \
&& composer --prefer-dist --no-dev install
RUN rm -rf html \
&& chown -R www-data:www-data .
VOLUME /var/www/shaarli/data
EXPOSE 80
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]

View file

@ -1,5 +0,0 @@
## shaarli:latest
- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
- [PHP5-FPM](http://php-fpm.org/)
- [Nginx](http://nginx.org/)
- [Shaarli](https://github.com/shaarli/Shaarli)

View file

@ -1,13 +0,0 @@
[program:php5-fpm]
command=/usr/sbin/php5-fpm -F
priority=5
autostart=true
autorestart=true
[program:nginx]
command=/usr/sbin/nginx
priority=10
autostart=true
autorestart=true
stdout_events_enabled=true
stderr_events_enabled=true

File diff suppressed because it is too large Load diff

178
index.php
View file

@ -64,7 +64,6 @@
require_once 'application/FileUtils.php';
require_once 'application/History.php';
require_once 'application/HttpUtils.php';
require_once 'application/Languages.php';
require_once 'application/LinkDB.php';
require_once 'application/LinkFilter.php';
require_once 'application/LinkUtils.php';
@ -76,8 +75,10 @@
require_once 'application/PluginManager.php';
require_once 'application/Router.php';
require_once 'application/Updater.php';
use \Shaarli\Languages;
use \Shaarli\ThemeUtils;
use \Shaarli\Config\ConfigManager;
use \Shaarli\SessionManager;
// Ensure the PHP version is supported
try {
@ -115,14 +116,23 @@
}
// Regenerate session ID if invalid or not defined in cookie.
if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) {
if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
session_regenerate_id(true);
$_COOKIE['shaarli'] = session_id();
}
$conf = new ConfigManager();
$sessionManager = new SessionManager($_SESSION, $conf);
// Sniff browser language and set date format accordingly.
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
}
new Languages(setlocale(LC_MESSAGES, 0), $conf);
$conf->setEmpty('general.timezone', date_default_timezone_get());
$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER)));
$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
@ -144,7 +154,7 @@
$errors = ApplicationUtils::checkResourcePermissions($conf);
if ($errors != array()) {
$message = '<p>Insufficient permissions:</p><ul>';
$message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
foreach ($errors as $error) {
$message .= '<li>'.$error.'</li>';
@ -157,17 +167,12 @@
}
// Display the installation form if no existing config is found
install($conf);
install($conf, $sessionManager);
}
// a token depending of deployment salt, user password, and the current ip
define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
// Sniff browser language and set date format accordingly.
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
}
/**
* Checking session state (i.e. is the user still logged in)
*
@ -376,9 +381,9 @@ function ban_canLogin($conf)
// Process login form: Check if login/password is correct.
if (isset($_POST['login']))
{
if (!ban_canLogin($conf)) die('I said: NO. You are banned for the moment. Go away.');
if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.'));
if (isset($_POST['password'])
&& tokenOk($_POST['token'])
&& $sessionManager->checkToken($_POST['token'])
&& (check_auth($_POST['login'], $_POST['password'], $conf))
) { // Login/password is OK.
ban_loginOk($conf);
@ -440,7 +445,8 @@ function ban_canLogin($conf)
}
}
}
echo '<script>alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen.
// Redirect to login screen.
echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
exit;
}
}
@ -450,32 +456,6 @@ function ban_canLogin($conf)
// Token should be used in any form which acts on data (create,update,delete,import...).
if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session.
/**
* Returns a token.
*
* @param ConfigManager $conf Configuration Manager instance.
*
* @return string token.
*/
function getToken($conf)
{
$rnd = sha1(uniqid('', true) .'_'. mt_rand() . $conf->get('credentials.salt')); // We generate a random string.
$_SESSION['tokens'][$rnd]=1; // Store it on the server side.
return $rnd;
}
// Tells if a token is OK. Using this function will destroy the token.
// true=token is OK.
function tokenOk($token)
{
if (isset($_SESSION['tokens'][$token]))
{
unset($_SESSION['tokens'][$token]); // Token is used: destroy it.
return true; // Token is OK.
}
return false; // Wrong token, or already used.
}
/**
* Daily RSS feed: 1 RSS entry per day giving all the links on that day.
* Gives the last 7 days (which have links).
@ -546,7 +526,11 @@ function showDailyRSS($conf) {
// We pre-format some fields for proper output.
foreach ($links as &$link) {
$link['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url'));
$link['formatedDescription'] = format_description(
$link['description'],
$conf->get('redirector.url'),
$conf->get('redirector.encode_url')
);
$link['thumbnail'] = thumbnail($conf, $link['url']);
$link['timestamp'] = $link['created']->getTimestamp();
if (startsWith($link['url'], '?')) {
@ -618,7 +602,11 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
$taglist = explode(' ',$link['tags']);
uasort($taglist, 'strcasecmp');
$linksToDisplay[$key]['taglist']=$taglist;
$linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url'));
$linksToDisplay[$key]['formatedDescription'] = format_description(
$link['description'],
$conf->get('redirector.url'),
$conf->get('redirector.encode_url')
);
$linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']);
$linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp();
}
@ -687,8 +675,9 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
* @param PluginManager $pluginManager Plugin Manager instance,
* @param LinkDB $LINKSDB
* @param History $history instance
* @param SessionManager $sessionManager SessionManager instance
*/
function renderPage($conf, $pluginManager, $LINKSDB, $history)
function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
{
$updater = new Updater(
read_updates_file($conf->get('resource.updates')),
@ -709,7 +698,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
die($e->getMessage());
}
$PAGE = new PageBuilder($conf, $LINKSDB);
$PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken());
$PAGE->assign('linkcount', count($LINKSDB));
$PAGE->assign('privateLinkcount', count_private($LINKSDB));
$PAGE->assign('plugin_errors', $pluginManager->getErrors());
@ -1100,16 +1089,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
if ($targetPage == Router::$PAGE_CHANGEPASSWORD)
{
if ($conf->get('security.open_shaarli')) {
die('You are not supposed to change a password on an Open Shaarli.');
die(t('You are not supposed to change a password on an Open Shaarli.'));
}
if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword']))
{
if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away!
if (!$sessionManager->checkToken($_POST['token'])) die(t('Wrong token.')); // Go away!
// Make sure old password is correct.
$oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt'));
if ($oldhash!= $conf->get('credentials.hash')) { echo '<script>alert("The old password is not correct.");document.location=\'?do=changepasswd\';</script>'; exit; }
if ($oldhash!= $conf->get('credentials.hash')) {
echo '<script>alert("'. t('The old password is not correct.') .'");document.location=\'?do=changepasswd\';</script>';
exit;
}
// Save new password
// Salt renders rainbow-tables attacks useless.
$conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
@ -1127,7 +1119,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
exit;
}
echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>';
echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
exit;
}
else // show the change password form.
@ -1142,8 +1134,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
{
if (!empty($_POST['title']) )
{
if (!tokenOk($_POST['token'])) {
die('Wrong token.'); // Go away!
if (!$sessionManager->checkToken($_POST['token'])) {
die(t('Wrong token.')); // Go away!
}
$tz = 'UTC';
if (!empty($_POST['continent']) && !empty($_POST['city'])
@ -1163,6 +1155,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
$conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
$conf->set('api.enabled', !empty($_POST['enableApi']));
$conf->set('api.secret', escape($_POST['apiSecret']));
$conf->set('translation.language', escape($_POST['language']));
try {
$conf->write(isLoggedIn());
$history->updateSettings();
@ -1178,7 +1172,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
exit;
}
echo '<script>alert("Configuration was saved.");document.location=\'?do=configure\';</script>';
echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
exit;
}
else // Show the configuration form.
@ -1200,6 +1194,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
$PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
$PAGE->assign('api_enabled', $conf->get('api.enabled', true));
$PAGE->assign('api_secret', $conf->get('api.secret'));
$PAGE->assign('languages', Languages::getAvailableLanguages());
$PAGE->assign('language', $conf->get('translation.language'));
$PAGE->renderPage('configure');
exit;
}
@ -1214,8 +1210,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
exit;
}
if (!tokenOk($_POST['token'])) {
die('Wrong token.');
if (!$sessionManager->checkToken($_POST['token'])) {
die(t('Wrong token.'));
}
$alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag']));
@ -1225,9 +1221,10 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
}
$delete = empty($_POST['totag']);
$redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
$count = count($alteredLinks);
$alert = $delete
? sprintf(t('The tag was removed from %d links.'), count($alteredLinks))
: sprintf(t('The tag was renamed in %d links.'), count($alteredLinks));
? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
: sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
exit;
}
@ -1243,8 +1240,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
if (isset($_POST['save_edit']))
{
// Go away!
if (! tokenOk($_POST['token'])) {
die('Wrong token.');
if (! $sessionManager->checkToken($_POST['token'])) {
die(t('Wrong token.'));
}
// lf_id should only be present if the link exists.
@ -1343,8 +1340,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
// -------- User clicked the "Delete" button when editing a link: Delete link from database.
if ($targetPage == Router::$PAGE_DELETELINK)
{
if (! tokenOk($_GET['token'])) {
die('Wrong token.');
if (! $sessionManager->checkToken($_GET['token'])) {
die(t('Wrong token.'));
}
$ids = trim($_GET['lf_linkdate']);
@ -1428,22 +1425,16 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
// If this is an HTTP(S) link, we try go get the page to extract the title (otherwise we will to straight to the edit form.)
if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
// Short timeout to keep the application responsive
list($headers, $content) = get_http_response($url, 4);
if (strpos($headers[0], '200 OK') !== false) {
// Retrieve charset.
$charset = get_charset($headers, $content);
// Extract title.
$title = html_extract_title($content);
// Re-encode title in utf-8 if necessary.
// The callback will fill $charset and $title with data from the downloaded page.
get_http_response($url, 25, 4194304, get_curl_download_callback($charset, $title));
if (! empty($title) && strtolower($charset) != 'utf-8') {
$title = mb_convert_encoding($title, 'utf-8', $charset);
}
}
}
if ($url == '') {
$url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
$title = $conf->get('general.default_note_title', 'Note: ');
$title = $conf->get('general.default_note_title', t('Note: '));
}
$url = escape($url);
$title = escape($title);
@ -1550,14 +1541,17 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
// Import bookmarks from an uploaded file
if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
// The file is too big or some form field may be missing.
echo '<script>alert("The file you are trying to upload is probably'
.' bigger than what this webserver can accept ('
.get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').'
.' Please upload in smaller chunks.");document.location=\'?do='
.Router::$PAGE_IMPORT .'\';</script>';
$msg = sprintf(
t(
'The file you are trying to upload is probably bigger than what this webserver can accept'
.' (%s). Please upload in smaller chunks.'
),
get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
);
echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
exit;
}
if (! tokenOk($_POST['token'])) {
if (! $sessionManager->checkToken($_POST['token'])) {
die('Wrong token.');
}
$status = NetscapeBookmarkUtils::import(
@ -1624,7 +1618,7 @@ function($a, $b) { return $a['order'] - $b['order']; }
// Get a fresh token
if ($targetPage == Router::$GET_TOKEN) {
header('Content-Type:text/plain');
echo getToken($conf);
echo $sessionManager->generateToken($conf);
exit;
}
@ -1696,7 +1690,11 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
while ($i<$end && $i<count($keys))
{
$link = $linksToDisplay[$keys[$i]];
$link['description'] = format_description($link['description'], $conf->get('redirector.url'));
$link['description'] = format_description(
$link['description'],
$conf->get('redirector.url'),
$conf->get('redirector.encode_url')
);
$classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight';
$link['class'] = $link['private'] == 0 ? $classLi : 'private';
$link['timestamp'] = $link['created']->getTimestamp();
@ -1951,9 +1949,9 @@ function lazyThumbnail($conf, $url,$href=false)
* This function should NEVER be called if the file data/config.php exists.
*
* @param ConfigManager $conf Configuration Manager instance.
* @param SessionManager $sessionManager SessionManager instance
*/
function install($conf)
{
function install($conf, $sessionManager) {
// On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
@ -1962,12 +1960,20 @@ function install($conf)
// (Because on some hosts, session.save_path may not be set correctly,
// or we may not have write access to it.)
if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working'))
{ // Step 2: Check if data in session is correct.
echo '<pre>Sessions do not seem to work correctly on your server.<br>';
echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>';
echo 'It currently points to '.session_save_path().'<br>';
echo 'Check that the hostname used to access Shaarli contains a dot. On some browsers, accessing your server via a hostname like \'localhost\' or any custom hostname without a dot causes cookie storage to fail. We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>';
echo '<br><a href="?">Click to try again.</a></pre>';
{
// Step 2: Check if data in session is correct.
$msg = t(
'<pre>Sessions do not seem to work correctly on your server.<br>'.
'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
'and that you have write access to it.<br>'.
'It currently points to %s.<br>'.
'On some browsers, accessing your server via a hostname like \'localhost\' '.
'or any custom hostname without a dot causes cookie storage to fail. '.
'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
);
$msg = sprintf($msg, session_save_path());
echo $msg;
echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
die;
}
if (!isset($_SESSION['session_tested']))
@ -2000,6 +2006,7 @@ function install($conf)
} else {
$conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
}
$conf->set('translation.language', escape($_POST['language']));
$conf->set('updates.check_updates', !empty($_POST['updateCheck']));
$conf->set('api.enabled', !empty($_POST['enableApi']));
$conf->set(
@ -2027,10 +2034,11 @@ function install($conf)
exit;
}
$PAGE = new PageBuilder($conf);
$PAGE = new PageBuilder($conf, null, $sessionManager->generateToken());
list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
$PAGE->assign('continents', $continents);
$PAGE->assign('cities', $cities);
$PAGE->assign('languages', Languages::getAvailableLanguages());
$PAGE->renderPage('install');
exit;
}
@ -2303,7 +2311,7 @@ function resizeImage($filepath)
if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
// We use UTF-8 for proper international characters handling.
header('Content-Type: text/html; charset=utf-8');
renderPage($conf, $pluginManager, $linkDb, $history);
renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager);
} else {
$app->respond($response);
}

View file

@ -22,16 +22,15 @@ pages:
- Reverse proxy configuration: docker/reverse-proxy-configuration.md
- Docker resources: docker/resources.md
- Usage:
- Features: Features.md
- Bookmarklet: Bookmarklet.md
- Browsing and searching: Browsing-and-searching.md
- Firefox share: Firefox-share.md
- RSS feeds: RSS-feeds.md
- REST API: REST-API.md
- Community & Related software: Community-&-Related-software.md
- How To:
- Backup, restore, import and export: Backup,-restore,-import-and-export.md
- Various hacks: Various-hacks.md
- Troubleshooting: Troubleshooting.md
- Development:
- Development guidelines: Development-guidelines.md
- Continuous integration tools: Continuous-integration-tools.md
@ -43,9 +42,9 @@ pages:
- Versioning and Branches: Versioning-and-Branches.md
- Security: Security.md
- Static analysis: Static-analysis.md
- Translations: Translations.md
- Theming: Theming.md
- Unit tests: Unit-tests.md
- Unit tests inside Docker: Unit-tests-Docker.md
- About:
- FAQ: FAQ.md
- Community & Related software: Community-&-Related-software.md
- Troubleshooting: Troubleshooting.md

View file

@ -1,28 +0,0 @@
https://github.com/shaarli/Shaarli/issues/181 - Add Disqus or Isso comments box on a permalink page
* http://posativ.org/isso/
* install debian package https://packages.debian.org/sid/isso
* configure server http://posativ.org/isso/docs/configuration/server/
* configure client http://posativ.org/isso/docs/configuration/client/
* http://posativ.org/isso/docs/quickstart/ and add `<script data-isso="//comments.example.tld/" src="//comments.example.tld/js/embed.min.js"></script>` to includes.html template; then add `<section id="isso-thread"></section>` in the linklist template where you want the comments (in the linklist_plugins loop for example)
Problem: by default, Isso thread ID is guessed from the current url (only one thread per page).
if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config,
with data-isso-id being the permalink of an item.
`<section data-isso-id="aH7klxW" id="isso-thread"></section>`
`data-isso-id: Set a custom thread id, defaults to current URI.`
Problem: feature is currently broken https://github.com/posativ/isso/issues/27
Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread
when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment
count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done
by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server.
Showing all full isso threads in the linklist would destroy layout
-----------------------------------------------------------
http://www.git-attitude.fr/2014/11/04/git-rerere/ for the merge

View file

@ -26,11 +26,11 @@ function hook_addlink_toolbar_render_header($data)
array(
'type' => 'text',
'name' => 'post',
'placeholder' => 'URI',
'placeholder' => t('URI'),
),
array(
'type' => 'submit',
'value' => 'Add link',
'value' => t('Add link'),
'class' => 'bigbutton',
),
),
@ -40,3 +40,12 @@ function hook_addlink_toolbar_render_header($data)
return $data;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function addlink_toolbar_dummy_translation()
{
// meta
t('Adds the addlink input on the linklist page.');
}

View file

@ -1 +1,5 @@
<span><a href="https://web.archive.org/web/%s"><img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="View on archive.org" alt="archive.org" /></a></span>
<span>
<a href="https://web.archive.org/web/%s">
<img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
</a>
</span>

View file

@ -20,9 +20,18 @@ function hook_archiveorg_render_linklist($data)
if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
continue;
}
$archive = sprintf($archive_html, $value['url']);
$archive = sprintf($archive_html, $value['url'], t('View on archive.org'));
$value['link_plugin'][] = $archive;
}
return $data;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function archiveorg_dummy_translation()
{
// meta
t('For each link, add an Archive.org icon.');
}

View file

@ -14,6 +14,26 @@
* and check user status with _LOGGEDIN_.
*/
use Shaarli\Config\ConfigManager;
/**
* In the footer hook, there is a working example of a translation extension for Shaarli.
*
* The extension must be attached to a new translation domain (i.e. NOT 'shaarli').
* Use case: any custom theme or non official plugin can use the translation system.
*
* See the documentation for more information.
*/
const EXT_TRANSLATION_DOMAIN = 'demo';
/*
* This is not necessary, but it's easier if you don't want Poedit to mix up your translations.
*/
function demo_plugin_t($text, $nText = '', $nb = 1)
{
return t($text, $nText, $nb, EXT_TRANSLATION_DOMAIN);
}
/**
* Initialization function.
* It will be called when the plugin is loaded.
@ -27,6 +47,12 @@ function demo_plugin_init($conf)
{
$conf->get('toto', 'nope');
if (! $conf->exists('translation.extensions.demo')) {
// Custom translation with the domain 'demo'
$conf->set('translation.extensions.demo', 'plugins/demo_plugin/languages/');
$conf->write(true);
}
$errors[] = 'This a demo init error.';
return $errors;
}
@ -160,7 +186,7 @@ function hook_demo_plugin_render_includes($data)
function hook_demo_plugin_render_footer($data)
{
// footer text
$data['text'][] = 'Shaarli is now enhanced by the awesome demo_plugin.';
$data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
// Free elements at the end of the page.
$data['endofpage'][] = '<marquee id="demo_marquee">' .
@ -433,3 +459,12 @@ function hook_demo_plugin_render_feed($data)
}
return $data;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function demo_dummy_translation()
{
// meta
t('A demo plugin covering all use cases for template designers and plugin developers.');
}

Binary file not shown.

View file

@ -0,0 +1,21 @@
msgid ""
msgstr ""
"Project-Id-Version: Demo plugin\n"
"POT-Creation-Date: 2017-08-19 10:45+0200\n"
"PO-Revision-Date: 2017-08-19 11:28+0200\n"
"Last-Translator: \n"
"Language-Team: demo\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.2\n"
"X-Poedit-Basepath: ../../..\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Poedit-KeywordsList: ;demo_plugin_t:1,2;demo_plugin_t\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-SearchPath-0: .\n"
#: demo_plugin.php:173
msgid "Shaarli is now enhanced by the awesome demo_plugin."
msgstr "Shaarli est maintenant amélioré avec le fantastique demo_plugin."

View file

@ -4,10 +4,11 @@
* Plugin Isso.
*/
use Shaarli\Config\ConfigManager;
/**
* Display an error everywhere if the plugin is enabled without configuration.
*
* @param $data array List of links
* @param $conf ConfigManager instance
*
* @return mixed - linklist data with Isso plugin.
@ -16,8 +17,8 @@ function isso_init($conf)
{
$issoUrl = $conf->get('plugins.ISSO_SERVER');
if (empty($issoUrl)) {
$error = 'Isso plugin error: '.
'Please define the "ISSO_SERVER" setting in the plugin administration page.';
$error = t('Isso plugin error: '.
'Please define the "ISSO_SERVER" setting in the plugin administration page.');
return array($error);
}
}
@ -52,3 +53,13 @@ function hook_isso_render_linklist($data, $conf)
return $data;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function isso_dummy_translation()
{
// meta
t('Let visitor comment your shaares on permalinks with Isso.');
t('Isso server URL (without \'http://\')');
}

View file

@ -1,5 +1,5 @@
<div class="md_help">
Description will be rendered with
<a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation">
Markdown syntax</a>.
%s
<a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
%s</a>.
</div>

View file

@ -154,8 +154,13 @@ function hook_markdown_render_includes($data)
function hook_markdown_render_editlink($data)
{
// Load help HTML into a string
$data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
$txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
$translations = [
t('Description will be rendered with'),
t('Markdown syntax documentation'),
t('Markdown syntax'),
];
$data['edit_link_plugin'][] = vsprintf($txt, $translations);
// Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
if (! in_array(NO_MD_TAG, $data['tags'])) {
$data['tags'][NO_MD_TAG] = 0;
@ -325,3 +330,15 @@ function process_markdown($description, $escape = true, $allowedProtocols = [])
return $processedDescription;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function markdown_dummy_translation()
{
// meta
t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
If your shaared descriptions contained HTML tags before enabling the markdown plugin,
enabling it might break your page.
See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
}

View file

@ -18,8 +18,8 @@ function piwik_init($conf)
$piwikUrl = $conf->get('plugins.PIWIK_URL');
$piwikSiteid = $conf->get('plugins.PIWIK_SITEID');
if (empty($piwikUrl) || empty($piwikSiteid)) {
$error = 'Piwik plugin error: ' .
'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.';
$error = t('Piwik plugin error: ' .
'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.');
return array($error);
}
}
@ -60,3 +60,14 @@ function hook_piwik_render_footer($data, $conf)
return $data;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function piwik_dummy_translation()
{
// meta
t('A plugin that adds Piwik tracking code to Shaarli pages.');
t('Piwik URL');
t('Piwik site ID');
}

View file

@ -19,10 +19,10 @@ function hook_playvideos_render_header($data)
$playvideo = array(
'attr' => array(
'href' => '#',
'title' => 'Video player',
'title' => t('Video player'),
'id' => 'playvideos',
),
'html' => '► Play Videos'
'html' => '► '. t('Play Videos')
);
$data['buttons_toolbar'][] = $playvideo;
}
@ -46,3 +46,12 @@ function hook_playvideos_render_footer($data)
return $data;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function playvideos_dummy_translation()
{
// meta
t('Add a button in the toolbar allowing to watch all videos.');
}

View file

@ -10,6 +10,7 @@
*/
use pubsubhubbub\publisher\Publisher;
use Shaarli\Config\ConfigManager;
/**
* Plugin init function - set the hub to the default appspot one.
@ -65,7 +66,7 @@ function hook_pubsubhubbub_save_link($data, $conf)
$p = new Publisher($conf->get('plugins.PUBSUBHUB_URL'));
$p->publish_update($feeds, $httpPost);
} catch (Exception $e) {
error_log('Could not publish to PubSubHubbub: ' . $e->getMessage());
error_log(sprintf(t('Could not publish to PubSubHubbub: %s'), $e->getMessage()));
}
return $data;
@ -91,11 +92,20 @@ function nocurl_http_post($url, $postString) {
$context = stream_context_create($params);
$fp = @fopen($url, 'rb', false, $context);
if (!$fp) {
throw new Exception('Could not post to '. $url);
throw new Exception(sprintf(t('Could not post to %s'), $url));
}
$response = @stream_get_contents($fp);
if ($response === false) {
throw new Exception('Bad response from the hub '. $url);
throw new Exception(sprintf(t('Bad response from the hub %s'), $url));
}
return $response;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function pubsubhubbub_dummy_translation()
{
// meta
t('Enable PubSubHubbub feed publishing.');
}

View file

@ -59,3 +59,12 @@ function hook_qrcode_render_includes($data)
return $data;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function qrcode_dummy_translation()
{
// meta
t('For each link, add a QRCode icon.');
}

View file

@ -1 +1,5 @@
<span><a href="%s%s" target="_blank"><img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="Save to wallabag" alt="wallabag" /></a></span>
<span>
<a href="%s%s" target="_blank">
<img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="%s" alt="wallabag" />
</a>
</span>

View file

@ -5,6 +5,7 @@
*/
require_once 'WallabagInstance.php';
use Shaarli\Config\ConfigManager;
/**
* Init function, return an error if the server is not set.
@ -17,8 +18,8 @@ function wallabag_init($conf)
{
$wallabagUrl = $conf->get('plugins.WALLABAG_URL');
if (empty($wallabagUrl)) {
$error = 'Wallabag plugin error: '.
'Please define the "WALLABAG_URL" setting in the plugin administration page.';
$error = t('Wallabag plugin error: '.
'Please define the "WALLABAG_URL" setting in the plugin administration page.');
return array($error);
}
}
@ -43,12 +44,14 @@ function hook_wallabag_render_linklist($data, $conf)
$wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
$linkTitle = t('Save to wallabag');
foreach ($data['links'] as &$value) {
$wallabag = sprintf(
$wallabagHtml,
$wallabagInstance->getWallabagUrl(),
urlencode($value['url']),
PluginManager::$PLUGINS_PATH
PluginManager::$PLUGINS_PATH,
$linkTitle
);
$value['link_plugin'][] = $wallabag;
}
@ -56,3 +59,14 @@ function hook_wallabag_render_linklist($data, $conf)
return $data;
}
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function wallabag_dummy_translation()
{
// meta
t('For each link, add a QRCode icon.');
t('Wallabag API URL');
t('Wallabag API version (1 or 2)');
}

View file

@ -1 +1 @@
<?php /* 0.9.3 */ ?>
<?php /* 0.9.4 */ ?>

View file

@ -186,4 +186,36 @@ public function testStandardHttpsPort()
)
);
}
/**
* Misconfigured server (see #1022): Proxy HTTP but 443
*/
public function testHttpWithPort433()
{
$this->assertEquals(
'https://host.tld',
server_url(
array(
'HTTPS' => 'Off',
'SERVER_NAME' => 'host.tld',
'SERVER_PORT' => '80',
'HTTP_X_FORWARDED_PROTO' => 'http',
'HTTP_X_FORWARDED_PORT' => '443'
)
)
);
$this->assertEquals(
'https://host.tld',
server_url(
array(
'HTTPS' => 'Off',
'SERVER_NAME' => 'host.tld',
'SERVER_PORT' => '80',
'HTTP_X_FORWARDED_PROTO' => 'https, http',
'HTTP_X_FORWARDED_PORT' => '443, 80'
)
)
);
}
}

View file

@ -1,41 +1,203 @@
<?php
require_once 'application/Languages.php';
namespace Shaarli;
use Shaarli\Config\ConfigManager;
/**
* Class LanguagesTest.
*/
class LanguagesTest extends PHPUnit_Framework_TestCase
class LanguagesTest extends \PHPUnit_Framework_TestCase
{
/**
* @var string Config file path (without extension).
*/
protected static $configFile = 'tests/utils/config/configJson';
/**
* @var ConfigManager
*/
protected $conf;
/**
*
*/
public function setUp()
{
$this->conf = new ConfigManager(self::$configFile);
}
/**
* Test t() with a simple non identified value.
*/
public function testTranslateSingleNotID()
public function testTranslateSingleNotIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'abcdé 564 fgK';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a non identified plural form.
* Test t() with a simple identified value in gettext mode.
*/
public function testTranslatePluralNotID()
public function testTranslateSingleIDGettext()
{
$text = '%s sandwich';
$nText = '%s sandwiches';
$this->assertEquals('0 sandwich', t($text, $nText));
$this->assertEquals('1 sandwich', t($text, $nText, 1));
$this->assertEquals('2 sandwiches', t($text, $nText, 2));
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'permalink';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a non identified invalid plural form.
* Test t() with a non identified plural form in gettext mode.
*/
public function testTranslatePluralNotIDInvalid()
public function testTranslatePluralNotIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'sandwich';
$nText = 'sandwiches';
$this->assertEquals('sandwiches', t($text, $nText, 0));
$this->assertEquals('sandwich', t($text, $nText, 1));
$this->assertEquals('sandwiches', t($text, $nText, 2));
}
/**
* Test t() with an identified plural form in gettext mode.
*/
public function testTranslatePluralIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'shaare';
$nText = 'shaares';
// In english, zero is followed by plural form
$this->assertEquals('shaares', t($text, $nText, 0));
$this->assertEquals('shaare', t($text, $nText, 1));
$this->assertEquals('shaares', t($text, $nText, 2));
}
/**
* Test t() with a simple non identified value.
*/
public function testTranslateSingleNotIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'abcdé 564 fgK';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a simple identified value in PHP mode.
*/
public function testTranslateSingleIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'permalink';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a non identified plural form in PHP mode.
*/
public function testTranslatePluralNotIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'sandwich';
$nText = 'sandwiches';
$this->assertEquals('sandwiches', t($text, $nText, 0));
$this->assertEquals('sandwich', t($text, $nText, 1));
$this->assertEquals('sandwiches', t($text, $nText, 2));
}
/**
* Test t() with an identified plural form in PHP mode.
*/
public function testTranslatePluralIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'shaare';
$nText = 'shaares';
// In english, zero is followed by plural form
$this->assertEquals('shaares', t($text, $nText, 0));
$this->assertEquals('shaare', t($text, $nText, 1));
$this->assertEquals('shaares', t($text, $nText, 2));
}
/**
* Test t() with an invalid language set in the configuration in gettext mode.
*/
public function testTranslateWithInvalidConfLanguageGettext()
{
$this->conf->set('translation.mode', 'gettext');
$this->conf->set('translation.language', 'nope');
new Languages('fr', $this->conf);
$text = 'grumble';
$this->assertEquals($text, t($text));
}
/**
* Test t() with an invalid language set in the configuration in PHP mode.
*/
public function testTranslateWithInvalidConfLanguagePhp()
{
$this->conf->set('translation.mode', 'php');
$this->conf->set('translation.language', 'nope');
new Languages('fr', $this->conf);
$text = 'grumble';
$this->assertEquals($text, t($text));
}
/**
* Test t() with an invalid language set with auto language in gettext mode.
*/
public function testTranslateWithInvalidAutoLanguageGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('nope', $this->conf);
$text = 'grumble';
$this->assertEquals($text, t($text));
}
/**
* Test t() with an invalid language set with auto language in PHP mode.
*/
public function testTranslateWithInvalidAutoLanguagePhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('nope', $this->conf);
$text = 'grumble';
$this->assertEquals($text, t($text));
}
/**
* Test t() with an extension language file in gettext mode
*/
public function testTranslationExtensionGettext()
{
$this->conf->set('translation.mode', 'gettext');
$this->conf->set('translation.extensions.test', 'tests/utils/languages/');
new Languages('en', $this->conf);
$txt = 'car'; // ignore me poedit
$this->assertEquals('car', t($txt, $txt, 1, 'test'));
$this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
}
/**
* Test t() with an extension language file in PHP mode
*/
public function testTranslationExtensionPhp()
{
$this->conf->set('translation.mode', 'php');
$this->conf->set('translation.extensions.test', 'tests/utils/languages/');
new Languages('en', $this->conf);
$txt = 'car'; // ignore me poedit
$this->assertEquals('car', t($txt, $txt, 1, 'test'));
$this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
}
}

View file

@ -7,6 +7,10 @@
*/
class LinkFilterTest extends PHPUnit_Framework_TestCase
{
/**
* @var string Test datastore path.
*/
protected static $testDatastore = 'sandbox/datastore.php';
/**
* @var LinkFilter instance.
*/
@ -17,13 +21,20 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
*/
protected static $refDB;
/**
* @var LinkDB instance
*/
protected static $linkDB;
/**
* Instanciate linkFilter with ReferenceLinkDB data.
*/
public static function setUpBeforeClass()
{
self::$refDB = new ReferenceLinkDB();
self::$linkFilter = new LinkFilter(self::$refDB->getLinks());
self::$refDB->write(self::$testDatastore);
self::$linkDB = new LinkDB(self::$testDatastore, true, false);
self::$linkFilter = new LinkFilter(self::$linkDB);
}
/**

View file

@ -28,28 +28,14 @@ public function testHtmlExtractNonExistentTitle()
$this->assertFalse(html_extract_title($html));
}
/**
* Test get_charset() with all priorities.
*/
public function testGetCharset()
{
$headers = array('Content-Type' => 'text/html; charset=Headers');
$html = '<html><meta>stuff</meta><meta charset="Html"/></html>';
$default = 'default';
$this->assertEquals('headers', get_charset($headers, $html, $default));
$this->assertEquals('html', get_charset(array(), $html, $default));
$this->assertEquals($default, get_charset(array(), '', $default));
$this->assertEquals('utf-8', get_charset(array(), ''));
}
/**
* Test headers_extract_charset() when the charset is found.
*/
public function testHeadersExtractExistentCharset()
{
$charset = 'x-MacCroatian';
$headers = array('Content-Type' => 'text/html; charset='. $charset);
$this->assertEquals(strtolower($charset), headers_extract_charset($headers));
$headers = 'text/html; charset='. $charset;
$this->assertEquals(strtolower($charset), header_extract_charset($headers));
}
/**
@ -57,11 +43,11 @@ public function testHeadersExtractExistentCharset()
*/
public function testHeadersExtractNonExistentCharset()
{
$headers = array();
$this->assertFalse(headers_extract_charset($headers));
$headers = '';
$this->assertFalse(header_extract_charset($headers));
$headers = array('Content-Type' => 'text/html');
$this->assertFalse(headers_extract_charset($headers));
$headers = 'text/html';
$this->assertFalse(header_extract_charset($headers));
}
/**
@ -85,6 +71,131 @@ public function testHtmlExtractNonExistentCharset()
$this->assertFalse(html_extract_charset($html));
}
/**
* Test the download callback with valid value
*/
public function testCurlDownloadCallbackOk()
{
$callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_ok');
$data = [
'HTTP/1.1 200 OK',
'Server: GitHub.com',
'Date: Sat, 28 Oct 2017 12:01:33 GMT',
'Content-Type: text/html; charset=utf-8',
'Status: 200 OK',
'end' => 'th=device-width"><title>Refactoring · GitHub</title><link rel="search" type="application/opensea',
'<title>ignored</title>',
];
foreach ($data as $key => $line) {
$ignore = null;
$expected = $key !== 'end' ? strlen($line) : false;
$this->assertEquals($expected, $callback($ignore, $line));
if ($expected === false) {
break;
}
}
$this->assertEquals('utf-8', $charset);
$this->assertEquals('Refactoring · GitHub', $title);
}
/**
* Test the download callback with valid values and no charset
*/
public function testCurlDownloadCallbackOkNoCharset()
{
$callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_no_charset');
$data = [
'HTTP/1.1 200 OK',
'end' => 'th=device-width"><title>Refactoring · GitHub</title><link rel="search" type="application/opensea',
'<title>ignored</title>',
];
foreach ($data as $key => $line) {
$ignore = null;
$this->assertEquals(strlen($line), $callback($ignore, $line));
}
$this->assertEmpty($charset);
$this->assertEquals('Refactoring · GitHub', $title);
}
/**
* Test the download callback with valid values and no charset
*/
public function testCurlDownloadCallbackOkHtmlCharset()
{
$callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_no_charset');
$data = [
'HTTP/1.1 200 OK',
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
'end' => 'th=device-width"><title>Refactoring · GitHub</title><link rel="search" type="application/opensea',
'<title>ignored</title>',
];
foreach ($data as $key => $line) {
$ignore = null;
$expected = $key !== 'end' ? strlen($line) : false;
$this->assertEquals($expected, $callback($ignore, $line));
if ($expected === false) {
break;
}
}
$this->assertEquals('utf-8', $charset);
$this->assertEquals('Refactoring · GitHub', $title);
}
/**
* Test the download callback with valid values and no title
*/
public function testCurlDownloadCallbackOkNoTitle()
{
$callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_ok');
$data = [
'HTTP/1.1 200 OK',
'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea',
'ignored',
];
foreach ($data as $key => $line) {
$ignore = null;
$this->assertEquals(strlen($line), $callback($ignore, $line));
}
$this->assertEquals('utf-8', $charset);
$this->assertEmpty($title);
}
/**
* Test the download callback with an invalid content type.
*/
public function testCurlDownloadCallbackInvalidContentType()
{
$callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_ct_ko');
$ignore = null;
$this->assertFalse($callback($ignore, ''));
$this->assertEmpty($charset);
$this->assertEmpty($title);
}
/**
* Test the download callback with an invalid response code.
*/
public function testCurlDownloadCallbackInvalidResponseCode()
{
$callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_rc_ko');
$ignore = null;
$this->assertFalse($callback($ignore, ''));
$this->assertEmpty($charset);
$this->assertEmpty($title);
}
/**
* Test the download callback with an invalid content type and response code.
*/
public function testCurlDownloadCallbackInvalidContentTypeAndResponseCode()
{
$callback = get_curl_download_callback($charset, $title, 'ut_curl_getinfo_rs_ct_ko');
$ignore = null;
$this->assertFalse($callback($ignore, ''));
$this->assertEmpty($charset);
$this->assertEmpty($title);
}
/**
* Test count_private.
*/
@ -130,6 +241,21 @@ public function testText2clickableWithRedirector()
$this->assertEquals($expectedText, $processedText);
}
/**
* Test text2clickable a redirector set and without URL encode.
*/
public function testText2clickableWithRedirectorDontEncode()
{
$text = 'stuff http://hello.there/?is=someone&or=something#here otherstuff';
$redirector = 'http://redirector.to';
$expectedText = 'stuff <a href="'.
$redirector .
'http://hello.there/?is=someone&or=something#here' .
'">http://hello.there/?is=someone&or=something#here</a> otherstuff';
$processedText = text2clickable($text, $redirector, false);
$this->assertEquals($expectedText, $processedText);
}
/**
* Test testSpace2nbsp.
*/
@ -192,3 +318,96 @@ private function getHashtagLink($hashtag, $index = '')
return str_replace('$1', $hashtag, $hashtagLink);
}
}
// old style mock: PHPUnit doesn't allow function mock
/**
* Returns code 200 or html content type.
*
* @param resource $ch cURL resource
* @param int $type cURL info type
*
* @return int|string 200 or 'text/html'
*/
function ut_curl_getinfo_ok($ch, $type)
{
switch ($type) {
case CURLINFO_RESPONSE_CODE:
return 200;
case CURLINFO_CONTENT_TYPE:
return 'text/html; charset=utf-8';
}
}
/**
* Returns code 200 or html content type without charset.
*
* @param resource $ch cURL resource
* @param int $type cURL info type
*
* @return int|string 200 or 'text/html'
*/
function ut_curl_getinfo_no_charset($ch, $type)
{
switch ($type) {
case CURLINFO_RESPONSE_CODE:
return 200;
case CURLINFO_CONTENT_TYPE:
return 'text/html';
}
}
/**
* Invalid response code.
*
* @param resource $ch cURL resource
* @param int $type cURL info type
*
* @return int|string 404 or 'text/html'
*/
function ut_curl_getinfo_rc_ko($ch, $type)
{
switch ($type) {
case CURLINFO_RESPONSE_CODE:
return 404;
case CURLINFO_CONTENT_TYPE:
return 'text/html; charset=utf-8';
}
}
/**
* Invalid content type.
*
* @param resource $ch cURL resource
* @param int $type cURL info type
*
* @return int|string 200 or 'text/plain'
*/
function ut_curl_getinfo_ct_ko($ch, $type)
{
switch ($type) {
case CURLINFO_RESPONSE_CODE:
return 200;
case CURLINFO_CONTENT_TYPE:
return 'text/plain';
}
}
/**
* Invalid response code and content type.
*
* @param resource $ch cURL resource
* @param int $type cURL info type
*
* @return int|string 404 or 'text/plain'
*/
function ut_curl_getinfo_rs_ct_ko($ch, $type)
{
switch ($type) {
case CURLINFO_RESPONSE_CODE:
return 404;
case CURLINFO_CONTENT_TYPE:
return 'text/plain';
}
}

View file

@ -132,8 +132,8 @@ public function testImportNoDoctype()
public function testImportInternetExplorerEncoding()
{
$files = file2array('internet_explorer_encoding.htm');
$this->assertEquals(
'File internet_explorer_encoding.htm (356 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
.' 1 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
);
@ -161,8 +161,8 @@ public function testImportInternetExplorerEncoding()
public function testImportNested()
{
$files = file2array('netscape_nested.htm');
$this->assertEquals(
'File netscape_nested.htm (1337 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
.' 8 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
);
@ -283,8 +283,8 @@ public function testImportNested()
public function testImportDefaultPrivacyNoPost()
{
$files = file2array('netscape_basic.htm');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
);
@ -328,8 +328,8 @@ public function testImportKeepPrivacy()
{
$post = array('privacy' => 'default');
$files = file2array('netscape_basic.htm');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -372,8 +372,8 @@ public function testImportAsPublic()
{
$post = array('privacy' => 'public');
$files = file2array('netscape_basic.htm');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -396,8 +396,8 @@ public function testImportAsPrivate()
{
$post = array('privacy' => 'private');
$files = file2array('netscape_basic.htm');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -422,8 +422,8 @@ public function testOverwriteAsPublic()
// import links as private
$post = array('privacy' => 'private');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -442,8 +442,8 @@ public function testOverwriteAsPublic()
'privacy' => 'public',
'overwrite' => 'true'
);
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 2 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -468,8 +468,8 @@ public function testOverwriteAsPrivate()
// import links as public
$post = array('privacy' => 'public');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -489,8 +489,8 @@ public function testOverwriteAsPrivate()
'privacy' => 'private',
'overwrite' => 'true'
);
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 2 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -513,8 +513,8 @@ public function testSkipOverwrite()
{
$post = array('privacy' => 'public');
$files = file2array('netscape_basic.htm');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -523,8 +523,8 @@ public function testSkipOverwrite()
// re-import as private, DO NOT enable overwriting
$post = array('privacy' => 'private');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 0 links imported, 0 links overwritten, 2 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -542,8 +542,8 @@ public function testSetDefaultTags()
'default_tags' => 'tag1,tag2 tag3'
);
$files = file2array('netscape_basic.htm');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -569,8 +569,8 @@ public function testSanitizeDefaultTags()
'default_tags' => 'tag1&,tag2 "tag3"'
);
$files = file2array('netscape_basic.htm');
$this->assertEquals(
'File netscape_basic.htm (482 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
@ -594,8 +594,8 @@ public function testSanitizeDefaultTags()
public function testImportSameDate()
{
$files = file2array('same_date.htm');
$this->assertEquals(
'File same_date.htm (453 bytes) was successfully processed:'
$this->assertStringMatchesFormat(
'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
.' 3 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history)
);
@ -622,24 +622,19 @@ public function testImportCreateUpdateHistory()
'overwrite' => 'true',
];
$files = file2array('netscape_basic.htm');
$nbLinks = 2;
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
$history = $this->history->getHistory();
$this->assertEquals($nbLinks, count($history));
foreach ($history as $value) {
$this->assertEquals(History::CREATED, $value['event']);
$this->assertTrue(new DateTime('-5 seconds') < $value['datetime']);
$this->assertTrue(is_int($value['id']));
}
$this->assertEquals(1, count($history));
$this->assertEquals(History::IMPORT, $history[0]['event']);
$this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
// re-import as private, enable overwriting
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
$history = $this->history->getHistory();
$this->assertEquals($nbLinks * 2, count($history));
for ($i = 0 ; $i < $nbLinks ; $i++) {
$this->assertEquals(History::UPDATED, $history[$i]['event']);
$this->assertTrue(new DateTime('-5 seconds') < $history[$i]['datetime']);
$this->assertTrue(is_int($history[$i]['id']));
}
$this->assertEquals(2, count($history));
$this->assertEquals(History::IMPORT, $history[0]['event']);
$this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
$this->assertEquals(History::IMPORT, $history[1]['event']);
$this->assertTrue(new DateTime('-5 seconds') < $history[1]['datetime']);
}
}

View file

@ -0,0 +1,149 @@
<?php
require_once 'tests/utils/FakeConfigManager.php';
// Initialize reference data _before_ PHPUnit starts a session
require_once 'tests/utils/ReferenceSessionIdHashes.php';
ReferenceSessionIdHashes::genAllHashes();
use \Shaarli\SessionManager;
use \PHPUnit\Framework\TestCase;
/**
* Test coverage for SessionManager
*/
class SessionManagerTest extends TestCase
{
// Session ID hashes
protected static $sidHashes = null;
// Fake ConfigManager
protected static $conf = null;
/**
* Assign reference data
*/
public static function setUpBeforeClass()
{
self::$sidHashes = ReferenceSessionIdHashes::getHashes();
self::$conf = new FakeConfigManager();
}
/**
* Generate a session token
*/
public function testGenerateToken()
{
$session = [];
$sessionManager = new SessionManager($session, self::$conf);
$token = $sessionManager->generateToken();
$this->assertEquals(1, $session['tokens'][$token]);
$this->assertEquals(40, strlen($token));
}
/**
* Check a session token
*/
public function testCheckToken()
{
$token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b';
$session = [
'tokens' => [
$token => 1,
],
];
$sessionManager = new SessionManager($session, self::$conf);
// check and destroy the token
$this->assertTrue($sessionManager->checkToken($token));
$this->assertFalse(isset($session['tokens'][$token]));
// ensure the token has been destroyed
$this->assertFalse($sessionManager->checkToken($token));
}
/**
* Generate and check a session token
*/
public function testGenerateAndCheckToken()
{
$session = [];
$sessionManager = new SessionManager($session, self::$conf);
$token = $sessionManager->generateToken();
// ensure a token has been generated
$this->assertEquals(1, $session['tokens'][$token]);
$this->assertEquals(40, strlen($token));
// check and destroy the token
$this->assertTrue($sessionManager->checkToken($token));
$this->assertFalse(isset($session['tokens'][$token]));
// ensure the token has been destroyed
$this->assertFalse($sessionManager->checkToken($token));
}
/**
* Check an invalid session token
*/
public function testCheckInvalidToken()
{
$session = [];
$sessionManager = new SessionManager($session, self::$conf);
$this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'));
}
/**
* Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES!
*
* This tests extensively covers all hash algorithms / bit representations
*/
public function testIsAnyHashSessionIdValid()
{
foreach (self::$sidHashes as $algo => $bpcs) {
foreach ($bpcs as $bpc => $hash) {
$this->assertTrue(SessionManager::checkId($hash));
}
}
}
/**
* Test checkId with a valid ID - SHA-1 hashes
*/
public function testIsSha1SessionIdValid()
{
$this->assertTrue(SessionManager::checkId(sha1('shaarli')));
}
/**
* Test checkId with a valid ID - SHA-256 hashes
*/
public function testIsSha256SessionIdValid()
{
$this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli')));
}
/**
* Test checkId with a valid ID - SHA-512 hashes
*/
public function testIsSha512SessionIdValid()
{
$this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli')));
}
/**
* Test checkId with invalid IDs.
*/
public function testIsSessionIdInvalid()
{
$this->assertFalse(SessionManager::checkId(''));
$this->assertFalse(SessionManager::checkId([]));
$this->assertFalse(
SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
);
}
}

View file

@ -5,10 +5,6 @@
require_once 'application/Utils.php';
require_once 'application/Languages.php';
require_once 'tests/utils/ReferenceSessionIdHashes.php';
// Initialize reference data before PHPUnit starts a session
ReferenceSessionIdHashes::genAllHashes();
/**
@ -16,9 +12,6 @@
*/
class UtilsTest extends PHPUnit_Framework_TestCase
{
// Session ID hashes
protected static $sidHashes = null;
// Log file
protected static $testLogFile = 'tests.log';
@ -30,13 +23,11 @@ class UtilsTest extends PHPUnit_Framework_TestCase
*/
protected static $defaultTimeZone;
/**
* Assign reference data
*/
public static function setUpBeforeClass()
{
self::$sidHashes = ReferenceSessionIdHashes::getHashes();
self::$defaultTimeZone = date_default_timezone_get();
// Timezone without DST for test consistency
date_default_timezone_set('Africa/Nairobi');
@ -221,55 +212,6 @@ public function testGenerateLocationOut() {
$this->assertEquals('?', generateLocation($ref, 'localhost'));
}
/**
* Test is_session_id_valid with a valid ID - TEST ALL THE HASHES!
*
* This tests extensively covers all hash algorithms / bit representations
*/
public function testIsAnyHashSessionIdValid()
{
foreach (self::$sidHashes as $algo => $bpcs) {
foreach ($bpcs as $bpc => $hash) {
$this->assertTrue(is_session_id_valid($hash));
}
}
}
/**
* Test is_session_id_valid with a valid ID - SHA-1 hashes
*/
public function testIsSha1SessionIdValid()
{
$this->assertTrue(is_session_id_valid(sha1('shaarli')));
}
/**
* Test is_session_id_valid with a valid ID - SHA-256 hashes
*/
public function testIsSha256SessionIdValid()
{
$this->assertTrue(is_session_id_valid(hash('sha256', 'shaarli')));
}
/**
* Test is_session_id_valid with a valid ID - SHA-512 hashes
*/
public function testIsSha512SessionIdValid()
{
$this->assertTrue(is_session_id_valid(hash('sha512', 'shaarli')));
}
/**
* Test is_session_id_valid with invalid IDs.
*/
public function testIsSessionIdInvalid()
{
$this->assertFalse(is_session_id_valid(''));
$this->assertFalse(is_session_id_valid(array()));
$this->assertFalse(
is_session_id_valid('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
);
}
/**
* Test generateSecretApi.
@ -384,18 +326,18 @@ public function testReturnBytes()
*/
public function testHumanBytes()
{
$this->assertEquals('2kiB', human_bytes(2 * 1024));
$this->assertEquals('2kiB', human_bytes(strval(2 * 1024)));
$this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2))));
$this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2)))));
$this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3))));
$this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3)))));
$this->assertEquals('374B', human_bytes(374));
$this->assertEquals('374B', human_bytes('374'));
$this->assertEquals('232kiB', human_bytes(237481));
$this->assertEquals('Unlimited', human_bytes('0'));
$this->assertEquals('Unlimited', human_bytes(0));
$this->assertEquals('Setting not set', human_bytes(''));
$this->assertEquals('2'. t('kiB'), human_bytes(2 * 1024));
$this->assertEquals('2'. t('kiB'), human_bytes(strval(2 * 1024)));
$this->assertEquals('2'. t('MiB'), human_bytes(2 * (pow(1024, 2))));
$this->assertEquals('2'. t('MiB'), human_bytes(strval(2 * (pow(1024, 2)))));
$this->assertEquals('2'. t('GiB'), human_bytes(2 * (pow(1024, 3))));
$this->assertEquals('2'. t('GiB'), human_bytes(strval(2 * (pow(1024, 3)))));
$this->assertEquals('374'. t('B'), human_bytes(374));
$this->assertEquals('374'. t('B'), human_bytes('374'));
$this->assertEquals('232'. t('kiB'), human_bytes(237481));
$this->assertEquals(t('Unlimited'), human_bytes('0'));
$this->assertEquals(t('Unlimited'), human_bytes(0));
$this->assertEquals(t('Setting not set'), human_bytes(''));
}
/**
@ -403,9 +345,9 @@ public function testHumanBytes()
*/
public function testGetMaxUploadSize()
{
$this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k'));
$this->assertEquals('1MiB', get_max_upload_size('1m', '2m'));
$this->assertEquals('100B', get_max_upload_size(100, 100));
$this->assertEquals('1'. t('MiB'), get_max_upload_size(2097152, '1024k'));
$this->assertEquals('1'. t('MiB'), get_max_upload_size('1m', '2m'));
$this->assertEquals('100'. t('B'), get_max_upload_size(100, 100));
}
/**

6
tests/bootstrap.php Normal file
View file

@ -0,0 +1,6 @@
<?php
require_once 'vendor/autoload.php';
$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
new \Shaarli\Languages('en', $conf);

View file

@ -1,7 +1,6 @@
<?php
if (! empty('UT_LOCALE')) {
require_once 'tests/bootstrap.php';
if (! empty(getenv('UT_LOCALE'))) {
setlocale(LC_ALL, getenv('UT_LOCALE'));
}
require_once 'vendor/autoload.php';

View file

@ -0,0 +1,175 @@
<?php
namespace Shaarli;
use Shaarli\Config\ConfigManager;
/**
* Class LanguagesFrTest
*
* Test the translation system in PHP and gettext mode with French language.
*
* @package Shaarli
*/
class LanguagesFrTest extends \PHPUnit_Framework_TestCase
{
/**
* @var string Config file path (without extension).
*/
protected static $configFile = 'tests/utils/config/configJson';
/**
* @var ConfigManager
*/
protected $conf;
/**
* Init: force French
*/
public function setUp()
{
$this->conf = new ConfigManager(self::$configFile);
$this->conf->set('translation.language', 'fr');
}
/**
* Reset the locale since gettext seems to mess with it, making it too long
*/
public static function tearDownAfterClass()
{
if (! empty(getenv('UT_LOCALE'))) {
setlocale(LC_ALL, getenv('UT_LOCALE'));
}
}
/**
* Test t() with a simple non identified value.
*/
public function testTranslateSingleNotIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'abcdé 564 fgK';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a simple identified value in gettext mode.
*/
public function testTranslateSingleIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'permalink';
$this->assertEquals('permalien', t($text));
}
/**
* Test t() with a non identified plural form in gettext mode.
*/
public function testTranslatePluralNotIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'sandwich';
$nText = 'sandwiches';
// Not ID, so English fallback, and in english, plural 0
$this->assertEquals('sandwiches', t($text, $nText, 0));
$this->assertEquals('sandwich', t($text, $nText, 1));
$this->assertEquals('sandwiches', t($text, $nText, 2));
}
/**
* Test t() with an identified plural form in gettext mode.
*/
public function testTranslatePluralIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'shaare';
$nText = 'shaares';
$this->assertEquals('shaare', t($text, $nText, 0));
$this->assertEquals('shaare', t($text, $nText, 1));
$this->assertEquals('shaares', t($text, $nText, 2));
}
/**
* Test t() with a simple non identified value.
*/
public function testTranslateSingleNotIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'abcdé 564 fgK';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a simple identified value in PHP mode.
*/
public function testTranslateSingleIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'permalink';
$this->assertEquals('permalien', t($text));
}
/**
* Test t() with a non identified plural form in PHP mode.
*/
public function testTranslatePluralNotIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'sandwich';
$nText = 'sandwiches';
// Not ID, so English fallback, and in english, plural 0
$this->assertEquals('sandwiches', t($text, $nText, 0));
$this->assertEquals('sandwich', t($text, $nText, 1));
$this->assertEquals('sandwiches', t($text, $nText, 2));
}
/**
* Test t() with an identified plural form in PHP mode.
*/
public function testTranslatePluralIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'shaare';
$nText = 'shaares';
// In english, zero is followed by plural form
$this->assertEquals('shaare', t($text, $nText, 0));
$this->assertEquals('shaare', t($text, $nText, 1));
$this->assertEquals('shaares', t($text, $nText, 2));
}
/**
* Test t() with an extension language file in gettext mode
*/
public function testTranslationExtensionGettext()
{
$this->conf->set('translation.mode', 'gettext');
$this->conf->set('translation.extensions.test', 'tests/utils/languages/');
new Languages('en', $this->conf);
$txt = 'car'; // ignore me poedit
$this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
$this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
}
/**
* Test t() with an extension language file in PHP mode
*/
public function testTranslationExtensionPhp()
{
$this->conf->set('translation.mode', 'php');
$this->conf->set('translation.extensions.test', 'tests/utils/languages/');
new Languages('en', $this->conf);
$txt = 'car'; // ignore me poedit
$this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
$this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* Fake ConfigManager
*/
class FakeConfigManager
{
public static function get($key)
{
return $key;
}
}

View file

@ -141,12 +141,34 @@ protected function addLink($id, $title, $url, $description, $private, $date, $ta
*/
public function write($filename)
{
$this->reorder();
file_put_contents(
$filename,
'<?php /* '.base64_encode(gzdeflate(serialize($this->_links))).' */ ?>'
);
}
/**
* Reorder links by creation date (newest first).
*
* Also update the urls and ids mapping arrays.
*
* @param string $order ASC|DESC
*/
public function reorder($order = 'DESC')
{
// backward compatibility: ignore reorder if the the `created` field doesn't exist
if (! isset(array_values($this->_links)[0]['created'])) {
return;
}
$order = $order === 'ASC' ? -1 : 1;
// Reorder array by dates.
usort($this->_links, function($a, $b) use ($order) {
return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
});
}
/**
* Returns the number of links in the reference data
*/
@ -187,6 +209,7 @@ public function countUntaggedLinks()
public function getLinks()
{
$this->reorder();
return $this->_links;
}

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