aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/PageBuilder.php37
-rw-r--r--application/Router.php12
-rw-r--r--application/Thumbnailer.php127
-rw-r--r--application/Updater.php36
-rw-r--r--application/config/ConfigManager.php56
-rw-r--r--assets/common/js/picwall.js10
-rw-r--r--assets/common/js/thumbnails-update.js51
-rw-r--r--assets/common/js/thumbnails.js7
-rw-r--r--assets/default/scss/shaarli.scss71
-rw-r--r--assets/vintage/css/shaarli.css48
-rw-r--r--composer.json1
-rw-r--r--composer.lock334
-rw-r--r--doc/md/Link-structure.md18
-rw-r--r--doc/md/Server-configuration.md2
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po356
-rw-r--r--inc/web-thumbnailer.json13
-rw-r--r--index.php527
-rw-r--r--mkdocs.yml1
-rw-r--r--tests/ThumbnailerTest.php114
-rw-r--r--tests/Updater/UpdaterTest.php50
-rw-r--r--tests/config/ConfigManagerTest.php23
-rw-r--r--tests/utils/config/configJson.json.php71
-rw-r--r--tests/utils/config/wt.json12
-rw-r--r--tpl/default/configure.html31
-rw-r--r--tpl/default/linklist.html15
-rw-r--r--tpl/default/page.header.html22
-rw-r--r--tpl/default/picwall.html74
-rw-r--r--tpl/default/thumbnails.html48
-rw-r--r--tpl/default/tools.html8
-rw-r--r--tpl/vintage/configure.html23
-rw-r--r--tpl/vintage/linklist.html12
-rw-r--r--tpl/vintage/picwall.html8
-rw-r--r--tpl/vintage/thumbnails.html28
-rw-r--r--webpack.config.js6
34 files changed, 1489 insertions, 763 deletions
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index a4483870..b1abe0d0 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2 2
3use Shaarli\Config\ConfigManager; 3use Shaarli\Config\ConfigManager;
4use Shaarli\Thumbnailer;
4 5
5/** 6/**
6 * This class is in charge of building the final page. 7 * This class is in charge of building the final page.
@@ -22,10 +23,20 @@ class PageBuilder
22 protected $conf; 23 protected $conf;
23 24
24 /** 25 /**
26 * @var array $_SESSION
27 */
28 protected $session;
29
30 /**
25 * @var LinkDB $linkDB instance. 31 * @var LinkDB $linkDB instance.
26 */ 32 */
27 protected $linkDB; 33 protected $linkDB;
28 34
35 /**
36 * @var null|string XSRF token
37 */
38 protected $token;
39
29 /** @var bool $isLoggedIn Whether the user is logged in **/ 40 /** @var bool $isLoggedIn Whether the user is logged in **/
30 protected $isLoggedIn = false; 41 protected $isLoggedIn = false;
31 42
@@ -33,14 +44,17 @@ class PageBuilder
33 * PageBuilder constructor. 44 * PageBuilder constructor.
34 * $tpl is initialized at false for lazy loading. 45 * $tpl is initialized at false for lazy loading.
35 * 46 *
36 * @param ConfigManager $conf Configuration Manager instance (reference). 47 * @param ConfigManager $conf Configuration Manager instance (reference).
37 * @param LinkDB $linkDB instance. 48 * @param array $session $_SESSION array
38 * @param string $token Session token 49 * @param LinkDB $linkDB instance.
50 * @param string $token Session token
51 * @param bool $isLoggedIn
39 */ 52 */
40 public function __construct(&$conf, $linkDB = null, $token = null, $isLoggedIn = false) 53 public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
41 { 54 {
42 $this->tpl = false; 55 $this->tpl = false;
43 $this->conf = $conf; 56 $this->conf = $conf;
57 $this->session = $session;
44 $this->linkDB = $linkDB; 58 $this->linkDB = $linkDB;
45 $this->token = $token; 59 $this->token = $token;
46 $this->isLoggedIn = $isLoggedIn; 60 $this->isLoggedIn = $isLoggedIn;
@@ -105,6 +119,19 @@ class PageBuilder
105 if ($this->linkDB !== null) { 119 if ($this->linkDB !== null) {
106 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); 120 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
107 } 121 }
122
123 $this->tpl->assign(
124 'thumbnails_enabled',
125 $this->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
126 );
127 $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
128 $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
129
130 if (! empty($_SESSION['warnings'])) {
131 $this->tpl->assign('global_warnings', $_SESSION['warnings']);
132 unset($_SESSION['warnings']);
133 }
134
108 // To be removed with a proper theme configuration. 135 // To be removed with a proper theme configuration.
109 $this->tpl->assign('conf', $this->conf); 136 $this->tpl->assign('conf', $this->conf);
110 } 137 }
diff --git a/application/Router.php b/application/Router.php
index 4df0387c..bf86b884 100644
--- a/application/Router.php
+++ b/application/Router.php
@@ -7,6 +7,8 @@
7 */ 7 */
8class Router 8class Router
9{ 9{
10 public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
11
10 public static $PAGE_LOGIN = 'login'; 12 public static $PAGE_LOGIN = 'login';
11 13
12 public static $PAGE_PICWALL = 'picwall'; 14 public static $PAGE_PICWALL = 'picwall';
@@ -47,6 +49,8 @@ class Router
47 49
48 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin'; 50 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
49 51
52 public static $PAGE_THUMBS_UPDATE = 'thumbs_update';
53
50 public static $GET_TOKEN = 'token'; 54 public static $GET_TOKEN = 'token';
51 55
52 /** 56 /**
@@ -101,6 +105,14 @@ class Router
101 return self::$PAGE_FEED_RSS; 105 return self::$PAGE_FEED_RSS;
102 } 106 }
103 107
108 if (startsWith($query, 'do='. self::$PAGE_THUMBS_UPDATE)) {
109 return self::$PAGE_THUMBS_UPDATE;
110 }
111
112 if (startsWith($query, 'do='. self::$AJAX_THUMB_UPDATE)) {
113 return self::$AJAX_THUMB_UPDATE;
114 }
115
104 // At this point, only loggedin pages. 116 // At this point, only loggedin pages.
105 if (!$loggedIn) { 117 if (!$loggedIn) {
106 return self::$PAGE_LINKLIST; 118 return self::$PAGE_LINKLIST;
diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php
new file mode 100644
index 00000000..7d0d9c33
--- /dev/null
+++ b/application/Thumbnailer.php
@@ -0,0 +1,127 @@
1<?php
2
3namespace Shaarli;
4
5use Shaarli\Config\ConfigManager;
6use WebThumbnailer\Exception\WebThumbnailerException;
7use WebThumbnailer\WebThumbnailer;
8use WebThumbnailer\Application\ConfigManager as WTConfigManager;
9
10/**
11 * Class Thumbnailer
12 *
13 * Utility class used to retrieve thumbnails using web-thumbnailer dependency.
14 */
15class Thumbnailer
16{
17 const COMMON_MEDIA_DOMAINS = [
18 'imgur.com',
19 'flickr.com',
20 'youtube.com',
21 'wikimedia.org',
22 'redd.it',
23 'gfycat.com',
24 'media.giphy.com',
25 'twitter.com',
26 'twimg.com',
27 'instagram.com',
28 'pinterest.com',
29 'pinterest.fr',
30 'tumblr.com',
31 'deviantart.com',
32 ];
33
34 const MODE_ALL = 'all';
35 const MODE_COMMON = 'common';
36 const MODE_NONE = 'none';
37
38 /**
39 * @var WebThumbnailer instance.
40 */
41 protected $wt;
42
43 /**
44 * @var ConfigManager instance.
45 */
46 protected $conf;
47
48 /**
49 * Thumbnailer constructor.
50 *
51 * @param ConfigManager $conf instance.
52 */
53 public function __construct($conf)
54 {
55 $this->conf = $conf;
56
57 if (! $this->checkRequirements()) {
58 $this->conf->set('thumbnails.enabled', false);
59 $this->conf->write(true);
60 // TODO: create a proper error handling system able to catch exceptions...
61 die(t('php-gd extension must be loaded to use thumbnails. Thumbnails are now disabled. Please reload the page.'));
62 }
63
64 $this->wt = new WebThumbnailer();
65 WTConfigManager::addFile('inc/web-thumbnailer.json');
66 $this->wt->maxWidth($this->conf->get('thumbnails.width'))
67 ->maxHeight($this->conf->get('thumbnails.height'))
68 ->crop(true)
69 ->debug($this->conf->get('dev.debug', false));
70 }
71
72 /**
73 * Retrieve a thumbnail for given URL
74 *
75 * @param string $url where to look for a thumbnail.
76 *
77 * @return bool|string The thumbnail relative cache file path, or false if none has been found.
78 */
79 public function get($url)
80 {
81 if ($this->conf->get('thumbnails.mode') === self::MODE_COMMON
82 && ! $this->isCommonMediaOrImage($url)
83 ) {
84 return false;
85 }
86
87 try {
88 return $this->wt->thumbnail($url);
89 } catch (WebThumbnailerException $e) {
90 // Exceptions are only thrown in debug mode.
91 error_log(get_class($e) . ': ' . $e->getMessage());
92 }
93 return false;
94 }
95
96 /**
97 * We check weather the given URL is from a common media domain,
98 * or if the file extension is an image.
99 *
100 * @param string $url to check
101 *
102 * @return bool true if it's an image or from a common media domain, false otherwise.
103 */
104 public function isCommonMediaOrImage($url)
105 {
106 foreach (self::COMMON_MEDIA_DOMAINS as $domain) {
107 if (strpos($url, $domain) !== false) {
108 return true;
109 }
110 }
111
112 if (endsWith($url, '.jpg') || endsWith($url, '.png') || endsWith($url, '.jpeg')) {
113 return true;
114 }
115
116 return false;
117 }
118
119 /**
120 * Make sure that requirements are match to use thumbnails:
121 * - php-gd is loaded
122 */
123 protected function checkRequirements()
124 {
125 return extension_loaded('gd');
126 }
127}
diff --git a/application/Updater.php b/application/Updater.php
index dece2c02..c2aa1568 100644
--- a/application/Updater.php
+++ b/application/Updater.php
@@ -2,6 +2,7 @@
2use Shaarli\Config\ConfigJson; 2use Shaarli\Config\ConfigJson;
3use Shaarli\Config\ConfigPhp; 3use Shaarli\Config\ConfigPhp;
4use Shaarli\Config\ConfigManager; 4use Shaarli\Config\ConfigManager;
5use Shaarli\Thumbnailer;
5 6
6/** 7/**
7 * Class Updater. 8 * Class Updater.
@@ -31,6 +32,11 @@ class Updater
31 protected $isLoggedIn; 32 protected $isLoggedIn;
32 33
33 /** 34 /**
35 * @var array $_SESSION
36 */
37 protected $session;
38
39 /**
34 * @var ReflectionMethod[] List of current class methods. 40 * @var ReflectionMethod[] List of current class methods.
35 */ 41 */
36 protected $methods; 42 protected $methods;
@@ -42,13 +48,17 @@ class Updater
42 * @param LinkDB $linkDB LinkDB instance. 48 * @param LinkDB $linkDB LinkDB instance.
43 * @param ConfigManager $conf Configuration Manager instance. 49 * @param ConfigManager $conf Configuration Manager instance.
44 * @param boolean $isLoggedIn True if the user is logged in. 50 * @param boolean $isLoggedIn True if the user is logged in.
51 * @param array $session $_SESSION (by reference)
52 *
53 * @throws ReflectionException
45 */ 54 */
46 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) 55 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
47 { 56 {
48 $this->doneUpdates = $doneUpdates; 57 $this->doneUpdates = $doneUpdates;
49 $this->linkDB = $linkDB; 58 $this->linkDB = $linkDB;
50 $this->conf = $conf; 59 $this->conf = $conf;
51 $this->isLoggedIn = $isLoggedIn; 60 $this->isLoggedIn = $isLoggedIn;
61 $this->session = &$session;
52 62
53 // Retrieve all update methods. 63 // Retrieve all update methods.
54 $class = new ReflectionClass($this); 64 $class = new ReflectionClass($this);
@@ -480,6 +490,30 @@ class Updater
480 } 490 }
481 491
482 $this->conf->write($this->isLoggedIn); 492 $this->conf->write($this->isLoggedIn);
493 return true;
494 }
495
496 /**
497 * * Move thumbnails management to WebThumbnailer, coming with new settings.
498 */
499 public function updateMethodWebThumbnailer()
500 {
501 if ($this->conf->exists('thumbnails.mode')) {
502 return true;
503 }
504
505 $thumbnailsEnabled = $this->conf->get('thumbnail.enable_thumbnails', true);
506 $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
507 $this->conf->set('thumbnails.width', 125);
508 $this->conf->set('thumbnails.height', 90);
509 $this->conf->remove('thumbnail');
510 $this->conf->write(true);
511
512 if ($thumbnailsEnabled) {
513 $this->session['warnings'][] = t(
514 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
515 );
516 }
483 517
484 return true; 518 return true;
485 } 519 }
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index 82f4a368..32aaea48 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -148,6 +148,33 @@ class ConfigManager
148 } 148 }
149 149
150 /** 150 /**
151 * Remove a config element from the config file.
152 *
153 * @param string $setting Asked setting, keys separated with dots.
154 * @param bool $write Write the new setting in the config file, default false.
155 * @param bool $isLoggedIn User login state, default false.
156 *
157 * @throws \Exception Invalid
158 */
159 public function remove($setting, $write = false, $isLoggedIn = false)
160 {
161 if (empty($setting) || ! is_string($setting)) {
162 throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
163 }
164
165 // During the ConfigIO transition, map legacy settings to the new ones.
166 if ($this->configIO instanceof ConfigPhp && isset(ConfigPhp::$LEGACY_KEYS_MAPPING[$setting])) {
167 $setting = ConfigPhp::$LEGACY_KEYS_MAPPING[$setting];
168 }
169
170 $settings = explode('.', $setting);
171 self::removeConfig($settings, $this->loadedConfig);
172 if ($write) {
173 $this->write($isLoggedIn);
174 }
175 }
176
177 /**
151 * Check if a settings exists. 178 * Check if a settings exists.
152 * 179 *
153 * Supports nested settings with dot separated keys. 180 * Supports nested settings with dot separated keys.
@@ -272,7 +299,7 @@ class ConfigManager
272 * 299 *
273 * @param array $settings Ordered array which contains keys to find. 300 * @param array $settings Ordered array which contains keys to find.
274 * @param mixed $value 301 * @param mixed $value
275 * @param array $conf Loaded settings, then sub-array. 302 * @param array $conf Loaded settings, then sub-array.
276 * 303 *
277 * @return mixed Found setting or NOT_FOUND flag. 304 * @return mixed Found setting or NOT_FOUND flag.
278 */ 305 */
@@ -290,6 +317,27 @@ class ConfigManager
290 } 317 }
291 318
292 /** 319 /**
320 * Recursive function which find asked setting in the loaded config and deletes it.
321 *
322 * @param array $settings Ordered array which contains keys to find.
323 * @param array $conf Loaded settings, then sub-array.
324 *
325 * @return mixed Found setting or NOT_FOUND flag.
326 */
327 protected static function removeConfig($settings, &$conf)
328 {
329 if (!is_array($settings) || count($settings) == 0) {
330 return self::$NOT_FOUND;
331 }
332
333 $setting = array_shift($settings);
334 if (count($settings) > 0) {
335 return self::removeConfig($settings, $conf[$setting]);
336 }
337 unset($conf[$setting]);
338 }
339
340 /**
293 * Set a bunch of default values allowing Shaarli to start without a config file. 341 * Set a bunch of default values allowing Shaarli to start without a config file.
294 */ 342 */
295 protected function setDefaultValues() 343 protected function setDefaultValues()
@@ -333,12 +381,12 @@ class ConfigManager
333 // default state of the 'remember me' checkbox of the login form 381 // default state of the 'remember me' checkbox of the login form
334 $this->setEmpty('privacy.remember_user_default', true); 382 $this->setEmpty('privacy.remember_user_default', true);
335 383
336 $this->setEmpty('thumbnail.enable_thumbnails', true);
337 $this->setEmpty('thumbnail.enable_localcache', true);
338
339 $this->setEmpty('redirector.url', ''); 384 $this->setEmpty('redirector.url', '');
340 $this->setEmpty('redirector.encode_url', true); 385 $this->setEmpty('redirector.encode_url', true);
341 386
387 $this->setEmpty('thumbnails.width', '125');
388 $this->setEmpty('thumbnails.height', '90');
389
342 $this->setEmpty('translation.language', 'auto'); 390 $this->setEmpty('translation.language', 'auto');
343 $this->setEmpty('translation.mode', 'php'); 391 $this->setEmpty('translation.mode', 'php');
344 $this->setEmpty('translation.extensions', []); 392 $this->setEmpty('translation.extensions', []);
diff --git a/assets/common/js/picwall.js b/assets/common/js/picwall.js
deleted file mode 100644
index 87a93fc3..00000000
--- a/assets/common/js/picwall.js
+++ /dev/null
@@ -1,10 +0,0 @@
1import Blazy from 'blazy';
2
3(() => {
4 const picwall = document.getElementById('picwall_container');
5 if (picwall != null) {
6 // Suppress ESLint error because that's how bLazy works
7 /* eslint-disable no-new */
8 new Blazy();
9 }
10})();
diff --git a/assets/common/js/thumbnails-update.js b/assets/common/js/thumbnails-update.js
new file mode 100644
index 00000000..b66ca3ae
--- /dev/null
+++ b/assets/common/js/thumbnails-update.js
@@ -0,0 +1,51 @@
1/**
2 * Script used in the thumbnails update page.
3 *
4 * It retrieves the list of link IDs to update, and execute AJAX requests
5 * to update their thumbnails, while updating the progress bar.
6 */
7
8/**
9 * Update the thumbnail of the link with the current i index in ids.
10 * It contains a recursive call to retrieve the thumb of the next link when it succeed.
11 * It also update the progress bar and other visual feedback elements.
12 *
13 * @param {array} ids List of LinkID to update
14 * @param {int} i Current index in ids
15 * @param {object} elements List of DOM element to avoid retrieving them at each iteration
16 */
17function updateThumb(ids, i, elements) {
18 const xhr = new XMLHttpRequest();
19 xhr.open('POST', '?do=ajax_thumb_update');
20 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
21 xhr.responseType = 'json';
22 xhr.onload = () => {
23 if (xhr.status !== 200) {
24 alert(`An error occurred. Return code: ${xhr.status}`);
25 } else {
26 const { response } = xhr;
27 i += 1;
28 elements.progressBar.style.width = `${(i * 100) / ids.length}%`;
29 elements.current.innerHTML = i;
30 elements.title.innerHTML = response.title;
31 if (response.thumbnail !== false) {
32 elements.thumbnail.innerHTML = `<img src="${response.thumbnail}">`;
33 }
34 if (i < ids.length) {
35 updateThumb(ids, i, elements);
36 }
37 }
38 };
39 xhr.send(`id=${ids[i]}`);
40}
41
42(() => {
43 const ids = document.getElementsByName('ids')[0].value.split(',');
44 const elements = {
45 progressBar: document.querySelector('.progressbar > div'),
46 current: document.querySelector('.progress-current'),
47 thumbnail: document.querySelector('.thumbnail-placeholder'),
48 title: document.querySelector('.thumbnail-link-title'),
49 };
50 updateThumb(ids, 0, elements);
51})();
diff --git a/assets/common/js/thumbnails.js b/assets/common/js/thumbnails.js
new file mode 100644
index 00000000..c28322bb
--- /dev/null
+++ b/assets/common/js/thumbnails.js
@@ -0,0 +1,7 @@
1import Blazy from 'blazy';
2
3(() => {
4 // Suppress ESLint error because that's how bLazy works
5 /* eslint-disable no-new */
6 new Blazy();
7})();
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index 09d5efbe..6b286f1e 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -146,6 +146,17 @@ body,
146 background-color: $main-green; 146 background-color: $main-green;
147} 147}
148 148
149.pure-alert-warning {
150 a {
151 color: $warning-text;
152 font-weight: bold;
153 }
154}
155
156.page-single-alert {
157 margin-top: 100px;
158}
159
149.anchor { 160.anchor {
150 &:target { 161 &:target {
151 padding-top: 40px; 162 padding-top: 40px;
@@ -625,23 +636,22 @@ body,
625} 636}
626 637
627.linklist-item { 638.linklist-item {
639 position: relative;
628 margin: 0 0 10px; 640 margin: 0 0 10px;
629 box-shadow: 1px 1px 3px $light-grey; 641 box-shadow: 1px 1px 3px $light-grey;
630 background: $almost-white; 642 background: $almost-white;
631 643
632 &.private { 644 &.private {
633 .linklist-item-title { 645 &::before {
634 &::before { 646 display: block;
635 @extend %private-border; 647 position: absolute;
636 margin-top: 3px; 648 top: 0;
637 } 649 left: 0;
638 } 650 z-index: 1;
639 651 background: $orange;
640 .linklist-item-description { 652 width: 2px;
641 &::before { 653 height: 100%;
642 @extend %private-border; 654 content: '';
643 height: 100%;
644 }
645 } 655 }
646 } 656 }
647} 657}
@@ -1543,3 +1553,40 @@ form {
1543.pure-button-shaarli { 1553.pure-button-shaarli {
1544 background-color: $main-green; 1554 background-color: $main-green;
1545} 1555}
1556
1557.progressbar {
1558 border-radius: 6px;
1559 background-color: $main-green;
1560 padding: 1px;
1561
1562 > div {
1563 border-radius: 10px;
1564 background: repeating-linear-gradient(
1565 -45deg,
1566 $almost-white,
1567 $almost-white 6px,
1568 $background-color 6px,
1569 $background-color 12px
1570 );
1571 width: 0%;
1572 height: 10px;
1573 }
1574}
1575
1576.thumbnails-page-container {
1577 .progress-counter {
1578 padding: 10px 0 20px;
1579 }
1580
1581 .thumbnail-placeholder {
1582 margin: 10px auto;
1583 background-color: $light-grey;
1584 }
1585
1586 .thumbnail-link-title {
1587 padding-bottom: 20px;
1588 overflow: hidden;
1589 text-overflow: ellipsis;
1590 white-space: nowrap;
1591 }
1592}
diff --git a/assets/vintage/css/shaarli.css b/assets/vintage/css/shaarli.css
index c919339b..87c440c8 100644
--- a/assets/vintage/css/shaarli.css
+++ b/assets/vintage/css/shaarli.css
@@ -701,8 +701,8 @@ a.bigbutton, #pageheader a.bigbutton {
701 position: relative; 701 position: relative;
702 display: table-cell; 702 display: table-cell;
703 vertical-align: middle; 703 vertical-align: middle;
704 width: 90px; 704 width: 120px;
705 height: 90px; 705 height: 120px;
706 overflow: hidden; 706 overflow: hidden;
707 text-align: center; 707 text-align: center;
708 float: left; 708 float: left;
@@ -739,9 +739,9 @@ a.bigbutton, #pageheader a.bigbutton {
739 position: absolute; 739 position: absolute;
740 top: 0; 740 top: 0;
741 left: 0; 741 left: 0;
742 width: 90px; 742 width: 120px;
743 font-weight: bold; 743 font-weight: bold;
744 font-size: 8pt; 744 font-size: 9pt;
745 color: #fff; 745 color: #fff;
746 text-align: left; 746 text-align: left;
747 background-color: transparent; 747 background-color: transparent;
@@ -1210,3 +1210,43 @@ ul.errors {
1210 width: 13px; 1210 width: 13px;
1211 height: 13px; 1211 height: 13px;
1212} 1212}
1213
1214.thumbnails-update-container {
1215 padding: 20px 0;
1216 width: 50%;
1217 margin: auto;
1218}
1219
1220.thumbnails-update-container .thumbnail-placeholder {
1221 background: grey;
1222 margin: auto;
1223}
1224
1225.thumbnails-update-container .thumbnail-link-title {
1226 width: 75%;
1227 margin: auto;
1228
1229 padding-bottom: 20px;
1230 overflow: hidden;
1231 text-overflow: ellipsis;
1232 white-space: nowrap;
1233}
1234
1235.progressbar {
1236 border-radius: 6px;
1237 background-color: #111;
1238 padding: 1px;
1239}
1240
1241.progressbar > div {
1242 border-radius: 10px;
1243 background: repeating-linear-gradient(
1244 -45deg,
1245 #f5f5f5,
1246 #f5f5f5 6px,
1247 #d0d0d0 6px,
1248 #d0d0d0 12px
1249 );
1250 width: 0%;
1251 height: 10px;
1252}
diff --git a/composer.json b/composer.json
index 0d4c623c..99ef0b5e 100644
--- a/composer.json
+++ b/composer.json
@@ -19,6 +19,7 @@
19 "shaarli/netscape-bookmark-parser": "^2.0", 19 "shaarli/netscape-bookmark-parser": "^2.0",
20 "erusev/parsedown": "^1.6", 20 "erusev/parsedown": "^1.6",
21 "slim/slim": "^3.0", 21 "slim/slim": "^3.0",
22 "arthurhoaro/web-thumbnailer": "^1.1",
22 "pubsubhubbub/publisher": "dev-master", 23 "pubsubhubbub/publisher": "dev-master",
23 "gettext/gettext": "^4.4" 24 "gettext/gettext": "^4.4"
24 }, 25 },
diff --git a/composer.lock b/composer.lock
index ee762c0e..08e915cf 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1,12 +1,60 @@
1{ 1{
2 "_readme": [ 2 "_readme": [
3 "This file locks the dependencies of your project to a known state", 3 "This file locks the dependencies of your project to a known state",
4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 "This file is @generated automatically" 5 "This file is @generated automatically"
6 ], 6 ],
7 "content-hash": "308a35eab91602fbb449f2c669c445ed", 7 "content-hash": "da7a0c081b61d949154c5d2e5370cbab",
8 "packages": [ 8 "packages": [
9 { 9 {
10 "name": "arthurhoaro/web-thumbnailer",
11 "version": "v1.2.1",
12 "source": {
13 "type": "git",
14 "url": "https://github.com/ArthurHoaro/web-thumbnailer.git",
15 "reference": "a5a52f69e8e8f3c71fab9649e2a927e2d3f418f1"
16 },
17 "dist": {
18 "type": "zip",
19 "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/a5a52f69e8e8f3c71fab9649e2a927e2d3f418f1",
20 "reference": "a5a52f69e8e8f3c71fab9649e2a927e2d3f418f1",
21 "shasum": ""
22 },
23 "require": {
24 "php": ">=5.6",
25 "phpunit/php-text-template": "^1.2"
26 },
27 "conflict": {
28 "phpunit/php-timer": ">=2"
29 },
30 "require-dev": {
31 "php-coveralls/php-coveralls": "^2.0",
32 "phpunit/phpunit": "5.2.*",
33 "squizlabs/php_codesniffer": "^3.2"
34 },
35 "type": "library",
36 "autoload": {
37 "psr-0": {
38 "WebThumbnailer\\": [
39 "src/",
40 "tests/"
41 ]
42 }
43 },
44 "notification-url": "https://packagist.org/downloads/",
45 "license": [
46 "MIT"
47 ],
48 "authors": [
49 {
50 "name": "Arthur Hoaro",
51 "homepage": "http://hoa.ro"
52 }
53 ],
54 "description": "PHP library which will retrieve a thumbnail for any given URL",
55 "time": "2018-07-17T10:21:14+00:00"
56 },
57 {
10 "name": "container-interop/container-interop", 58 "name": "container-interop/container-interop",
11 "version": "1.2.0", 59 "version": "1.2.0",
12 "source": { 60 "source": {
@@ -85,16 +133,16 @@
85 }, 133 },
86 { 134 {
87 "name": "gettext/gettext", 135 "name": "gettext/gettext",
88 "version": "v4.4.4", 136 "version": "v4.6.0",
89 "source": { 137 "source": {
90 "type": "git", 138 "type": "git",
91 "url": "https://github.com/oscarotero/Gettext.git", 139 "url": "https://github.com/oscarotero/Gettext.git",
92 "reference": "ab5e863de2f60806d02e6e6081e21efd45249168" 140 "reference": "cae84aff39a87e07bd6e5cddb5adb720a0ffa357"
93 }, 141 },
94 "dist": { 142 "dist": {
95 "type": "zip", 143 "type": "zip",
96 "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/ab5e863de2f60806d02e6e6081e21efd45249168", 144 "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/cae84aff39a87e07bd6e5cddb5adb720a0ffa357",
97 "reference": "ab5e863de2f60806d02e6e6081e21efd45249168", 145 "reference": "cae84aff39a87e07bd6e5cddb5adb720a0ffa357",
98 "shasum": "" 146 "shasum": ""
99 }, 147 },
100 "require": { 148 "require": {
@@ -103,7 +151,7 @@
103 }, 151 },
104 "require-dev": { 152 "require-dev": {
105 "illuminate/view": "*", 153 "illuminate/view": "*",
106 "phpunit/phpunit": "^4.8|^5.7", 154 "phpunit/phpunit": "^4.8|^5.7|^6.5",
107 "squizlabs/php_codesniffer": "^3.0", 155 "squizlabs/php_codesniffer": "^3.0",
108 "symfony/yaml": "~2", 156 "symfony/yaml": "~2",
109 "twig/extensions": "*", 157 "twig/extensions": "*",
@@ -143,20 +191,20 @@
143 "po", 191 "po",
144 "translation" 192 "translation"
145 ], 193 ],
146 "time": "2018-02-21T18:49:59+00:00" 194 "time": "2018-06-26T16:51:09+00:00"
147 }, 195 },
148 { 196 {
149 "name": "gettext/languages", 197 "name": "gettext/languages",
150 "version": "2.3.0", 198 "version": "2.4.0",
151 "source": { 199 "source": {
152 "type": "git", 200 "type": "git",
153 "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git", 201 "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git",
154 "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7" 202 "reference": "1b74377bd0c4cd87e8d72b948f5d8867e23505a5"
155 }, 203 },
156 "dist": { 204 "dist": {
157 "type": "zip", 205 "type": "zip",
158 "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/49c39e51569963cc917a924b489e7025bfb9d8c7", 206 "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/1b74377bd0c4cd87e8d72b948f5d8867e23505a5",
159 "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7", 207 "reference": "1b74377bd0c4cd87e8d72b948f5d8867e23505a5",
160 "shasum": "" 208 "shasum": ""
161 }, 209 },
162 "require": { 210 "require": {
@@ -204,7 +252,7 @@
204 "translations", 252 "translations",
205 "unicode" 253 "unicode"
206 ], 254 ],
207 "time": "2017-03-23T17:02:28+00:00" 255 "time": "2018-06-21T15:58:36+00:00"
208 }, 256 },
209 { 257 {
210 "name": "katzgrau/klogger", 258 "name": "katzgrau/klogger",
@@ -303,6 +351,47 @@
303 "time": "2018-02-13T20:26:39+00:00" 351 "time": "2018-02-13T20:26:39+00:00"
304 }, 352 },
305 { 353 {
354 "name": "phpunit/php-text-template",
355 "version": "1.2.1",
356 "source": {
357 "type": "git",
358 "url": "https://github.com/sebastianbergmann/php-text-template.git",
359 "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
360 },
361 "dist": {
362 "type": "zip",
363 "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
364 "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
365 "shasum": ""
366 },
367 "require": {
368 "php": ">=5.3.3"
369 },
370 "type": "library",
371 "autoload": {
372 "classmap": [
373 "src/"
374 ]
375 },
376 "notification-url": "https://packagist.org/downloads/",
377 "license": [
378 "BSD-3-Clause"
379 ],
380 "authors": [
381 {
382 "name": "Sebastian Bergmann",
383 "email": "sebastian@phpunit.de",
384 "role": "lead"
385 }
386 ],
387 "description": "Simple template engine.",
388 "homepage": "https://github.com/sebastianbergmann/php-text-template/",
389 "keywords": [
390 "template"
391 ],
392 "time": "2015-06-21T13:50:34+00:00"
393 },
394 {
306 "name": "pimple/pimple", 395 "name": "pimple/pimple",
307 "version": "v3.2.3", 396 "version": "v3.2.3",
308 "source": { 397 "source": {
@@ -504,12 +593,12 @@
504 "source": { 593 "source": {
505 "type": "git", 594 "type": "git",
506 "url": "https://github.com/pubsubhubbub/php-publisher.git", 595 "url": "https://github.com/pubsubhubbub/php-publisher.git",
507 "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f" 596 "reference": "5008fc529b057251b48f4d17a10fdb20047ea8f5"
508 }, 597 },
509 "dist": { 598 "dist": {
510 "type": "zip", 599 "type": "zip",
511 "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/0d224daebd504ab61c22fee4db58f8d1fc18945f", 600 "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/5008fc529b057251b48f4d17a10fdb20047ea8f5",
512 "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f", 601 "reference": "5008fc529b057251b48f4d17a10fdb20047ea8f5",
513 "shasum": "" 602 "shasum": ""
514 }, 603 },
515 "require": { 604 "require": {
@@ -539,7 +628,7 @@
539 "publishers", 628 "publishers",
540 "pubsubhubbub" 629 "pubsubhubbub"
541 ], 630 ],
542 "time": "2017-10-08T10:59:41+00:00" 631 "time": "2018-05-22T11:56:26+00:00"
543 }, 632 },
544 { 633 {
545 "name": "shaarli/netscape-bookmark-parser", 634 "name": "shaarli/netscape-bookmark-parser",
@@ -598,16 +687,16 @@
598 }, 687 },
599 { 688 {
600 "name": "slim/slim", 689 "name": "slim/slim",
601 "version": "3.9.2", 690 "version": "3.10.0",
602 "source": { 691 "source": {
603 "type": "git", 692 "type": "git",
604 "url": "https://github.com/slimphp/Slim.git", 693 "url": "https://github.com/slimphp/Slim.git",
605 "reference": "4086d0106cf5a7135c69fce4161fe355a8feb118" 694 "reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748"
606 }, 695 },
607 "dist": { 696 "dist": {
608 "type": "zip", 697 "type": "zip",
609 "url": "https://api.github.com/repos/slimphp/Slim/zipball/4086d0106cf5a7135c69fce4161fe355a8feb118", 698 "url": "https://api.github.com/repos/slimphp/Slim/zipball/d8aabeacc3688b25e2f2dd2db91df91ec6fdd748",
610 "reference": "4086d0106cf5a7135c69fce4161fe355a8feb118", 699 "reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748",
611 "shasum": "" 700 "shasum": ""
612 }, 701 },
613 "require": { 702 "require": {
@@ -665,7 +754,7 @@
665 "micro", 754 "micro",
666 "router" 755 "router"
667 ], 756 ],
668 "time": "2017-11-26T19:13:09+00:00" 757 "time": "2018-04-19T19:29:08+00:00"
669 } 758 }
670 ], 759 ],
671 "packages-dev": [ 760 "packages-dev": [
@@ -1022,23 +1111,23 @@
1022 }, 1111 },
1023 { 1112 {
1024 "name": "phpspec/prophecy", 1113 "name": "phpspec/prophecy",
1025 "version": "1.7.5", 1114 "version": "1.7.6",
1026 "source": { 1115 "source": {
1027 "type": "git", 1116 "type": "git",
1028 "url": "https://github.com/phpspec/prophecy.git", 1117 "url": "https://github.com/phpspec/prophecy.git",
1029 "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401" 1118 "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712"
1030 }, 1119 },
1031 "dist": { 1120 "dist": {
1032 "type": "zip", 1121 "type": "zip",
1033 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401", 1122 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
1034 "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401", 1123 "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
1035 "shasum": "" 1124 "shasum": ""
1036 }, 1125 },
1037 "require": { 1126 "require": {
1038 "doctrine/instantiator": "^1.0.2", 1127 "doctrine/instantiator": "^1.0.2",
1039 "php": "^5.3|^7.0", 1128 "php": "^5.3|^7.0",
1040 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", 1129 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
1041 "sebastian/comparator": "^1.1|^2.0", 1130 "sebastian/comparator": "^1.1|^2.0|^3.0",
1042 "sebastian/recursion-context": "^1.0|^2.0|^3.0" 1131 "sebastian/recursion-context": "^1.0|^2.0|^3.0"
1043 }, 1132 },
1044 "require-dev": { 1133 "require-dev": {
@@ -1081,7 +1170,7 @@
1081 "spy", 1170 "spy",
1082 "stub" 1171 "stub"
1083 ], 1172 ],
1084 "time": "2018-02-19T10:16:54+00:00" 1173 "time": "2018-04-18T13:57:24+00:00"
1085 }, 1174 },
1086 { 1175 {
1087 "name": "phpunit/php-code-coverage", 1176 "name": "phpunit/php-code-coverage",
@@ -1194,47 +1283,6 @@
1194 "time": "2017-11-27T13:52:08+00:00" 1283 "time": "2017-11-27T13:52:08+00:00"
1195 }, 1284 },
1196 { 1285 {
1197 "name": "phpunit/php-text-template",
1198 "version": "1.2.1",
1199 "source": {
1200 "type": "git",
1201 "url": "https://github.com/sebastianbergmann/php-text-template.git",
1202 "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
1203 },
1204 "dist": {
1205 "type": "zip",
1206 "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
1207 "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
1208 "shasum": ""
1209 },
1210 "require": {
1211 "php": ">=5.3.3"
1212 },
1213 "type": "library",
1214 "autoload": {
1215 "classmap": [
1216 "src/"
1217 ]
1218 },
1219 "notification-url": "https://packagist.org/downloads/",
1220 "license": [
1221 "BSD-3-Clause"
1222 ],
1223 "authors": [
1224 {
1225 "name": "Sebastian Bergmann",
1226 "email": "sebastian@phpunit.de",
1227 "role": "lead"
1228 }
1229 ],
1230 "description": "Simple template engine.",
1231 "homepage": "https://github.com/sebastianbergmann/php-text-template/",
1232 "keywords": [
1233 "template"
1234 ],
1235 "time": "2015-06-21T13:50:34+00:00"
1236 },
1237 {
1238 "name": "phpunit/php-timer", 1286 "name": "phpunit/php-timer",
1239 "version": "1.0.9", 1287 "version": "1.0.9",
1240 "source": { 1288 "source": {
@@ -2207,21 +2255,22 @@
2207 }, 2255 },
2208 { 2256 {
2209 "name": "symfony/config", 2257 "name": "symfony/config",
2210 "version": "v3.4.6", 2258 "version": "v3.4.12",
2211 "source": { 2259 "source": {
2212 "type": "git", 2260 "type": "git",
2213 "url": "https://github.com/symfony/config.git", 2261 "url": "https://github.com/symfony/config.git",
2214 "reference": "05e10567b529476a006b00746c5f538f1636810e" 2262 "reference": "1fffdeb349ff36a25184e5564c25289b1dbfc402"
2215 }, 2263 },
2216 "dist": { 2264 "dist": {
2217 "type": "zip", 2265 "type": "zip",
2218 "url": "https://api.github.com/repos/symfony/config/zipball/05e10567b529476a006b00746c5f538f1636810e", 2266 "url": "https://api.github.com/repos/symfony/config/zipball/1fffdeb349ff36a25184e5564c25289b1dbfc402",
2219 "reference": "05e10567b529476a006b00746c5f538f1636810e", 2267 "reference": "1fffdeb349ff36a25184e5564c25289b1dbfc402",
2220 "shasum": "" 2268 "shasum": ""
2221 }, 2269 },
2222 "require": { 2270 "require": {
2223 "php": "^5.5.9|>=7.0.8", 2271 "php": "^5.5.9|>=7.0.8",
2224 "symfony/filesystem": "~2.8|~3.0|~4.0" 2272 "symfony/filesystem": "~2.8|~3.0|~4.0",
2273 "symfony/polyfill-ctype": "~1.8"
2225 }, 2274 },
2226 "conflict": { 2275 "conflict": {
2227 "symfony/dependency-injection": "<3.3", 2276 "symfony/dependency-injection": "<3.3",
@@ -2266,20 +2315,20 @@
2266 ], 2315 ],
2267 "description": "Symfony Config Component", 2316 "description": "Symfony Config Component",
2268 "homepage": "https://symfony.com", 2317 "homepage": "https://symfony.com",
2269 "time": "2018-02-14T10:03:57+00:00" 2318 "time": "2018-06-19T14:02:58+00:00"
2270 }, 2319 },
2271 { 2320 {
2272 "name": "symfony/console", 2321 "name": "symfony/console",
2273 "version": "v3.4.6", 2322 "version": "v3.4.12",
2274 "source": { 2323 "source": {
2275 "type": "git", 2324 "type": "git",
2276 "url": "https://github.com/symfony/console.git", 2325 "url": "https://github.com/symfony/console.git",
2277 "reference": "067339e9b8ec30d5f19f5950208893ff026b94f7" 2326 "reference": "1b97071a26d028c9bd4588264e101e14f6e7cd00"
2278 }, 2327 },
2279 "dist": { 2328 "dist": {
2280 "type": "zip", 2329 "type": "zip",
2281 "url": "https://api.github.com/repos/symfony/console/zipball/067339e9b8ec30d5f19f5950208893ff026b94f7", 2330 "url": "https://api.github.com/repos/symfony/console/zipball/1b97071a26d028c9bd4588264e101e14f6e7cd00",
2282 "reference": "067339e9b8ec30d5f19f5950208893ff026b94f7", 2331 "reference": "1b97071a26d028c9bd4588264e101e14f6e7cd00",
2283 "shasum": "" 2332 "shasum": ""
2284 }, 2333 },
2285 "require": { 2334 "require": {
@@ -2300,7 +2349,7 @@
2300 "symfony/process": "~3.3|~4.0" 2349 "symfony/process": "~3.3|~4.0"
2301 }, 2350 },
2302 "suggest": { 2351 "suggest": {
2303 "psr/log": "For using the console logger", 2352 "psr/log-implementation": "For using the console logger",
2304 "symfony/event-dispatcher": "", 2353 "symfony/event-dispatcher": "",
2305 "symfony/lock": "", 2354 "symfony/lock": "",
2306 "symfony/process": "" 2355 "symfony/process": ""
@@ -2335,20 +2384,20 @@
2335 ], 2384 ],
2336 "description": "Symfony Console Component", 2385 "description": "Symfony Console Component",
2337 "homepage": "https://symfony.com", 2386 "homepage": "https://symfony.com",
2338 "time": "2018-02-26T15:46:28+00:00" 2387 "time": "2018-05-23T05:02:55+00:00"
2339 }, 2388 },
2340 { 2389 {
2341 "name": "symfony/debug", 2390 "name": "symfony/debug",
2342 "version": "v3.4.6", 2391 "version": "v3.4.12",
2343 "source": { 2392 "source": {
2344 "type": "git", 2393 "type": "git",
2345 "url": "https://github.com/symfony/debug.git", 2394 "url": "https://github.com/symfony/debug.git",
2346 "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc" 2395 "reference": "47e6788c5b151cf0cfdf3329116bf33800632d75"
2347 }, 2396 },
2348 "dist": { 2397 "dist": {
2349 "type": "zip", 2398 "type": "zip",
2350 "url": "https://api.github.com/repos/symfony/debug/zipball/9b1071f86e79e1999b3d3675d2e0e7684268b9bc", 2399 "url": "https://api.github.com/repos/symfony/debug/zipball/47e6788c5b151cf0cfdf3329116bf33800632d75",
2351 "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc", 2400 "reference": "47e6788c5b151cf0cfdf3329116bf33800632d75",
2352 "shasum": "" 2401 "shasum": ""
2353 }, 2402 },
2354 "require": { 2403 "require": {
@@ -2391,20 +2440,20 @@
2391 ], 2440 ],
2392 "description": "Symfony Debug Component", 2441 "description": "Symfony Debug Component",
2393 "homepage": "https://symfony.com", 2442 "homepage": "https://symfony.com",
2394 "time": "2018-02-28T21:49:22+00:00" 2443 "time": "2018-06-25T11:10:40+00:00"
2395 }, 2444 },
2396 { 2445 {
2397 "name": "symfony/dependency-injection", 2446 "name": "symfony/dependency-injection",
2398 "version": "v3.4.6", 2447 "version": "v3.4.12",
2399 "source": { 2448 "source": {
2400 "type": "git", 2449 "type": "git",
2401 "url": "https://github.com/symfony/dependency-injection.git", 2450 "url": "https://github.com/symfony/dependency-injection.git",
2402 "reference": "12e901abc1cb0d637a0e5abe9923471361d96b07" 2451 "reference": "a0be80e3f8c11aca506e250c00bb100c04c35d10"
2403 }, 2452 },
2404 "dist": { 2453 "dist": {
2405 "type": "zip", 2454 "type": "zip",
2406 "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/12e901abc1cb0d637a0e5abe9923471361d96b07", 2455 "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a0be80e3f8c11aca506e250c00bb100c04c35d10",
2407 "reference": "12e901abc1cb0d637a0e5abe9923471361d96b07", 2456 "reference": "a0be80e3f8c11aca506e250c00bb100c04c35d10",
2408 "shasum": "" 2457 "shasum": ""
2409 }, 2458 },
2410 "require": { 2459 "require": {
@@ -2462,24 +2511,25 @@
2462 ], 2511 ],
2463 "description": "Symfony DependencyInjection Component", 2512 "description": "Symfony DependencyInjection Component",
2464 "homepage": "https://symfony.com", 2513 "homepage": "https://symfony.com",
2465 "time": "2018-03-04T03:54:53+00:00" 2514 "time": "2018-06-25T08:36:56+00:00"
2466 }, 2515 },
2467 { 2516 {
2468 "name": "symfony/filesystem", 2517 "name": "symfony/filesystem",
2469 "version": "v3.4.6", 2518 "version": "v3.4.12",
2470 "source": { 2519 "source": {
2471 "type": "git", 2520 "type": "git",
2472 "url": "https://github.com/symfony/filesystem.git", 2521 "url": "https://github.com/symfony/filesystem.git",
2473 "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541" 2522 "reference": "8a721a5f2553c6c3482b1c5b22ed60fe94dd63ed"
2474 }, 2523 },
2475 "dist": { 2524 "dist": {
2476 "type": "zip", 2525 "type": "zip",
2477 "url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541", 2526 "url": "https://api.github.com/repos/symfony/filesystem/zipball/8a721a5f2553c6c3482b1c5b22ed60fe94dd63ed",
2478 "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541", 2527 "reference": "8a721a5f2553c6c3482b1c5b22ed60fe94dd63ed",
2479 "shasum": "" 2528 "shasum": ""
2480 }, 2529 },
2481 "require": { 2530 "require": {
2482 "php": "^5.5.9|>=7.0.8" 2531 "php": "^5.5.9|>=7.0.8",
2532 "symfony/polyfill-ctype": "~1.8"
2483 }, 2533 },
2484 "type": "library", 2534 "type": "library",
2485 "extra": { 2535 "extra": {
@@ -2511,20 +2561,20 @@
2511 ], 2561 ],
2512 "description": "Symfony Filesystem Component", 2562 "description": "Symfony Filesystem Component",
2513 "homepage": "https://symfony.com", 2563 "homepage": "https://symfony.com",
2514 "time": "2018-02-22T10:48:49+00:00" 2564 "time": "2018-06-21T11:10:19+00:00"
2515 }, 2565 },
2516 { 2566 {
2517 "name": "symfony/finder", 2567 "name": "symfony/finder",
2518 "version": "v3.4.6", 2568 "version": "v3.4.12",
2519 "source": { 2569 "source": {
2520 "type": "git", 2570 "type": "git",
2521 "url": "https://github.com/symfony/finder.git", 2571 "url": "https://github.com/symfony/finder.git",
2522 "reference": "a479817ce0a9e4adfd7d39c6407c95d97c254625" 2572 "reference": "3a8c3de91d2b2c68cd2d665cf9d00f7ef9eaa394"
2523 }, 2573 },
2524 "dist": { 2574 "dist": {
2525 "type": "zip", 2575 "type": "zip",
2526 "url": "https://api.github.com/repos/symfony/finder/zipball/a479817ce0a9e4adfd7d39c6407c95d97c254625", 2576 "url": "https://api.github.com/repos/symfony/finder/zipball/3a8c3de91d2b2c68cd2d665cf9d00f7ef9eaa394",
2527 "reference": "a479817ce0a9e4adfd7d39c6407c95d97c254625", 2577 "reference": "3a8c3de91d2b2c68cd2d665cf9d00f7ef9eaa394",
2528 "shasum": "" 2578 "shasum": ""
2529 }, 2579 },
2530 "require": { 2580 "require": {
@@ -2560,20 +2610,75 @@
2560 ], 2610 ],
2561 "description": "Symfony Finder Component", 2611 "description": "Symfony Finder Component",
2562 "homepage": "https://symfony.com", 2612 "homepage": "https://symfony.com",
2563 "time": "2018-03-05T18:28:11+00:00" 2613 "time": "2018-06-19T20:52:10+00:00"
2614 },
2615 {
2616 "name": "symfony/polyfill-ctype",
2617 "version": "v1.8.0",
2618 "source": {
2619 "type": "git",
2620 "url": "https://github.com/symfony/polyfill-ctype.git",
2621 "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae"
2622 },
2623 "dist": {
2624 "type": "zip",
2625 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
2626 "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
2627 "shasum": ""
2628 },
2629 "require": {
2630 "php": ">=5.3.3"
2631 },
2632 "type": "library",
2633 "extra": {
2634 "branch-alias": {
2635 "dev-master": "1.8-dev"
2636 }
2637 },
2638 "autoload": {
2639 "psr-4": {
2640 "Symfony\\Polyfill\\Ctype\\": ""
2641 },
2642 "files": [
2643 "bootstrap.php"
2644 ]
2645 },
2646 "notification-url": "https://packagist.org/downloads/",
2647 "license": [
2648 "MIT"
2649 ],
2650 "authors": [
2651 {
2652 "name": "Symfony Community",
2653 "homepage": "https://symfony.com/contributors"
2654 },
2655 {
2656 "name": "Gert de Pagter",
2657 "email": "BackEndTea@gmail.com"
2658 }
2659 ],
2660 "description": "Symfony polyfill for ctype functions",
2661 "homepage": "https://symfony.com",
2662 "keywords": [
2663 "compatibility",
2664 "ctype",
2665 "polyfill",
2666 "portable"
2667 ],
2668 "time": "2018-04-30T19:57:29+00:00"
2564 }, 2669 },
2565 { 2670 {
2566 "name": "symfony/polyfill-mbstring", 2671 "name": "symfony/polyfill-mbstring",
2567 "version": "v1.7.0", 2672 "version": "v1.8.0",
2568 "source": { 2673 "source": {
2569 "type": "git", 2674 "type": "git",
2570 "url": "https://github.com/symfony/polyfill-mbstring.git", 2675 "url": "https://github.com/symfony/polyfill-mbstring.git",
2571 "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b" 2676 "reference": "3296adf6a6454a050679cde90f95350ad604b171"
2572 }, 2677 },
2573 "dist": { 2678 "dist": {
2574 "type": "zip", 2679 "type": "zip",
2575 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b", 2680 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171",
2576 "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b", 2681 "reference": "3296adf6a6454a050679cde90f95350ad604b171",
2577 "shasum": "" 2682 "shasum": ""
2578 }, 2683 },
2579 "require": { 2684 "require": {
@@ -2585,7 +2690,7 @@
2585 "type": "library", 2690 "type": "library",
2586 "extra": { 2691 "extra": {
2587 "branch-alias": { 2692 "branch-alias": {
2588 "dev-master": "1.7-dev" 2693 "dev-master": "1.8-dev"
2589 } 2694 }
2590 }, 2695 },
2591 "autoload": { 2696 "autoload": {
@@ -2619,24 +2724,25 @@
2619 "portable", 2724 "portable",
2620 "shim" 2725 "shim"
2621 ], 2726 ],
2622 "time": "2018-01-30T19:27:44+00:00" 2727 "time": "2018-04-26T10:06:28+00:00"
2623 }, 2728 },
2624 { 2729 {
2625 "name": "symfony/yaml", 2730 "name": "symfony/yaml",
2626 "version": "v3.4.6", 2731 "version": "v3.4.12",
2627 "source": { 2732 "source": {
2628 "type": "git", 2733 "type": "git",
2629 "url": "https://github.com/symfony/yaml.git", 2734 "url": "https://github.com/symfony/yaml.git",
2630 "reference": "6af42631dcf89e9c616242c900d6c52bd53bd1bb" 2735 "reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0"
2631 }, 2736 },
2632 "dist": { 2737 "dist": {
2633 "type": "zip", 2738 "type": "zip",
2634 "url": "https://api.github.com/repos/symfony/yaml/zipball/6af42631dcf89e9c616242c900d6c52bd53bd1bb", 2739 "url": "https://api.github.com/repos/symfony/yaml/zipball/c5010cc1692ce1fa328b1fb666961eb3d4a85bb0",
2635 "reference": "6af42631dcf89e9c616242c900d6c52bd53bd1bb", 2740 "reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0",
2636 "shasum": "" 2741 "shasum": ""
2637 }, 2742 },
2638 "require": { 2743 "require": {
2639 "php": "^5.5.9|>=7.0.8" 2744 "php": "^5.5.9|>=7.0.8",
2745 "symfony/polyfill-ctype": "~1.8"
2640 }, 2746 },
2641 "conflict": { 2747 "conflict": {
2642 "symfony/console": "<3.4" 2748 "symfony/console": "<3.4"
@@ -2677,7 +2783,7 @@
2677 ], 2783 ],
2678 "description": "Symfony Yaml Component", 2784 "description": "Symfony Yaml Component",
2679 "homepage": "https://symfony.com", 2785 "homepage": "https://symfony.com",
2680 "time": "2018-02-16T09:50:28+00:00" 2786 "time": "2018-05-03T23:18:14+00:00"
2681 }, 2787 },
2682 { 2788 {
2683 "name": "theseer/fdomdocument", 2789 "name": "theseer/fdomdocument",
diff --git a/doc/md/Link-structure.md b/doc/md/Link-structure.md
new file mode 100644
index 00000000..0a2d0f88
--- /dev/null
+++ b/doc/md/Link-structure.md
@@ -0,0 +1,18 @@
1## Link structure
2
3Every link available through the `LinkDB` object is represented as an array
4containing the following fields:
5
6 * `id` (integer): Unique identifier.
7 * `title` (string): Title of the link.
8 * `url` (string): URL of the link. Used for displayable links (without redirector, url encoding, etc.).
9 Can be absolute or relative for Notes.
10 * `real_url` (string): Real destination URL, can be redirected, encoded, etc.
11 * `shorturl` (string): Permalink small hash.
12 * `description` (string): Link text description.
13 * `private` (boolean): whether the link is private or not.
14 * `tags` (string): all link tags separated by a single space
15 * `thumbnail` (string|boolean): relative path of the thumbnail cache file, or false if there isn't any.
16 * `created` (DateTime): link creation date time.
17 * `updated` (DateTime): last modification date time.
18 \ No newline at end of file
diff --git a/doc/md/Server-configuration.md b/doc/md/Server-configuration.md
index ca82b2ec..e281dc85 100644
--- a/doc/md/Server-configuration.md
+++ b/doc/md/Server-configuration.md
@@ -29,7 +29,7 @@ Extension | Required? | Usage
29---|:---:|--- 29---|:---:|---
30[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS 30[`openssl`](http://php.net/manual/en/book.openssl.php) | All | OpenSSL, HTTPS
31[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support 31[`php-mbstring`](http://php.net/manual/en/book.mbstring.php) | CentOS, Fedora, RHEL, Windows, some hosting providers | multibyte (Unicode) string support
32[`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing 32[`php-gd`](http://php.net/manual/en/book.image.php) | optional | required to use thumbnails
33[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`) 33[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
34[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way 34[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
35[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster) 35[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
index 2ebeccbc..155eb52e 100644
--- a/inc/languages/fr/LC_MESSAGES/shaarli.po
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -1,15 +1,15 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: Shaarli\n" 3"Project-Id-Version: Shaarli\n"
4"POT-Creation-Date: 2018-01-24 18:43+0100\n" 4"POT-Creation-Date: 2018-07-17 13:04+0200\n"
5"PO-Revision-Date: 2018-03-06 18:44+0100\n" 5"PO-Revision-Date: 2018-07-17 13:07+0200\n"
6"Last-Translator: \n" 6"Last-Translator: \n"
7"Language-Team: Shaarli\n" 7"Language-Team: Shaarli\n"
8"Language: fr_FR\n" 8"Language: fr_FR\n"
9"MIME-Version: 1.0\n" 9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n" 10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n" 11"Content-Transfer-Encoding: 8bit\n"
12"X-Generator: Poedit 2.0.6\n" 12"X-Generator: Poedit 2.0.9\n"
13"X-Poedit-Basepath: ../../../..\n" 13"X-Poedit-Basepath: ../../../..\n"
14"Plural-Forms: nplurals=2; plural=(n > 1);\n" 14"Plural-Forms: nplurals=2; plural=(n > 1);\n"
15"X-Poedit-SourceCharset: UTF-8\n" 15"X-Poedit-SourceCharset: UTF-8\n"
@@ -56,7 +56,7 @@ msgstr "Liens directs"
56 56
57#: application/FeedBuilder.php:153 57#: application/FeedBuilder.php:153
58#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 58#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
59#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178 59#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
60msgid "Permalink" 60msgid "Permalink"
61msgstr "Permalien" 61msgstr "Permalien"
62 62
@@ -68,18 +68,22 @@ msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
68msgid "Could not parse history file" 68msgid "Could not parse history file"
69msgstr "Format incorrect pour le fichier d'historique" 69msgstr "Format incorrect pour le fichier d'historique"
70 70
71#: application/Languages.php:161 71#: application/Languages.php:177
72msgid "Automatic" 72msgid "Automatic"
73msgstr "Automatique" 73msgstr "Automatique"
74 74
75#: application/Languages.php:162 75#: application/Languages.php:178
76msgid "English" 76msgid "English"
77msgstr "Anglais" 77msgstr "Anglais"
78 78
79#: application/Languages.php:163 79#: application/Languages.php:179
80msgid "French" 80msgid "French"
81msgstr "Français" 81msgstr "Français"
82 82
83#: application/Languages.php:180
84msgid "German"
85msgstr "Allemand"
86
83#: application/LinkDB.php:136 87#: application/LinkDB.php:136
84msgid "You are not authorized to add a link." 88msgid "You are not authorized to add a link."
85msgstr "Vous n'êtes pas autorisé à ajouter un lien." 89msgstr "Vous n'êtes pas autorisé à ajouter un lien."
@@ -163,11 +167,11 @@ msgstr ""
163"a été importé avec succès en %d secondes : %d liens importés, %d liens " 167"a été importé avec succès en %d secondes : %d liens importés, %d liens "
164"écrasés, %d liens ignorés." 168"écrasés, %d liens ignorés."
165 169
166#: application/PageBuilder.php:168 170#: application/PageBuilder.php:200
167msgid "The page you are trying to reach does not exist or has been deleted." 171msgid "The page you are trying to reach does not exist or has been deleted."
168msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée." 172msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée."
169 173
170#: application/PageBuilder.php:170 174#: application/PageBuilder.php:202
171msgid "404 Not Found" 175msgid "404 Not Found"
172msgstr "404 Introuvable" 176msgstr "404 Introuvable"
173 177
@@ -176,21 +180,37 @@ msgstr "404 Introuvable"
176msgid "Plugin \"%s\" files not found." 180msgid "Plugin \"%s\" files not found."
177msgstr "Les fichiers de l'extension \"%s\" sont introuvables." 181msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
178 182
179#: application/Updater.php:76 183#: application/Thumbnailer.php:61
184msgid ""
185"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
186"disabled. Please reload the page."
187msgstr ""
188"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
189"disabled. Please reload the page."
190
191#: application/Updater.php:86
180msgid "Couldn't retrieve Updater class methods." 192msgid "Couldn't retrieve Updater class methods."
181msgstr "Impossible de récupérer les méthodes de la classe Updater." 193msgstr "Impossible de récupérer les méthodes de la classe Updater."
182 194
183#: application/Updater.php:506 195#: application/Updater.php:514 index.php:1023
196msgid ""
197"You have enabled or changed thumbnails mode. <a href=\"?do=thumbs_update"
198"\">Please synchronize them</a>."
199msgstr ""
200"Vous avez activé ou changé le mode de miniatures. <a href=\"?do=thumbs_update"
201"\">Merci de les synchroniser</a>."
202
203#: application/Updater.php:566
184msgid "An error occurred while running the update " 204msgid "An error occurred while running the update "
185msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " 205msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
186 206
187#: application/Updater.php:546 207#: application/Updater.php:606
188msgid "Updates file path is not set, can't write updates." 208msgid "Updates file path is not set, can't write updates."
189msgstr "" 209msgstr ""
190"Le chemin vers le fichier de mise à jour n'est pas défini, impossible " 210"Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
191"d'écrire les mises à jour." 211"d'écrire les mises à jour."
192 212
193#: application/Updater.php:551 213#: application/Updater.php:611
194msgid "Unable to write updates in " 214msgid "Unable to write updates in "
195msgstr "Impossible d'écrire les mises à jour dans " 215msgstr "Impossible d'écrire les mises à jour dans "
196 216
@@ -230,6 +250,7 @@ msgstr ""
230"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé." 250"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
231 251
232#: application/config/ConfigManager.php:135 252#: application/config/ConfigManager.php:135
253#: application/config/ConfigManager.php:162
233msgid "Invalid setting key parameter. String expected, got: " 254msgid "Invalid setting key parameter. String expected, got: "
234msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " 255msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
235 256
@@ -251,135 +272,133 @@ msgstr "Vous n'êtes pas autorisé à modifier la configuration."
251msgid "Error accessing" 272msgid "Error accessing"
252msgstr "Une erreur s'est produite en accédant à" 273msgstr "Une erreur s'est produite en accédant à"
253 274
254#: index.php:142 275#: index.php:143
255msgid "Shared links on " 276msgid "Shared links on "
256msgstr "Liens partagés sur " 277msgstr "Liens partagés sur "
257 278
258#: index.php:164 279#: index.php:165
259msgid "Insufficient permissions:" 280msgid "Insufficient permissions:"
260msgstr "Permissions insuffisantes :" 281msgstr "Permissions insuffisantes :"
261 282
262#: index.php:303 283#: index.php:201
263msgid "I said: NO. You are banned for the moment. Go away." 284msgid "I said: NO. You are banned for the moment. Go away."
264msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard." 285msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
265 286
266#: index.php:368 287#: index.php:273
267msgid "Wrong login/password." 288msgid "Wrong login/password."
268msgstr "Nom d'utilisateur ou mot de passe incorrects." 289msgstr "Nom d'utilisateur ou mot de passe incorrects."
269 290
270#: index.php:576 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 291#: index.php:483 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
271#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42 292#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:46
272msgid "Daily" 293msgid "Daily"
273msgstr "Quotidien" 294msgstr "Quotidien"
274 295
275#: index.php:681 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 296#: index.php:589 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
276#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 297#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
277#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 298#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
278#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95 299#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
279#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71 300#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:75
280#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95 301#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:99
281msgid "Login" 302msgid "Login"
282msgstr "Connexion" 303msgstr "Connexion"
283 304
284#: index.php:722 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 305#: index.php:606 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
285#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39 306#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:41
286msgid "Picture wall" 307msgid "Picture wall"
287msgstr "Mur d'images" 308msgstr "Mur d'images"
288 309
289#: index.php:770 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 310#: index.php:683 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
290#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36 311#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36
291#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 312#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
292msgid "Tag cloud" 313msgid "Tag cloud"
293msgstr "Nuage de tags" 314msgstr "Nuage de tags"
294 315
295#: index.php:803 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 316#: index.php:716 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
296msgid "Tag list" 317msgid "Tag list"
297msgstr "Liste des tags" 318msgstr "Liste des tags"
298 319
299#: index.php:1028 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 320#: index.php:941 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
300#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31 321#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
301msgid "Tools" 322msgid "Tools"
302msgstr "Outils" 323msgstr "Outils"
303 324
304#: index.php:1037 325#: index.php:950
305msgid "You are not supposed to change a password on an Open Shaarli." 326msgid "You are not supposed to change a password on an Open Shaarli."
306msgstr "" 327msgstr ""
307"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert." 328"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
308 329
309#: index.php:1042 index.php:1084 index.php:1162 index.php:1193 index.php:1293 330#: index.php:955 index.php:997 index.php:1085 index.php:1116 index.php:1221
310msgid "Wrong token." 331msgid "Wrong token."
311msgstr "Jeton invalide." 332msgstr "Jeton invalide."
312 333
313#: index.php:1047 334#: index.php:960
314msgid "The old password is not correct." 335msgid "The old password is not correct."
315msgstr "L'ancien mot de passe est incorrect." 336msgstr "L'ancien mot de passe est incorrect."
316 337
317#: index.php:1067 338#: index.php:980
318msgid "Your password has been changed" 339msgid "Your password has been changed"
319msgstr "Votre mot de passe a été modifié" 340msgstr "Votre mot de passe a été modifié"
320 341
321#: index.php:1072 342#: index.php:985
322#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 343#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
323#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 344#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
324msgid "Change password" 345msgid "Change password"
325msgstr "Modification du mot de passe" 346msgstr "Modification du mot de passe"
326 347
327#: index.php:1121 348#: index.php:1043
328msgid "Configuration was saved." 349msgid "Configuration was saved."
329msgstr "La configuration a été sauvegardé." 350msgstr "La configuration a été sauvegardé."
330 351
331#: index.php:1145 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 352#: index.php:1068 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
332msgid "Configure" 353msgid "Configure"
333msgstr "Configurer" 354msgstr "Configurer"
334 355
335#: index.php:1156 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 356#: index.php:1079 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
336#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 357#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
337msgid "Manage tags" 358msgid "Manage tags"
338msgstr "Gérer les tags" 359msgstr "Gérer les tags"
339 360
340#: index.php:1174 361#: index.php:1097
341#, php-format 362#, php-format
342msgid "The tag was removed from %d link." 363msgid "The tag was removed from %d link."
343msgid_plural "The tag was removed from %d links." 364msgid_plural "The tag was removed from %d links."
344msgstr[0] "Le tag a été supprimé de %d lien." 365msgstr[0] "Le tag a été supprimé de %d lien."
345msgstr[1] "Le tag a été supprimé de %d liens." 366msgstr[1] "Le tag a été supprimé de %d liens."
346 367
347#: index.php:1175 368#: index.php:1098
348#, php-format 369#, php-format
349msgid "The tag was renamed in %d link." 370msgid "The tag was renamed in %d link."
350msgid_plural "The tag was renamed in %d links." 371msgid_plural "The tag was renamed in %d links."
351msgstr[0] "Le tag a été renommé dans %d lien." 372msgstr[0] "Le tag a été renommé dans %d lien."
352msgstr[1] "Le tag a été renommé dans %d liens." 373msgstr[1] "Le tag a été renommé dans %d liens."
353 374
354#: index.php:1183 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 375#: index.php:1106 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
355msgid "Shaare a new link" 376msgid "Shaare a new link"
356msgstr "Partager un nouveau lien" 377msgstr "Partager un nouveau lien"
357 378
358#: index.php:1353 tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 379#: index.php:1281 tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
359#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
360msgid "Edit" 380msgid "Edit"
361msgstr "Modifier" 381msgstr "Modifier"
362 382
363#: index.php:1353 index.php:1418 383#: index.php:1281 index.php:1351
364#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
365#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 384#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
366#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26 385#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
367msgid "Shaare" 386msgid "Shaare"
368msgstr "Shaare" 387msgstr "Shaare"
369 388
370#: index.php:1387 389#: index.php:1320
371msgid "Note: " 390msgid "Note: "
372msgstr "Note : " 391msgstr "Note : "
373 392
374#: index.php:1427 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 393#: index.php:1360 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
375msgid "Export" 394msgid "Export"
376msgstr "Exporter" 395msgstr "Exporter"
377 396
378#: index.php:1489 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 397#: index.php:1422 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
379msgid "Import" 398msgid "Import"
380msgstr "Importer" 399msgstr "Importer"
381 400
382#: index.php:1499 401#: index.php:1432
383#, php-format 402#, php-format
384msgid "" 403msgid ""
385"The file you are trying to upload is probably bigger than what this " 404"The file you are trying to upload is probably bigger than what this "
@@ -389,16 +408,20 @@ msgstr ""
389"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " 408"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
390"légères." 409"légères."
391 410
392#: index.php:1538 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 411#: index.php:1471 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
393#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 412#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
394msgid "Plugin administration" 413msgid "Plugin administration"
395msgstr "Administration des extensions" 414msgstr "Administration des extensions"
396 415
397#: index.php:1703 416#: index.php:1523 tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
417msgid "Thumbnails update"
418msgstr "Mise à jour des miniatures"
419
420#: index.php:1695
398msgid "Search: " 421msgid "Search: "
399msgstr "Recherche : " 422msgstr "Recherche : "
400 423
401#: index.php:1930 424#: index.php:1735
402#, php-format 425#, php-format
403msgid "" 426msgid ""
404"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " 427"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@@ -417,7 +440,7 @@ msgstr ""
417"cookies. Nous vous recommandons d'accéder à votre serveur depuis son adresse " 440"cookies. Nous vous recommandons d'accéder à votre serveur depuis son adresse "
418"IP ou un <em>Fully Qualified Domain Name</em>.<br>" 441"IP ou un <em>Fully Qualified Domain Name</em>.<br>"
419 442
420#: index.php:1940 443#: index.php:1745
421msgid "Click to try again." 444msgid "Click to try again."
422msgstr "Cliquer ici pour réessayer." 445msgstr "Cliquer ici pour réessayer."
423 446
@@ -467,19 +490,19 @@ msgstr ""
467msgid "Isso server URL (without 'http://')" 490msgid "Isso server URL (without 'http://')"
468msgstr "URL du serveur Isso (sans 'http://')" 491msgstr "URL du serveur Isso (sans 'http://')"
469 492
470#: plugins/markdown/markdown.php:158 493#: plugins/markdown/markdown.php:161
471msgid "Description will be rendered with" 494msgid "Description will be rendered with"
472msgstr "La description sera générée avec" 495msgstr "La description sera générée avec"
473 496
474#: plugins/markdown/markdown.php:159 497#: plugins/markdown/markdown.php:162
475msgid "Markdown syntax documentation" 498msgid "Markdown syntax documentation"
476msgstr "Documentation sur la syntaxe Markdown" 499msgstr "Documentation sur la syntaxe Markdown"
477 500
478#: plugins/markdown/markdown.php:160 501#: plugins/markdown/markdown.php:163
479msgid "Markdown syntax" 502msgid "Markdown syntax"
480msgstr "la syntaxe Markdown" 503msgstr "la syntaxe Markdown"
481 504
482#: plugins/markdown/markdown.php:339 505#: plugins/markdown/markdown.php:347
483msgid "" 506msgid ""
484"Render shaare description with Markdown syntax.<br><strong>Warning</" 507"Render shaare description with Markdown syntax.<br><strong>Warning</"
485"strong>:\n" 508"strong>:\n"
@@ -577,11 +600,11 @@ msgstr "URL de l'API Wallabag"
577msgid "Wallabag API version (1 or 2)" 600msgid "Wallabag API version (1 or 2)"
578msgstr "Version de l'API Wallabag (1 ou 2)" 601msgstr "Version de l'API Wallabag (1 ou 2)"
579 602
580#: tests/LanguagesTest.php:188 tests/LanguagesTest.php:201 603#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
581#: tests/languages/fr/LanguagesFrTest.php:160 604#: tests/languages/fr/LanguagesFrTest.php:160
582#: tests/languages/fr/LanguagesFrTest.php:173 605#: tests/languages/fr/LanguagesFrTest.php:173
583#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81 606#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
584#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81 607#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:85
585msgid "Search" 608msgid "Search"
586msgid_plural "Search" 609msgid_plural "Search"
587msgstr[0] "Rechercher" 610msgstr[0] "Rechercher"
@@ -625,8 +648,8 @@ msgid "Rename"
625msgstr "Renommer" 648msgstr "Renommer"
626 649
627#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 650#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
628#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 651#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
629#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172 652#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
630msgid "Delete" 653msgid "Delete"
631msgstr "Supprimer" 654msgstr "Supprimer"
632 655
@@ -736,8 +759,36 @@ msgstr ""
736msgid "API secret" 759msgid "API secret"
737msgstr "Clé d'API secrète" 760msgstr "Clé d'API secrète"
738 761
739#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274 762#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:277
740#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 763msgid "Enable thumbnails"
764msgstr "Activer les miniatures"
765
766#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:281
767msgid "You need to enable the extension <code>php-gd</code> to use thumbnails."
768msgstr ""
769"Vous devez activer l'extension <code>php-gd</code> pour utiliser les "
770"miniatures."
771
772#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:285
773#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
774msgid "Synchronize thumbnails"
775msgstr "Synchroniser les miniatures"
776
777#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:296
778#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
779msgid "All"
780msgstr "Tous"
781
782#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:300
783msgid "Only common media hosts"
784msgstr "Seulement les hébergeurs de média connus"
785
786#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:304
787msgid "None"
788msgstr "Aucune"
789
790#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:312
791#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
741#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 792#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
742#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 793#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
743msgid "Save" 794msgid "Save"
@@ -763,25 +814,27 @@ msgstr "Tous les liens d'un jour sur une page."
763msgid "Next day" 814msgid "Next day"
764msgstr "Jour suivant" 815msgstr "Jour suivant"
765 816
766#: tpl/editlink.html 817#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
767msgid "Edit Shaare" 818msgid "Edit Shaare"
768msgstr "Modifier le Shaare" 819msgstr "Modifier le Shaare"
820
821#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
769msgid "New Shaare" 822msgid "New Shaare"
770msgstr "Nouveau Shaare" 823msgstr "Nouveau Shaare"
771 824
772#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 825#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
773msgid "Created:" 826msgid "Created:"
774msgstr "Création :" 827msgstr "Création :"
775 828
776#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 829#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
777msgid "URL" 830msgid "URL"
778msgstr "URL" 831msgstr "URL"
779 832
780#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 833#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
781msgid "Title" 834msgid "Title"
782msgstr "Titre" 835msgstr "Titre"
783 836
784#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 837#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
785#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 838#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
786#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 839#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
787#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 840#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@@ -789,17 +842,17 @@ msgstr "Titre"
789msgid "Description" 842msgid "Description"
790msgstr "Description" 843msgstr "Description"
791 844
792#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 845#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
793msgid "Tags" 846msgid "Tags"
794msgstr "Tags" 847msgstr "Tags"
795 848
796#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 849#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
797#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 850#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
798#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 851#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167
799msgid "Private" 852msgid "Private"
800msgstr "Privé" 853msgstr "Privé"
801 854
802#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 855#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
803msgid "Apply Changes" 856msgid "Apply Changes"
804msgstr "Appliquer" 857msgstr "Appliquer"
805 858
@@ -811,10 +864,6 @@ msgstr "Exporter les données"
811msgid "Selection" 864msgid "Selection"
812msgstr "Choisir" 865msgstr "Choisir"
813 866
814#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
815msgid "All"
816msgstr "Tous"
817
818#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 867#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
819msgid "Public" 868msgid "Public"
820msgstr "Publics" 869msgstr "Publics"
@@ -876,15 +925,15 @@ msgstr ""
876 925
877#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 926#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
878#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 927#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
879#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 928#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
880#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 929#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151
881msgid "Username" 930msgid "Username"
882msgstr "Nom d'utilisateur" 931msgstr "Nom d'utilisateur"
883 932
884#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 933#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
885#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 934#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
886#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 935#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
887#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148 936#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152
888msgid "Password" 937msgid "Password"
889msgstr "Mot de passe" 938msgstr "Mot de passe"
890 939
@@ -901,28 +950,28 @@ msgid "Install"
901msgstr "Installer" 950msgstr "Installer"
902 951
903#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 952#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
904#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 953#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
905msgid "shaare" 954msgid "shaare"
906msgid_plural "shaares" 955msgid_plural "shaares"
907msgstr[0] "shaare" 956msgstr[0] "shaare"
908msgstr[1] "shaares" 957msgstr[1] "shaares"
909 958
910#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 959#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
911#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 960#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
912msgid "private link" 961msgid "private link"
913msgid_plural "private links" 962msgid_plural "private links"
914msgstr[0] "lien privé" 963msgstr[0] "lien privé"
915msgstr[1] "liens privés" 964msgstr[1] "liens privés"
916 965
917#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 966#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
918#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 967#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
919#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117 968#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:121
920msgid "Search text" 969msgid "Search text"
921msgstr "Recherche texte" 970msgstr "Recherche texte"
922 971
923#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 972#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
924#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124 973#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:128
925#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124 974#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:128
926#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 975#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
927#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 976#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
928#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 977#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
@@ -930,52 +979,52 @@ msgstr "Recherche texte"
930msgid "Filter by tag" 979msgid "Filter by tag"
931msgstr "Filtrer par tag" 980msgstr "Filtrer par tag"
932 981
933#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 982#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
934msgid "Nothing found." 983msgid "Nothing found."
935msgstr "Aucun résultat." 984msgstr "Aucun résultat."
936 985
937#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119 986#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118
938#, php-format 987#, php-format
939msgid "%s result" 988msgid "%s result"
940msgid_plural "%s results" 989msgid_plural "%s results"
941msgstr[0] "%s résultat" 990msgstr[0] "%s résultat"
942msgstr[1] "%s résultats" 991msgstr[1] "%s résultats"
943 992
944#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 993#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
945msgid "for" 994msgid "for"
946msgstr "pour" 995msgstr "pour"
947 996
948#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 997#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
949msgid "tagged" 998msgid "tagged"
950msgstr "taggé" 999msgstr "taggé"
951 1000
952#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 1001#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133
953msgid "Remove tag" 1002msgid "Remove tag"
954msgstr "Retirer le tag" 1003msgstr "Retirer le tag"
955 1004
956#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143 1005#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:142
957msgid "with status" 1006msgid "with status"
958msgstr "avec le statut" 1007msgstr "avec le statut"
959 1008
960#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 1009#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
961msgid "without any tag" 1010msgid "without any tag"
962msgstr "sans tag" 1011msgstr "sans tag"
963 1012
964#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174 1013#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
965#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 1014#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
966#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 1015#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
967msgid "Fold" 1016msgid "Fold"
968msgstr "Replier" 1017msgstr "Replier"
969 1018
970#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 1019#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
971msgid "Edited: " 1020msgid "Edited: "
972msgstr "Modifié : " 1021msgstr "Modifié : "
973 1022
974#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180 1023#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
975msgid "permalink" 1024msgid "permalink"
976msgstr "permalien" 1025msgstr "permalien"
977 1026
978#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 1027#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
979msgid "Add tag" 1028msgid "Add tag"
980msgstr "Ajouter un tag" 1029msgstr "Ajouter un tag"
981 1030
@@ -1021,8 +1070,8 @@ msgstr ""
1021"réessayer plus tard." 1070"réessayer plus tard."
1022 1071
1023#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 1072#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1024#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151 1073#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1025#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151 1074#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:155
1026msgid "Remember me" 1075msgid "Remember me"
1027msgstr "Rester connecté" 1076msgstr "Rester connecté"
1028 1077
@@ -1053,35 +1102,52 @@ msgstr "Déplier tout"
1053msgid "Are you sure you want to delete this link?" 1102msgid "Are you sure you want to delete this link?"
1054msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?" 1103msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
1055 1104
1056#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 1105#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
1057#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 1106#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:90
1058#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61 1107#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:65
1059#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86 1108#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:90
1060msgid "RSS Feed" 1109msgid "RSS Feed"
1061msgstr "Flux RSS" 1110msgstr "Flux RSS"
1062 1111
1063#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 1112#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
1064#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 1113#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
1065#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66 1114#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:70
1066#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102 1115#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:106
1067msgid "Logout" 1116msgid "Logout"
1068msgstr "Déconnexion" 1117msgstr "Déconnexion"
1069 1118
1070#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 1119#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
1071#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169 1120#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:173
1072msgid "is available" 1121msgid "is available"
1073msgstr "est disponible" 1122msgstr "est disponible"
1074 1123
1075#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 1124#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
1076#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176 1125#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:180
1077msgid "Error" 1126msgid "Error"
1078msgstr "Erreur" 1127msgstr "Erreur"
1079 1128
1080#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 1129#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1130msgid "Picture wall unavailable (thumbnails are disabled)."
1131msgstr ""
1132"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
1133
1134#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
1135#, fuzzy
1136#| msgid ""
1137#| "You don't have any cached thumbnail. Try to <a href=\"?do=thumbs_update"
1138#| "\">synchronize them</a>."
1139msgid ""
1140"There is no cached thumbnail. Try to <a href=\"?do=thumbs_update"
1141"\">synchronize them</a>."
1142msgstr ""
1143"Il n'y a aucune miniature en cache. Essayer de <a href=\"?do=thumbs_update"
1144"\">les synchroniser</a>."
1145
1146#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1081msgid "Picture Wall" 1147msgid "Picture Wall"
1082msgstr "Mur d'images" 1148msgstr "Mur d'images"
1083 1149
1084#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 1150#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1085msgid "pics" 1151msgid "pics"
1086msgstr "images" 1152msgstr "images"
1087 1153
@@ -1223,7 +1289,11 @@ msgstr ""
1223msgid "Export database" 1289msgid "Export database"
1224msgstr "Exporter les données" 1290msgstr "Exporter les données"
1225 1291
1226#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 1292#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55
1293msgid "Synchronize all link thumbnails"
1294msgstr "Synchroniser toutes les miniatures"
1295
1296#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
1227msgid "" 1297msgid ""
1228"Drag one of these button to your bookmarks toolbar or right-click it and " 1298"Drag one of these button to your bookmarks toolbar or right-click it and "
1229"\"Bookmark This Link\"" 1299"\"Bookmark This Link\""
@@ -1231,13 +1301,13 @@ msgstr ""
1231"Glisser un de ces bouttons dans votre barre de favoris ou cliquer droit " 1301"Glisser un de ces bouttons dans votre barre de favoris ou cliquer droit "
1232"dessus et « Ajouter aux favoris »" 1302"dessus et « Ajouter aux favoris »"
1233 1303
1234#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 1304#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
1235msgid "then click on the bookmarklet in any page you want to share." 1305msgid "then click on the bookmarklet in any page you want to share."
1236msgstr "" 1306msgstr ""
1237"puis cliquer sur le marque page depuis un site que vous souhaitez partager." 1307"puis cliquer sur le marque page depuis un site que vous souhaitez partager."
1238 1308
1239#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 1309#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1240#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100 1310#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
1241msgid "" 1311msgid ""
1242"Drag this link to your bookmarks toolbar or right-click it and Bookmark This " 1312"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
1243"Link" 1313"Link"
@@ -1245,31 +1315,31 @@ msgstr ""
1245"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " 1315"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1246"Ajouter aux favoris »" 1316"Ajouter aux favoris »"
1247 1317
1248#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 1318#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
1249msgid "then click ✚Shaare link button in any page you want to share" 1319msgid "then click ✚Shaare link button in any page you want to share"
1250msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" 1320msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
1251 1321
1252#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 1322#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
1253#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108 1323#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118
1254msgid "The selected text is too long, it will be truncated." 1324msgid "The selected text is too long, it will be truncated."
1255msgstr "Le texte sélectionné est trop long, il sera tronqué." 1325msgstr "Le texte sélectionné est trop long, il sera tronqué."
1256 1326
1257#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 1327#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
1258msgid "Shaare link" 1328msgid "Shaare link"
1259msgstr "Shaare" 1329msgstr "Shaare"
1260 1330
1261#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 1331#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
1262msgid "" 1332msgid ""
1263"Then click ✚Add Note button anytime to start composing a private Note (text " 1333"Then click ✚Add Note button anytime to start composing a private Note (text "
1264"post) to your Shaarli" 1334"post) to your Shaarli"
1265msgstr "" 1335msgstr ""
1266"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" 1336"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
1267 1337
1268#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 1338#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127
1269msgid "Add Note" 1339msgid "Add Note"
1270msgstr "Ajouter une Note" 1340msgstr "Ajouter une Note"
1271 1341
1272#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 1342#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
1273msgid "" 1343msgid ""
1274"You need to browse your Shaarli over <strong>HTTPS</strong> to use this " 1344"You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1275"functionality." 1345"functionality."
@@ -1277,25 +1347,25 @@ msgstr ""
1277"Vous devez utiliser Shaarli en <strong>HTTPS</strong> pour utiliser cette " 1347"Vous devez utiliser Shaarli en <strong>HTTPS</strong> pour utiliser cette "
1278"fonctionalité." 1348"fonctionalité."
1279 1349
1280#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 1350#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
1281msgid "Add to" 1351msgid "Add to"
1282msgstr "Ajouter à" 1352msgstr "Ajouter à"
1283 1353
1284#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145 1354#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1285msgid "3rd party" 1355msgid "3rd party"
1286msgstr "Applications tierces" 1356msgstr "Applications tierces"
1287 1357
1288#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 1358#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157
1289#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153 1359#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:163
1290msgid "Plugin" 1360msgid "Plugin"
1291msgstr "Extension" 1361msgstr "Extension"
1292 1362
1293#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 1363#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:158
1294#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 1364#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
1295msgid "plugin" 1365msgid "plugin"
1296msgstr "extension" 1366msgstr "extension"
1297 1367
1298#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 1368#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191
1299msgid "" 1369msgid ""
1300"Drag this link to your bookmarks toolbar, or right-click it and choose " 1370"Drag this link to your bookmarks toolbar, or right-click it and choose "
1301"Bookmark This Link" 1371"Bookmark This Link"
@@ -1304,6 +1374,26 @@ msgstr ""
1304"Ajouter aux favoris »" 1374"Ajouter aux favoris »"
1305 1375
1306#, fuzzy 1376#, fuzzy
1377#~| msgid "Enable thumbnails"
1378#~ msgid "Synchonize thumbnails"
1379#~ msgstr "Activer les miniatures"
1380
1381#~ msgid "Warning: "
1382#~ msgstr "Attention : "
1383
1384#~ msgid ""
1385#~ "It's recommended to visit the picture wall after enabling this feature."
1386#~ msgstr ""
1387#~ "Il est recommandé de visiter le Mur d'images après avoir activé cette "
1388#~ "fonctionnalité."
1389
1390#~ msgid ""
1391#~ "If you have a large database, the first retrieval may take a few minutes."
1392#~ msgstr ""
1393#~ "Si vous avez beaucoup de liens, la première récupération peut prendre "
1394#~ "plusieurs minutes."
1395
1396#, fuzzy
1307#~| msgid "Change" 1397#~| msgid "Change"
1308#~ msgid "range" 1398#~ msgid "range"
1309#~ msgstr "Changer" 1399#~ msgstr "Changer"
diff --git a/inc/web-thumbnailer.json b/inc/web-thumbnailer.json
new file mode 100644
index 00000000..dcaa149e
--- /dev/null
+++ b/inc/web-thumbnailer.json
@@ -0,0 +1,13 @@
1{
2 "settings": {
3 "default": {
4 "download_mode": "DOWNLOAD",
5 "_comment": "infinite cache",
6 "cache_duration": -1,
7 "timeout": 10
8 },
9 "path": {
10 "cache": "cache/"
11 }
12 }
13}
diff --git a/index.php b/index.php
index 5fc880e6..1480bbc5 100644
--- a/index.php
+++ b/index.php
@@ -75,11 +75,12 @@ require_once 'application/Utils.php';
75require_once 'application/PluginManager.php'; 75require_once 'application/PluginManager.php';
76require_once 'application/Router.php'; 76require_once 'application/Router.php';
77require_once 'application/Updater.php'; 77require_once 'application/Updater.php';
78use \Shaarli\Languages;
79use \Shaarli\ThemeUtils;
80use \Shaarli\Config\ConfigManager; 78use \Shaarli\Config\ConfigManager;
79use \Shaarli\Languages;
81use \Shaarli\Security\LoginManager; 80use \Shaarli\Security\LoginManager;
82use \Shaarli\Security\SessionManager; 81use \Shaarli\Security\SessionManager;
82use \Shaarli\ThemeUtils;
83use \Shaarli\Thumbnailer;
83 84
84// Ensure the PHP version is supported 85// Ensure the PHP version is supported
85try { 86try {
@@ -513,7 +514,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
513 read_updates_file($conf->get('resource.updates')), 514 read_updates_file($conf->get('resource.updates')),
514 $LINKSDB, 515 $LINKSDB,
515 $conf, 516 $conf,
516 $loginManager->isLoggedIn() 517 $loginManager->isLoggedIn(),
518 $_SESSION
517 ); 519 );
518 try { 520 try {
519 $newUpdates = $updater->update(); 521 $newUpdates = $updater->update();
@@ -528,7 +530,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
528 die($e->getMessage()); 530 die($e->getMessage());
529 } 531 }
530 532
531 $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn()); 533 $PAGE = new PageBuilder($conf, $_SESSION, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
532 $PAGE->assign('linkcount', count($LINKSDB)); 534 $PAGE->assign('linkcount', count($LINKSDB));
533 $PAGE->assign('privateLinkcount', count_private($LINKSDB)); 535 $PAGE->assign('privateLinkcount', count_private($LINKSDB));
534 $PAGE->assign('plugin_errors', $pluginManager->getErrors()); 536 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
@@ -601,19 +603,23 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
601 // -------- Picture wall 603 // -------- Picture wall
602 if ($targetPage == Router::$PAGE_PICWALL) 604 if ($targetPage == Router::$PAGE_PICWALL)
603 { 605 {
606 $PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
607 if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
608 $PAGE->assign('linksToDisplay', []);
609 $PAGE->renderPage('picwall');
610 exit;
611 }
612
604 // Optionally filter the results: 613 // Optionally filter the results:
605 $links = $LINKSDB->filterSearch($_GET); 614 $links = $LINKSDB->filterSearch($_GET);
606 $linksToDisplay = array(); 615 $linksToDisplay = array();
607 616
608 // Get only links which have a thumbnail. 617 // Get only links which have a thumbnail.
609 foreach($links as $link) 618 // Note: we do not retrieve thumbnails here, the request is too heavy.
619 foreach($links as $key => $link)
610 { 620 {
611 $permalink='?'.$link['shorturl']; 621 if (isset($link['thumbnail']) && $link['thumbnail'] !== false) {
612 $thumb=lazyThumbnail($conf, $link['url'],$permalink); 622 $linksToDisplay[] = $link; // Add to array.
613 if ($thumb!='') // Only output links which have a thumbnail.
614 {
615 $link['thumbnail']=$thumb; // Thumbnail HTML code.
616 $linksToDisplay[]=$link; // Add to array.
617 } 623 }
618 } 624 }
619 625
@@ -626,7 +632,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
626 $PAGE->assign($key, $value); 632 $PAGE->assign($key, $value);
627 } 633 }
628 634
629 $PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli')); 635
630 $PAGE->renderPage('picwall'); 636 $PAGE->renderPage('picwall');
631 exit; 637 exit;
632 } 638 }
@@ -1009,6 +1015,16 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1009 $conf->set('api.secret', escape($_POST['apiSecret'])); 1015 $conf->set('api.secret', escape($_POST['apiSecret']));
1010 $conf->set('translation.language', escape($_POST['language'])); 1016 $conf->set('translation.language', escape($_POST['language']));
1011 1017
1018 $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
1019 if ($thumbnailsMode !== Thumbnailer::MODE_NONE
1020 && $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
1021 ) {
1022 $_SESSION['warnings'][] = t(
1023 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
1024 );
1025 }
1026 $conf->set('thumbnails.mode', $thumbnailsMode);
1027
1012 try { 1028 try {
1013 $conf->write($loginManager->isLoggedIn()); 1029 $conf->write($loginManager->isLoggedIn());
1014 $history->updateSettings(); 1030 $history->updateSettings();
@@ -1047,6 +1063,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1047 $PAGE->assign('api_secret', $conf->get('api.secret')); 1063 $PAGE->assign('api_secret', $conf->get('api.secret'));
1048 $PAGE->assign('languages', Languages::getAvailableLanguages()); 1064 $PAGE->assign('languages', Languages::getAvailableLanguages());
1049 $PAGE->assign('language', $conf->get('translation.language')); 1065 $PAGE->assign('language', $conf->get('translation.language'));
1066 $PAGE->assign('gd_enabled', extension_loaded('gd'));
1067 $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
1050 $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli')); 1068 $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
1051 $PAGE->renderPage('configure'); 1069 $PAGE->renderPage('configure');
1052 exit; 1070 exit;
@@ -1148,6 +1166,11 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1148 $link['title'] = $link['url']; 1166 $link['title'] = $link['url'];
1149 } 1167 }
1150 1168
1169 if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE) {
1170 $thumbnailer = new Thumbnailer($conf);
1171 $link['thumbnail'] = $thumbnailer->get($url);
1172 }
1173
1151 $pluginManager->executeHooks('save_link', $link); 1174 $pluginManager->executeHooks('save_link', $link);
1152 1175
1153 $LINKSDB[$id] = $link; 1176 $LINKSDB[$id] = $link;
@@ -1486,6 +1509,43 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1486 exit; 1509 exit;
1487 } 1510 }
1488 1511
1512 // -------- Thumbnails Update
1513 if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
1514 $ids = [];
1515 foreach ($LINKSDB as $link) {
1516 // A note or not HTTP(S)
1517 if ($link['url'][0] === '?' || ! startsWith(strtolower($link['url']), 'http')) {
1518 continue;
1519 }
1520 $ids[] = $link['id'];
1521 }
1522 $PAGE->assign('ids', $ids);
1523 $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
1524 $PAGE->renderPage('thumbnails');
1525 exit;
1526 }
1527
1528 // -------- Single Thumbnail Update
1529 if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
1530 if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
1531 http_response_code(400);
1532 exit;
1533 }
1534 $id = (int) $_POST['id'];
1535 if (empty($LINKSDB[$id])) {
1536 http_response_code(404);
1537 exit;
1538 }
1539 $thumbnailer = new Thumbnailer($conf);
1540 $link = $LINKSDB[$id];
1541 $link['thumbnail'] = $thumbnailer->get($link['url']);
1542 $LINKSDB[$id] = $link;
1543 $LINKSDB->save($conf->get('resource.page_cache'));
1544
1545 echo json_encode($link);
1546 exit;
1547 }
1548
1489 // -------- Otherwise, simply display search form and links: 1549 // -------- Otherwise, simply display search form and links:
1490 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); 1550 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
1491 exit; 1551 exit;
@@ -1549,6 +1609,12 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1549 // Start index. 1609 // Start index.
1550 $i = ($page-1) * $_SESSION['LINKS_PER_PAGE']; 1610 $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
1551 $end = $i + $_SESSION['LINKS_PER_PAGE']; 1611 $end = $i + $_SESSION['LINKS_PER_PAGE'];
1612
1613 $thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE;
1614 if ($thumbnailsEnabled) {
1615 $thumbnailer = new Thumbnailer($conf);
1616 }
1617
1552 $linkDisp = array(); 1618 $linkDisp = array();
1553 while ($i<$end && $i<count($keys)) 1619 while ($i<$end && $i<count($keys))
1554 { 1620 {
@@ -1569,9 +1635,21 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1569 $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY); 1635 $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
1570 uasort($taglist, 'strcasecmp'); 1636 uasort($taglist, 'strcasecmp');
1571 $link['taglist'] = $taglist; 1637 $link['taglist'] = $taglist;
1638
1639 // Thumbnails enabled, not a note,
1640 // and (never retrieved yet or no valid cache file)
1641 if ($thumbnailsEnabled && $link['url'][0] != '?'
1642 && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail'])))
1643 ) {
1644 $elem = $LINKSDB[$keys[$i]];
1645 $elem['thumbnail'] = $thumbnailer->get($link['url']);
1646 $LINKSDB[$keys[$i]] = $elem;
1647 $updateDB = true;
1648 $link['thumbnail'] = $elem['thumbnail'];
1649 }
1650
1572 // Check for both signs of a note: starting with ? and 7 chars long. 1651 // Check for both signs of a note: starting with ? and 7 chars long.
1573 if ($link['url'][0] === '?' && 1652 if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
1574 strlen($link['url']) === 7) {
1575 $link['url'] = index_url($_SERVER) . $link['url']; 1653 $link['url'] = index_url($_SERVER) . $link['url'];
1576 } 1654 }
1577 1655
@@ -1579,6 +1657,11 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1579 $i++; 1657 $i++;
1580 } 1658 }
1581 1659
1660 // If we retrieved new thumbnails, we update the database.
1661 if (!empty($updateDB)) {
1662 $LINKSDB->save($conf->get('resource.page_cache'));
1663 }
1664
1582 // Compute paging navigation 1665 // Compute paging navigation
1583 $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags); 1666 $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags);
1584 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); 1667 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
@@ -1630,194 +1713,6 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1630} 1713}
1631 1714
1632/** 1715/**
1633 * Compute the thumbnail for a link.
1634 *
1635 * With a link to the original URL.
1636 * Understands various services (youtube.com...)
1637 * Input: $url = URL for which the thumbnail must be found.
1638 * $href = if provided, this URL will be followed instead of $url
1639 * Returns an associative array with thumbnail attributes (src,href,width,height,style,alt)
1640 * Some of them may be missing.
1641 * Return an empty array if no thumbnail available.
1642 *
1643 * @param ConfigManager $conf Configuration Manager instance.
1644 * @param string $url
1645 * @param string|bool $href
1646 *
1647 * @return array
1648 */
1649function computeThumbnail($conf, $url, $href = false)
1650{
1651 if (!$conf->get('thumbnail.enable_thumbnails')) return array();
1652 if ($href==false) $href=$url;
1653
1654 // For most hosts, the URL of the thumbnail can be easily deduced from the URL of the link.
1655 // (e.g. http://www.youtube.com/watch?v=spVypYk4kto ---> http://img.youtube.com/vi/spVypYk4kto/default.jpg )
1656 // ^^^^^^^^^^^ ^^^^^^^^^^^
1657 $domain = parse_url($url,PHP_URL_HOST);
1658 if ($domain=='youtube.com' || $domain=='www.youtube.com')
1659 {
1660 parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract video ID and get thumbnail
1661 if (!empty($params['v'])) return array('src'=>'https://img.youtube.com/vi/'.$params['v'].'/default.jpg',
1662 'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail');
1663 }
1664 if ($domain=='youtu.be') // Youtube short links
1665 {
1666 $path = parse_url($url,PHP_URL_PATH);
1667 return array('src'=>'https://img.youtube.com/vi'.$path.'/default.jpg',
1668 'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail');
1669 }
1670 if ($domain=='pix.toile-libre.org') // pix.toile-libre.org image hosting
1671 {
1672 parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract image filename.
1673 if (!empty($params) && !empty($params['img'])) return array('src'=>'http://pix.toile-libre.org/upload/thumb/'.urlencode($params['img']),
1674 'href'=>$href,'style'=>'max-width:120px; max-height:150px','alt'=>'pix.toile-libre.org thumbnail');
1675 }
1676
1677 if ($domain=='imgur.com')
1678 {
1679 $path = parse_url($url,PHP_URL_PATH);
1680 if (startsWith($path,'/a/')) return array(); // Thumbnails for albums are not available.
1681 if (startsWith($path,'/r/')) return array('src'=>'https://i.imgur.com/'.basename($path).'s.jpg',
1682 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
1683 if (startsWith($path,'/gallery/')) return array('src'=>'https://i.imgur.com'.substr($path,8).'s.jpg',
1684 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
1685
1686 if (substr_count($path,'/')==1) return array('src'=>'https://i.imgur.com/'.substr($path,1).'s.jpg',
1687 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
1688 }
1689 if ($domain=='i.imgur.com')
1690 {
1691 $pi = pathinfo(parse_url($url,PHP_URL_PATH));
1692 if (!empty($pi['filename'])) return array('src'=>'https://i.imgur.com/'.$pi['filename'].'s.jpg',
1693 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
1694 }
1695 if ($domain=='dailymotion.com' || $domain=='www.dailymotion.com')
1696 {
1697 if (strpos($url,'dailymotion.com/video/')!==false)
1698 {
1699 $thumburl=str_replace('dailymotion.com/video/','dailymotion.com/thumbnail/video/',$url);
1700 return array('src'=>$thumburl,
1701 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'DailyMotion thumbnail');
1702 }
1703 }
1704 if (endsWith($domain,'.imageshack.us'))
1705 {
1706 $ext=strtolower(pathinfo($url,PATHINFO_EXTENSION));
1707 if ($ext=='jpg' || $ext=='jpeg' || $ext=='png' || $ext=='gif')
1708 {
1709 $thumburl = substr($url,0,strlen($url)-strlen($ext)).'th.'.$ext;
1710 return array('src'=>$thumburl,
1711 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'imageshack.us thumbnail');
1712 }
1713 }
1714
1715 // Some other hosts are SLOW AS HELL and usually require an extra HTTP request to get the thumbnail URL.
1716 // So we deport the thumbnail generation in order not to slow down page generation
1717 // (and we also cache the thumbnail)
1718
1719 if (! $conf->get('thumbnail.enable_localcache')) return array(); // If local cache is disabled, no thumbnails for services which require the use a local cache.
1720
1721 if ($domain=='flickr.com' || endsWith($domain,'.flickr.com')
1722 || $domain=='vimeo.com'
1723 || $domain=='ted.com' || endsWith($domain,'.ted.com')
1724 || $domain=='xkcd.com' || endsWith($domain,'.xkcd.com')
1725 )
1726 {
1727 if ($domain=='vimeo.com')
1728 { // Make sure this vimeo URL points to a video (/xxx... where xxx is numeric)
1729 $path = parse_url($url,PHP_URL_PATH);
1730 if (!preg_match('!/\d+.+?!',$path)) return array(); // This is not a single video URL.
1731 }
1732 if ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com'))
1733 { // Make sure this URL points to a single comic (/xxx... where xxx is numeric)
1734 $path = parse_url($url,PHP_URL_PATH);
1735 if (!preg_match('!/\d+.+?!',$path)) return array();
1736 }
1737 if ($domain=='ted.com' || endsWith($domain,'.ted.com'))
1738 { // Make sure this TED URL points to a video (/talks/...)
1739 $path = parse_url($url,PHP_URL_PATH);
1740 if ("/talks/" !== substr($path,0,7)) return array(); // This is not a single video URL.
1741 }
1742 $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)
1743 return array('src'=>index_url($_SERVER).'?do=genthumbnail&hmac='.$sign.'&url='.urlencode($url),
1744 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail');
1745 }
1746
1747 // For all other, we try to make a thumbnail of links ending with .jpg/jpeg/png/gif
1748 // Technically speaking, we should download ALL links and check their Content-Type to see if they are images.
1749 // But using the extension will do.
1750 $ext=strtolower(pathinfo($url,PATHINFO_EXTENSION));
1751 if ($ext=='jpg' || $ext=='jpeg' || $ext=='png' || $ext=='gif')
1752 {
1753 $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)
1754 return array('src'=>index_url($_SERVER).'?do=genthumbnail&hmac='.$sign.'&url='.urlencode($url),
1755 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail');
1756 }
1757 return array(); // No thumbnail.
1758
1759}
1760
1761
1762// Returns the HTML code to display a thumbnail for a link
1763// with a link to the original URL.
1764// Understands various services (youtube.com...)
1765// Input: $url = URL for which the thumbnail must be found.
1766// $href = if provided, this URL will be followed instead of $url
1767// Returns '' if no thumbnail available.
1768function thumbnail($url,$href=false)
1769{
1770 // FIXME!
1771 global $conf;
1772 $t = computeThumbnail($conf, $url,$href);
1773 if (count($t)==0) return ''; // Empty array = no thumbnail for this URL.
1774
1775 $html='<a href="'.escape($t['href']).'"><img src="'.escape($t['src']).'"';
1776 if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"';
1777 if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"';
1778 if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"';
1779 if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"';
1780 $html.='></a>';
1781 return $html;
1782}
1783
1784// Returns the HTML code to display a thumbnail for a link
1785// for the picture wall (using lazy image loading)
1786// Understands various services (youtube.com...)
1787// Input: $url = URL for which the thumbnail must be found.
1788// $href = if provided, this URL will be followed instead of $url
1789// Returns '' if no thumbnail available.
1790function lazyThumbnail($conf, $url,$href=false)
1791{
1792 // FIXME!
1793 global $conf;
1794 $t = computeThumbnail($conf, $url,$href);
1795 if (count($t)==0) return ''; // Empty array = no thumbnail for this URL.
1796
1797 $html='<a href="'.escape($t['href']).'">';
1798
1799 // Lazy image
1800 $html.='<img class="b-lazy" src="#" data-src="'.escape($t['src']).'"';
1801
1802 if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"';
1803 if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"';
1804 if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"';
1805 if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"';
1806 $html.='>';
1807
1808 // No-JavaScript fallback.
1809 $html.='<noscript><img src="'.escape($t['src']).'"';
1810 if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"';
1811 if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"';
1812 if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"';
1813 if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"';
1814 $html.='></noscript></a>';
1815
1816 return $html;
1817}
1818
1819
1820/**
1821 * Installation 1716 * Installation
1822 * This function should NEVER be called if the file data/config.php exists. 1717 * This function should NEVER be called if the file data/config.php exists.
1823 * 1718 *
@@ -1908,7 +1803,7 @@ function install($conf, $sessionManager, $loginManager) {
1908 exit; 1803 exit;
1909 } 1804 }
1910 1805
1911 $PAGE = new PageBuilder($conf, null, $sessionManager->generateToken()); 1806 $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
1912 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); 1807 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
1913 $PAGE->assign('continents', $continents); 1808 $PAGE->assign('continents', $continents);
1914 $PAGE->assign('cities', $cities); 1809 $PAGE->assign('cities', $cities);
@@ -1917,232 +1812,6 @@ function install($conf, $sessionManager, $loginManager) {
1917 exit; 1812 exit;
1918} 1813}
1919 1814
1920/**
1921 * Because some f*cking services like flickr require an extra HTTP request to get the thumbnail URL,
1922 * I have deported the thumbnail URL code generation here, otherwise this would slow down page generation.
1923 * The following function takes the URL a link (e.g. a flickr page) and return the proper thumbnail.
1924 * This function is called by passing the URL:
1925 * http://mywebsite.com/shaarli/?do=genthumbnail&hmac=[HMAC]&url=[URL]
1926 * [URL] is the URL of the link (e.g. a flickr page)
1927 * [HMAC] is the signature for the [URL] (so that these URL cannot be forged).
1928 * The function below will fetch the image from the webservice and store it in the cache.
1929 *
1930 * @param ConfigManager $conf Configuration Manager instance,
1931 */
1932function genThumbnail($conf)
1933{
1934 // Make sure the parameters in the URL were generated by us.
1935 $sign = hash_hmac('sha256', $_GET['url'], $conf->get('credentials.salt'));
1936 if ($sign!=$_GET['hmac']) die('Naughty boy!');
1937
1938 $cacheDir = $conf->get('resource.thumbnails_cache', 'cache');
1939 // Let's see if we don't already have the image for this URL in the cache.
1940 $thumbname=hash('sha1',$_GET['url']).'.jpg';
1941 if (is_file($cacheDir .'/'. $thumbname))
1942 { // We have the thumbnail, just serve it:
1943 header('Content-Type: image/jpeg');
1944 echo file_get_contents($cacheDir .'/'. $thumbname);
1945 return;
1946 }
1947 // We may also serve a blank image (if service did not respond)
1948 $blankname=hash('sha1',$_GET['url']).'.gif';
1949 if (is_file($cacheDir .'/'. $blankname))
1950 {
1951 header('Content-Type: image/gif');
1952 echo file_get_contents($cacheDir .'/'. $blankname);
1953 return;
1954 }
1955
1956 // Otherwise, generate the thumbnail.
1957 $url = $_GET['url'];
1958 $domain = parse_url($url,PHP_URL_HOST);
1959
1960 if ($domain=='flickr.com' || endsWith($domain,'.flickr.com'))
1961 {
1962 // Crude replacement to handle new flickr domain policy (They prefer www. now)
1963 $url = str_replace('http://flickr.com/','http://www.flickr.com/',$url);
1964
1965 // Is this a link to an image, or to a flickr page ?
1966 $imageurl='';
1967 if (endsWith(parse_url($url, PHP_URL_PATH), '.jpg'))
1968 { // This is a direct link to an image. e.g. http://farm1.staticflickr.com/5/5921913_ac83ed27bd_o.jpg
1969 preg_match('!(http://farm\d+\.staticflickr\.com/\d+/\d+_\w+_)\w.jpg!',$url,$matches);
1970 if (!empty($matches[1])) $imageurl=$matches[1].'m.jpg';
1971 }
1972 else // This is a flickr page (html)
1973 {
1974 // Get the flickr html page.
1975 list($headers, $content) = get_http_response($url, 20);
1976 if (strpos($headers[0], '200 OK') !== false)
1977 {
1978 // flickr now nicely provides the URL of the thumbnail in each flickr page.
1979 preg_match('!<link rel=\"image_src\" href=\"(.+?)\"!', $content, $matches);
1980 if (!empty($matches[1])) $imageurl=$matches[1];
1981
1982 // In albums (and some other pages), the link rel="image_src" is not provided,
1983 // but flickr provides:
1984 // <meta property="og:image" content="http://farm4.staticflickr.com/3398/3239339068_25d13535ff_z.jpg" />
1985 if ($imageurl=='')
1986 {
1987 preg_match('!<meta property=\"og:image\" content=\"(.+?)\"!', $content, $matches);
1988 if (!empty($matches[1])) $imageurl=$matches[1];
1989 }
1990 }
1991 }
1992
1993 if ($imageurl!='')
1994 { // Let's download the image.
1995 // Image is 240x120, so 10 seconds to download should be enough.
1996 list($headers, $content) = get_http_response($imageurl, 10);
1997 if (strpos($headers[0], '200 OK') !== false) {
1998 // Save image to cache.
1999 file_put_contents($cacheDir .'/'. $thumbname, $content);
2000 header('Content-Type: image/jpeg');
2001 echo $content;
2002 return;
2003 }
2004 }
2005 }
2006
2007 elseif ($domain=='vimeo.com' )
2008 {
2009 // This is more complex: we have to perform a HTTP request, then parse the result.
2010 // Maybe we should deport this to JavaScript ? Example: http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo/4285098#4285098
2011 $vid = substr(parse_url($url,PHP_URL_PATH),1);
2012 list($headers, $content) = get_http_response('https://vimeo.com/api/v2/video/'.escape($vid).'.php', 5);
2013 if (strpos($headers[0], '200 OK') !== false) {
2014 $t = unserialize($content);
2015 $imageurl = $t[0]['thumbnail_medium'];
2016 // Then we download the image and serve it to our client.
2017 list($headers, $content) = get_http_response($imageurl, 10);
2018 if (strpos($headers[0], '200 OK') !== false) {
2019 // Save image to cache.
2020 file_put_contents($cacheDir .'/'. $thumbname, $content);
2021 header('Content-Type: image/jpeg');
2022 echo $content;
2023 return;
2024 }
2025 }
2026 }
2027
2028 elseif ($domain=='ted.com' || endsWith($domain,'.ted.com'))
2029 {
2030 // The thumbnail for TED talks is located in the <link rel="image_src" [...]> tag on that page
2031 // http://www.ted.com/talks/mikko_hypponen_fighting_viruses_defending_the_net.html
2032 // <link rel="image_src" href="http://images.ted.com/images/ted/28bced335898ba54d4441809c5b1112ffaf36781_389x292.jpg" />
2033 list($headers, $content) = get_http_response($url, 5);
2034 if (strpos($headers[0], '200 OK') !== false) {
2035 // Extract the link to the thumbnail
2036 preg_match('!link rel="image_src" href="(http://images.ted.com/images/ted/.+_\d+x\d+\.jpg)"!', $content, $matches);
2037 if (!empty($matches[1]))
2038 { // Let's download the image.
2039 $imageurl=$matches[1];
2040 // No control on image size, so wait long enough
2041 list($headers, $content) = get_http_response($imageurl, 20);
2042 if (strpos($headers[0], '200 OK') !== false) {
2043 $filepath = $cacheDir .'/'. $thumbname;
2044 file_put_contents($filepath, $content); // Save image to cache.
2045 if (resizeImage($filepath))
2046 {
2047 header('Content-Type: image/jpeg');
2048 echo file_get_contents($filepath);
2049 return;
2050 }
2051 }
2052 }
2053 }
2054 }
2055
2056 elseif ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com'))
2057 {
2058 // There is no thumbnail available for xkcd comics, so download the whole image and resize it.
2059 // http://xkcd.com/327/
2060 // <img src="http://imgs.xkcd.com/comics/exploits_of_a_mom.png" title="<BLABLA>" alt="<BLABLA>" />
2061 list($headers, $content) = get_http_response($url, 5);
2062 if (strpos($headers[0], '200 OK') !== false) {
2063 // Extract the link to the thumbnail
2064 preg_match('!<img src="(http://imgs.xkcd.com/comics/.*)" title="[^s]!', $content, $matches);
2065 if (!empty($matches[1]))
2066 { // Let's download the image.
2067 $imageurl=$matches[1];
2068 // No control on image size, so wait long enough
2069 list($headers, $content) = get_http_response($imageurl, 20);
2070 if (strpos($headers[0], '200 OK') !== false) {
2071 $filepath = $cacheDir.'/'.$thumbname;
2072 // Save image to cache.
2073 file_put_contents($filepath, $content);
2074 if (resizeImage($filepath))
2075 {
2076 header('Content-Type: image/jpeg');
2077 echo file_get_contents($filepath);
2078 return;
2079 }
2080 }
2081 }
2082 }
2083 }
2084
2085 else
2086 {
2087 // For all other domains, we try to download the image and make a thumbnail.
2088 // We allow 30 seconds max to download (and downloads are limited to 4 Mb)
2089 list($headers, $content) = get_http_response($url, 30);
2090 if (strpos($headers[0], '200 OK') !== false) {
2091 $filepath = $cacheDir .'/'.$thumbname;
2092 // Save image to cache.
2093 file_put_contents($filepath, $content);
2094 if (resizeImage($filepath))
2095 {
2096 header('Content-Type: image/jpeg');
2097 echo file_get_contents($filepath);
2098 return;
2099 }
2100 }
2101 }
2102
2103
2104 // Otherwise, return an empty image (8x8 transparent gif)
2105 $blankgif = base64_decode('R0lGODlhCAAIAIAAAP///////yH5BAEKAAEALAAAAAAIAAgAAAIHjI+py+1dAAA7');
2106 // Also put something in cache so that this URL is not requested twice.
2107 file_put_contents($cacheDir .'/'. $blankname, $blankgif);
2108 header('Content-Type: image/gif');
2109 echo $blankgif;
2110}
2111
2112// Make a thumbnail of the image (to width: 120 pixels)
2113// Returns true if success, false otherwise.
2114function resizeImage($filepath)
2115{
2116 if (!function_exists('imagecreatefromjpeg')) return false; // GD not present: no thumbnail possible.
2117
2118 // Trick: some stupid people rename GIF as JPEG... or else.
2119 // So we really try to open each image type whatever the extension is.
2120 $header=file_get_contents($filepath,false,NULL,0,256); // Read first 256 bytes and try to sniff file type.
2121 $im=false;
2122 $i=strpos($header,'GIF8'); if (($i!==false) && ($i==0)) $im = imagecreatefromgif($filepath); // Well this is crude, but it should be enough.
2123 $i=strpos($header,'PNG'); if (($i!==false) && ($i==1)) $im = imagecreatefrompng($filepath);
2124 $i=strpos($header,'JFIF'); if ($i!==false) $im = imagecreatefromjpeg($filepath);
2125 if (!$im) return false; // Unable to open image (corrupted or not an image)
2126 $w = imagesx($im);
2127 $h = imagesy($im);
2128 $ystart = 0; $yheight=$h;
2129 if ($h>$w) { $ystart= ($h/2)-($w/2); $yheight=$w/2; }
2130 $nw = 120; // Desired width
2131 $nh = min(floor(($h*$nw)/$w),120); // Compute new width/height, but maximum 120 pixels height.
2132 // Resize image:
2133 $im2 = imagecreatetruecolor($nw,$nh);
2134 imagecopyresampled($im2, $im, 0, 0, 0, $ystart, $nw, $nh, $w, $yheight);
2135 imageinterlace($im2,true); // For progressive JPEG.
2136 $tempname=$filepath.'_TEMP.jpg';
2137 imagejpeg($im2, $tempname, 90);
2138 imagedestroy($im);
2139 imagedestroy($im2);
2140 unlink($filepath);
2141 rename($tempname,$filepath); // Overwrite original picture with thumbnail.
2142 return true;
2143}
2144
2145if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=genthumbnail')) { genThumbnail($conf); exit; } // Thumbnail generation/cache does not need the link database.
2146if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) { showDailyRSS($conf); exit; } 1815if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) { showDailyRSS($conf); exit; }
2147if (!isset($_SESSION['LINKS_PER_PAGE'])) { 1816if (!isset($_SESSION['LINKS_PER_PAGE'])) {
2148 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); 1817 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
diff --git a/mkdocs.yml b/mkdocs.yml
index ae38459b..941fce3a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -39,6 +39,7 @@ pages:
39 - Continuous integration tools: Continuous-integration-tools.md 39 - Continuous integration tools: Continuous-integration-tools.md
40 - GnuPG signature: GnuPG-signature.md 40 - GnuPG signature: GnuPG-signature.md
41 - Directory structure: Directory-structure.md 41 - Directory structure: Directory-structure.md
42 - Link Structure: Link-structure.md
42 - 3rd party libraries: 3rd-party-libraries.md 43 - 3rd party libraries: 3rd-party-libraries.md
43 - Plugin System: Plugin-System.md 44 - Plugin System: Plugin-System.md
44 - Release Shaarli: Release-Shaarli.md 45 - Release Shaarli: Release-Shaarli.md
diff --git a/tests/ThumbnailerTest.php b/tests/ThumbnailerTest.php
new file mode 100644
index 00000000..08311545
--- /dev/null
+++ b/tests/ThumbnailerTest.php
@@ -0,0 +1,114 @@
1<?php
2
3namespace Shaarli;
4
5use PHPUnit\Framework\TestCase;
6use Shaarli\Config\ConfigManager;
7use WebThumbnailer\Application\ConfigManager as WTConfigManager;
8
9/**
10 * Class ThumbnailerTest
11 *
12 * We only make 1 thumb test because:
13 *
14 * 1. the thumbnailer library is itself tested
15 * 2. we don't want to make too many external requests during the tests
16 */
17class ThumbnailerTest extends TestCase
18{
19 const WIDTH = 190;
20
21 const HEIGHT = 210;
22
23 /**
24 * @var Thumbnailer;
25 */
26 protected $thumbnailer;
27
28 /**
29 * @var ConfigManager
30 */
31 protected $conf;
32
33 public function setUp()
34 {
35 $this->conf = new ConfigManager('tests/utils/config/configJson');
36 $this->conf->set('thumbnails.mode', Thumbnailer::MODE_ALL);
37 $this->conf->set('thumbnails.width', self::WIDTH);
38 $this->conf->set('thumbnails.height', self::HEIGHT);
39 $this->conf->set('dev.debug', true);
40
41 $this->thumbnailer = new Thumbnailer($this->conf);
42 // cache files in the sandbox
43 WTConfigManager::addFile('tests/utils/config/wt.json');
44 }
45
46 public function tearDown()
47 {
48 $this->rrmdirContent('sandbox/');
49 }
50
51 /**
52 * Test a thumbnail with a custom size in 'all' mode.
53 */
54 public function testThumbnailAllValid()
55 {
56 $thumb = $this->thumbnailer->get('https://github.com/shaarli/Shaarli/');
57 $this->assertNotFalse($thumb);
58 $image = imagecreatefromstring(file_get_contents($thumb));
59 $this->assertEquals(self::WIDTH, imagesx($image));
60 $this->assertEquals(self::HEIGHT, imagesy($image));
61 }
62
63 /**
64 * Test a thumbnail with a custom size in 'common' mode.
65 */
66 public function testThumbnailCommonValid()
67 {
68 $this->conf->set('thumbnails.mode', Thumbnailer::MODE_COMMON);
69 $thumb = $this->thumbnailer->get('https://imgur.com/jlFgGpe');
70 $this->assertNotFalse($thumb);
71 $image = imagecreatefromstring(file_get_contents($thumb));
72 $this->assertEquals(self::WIDTH, imagesx($image));
73 $this->assertEquals(self::HEIGHT, imagesy($image));
74 }
75
76 /**
77 * Test a thumbnail in 'common' mode which isn't include in common websites.
78 */
79 public function testThumbnailCommonInvalid()
80 {
81 $this->conf->set('thumbnails.mode', Thumbnailer::MODE_COMMON);
82 $thumb = $this->thumbnailer->get('https://github.com/shaarli/Shaarli/');
83 $this->assertFalse($thumb);
84 }
85
86 /**
87 * Test a thumbnail that can't be retrieved.
88 */
89 public function testThumbnailNotValid()
90 {
91 $oldlog = ini_get('error_log');
92 ini_set('error_log', '/dev/null');
93
94 $thumbnailer = new Thumbnailer(new ConfigManager());
95 $thumb = $thumbnailer->get('nope');
96 $this->assertFalse($thumb);
97
98 ini_set('error_log', $oldlog);
99 }
100
101 protected function rrmdirContent($dir) {
102 if (is_dir($dir)) {
103 $objects = scandir($dir);
104 foreach ($objects as $object) {
105 if ($object != "." && $object != "..") {
106 if (is_dir($dir."/".$object))
107 $this->rrmdirContent($dir."/".$object);
108 else
109 unlink($dir."/".$object);
110 }
111 }
112 }
113 }
114}
diff --git a/tests/Updater/UpdaterTest.php b/tests/Updater/UpdaterTest.php
index 94e3c7d3..cacee2d2 100644
--- a/tests/Updater/UpdaterTest.php
+++ b/tests/Updater/UpdaterTest.php
@@ -2,6 +2,7 @@
2use Shaarli\Config\ConfigJson; 2use Shaarli\Config\ConfigJson;
3use Shaarli\Config\ConfigManager; 3use Shaarli\Config\ConfigManager;
4use Shaarli\Config\ConfigPhp; 4use Shaarli\Config\ConfigPhp;
5use Shaarli\Thumbnailer;
5 6
6require_once 'tests/Updater/DummyUpdater.php'; 7require_once 'tests/Updater/DummyUpdater.php';
7require_once 'inc/rain.tpl.class.php'; 8require_once 'inc/rain.tpl.class.php';
@@ -20,7 +21,7 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
20 /** 21 /**
21 * @var string Config file path (without extension). 22 * @var string Config file path (without extension).
22 */ 23 */
23 protected static $configFile = 'tests/utils/config/configJson'; 24 protected static $configFile = 'sandbox/config';
24 25
25 /** 26 /**
26 * @var ConfigManager 27 * @var ConfigManager
@@ -32,6 +33,7 @@ class UpdaterTest extends PHPUnit_Framework_TestCase
32 */ 33 */
33 public function setUp() 34 public function setUp()
34 { 35 {
36 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
35 $this->conf = new ConfigManager(self::$configFile); 37 $this->conf = new ConfigManager(self::$configFile);
36 } 38 }
37 39
@@ -684,4 +686,50 @@ $GLOBALS[\'privateLinkByDefault\'] = true;';
684 $this->assertEquals(4194304, $this->conf->get('general.download_max_size')); 686 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
685 $this->assertEquals(3, $this->conf->get('general.download_timeout')); 687 $this->assertEquals(3, $this->conf->get('general.download_timeout'));
686 } 688 }
689
690 /**
691 * Test updateMethodWebThumbnailer with thumbnails enabled.
692 */
693 public function testUpdateMethodWebThumbnailerEnabled()
694 {
695 $this->conf->remove('thumbnails');
696 $this->conf->set('thumbnail.enable_thumbnails', true);
697 $updater = new Updater([], [], $this->conf, true, $_SESSION);
698 $this->assertTrue($updater->updateMethodWebThumbnailer());
699 $this->assertFalse($this->conf->exists('thumbnail'));
700 $this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode'));
701 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
702 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
703 $this->assertContains('You have enabled or changed thumbnails', $_SESSION['warnings'][0]);
704 }
705
706 /**
707 * Test updateMethodWebThumbnailer with thumbnails disabled.
708 */
709 public function testUpdateMethodWebThumbnailerDisabled()
710 {
711 $this->conf->remove('thumbnails');
712 $this->conf->set('thumbnail.enable_thumbnails', false);
713 $updater = new Updater([], [], $this->conf, true, $_SESSION);
714 $this->assertTrue($updater->updateMethodWebThumbnailer());
715 $this->assertFalse($this->conf->exists('thumbnail'));
716 $this->assertEquals(Thumbnailer::MODE_NONE, $this->conf->get('thumbnails.mode'));
717 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
718 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
719 $this->assertTrue(empty($_SESSION['warnings']));
720 }
721
722 /**
723 * Test updateMethodWebThumbnailer with thumbnails disabled.
724 */
725 public function testUpdateMethodWebThumbnailerNothingToDo()
726 {
727 $updater = new Updater([], [], $this->conf, true, $_SESSION);
728 $this->assertTrue($updater->updateMethodWebThumbnailer());
729 $this->assertFalse($this->conf->exists('thumbnail'));
730 $this->assertEquals(Thumbnailer::MODE_COMMON, $this->conf->get('thumbnails.mode'));
731 $this->assertEquals(90, $this->conf->get('thumbnails.width'));
732 $this->assertEquals(53, $this->conf->get('thumbnails.height'));
733 $this->assertTrue(empty($_SESSION['warnings']));
734 }
687} 735}
diff --git a/tests/config/ConfigManagerTest.php b/tests/config/ConfigManagerTest.php
index 1ec447b2..4a4e94ac 100644
--- a/tests/config/ConfigManagerTest.php
+++ b/tests/config/ConfigManagerTest.php
@@ -81,6 +81,18 @@ class ConfigManagerTest extends \PHPUnit_Framework_TestCase
81 $this->assertEquals('testSetWriteGetNested', $this->conf->get('foo.bar.key.stuff')); 81 $this->assertEquals('testSetWriteGetNested', $this->conf->get('foo.bar.key.stuff'));
82 } 82 }
83 83
84 public function testSetDeleteNested()
85 {
86 $this->conf->set('foo.bar.key.stuff', 'testSetDeleteNested');
87 $this->assertTrue($this->conf->exists('foo.bar'));
88 $this->assertTrue($this->conf->exists('foo.bar.key.stuff'));
89 $this->assertEquals('testSetDeleteNested', $this->conf->get('foo.bar.key.stuff'));
90
91 $this->conf->remove('foo.bar');
92 $this->assertFalse($this->conf->exists('foo.bar.key.stuff'));
93 $this->assertFalse($this->conf->exists('foo.bar'));
94 }
95
84 /** 96 /**
85 * Set with an empty key. 97 * Set with an empty key.
86 * 98 *
@@ -104,6 +116,17 @@ class ConfigManagerTest extends \PHPUnit_Framework_TestCase
104 } 116 }
105 117
106 /** 118 /**
119 * Remove with an empty key.
120 *
121 * @expectedException \Exception
122 * @expectedExceptionMessageRegExp #^Invalid setting key parameter. String expected, got.*#
123 */
124 public function testRmoveEmptyKey()
125 {
126 $this->conf->remove('');
127 }
128
129 /**
107 * Try to write the config without mandatory parameter (e.g. 'login'). 130 * Try to write the config without mandatory parameter (e.g. 'login').
108 * 131 *
109 * @expectedException Shaarli\Config\Exception\MissingFieldConfigException 132 * @expectedException Shaarli\Config\Exception\MissingFieldConfigException
diff --git a/tests/utils/config/configJson.json.php b/tests/utils/config/configJson.json.php
index 9c9288f3..1549ddfc 100644
--- a/tests/utils/config/configJson.json.php
+++ b/tests/utils/config/configJson.json.php
@@ -1,35 +1,84 @@
1<?php /* 1<?php /*
2{ 2{
3 "credentials": { 3 "credentials": {
4 "login":"root", 4 "login": "root",
5 "hash":"hash", 5 "hash": "hash",
6 "salt":"salt" 6 "salt": "salt"
7 }, 7 },
8 "security": { 8 "security": {
9 "session_protection_disabled":false 9 "session_protection_disabled": false,
10 "ban_after": 4,
11 "ban_duration": 1800,
12 "open_shaarli": false,
13 "allowed_protocols": [
14 "ftp",
15 "ftps",
16 "magnet"
17 ]
10 }, 18 },
11 "general": { 19 "general": {
12 "timezone":"Europe\/Paris", 20 "timezone": "Europe\/Paris",
13 "title": "Shaarli", 21 "title": "Shaarli",
14 "header_link": "?" 22 "header_link": "?",
23 "links_per_page": 20,
24 "enabled_plugins": [
25 "qrcode"
26 ],
27 "default_note_title": "Note: "
15 }, 28 },
16 "privacy": { 29 "privacy": {
17 "default_private_links":true 30 "default_private_links": true,
31 "hide_public_links": false,
32 "force_login": false,
33 "hide_timestamps": false,
34 "remember_user_default": true
18 }, 35 },
19 "redirector": { 36 "redirector": {
20 "url":"lala" 37 "url": "lala",
38 "encode_url": true
21 }, 39 },
22 "config": { 40 "config": {
23 "foo": "bar" 41 "foo": "bar"
24 }, 42 },
25 "resource": { 43 "resource": {
26 "datastore": "tests\/utils\/config\/datastore.php", 44 "datastore": "tests\/utils\/config\/datastore.php",
27 "data_dir": "sandbox/", 45 "data_dir": "sandbox\/",
28 "raintpl_tpl": "tpl/" 46 "raintpl_tpl": "tpl\/",
47 "config": "data\/config.php",
48 "ban_file": "data\/ipbans.php",
49 "updates": "data\/updates.txt",
50 "log": "data\/log.txt",
51 "update_check": "data\/lastupdatecheck.txt",
52 "history": "data\/history.php",
53 "theme": "default",
54 "raintpl_tmp": "tmp\/",
55 "thumbnails_cache": "cache",
56 "page_cache": "pagecache"
29 }, 57 },
30 "plugins": { 58 "plugins": {
31 "WALLABAG_VERSION": 1 59 "WALLABAG_VERSION": 1
60 },
61 "dev": {
62 "debug": true
63 },
64 "updates": {
65 "check_updates": false,
66 "check_updates_branch": "stable",
67 "check_updates_interval": 86400
68 },
69 "feed": {
70 "rss_permalinks": true,
71 "show_atom": true
72 },
73 "translation": {
74 "language": "auto",
75 "mode": "php",
76 "extensions": []
77 },
78 "thumbnails": {
79 "mode": "common",
80 "width": 90,
81 "height": 53
32 } 82 }
33} 83}
34*/ ?> 84*/ ?>
35
diff --git a/tests/utils/config/wt.json b/tests/utils/config/wt.json
new file mode 100644
index 00000000..69ce49a6
--- /dev/null
+++ b/tests/utils/config/wt.json
@@ -0,0 +1,12 @@
1{
2 "settings": {
3 "default": {
4 "_comment": "infinite cache",
5 "cache_duration": -1,
6 "timeout": 10
7 },
8 "path": {
9 "cache": "sandbox/"
10 }
11 }
12} \ No newline at end of file
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index a63c7ad3..42e32230 100644
--- a/tpl/default/configure.html
+++ b/tpl/default/configure.html
@@ -242,6 +242,37 @@
242 </div> 242 </div>
243 </div> 243 </div>
244 </div> 244 </div>
245 <div class="pure-g">
246 <div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile}">
247 <div class="form-label">
248 <label for="enableThumbnails">
249 <span class="label-name">{'Enable thumbnails'|t}</span><br>
250 <span class="label-desc">
251 {if="! $gd_enabled"}
252 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
253 {elseif="$thumbnails_enabled"}
254 <a href="?do=thumbs_update">{'Synchronize thumbnails'|t}</a>
255 {/if}
256 </span>
257 </label>
258 </div>
259 </div>
260 <div class="pure-u-lg-{$ratioInput} pure-u-{$ratioInputMobile}">
261 <div class="form-input">
262 <select name="enableThumbnails" id="enableThumbnails" class="align">
263 <option value="all" {if="$thumbnails_mode=='all'"}selected{/if}>
264 {'All'|t}
265 </option>
266 <option value="common" {if="$thumbnails_mode=='common'"}selected{/if}>
267 {'Only common media hosts'|t}
268 </option>
269 <option value="none" {if="$thumbnails_mode=='none'"}selected{/if}>
270 {'None'|t}
271 </option>
272 </select>
273 </div>
274 </div>
275 </div>
245 <div class="center"> 276 <div class="center">
246 <input type="submit" value="{'Save'|t}" name="save"> 277 <input type="submit" value="{'Save'|t}" name="save">
247 </div> 278 </div>
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index 322cddd5..8ea2ce66 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -131,9 +131,17 @@
131 131
132 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> 132 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
133 <div class="linklist-item-title"> 133 <div class="linklist-item-title">
134 {$thumb=thumbnail($value.url)} 134 {if="$thumbnails_enabled && !empty($value.thumbnail)"}
135 {if="$thumb!=false"} 135 <div class="linklist-item-thumbnail" style="width:{$thumbnails_width}px;height:{$thumbnails_height}px;">
136 <div class="linklist-item-thumbnail">{$thumb}</div> 136 <div class="thumbnail">
137 <a href="{$value.real_url}">
138 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
139 <img data-src="{$value.thumbnail}#" class="b-lazy"
140 src="#"
141 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
142 </a>
143 </div>
144 </div>
137 {/if} 145 {/if}
138 146
139 {if="$is_logged_in"} 147 {if="$is_logged_in"}
@@ -268,5 +276,6 @@
268</div> 276</div>
269 277
270{include="page.footer"} 278{include="page.footer"}
279<script src="js/thumbnails.min.js?v={$version_hash}"></script>
271</body> 280</body>
272</html> 281</html>
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html
index 82568d63..fc03404e 100644
--- a/tpl/default/page.header.html
+++ b/tpl/default/page.header.html
@@ -30,9 +30,11 @@
30 <li class="pure-menu-item" id="shaarli-menu-tags"> 30 <li class="pure-menu-item" id="shaarli-menu-tags">
31 <a href="?do=tagcloud" class="pure-menu-link">{'Tag cloud'|t}</a> 31 <a href="?do=tagcloud" class="pure-menu-link">{'Tag cloud'|t}</a>
32 </li> 32 </li>
33 <li class="pure-menu-item" id="shaarli-menu-picwall"> 33 {if="$thumbnails_enabled"}
34 <a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a> 34 <li class="pure-menu-item" id="shaarli-menu-picwall">
35 </li> 35 <a href="?do=picwall{$searchcrits}" class="pure-menu-link">{'Picture wall'|t}</a>
36 </li>
37 {/if}
36 <li class="pure-menu-item" id="shaarli-menu-daily"> 38 <li class="pure-menu-item" id="shaarli-menu-daily">
37 <a href="?do=daily" class="pure-menu-link">{'Daily'|t}</a> 39 <a href="?do=daily" class="pure-menu-link">{'Daily'|t}</a>
38 </li> 40 </li>
@@ -169,4 +171,18 @@
169 </div> 171 </div>
170{/if} 172{/if}
171 173
174{if="!empty($global_warnings) && $is_logged_in"}
175 <div class="pure-g pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
176 <div class="pure-u-2-24"></div>
177 <div class="pure-u-20-24">
178 {loop="global_warnings"}
179 <p>{$value}</p>
180 {/loop}
181 </div>
182 <div class="pure-u-2-24">
183 <i class="fa fa-times pure-alert-close"></i>
184 </div>
185 </div>
186{/if}
187
172 <div class="clear"></div> 188 <div class="clear"></div>
diff --git a/tpl/default/picwall.html b/tpl/default/picwall.html
index 2f7e03dc..9a0b10dc 100644
--- a/tpl/default/picwall.html
+++ b/tpl/default/picwall.html
@@ -5,41 +5,61 @@
5</head> 5</head>
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8{if="!$thumbnails_enabled"}
9<div class="pure-g pure-alert pure-alert-warning page-single-alert">
10 <div class="pure-u-1 center">
11 {'Picture wall unavailable (thumbnails are disabled).'|t}
12 </div>
13</div>
14{else}
15 {if="count($linksToDisplay)===0 && $is_logged_in"}
16 <div class="pure-g pure-alert pure-alert-warning page-single-alert">
17 <div class="pure-u-1 center">
18 {'There is no cached thumbnail. Try to <a href="?do=thumbs_update">synchronize them</a>.'|t}
19 </div>
20 </div>
21 {/if}
8 22
9<div class="pure-g"> 23 <div class="pure-g">
10 <div class="pure-u-lg-1-6 pure-u-1-24"></div> 24 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
11 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor"> 25 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
12 {$countPics=count($linksToDisplay)} 26 {$countPics=count($linksToDisplay)}
13 <h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2> 27 <h2 class="window-title">{'Picture Wall'|t} - {$countPics} {'pics'|t}</h2>
14 28
15 <div id="plugin_zone_start_picwall" class="plugin_zone"> 29 <div id="plugin_zone_start_picwall" class="plugin_zone">
16 {loop="$plugin_start_zone"} 30 {loop="$plugin_start_zone"}
17 {$value} 31 {$value}
18 {/loop} 32 {/loop}
19 </div> 33 </div>
20 34
21 <div id="picwall_container" class="picwall-container"> 35 <div id="picwall-container" class="picwall-container">
22 {loop="$linksToDisplay"} 36 {loop="$linksToDisplay"}
23 <div class="picwall-pictureframe"> 37 <div class="picwall-pictureframe">
24 {$value.thumbnail}<a href="{$value.real_url}"><span class="info">{$value.title}</span></a> 38 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
25 {loop="$value.picwall_plugin"} 39 <img data-src="{$value.thumbnail}#" class="b-lazy"
26 {$value} 40 src="#"
27 {/loop} 41 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
28 </div> 42 <a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
29 {/loop} 43 {loop="$value.picwall_plugin"}
30 <div class="clear"></div> 44 {$value}
31 </div> 45 {/loop}
46 </div>
47 {/loop}
48 <div class="clear"></div>
49 </div>
32 50
33 <div id="plugin_zone_end_picwall" class="plugin_zone"> 51 <div id="plugin_zone_end_picwall" class="plugin_zone">
34 {loop="$plugin_end_zone"} 52 {loop="$plugin_end_zone"}
35 {$value} 53 {$value}
36 {/loop} 54 {/loop}
55 </div>
37 </div> 56 </div>
57 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
38 </div> 58 </div>
39</div> 59{/if}
40 60
41{include="page.footer"} 61{include="page.footer"}
42<script src="js/picwall.min.js?v={$version_hash}"></script> 62<script src="js/thumbnails.min.js?v={$version_hash}"></script>
43</body> 63</body>
44</html> 64</html>
45 65
diff --git a/tpl/default/thumbnails.html b/tpl/default/thumbnails.html
new file mode 100644
index 00000000..a8cf904e
--- /dev/null
+++ b/tpl/default/thumbnails.html
@@ -0,0 +1,48 @@
1<!DOCTYPE html>
2<html>
3<head>
4 {include="includes"}
5</head>
6<body>
7{include="page.header"}
8
9<div class="pure-g thumbnails-page-container">
10 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
11 <div class="pure-u-lg-1-3 pure-u-22-24 page-form page-form-light">
12 <h2 class="window-title">{'Thumbnails update'|t}</h2>
13
14 <div class="pure-g">
15 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
16 <div class="pure-u-lg-1-3 pure-u-22-24">
17 <div class="thumbnail-placeholder" style="width: {$thumbnails_width}px; height: {$thumbnails_height}px;"></div>
18 </div>
19 </div>
20
21 <div class="pure-g">
22 <div class="pure-u-1-12"></div>
23 <div class="pure-u-5-6">
24 <div class="thumbnail-link-title"></div>
25
26 <div class="progressbar">
27 <div></div>
28 </div>
29 </div>
30 </div>
31
32 <div class="pure-g">
33 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
34 <div class="pure-u-lg-1-3 pure-u-22-24">
35 <div class="progress-counter">
36 <span class="progress-current">0</span> / <span class="progress-total">{$ids|count}</span>
37 </div>
38 </div>
39 </div>
40
41 <input type="hidden" name="ids" value="{function="implode($ids, ',')"}" />
42 </div>
43</div>
44
45{include="page.footer"}
46<script src="js/thumbnails_update.min.js?v={$version_hash}"></script>
47</body>
48</html>
diff --git a/tpl/default/tools.html b/tpl/default/tools.html
index ece66884..20060994 100644
--- a/tpl/default/tools.html
+++ b/tpl/default/tools.html
@@ -45,6 +45,14 @@
45 </a> 45 </a>
46 </div> 46 </div>
47 47
48 {if="$thumbnails_enabled"}
49 <div class="tools-item">
50 <a href="?do=thumbs_update" title="{'Synchronize all link thumbnails'|t}">
51 <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
52 </a>
53 </div>
54 {/if}
55
48 {loop="$tools_plugin"} 56 {loop="$tools_plugin"}
49 <div class="tools-item"> 57 <div class="tools-item">
50 {$value} 58 {$value}
diff --git a/tpl/vintage/configure.html b/tpl/vintage/configure.html
index 479284eb..9466c235 100644
--- a/tpl/vintage/configure.html
+++ b/tpl/vintage/configure.html
@@ -128,6 +128,29 @@
128 <input type="text" name="apiSecret" id="apiSecret" size="50" value="{$api_secret}" /> 128 <input type="text" name="apiSecret" id="apiSecret" size="50" value="{$api_secret}" />
129 </td> 129 </td>
130 </tr> 130 </tr>
131 <tr>
132 <td valign="top"><b>Enable thumbnails</b></td>
133 <td>
134 <select name="enableThumbnails" id="enableThumbnails" class="align">
135 <option value="all" {if="$thumbnails_mode=='all'"}selected{/if}>
136 {'All'|t}
137 </option>
138 <option value="common" {if="$thumbnails_mode=='common'"}selected{/if}>
139 {'Only common media hosts'|t}
140 </option>
141 <option value="none" {if="$thumbnails_mode=='none'"}selected{/if}>
142 {'None'|t}
143 </option>
144 </select>
145 <label for="enableThumbnails">
146 {if="! $gd_enabled"}
147 {'You need to enable the extension <code>php-gd</code> to use thumbnails.'|t}
148 {elseif="$thumbnails_enabled"}
149 <a href="?do=thumbs_update">{'Synchonize thumbnails'|t}</a>
150 {/if}
151 </label>
152 </td>
153 </tr>
131 154
132 <tr> 155 <tr>
133 <td></td> 156 <td></td>
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html
index 1ca51be3..3f202849 100644
--- a/tpl/vintage/linklist.html
+++ b/tpl/vintage/linklist.html
@@ -80,7 +80,16 @@
80 {loop="$links"} 80 {loop="$links"}
81 <li{if="$value.class"} class="{$value.class}"{/if}> 81 <li{if="$value.class"} class="{$value.class}"{/if}>
82 <a id="{$value.shorturl}"></a> 82 <a id="{$value.shorturl}"></a>
83 <div class="thumbnail">{$value.url|thumbnail}</div> 83 {if="$thumbnails_enabled && !empty($value.thumbnail)"}
84 <div class="thumbnail">
85 <a href="{$value.real_url}">
86 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
87 <img data-src="{$value.thumbnail}#" class="b-lazy"
88 src="#"
89 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
90 </a>
91 </div>
92 {/if}
84 <div class="linkcontainer"> 93 <div class="linkcontainer">
85 {if="$is_logged_in"} 94 {if="$is_logged_in"}
86 <div class="linkeditbuttons"> 95 <div class="linkeditbuttons">
@@ -145,6 +154,7 @@
145</div> 154</div>
146 155
147 {include="page.footer"} 156 {include="page.footer"}
157<script src="js/thumbnails.min.js"></script>
148 158
149</body> 159</body>
150</html> 160</html>
diff --git a/tpl/vintage/picwall.html b/tpl/vintage/picwall.html
index 29688914..5f1d266e 100644
--- a/tpl/vintage/picwall.html
+++ b/tpl/vintage/picwall.html
@@ -15,7 +15,11 @@
15 <div id="picwall_container"> 15 <div id="picwall_container">
16 {loop="$linksToDisplay"} 16 {loop="$linksToDisplay"}
17 <div class="picwall_pictureframe"> 17 <div class="picwall_pictureframe">
18 {$value.thumbnail}<a href="{$value.real_url}"><span class="info">{$value.title}</span></a> 18 {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore}
19 <img data-src="{$value.thumbnail}#" class="b-lazy"
20 src="#"
21 alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" />
22 <a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
19 {loop="$value.picwall_plugin"} 23 {loop="$value.picwall_plugin"}
20 {$value} 24 {$value}
21 {/loop} 25 {/loop}
@@ -34,6 +38,6 @@
34 38
35{include="page.footer"} 39{include="page.footer"}
36 40
37<script src="js/picwall.min.js"></script> 41<script src="js/thumbnails.min.js"></script>
38</body> 42</body>
39</html> 43</html>
diff --git a/tpl/vintage/thumbnails.html b/tpl/vintage/thumbnails.html
new file mode 100644
index 00000000..79aebf8d
--- /dev/null
+++ b/tpl/vintage/thumbnails.html
@@ -0,0 +1,28 @@
1<!DOCTYPE html>
2<html>
3<head>{include="includes"}</head>
4<body>
5<div id="pageheader">
6{include="page.header"}
7</div>
8
9<div class="center thumbnails-update-container">
10 <div class="thumbnail-placeholder" style="width: {$thumbnails_width}px; height: {$thumbnails_height}px;"></div>
11
12 <div class="thumbnail-link-title"></div>
13
14 <div class="progressbar">
15 <div></div>
16 </div>
17
18 <div class="progress-counter">
19 <span class="progress-current">0</span> / <span class="progress-total">{$ids|count}</span>
20 </div>
21</div>
22
23<input type="hidden" name="ids" value="{function="implode($ids, ',')"}" />
24
25{include="page.footer"}
26<script src="js/thumbnails_update.min.js?v={$version_hash}"></script>
27</body>
28</html>
diff --git a/webpack.config.js b/webpack.config.js
index 94b7aa70..ed548c73 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -23,7 +23,8 @@ const extractCssVintage = new ExtractTextPlugin({
23module.exports = [ 23module.exports = [
24 { 24 {
25 entry: { 25 entry: {
26 picwall: './assets/common/js/picwall.js', 26 thumbnails: './assets/common/js/thumbnails.js',
27 thumbnails_update: './assets/common/js/thumbnails-update.js',
27 pluginsadmin: './assets/default/js/plugins-admin.js', 28 pluginsadmin: './assets/default/js/plugins-admin.js',
28 shaarli: [ 29 shaarli: [
29 './assets/default/js/base.js', 30 './assets/default/js/base.js',
@@ -96,7 +97,8 @@ module.exports = [
96 './assets/vintage/css/reset.css', 97 './assets/vintage/css/reset.css',
97 './assets/vintage/css/shaarli.css', 98 './assets/vintage/css/shaarli.css',
98 ].concat(glob.sync('./assets/vintage/img/*')), 99 ].concat(glob.sync('./assets/vintage/img/*')),
99 picwall: './assets/common/js/picwall.js', 100 thumbnails: './assets/common/js/thumbnails.js',
101 thumbnails_update: './assets/common/js/thumbnails-update.js',
100 }, 102 },
101 output: { 103 output: {
102 filename: '[name].min.js', 104 filename: '[name].min.js',