Merge pull request #687 from ArthurHoaro/web-thumb

Use web-thumbnailer to retrieve thumbnails
This commit is contained in:
ArthurHoaro 2018-07-28 09:41:29 +02:00 committed by GitHub
commit ad5f47adba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1495 additions and 769 deletions

View file

@ -1,6 +1,7 @@
<?php
use Shaarli\Config\ConfigManager;
use Shaarli\Thumbnailer;
/**
* This class is in charge of building the final page.
@ -21,11 +22,21 @@ class PageBuilder
*/
protected $conf;
/**
* @var array $_SESSION
*/
protected $session;
/**
* @var LinkDB $linkDB instance.
*/
protected $linkDB;
/**
* @var null|string XSRF token
*/
protected $token;
/** @var bool $isLoggedIn Whether the user is logged in **/
protected $isLoggedIn = false;
@ -33,14 +44,17 @@ class PageBuilder
* PageBuilder constructor.
* $tpl is initialized at false for lazy loading.
*
* @param ConfigManager $conf Configuration Manager instance (reference).
* @param LinkDB $linkDB instance.
* @param string $token Session token
* @param ConfigManager $conf Configuration Manager instance (reference).
* @param array $session $_SESSION array
* @param LinkDB $linkDB instance.
* @param string $token Session token
* @param bool $isLoggedIn
*/
public function __construct(&$conf, $linkDB = null, $token = null, $isLoggedIn = false)
public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
{
$this->tpl = false;
$this->conf = $conf;
$this->session = $session;
$this->linkDB = $linkDB;
$this->token = $token;
$this->isLoggedIn = $isLoggedIn;
@ -105,6 +119,19 @@ private function initialize()
if ($this->linkDB !== null) {
$this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
}
$this->tpl->assign(
'thumbnails_enabled',
$this->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
);
$this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
$this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
if (! empty($_SESSION['warnings'])) {
$this->tpl->assign('global_warnings', $_SESSION['warnings']);
unset($_SESSION['warnings']);
}
// To be removed with a proper theme configuration.
$this->tpl->assign('conf', $this->conf);
}

View file

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

127
application/Thumbnailer.php Normal file
View file

@ -0,0 +1,127 @@
<?php
namespace Shaarli;
use Shaarli\Config\ConfigManager;
use WebThumbnailer\Exception\WebThumbnailerException;
use WebThumbnailer\WebThumbnailer;
use WebThumbnailer\Application\ConfigManager as WTConfigManager;
/**
* Class Thumbnailer
*
* Utility class used to retrieve thumbnails using web-thumbnailer dependency.
*/
class Thumbnailer
{
const COMMON_MEDIA_DOMAINS = [
'imgur.com',
'flickr.com',
'youtube.com',
'wikimedia.org',
'redd.it',
'gfycat.com',
'media.giphy.com',
'twitter.com',
'twimg.com',
'instagram.com',
'pinterest.com',
'pinterest.fr',
'tumblr.com',
'deviantart.com',
];
const MODE_ALL = 'all';
const MODE_COMMON = 'common';
const MODE_NONE = 'none';
/**
* @var WebThumbnailer instance.
*/
protected $wt;
/**
* @var ConfigManager instance.
*/
protected $conf;
/**
* Thumbnailer constructor.
*
* @param ConfigManager $conf instance.
*/
public function __construct($conf)
{
$this->conf = $conf;
if (! $this->checkRequirements()) {
$this->conf->set('thumbnails.enabled', false);
$this->conf->write(true);
// TODO: create a proper error handling system able to catch exceptions...
die(t('php-gd extension must be loaded to use thumbnails. Thumbnails are now disabled. Please reload the page.'));
}
$this->wt = new WebThumbnailer();
WTConfigManager::addFile('inc/web-thumbnailer.json');
$this->wt->maxWidth($this->conf->get('thumbnails.width'))
->maxHeight($this->conf->get('thumbnails.height'))
->crop(true)
->debug($this->conf->get('dev.debug', false));
}
/**
* Retrieve a thumbnail for given URL
*
* @param string $url where to look for a thumbnail.
*
* @return bool|string The thumbnail relative cache file path, or false if none has been found.
*/
public function get($url)
{
if ($this->conf->get('thumbnails.mode') === self::MODE_COMMON
&& ! $this->isCommonMediaOrImage($url)
) {
return false;
}
try {
return $this->wt->thumbnail($url);
} catch (WebThumbnailerException $e) {
// Exceptions are only thrown in debug mode.
error_log(get_class($e) . ': ' . $e->getMessage());
}
return false;
}
/**
* We check weather the given URL is from a common media domain,
* or if the file extension is an image.
*
* @param string $url to check
*
* @return bool true if it's an image or from a common media domain, false otherwise.
*/
public function isCommonMediaOrImage($url)
{
foreach (self::COMMON_MEDIA_DOMAINS as $domain) {
if (strpos($url, $domain) !== false) {
return true;
}
}
if (endsWith($url, '.jpg') || endsWith($url, '.png') || endsWith($url, '.jpeg')) {
return true;
}
return false;
}
/**
* Make sure that requirements are match to use thumbnails:
* - php-gd is loaded
*/
protected function checkRequirements()
{
return extension_loaded('gd');
}
}

View file

@ -2,6 +2,7 @@
use Shaarli\Config\ConfigJson;
use Shaarli\Config\ConfigPhp;
use Shaarli\Config\ConfigManager;
use Shaarli\Thumbnailer;
/**
* Class Updater.
@ -30,6 +31,11 @@ class Updater
*/
protected $isLoggedIn;
/**
* @var array $_SESSION
*/
protected $session;
/**
* @var ReflectionMethod[] List of current class methods.
*/
@ -42,13 +48,17 @@ class Updater
* @param LinkDB $linkDB LinkDB instance.
* @param ConfigManager $conf Configuration Manager instance.
* @param boolean $isLoggedIn True if the user is logged in.
* @param array $session $_SESSION (by reference)
*
* @throws ReflectionException
*/
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
{
$this->doneUpdates = $doneUpdates;
$this->linkDB = $linkDB;
$this->conf = $conf;
$this->isLoggedIn = $isLoggedIn;
$this->session = &$session;
// Retrieve all update methods.
$class = new ReflectionClass($this);
@ -480,6 +490,30 @@ public function updateMethodDownloadSizeAndTimeoutConf()
}
$this->conf->write($this->isLoggedIn);
return true;
}
/**
* * Move thumbnails management to WebThumbnailer, coming with new settings.
*/
public function updateMethodWebThumbnailer()
{
if ($this->conf->exists('thumbnails.mode')) {
return true;
}
$thumbnailsEnabled = $this->conf->get('thumbnail.enable_thumbnails', true);
$this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
$this->conf->set('thumbnails.width', 125);
$this->conf->set('thumbnails.height', 90);
$this->conf->remove('thumbnail');
$this->conf->write(true);
if ($thumbnailsEnabled) {
$this->session['warnings'][] = t(
'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
);
}
return true;
}

View file

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

View file

@ -1,10 +0,0 @@
import Blazy from 'blazy';
(() => {
const picwall = document.getElementById('picwall_container');
if (picwall != null) {
// Suppress ESLint error because that's how bLazy works
/* eslint-disable no-new */
new Blazy();
}
})();

View file

@ -0,0 +1,51 @@
/**
* Script used in the thumbnails update page.
*
* It retrieves the list of link IDs to update, and execute AJAX requests
* to update their thumbnails, while updating the progress bar.
*/
/**
* Update the thumbnail of the link with the current i index in ids.
* It contains a recursive call to retrieve the thumb of the next link when it succeed.
* It also update the progress bar and other visual feedback elements.
*
* @param {array} ids List of LinkID to update
* @param {int} i Current index in ids
* @param {object} elements List of DOM element to avoid retrieving them at each iteration
*/
function updateThumb(ids, i, elements) {
const xhr = new XMLHttpRequest();
xhr.open('POST', '?do=ajax_thumb_update');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status !== 200) {
alert(`An error occurred. Return code: ${xhr.status}`);
} else {
const { response } = xhr;
i += 1;
elements.progressBar.style.width = `${(i * 100) / ids.length}%`;
elements.current.innerHTML = i;
elements.title.innerHTML = response.title;
if (response.thumbnail !== false) {
elements.thumbnail.innerHTML = `<img src="${response.thumbnail}">`;
}
if (i < ids.length) {
updateThumb(ids, i, elements);
}
}
};
xhr.send(`id=${ids[i]}`);
}
(() => {
const ids = document.getElementsByName('ids')[0].value.split(',');
const elements = {
progressBar: document.querySelector('.progressbar > div'),
current: document.querySelector('.progress-current'),
thumbnail: document.querySelector('.thumbnail-placeholder'),
title: document.querySelector('.thumbnail-link-title'),
};
updateThumb(ids, 0, elements);
})();

View file

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

View file

@ -146,6 +146,17 @@ body,
background-color: $main-green;
}
.pure-alert-warning {
a {
color: $warning-text;
font-weight: bold;
}
}
.page-single-alert {
margin-top: 100px;
}
.anchor {
&:target {
padding-top: 40px;
@ -625,23 +636,22 @@ body,
}
.linklist-item {
position: relative;
margin: 0 0 10px;
box-shadow: 1px 1px 3px $light-grey;
background: $almost-white;
&.private {
.linklist-item-title {
&::before {
@extend %private-border;
margin-top: 3px;
}
}
.linklist-item-description {
&::before {
@extend %private-border;
height: 100%;
}
&::before {
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 1;
background: $orange;
width: 2px;
height: 100%;
content: '';
}
}
}
@ -1543,3 +1553,40 @@ form {
.pure-button-shaarli {
background-color: $main-green;
}
.progressbar {
border-radius: 6px;
background-color: $main-green;
padding: 1px;
> div {
border-radius: 10px;
background: repeating-linear-gradient(
-45deg,
$almost-white,
$almost-white 6px,
$background-color 6px,
$background-color 12px
);
width: 0%;
height: 10px;
}
}
.thumbnails-page-container {
.progress-counter {
padding: 10px 0 20px;
}
.thumbnail-placeholder {
margin: 10px auto;
background-color: $light-grey;
}
.thumbnail-link-title {
padding-bottom: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View file

@ -701,8 +701,8 @@ a.bigbutton, #pageheader a.bigbutton {
position: relative;
display: table-cell;
vertical-align: middle;
width: 90px;
height: 90px;
width: 120px;
height: 120px;
overflow: hidden;
text-align: center;
float: left;
@ -739,9 +739,9 @@ a.bigbutton, #pageheader a.bigbutton {
position: absolute;
top: 0;
left: 0;
width: 90px;
width: 120px;
font-weight: bold;
font-size: 8pt;
font-size: 9pt;
color: #fff;
text-align: left;
background-color: transparent;
@ -1210,3 +1210,43 @@ ul.errors {
width: 13px;
height: 13px;
}
.thumbnails-update-container {
padding: 20px 0;
width: 50%;
margin: auto;
}
.thumbnails-update-container .thumbnail-placeholder {
background: grey;
margin: auto;
}
.thumbnails-update-container .thumbnail-link-title {
width: 75%;
margin: auto;
padding-bottom: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progressbar {
border-radius: 6px;
background-color: #111;
padding: 1px;
}
.progressbar > div {
border-radius: 10px;
background: repeating-linear-gradient(
-45deg,
#f5f5f5,
#f5f5f5 6px,
#d0d0d0 6px,
#d0d0d0 12px
);
width: 0%;
height: 10px;
}

View file

@ -19,6 +19,7 @@
"shaarli/netscape-bookmark-parser": "^2.0",
"erusev/parsedown": "^1.6",
"slim/slim": "^3.0",
"arthurhoaro/web-thumbnailer": "^1.1",
"pubsubhubbub/publisher": "dev-master",
"gettext/gettext": "^4.4"
},

338
composer.lock generated
View file

@ -1,11 +1,59 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "308a35eab91602fbb449f2c669c445ed",
"content-hash": "da7a0c081b61d949154c5d2e5370cbab",
"packages": [
{
"name": "arthurhoaro/web-thumbnailer",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/ArthurHoaro/web-thumbnailer.git",
"reference": "a5a52f69e8e8f3c71fab9649e2a927e2d3f418f1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/a5a52f69e8e8f3c71fab9649e2a927e2d3f418f1",
"reference": "a5a52f69e8e8f3c71fab9649e2a927e2d3f418f1",
"shasum": ""
},
"require": {
"php": ">=5.6",
"phpunit/php-text-template": "^1.2"
},
"conflict": {
"phpunit/php-timer": ">=2"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.0",
"phpunit/phpunit": "5.2.*",
"squizlabs/php_codesniffer": "^3.2"
},
"type": "library",
"autoload": {
"psr-0": {
"WebThumbnailer\\": [
"src/",
"tests/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Arthur Hoaro",
"homepage": "http://hoa.ro"
}
],
"description": "PHP library which will retrieve a thumbnail for any given URL",
"time": "2018-07-17T10:21:14+00:00"
},
{
"name": "container-interop/container-interop",
"version": "1.2.0",
@ -85,16 +133,16 @@
},
{
"name": "gettext/gettext",
"version": "v4.4.4",
"version": "v4.6.0",
"source": {
"type": "git",
"url": "https://github.com/oscarotero/Gettext.git",
"reference": "ab5e863de2f60806d02e6e6081e21efd45249168"
"reference": "cae84aff39a87e07bd6e5cddb5adb720a0ffa357"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/oscarotero/Gettext/zipball/ab5e863de2f60806d02e6e6081e21efd45249168",
"reference": "ab5e863de2f60806d02e6e6081e21efd45249168",
"url": "https://api.github.com/repos/oscarotero/Gettext/zipball/cae84aff39a87e07bd6e5cddb5adb720a0ffa357",
"reference": "cae84aff39a87e07bd6e5cddb5adb720a0ffa357",
"shasum": ""
},
"require": {
@ -103,7 +151,7 @@
},
"require-dev": {
"illuminate/view": "*",
"phpunit/phpunit": "^4.8|^5.7",
"phpunit/phpunit": "^4.8|^5.7|^6.5",
"squizlabs/php_codesniffer": "^3.0",
"symfony/yaml": "~2",
"twig/extensions": "*",
@ -143,20 +191,20 @@
"po",
"translation"
],
"time": "2018-02-21T18:49:59+00:00"
"time": "2018-06-26T16:51:09+00:00"
},
{
"name": "gettext/languages",
"version": "2.3.0",
"version": "2.4.0",
"source": {
"type": "git",
"url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git",
"reference": "49c39e51569963cc917a924b489e7025bfb9d8c7"
"reference": "1b74377bd0c4cd87e8d72b948f5d8867e23505a5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/49c39e51569963cc917a924b489e7025bfb9d8c7",
"reference": "49c39e51569963cc917a924b489e7025bfb9d8c7",
"url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/1b74377bd0c4cd87e8d72b948f5d8867e23505a5",
"reference": "1b74377bd0c4cd87e8d72b948f5d8867e23505a5",
"shasum": ""
},
"require": {
@ -204,7 +252,7 @@
"translations",
"unicode"
],
"time": "2017-03-23T17:02:28+00:00"
"time": "2018-06-21T15:58:36+00:00"
},
{
"name": "katzgrau/klogger",
@ -302,6 +350,47 @@
],
"time": "2018-02-13T20:26:39+00:00"
},
{
"name": "phpunit/php-text-template",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-text-template.git",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Simple template engine.",
"homepage": "https://github.com/sebastianbergmann/php-text-template/",
"keywords": [
"template"
],
"time": "2015-06-21T13:50:34+00:00"
},
{
"name": "pimple/pimple",
"version": "v3.2.3",
@ -504,12 +593,12 @@
"source": {
"type": "git",
"url": "https://github.com/pubsubhubbub/php-publisher.git",
"reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f"
"reference": "5008fc529b057251b48f4d17a10fdb20047ea8f5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/0d224daebd504ab61c22fee4db58f8d1fc18945f",
"reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f",
"url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/5008fc529b057251b48f4d17a10fdb20047ea8f5",
"reference": "5008fc529b057251b48f4d17a10fdb20047ea8f5",
"shasum": ""
},
"require": {
@ -539,7 +628,7 @@
"publishers",
"pubsubhubbub"
],
"time": "2017-10-08T10:59:41+00:00"
"time": "2018-05-22T11:56:26+00:00"
},
{
"name": "shaarli/netscape-bookmark-parser",
@ -598,16 +687,16 @@
},
{
"name": "slim/slim",
"version": "3.9.2",
"version": "3.10.0",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim.git",
"reference": "4086d0106cf5a7135c69fce4161fe355a8feb118"
"reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/4086d0106cf5a7135c69fce4161fe355a8feb118",
"reference": "4086d0106cf5a7135c69fce4161fe355a8feb118",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/d8aabeacc3688b25e2f2dd2db91df91ec6fdd748",
"reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748",
"shasum": ""
},
"require": {
@ -665,7 +754,7 @@
"micro",
"router"
],
"time": "2017-11-26T19:13:09+00:00"
"time": "2018-04-19T19:29:08+00:00"
}
],
"packages-dev": [
@ -1022,23 +1111,23 @@
},
{
"name": "phpspec/prophecy",
"version": "1.7.5",
"version": "1.7.6",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401"
"reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401",
"reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
"reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
"sebastian/comparator": "^1.1|^2.0",
"sebastian/comparator": "^1.1|^2.0|^3.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
},
"require-dev": {
@ -1081,7 +1170,7 @@
"spy",
"stub"
],
"time": "2018-02-19T10:16:54+00:00"
"time": "2018-04-18T13:57:24+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -1193,47 +1282,6 @@
],
"time": "2017-11-27T13:52:08+00:00"
},
{
"name": "phpunit/php-text-template",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-text-template.git",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Simple template engine.",
"homepage": "https://github.com/sebastianbergmann/php-text-template/",
"keywords": [
"template"
],
"time": "2015-06-21T13:50:34+00:00"
},
{
"name": "phpunit/php-timer",
"version": "1.0.9",
@ -2207,21 +2255,22 @@
},
{
"name": "symfony/config",
"version": "v3.4.6",
"version": "v3.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "05e10567b529476a006b00746c5f538f1636810e"
"reference": "1fffdeb349ff36a25184e5564c25289b1dbfc402"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/05e10567b529476a006b00746c5f538f1636810e",
"reference": "05e10567b529476a006b00746c5f538f1636810e",
"url": "https://api.github.com/repos/symfony/config/zipball/1fffdeb349ff36a25184e5564c25289b1dbfc402",
"reference": "1fffdeb349ff36a25184e5564c25289b1dbfc402",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8",
"symfony/filesystem": "~2.8|~3.0|~4.0"
"symfony/filesystem": "~2.8|~3.0|~4.0",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/dependency-injection": "<3.3",
@ -2266,20 +2315,20 @@
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
"time": "2018-02-14T10:03:57+00:00"
"time": "2018-06-19T14:02:58+00:00"
},
{
"name": "symfony/console",
"version": "v3.4.6",
"version": "v3.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "067339e9b8ec30d5f19f5950208893ff026b94f7"
"reference": "1b97071a26d028c9bd4588264e101e14f6e7cd00"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/067339e9b8ec30d5f19f5950208893ff026b94f7",
"reference": "067339e9b8ec30d5f19f5950208893ff026b94f7",
"url": "https://api.github.com/repos/symfony/console/zipball/1b97071a26d028c9bd4588264e101e14f6e7cd00",
"reference": "1b97071a26d028c9bd4588264e101e14f6e7cd00",
"shasum": ""
},
"require": {
@ -2300,7 +2349,7 @@
"symfony/process": "~3.3|~4.0"
},
"suggest": {
"psr/log": "For using the console logger",
"psr/log-implementation": "For using the console logger",
"symfony/event-dispatcher": "",
"symfony/lock": "",
"symfony/process": ""
@ -2335,20 +2384,20 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2018-02-26T15:46:28+00:00"
"time": "2018-05-23T05:02:55+00:00"
},
{
"name": "symfony/debug",
"version": "v3.4.6",
"version": "v3.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc"
"reference": "47e6788c5b151cf0cfdf3329116bf33800632d75"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
"reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
"url": "https://api.github.com/repos/symfony/debug/zipball/47e6788c5b151cf0cfdf3329116bf33800632d75",
"reference": "47e6788c5b151cf0cfdf3329116bf33800632d75",
"shasum": ""
},
"require": {
@ -2391,20 +2440,20 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2018-02-28T21:49:22+00:00"
"time": "2018-06-25T11:10:40+00:00"
},
{
"name": "symfony/dependency-injection",
"version": "v3.4.6",
"version": "v3.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
"reference": "12e901abc1cb0d637a0e5abe9923471361d96b07"
"reference": "a0be80e3f8c11aca506e250c00bb100c04c35d10"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/12e901abc1cb0d637a0e5abe9923471361d96b07",
"reference": "12e901abc1cb0d637a0e5abe9923471361d96b07",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a0be80e3f8c11aca506e250c00bb100c04c35d10",
"reference": "a0be80e3f8c11aca506e250c00bb100c04c35d10",
"shasum": ""
},
"require": {
@ -2462,24 +2511,25 @@
],
"description": "Symfony DependencyInjection Component",
"homepage": "https://symfony.com",
"time": "2018-03-04T03:54:53+00:00"
"time": "2018-06-25T08:36:56+00:00"
},
{
"name": "symfony/filesystem",
"version": "v3.4.6",
"version": "v3.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541"
"reference": "8a721a5f2553c6c3482b1c5b22ed60fe94dd63ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541",
"reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/8a721a5f2553c6c3482b1c5b22ed60fe94dd63ed",
"reference": "8a721a5f2553c6c3482b1c5b22ed60fe94dd63ed",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
"php": "^5.5.9|>=7.0.8",
"symfony/polyfill-ctype": "~1.8"
},
"type": "library",
"extra": {
@ -2511,20 +2561,20 @@
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
"time": "2018-02-22T10:48:49+00:00"
"time": "2018-06-21T11:10:19+00:00"
},
{
"name": "symfony/finder",
"version": "v3.4.6",
"version": "v3.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "a479817ce0a9e4adfd7d39c6407c95d97c254625"
"reference": "3a8c3de91d2b2c68cd2d665cf9d00f7ef9eaa394"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/a479817ce0a9e4adfd7d39c6407c95d97c254625",
"reference": "a479817ce0a9e4adfd7d39c6407c95d97c254625",
"url": "https://api.github.com/repos/symfony/finder/zipball/3a8c3de91d2b2c68cd2d665cf9d00f7ef9eaa394",
"reference": "3a8c3de91d2b2c68cd2d665cf9d00f7ef9eaa394",
"shasum": ""
},
"require": {
@ -2560,20 +2610,75 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2018-03-05T18:28:11+00:00"
"time": "2018-06-19T20:52:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.7.0",
"name": "symfony/polyfill-ctype",
"version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b"
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b",
"reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
"reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
},
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"time": "2018-04-30T19:57:29+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "3296adf6a6454a050679cde90f95350ad604b171"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171",
"reference": "3296adf6a6454a050679cde90f95350ad604b171",
"shasum": ""
},
"require": {
@ -2585,7 +2690,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.7-dev"
"dev-master": "1.8-dev"
}
},
"autoload": {
@ -2619,24 +2724,25 @@
"portable",
"shim"
],
"time": "2018-01-30T19:27:44+00:00"
"time": "2018-04-26T10:06:28+00:00"
},
{
"name": "symfony/yaml",
"version": "v3.4.6",
"version": "v3.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "6af42631dcf89e9c616242c900d6c52bd53bd1bb"
"reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/6af42631dcf89e9c616242c900d6c52bd53bd1bb",
"reference": "6af42631dcf89e9c616242c900d6c52bd53bd1bb",
"url": "https://api.github.com/repos/symfony/yaml/zipball/c5010cc1692ce1fa328b1fb666961eb3d4a85bb0",
"reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
"php": "^5.5.9|>=7.0.8",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/console": "<3.4"
@ -2677,7 +2783,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2018-02-16T09:50:28+00:00"
"time": "2018-05-03T23:18:14+00:00"
},
{
"name": "theseer/fdomdocument",

18
doc/md/Link-structure.md Normal file
View file

@ -0,0 +1,18 @@
## Link structure
Every link available through the `LinkDB` object is represented as an array
containing the following fields:
* `id` (integer): Unique identifier.
* `title` (string): Title of the link.
* `url` (string): URL of the link. Used for displayable links (without redirector, url encoding, etc.).
Can be absolute or relative for Notes.
* `real_url` (string): Real destination URL, can be redirected, encoded, etc.
* `shorturl` (string): Permalink small hash.
* `description` (string): Link text description.
* `private` (boolean): whether the link is private or not.
* `tags` (string): all link tags separated by a single space
* `thumbnail` (string|boolean): relative path of the thumbnail cache file, or false if there isn't any.
* `created` (DateTime): link creation date time.
* `updated` (DateTime): last modification date time.

View file

@ -29,7 +29,7 @@ Extension | Required? | Usage
---|:---:|---
[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS
[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
[`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing
[`php-gd`](http://php.net/manual/en/book.image.php) | optional | required to use thumbnails
[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)

View file

@ -1,15 +1,15 @@
msgid ""
msgstr ""
"Project-Id-Version: Shaarli\n"
"POT-Creation-Date: 2018-01-24 18:43+0100\n"
"PO-Revision-Date: 2018-03-06 18:44+0100\n"
"POT-Creation-Date: 2018-07-17 13:04+0200\n"
"PO-Revision-Date: 2018-07-17 13:07+0200\n"
"Last-Translator: \n"
"Language-Team: Shaarli\n"
"Language: fr_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.6\n"
"X-Generator: Poedit 2.0.9\n"
"X-Poedit-Basepath: ../../../..\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
@ -56,7 +56,7 @@ msgstr "Liens directs"
#: application/FeedBuilder.php:153
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
msgid "Permalink"
msgstr "Permalien"
@ -68,18 +68,22 @@ msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
msgid "Could not parse history file"
msgstr "Format incorrect pour le fichier d'historique"
#: application/Languages.php:161
#: application/Languages.php:177
msgid "Automatic"
msgstr "Automatique"
#: application/Languages.php:162
#: application/Languages.php:178
msgid "English"
msgstr "Anglais"
#: application/Languages.php:163
#: application/Languages.php:179
msgid "French"
msgstr "Français"
#: application/Languages.php:180
msgid "German"
msgstr "Allemand"
#: application/LinkDB.php:136
msgid "You are not authorized to add a link."
msgstr "Vous n'êtes pas autorisé à ajouter un lien."
@ -163,11 +167,11 @@ msgstr ""
"a été importé avec succès en %d secondes : %d liens importés, %d liens "
"écrasés, %d liens ignorés."
#: application/PageBuilder.php:168
#: application/PageBuilder.php:200
msgid "The page you are trying to reach does not exist or has been deleted."
msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée."
#: application/PageBuilder.php:170
#: application/PageBuilder.php:202
msgid "404 Not Found"
msgstr "404 Introuvable"
@ -176,21 +180,37 @@ msgstr "404 Introuvable"
msgid "Plugin \"%s\" files not found."
msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
#: application/Updater.php:76
#: application/Thumbnailer.php:61
msgid ""
"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
"disabled. Please reload the page."
msgstr ""
"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
"disabled. Please reload the page."
#: application/Updater.php:86
msgid "Couldn't retrieve Updater class methods."
msgstr "Impossible de récupérer les méthodes de la classe Updater."
#: application/Updater.php:506
#: application/Updater.php:514 index.php:1023
msgid ""
"You have enabled or changed thumbnails mode. <a href=\"?do=thumbs_update"
"\">Please synchronize them</a>."
msgstr ""
"Vous avez activé ou changé le mode de miniatures. <a href=\"?do=thumbs_update"
"\">Merci de les synchroniser</a>."
#: application/Updater.php:566
msgid "An error occurred while running the update "
msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
#: application/Updater.php:546
#: application/Updater.php:606
msgid "Updates file path is not set, can't write updates."
msgstr ""
"Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
"d'écrire les mises à jour."
#: application/Updater.php:551
#: application/Updater.php:611
msgid "Unable to write updates in "
msgstr "Impossible d'écrire les mises à jour dans "
@ -230,6 +250,7 @@ msgstr ""
"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
#: application/config/ConfigManager.php:135
#: application/config/ConfigManager.php:162
msgid "Invalid setting key parameter. String expected, got: "
msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
@ -251,135 +272,133 @@ msgstr "Vous n'êtes pas autorisé à modifier la configuration."
msgid "Error accessing"
msgstr "Une erreur s'est produite en accédant à"
#: index.php:142
#: index.php:143
msgid "Shared links on "
msgstr "Liens partagés sur "
#: index.php:164
#: index.php:165
msgid "Insufficient permissions:"
msgstr "Permissions insuffisantes :"
#: index.php:303
#: index.php:201
msgid "I said: NO. You are banned for the moment. Go away."
msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
#: index.php:368
#: index.php:273
msgid "Wrong login/password."
msgstr "Nom d'utilisateur ou mot de passe incorrects."
#: index.php:576 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42
#: index.php:483 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:46
msgid "Daily"
msgstr "Quotidien"
#: index.php:681 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
#: index.php:589 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:75
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:99
msgid "Login"
msgstr "Connexion"
#: index.php:722 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39
#: index.php:606 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:41
msgid "Picture wall"
msgstr "Mur d'images"
#: index.php:770 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
#: index.php:683 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36
#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
msgid "Tag cloud"
msgstr "Nuage de tags"
#: index.php:803 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
#: index.php:716 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
msgid "Tag list"
msgstr "Liste des tags"
#: index.php:1028 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
#: index.php:941 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
msgid "Tools"
msgstr "Outils"
#: index.php:1037
#: index.php:950
msgid "You are not supposed to change a password on an Open Shaarli."
msgstr ""
"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
#: index.php:1042 index.php:1084 index.php:1162 index.php:1193 index.php:1293
#: index.php:955 index.php:997 index.php:1085 index.php:1116 index.php:1221
msgid "Wrong token."
msgstr "Jeton invalide."
#: index.php:1047
#: index.php:960
msgid "The old password is not correct."
msgstr "L'ancien mot de passe est incorrect."
#: index.php:1067
#: index.php:980
msgid "Your password has been changed"
msgstr "Votre mot de passe a été modifié"
#: index.php:1072
#: index.php:985
#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
msgid "Change password"
msgstr "Modification du mot de passe"
#: index.php:1121
#: index.php:1043
msgid "Configuration was saved."
msgstr "La configuration a été sauvegardé."
#: index.php:1145 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
#: index.php:1068 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
msgid "Configure"
msgstr "Configurer"
#: index.php:1156 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
#: index.php:1079 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
msgid "Manage tags"
msgstr "Gérer les tags"
#: index.php:1174
#: index.php:1097
#, php-format
msgid "The tag was removed from %d link."
msgid_plural "The tag was removed from %d links."
msgstr[0] "Le tag a été supprimé de %d lien."
msgstr[1] "Le tag a été supprimé de %d liens."
#: index.php:1175
#: index.php:1098
#, php-format
msgid "The tag was renamed in %d link."
msgid_plural "The tag was renamed in %d links."
msgstr[0] "Le tag a été renommé dans %d lien."
msgstr[1] "Le tag a été renommé dans %d liens."
#: index.php:1183 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
#: index.php:1106 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
msgid "Shaare a new link"
msgstr "Partager un nouveau lien"
#: index.php:1353 tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
#: index.php:1281 tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
msgid "Edit"
msgstr "Modifier"
#: index.php:1353 index.php:1418
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
#: index.php:1281 index.php:1351
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
msgid "Shaare"
msgstr "Shaare"
#: index.php:1387
#: index.php:1320
msgid "Note: "
msgstr "Note : "
#: index.php:1427 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
#: index.php:1360 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
msgid "Export"
msgstr "Exporter"
#: index.php:1489 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
#: index.php:1422 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
msgid "Import"
msgstr "Importer"
#: index.php:1499
#: index.php:1432
#, php-format
msgid ""
"The file you are trying to upload is probably bigger than what this "
@ -389,16 +408,20 @@ msgstr ""
"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
"légères."
#: index.php:1538 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
#: index.php:1471 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
msgid "Plugin administration"
msgstr "Administration des extensions"
#: index.php:1703
#: index.php:1523 tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
msgid "Thumbnails update"
msgstr "Mise à jour des miniatures"
#: index.php:1695
msgid "Search: "
msgstr "Recherche : "
#: index.php:1930
#: index.php:1735
#, php-format
msgid ""
"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@ -417,7 +440,7 @@ msgstr ""
"cookies. Nous vous recommandons d'accéder à votre serveur depuis son adresse "
"IP ou un <em>Fully Qualified Domain Name</em>.<br>"
#: index.php:1940
#: index.php:1745
msgid "Click to try again."
msgstr "Cliquer ici pour réessayer."
@ -467,19 +490,19 @@ msgstr ""
msgid "Isso server URL (without 'http://')"
msgstr "URL du serveur Isso (sans 'http://')"
#: plugins/markdown/markdown.php:158
#: plugins/markdown/markdown.php:161
msgid "Description will be rendered with"
msgstr "La description sera générée avec"
#: plugins/markdown/markdown.php:159
#: plugins/markdown/markdown.php:162
msgid "Markdown syntax documentation"
msgstr "Documentation sur la syntaxe Markdown"
#: plugins/markdown/markdown.php:160
#: plugins/markdown/markdown.php:163
msgid "Markdown syntax"
msgstr "la syntaxe Markdown"
#: plugins/markdown/markdown.php:339
#: plugins/markdown/markdown.php:347
msgid ""
"Render shaare description with Markdown syntax.<br><strong>Warning</"
"strong>:\n"
@ -577,11 +600,11 @@ msgstr "URL de l'API Wallabag"
msgid "Wallabag API version (1 or 2)"
msgstr "Version de l'API Wallabag (1 ou 2)"
#: tests/LanguagesTest.php:188 tests/LanguagesTest.php:201
#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
#: tests/languages/fr/LanguagesFrTest.php:160
#: tests/languages/fr/LanguagesFrTest.php:173
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:85
msgid "Search"
msgid_plural "Search"
msgstr[0] "Rechercher"
@ -625,8 +648,8 @@ msgid "Rename"
msgstr "Renommer"
#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
msgid "Delete"
msgstr "Supprimer"
@ -736,8 +759,36 @@ msgstr ""
msgid "API secret"
msgstr "Clé d'API secrète"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:277
msgid "Enable thumbnails"
msgstr "Activer les miniatures"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:281
msgid "You need to enable the extension <code>php-gd</code> to use thumbnails."
msgstr ""
"Vous devez activer l'extension <code>php-gd</code> pour utiliser les "
"miniatures."
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:285
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
msgid "Synchronize thumbnails"
msgstr "Synchroniser les miniatures"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:296
#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
msgid "All"
msgstr "Tous"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:300
msgid "Only common media hosts"
msgstr "Seulement les hébergeurs de média connus"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:304
msgid "None"
msgstr "Aucune"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:312
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
msgid "Save"
@ -763,25 +814,27 @@ msgstr "Tous les liens d'un jour sur une page."
msgid "Next day"
msgstr "Jour suivant"
#: tpl/editlink.html
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
msgid "Edit Shaare"
msgstr "Modifier le Shaare"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
msgid "New Shaare"
msgstr "Nouveau Shaare"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
msgid "Created:"
msgstr "Création :"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
msgid "URL"
msgstr "URL"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
msgid "Title"
msgstr "Titre"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@ -789,17 +842,17 @@ msgstr "Titre"
msgid "Description"
msgstr "Description"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
msgid "Tags"
msgstr "Tags"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167
msgid "Private"
msgstr "Privé"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
msgid "Apply Changes"
msgstr "Appliquer"
@ -811,10 +864,6 @@ msgstr "Exporter les données"
msgid "Selection"
msgstr "Choisir"
#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
msgid "All"
msgstr "Tous"
#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
msgid "Public"
msgstr "Publics"
@ -876,15 +925,15 @@ msgstr ""
#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151
msgid "Username"
msgstr "Nom d'utilisateur"
#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152
msgid "Password"
msgstr "Mot de passe"
@ -901,28 +950,28 @@ msgid "Install"
msgstr "Installer"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
msgid "shaare"
msgid_plural "shaares"
msgstr[0] "shaare"
msgstr[1] "shaares"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
msgid "private link"
msgid_plural "private links"
msgstr[0] "lien privé"
msgstr[1] "liens privés"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:121
msgid "Search text"
msgstr "Recherche texte"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:128
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:128
#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
@ -930,52 +979,52 @@ msgstr "Recherche texte"
msgid "Filter by tag"
msgstr "Filtrer par tag"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
msgid "Nothing found."
msgstr "Aucun résultat."
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118
#, php-format
msgid "%s result"
msgid_plural "%s results"
msgstr[0] "%s résultat"
msgstr[1] "%s résultats"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
msgid "for"
msgstr "pour"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
msgid "tagged"
msgstr "taggé"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133
msgid "Remove tag"
msgstr "Retirer le tag"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:142
msgid "with status"
msgstr "avec le statut"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
msgid "without any tag"
msgstr "sans tag"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
msgid "Fold"
msgstr "Replier"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
msgid "Edited: "
msgstr "Modifié : "
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
msgid "permalink"
msgstr "permalien"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
msgid "Add tag"
msgstr "Ajouter un tag"
@ -1021,8 +1070,8 @@ msgstr ""
"réessayer plus tard."
#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:155
msgid "Remember me"
msgstr "Rester connecté"
@ -1053,35 +1102,52 @@ msgstr "Déplier tout"
msgid "Are you sure you want to delete this link?"
msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:90
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:65
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:90
msgid "RSS Feed"
msgstr "Flux RSS"
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:70
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:106
msgid "Logout"
msgstr "Déconnexion"
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:173
msgid "is available"
msgstr "est disponible"
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:180
msgid "Error"
msgstr "Erreur"
#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
msgid "Picture wall unavailable (thumbnails are disabled)."
msgstr ""
"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
#, fuzzy
#| msgid ""
#| "You don't have any cached thumbnail. Try to <a href=\"?do=thumbs_update"
#| "\">synchronize them</a>."
msgid ""
"There is no cached thumbnail. Try to <a href=\"?do=thumbs_update"
"\">synchronize them</a>."
msgstr ""
"Il n'y a aucune miniature en cache. Essayer de <a href=\"?do=thumbs_update"
"\">les synchroniser</a>."
#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
msgid "Picture Wall"
msgstr "Mur d'images"
#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
msgid "pics"
msgstr "images"
@ -1223,7 +1289,11 @@ msgstr ""
msgid "Export database"
msgstr "Exporter les données"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55
msgid "Synchronize all link thumbnails"
msgstr "Synchroniser toutes les miniatures"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
msgid ""
"Drag one of these button to your bookmarks toolbar or right-click it and "
"\"Bookmark This Link\""
@ -1231,13 +1301,13 @@ msgstr ""
"Glisser un de ces bouttons dans votre barre de favoris ou cliquer droit "
"dessus et « Ajouter aux favoris »"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
msgid "then click on the bookmarklet in any page you want to share."
msgstr ""
"puis cliquer sur le marque page depuis un site que vous souhaitez partager."
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
msgid ""
"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
"Link"
@ -1245,31 +1315,31 @@ msgstr ""
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
msgid "then click ✚Shaare link button in any page you want to share"
msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118
msgid "The selected text is too long, it will be truncated."
msgstr "Le texte sélectionné est trop long, il sera tronqué."
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
msgid "Shaare link"
msgstr "Shaare"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
msgid ""
"Then click ✚Add Note button anytime to start composing a private Note (text "
"post) to your Shaarli"
msgstr ""
"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127
msgid "Add Note"
msgstr "Ajouter une Note"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
msgid ""
"You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
"functionality."
@ -1277,25 +1347,25 @@ msgstr ""
"Vous devez utiliser Shaarli en <strong>HTTPS</strong> pour utiliser cette "
"fonctionalité."
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
msgid "Add to"
msgstr "Ajouter à"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
msgid "3rd party"
msgstr "Applications tierces"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:163
msgid "Plugin"
msgstr "Extension"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:158
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
msgid "plugin"
msgstr "extension"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191
msgid ""
"Drag this link to your bookmarks toolbar, or right-click it and choose "
"Bookmark This Link"
@ -1303,6 +1373,26 @@ msgstr ""
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »"
#, fuzzy
#~| msgid "Enable thumbnails"
#~ msgid "Synchonize thumbnails"
#~ msgstr "Activer les miniatures"
#~ msgid "Warning: "
#~ msgstr "Attention : "
#~ msgid ""
#~ "It's recommended to visit the picture wall after enabling this feature."
#~ msgstr ""
#~ "Il est recommandé de visiter le Mur d'images après avoir activé cette "
#~ "fonctionnalité."
#~ msgid ""
#~ "If you have a large database, the first retrieval may take a few minutes."
#~ msgstr ""
#~ "Si vous avez beaucoup de liens, la première récupération peut prendre "
#~ "plusieurs minutes."
#, fuzzy
#~| msgid "Change"
#~ msgid "range"

13
inc/web-thumbnailer.json Normal file
View file

@ -0,0 +1,13 @@
{
"settings": {
"default": {
"download_mode": "DOWNLOAD",
"_comment": "infinite cache",
"cache_duration": -1,
"timeout": 10
},
"path": {
"cache": "cache/"
}
}
}

527
index.php
View file

@ -75,11 +75,12 @@
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\Languages;
use \Shaarli\Security\LoginManager;
use \Shaarli\Security\SessionManager;
use \Shaarli\ThemeUtils;
use \Shaarli\Thumbnailer;
// Ensure the PHP version is supported
try {
@ -513,7 +514,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
read_updates_file($conf->get('resource.updates')),
$LINKSDB,
$conf,
$loginManager->isLoggedIn()
$loginManager->isLoggedIn(),
$_SESSION
);
try {
$newUpdates = $updater->update();
@ -528,7 +530,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
die($e->getMessage());
}
$PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
$PAGE = new PageBuilder($conf, $_SESSION, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
$PAGE->assign('linkcount', count($LINKSDB));
$PAGE->assign('privateLinkcount', count_private($LINKSDB));
$PAGE->assign('plugin_errors', $pluginManager->getErrors());
@ -601,19 +603,23 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
// -------- Picture wall
if ($targetPage == Router::$PAGE_PICWALL)
{
$PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
$PAGE->assign('linksToDisplay', []);
$PAGE->renderPage('picwall');
exit;
}
// Optionally filter the results:
$links = $LINKSDB->filterSearch($_GET);
$linksToDisplay = array();
// Get only links which have a thumbnail.
foreach($links as $link)
// Note: we do not retrieve thumbnails here, the request is too heavy.
foreach($links as $key => $link)
{
$permalink='?'.$link['shorturl'];
$thumb=lazyThumbnail($conf, $link['url'],$permalink);
if ($thumb!='') // Only output links which have a thumbnail.
{
$link['thumbnail']=$thumb; // Thumbnail HTML code.
$linksToDisplay[]=$link; // Add to array.
if (isset($link['thumbnail']) && $link['thumbnail'] !== false) {
$linksToDisplay[] = $link; // Add to array.
}
}
@ -626,7 +632,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
$PAGE->assign($key, $value);
}
$PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
$PAGE->renderPage('picwall');
exit;
}
@ -1009,6 +1015,16 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
$conf->set('api.secret', escape($_POST['apiSecret']));
$conf->set('translation.language', escape($_POST['language']));
$thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
if ($thumbnailsMode !== Thumbnailer::MODE_NONE
&& $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
) {
$_SESSION['warnings'][] = t(
'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
);
}
$conf->set('thumbnails.mode', $thumbnailsMode);
try {
$conf->write($loginManager->isLoggedIn());
$history->updateSettings();
@ -1047,6 +1063,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
$PAGE->assign('api_secret', $conf->get('api.secret'));
$PAGE->assign('languages', Languages::getAvailableLanguages());
$PAGE->assign('language', $conf->get('translation.language'));
$PAGE->assign('gd_enabled', extension_loaded('gd'));
$PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
$PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
$PAGE->renderPage('configure');
exit;
@ -1148,6 +1166,11 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
$link['title'] = $link['url'];
}
if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE) {
$thumbnailer = new Thumbnailer($conf);
$link['thumbnail'] = $thumbnailer->get($url);
}
$pluginManager->executeHooks('save_link', $link);
$LINKSDB[$id] = $link;
@ -1486,6 +1509,43 @@ function($a, $b) { return $a['order'] - $b['order']; }
exit;
}
// -------- Thumbnails Update
if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
$ids = [];
foreach ($LINKSDB as $link) {
// A note or not HTTP(S)
if ($link['url'][0] === '?' || ! startsWith(strtolower($link['url']), 'http')) {
continue;
}
$ids[] = $link['id'];
}
$PAGE->assign('ids', $ids);
$PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
$PAGE->renderPage('thumbnails');
exit;
}
// -------- Single Thumbnail Update
if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
http_response_code(400);
exit;
}
$id = (int) $_POST['id'];
if (empty($LINKSDB[$id])) {
http_response_code(404);
exit;
}
$thumbnailer = new Thumbnailer($conf);
$link = $LINKSDB[$id];
$link['thumbnail'] = $thumbnailer->get($link['url']);
$LINKSDB[$id] = $link;
$LINKSDB->save($conf->get('resource.page_cache'));
echo json_encode($link);
exit;
}
// -------- Otherwise, simply display search form and links:
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
exit;
@ -1549,6 +1609,12 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
// Start index.
$i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
$end = $i + $_SESSION['LINKS_PER_PAGE'];
$thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE;
if ($thumbnailsEnabled) {
$thumbnailer = new Thumbnailer($conf);
}
$linkDisp = array();
while ($i<$end && $i<count($keys))
{
@ -1569,9 +1635,21 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
$taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
uasort($taglist, 'strcasecmp');
$link['taglist'] = $taglist;
// Thumbnails enabled, not a note,
// and (never retrieved yet or no valid cache file)
if ($thumbnailsEnabled && $link['url'][0] != '?'
&& (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail'])))
) {
$elem = $LINKSDB[$keys[$i]];
$elem['thumbnail'] = $thumbnailer->get($link['url']);
$LINKSDB[$keys[$i]] = $elem;
$updateDB = true;
$link['thumbnail'] = $elem['thumbnail'];
}
// Check for both signs of a note: starting with ? and 7 chars long.
if ($link['url'][0] === '?' &&
strlen($link['url']) === 7) {
if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
$link['url'] = index_url($_SERVER) . $link['url'];
}
@ -1579,6 +1657,11 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
$i++;
}
// If we retrieved new thumbnails, we update the database.
if (!empty($updateDB)) {
$LINKSDB->save($conf->get('resource.page_cache'));
}
// Compute paging navigation
$searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags);
$searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
@ -1629,194 +1712,6 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
return;
}
/**
* Compute the thumbnail for a link.
*
* With a link to the original URL.
* Understands various services (youtube.com...)
* Input: $url = URL for which the thumbnail must be found.
* $href = if provided, this URL will be followed instead of $url
* Returns an associative array with thumbnail attributes (src,href,width,height,style,alt)
* Some of them may be missing.
* Return an empty array if no thumbnail available.
*
* @param ConfigManager $conf Configuration Manager instance.
* @param string $url
* @param string|bool $href
*
* @return array
*/
function computeThumbnail($conf, $url, $href = false)
{
if (!$conf->get('thumbnail.enable_thumbnails')) return array();
if ($href==false) $href=$url;
// For most hosts, the URL of the thumbnail can be easily deduced from the URL of the link.
// (e.g. http://www.youtube.com/watch?v=spVypYk4kto ---> http://img.youtube.com/vi/spVypYk4kto/default.jpg )
// ^^^^^^^^^^^ ^^^^^^^^^^^
$domain = parse_url($url,PHP_URL_HOST);
if ($domain=='youtube.com' || $domain=='www.youtube.com')
{
parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract video ID and get thumbnail
if (!empty($params['v'])) return array('src'=>'https://img.youtube.com/vi/'.$params['v'].'/default.jpg',
'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail');
}
if ($domain=='youtu.be') // Youtube short links
{
$path = parse_url($url,PHP_URL_PATH);
return array('src'=>'https://img.youtube.com/vi'.$path.'/default.jpg',
'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail');
}
if ($domain=='pix.toile-libre.org') // pix.toile-libre.org image hosting
{
parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract image filename.
if (!empty($params) && !empty($params['img'])) return array('src'=>'http://pix.toile-libre.org/upload/thumb/'.urlencode($params['img']),
'href'=>$href,'style'=>'max-width:120px; max-height:150px','alt'=>'pix.toile-libre.org thumbnail');
}
if ($domain=='imgur.com')
{
$path = parse_url($url,PHP_URL_PATH);
if (startsWith($path,'/a/')) return array(); // Thumbnails for albums are not available.
if (startsWith($path,'/r/')) return array('src'=>'https://i.imgur.com/'.basename($path).'s.jpg',
'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
if (startsWith($path,'/gallery/')) return array('src'=>'https://i.imgur.com'.substr($path,8).'s.jpg',
'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
if (substr_count($path,'/')==1) return array('src'=>'https://i.imgur.com/'.substr($path,1).'s.jpg',
'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
}
if ($domain=='i.imgur.com')
{
$pi = pathinfo(parse_url($url,PHP_URL_PATH));
if (!empty($pi['filename'])) return array('src'=>'https://i.imgur.com/'.$pi['filename'].'s.jpg',
'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
}
if ($domain=='dailymotion.com' || $domain=='www.dailymotion.com')
{
if (strpos($url,'dailymotion.com/video/')!==false)
{
$thumburl=str_replace('dailymotion.com/video/','dailymotion.com/thumbnail/video/',$url);
return array('src'=>$thumburl,
'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'DailyMotion thumbnail');
}
}
if (endsWith($domain,'.imageshack.us'))
{
$ext=strtolower(pathinfo($url,PATHINFO_EXTENSION));
if ($ext=='jpg' || $ext=='jpeg' || $ext=='png' || $ext=='gif')
{
$thumburl = substr($url,0,strlen($url)-strlen($ext)).'th.'.$ext;
return array('src'=>$thumburl,
'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'imageshack.us thumbnail');
}
}
// Some other hosts are SLOW AS HELL and usually require an extra HTTP request to get the thumbnail URL.
// So we deport the thumbnail generation in order not to slow down page generation
// (and we also cache the thumbnail)
if (! $conf->get('thumbnail.enable_localcache')) return array(); // If local cache is disabled, no thumbnails for services which require the use a local cache.
if ($domain=='flickr.com' || endsWith($domain,'.flickr.com')
|| $domain=='vimeo.com'
|| $domain=='ted.com' || endsWith($domain,'.ted.com')
|| $domain=='xkcd.com' || endsWith($domain,'.xkcd.com')
)
{
if ($domain=='vimeo.com')
{ // Make sure this vimeo URL points to a video (/xxx... where xxx is numeric)
$path = parse_url($url,PHP_URL_PATH);
if (!preg_match('!/\d+.+?!',$path)) return array(); // This is not a single video URL.
}
if ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com'))
{ // Make sure this URL points to a single comic (/xxx... where xxx is numeric)
$path = parse_url($url,PHP_URL_PATH);
if (!preg_match('!/\d+.+?!',$path)) return array();
}
if ($domain=='ted.com' || endsWith($domain,'.ted.com'))
{ // Make sure this TED URL points to a video (/talks/...)
$path = parse_url($url,PHP_URL_PATH);
if ("/talks/" !== substr($path,0,7)) return array(); // This is not a single video URL.
}
$sign = hash_hmac('sha256', $url, $conf->get('credentials.salt')); // We use the salt to sign data (it's random, secret, and specific to each installation)
return array('src'=>index_url($_SERVER).'?do=genthumbnail&hmac='.$sign.'&url='.urlencode($url),
'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail');
}
// For all other, we try to make a thumbnail of links ending with .jpg/jpeg/png/gif
// Technically speaking, we should download ALL links and check their Content-Type to see if they are images.
// But using the extension will do.
$ext=strtolower(pathinfo($url,PATHINFO_EXTENSION));
if ($ext=='jpg' || $ext=='jpeg' || $ext=='png' || $ext=='gif')
{
$sign = hash_hmac('sha256', $url, $conf->get('credentials.salt')); // We use the salt to sign data (it's random, secret, and specific to each installation)
return array('src'=>index_url($_SERVER).'?do=genthumbnail&hmac='.$sign.'&url='.urlencode($url),
'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail');
}
return array(); // No thumbnail.
}
// Returns the HTML code to display a thumbnail for a link
// with a link to the original URL.
// Understands various services (youtube.com...)
// Input: $url = URL for which the thumbnail must be found.
// $href = if provided, this URL will be followed instead of $url
// Returns '' if no thumbnail available.
function thumbnail($url,$href=false)
{
// FIXME!
global $conf;
$t = computeThumbnail($conf, $url,$href);
if (count($t)==0) return ''; // Empty array = no thumbnail for this URL.
$html='<a href="'.escape($t['href']).'"><img src="'.escape($t['src']).'"';
if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"';
if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"';
if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"';
if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"';
$html.='></a>';
return $html;
}
// Returns the HTML code to display a thumbnail for a link
// for the picture wall (using lazy image loading)
// Understands various services (youtube.com...)
// Input: $url = URL for which the thumbnail must be found.
// $href = if provided, this URL will be followed instead of $url
// Returns '' if no thumbnail available.
function lazyThumbnail($conf, $url,$href=false)
{
// FIXME!
global $conf;
$t = computeThumbnail($conf, $url,$href);
if (count($t)==0) return ''; // Empty array = no thumbnail for this URL.
$html='<a href="'.escape($t['href']).'">';
// Lazy image
$html.='<img class="b-lazy" src="#" data-src="'.escape($t['src']).'"';
if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"';
if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"';
if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"';
if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"';
$html.='>';
// No-JavaScript fallback.
$html.='<noscript><img src="'.escape($t['src']).'"';
if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"';
if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"';
if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"';
if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"';
$html.='></noscript></a>';
return $html;
}
/**
* Installation
* This function should NEVER be called if the file data/config.php exists.
@ -1908,7 +1803,7 @@ function install($conf, $sessionManager, $loginManager) {
exit;
}
$PAGE = new PageBuilder($conf, null, $sessionManager->generateToken());
$PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
$PAGE->assign('continents', $continents);
$PAGE->assign('cities', $cities);
@ -1917,232 +1812,6 @@ function install($conf, $sessionManager, $loginManager) {
exit;
}
/**
* Because some f*cking services like flickr require an extra HTTP request to get the thumbnail URL,
* I have deported the thumbnail URL code generation here, otherwise this would slow down page generation.
* The following function takes the URL a link (e.g. a flickr page) and return the proper thumbnail.
* This function is called by passing the URL:
* http://mywebsite.com/shaarli/?do=genthumbnail&hmac=[HMAC]&url=[URL]
* [URL] is the URL of the link (e.g. a flickr page)
* [HMAC] is the signature for the [URL] (so that these URL cannot be forged).
* The function below will fetch the image from the webservice and store it in the cache.
*
* @param ConfigManager $conf Configuration Manager instance,
*/
function genThumbnail($conf)
{
// Make sure the parameters in the URL were generated by us.
$sign = hash_hmac('sha256', $_GET['url'], $conf->get('credentials.salt'));
if ($sign!=$_GET['hmac']) die('Naughty boy!');
$cacheDir = $conf->get('resource.thumbnails_cache', 'cache');
// Let's see if we don't already have the image for this URL in the cache.
$thumbname=hash('sha1',$_GET['url']).'.jpg';
if (is_file($cacheDir .'/'. $thumbname))
{ // We have the thumbnail, just serve it:
header('Content-Type: image/jpeg');
echo file_get_contents($cacheDir .'/'. $thumbname);
return;
}
// We may also serve a blank image (if service did not respond)
$blankname=hash('sha1',$_GET['url']).'.gif';
if (is_file($cacheDir .'/'. $blankname))
{
header('Content-Type: image/gif');
echo file_get_contents($cacheDir .'/'. $blankname);
return;
}
// Otherwise, generate the thumbnail.
$url = $_GET['url'];
$domain = parse_url($url,PHP_URL_HOST);
if ($domain=='flickr.com' || endsWith($domain,'.flickr.com'))
{
// Crude replacement to handle new flickr domain policy (They prefer www. now)
$url = str_replace('http://flickr.com/','http://www.flickr.com/',$url);
// Is this a link to an image, or to a flickr page ?
$imageurl='';
if (endsWith(parse_url($url, PHP_URL_PATH), '.jpg'))
{ // This is a direct link to an image. e.g. http://farm1.staticflickr.com/5/5921913_ac83ed27bd_o.jpg
preg_match('!(http://farm\d+\.staticflickr\.com/\d+/\d+_\w+_)\w.jpg!',$url,$matches);
if (!empty($matches[1])) $imageurl=$matches[1].'m.jpg';
}
else // This is a flickr page (html)
{
// Get the flickr html page.
list($headers, $content) = get_http_response($url, 20);
if (strpos($headers[0], '200 OK') !== false)
{
// flickr now nicely provides the URL of the thumbnail in each flickr page.
preg_match('!<link rel=\"image_src\" href=\"(.+?)\"!', $content, $matches);
if (!empty($matches[1])) $imageurl=$matches[1];
// In albums (and some other pages), the link rel="image_src" is not provided,
// but flickr provides:
// <meta property="og:image" content="http://farm4.staticflickr.com/3398/3239339068_25d13535ff_z.jpg" />
if ($imageurl=='')
{
preg_match('!<meta property=\"og:image\" content=\"(.+?)\"!', $content, $matches);
if (!empty($matches[1])) $imageurl=$matches[1];
}
}
}
if ($imageurl!='')
{ // Let's download the image.
// Image is 240x120, so 10 seconds to download should be enough.
list($headers, $content) = get_http_response($imageurl, 10);
if (strpos($headers[0], '200 OK') !== false) {
// Save image to cache.
file_put_contents($cacheDir .'/'. $thumbname, $content);
header('Content-Type: image/jpeg');
echo $content;
return;
}
}
}
elseif ($domain=='vimeo.com' )
{
// This is more complex: we have to perform a HTTP request, then parse the result.
// Maybe we should deport this to JavaScript ? Example: http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo/4285098#4285098
$vid = substr(parse_url($url,PHP_URL_PATH),1);
list($headers, $content) = get_http_response('https://vimeo.com/api/v2/video/'.escape($vid).'.php', 5);
if (strpos($headers[0], '200 OK') !== false) {
$t = unserialize($content);
$imageurl = $t[0]['thumbnail_medium'];
// Then we download the image and serve it to our client.
list($headers, $content) = get_http_response($imageurl, 10);
if (strpos($headers[0], '200 OK') !== false) {
// Save image to cache.
file_put_contents($cacheDir .'/'. $thumbname, $content);
header('Content-Type: image/jpeg');
echo $content;
return;
}
}
}
elseif ($domain=='ted.com' || endsWith($domain,'.ted.com'))
{
// The thumbnail for TED talks is located in the <link rel="image_src" [...]> tag on that page
// http://www.ted.com/talks/mikko_hypponen_fighting_viruses_defending_the_net.html
// <link rel="image_src" href="http://images.ted.com/images/ted/28bced335898ba54d4441809c5b1112ffaf36781_389x292.jpg" />
list($headers, $content) = get_http_response($url, 5);
if (strpos($headers[0], '200 OK') !== false) {
// Extract the link to the thumbnail
preg_match('!link rel="image_src" href="(http://images.ted.com/images/ted/.+_\d+x\d+\.jpg)"!', $content, $matches);
if (!empty($matches[1]))
{ // Let's download the image.
$imageurl=$matches[1];
// No control on image size, so wait long enough
list($headers, $content) = get_http_response($imageurl, 20);
if (strpos($headers[0], '200 OK') !== false) {
$filepath = $cacheDir .'/'. $thumbname;
file_put_contents($filepath, $content); // Save image to cache.
if (resizeImage($filepath))
{
header('Content-Type: image/jpeg');
echo file_get_contents($filepath);
return;
}
}
}
}
}
elseif ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com'))
{
// There is no thumbnail available for xkcd comics, so download the whole image and resize it.
// http://xkcd.com/327/
// <img src="http://imgs.xkcd.com/comics/exploits_of_a_mom.png" title="<BLABLA>" alt="<BLABLA>" />
list($headers, $content) = get_http_response($url, 5);
if (strpos($headers[0], '200 OK') !== false) {
// Extract the link to the thumbnail
preg_match('!<img src="(http://imgs.xkcd.com/comics/.*)" title="[^s]!', $content, $matches);
if (!empty($matches[1]))
{ // Let's download the image.
$imageurl=$matches[1];
// No control on image size, so wait long enough
list($headers, $content) = get_http_response($imageurl, 20);
if (strpos($headers[0], '200 OK') !== false) {
$filepath = $cacheDir.'/'.$thumbname;
// Save image to cache.
file_put_contents($filepath, $content);
if (resizeImage($filepath))
{
header('Content-Type: image/jpeg');
echo file_get_contents($filepath);
return;
}
}
}
}
}
else
{
// For all other domains, we try to download the image and make a thumbnail.
// We allow 30 seconds max to download (and downloads are limited to 4 Mb)
list($headers, $content) = get_http_response($url, 30);
if (strpos($headers[0], '200 OK') !== false) {
$filepath = $cacheDir .'/'.$thumbname;
// Save image to cache.
file_put_contents($filepath, $content);
if (resizeImage($filepath))
{
header('Content-Type: image/jpeg');
echo file_get_contents($filepath);
return;
}
}
}
// Otherwise, return an empty image (8x8 transparent gif)
$blankgif = base64_decode('R0lGODlhCAAIAIAAAP///////yH5BAEKAAEALAAAAAAIAAgAAAIHjI+py+1dAAA7');
// Also put something in cache so that this URL is not requested twice.
file_put_contents($cacheDir .'/'. $blankname, $blankgif);
header('Content-Type: image/gif');
echo $blankgif;
}
// Make a thumbnail of the image (to width: 120 pixels)
// Returns true if success, false otherwise.
function resizeImage($filepath)
{
if (!function_exists('imagecreatefromjpeg')) return false; // GD not present: no thumbnail possible.
// Trick: some stupid people rename GIF as JPEG... or else.
// So we really try to open each image type whatever the extension is.
$header=file_get_contents($filepath,false,NULL,0,256); // Read first 256 bytes and try to sniff file type.
$im=false;
$i=strpos($header,'GIF8'); if (($i!==false) && ($i==0)) $im = imagecreatefromgif($filepath); // Well this is crude, but it should be enough.
$i=strpos($header,'PNG'); if (($i!==false) && ($i==1)) $im = imagecreatefrompng($filepath);
$i=strpos($header,'JFIF'); if ($i!==false) $im = imagecreatefromjpeg($filepath);
if (!$im) return false; // Unable to open image (corrupted or not an image)
$w = imagesx($im);
$h = imagesy($im);
$ystart = 0; $yheight=$h;
if ($h>$w) { $ystart= ($h/2)-($w/2); $yheight=$w/2; }
$nw = 120; // Desired width
$nh = min(floor(($h*$nw)/$w),120); // Compute new width/height, but maximum 120 pixels height.
// Resize image:
$im2 = imagecreatetruecolor($nw,$nh);
imagecopyresampled($im2, $im, 0, 0, 0, $ystart, $nw, $nh, $w, $yheight);
imageinterlace($im2,true); // For progressive JPEG.
$tempname=$filepath.'_TEMP.jpg';
imagejpeg($im2, $tempname, 90);
imagedestroy($im);
imagedestroy($im2);
unlink($filepath);
rename($tempname,$filepath); // Overwrite original picture with thumbnail.
return true;
}
if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=genthumbnail')) { genThumbnail($conf); exit; } // Thumbnail generation/cache does not need the link database.
if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) { showDailyRSS($conf); exit; }
if (!isset($_SESSION['LINKS_PER_PAGE'])) {
$_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);

View file

@ -39,6 +39,7 @@ pages:
- Continuous integration tools: Continuous-integration-tools.md
- GnuPG signature: GnuPG-signature.md
- Directory structure: Directory-structure.md
- Link Structure: Link-structure.md
- 3rd party libraries: 3rd-party-libraries.md
- Plugin System: Plugin-System.md
- Release Shaarli: Release-Shaarli.md

114
tests/ThumbnailerTest.php Normal file
View file

@ -0,0 +1,114 @@
<?php
namespace Shaarli;
use PHPUnit\Framework\TestCase;
use Shaarli\Config\ConfigManager;
use WebThumbnailer\Application\ConfigManager as WTConfigManager;
/**
* Class ThumbnailerTest
*
* We only make 1 thumb test because:
*
* 1. the thumbnailer library is itself tested
* 2. we don't want to make too many external requests during the tests
*/
class ThumbnailerTest extends TestCase
{
const WIDTH = 190;
const HEIGHT = 210;
/**
* @var Thumbnailer;
*/
protected $thumbnailer;
/**
* @var ConfigManager
*/
protected $conf;
public function setUp()
{
$this->conf = new ConfigManager('tests/utils/config/configJson');
$this->conf->set('thumbnails.mode', Thumbnailer::MODE_ALL);
$this->conf->set('thumbnails.width', self::WIDTH);
$this->conf->set('thumbnails.height', self::HEIGHT);
$this->conf->set('dev.debug', true);
$this->thumbnailer = new Thumbnailer($this->conf);
// cache files in the sandbox
WTConfigManager::addFile('tests/utils/config/wt.json');
}
public function tearDown()
{
$this->rrmdirContent('sandbox/');
}
/**
* Test a thumbnail with a custom size in 'all' mode.
*/
public function testThumbnailAllValid()
{
$thumb = $this->thumbnailer->get('https://github.com/shaarli/Shaarli/');
$this->assertNotFalse($thumb);
$image = imagecreatefromstring(file_get_contents($thumb));
$this->assertEquals(self::WIDTH, imagesx($image));
$this->assertEquals(self::HEIGHT, imagesy($image));
}
/**
* Test a thumbnail with a custom size in 'common' mode.
*/
public function testThumbnailCommonValid()
{
$this->conf->set('thumbnails.mode', Thumbnailer::MODE_COMMON);
$thumb = $this->thumbnailer->get('https://imgur.com/jlFgGpe');
$this->assertNotFalse($thumb);
$image = imagecreatefromstring(file_get_contents($thumb));
$this->assertEquals(self::WIDTH, imagesx($image));
$this->assertEquals(self::HEIGHT, imagesy($image));
}
/**
* Test a thumbnail in 'common' mode which isn't include in common websites.
*/
public function testThumbnailCommonInvalid()
{
$this->conf->set('thumbnails.mode', Thumbnailer::MODE_COMMON);
$thumb = $this->thumbnailer->get('https://github.com/shaarli/Shaarli/');
$this->assertFalse($thumb);
}
/**
* Test a thumbnail that can't be retrieved.
*/
public function testThumbnailNotValid()
{
$oldlog = ini_get('error_log');
ini_set('error_log', '/dev/null');
$thumbnailer = new Thumbnailer(new ConfigManager());
$thumb = $thumbnailer->get('nope');
$this->assertFalse($thumb);
ini_set('error_log', $oldlog);
}
protected function rrmdirContent($dir) {
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
if (is_dir($dir."/".$object))
$this->rrmdirContent($dir."/".$object);
else
unlink($dir."/".$object);
}
}
}
}
}

View file

@ -2,6 +2,7 @@
use Shaarli\Config\ConfigJson;
use Shaarli\Config\ConfigManager;
use Shaarli\Config\ConfigPhp;
use Shaarli\Thumbnailer;
require_once 'tests/Updater/DummyUpdater.php';
require_once 'inc/rain.tpl.class.php';
@ -20,7 +21,7 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
/**
* @var string Config file path (without extension).
*/
protected static $configFile = 'tests/utils/config/configJson';
protected static $configFile = 'sandbox/config';
/**
* @var ConfigManager
@ -32,6 +33,7 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
*/
public function setUp()
{
copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
$this->conf = new ConfigManager(self::$configFile);
}
@ -684,4 +686,50 @@ public function testUpdateMethodDownloadSizeAndTimeoutConfOnlyTimeout()
$this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
$this->assertEquals(3, $this->conf->get('general.download_timeout'));
}
/**
* Test updateMethodWebThumbnailer with thumbnails enabled.
*/
public function testUpdateMethodWebThumbnailerEnabled()
{
$this->conf->remove('thumbnails');
$this->conf->set('thumbnail.enable_thumbnails', true);
$updater = new Updater([], [], $this->conf, true, $_SESSION);
$this->assertTrue($updater->updateMethodWebThumbnailer());
$this->assertFalse($this->conf->exists('thumbnail'));
$this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode'));
$this->assertEquals(125, $this->conf->get('thumbnails.width'));
$this->assertEquals(90, $this->conf->get('thumbnails.height'));
$this->assertContains('You have enabled or changed thumbnails', $_SESSION['warnings'][0]);
}
/**
* Test updateMethodWebThumbnailer with thumbnails disabled.
*/
public function testUpdateMethodWebThumbnailerDisabled()
{
$this->conf->remove('thumbnails');
$this->conf->set('thumbnail.enable_thumbnails', false);
$updater = new Updater([], [], $this->conf, true, $_SESSION);
$this->assertTrue($updater->updateMethodWebThumbnailer());
$this->assertFalse($this->conf->exists('thumbnail'));
$this->assertEquals(Thumbnailer::MODE_NONE, $this->conf->get('thumbnails.mode'));
$this->assertEquals(125, $this->conf->get('thumbnails.width'));
$this->assertEquals(90, $this->conf->get('thumbnails.height'));
$this->assertTrue(empty($_SESSION['warnings']));
}
/**
* Test updateMethodWebThumbnailer with thumbnails disabled.
*/
public function testUpdateMethodWebThumbnailerNothingToDo()
{
$updater = new Updater([], [], $this->conf, true, $_SESSION);
$this->assertTrue($updater->updateMethodWebThumbnailer());
$this->assertFalse($this->conf->exists('thumbnail'));
$this->assertEquals(Thumbnailer::MODE_COMMON, $this->conf->get('thumbnails.mode'));
$this->assertEquals(90, $this->conf->get('thumbnails.width'));
$this->assertEquals(53, $this->conf->get('thumbnails.height'));
$this->assertTrue(empty($_SESSION['warnings']));
}
}

View file

@ -81,6 +81,18 @@ public function testSetWriteGetNested()
$this->assertEquals('testSetWriteGetNested', $this->conf->get('foo.bar.key.stuff'));
}
public function testSetDeleteNested()
{
$this->conf->set('foo.bar.key.stuff', 'testSetDeleteNested');
$this->assertTrue($this->conf->exists('foo.bar'));
$this->assertTrue($this->conf->exists('foo.bar.key.stuff'));
$this->assertEquals('testSetDeleteNested', $this->conf->get('foo.bar.key.stuff'));
$this->conf->remove('foo.bar');
$this->assertFalse($this->conf->exists('foo.bar.key.stuff'));
$this->assertFalse($this->conf->exists('foo.bar'));
}
/**
* Set with an empty key.
*
@ -103,6 +115,17 @@ public function testSetArrayKey()
$this->conf->set(array('foo' => 'bar'), 'stuff');
}
/**
* Remove with an empty key.
*
* @expectedException \Exception
* @expectedExceptionMessageRegExp #^Invalid setting key parameter. String expected, got.*#
*/
public function testRmoveEmptyKey()
{
$this->conf->remove('');
}
/**
* Try to write the config without mandatory parameter (e.g. 'login').
*

View file

@ -1,35 +1,84 @@
<?php /*
{
"credentials": {
"login":"root",
"hash":"hash",
"salt":"salt"
"login": "root",
"hash": "hash",
"salt": "salt"
},
"security": {
"session_protection_disabled":false
"session_protection_disabled": false,
"ban_after": 4,
"ban_duration": 1800,
"open_shaarli": false,
"allowed_protocols": [
"ftp",
"ftps",
"magnet"
]
},
"general": {
"timezone":"Europe\/Paris",
"timezone": "Europe\/Paris",
"title": "Shaarli",
"header_link": "?"
"header_link": "?",
"links_per_page": 20,
"enabled_plugins": [
"qrcode"
],
"default_note_title": "Note: "
},
"privacy": {
"default_private_links":true
"default_private_links": true,
"hide_public_links": false,
"force_login": false,
"hide_timestamps": false,
"remember_user_default": true
},
"redirector": {
"url":"lala"
"url": "lala",
"encode_url": true
},
"config": {
"foo": "bar"
},
"resource": {
"datastore": "tests\/utils\/config\/datastore.php",
"data_dir": "sandbox/",
"raintpl_tpl": "tpl/"
"data_dir": "sandbox\/",
"raintpl_tpl": "tpl\/",
"config": "data\/config.php",
"ban_file": "data\/ipbans.php",
"updates": "data\/updates.txt",
"log": "data\/log.txt",
"update_check": "data\/lastupdatecheck.txt",
"history": "data\/history.php",
"theme": "default",
"raintpl_tmp": "tmp\/",
"thumbnails_cache": "cache",
"page_cache": "pagecache"
},
"plugins": {
"WALLABAG_VERSION": 1
},
"dev": {
"debug": true
},
"updates": {
"check_updates": false,
"check_updates_branch": "stable",
"check_updates_interval": 86400
},
"feed": {
"rss_permalinks": true,
"show_atom": true
},
"translation": {
"language": "auto",
"mode": "php",
"extensions": []
},
"thumbnails": {
"mode": "common",
"width": 90,
"height": 53
}
}
*/ ?>

View file

@ -0,0 +1,12 @@
{
"settings": {
"default": {
"_comment": "infinite cache",
"cache_duration": -1,
"timeout": 10
},
"path": {
"cache": "sandbox/"
}
}
}

View file

@ -242,6 +242,37 @@ <h2 class="window-title">{'Configure'|t}</h2>
</div>
</div>
</div>
<div class="pure-g">
<div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile}">
<div class="form-label">
<label for="enableThumbnails">
<span class="label-name">{'Enable thumbnails'|t}</span><br>
<span class="label-desc">
{if="! $gd_enabled"}
{'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
{elseif="$thumbnails_enabled"}
<a href="?do=thumbs_update">{'Synchronize thumbnails'|t}</a>
{/if}
</span>
</label>
</div>
</div>
<div class="pure-u-lg-{$ratioInput} pure-u-{$ratioInputMobile}">
<div class="form-input">
<select name="enableThumbnails" id="enableThumbnails" class="align">
<option value="all" {if="$thumbnails_mode=='all'"}selected{/if}>
{'All'|t}
</option>
<option value="common" {if="$thumbnails_mode=='common'"}selected{/if}>
{'Only common media hosts'|t}
</option>
<option value="none" {if="$thumbnails_mode=='none'"}selected{/if}>
{'None'|t}
</option>
</select>
</div>
</div>
</div>
<div class="center">
<input type="submit" value="{'Save'|t}" name="save">
</div>

View file

@ -131,9 +131,17 @@
<div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
<div class="linklist-item-title">
{$thumb=thumbnail($value.url)}
{if="$thumb!=false"}
<div class="linklist-item-thumbnail">{$thumb}</div>
{if="$thumbnails_enabled && !empty($value.thumbnail)"}
<div class="linklist-item-thumbnail" style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;">
<div class="thumbnail">
<a href="{$value.real_url}">
{ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
<img data-src="{$value.thumbnail}#" class="b-lazy"
src="#"
alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
</a>
</div>
</div>
{/if}
{if="$is_logged_in"}
@ -268,5 +276,6 @@ <h2>
</div>
{include="page.footer"}
<script src="js/thumbnails.min.js?v={$version_hash}"></script>
</body>
</html>

View file

@ -30,9 +30,11 @@
<li class="pure-menu-item" id="shaarli-menu-tags">
<a href="?do=tagcloud" class="pure-menu-link">{'Tag cloud'|t}</a>
</li>
<li class="pure-menu-item" id="shaarli-menu-picwall">
<a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a>
</li>
{if="$thumbnails_enabled"}
<li class="pure-menu-item" id="shaarli-menu-picwall">
<a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a>
</li>
{/if}
<li class="pure-menu-item" id="shaarli-menu-daily">
<a href="?do=daily" class="pure-menu-link">{'Daily'|t}</a>
</li>
@ -169,4 +171,18 @@
</div>
{/if}
{if="!empty($global_warnings) && $is_logged_in"}
<div class="pure-g pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
<div class="pure-u-2-24"></div>
<div class="pure-u-20-24">
{loop="global_warnings"}
<p>{$value}</p>
{/loop}
</div>
<div class="pure-u-2-24">
<i class="fa fa-times pure-alert-close"></i>
</div>
</div>
{/if}
<div class="clear"></div>

View file

@ -5,41 +5,61 @@
</head>
<body>
{include="page.header"}
<div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
{$countPics=count($linksToDisplay)}
<h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
<div id="plugin_zone_start_picwall" class="plugin_zone">
{loop="$plugin_start_zone"}
{$value}
{/loop}
</div>
<div id="picwall_container" class="picwall-container">
{loop="$linksToDisplay"}
<div class="picwall-pictureframe">
{$value.thumbnail}<a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
{loop="$value.picwall_plugin"}
{$value}
{/loop}
</div>
{/loop}
<div class="clear"></div>
</div>
<div id="plugin_zone_end_picwall" class="plugin_zone">
{loop="$plugin_end_zone"}
{$value}
{/loop}
</div>
{if="!$thumbnails_enabled"}
<div class="pure-g pure-alert pure-alert-warning page-single-alert">
<div class="pure-u-1 center">
{'Picture wall unavailable (thumbnails are disabled).'|t}
</div>
</div>
{else}
{if="count($linksToDisplay)===0 && $is_logged_in"}
<div class="pure-g pure-alert pure-alert-warning page-single-alert">
<div class="pure-u-1 center">
{'There is no cached thumbnail. Try to <a href="?do=thumbs_update">synchronize them</a>.'|t}
</div>
</div>
{/if}
<div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
{$countPics=count($linksToDisplay)}
<h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
<div id="plugin_zone_start_picwall" class="plugin_zone">
{loop="$plugin_start_zone"}
{$value}
{/loop}
</div>
<div id="picwall-container" class="picwall-container">
{loop="$linksToDisplay"}
<div class="picwall-pictureframe">
{ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
<img data-src="{$value.thumbnail}#" class="b-lazy"
src="#"
alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
<a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
{loop="$value.picwall_plugin"}
{$value}
{/loop}
</div>
{/loop}
<div class="clear"></div>
</div>
<div id="plugin_zone_end_picwall" class="plugin_zone">
{loop="$plugin_end_zone"}
{$value}
{/loop}
</div>
</div>
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
</div>
{/if}
{include="page.footer"}
<script src="js/picwall.min.js?v={$version_hash}"></script>
<script src="js/thumbnails.min.js?v={$version_hash}"></script>
</body>
</html>

View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
{include="includes"}
</head>
<body>
{include="page.header"}
<div class="pure-g thumbnails-page-container">
<div class="pure-u-lg-1-3 pure-u-1-24"></div>
<div class="pure-u-lg-1-3 pure-u-22-24 page-form page-form-light">
<h2 class="window-title">{'Thumbnails update'|t}</h2>
<div class="pure-g">
<div class="pure-u-lg-1-3 pure-u-1-24"></div>
<div class="pure-u-lg-1-3 pure-u-22-24">
<div class="thumbnail-placeholder" style="width: {$thumbnails_width}px; height: {$thumbnails_height}px;"></div>
</div>
</div>
<div class="pure-g">
<div class="pure-u-1-12"></div>
<div class="pure-u-5-6">
<div class="thumbnail-link-title"></div>
<div class="progressbar">
<div></div>
</div>
</div>
</div>
<div class="pure-g">
<div class="pure-u-lg-1-3 pure-u-1-24"></div>
<div class="pure-u-lg-1-3 pure-u-22-24">
<div class="progress-counter">
<span class="progress-current">0</span> / <span class="progress-total">{$ids|count}</span>
</div>
</div>
</div>
<input type="hidden" name="ids" value="{function="implode($ids, ',')"}" />
</div>
</div>
{include="page.footer"}
<script src="js/thumbnails_update.min.js?v={$version_hash}"></script>
</body>
</html>

View file

@ -45,6 +45,14 @@ <h2 class="window-title">{'Settings'|t}</h2>
</a>
</div>
{if="$thumbnails_enabled"}
<div class="tools-item">
<a href="?do=thumbs_update" title="{'Synchronize all link thumbnails'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
</a>
</div>
{/if}
{loop="$tools_plugin"}
<div class="tools-item">
{$value}

View file

@ -128,6 +128,29 @@
<input type="text" name="apiSecret" id="apiSecret" size="50" value="{$api_secret}" />
</td>
</tr>
<tr>
<td valign="top"><b>Enable thumbnails</b></td>
<td>
<select name="enableThumbnails" id="enableThumbnails" class="align">
<option value="all" {if="$thumbnails_mode=='all'"}selected{/if}>
{'All'|t}
</option>
<option value="common" {if="$thumbnails_mode=='common'"}selected{/if}>
{'Only common media hosts'|t}
</option>
<option value="none" {if="$thumbnails_mode=='none'"}selected{/if}>
{'None'|t}
</option>
</select>
<label for="enableThumbnails">
{if="! $gd_enabled"}
{'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
{elseif="$thumbnails_enabled"}
<a href="?do=thumbs_update">{'Synchonize thumbnails'|t}</a>
{/if}
</label>
</td>
</tr>
<tr>
<td></td>

View file

@ -80,7 +80,16 @@
{loop="$links"}
<li{if="$value.class"} class="{$value.class}"{/if}>
<a id="{$value.shorturl}"></a>
<div class="thumbnail">{$value.url|thumbnail}</div>
{if="$thumbnails_enabled && !empty($value.thumbnail)"}
<div class="thumbnail">
<a href="{$value.real_url}">
{ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
<img data-src="{$value.thumbnail}#" class="b-lazy"
src="#"
alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
</a>
</div>
{/if}
<div class="linkcontainer">
{if="$is_logged_in"}
<div class="linkeditbuttons">
@ -145,6 +154,7 @@
</div>
{include="page.footer"}
<script src="js/thumbnails.min.js"></script>
</body>
</html>

View file

@ -15,7 +15,11 @@
<div id="picwall_container">
{loop="$linksToDisplay"}
<div class="picwall_pictureframe">
{$value.thumbnail}<a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
{ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
<img data-src="{$value.thumbnail}#" class="b-lazy"
src="#"
alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
<a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
{loop="$value.picwall_plugin"}
{$value}
{/loop}
@ -34,6 +38,6 @@
{include="page.footer"}
<script src="js/picwall.min.js"></script>
<script src="js/thumbnails.min.js"></script>
</body>
</html>

View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>{include="includes"}</head>
<body>
<div id="pageheader">
{include="page.header"}
</div>
<div class="center thumbnails-update-container">
<div class="thumbnail-placeholder" style="width: {$thumbnails_width}px; height: {$thumbnails_height}px;"></div>
<div class="thumbnail-link-title"></div>
<div class="progressbar">
<div></div>
</div>
<div class="progress-counter">
<span class="progress-current">0</span> / <span class="progress-total">{$ids|count}</span>
</div>
</div>
<input type="hidden" name="ids" value="{function="implode($ids, ',')"}" />
{include="page.footer"}
<script src="js/thumbnails_update.min.js?v={$version_hash}"></script>
</body>
</html>

View file

@ -23,7 +23,8 @@ const extractCssVintage = new ExtractTextPlugin({
module.exports = [
{
entry: {
picwall: './assets/common/js/picwall.js',
thumbnails: './assets/common/js/thumbnails.js',
thumbnails_update: './assets/common/js/thumbnails-update.js',
pluginsadmin: './assets/default/js/plugins-admin.js',
shaarli: [
'./assets/default/js/base.js',
@ -96,7 +97,8 @@ module.exports = [
'./assets/vintage/css/reset.css',
'./assets/vintage/css/shaarli.css',
].concat(glob.sync('./assets/vintage/img/*')),
picwall: './assets/common/js/picwall.js',
thumbnails: './assets/common/js/thumbnails.js',
thumbnails_update: './assets/common/js/thumbnails-update.js',
},
output: {
filename: '[name].min.js',