aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--AUTHORS7
-rw-r--r--CHANGELOG.md12
-rw-r--r--README.md10
-rw-r--r--application/FileUtils.php75
-rw-r--r--application/History.php200
-rw-r--r--application/LinkDB.php46
-rw-r--r--application/NetscapeBookmarkUtils.php6
-rw-r--r--application/PageBuilder.php16
-rw-r--r--application/Router.php12
-rw-r--r--application/TimeZone.php101
-rw-r--r--application/Updater.php26
-rw-r--r--application/Utils.php129
-rw-r--r--application/api/ApiMiddleware.php5
-rw-r--r--application/api/ApiUtils.php61
-rw-r--r--application/api/controllers/ApiController.php19
-rw-r--r--application/api/controllers/History.php70
-rw-r--r--application/api/controllers/Links.php112
-rw-r--r--application/config/ConfigManager.php1
-rw-r--r--application/exceptions/IOException.php22
-rw-r--r--composer.lock80
-rw-r--r--doc/3rd-party-libraries.html2
-rw-r--r--doc/Backup,-restore,-import-and-export.html2
-rw-r--r--doc/Browsing-and-searching.html4
-rw-r--r--doc/Browsing-and-searching.md4
-rw-r--r--doc/Coding-guidelines.html2
-rw-r--r--doc/Community-&-Related-software.html10
-rw-r--r--doc/Community-&-Related-software.md6
-rw-r--r--doc/Copy-an-existing-installation-over-SSH-and-serve-it-locally.html2
-rw-r--r--doc/Create-and-serve-multiple-Shaarlis-(farm).html2
-rw-r--r--doc/Datastore-hacks.html2
-rw-r--r--doc/Development.html2
-rw-r--r--doc/Directory-structure.html42
-rw-r--r--doc/Docker.html13
-rw-r--r--doc/Docker.md1
-rw-r--r--doc/Download-CSS-styles-from-an-OPML-list.html4
-rw-r--r--doc/Download-and-Installation.html18
-rw-r--r--doc/Download-and-Installation.md16
-rw-r--r--doc/Example-patch---add-new-via-field-for-links.html2
-rw-r--r--doc/FAQ.html2
-rw-r--r--doc/Firefox-share.html2
-rw-r--r--doc/GnuPG-signature.html2
-rw-r--r--doc/Home.html2
-rw-r--r--doc/Plugin-System.html117
-rw-r--r--doc/Plugin-System.md130
-rw-r--r--doc/Plugins.html3
-rw-r--r--doc/Plugins.md1
-rw-r--r--doc/REST-API.html169
-rw-r--r--doc/REST-API.md105
-rw-r--r--doc/RSS-feeds.html2
-rw-r--r--doc/Release-Shaarli.html4
-rw-r--r--doc/Release-Shaarli.md3
-rw-r--r--doc/Security.html2
-rw-r--r--doc/Server-configuration.html16
-rw-r--r--doc/Server-configuration.md15
-rw-r--r--doc/Server-requirements.html25
-rw-r--r--doc/Server-requirements.md3
-rw-r--r--doc/Server-security.html2
-rw-r--r--doc/Shaarli-configuration.html3
-rw-r--r--doc/Shaarli-configuration.md1
-rw-r--r--doc/Sharing-button.html2
-rw-r--r--doc/Static-analysis.html2
-rw-r--r--doc/Theming.html58
-rw-r--r--doc/Theming.md54
-rw-r--r--doc/Troubleshooting.html4
-rw-r--r--doc/Unit-tests.html14
-rw-r--r--doc/Unit-tests.md19
-rw-r--r--doc/Upgrade-and-migration.html37
-rw-r--r--doc/Upgrade-and-migration.md41
-rw-r--r--doc/Usage.html2
-rw-r--r--doc/Versioning-and-Branches.html156
-rw-r--r--doc/Versioning-and-Branches.md76
-rw-r--r--doc/_Footer.html2
-rw-r--r--doc/_Sidebar.html4
-rw-r--r--doc/_Sidebar.md2
-rw-r--r--doc/sidebar.html2
-rw-r--r--index.php207
-rw-r--r--plugins/readityourself/book-open.pngbin568 -> 0 bytes
-rw-r--r--plugins/readityourself/readityourself.html1
-rw-r--r--plugins/readityourself/readityourself.meta2
-rw-r--r--plugins/readityourself/readityourself.php51
-rw-r--r--tests/FileUtilsTest.php108
-rw-r--r--tests/HistoryTest.php207
-rw-r--r--tests/LinkDBTest.php33
-rw-r--r--tests/NetscapeBookmarkUtils/BookmarkImportTest.php81
-rw-r--r--tests/TimeZoneTest.php83
-rw-r--r--tests/Updater/UpdaterTest.php40
-rw-r--r--tests/UtilsTest.php203
-rw-r--r--tests/api/ApiUtilsTest.php78
-rw-r--r--tests/api/controllers/DeleteLinkTest.php126
-rw-r--r--tests/api/controllers/GetLinkIdTest.php1
-rw-r--r--tests/api/controllers/GetLinksTest.php1
-rw-r--r--tests/api/controllers/HistoryTest.php216
-rw-r--r--tests/api/controllers/InfoTest.php1
-rw-r--r--tests/api/controllers/PostLinkTest.php216
-rw-r--r--tests/api/controllers/PutLinkTest.php222
-rw-r--r--tests/languages/de/UtilsDeTest.php22
-rw-r--r--tests/languages/en/UtilsEnTest.php22
-rw-r--r--tests/languages/fr/UtilsFrTest.php20
-rw-r--r--tests/plugins/PluginReadityourselfTest.php99
-rw-r--r--tests/utils/ReferenceHistory.php82
-rw-r--r--tests/utils/ReferenceLinkDB.php2
-rw-r--r--tpl/default/changetag.html4
-rw-r--r--tpl/default/configure.html30
-rw-r--r--tpl/default/css/shaarli.css104
-rw-r--r--tpl/default/daily.html2
-rw-r--r--tpl/default/fonts/Fira-Sans-regular.woffbin17100 -> 0 bytes
-rw-r--r--tpl/default/fonts/Fira-Sans-regular.woff2bin13836 -> 0 bytes
-rw-r--r--tpl/default/fonts/Roboto-Bold.woffbin0 -> 89584 bytes
-rw-r--r--tpl/default/fonts/Roboto-Bold.woff2bin0 -> 63320 bytes
-rw-r--r--tpl/default/fonts/Roboto-Regular.woffbin0 -> 89732 bytes
-rw-r--r--tpl/default/fonts/Roboto-Regular.woff2bin0 -> 63412 bytes
-rw-r--r--tpl/default/import.html1
-rw-r--r--tpl/default/includes.html2
-rw-r--r--tpl/default/install.html42
-rw-r--r--tpl/default/js/shaarli.js312
-rw-r--r--tpl/default/linklist.html5
-rw-r--r--tpl/default/page.footer.html3
-rw-r--r--tpl/default/page.header.html7
-rw-r--r--tpl/default/tag.cloud.html (renamed from tpl/default/tagcloud.html)24
-rw-r--r--tpl/default/tag.list.html86
-rw-r--r--tpl/default/tag.sort.html8
-rw-r--r--tpl/vintage/configure.html26
-rw-r--r--tpl/vintage/css/shaarli.css4
-rw-r--r--tpl/vintage/import.html2
-rw-r--r--tpl/vintage/install.html28
-rw-r--r--tpl/vintage/js/shaarli.js32
-rw-r--r--tpl/vintage/page.footer.html1
-rw-r--r--tpl/vintage/page.header.html4
-rw-r--r--tpl/vintage/tag.cloud.html (renamed from tpl/vintage/tagcloud.html)2
129 files changed, 4358 insertions, 723 deletions
diff --git a/AUTHORS b/AUTHORS
index aa041ae9..c0e35949 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,11 +1,11 @@
1 327 ArthurHoaro <arthur@hoa.ro> 1 472 ArthurHoaro <arthur@hoa.ro>
2 188 VirtualTam <virtualtam@flibidi.net> 2 201 VirtualTam <virtualtam@flibidi.net>
3 132 nodiscc <nodiscc@gmail.com> 3 132 nodiscc <nodiscc@gmail.com>
4 56 Sébastien Sauvage <sebsauvage@sebsauvage.net> 4 56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
5 15 Florian Eula <eula.florian@gmail.com> 5 15 Florian Eula <eula.florian@gmail.com>
6 13 Emilien Klein <emilien@klein.st> 6 13 Emilien Klein <emilien@klein.st>
7 12 Nicolas Danelon <hi@nicolasmd.com.ar> 7 12 Nicolas Danelon <hi@nicolasmd.com.ar>
8 7 Christophe HENRY <christophe.henry@sbgodin.fr> 8 8 Christophe HENRY <christophe.henry@sbgodin.fr>
9 4 Alexandre Alapetite <alexandre@alapetite.fr> 9 4 Alexandre Alapetite <alexandre@alapetite.fr>
10 4 David Sferruzza <david.sferruzza@gmail.com> 10 4 David Sferruzza <david.sferruzza@gmail.com>
11 3 Teromene <teromene@teromene.fr> 11 3 Teromene <teromene@teromene.fr>
@@ -38,3 +38,4 @@
38 1 Sbgodin <Sbgodin@users.noreply.github.com> 38 1 Sbgodin <Sbgodin@users.noreply.github.com>
39 1 TsT <tst2005@gmail.com> 39 1 TsT <tst2005@gmail.com>
40 1 dimtion <zizou.xena@gmail.com> 40 1 dimtion <zizou.xena@gmail.com>
41 1 philipp-r <philipp-r@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0813b97..c63337ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
5and this project adheres to [Semantic Versioning](http://semver.org/). 5and this project adheres to [Semantic Versioning](http://semver.org/).
6 6
7 7
8## [v0.9.0](https://github.com/shaarli/Shaarli/releases/tag/v0.9.0) - UNPUBLISHED 8## [v0.9.0](https://github.com/shaarli/Shaarli/releases/tag/v0.9.0) - 2017-05-07
9 9
10This release introduces the REST API, and requires updating HTTP server 10This release introduces the REST API, and requires updating HTTP server
11configuration to enable URL rewriting, see: 11configuration to enable URL rewriting, see:
@@ -21,6 +21,7 @@ configuration to enable URL rewriting, see:
21 - versioned API endpoints: 21 - versioned API endpoints:
22 - `/api/v1/info`: get general information on the Shaarli instance 22 - `/api/v1/info`: get general information on the Shaarli instance
23 - `/api/v1/links`: get a list of shaared links 23 - `/api/v1/links`: get a list of shaared links
24 - `/api/v1/history`: get a list of latest actions
24Theming: 25Theming:
25 - Introduce a new theme 26 - Introduce a new theme
26 - Allow selecting themes/templates from the configuration page 27 - Allow selecting themes/templates from the configuration page
@@ -31,6 +32,8 @@ Theming:
31- Add `campaign_` to the URL cleanup pattern list 32- Add `campaign_` to the URL cleanup pattern list
32- Add an AUTHORS file and Makefile target to list authors from Git commit data 33- Add an AUTHORS file and Makefile target to list authors from Git commit data
33- Link imports are now logged in `data/` folder, and can be debug using `dev.debug=true` setting. 34- Link imports are now logged in `data/` folder, and can be debug using `dev.debug=true` setting.
35- `composer.lock` is now included in git file to allow proper `composer install`
36- History mechanism which logs link addition/modification/deletion
34 37
35### Changed 38### Changed
36- Docker: enable nginx URL rewriting for the REST API 39- Docker: enable nginx URL rewriting for the REST API
@@ -40,6 +43,8 @@ Theming:
40 - Rename the legacy theme to `vintage` 43 - Rename the legacy theme to `vintage`
41 - Private only filter is now displayed as a search parameter 44 - Private only filter is now displayed as a search parameter
42 - Autocomplete: pre-select the first element 45 - Autocomplete: pre-select the first element
46 - Display daily date in the page title (browser title)
47 - Timezone lists are now passed as an array instead of raw HTML
43- Move PubSubHub to a dedicated plugin 48- Move PubSubHub to a dedicated plugin
44- Coding style: 49- Coding style:
45 - explicit method visibility 50 - explicit method visibility
@@ -50,9 +55,13 @@ Theming:
50- Improved client locale detection 55- Improved client locale detection
51- Improved date time display depending on the locale 56- Improved date time display depending on the locale
52- Partial namespace support for Shaarli classes 57- Partial namespace support for Shaarli classes
58- Shaarli version is now only present in `shaarli_version.php`
59- Human readable maximum file size upload
60
53 61
54### Removed 62### Removed
55- PHP < 5.5 compatibility 63- PHP < 5.5 compatibility
64- ReadItYourself plugin
56 65
57### Fixed 66### Fixed
58- Ignore generated release tarballs 67- Ignore generated release tarballs
@@ -67,6 +76,7 @@ Theming:
67- Remove extra spaces in the bookmarklet's name 76- Remove extra spaces in the bookmarklet's name
68- Piwik plugin: Piwik URL protocol can now be set (http or https) 77- Piwik plugin: Piwik URL protocol can now be set (http or https)
69- All inline JS has been moved to dedicated JS files 78- All inline JS has been moved to dedicated JS files
79- Keep tags after login redirection
70 80
71### Security 81### Security
72- Markdown plugin: escape HTML entities by default 82- Markdown plugin: escape HTML entities by default
diff --git a/README.md b/README.md
index cfdefc66..7633b2cb 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,10 @@ _Do you want to share the links you discover?_
6_Shaarli is a minimalist delicious clone that you can install on your own server._ 6_Shaarli is a minimalist delicious clone that you can install on your own server._
7_It is designed to be personal (single-user), fast and handy._ 7_It is designed to be personal (single-user), fast and handy._
8 8
9[![](https://img.shields.io/badge/stable-v0.7.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.7.1) 9[![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4)
10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) 10[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
11&bull; 11&bull;
12[![](https://img.shields.io/badge/latest-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) 12[![](https://img.shields.io/badge/latest-v0.9.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.0)
13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) 13[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
14&bull; 14&bull;
15[![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli) 15[![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli)
@@ -85,6 +85,12 @@ dailymotion, flickr, imageshack, imgur, vimeo, xkcd, youtube...
85- URL cleanup: automatic removal of `?utm_source=...`, `fb=...` 85- URL cleanup: automatic removal of `?utm_source=...`, `fb=...`
86- discreet pop-up notification when a new release is available 86- discreet pop-up notification when a new release is available
87 87
88### REST API
89
90Easily extensible by any client using the REST API exposed by Shaarli.
91
92See the [API documentation](http://shaarli.github.io/api-documentation/).
93
88### Other usages 94### Other usages
89Though Shaarli is primarily a bookmarking application, it can serve other purposes 95Though Shaarli is primarily a bookmarking application, it can serve other purposes
90(see [usage examples](https://github.com/shaarli/Shaarli/wiki#usage-examples)): 96(see [usage examples](https://github.com/shaarli/Shaarli/wiki#usage-examples)):
diff --git a/application/FileUtils.php b/application/FileUtils.php
index 6cac9825..a167f642 100644
--- a/application/FileUtils.php
+++ b/application/FileUtils.php
@@ -1,21 +1,76 @@
1<?php 1<?php
2
3require_once 'exceptions/IOException.php';
4
2/** 5/**
3 * Exception class thrown when a filesystem access failure happens 6 * Class FileUtils
7 *
8 * Utility class for file manipulation.
4 */ 9 */
5class IOException extends Exception 10class FileUtils
6{ 11{
7 private $path; 12 /**
13 * @var string
14 */
15 protected static $phpPrefix = '<?php /* ';
16
17 /**
18 * @var string
19 */
20 protected static $phpSuffix = ' */ ?>';
8 21
9 /** 22 /**
10 * Construct a new IOException 23 * Write data into a file (Shaarli database format).
24 * The data is stored in a PHP file, as a comment, in compressed base64 format.
25 *
26 * The file will be created if it doesn't exist.
27 *
28 * @param string $file File path.
29 * @param mixed $content Content to write.
30 *
31 * @return int|bool Number of bytes written or false if it fails.
11 * 32 *
12 * @param string $path path to the resource that cannot be accessed 33 * @throws IOException The destination file can't be written.
13 * @param string $message Custom exception message.
14 */ 34 */
15 public function __construct($path, $message = '') 35 public static function writeFlatDB($file, $content)
16 { 36 {
17 $this->path = $path; 37 if (is_file($file) && !is_writeable($file)) {
18 $this->message = empty($message) ? 'Error accessing' : $message; 38 // The datastore exists but is not writeable
19 $this->message .= PHP_EOL . $this->path; 39 throw new IOException($file);
40 } else if (!is_file($file) && !is_writeable(dirname($file))) {
41 // The datastore does not exist and its parent directory is not writeable
42 throw new IOException(dirname($file));
43 }
44
45 return file_put_contents(
46 $file,
47 self::$phpPrefix.base64_encode(gzdeflate(serialize($content))).self::$phpSuffix
48 );
49 }
50
51 /**
52 * Read data from a file containing Shaarli database format content.
53 * If the file isn't readable or doesn't exists, default data will be returned.
54 *
55 * @param string $file File path.
56 * @param mixed $default The default value to return if the file isn't readable.
57 *
58 * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails.
59 */
60 public static function readFlatDB($file, $default = null)
61 {
62 // Note that gzinflate is faster than gzuncompress.
63 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
64 if (is_readable($file)) {
65 return unserialize(
66 gzinflate(
67 base64_decode(
68 substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
69 )
70 )
71 );
72 }
73
74 return $default;
20 } 75 }
21} 76}
diff --git a/application/History.php b/application/History.php
new file mode 100644
index 00000000..116b9264
--- /dev/null
+++ b/application/History.php
@@ -0,0 +1,200 @@
1<?php
2
3/**
4 * Class History
5 *
6 * Handle the history file tracing events in Shaarli.
7 * The history is stored as JSON in a file set by 'resource.history' setting.
8 *
9 * Available data:
10 * - event: event key
11 * - datetime: event date, in ISO8601 format.
12 * - id: event item identifier (currently only link IDs).
13 *
14 * Available event keys:
15 * - CREATED: new link
16 * - UPDATED: link updated
17 * - DELETED: link deleted
18 * - SETTINGS: the settings have been updated through the UI.
19 *
20 * Note: new events are put at the beginning of the file and history array.
21 */
22class History
23{
24 /**
25 * @var string Action key: a new link has been created.
26 */
27 const CREATED = 'CREATED';
28
29 /**
30 * @var string Action key: a link has been updated.
31 */
32 const UPDATED = 'UPDATED';
33
34 /**
35 * @var string Action key: a link has been deleted.
36 */
37 const DELETED = 'DELETED';
38
39 /**
40 * @var string Action key: settings have been updated.
41 */
42 const SETTINGS = 'SETTINGS';
43
44 /**
45 * @var string History file path.
46 */
47 protected $historyFilePath;
48
49 /**
50 * @var array History data.
51 */
52 protected $history;
53
54 /**
55 * @var int History retention time in seconds (1 month).
56 */
57 protected $retentionTime = 2678400;
58
59 /**
60 * History constructor.
61 *
62 * @param string $historyFilePath History file path.
63 * @param int $retentionTime History content rentention time in seconds.
64 *
65 * @throws Exception if something goes wrong.
66 */
67 public function __construct($historyFilePath, $retentionTime = null)
68 {
69 $this->historyFilePath = $historyFilePath;
70 if ($retentionTime !== null) {
71 $this->retentionTime = $retentionTime;
72 }
73 }
74
75 /**
76 * Initialize: read history file.
77 *
78 * Allow lazy loading (don't read the file if it isn't necessary).
79 */
80 protected function initialize()
81 {
82 $this->check();
83 $this->read();
84 }
85
86 /**
87 * Add Event: new link.
88 *
89 * @param array $link Link data.
90 */
91 public function addLink($link)
92 {
93 $this->addEvent(self::CREATED, $link['id']);
94 }
95
96 /**
97 * Add Event: update existing link.
98 *
99 * @param array $link Link data.
100 */
101 public function updateLink($link)
102 {
103 $this->addEvent(self::UPDATED, $link['id']);
104 }
105
106 /**
107 * Add Event: delete existing link.
108 *
109 * @param array $link Link data.
110 */
111 public function deleteLink($link)
112 {
113 $this->addEvent(self::DELETED, $link['id']);
114 }
115
116 /**
117 * Add Event: settings updated.
118 */
119 public function updateSettings()
120 {
121 $this->addEvent(self::SETTINGS);
122 }
123
124 /**
125 * Save a new event and write it in the history file.
126 *
127 * @param string $status Event key, should be defined as constant.
128 * @param mixed $id Event item identifier (e.g. link ID).
129 */
130 protected function addEvent($status, $id = null)
131 {
132 if ($this->history === null) {
133 $this->initialize();
134 }
135
136 $item = [
137 'event' => $status,
138 'datetime' => new DateTime(),
139 'id' => $id !== null ? $id : '',
140 ];
141 $this->history = array_merge([$item], $this->history);
142 $this->write();
143 }
144
145 /**
146 * Check that the history file is writable.
147 * Create the file if it doesn't exist.
148 *
149 * @throws Exception if it isn't writable.
150 */
151 protected function check()
152 {
153 if (! is_file($this->historyFilePath)) {
154 FileUtils::writeFlatDB($this->historyFilePath, []);
155 }
156
157 if (! is_writable($this->historyFilePath)) {
158 throw new Exception('History file isn\'t readable or writable');
159 }
160 }
161
162 /**
163 * Read JSON history file.
164 */
165 protected function read()
166 {
167 $this->history = FileUtils::readFlatDB($this->historyFilePath, []);
168 if ($this->history === false) {
169 throw new Exception('Could not parse history file');
170 }
171 }
172
173 /**
174 * Write JSON history file and delete old entries.
175 */
176 protected function write()
177 {
178 $comparaison = new DateTime('-'. $this->retentionTime . ' seconds');
179 foreach ($this->history as $key => $value) {
180 if ($value['datetime'] < $comparaison) {
181 unset($this->history[$key]);
182 }
183 }
184 FileUtils::writeFlatDB($this->historyFilePath, array_values($this->history));
185 }
186
187 /**
188 * Get the History.
189 *
190 * @return array
191 */
192 public function getHistory()
193 {
194 if ($this->history === null) {
195 $this->initialize();
196 }
197
198 return $this->history;
199 }
200}
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 4cee2af9..7802cc8a 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -50,12 +50,6 @@ class LinkDB implements Iterator, Countable, ArrayAccess
50 // Link date storage format 50 // Link date storage format
51 const LINK_DATE_FORMAT = 'Ymd_His'; 51 const LINK_DATE_FORMAT = 'Ymd_His';
52 52
53 // Datastore PHP prefix
54 protected static $phpPrefix = '<?php /* ';
55
56 // Datastore PHP suffix
57 protected static $phpSuffix = ' */ ?>';
58
59 // List of links (associative array) 53 // List of links (associative array)
60 // - key: link date (e.g. "20110823_124546"), 54 // - key: link date (e.g. "20110823_124546"),
61 // - value: associative array (keys: title, description...) 55 // - value: associative array (keys: title, description...)
@@ -144,10 +138,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess
144 if (!isset($value['id']) || empty($value['url'])) { 138 if (!isset($value['id']) || empty($value['url'])) {
145 die('Internal Error: A link should always have an id and URL.'); 139 die('Internal Error: A link should always have an id and URL.');
146 } 140 }
147 if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) { 141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
148 die('You must specify an integer as a key.'); 142 die('You must specify an integer as a key.');
149 } 143 }
150 if (! empty($offset) && $offset !== $value['id']) { 144 if ($offset !== null && $offset !== $value['id']) {
151 die('Array offset and link ID must be equal.'); 145 die('Array offset and link ID must be equal.');
152 } 146 }
153 147
@@ -295,16 +289,7 @@ You use the community supported version of the original Shaarli project, by Seba
295 return; 289 return;
296 } 290 }
297 291
298 // Read data 292 $this->links = FileUtils::readFlatDB($this->datastore, []);
299 // Note that gzinflate is faster than gzuncompress.
300 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
301 $this->links = array();
302
303 if (file_exists($this->datastore)) {
304 $this->links = unserialize(gzinflate(base64_decode(
305 substr(file_get_contents($this->datastore),
306 strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
307 }
308 293
309 $toremove = array(); 294 $toremove = array();
310 foreach ($this->links as $key => &$link) { 295 foreach ($this->links as $key => &$link) {
@@ -361,19 +346,7 @@ You use the community supported version of the original Shaarli project, by Seba
361 */ 346 */
362 private function write() 347 private function write()
363 { 348 {
364 if (is_file($this->datastore) && !is_writeable($this->datastore)) { 349 FileUtils::writeFlatDB($this->datastore, $this->links);
365 // The datastore exists but is not writeable
366 throw new IOException($this->datastore);
367 } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
368 // The datastore does not exist and its parent directory is not writeable
369 throw new IOException(dirname($this->datastore));
370 }
371
372 file_put_contents(
373 $this->datastore,
374 self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix
375 );
376
377 } 350 }
378 351
379 /** 352 /**
@@ -479,14 +452,17 @@ You use the community supported version of the original Shaarli project, by Seba
479 } 452 }
480 453
481 /** 454 /**
482 * Returns the list of all tags 455 * Returns the list tags appearing in the links with the given tags
483 * Output: associative array key=tags, value=0 456 * @param $filteringTags: tags selecting the links to consider
457 * @param $visibility: process only all/private/public links
458 * @return: a tag=>linksCount array
484 */ 459 */
485 public function allTags() 460 public function linksCountPerTag($filteringTags = [], $visibility = 'all')
486 { 461 {
462 $links = empty($filteringTags) ? $this->links : $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
487 $tags = array(); 463 $tags = array();
488 $caseMapping = array(); 464 $caseMapping = array();
489 foreach ($this->links as $link) { 465 foreach ($links as $link) {
490 foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) { 466 foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
491 if (empty($tag)) { 467 if (empty($tag)) {
492 continue; 468 continue;
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
index ab346f81..2a10ff22 100644
--- a/application/NetscapeBookmarkUtils.php
+++ b/application/NetscapeBookmarkUtils.php
@@ -95,10 +95,11 @@ class NetscapeBookmarkUtils
95 * @param array $files Server $_FILES parameters 95 * @param array $files Server $_FILES parameters
96 * @param LinkDB $linkDb Loaded LinkDB instance 96 * @param LinkDB $linkDb Loaded LinkDB instance
97 * @param ConfigManager $conf instance 97 * @param ConfigManager $conf instance
98 * @param History $history History instance
98 * 99 *
99 * @return string Summary of the bookmark import status 100 * @return string Summary of the bookmark import status
100 */ 101 */
101 public static function import($post, $files, $linkDb, $conf) 102 public static function import($post, $files, $linkDb, $conf, $history)
102 { 103 {
103 $filename = $files['filetoupload']['name']; 104 $filename = $files['filetoupload']['name'];
104 $filesize = $files['filetoupload']['size']; 105 $filesize = $files['filetoupload']['size'];
@@ -179,9 +180,11 @@ class NetscapeBookmarkUtils
179 $newLink['id'] = $existingLink['id']; 180 $newLink['id'] = $existingLink['id'];
180 $newLink['created'] = $existingLink['created']; 181 $newLink['created'] = $existingLink['created'];
181 $newLink['updated'] = new DateTime(); 182 $newLink['updated'] = new DateTime();
183 $newLink['shorturl'] = $existingLink['shorturl'];
182 $linkDb[$existingLink['id']] = $newLink; 184 $linkDb[$existingLink['id']] = $newLink;
183 $importCount++; 185 $importCount++;
184 $overwriteCount++; 186 $overwriteCount++;
187 $history->updateLink($newLink);
185 continue; 188 continue;
186 } 189 }
187 190
@@ -193,6 +196,7 @@ class NetscapeBookmarkUtils
193 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); 196 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
194 $linkDb[$newLink['id']] = $newLink; 197 $linkDb[$newLink['id']] = $newLink;
195 $importCount++; 198 $importCount++;
199 $history->addLink($newLink);
196 } 200 }
197 201
198 $linkDb->save($conf->get('resource.page_cache')); 202 $linkDb->save($conf->get('resource.page_cache'));
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index b133dee8..c86621a2 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -1,5 +1,7 @@
1<?php 1<?php
2 2
3use Shaarli\Config\ConfigManager;
4
3/** 5/**
4 * This class is in charge of building the final page. 6 * This class is in charge of building the final page.
5 * (This is basically a wrapper around RainTPL which pre-fills some fields.) 7 * (This is basically a wrapper around RainTPL which pre-fills some fields.)
@@ -20,15 +22,22 @@ class PageBuilder
20 protected $conf; 22 protected $conf;
21 23
22 /** 24 /**
25 * @var LinkDB $linkDB instance.
26 */
27 protected $linkDB;
28
29 /**
23 * PageBuilder constructor. 30 * PageBuilder constructor.
24 * $tpl is initialized at false for lazy loading. 31 * $tpl is initialized at false for lazy loading.
25 * 32 *
26 * @param ConfigManager $conf Configuration Manager instance (reference). 33 * @param ConfigManager $conf Configuration Manager instance (reference).
34 * @param LinkDB $linkDB instance.
27 */ 35 */
28 public function __construct(&$conf) 36 public function __construct(&$conf, $linkDB = null)
29 { 37 {
30 $this->tpl = false; 38 $this->tpl = false;
31 $this->conf = $conf; 39 $this->conf = $conf;
40 $this->linkDB = $linkDB;
32 } 41 }
33 42
34 /** 43 /**
@@ -79,6 +88,9 @@ class PageBuilder
79 $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss'); 88 $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss');
80 $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); 89 $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false));
81 $this->tpl->assign('token', getToken($this->conf)); 90 $this->tpl->assign('token', getToken($this->conf));
91 if ($this->linkDB !== null) {
92 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
93 }
82 // To be removed with a proper theme configuration. 94 // To be removed with a proper theme configuration.
83 $this->tpl->assign('conf', $this->conf); 95 $this->tpl->assign('conf', $this->conf);
84 } 96 }
diff --git a/application/Router.php b/application/Router.php
index c9a51912..4df0387c 100644
--- a/application/Router.php
+++ b/application/Router.php
@@ -13,6 +13,8 @@ class Router
13 13
14 public static $PAGE_TAGCLOUD = 'tagcloud'; 14 public static $PAGE_TAGCLOUD = 'tagcloud';
15 15
16 public static $PAGE_TAGLIST = 'taglist';
17
16 public static $PAGE_DAILY = 'daily'; 18 public static $PAGE_DAILY = 'daily';
17 19
18 public static $PAGE_FEED_ATOM = 'atom'; 20 public static $PAGE_FEED_ATOM = 'atom';
@@ -45,6 +47,8 @@ class Router
45 47
46 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin'; 48 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
47 49
50 public static $GET_TOKEN = 'token';
51
48 /** 52 /**
49 * Reproducing renderPage() if hell, to avoid regression. 53 * Reproducing renderPage() if hell, to avoid regression.
50 * 54 *
@@ -77,6 +81,10 @@ class Router
77 return self::$PAGE_TAGCLOUD; 81 return self::$PAGE_TAGCLOUD;
78 } 82 }
79 83
84 if (startsWith($query, 'do='. self::$PAGE_TAGLIST)) {
85 return self::$PAGE_TAGLIST;
86 }
87
80 if (startsWith($query, 'do='. self::$PAGE_OPENSEARCH)) { 88 if (startsWith($query, 'do='. self::$PAGE_OPENSEARCH)) {
81 return self::$PAGE_OPENSEARCH; 89 return self::$PAGE_OPENSEARCH;
82 } 90 }
@@ -142,6 +150,10 @@ class Router
142 return self::$PAGE_SAVE_PLUGINSADMIN; 150 return self::$PAGE_SAVE_PLUGINSADMIN;
143 } 151 }
144 152
153 if (startsWith($query, 'do='. self::$GET_TOKEN)) {
154 return self::$GET_TOKEN;
155 }
156
145 return self::$PAGE_LINKLIST; 157 return self::$PAGE_LINKLIST;
146 } 158 }
147} 159}
diff --git a/application/TimeZone.php b/application/TimeZone.php
index 36a8fb12..c1869ef8 100644
--- a/application/TimeZone.php
+++ b/application/TimeZone.php
@@ -1,23 +1,42 @@
1<?php 1<?php
2/** 2/**
3 * Generates the timezone selection form and JavaScript. 3 * Generates a list of available timezone continents and cities.
4 * 4 *
5 * Note: 'UTC/UTC' is mapped to 'UTC' to form a valid option 5 * Two distinct array based on available timezones
6 * and the one selected in the settings:
7 * - (0) continents:
8 * + list of available continents
9 * + special key 'selected' containing the value of the selected timezone's continent
10 * - (1) cities:
11 * + list of available cities associated with their continent
12 * + special key 'selected' containing the value of the selected timezone's city (without the continent)
6 * 13 *
7 * Example: preselect Europe/Paris 14 * Example:
8 * list($htmlform, $js) = generateTimeZoneForm('Europe/Paris'); 15 * [
16 * [
17 * 'America',
18 * 'Europe',
19 * 'selected' => 'Europe',
20 * ],
21 * [
22 * ['continent' => 'America', 'city' => 'Toronto'],
23 * ['continent' => 'Europe', 'city' => 'Paris'],
24 * 'selected' => 'Paris',
25 * ],
26 * ];
9 * 27 *
28 * Notes:
29 * - 'UTC/UTC' is mapped to 'UTC' to form a valid option
30 * - a few timezone cities includes the country/state, such as Argentina/Buenos_Aires
31 * - these arrays are designed to build timezone selects in template files with any HTML structure
32 *
33 * @param array $installedTimeZones List of installed timezones as string
10 * @param string $preselectedTimezone preselected timezone (optional) 34 * @param string $preselectedTimezone preselected timezone (optional)
11 * 35 *
12 * @return array containing the generated HTML form and Javascript code 36 * @return array[] continents and cities
13 **/ 37 **/
14function generateTimeZoneForm($preselectedTimezone='') 38function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
15{ 39{
16 // Select the server timezone
17 if ($preselectedTimezone == '') {
18 $preselectedTimezone = date_default_timezone_get();
19 }
20
21 if ($preselectedTimezone == 'UTC') { 40 if ($preselectedTimezone == 'UTC') {
22 $pcity = $pcontinent = 'UTC'; 41 $pcity = $pcontinent = 'UTC';
23 } else { 42 } else {
@@ -27,62 +46,30 @@ function generateTimeZoneForm($preselectedTimezone='')
27 $pcity = substr($preselectedTimezone, $spos+1); 46 $pcity = substr($preselectedTimezone, $spos+1);
28 } 47 }
29 48
30 // The list is in the form 'Europe/Paris', 'America/Argentina/Buenos_Aires' 49 $continents = [];
31 // We split the list in continents/cities. 50 $cities = [];
32 $continents = array(); 51 foreach ($installedTimeZones as $tz) {
33 $cities = array();
34
35 // TODO: use a template to generate the HTML/Javascript form
36
37 foreach (timezone_identifiers_list() as $tz) {
38 if ($tz == 'UTC') { 52 if ($tz == 'UTC') {
39 $tz = 'UTC/UTC'; 53 $tz = 'UTC/UTC';
40 } 54 }
41 $spos = strpos($tz, '/'); 55 $spos = strpos($tz, '/');
42 56
43 if ($spos !== false) { 57 // Ignore invalid timezones
44 $continent = substr($tz, 0, $spos); 58 if ($spos === false) {
45 $city = substr($tz, $spos+1); 59 continue;
46 $continents[$continent] = 1;
47
48 if (!isset($cities[$continent])) {
49 $cities[$continent] = '';
50 }
51 $cities[$continent] .= '<option value="'.$city.'"';
52 if ($pcity == $city) {
53 $cities[$continent] .= ' selected="selected"';
54 }
55 $cities[$continent] .= '>'.$city.'</option>';
56 } 60 }
57 }
58
59 $continentsHtml = '';
60 $continents = array_keys($continents);
61 61
62 foreach ($continents as $continent) { 62 $continent = substr($tz, 0, $spos);
63 $continentsHtml .= '<option value="'.$continent.'"'; 63 $city = substr($tz, $spos+1);
64 if ($pcontinent == $continent) { 64 $cities[] = ['continent' => $continent, 'city' => $city];
65 $continentsHtml .= ' selected="selected"'; 65 $continents[$continent] = true;
66 }
67 $continentsHtml .= '>'.$continent.'</option>';
68 } 66 }
69 67
70 // Timezone selection form 68 $continents = array_keys($continents);
71 $timezoneForm = 'Continent:'; 69 $continents['selected'] = $pcontinent;
72 $timezoneForm .= '<select name="continent" id="continent" onChange="onChangecontinent();">'; 70 $cities['selected'] = $pcity;
73 $timezoneForm .= $continentsHtml.'</select>';
74 $timezoneForm .= '&nbsp;&nbsp;&nbsp;&nbsp;City:';
75 $timezoneForm .= '<select name="city" id="city">'.$cities[$pcontinent].'</select><br />';
76
77 // Javascript handler - updates the city list when the user selects a continent
78 $timezoneJs = '<script>';
79 $timezoneJs .= 'function onChangecontinent() {';
80 $timezoneJs .= 'document.getElementById("city").innerHTML =';
81 $timezoneJs .= ' citiescontinent[document.getElementById("continent").value]; }';
82 $timezoneJs .= 'var citiescontinent = '.json_encode($cities).';';
83 $timezoneJs .= '</script>';
84 71
85 return array($timezoneForm, $timezoneJs); 72 return [$continents, $cities];
86} 73}
87 74
88/** 75/**
diff --git a/application/Updater.php b/application/Updater.php
index 0fb68c5a..40a15906 100644
--- a/application/Updater.php
+++ b/application/Updater.php
@@ -329,21 +329,6 @@ class Updater
329 } 329 }
330 330
331 /** 331 /**
332 * While the new default theme is in an unstable state
333 * continue to use the vintage theme
334 */
335 public function updateMethodDefaultThemeVintage()
336 {
337 if ($this->conf->get('resource.theme') !== 'default') {
338 return true;
339 }
340 $this->conf->set('resource.theme', 'vintage');
341 $this->conf->write($this->isLoggedIn);
342
343 return true;
344 }
345
346 /**
347 * * `markdown_escape` is a new setting, set to true as default. 332 * * `markdown_escape` is a new setting, set to true as default.
348 * 333 *
349 * If the markdown plugin was already enabled, escaping is disabled to avoid 334 * If the markdown plugin was already enabled, escaping is disabled to avoid
@@ -440,6 +425,17 @@ class Updater
440 $this->conf->write($this->isLoggedIn); 425 $this->conf->write($this->isLoggedIn);
441 return true; 426 return true;
442 } 427 }
428
429 /**
430 * Reset history store file due to date format change.
431 */
432 public function updateMethodResetHistoryFile()
433 {
434 if (is_file($this->conf->get('resource.history'))) {
435 unlink($this->conf->get('resource.history'));
436 }
437 return true;
438 }
443} 439}
444 440
445/** 441/**
diff --git a/application/Utils.php b/application/Utils.php
index 5c077450..9d0ebc5e 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -321,25 +321,148 @@ function normalize_spaces($string)
321 * otherwise default format '%c' will be returned. 321 * otherwise default format '%c' will be returned.
322 * 322 *
323 * @param DateTime $date to format. 323 * @param DateTime $date to format.
324 * @param bool $time Displays time if true.
324 * @param bool $intl Use international format if true. 325 * @param bool $intl Use international format if true.
325 * 326 *
326 * @return bool|string Formatted date, or false if the input is invalid. 327 * @return bool|string Formatted date, or false if the input is invalid.
327 */ 328 */
328function format_date($date, $intl = true) 329function format_date($date, $time = true, $intl = true)
329{ 330{
330 if (! $date instanceof DateTime) { 331 if (! $date instanceof DateTime) {
331 return false; 332 return false;
332 } 333 }
333 334
334 if (! $intl || ! class_exists('IntlDateFormatter')) { 335 if (! $intl || ! class_exists('IntlDateFormatter')) {
335 return strftime('%c', $date->getTimestamp()); 336 $format = $time ? '%c' : '%x';
337 return strftime($format, $date->getTimestamp());
336 } 338 }
337 339
338 $formatter = new IntlDateFormatter( 340 $formatter = new IntlDateFormatter(
339 setlocale(LC_TIME, 0), 341 setlocale(LC_TIME, 0),
340 IntlDateFormatter::LONG, 342 IntlDateFormatter::LONG,
341 IntlDateFormatter::LONG 343 $time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE
342 ); 344 );
343 345
344 return $formatter->format($date); 346 return $formatter->format($date);
345} 347}
348
349/**
350 * Check if the input is an integer, no matter its real type.
351 *
352 * PHP is a bit messy regarding this:
353 * - is_int returns false if the input is a string
354 * - ctype_digit returns false if the input is an integer or negative
355 *
356 * @param mixed $input value
357 *
358 * @return bool true if the input is an integer, false otherwise
359 */
360function is_integer_mixed($input)
361{
362 if (is_array($input) || is_bool($input) || is_object($input)) {
363 return false;
364 }
365 $input = strval($input);
366 return ctype_digit($input) || (startsWith($input, '-') && ctype_digit(substr($input, 1)));
367}
368
369/**
370 * Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
371 *
372 * @param string $val Size expressed in string.
373 *
374 * @return int Size expressed in bytes.
375 */
376function return_bytes($val)
377{
378 if (is_integer_mixed($val) || $val === '0' || empty($val)) {
379 return $val;
380 }
381 $val = trim($val);
382 $last = strtolower($val[strlen($val)-1]);
383 $val = intval(substr($val, 0, -1));
384 switch($last) {
385 case 'g': $val *= 1024;
386 case 'm': $val *= 1024;
387 case 'k': $val *= 1024;
388 }
389 return $val;
390}
391
392/**
393 * Return a human readable size from bytes.
394 *
395 * @param int $bytes value
396 *
397 * @return string Human readable size
398 */
399function human_bytes($bytes)
400{
401 if ($bytes === '') {
402 return t('Setting not set');
403 }
404 if (! is_integer_mixed($bytes)) {
405 return $bytes;
406 }
407 $bytes = intval($bytes);
408 if ($bytes === 0) {
409 return t('Unlimited');
410 }
411
412 $units = [t('B'), t('kiB'), t('MiB'), t('GiB')];
413 for ($i = 0; $i < count($units) && $bytes >= 1024; ++$i) {
414 $bytes /= 1024;
415 }
416
417 return round($bytes) . $units[$i];
418}
419
420/**
421 * Try to determine max file size for uploads (POST).
422 * Returns an integer (in bytes) or formatted depending on $format.
423 *
424 * @param mixed $limitPost post_max_size PHP setting
425 * @param mixed $limitUpload upload_max_filesize PHP setting
426 * @param bool $format Format max upload size to human readable size
427 *
428 * @return int|string max upload file size
429 */
430function get_max_upload_size($limitPost, $limitUpload, $format = true)
431{
432 $size1 = return_bytes($limitPost);
433 $size2 = return_bytes($limitUpload);
434 // Return the smaller of two:
435 $maxsize = min($size1, $size2);
436 return $format ? human_bytes($maxsize) : $maxsize;
437}
438
439/**
440 * Sort the given array alphabetically using php-intl if available.
441 * Case sensitive.
442 *
443 * Note: doesn't support multidimensional arrays
444 *
445 * @param array $data Input array, passed by reference
446 * @param bool $reverse Reverse sort if set to true
447 * @param bool $byKeys Sort the array by keys if set to true, by value otherwise.
448 */
449function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
450{
451 $callback = function($a, $b) use ($reverse) {
452 // Collator is part of PHP intl.
453 if (class_exists('Collator')) {
454 $collator = new Collator(setlocale(LC_COLLATE, 0));
455 if (!intl_is_failure(intl_get_error_code())) {
456 return $collator->compare($a, $b) * ($reverse ? -1 : 1);
457 }
458 }
459
460 return strcasecmp($a, $b) * ($reverse ? -1 : 1);
461 };
462
463 if ($byKeys) {
464 uksort($data, $callback);
465 } else {
466 usort($data, $callback);
467 }
468}
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index 4120f7a9..ff209393 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -4,6 +4,7 @@ namespace Shaarli\Api;
4use Shaarli\Api\Exceptions\ApiException; 4use Shaarli\Api\Exceptions\ApiException;
5use Shaarli\Api\Exceptions\ApiAuthorizationException; 5use Shaarli\Api\Exceptions\ApiAuthorizationException;
6 6
7use Shaarli\Config\ConfigManager;
7use Slim\Container; 8use Slim\Container;
8use Slim\Http\Request; 9use Slim\Http\Request;
9use Slim\Http\Response; 10use Slim\Http\Response;
@@ -31,7 +32,7 @@ class ApiMiddleware
31 protected $container; 32 protected $container;
32 33
33 /** 34 /**
34 * @var \ConfigManager instance. 35 * @var ConfigManager instance.
35 */ 36 */
36 protected $conf; 37 protected $conf;
37 38
@@ -121,7 +122,7 @@ class ApiMiddleware
121 * 122 *
122 * FIXME! LinkDB could use a refactoring to avoid this trick. 123 * FIXME! LinkDB could use a refactoring to avoid this trick.
123 * 124 *
124 * @param \ConfigManager $conf instance. 125 * @param ConfigManager $conf instance.
125 */ 126 */
126 protected function setLinkDb($conf) 127 protected function setLinkDb($conf)
127 { 128 {
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index d4015865..f154bb52 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -12,7 +12,7 @@ class ApiUtils
12 /** 12 /**
13 * Validates a JWT token authenticity. 13 * Validates a JWT token authenticity.
14 * 14 *
15 * @param string $token JWT token extracted from the headers. 15 * @param string $token JWT token extracted from the headers.
16 * @param string $secret API secret set in the settings. 16 * @param string $secret API secret set in the settings.
17 * 17 *
18 * @throws ApiAuthorizationException the token is not valid. 18 * @throws ApiAuthorizationException the token is not valid.
@@ -50,7 +50,7 @@ class ApiUtils
50 /** 50 /**
51 * Format a Link for the REST API. 51 * Format a Link for the REST API.
52 * 52 *
53 * @param array $link Link data read from the datastore. 53 * @param array $link Link data read from the datastore.
54 * @param string $indexUrl Shaarli's index URL (used for relative URL). 54 * @param string $indexUrl Shaarli's index URL (used for relative URL).
55 * 55 *
56 * @return array Link data formatted for the REST API. 56 * @return array Link data formatted for the REST API.
@@ -77,4 +77,61 @@ class ApiUtils
77 } 77 }
78 return $out; 78 return $out;
79 } 79 }
80
81 /**
82 * Convert a link given through a request, to a valid link for LinkDB.
83 *
84 * If no URL is provided, it will generate a local note URL.
85 * If no title is provided, it will use the URL as title.
86 *
87 * @param array $input Request Link.
88 * @param bool $defaultPrivate Request Link.
89 *
90 * @return array Formatted link.
91 */
92 public static function buildLinkFromRequest($input, $defaultPrivate)
93 {
94 $input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : '';
95 if (isset($input['private'])) {
96 $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
97 } else {
98 $private = $defaultPrivate;
99 }
100
101 $link = [
102 'title' => ! empty($input['title']) ? $input['title'] : $input['url'],
103 'url' => $input['url'],
104 'description' => ! empty($input['description']) ? $input['description'] : '',
105 'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '',
106 'private' => $private,
107 'created' => new \DateTime(),
108 ];
109 return $link;
110 }
111
112 /**
113 * Update link fields using an updated link object.
114 *
115 * @param array $oldLink data
116 * @param array $newLink data
117 *
118 * @return array $oldLink updated with $newLink values
119 */
120 public static function updateLink($oldLink, $newLink)
121 {
122 foreach (['title', 'url', 'description', 'tags', 'private'] as $field) {
123 $oldLink[$field] = $newLink[$field];
124 }
125 $oldLink['updated'] = new \DateTime();
126
127 if (empty($oldLink['url'])) {
128 $oldLink['url'] = '?' . $oldLink['shorturl'];
129 }
130
131 if (empty($oldLink['title'])) {
132 $oldLink['title'] = $oldLink['url'];
133 }
134
135 return $oldLink;
136 }
80} 137}
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php
index 1dd47f17..3be85b98 100644
--- a/application/api/controllers/ApiController.php
+++ b/application/api/controllers/ApiController.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Config\ConfigManager;
5use \Slim\Container; 6use \Slim\Container;
6 7
7/** 8/**
@@ -19,7 +20,7 @@ abstract class ApiController
19 protected $ci; 20 protected $ci;
20 21
21 /** 22 /**
22 * @var \ConfigManager 23 * @var ConfigManager
23 */ 24 */
24 protected $conf; 25 protected $conf;
25 26
@@ -29,6 +30,11 @@ abstract class ApiController
29 protected $linkDb; 30 protected $linkDb;
30 31
31 /** 32 /**
33 * @var \History
34 */
35 protected $history;
36
37 /**
32 * @var int|null JSON style option. 38 * @var int|null JSON style option.
33 */ 39 */
34 protected $jsonStyle; 40 protected $jsonStyle;
@@ -45,10 +51,21 @@ abstract class ApiController
45 $this->ci = $ci; 51 $this->ci = $ci;
46 $this->conf = $ci->get('conf'); 52 $this->conf = $ci->get('conf');
47 $this->linkDb = $ci->get('db'); 53 $this->linkDb = $ci->get('db');
54 $this->history = $ci->get('history');
48 if ($this->conf->get('dev.debug', false)) { 55 if ($this->conf->get('dev.debug', false)) {
49 $this->jsonStyle = JSON_PRETTY_PRINT; 56 $this->jsonStyle = JSON_PRETTY_PRINT;
50 } else { 57 } else {
51 $this->jsonStyle = null; 58 $this->jsonStyle = null;
52 } 59 }
53 } 60 }
61
62 /**
63 * Get the container.
64 *
65 * @return Container
66 */
67 public function getCi()
68 {
69 return $this->ci;
70 }
54} 71}
diff --git a/application/api/controllers/History.php b/application/api/controllers/History.php
new file mode 100644
index 00000000..2ff9deaf
--- /dev/null
+++ b/application/api/controllers/History.php
@@ -0,0 +1,70 @@
1<?php
2
3
4namespace Shaarli\Api\Controllers;
5
6use Shaarli\Api\Exceptions\ApiBadParametersException;
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Class History
12 *
13 * REST API Controller: /history
14 *
15 * @package Shaarli\Api\Controllers
16 */
17class History extends ApiController
18{
19 /**
20 * Service providing operation regarding Shaarli datastore and settings.
21 *
22 * @param Request $request Slim request.
23 * @param Response $response Slim response.
24 *
25 * @return Response response.
26 *
27 * @throws ApiBadParametersException Invalid parameters.
28 */
29 public function getHistory($request, $response)
30 {
31 $history = $this->history->getHistory();
32
33 // Return history operations from the {offset}th, starting from {since}.
34 $since = \DateTime::createFromFormat(\DateTime::ATOM, $request->getParam('since'));
35 $offset = $request->getParam('offset');
36 if (empty($offset)) {
37 $offset = 0;
38 }
39 else if (ctype_digit($offset)) {
40 $offset = (int) $offset;
41 } else {
42 throw new ApiBadParametersException('Invalid offset');
43 }
44
45 // limit parameter is either a number of links or 'all' for everything.
46 $limit = $request->getParam('limit');
47 if (empty($limit)) {
48 $limit = count($history);
49 } else if (ctype_digit($limit)) {
50 $limit = (int) $limit;
51 } else {
52 throw new ApiBadParametersException('Invalid limit');
53 }
54
55 $out = [];
56 $i = 0;
57 foreach ($history as $entry) {
58 if ((! empty($since) && $entry['datetime'] <= $since) || count($out) >= $limit) {
59 break;
60 }
61 if (++$i > $offset) {
62 $out[$i] = $entry;
63 $out[$i]['datetime'] = $out[$i]['datetime']->format(\DateTime::ATOM);
64 }
65 }
66 $out = array_values($out);
67
68 return $response->withJson($out, 200, $this->jsonStyle);
69 }
70}
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index d4f1a09c..eb78dd26 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -97,11 +97,121 @@ class Links extends ApiController
97 */ 97 */
98 public function getLink($request, $response, $args) 98 public function getLink($request, $response, $args)
99 { 99 {
100 if (! isset($this->linkDb[$args['id']])) { 100 if (!isset($this->linkDb[$args['id']])) {
101 throw new ApiLinkNotFoundException(); 101 throw new ApiLinkNotFoundException();
102 } 102 }
103 $index = index_url($this->ci['environment']); 103 $index = index_url($this->ci['environment']);
104 $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index); 104 $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index);
105
105 return $response->withJson($out, 200, $this->jsonStyle); 106 return $response->withJson($out, 200, $this->jsonStyle);
106 } 107 }
108
109 /**
110 * Creates a new link from posted request body.
111 *
112 * @param Request $request Slim request.
113 * @param Response $response Slim response.
114 *
115 * @return Response response.
116 */
117 public function postLink($request, $response)
118 {
119 $data = $request->getParsedBody();
120 $link = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
121 // duplicate by URL, return 409 Conflict
122 if (! empty($link['url']) && ! empty($dup = $this->linkDb->getLinkFromUrl($link['url']))) {
123 return $response->withJson(
124 ApiUtils::formatLink($dup, index_url($this->ci['environment'])),
125 409,
126 $this->jsonStyle
127 );
128 }
129
130 $link['id'] = $this->linkDb->getNextId();
131 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
132
133 // note: general relative URL
134 if (empty($link['url'])) {
135 $link['url'] = '?' . $link['shorturl'];
136 }
137
138 if (empty($link['title'])) {
139 $link['title'] = $link['url'];
140 }
141
142 $this->linkDb[$link['id']] = $link;
143 $this->linkDb->save($this->conf->get('resource.page_cache'));
144 $this->history->addLink($link);
145 $out = ApiUtils::formatLink($link, index_url($this->ci['environment']));
146 $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $link['id']]);
147 return $response->withAddedHeader('Location', $redirect)
148 ->withJson($out, 201, $this->jsonStyle);
149 }
150
151 /**
152 * Updates an existing link from posted request body.
153 *
154 * @param Request $request Slim request.
155 * @param Response $response Slim response.
156 * @param array $args Path parameters. including the ID.
157 *
158 * @return Response response.
159 *
160 * @throws ApiLinkNotFoundException generating a 404 error.
161 */
162 public function putLink($request, $response, $args)
163 {
164 if (! isset($this->linkDb[$args['id']])) {
165 throw new ApiLinkNotFoundException();
166 }
167
168 $index = index_url($this->ci['environment']);
169 $data = $request->getParsedBody();
170
171 $requestLink = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
172 // duplicate URL on a different link, return 409 Conflict
173 if (! empty($requestLink['url'])
174 && ! empty($dup = $this->linkDb->getLinkFromUrl($requestLink['url']))
175 && $dup['id'] != $args['id']
176 ) {
177 return $response->withJson(
178 ApiUtils::formatLink($dup, $index),
179 409,
180 $this->jsonStyle
181 );
182 }
183
184 $responseLink = $this->linkDb[$args['id']];
185 $responseLink = ApiUtils::updateLink($responseLink, $requestLink);
186 $this->linkDb[$responseLink['id']] = $responseLink;
187 $this->linkDb->save($this->conf->get('resource.page_cache'));
188 $this->history->updateLink($responseLink);
189
190 $out = ApiUtils::formatLink($responseLink, $index);
191 return $response->withJson($out, 200, $this->jsonStyle);
192 }
193
194 /**
195 * Delete an existing link by its ID.
196 *
197 * @param Request $request Slim request.
198 * @param Response $response Slim response.
199 * @param array $args Path parameters. including the ID.
200 *
201 * @return Response response.
202 *
203 * @throws ApiLinkNotFoundException generating a 404 error.
204 */
205 public function deleteLink($request, $response, $args)
206 {
207 if (! isset($this->linkDb[$args['id']])) {
208 throw new ApiLinkNotFoundException();
209 }
210 $link = $this->linkDb[$args['id']];
211 unset($this->linkDb[(int) $args['id']]);
212 $this->linkDb->save($this->conf->get('resource.page_cache'));
213 $this->history->deleteLink($link);
214
215 return $response->withStatus(204);
216 }
107} 217}
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index 7bfbfc72..86a917fb 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -301,6 +301,7 @@ class ConfigManager
301 $this->setEmpty('resource.updates', 'data/updates.txt'); 301 $this->setEmpty('resource.updates', 'data/updates.txt');
302 $this->setEmpty('resource.log', 'data/log.txt'); 302 $this->setEmpty('resource.log', 'data/log.txt');
303 $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt'); 303 $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
304 $this->setEmpty('resource.history', 'data/history.php');
304 $this->setEmpty('resource.raintpl_tpl', 'tpl/'); 305 $this->setEmpty('resource.raintpl_tpl', 'tpl/');
305 $this->setEmpty('resource.theme', 'default'); 306 $this->setEmpty('resource.theme', 'default');
306 $this->setEmpty('resource.raintpl_tmp', 'tmp/'); 307 $this->setEmpty('resource.raintpl_tmp', 'tmp/');
diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php
new file mode 100644
index 00000000..b563b23d
--- /dev/null
+++ b/application/exceptions/IOException.php
@@ -0,0 +1,22 @@
1<?php
2
3/**
4 * Exception class thrown when a filesystem access failure happens
5 */
6class IOException extends Exception
7{
8 private $path;
9
10 /**
11 * Construct a new IOException
12 *
13 * @param string $path path to the resource that cannot be accessed
14 * @param string $message Custom exception message.
15 */
16 public function __construct($path, $message = '')
17 {
18 $this->path = $path;
19 $this->message = empty($message) ? 'Error accessing' : $message;
20 $this->message .= ' "' . $this->path .'"';
21 }
22}
diff --git a/composer.lock b/composer.lock
index b285fcc9..0c1efa6b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1793,16 +1793,16 @@
1793 }, 1793 },
1794 { 1794 {
1795 "name": "squizlabs/php_codesniffer", 1795 "name": "squizlabs/php_codesniffer",
1796 "version": "2.8.1", 1796 "version": "2.9.0",
1797 "source": { 1797 "source": {
1798 "type": "git", 1798 "type": "git",
1799 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 1799 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
1800 "reference": "d7cf0d894e8aa4c73712ee4a331cc1eaa37cdc7d" 1800 "reference": "f7dfecbee89d68ab475a6c9e17d22bc9b69aed97"
1801 }, 1801 },
1802 "dist": { 1802 "dist": {
1803 "type": "zip", 1803 "type": "zip",
1804 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d7cf0d894e8aa4c73712ee4a331cc1eaa37cdc7d", 1804 "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/f7dfecbee89d68ab475a6c9e17d22bc9b69aed97",
1805 "reference": "d7cf0d894e8aa4c73712ee4a331cc1eaa37cdc7d", 1805 "reference": "f7dfecbee89d68ab475a6c9e17d22bc9b69aed97",
1806 "shasum": "" 1806 "shasum": ""
1807 }, 1807 },
1808 "require": { 1808 "require": {
@@ -1867,20 +1867,20 @@
1867 "phpcs", 1867 "phpcs",
1868 "standards" 1868 "standards"
1869 ], 1869 ],
1870 "time": "2017-03-01T22:17:45+00:00" 1870 "time": "2017-05-03T23:30:39+00:00"
1871 }, 1871 },
1872 { 1872 {
1873 "name": "symfony/config", 1873 "name": "symfony/config",
1874 "version": "v3.2.6", 1874 "version": "v3.2.8",
1875 "source": { 1875 "source": {
1876 "type": "git", 1876 "type": "git",
1877 "url": "https://github.com/symfony/config.git", 1877 "url": "https://github.com/symfony/config.git",
1878 "reference": "741d6d4cd1414d67d48eb71aba6072b46ba740c2" 1878 "reference": "e5533fcc0b3dd377626153b2852707878f363728"
1879 }, 1879 },
1880 "dist": { 1880 "dist": {
1881 "type": "zip", 1881 "type": "zip",
1882 "url": "https://api.github.com/repos/symfony/config/zipball/741d6d4cd1414d67d48eb71aba6072b46ba740c2", 1882 "url": "https://api.github.com/repos/symfony/config/zipball/e5533fcc0b3dd377626153b2852707878f363728",
1883 "reference": "741d6d4cd1414d67d48eb71aba6072b46ba740c2", 1883 "reference": "e5533fcc0b3dd377626153b2852707878f363728",
1884 "shasum": "" 1884 "shasum": ""
1885 }, 1885 },
1886 "require": { 1886 "require": {
@@ -1923,20 +1923,20 @@
1923 ], 1923 ],
1924 "description": "Symfony Config Component", 1924 "description": "Symfony Config Component",
1925 "homepage": "https://symfony.com", 1925 "homepage": "https://symfony.com",
1926 "time": "2017-03-01T18:18:25+00:00" 1926 "time": "2017-04-12T14:13:17+00:00"
1927 }, 1927 },
1928 { 1928 {
1929 "name": "symfony/console", 1929 "name": "symfony/console",
1930 "version": "v2.8.18", 1930 "version": "v2.8.20",
1931 "source": { 1931 "source": {
1932 "type": "git", 1932 "type": "git",
1933 "url": "https://github.com/symfony/console.git", 1933 "url": "https://github.com/symfony/console.git",
1934 "reference": "81508e6fac4476771275a3f4f53c3fee9b956bfa" 1934 "reference": "2cfcbced8e39e2313ed4da8896fc8c59a56c0d7e"
1935 }, 1935 },
1936 "dist": { 1936 "dist": {
1937 "type": "zip", 1937 "type": "zip",
1938 "url": "https://api.github.com/repos/symfony/console/zipball/81508e6fac4476771275a3f4f53c3fee9b956bfa", 1938 "url": "https://api.github.com/repos/symfony/console/zipball/2cfcbced8e39e2313ed4da8896fc8c59a56c0d7e",
1939 "reference": "81508e6fac4476771275a3f4f53c3fee9b956bfa", 1939 "reference": "2cfcbced8e39e2313ed4da8896fc8c59a56c0d7e",
1940 "shasum": "" 1940 "shasum": ""
1941 }, 1941 },
1942 "require": { 1942 "require": {
@@ -1984,7 +1984,7 @@
1984 ], 1984 ],
1985 "description": "Symfony Console Component", 1985 "description": "Symfony Console Component",
1986 "homepage": "https://symfony.com", 1986 "homepage": "https://symfony.com",
1987 "time": "2017-03-04T11:00:12+00:00" 1987 "time": "2017-04-26T01:38:53+00:00"
1988 }, 1988 },
1989 { 1989 {
1990 "name": "symfony/debug", 1990 "name": "symfony/debug",
@@ -2045,16 +2045,16 @@
2045 }, 2045 },
2046 { 2046 {
2047 "name": "symfony/dependency-injection", 2047 "name": "symfony/dependency-injection",
2048 "version": "v3.2.6", 2048 "version": "v3.2.8",
2049 "source": { 2049 "source": {
2050 "type": "git", 2050 "type": "git",
2051 "url": "https://github.com/symfony/dependency-injection.git", 2051 "url": "https://github.com/symfony/dependency-injection.git",
2052 "reference": "74e0935e414ad33d5e82074212c0eedb4681a691" 2052 "reference": "5e00857475b6d1fa31ff4c76f1fddf1cfa9e8d59"
2053 }, 2053 },
2054 "dist": { 2054 "dist": {
2055 "type": "zip", 2055 "type": "zip",
2056 "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/74e0935e414ad33d5e82074212c0eedb4681a691", 2056 "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5e00857475b6d1fa31ff4c76f1fddf1cfa9e8d59",
2057 "reference": "74e0935e414ad33d5e82074212c0eedb4681a691", 2057 "reference": "5e00857475b6d1fa31ff4c76f1fddf1cfa9e8d59",
2058 "shasum": "" 2058 "shasum": ""
2059 }, 2059 },
2060 "require": { 2060 "require": {
@@ -2104,20 +2104,20 @@
2104 ], 2104 ],
2105 "description": "Symfony DependencyInjection Component", 2105 "description": "Symfony DependencyInjection Component",
2106 "homepage": "https://symfony.com", 2106 "homepage": "https://symfony.com",
2107 "time": "2017-03-05T00:06:55+00:00" 2107 "time": "2017-04-26T01:39:17+00:00"
2108 }, 2108 },
2109 { 2109 {
2110 "name": "symfony/filesystem", 2110 "name": "symfony/filesystem",
2111 "version": "v3.2.6", 2111 "version": "v3.2.8",
2112 "source": { 2112 "source": {
2113 "type": "git", 2113 "type": "git",
2114 "url": "https://github.com/symfony/filesystem.git", 2114 "url": "https://github.com/symfony/filesystem.git",
2115 "reference": "bc0f17bed914df2cceb989972c3b996043c4da4a" 2115 "reference": "040651db13cf061827a460cc10f6e36a445c45b4"
2116 }, 2116 },
2117 "dist": { 2117 "dist": {
2118 "type": "zip", 2118 "type": "zip",
2119 "url": "https://api.github.com/repos/symfony/filesystem/zipball/bc0f17bed914df2cceb989972c3b996043c4da4a", 2119 "url": "https://api.github.com/repos/symfony/filesystem/zipball/040651db13cf061827a460cc10f6e36a445c45b4",
2120 "reference": "bc0f17bed914df2cceb989972c3b996043c4da4a", 2120 "reference": "040651db13cf061827a460cc10f6e36a445c45b4",
2121 "shasum": "" 2121 "shasum": ""
2122 }, 2122 },
2123 "require": { 2123 "require": {
@@ -2153,20 +2153,20 @@
2153 ], 2153 ],
2154 "description": "Symfony Filesystem Component", 2154 "description": "Symfony Filesystem Component",
2155 "homepage": "https://symfony.com", 2155 "homepage": "https://symfony.com",
2156 "time": "2017-03-06T19:30:27+00:00" 2156 "time": "2017-04-12T14:13:17+00:00"
2157 }, 2157 },
2158 { 2158 {
2159 "name": "symfony/finder", 2159 "name": "symfony/finder",
2160 "version": "v3.2.6", 2160 "version": "v3.2.8",
2161 "source": { 2161 "source": {
2162 "type": "git", 2162 "type": "git",
2163 "url": "https://github.com/symfony/finder.git", 2163 "url": "https://github.com/symfony/finder.git",
2164 "reference": "92d7476d2df60cd851a3e13e078664b1deb8ce10" 2164 "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930"
2165 }, 2165 },
2166 "dist": { 2166 "dist": {
2167 "type": "zip", 2167 "type": "zip",
2168 "url": "https://api.github.com/repos/symfony/finder/zipball/92d7476d2df60cd851a3e13e078664b1deb8ce10", 2168 "url": "https://api.github.com/repos/symfony/finder/zipball/9cf076f8f492f4b1ffac40aae9c2d287b4ca6930",
2169 "reference": "92d7476d2df60cd851a3e13e078664b1deb8ce10", 2169 "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930",
2170 "shasum": "" 2170 "shasum": ""
2171 }, 2171 },
2172 "require": { 2172 "require": {
@@ -2202,7 +2202,7 @@
2202 ], 2202 ],
2203 "description": "Symfony Finder Component", 2203 "description": "Symfony Finder Component",
2204 "homepage": "https://symfony.com", 2204 "homepage": "https://symfony.com",
2205 "time": "2017-02-21T09:12:04+00:00" 2205 "time": "2017-04-12T14:13:17+00:00"
2206 }, 2206 },
2207 { 2207 {
2208 "name": "symfony/polyfill-mbstring", 2208 "name": "symfony/polyfill-mbstring",
@@ -2265,16 +2265,16 @@
2265 }, 2265 },
2266 { 2266 {
2267 "name": "symfony/yaml", 2267 "name": "symfony/yaml",
2268 "version": "v3.2.6", 2268 "version": "v3.2.8",
2269 "source": { 2269 "source": {
2270 "type": "git", 2270 "type": "git",
2271 "url": "https://github.com/symfony/yaml.git", 2271 "url": "https://github.com/symfony/yaml.git",
2272 "reference": "093e416ad096355149e265ea2e4cc1f9ee40ab1a" 2272 "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6"
2273 }, 2273 },
2274 "dist": { 2274 "dist": {
2275 "type": "zip", 2275 "type": "zip",
2276 "url": "https://api.github.com/repos/symfony/yaml/zipball/093e416ad096355149e265ea2e4cc1f9ee40ab1a", 2276 "url": "https://api.github.com/repos/symfony/yaml/zipball/acec26fcf7f3031e094e910b94b002fa53d4e4d6",
2277 "reference": "093e416ad096355149e265ea2e4cc1f9ee40ab1a", 2277 "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6",
2278 "shasum": "" 2278 "shasum": ""
2279 }, 2279 },
2280 "require": { 2280 "require": {
@@ -2316,20 +2316,20 @@
2316 ], 2316 ],
2317 "description": "Symfony Yaml Component", 2317 "description": "Symfony Yaml Component",
2318 "homepage": "https://symfony.com", 2318 "homepage": "https://symfony.com",
2319 "time": "2017-03-07T16:47:02+00:00" 2319 "time": "2017-05-01T14:55:58+00:00"
2320 }, 2320 },
2321 { 2321 {
2322 "name": "theseer/fdomdocument", 2322 "name": "theseer/fdomdocument",
2323 "version": "1.6.1", 2323 "version": "1.6.5",
2324 "source": { 2324 "source": {
2325 "type": "git", 2325 "type": "git",
2326 "url": "https://github.com/theseer/fDOMDocument.git", 2326 "url": "https://github.com/theseer/fDOMDocument.git",
2327 "reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684" 2327 "reference": "8dcfd392135a5bd938c3c83ea71419501ad9855d"
2328 }, 2328 },
2329 "dist": { 2329 "dist": {
2330 "type": "zip", 2330 "type": "zip",
2331 "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/d9ad139d6c2e8edf5e313ffbe37ff13344cf0684", 2331 "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/8dcfd392135a5bd938c3c83ea71419501ad9855d",
2332 "reference": "d9ad139d6c2e8edf5e313ffbe37ff13344cf0684", 2332 "reference": "8dcfd392135a5bd938c3c83ea71419501ad9855d",
2333 "shasum": "" 2333 "shasum": ""
2334 }, 2334 },
2335 "require": { 2335 "require": {
@@ -2356,7 +2356,7 @@
2356 ], 2356 ],
2357 "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.", 2357 "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
2358 "homepage": "https://github.com/theseer/fDOMDocument", 2358 "homepage": "https://github.com/theseer/fDOMDocument",
2359 "time": "2015-05-27T22:58:02+00:00" 2359 "time": "2017-04-21T14:50:31+00:00"
2360 }, 2360 },
2361 { 2361 {
2362 "name": "webmozart/assert", 2362 "name": "webmozart/assert",
diff --git a/doc/3rd-party-libraries.html b/doc/3rd-party-libraries.html
index 946ca037..50aba6c0 100644
--- a/doc/3rd-party-libraries.html
+++ b/doc/3rd-party-libraries.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Backup,-restore,-import-and-export.html b/doc/Backup,-restore,-import-and-export.html
index a4a48ad7..3c168824 100644
--- a/doc/Backup,-restore,-import-and-export.html
+++ b/doc/Backup,-restore,-import-and-export.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Browsing-and-searching.html b/doc/Browsing-and-searching.html
index 23001bcb..ef5b5245 100644
--- a/doc/Browsing-and-searching.html
+++ b/doc/Browsing-and-searching.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
@@ -64,7 +66,6 @@
64</div> 66</div>
65<h1 id="browsing-and-searching">Browsing and searching</h1> 67<h1 id="browsing-and-searching">Browsing and searching</h1>
66<h1 id="browsing-and-searching-1">Browsing and Searching</h1> 68<h1 id="browsing-and-searching-1">Browsing and Searching</h1>
67<p>Status: DRAFT</p>
68<p><embed src="(http://pix.toile-libre.org/upload/original/1455571378.png).html" /></p> 69<p><embed src="(http://pix.toile-libre.org/upload/original/1455571378.png).html" /></p>
69<h2 id="plain-text-search">Plain text search</h2> 70<h2 id="plain-text-search">Plain text search</h2>
70<p>Use the <code>Search text</code> field to search in <em>any</em> of the fields of all links (Title, URL, Description...)</p> 71<p>Use the <code>Search text</code> field to search in <em>any</em> of the fields of all links (Title, URL, Description...)</p>
@@ -75,6 +76,7 @@
75<p>Use the <code>Filter by tags</code> field to restrict displayed links to entries tagged with one or multiple tags (use space to separate tags).</p> 76<p>Use the <code>Filter by tags</code> field to restrict displayed links to entries tagged with one or multiple tags (use space to separate tags).</p>
76<p><strong>Hidden tags:</strong> Tags starting with a dot <code>.</code> (example <code>.secret</code>) are private. They can only be seen and searched when logged in.</p> 77<p><strong>Hidden tags:</strong> Tags starting with a dot <code>.</code> (example <code>.secret</code>) are private. They can only be seen and searched when logged in.</p>
77<p>Alternatively you can use the <code>Tag cloud</code> to discover all tags and click on any of them to display related links.</p> 78<p>Alternatively you can use the <code>Tag cloud</code> to discover all tags and click on any of them to display related links.</p>
79<p>To search for links that are not tagged, enter <code>&quot;&quot;</code> in the tag search field.</p>
78<h2 id="filtering-rss-feedspicture-wall">Filtering RSS feeds/Picture wall</h2> 80<h2 id="filtering-rss-feedspicture-wall">Filtering RSS feeds/Picture wall</h2>
79<p>RSS feeds can also be restricted to only return items matching a text/tag search: see <a href="RSS-feeds.html">RSS feeds</a>.</p> 81<p>RSS feeds can also be restricted to only return items matching a text/tag search: see <a href="RSS-feeds.html">RSS feeds</a>.</p>
80</body> 82</body>
diff --git a/doc/Browsing-and-searching.md b/doc/Browsing-and-searching.md
index 187fe447..854b6b60 100644
--- a/doc/Browsing-and-searching.md
+++ b/doc/Browsing-and-searching.md
@@ -1,8 +1,6 @@
1#Browsing and searching 1#Browsing and searching
2# Browsing and Searching 2# Browsing and Searching
3 3
4Status: DRAFT
5
6![(http://pix.toile-libre.org/upload/original/1455571378.png)]((http://pix.toile-libre.org/upload/original/1455571378.png).html) 4![(http://pix.toile-libre.org/upload/original/1455571378.png)]((http://pix.toile-libre.org/upload/original/1455571378.png).html)
7 5
8## Plain text search 6## Plain text search
@@ -23,6 +21,8 @@ Use the `Filter by tags` field to restrict displayed links to entries tagged wit
23 21
24Alternatively you can use the `Tag cloud` to discover all tags and click on any of them to display related links. 22Alternatively you can use the `Tag cloud` to discover all tags and click on any of them to display related links.
25 23
24To search for links that are not tagged, enter `""` in the tag search field.
25
26## Filtering RSS feeds/Picture wall 26## Filtering RSS feeds/Picture wall
27 27
28RSS feeds can also be restricted to only return items matching a text/tag search: see [RSS feeds](RSS-feeds.html). 28RSS feeds can also be restricted to only return items matching a text/tag search: see [RSS feeds](RSS-feeds.html).
diff --git a/doc/Coding-guidelines.html b/doc/Coding-guidelines.html
index 1a2a9351..8df12182 100644
--- a/doc/Coding-guidelines.html
+++ b/doc/Coding-guidelines.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Community-&-Related-software.html b/doc/Community-&-Related-software.html
index cbc73d54..28b96185 100644
--- a/doc/Community-&-Related-software.html
+++ b/doc/Community-&-Related-software.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
@@ -77,12 +79,20 @@
77<li><a href="http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history">Original revisions history</a><a href=".html"></a></li> 79<li><a href="http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history">Original revisions history</a><a href=".html"></a></li>
78<li><a href="https://www.shaarli.fr/my.php">Shaarli.fr/my</a> - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of <a href="https://github.com/DMeloni">DMeloni</a><a href=".html"></a></li> 80<li><a href="https://www.shaarli.fr/my.php">Shaarli.fr/my</a> - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of <a href="https://github.com/DMeloni">DMeloni</a><a href=".html"></a></li>
79</ul> 81</ul>
82<h3 id="articles-and-social-media-discussions">Articles and social media discussions</h3>
83<ul>
84<li>2016-09-22 - Hacker News - <a href="https://news.ycombinator.com/item?id=12552176" class="uri">https://news.ycombinator.com/item?id=12552176</a></li>
85<li>2015-08-15 - Reddit - <a href="https://www.reddit.com/r/selfhosted/comments/3h3zwh/question_about_migrating_from_wordpress_to_shaarli/">Question about migrating from WordPress to Shaarli.</a><a href=".html"></a></li>
86<li>2015-06-22 - Hacker News - <a href="https://news.ycombinator.com/item?id=9755366" class="uri">https://news.ycombinator.com/item?id=9755366</a></li>
87<li>2015-05-12 - Reddit - <a href="https://www.reddit.com/r/selfhosted/comments/35pkkc/shaarli_self_hosted_bookmarking_delicious_php/">shaarli - Self hosted Bookmarking / Delicious (PHP, MySQL)</a><a href=".html"></a></li>
88</ul>
80<h3 id="third-party-plugins">Third party plugins</h3> 89<h3 id="third-party-plugins">Third party plugins</h3>
81<ul> 90<ul>
82<li><a href="https://github.com/kalvn/shaarli-plugin-autosave">autosave</a> by <a href="https://github.com/kalvn">@kalvn</a>: Automatically saves data when editing a link to avoid any loss in case of crash or unexpected shutdown.<a href=".html"></a></li> 91<li><a href="https://github.com/kalvn/shaarli-plugin-autosave">autosave</a> by <a href="https://github.com/kalvn">@kalvn</a>: Automatically saves data when editing a link to avoid any loss in case of crash or unexpected shutdown.<a href=".html"></a></li>
83<li><a href="https://github.com/ArthurHoaro/code-coloration">Code Coloration</a> by <a href="https://github.com/ArthurHoaro">@ArthurHoaro</a>: client side code syntax highlighter.<a href=".html"></a></li> 92<li><a href="https://github.com/ArthurHoaro/code-coloration">Code Coloration</a> by <a href="https://github.com/ArthurHoaro">@ArthurHoaro</a>: client side code syntax highlighter.<a href=".html"></a></li>
84<li><a href="https://github.com/kalvn/shaarli-plugin-disqus">Disqus</a> by <a href="https://github.com/kalvn">@kalvn</a>: Adds Disqus comment system to your Shaarli.<a href=".html"></a></li> 93<li><a href="https://github.com/kalvn/shaarli-plugin-disqus">Disqus</a> by <a href="https://github.com/kalvn">@kalvn</a>: Adds Disqus comment system to your Shaarli.<a href=".html"></a></li>
85<li><a href="https://github.com/NerosTie/emojione">emojione</a> by <a href="https://github.com/NerosTie">@NerosTie</a>: Add colorful emojis to your Shaarli.<a href=".html"></a></li> 94<li><a href="https://github.com/NerosTie/emojione">emojione</a> by <a href="https://github.com/NerosTie">@NerosTie</a>: Add colorful emojis to your Shaarli.<a href=".html"></a></li>
95<li><a href="https://github.com/ericjuden/Shaarli-Google-Analytics-Plugin">google analytics</a> by <a href="http://github.com/ericjuden">@ericjuden</a>: Adds Google Analytics tracking support<a href=".html"></a></li>
86<li><a href="https://github.com/ArthurHoaro/launch-plugin">launch</a> - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.<a href=".html"></a></li> 96<li><a href="https://github.com/ArthurHoaro/launch-plugin">launch</a> - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.<a href=".html"></a></li>
87<li><a href="https://github.com/alexisju/social">social</a> by <a href="https://github.com/alexisju">@alexisju</a>: share links to social networks.<a href=".html"></a></li> 97<li><a href="https://github.com/alexisju/social">social</a> by <a href="https://github.com/alexisju">@alexisju</a>: share links to social networks.<a href=".html"></a></li>
88<li><a href="https://github.com/ArthurHoaro/shaarli2twitter">shaarli2twitter</a> by <a href="https://github.com/ArthurHoaro">@ArthurHoaro</a> - Automatically tweet your shared links from Shaarli<a href=".html"></a></li> 98<li><a href="https://github.com/ArthurHoaro/shaarli2twitter">shaarli2twitter</a> by <a href="https://github.com/ArthurHoaro">@ArthurHoaro</a> - Automatically tweet your shared links from Shaarli<a href=".html"></a></li>
diff --git a/doc/Community-&-Related-software.md b/doc/Community-&-Related-software.md
index 291bf643..52123a1e 100644
--- a/doc/Community-&-Related-software.md
+++ b/doc/Community-&-Related-software.md
@@ -14,6 +14,11 @@ _TODO: contact repos owners to see if they'd like to standardize their work with
14- [Original revisions history](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)[](.html) 14- [Original revisions history](http://sebsauvage.net/wiki/doku.php?id=php:shaarli:history)[](.html)
15- [Shaarli.fr/my](https://www.shaarli.fr/my.php) - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of [DMeloni](https://github.com/DMeloni)[](.html) 15- [Shaarli.fr/my](https://www.shaarli.fr/my.php) - Unofficial, unsupported (old fork) hosted Shaarlis provider, courtesy of [DMeloni](https://github.com/DMeloni)[](.html)
16 16
17### Articles and social media discussions
18- 2016-09-22 - Hacker News - https://news.ycombinator.com/item?id=12552176
19- 2015-08-15 - Reddit - [Question about migrating from WordPress to Shaarli.](https://www.reddit.com/r/selfhosted/comments/3h3zwh/question_about_migrating_from_wordpress_to_shaarli/)[](.html)
20- 2015-06-22 - Hacker News - https://news.ycombinator.com/item?id=9755366
21- 2015-05-12 - Reddit - [shaarli - Self hosted Bookmarking / Delicious (PHP, MySQL)](https://www.reddit.com/r/selfhosted/comments/35pkkc/shaarli_self_hosted_bookmarking_delicious_php/)[](.html)
17 22
18### Third party plugins 23### Third party plugins
19 24
@@ -22,6 +27,7 @@ _TODO: contact repos owners to see if they'd like to standardize their work with
22 * [Code Coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter.[](.html) 27 * [Code Coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter.[](.html)
23 * [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli.[](.html) 28 * [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli.[](.html)
24 * [emojione](https://github.com/NerosTie/emojione) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli.[](.html) 29 * [emojione](https://github.com/NerosTie/emojione) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli.[](.html)
30 * [google analytics](https://github.com/ericjuden/Shaarli-Google-Analytics-Plugin) by [@ericjuden](http://github.com/ericjuden): Adds Google Analytics tracking support[](.html)
25 * [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.[](.html) 31 * [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.[](.html)
26 * [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks.[](.html) 32 * [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks.[](.html)
27 * [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli[](.html) 33 * [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli[](.html)
diff --git a/doc/Copy-an-existing-installation-over-SSH-and-serve-it-locally.html b/doc/Copy-an-existing-installation-over-SSH-and-serve-it-locally.html
index 9efb1ad6..d6b76add 100644
--- a/doc/Copy-an-existing-installation-over-SSH-and-serve-it-locally.html
+++ b/doc/Copy-an-existing-installation-over-SSH-and-serve-it-locally.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Create-and-serve-multiple-Shaarlis-(farm).html b/doc/Create-and-serve-multiple-Shaarlis-(farm).html
index 672e4bf3..0be81d56 100644
--- a/doc/Create-and-serve-multiple-Shaarlis-(farm).html
+++ b/doc/Create-and-serve-multiple-Shaarlis-(farm).html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Datastore-hacks.html b/doc/Datastore-hacks.html
index 15da09d4..ef3e17bb 100644
--- a/doc/Datastore-hacks.html
+++ b/doc/Datastore-hacks.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Development.html b/doc/Development.html
index c5776413..8a2be413 100644
--- a/doc/Development.html
+++ b/doc/Development.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Directory-structure.html b/doc/Directory-structure.html
index 404ff7c8..3f75db8e 100644
--- a/doc/Directory-structure.html
+++ b/doc/Directory-structure.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -101,33 +103,33 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
101</div> 103</div>
102<h1 id="directory-structure">Directory structure</h1> 104<h1 id="directory-structure">Directory structure</h1>
103<p>Here is the directory structure of Shaarli and the purpose of the different files:</p> 105<p>Here is the directory structure of Shaarli and the purpose of the different files:</p>
104<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"> <span class="ex">index.php</span> <span class="co"># Main program</span> 106<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"> <span class="ex">index.php</span> # Main program
105 <span class="ex">application/</span> <span class="co"># Shaarli classes</span> 107 <span class="ex">application/</span> # Shaarli classes
106 ├── <span class="ex">LinkDB.php</span> 108 ├── <span class="ex">LinkDB.php</span>
107 └── <span class="ex">Utils.php</span> 109 └── <span class="ex">Utils.php</span>
108 <span class="ex">tests/</span> <span class="co"># Shaarli unitary &amp; functional tests</span> 110 <span class="ex">tests/</span> # Shaarli unitary <span class="kw">&amp;</span> <span class="ex">functional</span> tests
109 ├── <span class="ex">LinkDBTest.php</span> 111 ├── <span class="ex">LinkDBTest.php</span>
110 ├── <span class="ex">utils</span> <span class="co"># utilities to ease testing</span> 112 ├── <span class="ex">utils</span> # utilities to ease testing
111 │ └── <span class="ex">ReferenceLinkDB.php</span> 113 │ └── <span class="ex">ReferenceLinkDB.php</span>
112 └── <span class="ex">UtilsTest.php</span> 114 └── <span class="ex">UtilsTest.php</span>
113 <span class="ex">COPYING</span> <span class="co"># Shaarli license</span> 115 <span class="ex">COPYING</span> # Shaarli license
114 <span class="ex">inc/</span> <span class="co"># static assets and 3rd party libraries</span> 116 <span class="ex">inc/</span> # static assets and 3rd party libraries
115 ├── <span class="ex">awesomplete.*</span> <span class="co"># tags autocompletion library</span> 117 ├── <span class="ex">awesomplete.*</span> # tags autocompletion library
116 ├── <span class="ex">blazy.*</span> <span class="co"># picture wall lazy image loading library</span> 118 ├── <span class="ex">blazy.*</span> # picture wall lazy image loading library
117 ├── <span class="ex">shaarli.css</span>, reset.css <span class="co"># Shaarli stylesheet.</span> 119 ├── <span class="ex">shaarli.css</span>, reset.css <span class="co"># Shaarli stylesheet.</span>
118 ├── <span class="ex">qr.*</span> <span class="co"># qr code generation library</span> 120 ├── <span class="ex">qr.*</span> # qr code generation library
119 └──<span class="ex">rain.tpl.class.php</span> <span class="co"># RainTPL templating library</span> 121 └──<span class="ex">rain.tpl.class.php</span> # RainTPL templating library
120 <span class="ex">tpl/</span> <span class="co"># RainTPL templates for Shaarli. They are used to build the pages.</span> 122 <span class="ex">tpl/</span> # RainTPL templates for Shaarli. They are used to build the pages.
121 <span class="ex">images/</span> <span class="co"># Images and icons used in Shaarli</span> 123 <span class="ex">images/</span> # Images and icons used in Shaarli
122 <span class="ex">data/</span> <span class="co"># data storage: bookmark database, configuration, logs, banlist…</span> 124 <span class="ex">data/</span> # data storage: bookmark database, configuration, logs, banlist…
123 ├── <span class="ex">config.php</span> <span class="co"># Shaarli configuration (login, password, timezone, title…)</span> 125 ├── <span class="ex">config.php</span> # Shaarli configuration (login, password, timezone, title…)
124 ├── <span class="ex">datastore.php</span> <span class="co"># Your link database (compressed).</span> 126 ├── <span class="ex">datastore.php</span> # Your link database (compressed)<span class="ex">.</span>
125 ├── <span class="ex">ipban.php</span> <span class="co"># IP address ban system data</span> 127 ├── <span class="ex">ipban.php</span> # IP address ban system data
126 ├── <span class="ex">lastupdatecheck.txt</span> <span class="co"># Update check timestamp file</span> 128 ├── <span class="ex">lastupdatecheck.txt</span> # Update check timestamp file
127 └──<span class="ex">log.txt</span> <span class="co"># login/IPban log.</span> 129 └──<span class="ex">log.txt</span> # login/IPban log.
128 <span class="ex">cache/</span> <span class="co"># thumbnails cache</span> 130 <span class="ex">cache/</span> # thumbnails cache
129 <span class="co"># This directory is automatically created. You can erase it anytime you want.</span> 131 <span class="co"># This directory is automatically created. You can erase it anytime you want.</span>
130 <span class="ex">tmp/</span> <span class="co"># Temporary directory for compiled RainTPL templates.</span> 132 <span class="ex">tmp/</span> # Temporary directory for compiled RainTPL templates.
131 <span class="co"># This directory is automatically created. You can erase it anytime you want.</span></code></pre></div> 133 <span class="co"># This directory is automatically created. You can erase it anytime you want.</span></code></pre></div>
132</body> 134</body>
133</html> 135</html>
diff --git a/doc/Docker.html b/doc/Docker.html
index e89c90fb..fd0dec4b 100644
--- a/doc/Docker.html
+++ b/doc/Docker.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -200,7 +202,7 @@ $ <span class="ex">docker</span> ps
200<span class="ex">CONTAINER</span> ID IMAGE COMMAND CREATED STATUS PORTS NAMES 202<span class="ex">CONTAINER</span> ID IMAGE COMMAND CREATED STATUS PORTS NAMES
201<span class="ex">d40b7af693d6</span> shaarli/shaarli /usr/bin/supervisor 15 seconds ago Up 4 seconds 0.0.0.0:8000-<span class="op">&gt;</span>80/tcp backstabbing_galileo</code></pre></div> 203<span class="ex">d40b7af693d6</span> shaarli/shaarli /usr/bin/supervisor 15 seconds ago Up 4 seconds 0.0.0.0:8000-<span class="op">&gt;</span>80/tcp backstabbing_galileo</code></pre></div>
202<h3 id="stop-and-destroy-a-container">Stop and destroy a container</h3> 204<h3 id="stop-and-destroy-a-container">Stop and destroy a container</h3>
203<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="ex">docker</span> stop backstabbing_galileo <span class="co"># those docker guys are really rude to physicists!</span> 205<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="ex">docker</span> stop backstabbing_galileo # those docker guys are really rude to physicists!
204<span class="ex">backstabbing_galileo</span> 206<span class="ex">backstabbing_galileo</span>
205 207
206<span class="co"># check the container is stopped</span> 208<span class="co"># check the container is stopped</span>
@@ -213,14 +215,15 @@ $ <span class="ex">docker</span> ps -a
213<span class="ex">d40b7af693d6</span> shaarli/shaarli /usr/bin/supervisor 5 minutes ago Exited (0) <span class="ex">48</span> seconds ago backstabbing_galileo 215<span class="ex">d40b7af693d6</span> shaarli/shaarli /usr/bin/supervisor 5 minutes ago Exited (0) <span class="ex">48</span> seconds ago backstabbing_galileo
214 216
215<span class="co"># destroy the container</span> 217<span class="co"># destroy the container</span>
216$ <span class="ex">docker</span> rm backstabbing_galileo <span class="co"># let&#39;s put an end to these barbarian practices</span> 218$ <span class="ex">docker</span> rm backstabbing_galileo # let<span class="st">&#39;s put an end to these barbarian practices</span>
217<span class="ex">backstabbing_galileo</span> 219<span class="st">backstabbing_galileo</span>
218 220
219$ <span class="ex">docker</span> ps -a 221<span class="st">$ docker ps -a</span>
220<span class="ex">CONTAINER</span> ID IMAGE COMMAND CREATED STATUS PORTS NAMES</code></pre></div> 222<span class="st">CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES</span></code></pre></div>
221<h2 id="resources">Resources</h2> 223<h2 id="resources">Resources</h2>
222<h3 id="docker-1">Docker</h3> 224<h3 id="docker-1">Docker</h3>
223<ul> 225<ul>
226<li><a href="https://www.katacoda.com/courses/docker/">Interactive Docker training portal</a> on <a href="https://www.katacoda.com/">Katakoda</a><a href=".html"></a></li>
224<li><a href="http://blog.thoward37.me/articles/where-are-docker-images-stored/">Where are Docker images stored?</a><a href=".html"></a></li> 227<li><a href="http://blog.thoward37.me/articles/where-are-docker-images-stored/">Where are Docker images stored?</a><a href=".html"></a></li>
225<li><a href="https://docs.docker.com/reference/builder/">Dockerfile reference</a><a href=".html"></a></li> 228<li><a href="https://docs.docker.com/reference/builder/">Dockerfile reference</a><a href=".html"></a></li>
226<li><a href="https://docs.docker.com/articles/dockerfile_best-practices/">Dockerfile best practices</a><a href=".html"></a></li> 229<li><a href="https://docs.docker.com/articles/dockerfile_best-practices/">Dockerfile best practices</a><a href=".html"></a></li>
diff --git a/doc/Docker.md b/doc/Docker.md
index 1faa7904..a7d2efb5 100644
--- a/doc/Docker.md
+++ b/doc/Docker.md
@@ -141,6 +141,7 @@ CONTAINER ID IMAGE COMMAND CREATED STATUS
141 141
142## Resources 142## Resources
143### Docker 143### Docker
144- [Interactive Docker training portal](https://www.katacoda.com/courses/docker/) on [Katakoda](https://www.katacoda.com/)[](.html)
144- [Where are Docker images stored?](http://blog.thoward37.me/articles/where-are-docker-images-stored/)[](.html) 145- [Where are Docker images stored?](http://blog.thoward37.me/articles/where-are-docker-images-stored/)[](.html)
145- [Dockerfile reference](https://docs.docker.com/reference/builder/)[](.html) 146- [Dockerfile reference](https://docs.docker.com/reference/builder/)[](.html)
146- [Dockerfile best practices](https://docs.docker.com/articles/dockerfile_best-practices/)[](.html) 147- [Dockerfile best practices](https://docs.docker.com/articles/dockerfile_best-practices/)[](.html)
diff --git a/doc/Download-CSS-styles-from-an-OPML-list.html b/doc/Download-CSS-styles-from-an-OPML-list.html
index a4f68ac6..18cc5d9a 100644
--- a/doc/Download-CSS-styles-from-an-OPML-list.html
+++ b/doc/Download-CSS-styles-from-an-OPML-list.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -155,7 +157,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
155<span class="kw">function</span> copyUserStyleFrom<span class="ot">(</span><span class="kw">$url</span><span class="ot">,</span> <span class="kw">$name</span><span class="ot">,</span> <span class="kw">$knownStyles</span><span class="ot">)</span> { 157<span class="kw">function</span> copyUserStyleFrom<span class="ot">(</span><span class="kw">$url</span><span class="ot">,</span> <span class="kw">$name</span><span class="ot">,</span> <span class="kw">$knownStyles</span><span class="ot">)</span> {
156 <span class="kw">$userStyle</span> = <span class="kw">$url</span>.<span class="st">&quot;inc/user.css&quot;</span><span class="ot">;</span> 158 <span class="kw">$userStyle</span> = <span class="kw">$url</span>.<span class="st">&quot;inc/user.css&quot;</span><span class="ot">;</span>
157 <span class="kw">if</span><span class="ot">(</span><span class="fu">in_array</span><span class="ot">(</span><span class="kw">$url</span><span class="ot">,</span> <span class="kw">$knownStyles</span><span class="ot">))</span> { 159 <span class="kw">if</span><span class="ot">(</span><span class="fu">in_array</span><span class="ot">(</span><span class="kw">$url</span><span class="ot">,</span> <span class="kw">$knownStyles</span><span class="ot">))</span> {
158 <span class="co">// TODO add log message</span> 160 <span class="co">// </span><span class="al">TODO</span><span class="co"> add log message</span>
159 } <span class="kw">else</span> { 161 } <span class="kw">else</span> {
160 <span class="kw">$statusCode</span> = get_http_response_code<span class="ot">(</span><span class="kw">$userStyle</span><span class="ot">);</span> 162 <span class="kw">$statusCode</span> = get_http_response_code<span class="ot">(</span><span class="kw">$userStyle</span><span class="ot">);</span>
161 <span class="kw">if</span><span class="ot">(</span><span class="fu">intval</span><span class="ot">(</span><span class="kw">$statusCode</span><span class="ot">)</span>&lt;<span class="dv">300</span><span class="ot">)</span> { 163 <span class="kw">if</span><span class="ot">(</span><span class="fu">intval</span><span class="ot">(</span><span class="kw">$statusCode</span><span class="ot">)</span>&lt;<span class="dv">300</span><span class="ot">)</span> {
diff --git a/doc/Download-and-Installation.html b/doc/Download-and-Installation.html
index b9cac360..2c5b3be2 100644
--- a/doc/Download-and-Installation.html
+++ b/doc/Download-and-Installation.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -108,10 +110,10 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
108<h3 id="download-as-an-archive">Download as an archive</h3> 110<h3 id="download-as-an-archive">Download as an archive</h3>
109<p>Get the latest released version from the <a href="https://github.com/shaarli/Shaarli/releases">releases</a> page.<a href=".html"></a></p> 111<p>Get the latest released version from the <a href="https://github.com/shaarli/Shaarli/releases">releases</a> page.<a href=".html"></a></p>
110<p><strong>Download our <em>shaarli-full</em> archive</strong> to include dependencies.</p> 112<p><strong>Download our <em>shaarli-full</em> archive</strong> to include dependencies.</p>
111<p>The current latest released version is <code>v0.8.0</code></p> 113<p>The current latest released version is <code>v0.8.4</code></p>
112<p>Or in command lines:</p> 114<p>Or in command lines:</p>
113<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="fu">wget</span> https://github.com/shaarli/Shaarli/releases/download/v0.8.0/shaarli-v0.8.0-full.zip 115<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="fu">wget</span> https://github.com/shaarli/Shaarli/releases/download/v0.8.4/shaarli-v0.8.4-full.zip
114$ <span class="fu">unzip</span> shaarli-v0.8.0-full.zip 116$ <span class="fu">unzip</span> shaarli-v0.8.4-full.zip
115$ <span class="fu">mv</span> Shaarli /path/to/shaarli/</code></pre></div> 117$ <span class="fu">mv</span> Shaarli /path/to/shaarli/</code></pre></div>
116<table style="width:46%;"> 118<table style="width:46%;">
117<colgroup> 119<colgroup>
@@ -129,8 +131,8 @@ $ <span class="fu">mv</span> Shaarli /path/to/shaarli/</code></pre></div>
129</table> 131</table>
130<h3 id="using-git">Using git</h3> 132<h3 id="using-git">Using git</h3>
131<pre><code>mkdir -p /path/to/shaarli &amp;&amp; cd /path/to/shaarli/ 133<pre><code>mkdir -p /path/to/shaarli &amp;&amp; cd /path/to/shaarli/
132git clone -b v0.8.0 https://github.com/shaarli/Shaarli.git . 134git clone -b v0.8 https://github.com/shaarli/Shaarli.git .
133composer update --no-dev</code></pre> 135composer install --no-dev</code></pre>
134<hr /> 136<hr />
135<h2 id="stable-version">Stable version</h2> 137<h2 id="stable-version">Stable version</h2>
136<p>The stable version has been experienced by Shaarli users, and will receive security updates.</p> 138<p>The stable version has been experienced by Shaarli users, and will receive security updates.</p>
@@ -148,16 +150,16 @@ $ <span class="fu">mv</span> Shaarli-stable /path/to/shaarli/</code></pre></div>
148<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="fu">git</span> clone https://github.com/shaarli/Shaarli.git -b stable /path/to/shaarli/ 150<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="fu">git</span> clone https://github.com/shaarli/Shaarli.git -b stable /path/to/shaarli/
149<span class="co"># install/update third-party dependencies</span> 151<span class="co"># install/update third-party dependencies</span>
150$ <span class="bu">cd</span> /path/to/shaarli/ 152$ <span class="bu">cd</span> /path/to/shaarli/
151$ <span class="ex">composer</span> update --no-dev</code></pre></div> 153$ <span class="ex">composer</span> install --no-dev</code></pre></div>
152<hr /> 154<hr />
153<h2 id="development-version-mainline">Development version (mainline)</h2> 155<h2 id="development-version-mainline">Development version (mainline)</h2>
154<p><em>Use at your own risk!</em></p> 156<p><em>Use at your own risk!</em></p>
155<p>To get the latest changes from the <code>master</code> branch:</p> 157<p>To get the latest changes from the <code>master</code> branch:</p>
156<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="co"># clone the repository </span> 158<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="co"># clone the repository </span>
157$ <span class="fu">git</span> clone https://github.com/shaarli/Shaarli.git master /path/to/shaarli/ 159$ <span class="fu">git</span> clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
158<span class="co"># install/update third-party dependencies</span> 160<span class="co"># install/update third-party dependencies</span>
159$ <span class="bu">cd</span> /path/to/shaarli 161$ <span class="bu">cd</span> /path/to/shaarli
160$ <span class="ex">composer</span> update --no-dev</code></pre></div> 162$ <span class="ex">composer</span> install --no-dev</code></pre></div>
161<hr /> 163<hr />
162<h2 id="finish-installation">Finish Installation</h2> 164<h2 id="finish-installation">Finish Installation</h2>
163<p>Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.</p> 165<p>Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.</p>
diff --git a/doc/Download-and-Installation.md b/doc/Download-and-Installation.md
index 32df8984..970144a5 100644
--- a/doc/Download-and-Installation.md
+++ b/doc/Download-and-Installation.md
@@ -13,13 +13,13 @@ Get the latest released version from the [releases](https://github.com/shaarli/S
13 13
14**Download our *shaarli-full* archive** to include dependencies. 14**Download our *shaarli-full* archive** to include dependencies.
15 15
16The current latest released version is `v0.8.0` 16The current latest released version is `v0.8.4`
17 17
18Or in command lines: 18Or in command lines:
19 19
20```bash 20```bash
21$ wget https://github.com/shaarli/Shaarli/releases/download/v0.8.0/shaarli-v0.8.0-full.zip 21$ wget https://github.com/shaarli/Shaarli/releases/download/v0.8.4/shaarli-v0.8.4-full.zip
22$ unzip shaarli-v0.8.0-full.zip 22$ unzip shaarli-v0.8.4-full.zip
23$ mv Shaarli /path/to/shaarli/ 23$ mv Shaarli /path/to/shaarli/
24``` 24```
25 25
@@ -30,8 +30,8 @@ $ mv Shaarli /path/to/shaarli/
30 30
31``` 31```
32mkdir -p /path/to/shaarli && cd /path/to/shaarli/ 32mkdir -p /path/to/shaarli && cd /path/to/shaarli/
33git clone -b v0.8.0 https://github.com/shaarli/Shaarli.git . 33git clone -b v0.8 https://github.com/shaarli/Shaarli.git .
34composer update --no-dev 34composer install --no-dev
35``` 35```
36 36
37-------------------------------------------------------- 37--------------------------------------------------------
@@ -66,7 +66,7 @@ $ mv Shaarli-stable /path/to/shaarli/
66$ git clone https://github.com/shaarli/Shaarli.git -b stable /path/to/shaarli/ 66$ git clone https://github.com/shaarli/Shaarli.git -b stable /path/to/shaarli/
67# install/update third-party dependencies 67# install/update third-party dependencies
68$ cd /path/to/shaarli/ 68$ cd /path/to/shaarli/
69$ composer update --no-dev 69$ composer install --no-dev
70``` 70```
71 71
72-------------------------------------------------------- 72--------------------------------------------------------
@@ -79,10 +79,10 @@ To get the latest changes from the `master` branch:
79 79
80```bash 80```bash
81# clone the repository 81# clone the repository
82$ git clone https://github.com/shaarli/Shaarli.git master /path/to/shaarli/ 82$ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
83# install/update third-party dependencies 83# install/update third-party dependencies
84$ cd /path/to/shaarli 84$ cd /path/to/shaarli
85$ composer update --no-dev 85$ composer install --no-dev
86``` 86```
87 87
88-------------------------------------------------------- 88--------------------------------------------------------
diff --git a/doc/Example-patch---add-new-via-field-for-links.html b/doc/Example-patch---add-new-via-field-for-links.html
index 133224e2..49036a74 100644
--- a/doc/Example-patch---add-new-via-field-for-links.html
+++ b/doc/Example-patch---add-new-via-field-for-links.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/FAQ.html b/doc/FAQ.html
index 61f3475f..25584f22 100644
--- a/doc/FAQ.html
+++ b/doc/FAQ.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Firefox-share.html b/doc/Firefox-share.html
index d7dcc282..707119a6 100644
--- a/doc/Firefox-share.html
+++ b/doc/Firefox-share.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/GnuPG-signature.html b/doc/GnuPG-signature.html
index 50b904d5..182a71d4 100644
--- a/doc/GnuPG-signature.html
+++ b/doc/GnuPG-signature.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Home.html b/doc/Home.html
index 970f547e..7f51b93b 100644
--- a/doc/Home.html
+++ b/doc/Home.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Plugin-System.html b/doc/Plugin-System.html
index 655536c6..123bf106 100644
--- a/doc/Plugin-System.html
+++ b/doc/Plugin-System.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -100,9 +102,6 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
100</ul> 102</ul>
101</div> 103</div>
102<h1 id="plugin-system">Plugin System</h1> 104<h1 id="plugin-system">Plugin System</h1>
103<blockquote>
104<p>Note: Plugin current status - in development (not merged into master).</p>
105</blockquote>
106<p><a href="#developer-api"><strong>I am a developer.</strong> Developer API.</a><a href=".html"></a></p> 105<p><a href="#developer-api"><strong>I am a developer.</strong> Developer API.</a><a href=".html"></a></p>
107<p><a href="#guide-for-template-designer"><strong>I am a template designer.</strong> Guide for template designer.</a><a href=".html"></a></p> 106<p><a href="#guide-for-template-designer"><strong>I am a template designer.</strong> Guide for template designer.</a><a href=".html"></a></p>
108<h2 id="developer-api">Developer API</h2> 107<h2 id="developer-api">Developer API</h2>
@@ -121,12 +120,21 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
121| plugins/ 120| plugins/
122|---| demo_plugin/ 121|---| demo_plugin/
123| |---| demo_plugin.php</code></pre> 122| |---| demo_plugin.php</code></pre>
123<h3 id="plugin-initialization">Plugin initialization</h3>
124<p>At the beginning of Shaarli execution, all enabled plugins are loaded. At this point, the plugin system looks for an <code>init()</code> function to execute and run it if it exists. This function must be named this way, and takes the <code>ConfigManager</code> as parameter.</p>
125<pre><code>&lt;plugin_name&gt;_init($conf)</code></pre>
126<p>This function can be used to create initial data, load default settings, etc. But also to set <em>plugin errors</em>. If the initialization function returns an array of strings, they will be understand as errors, and displayed in the header to logged in users.</p>
124<h3 id="understanding-hooks">Understanding hooks</h3> 127<h3 id="understanding-hooks">Understanding hooks</h3>
125<p>A plugin is a set of functions. Each function will be triggered by the plugin system at certain point in Shaarli execution.</p> 128<p>A plugin is a set of functions. Each function will be triggered by the plugin system at certain point in Shaarli execution.</p>
126<p>These functions need to be named with this pattern:</p> 129<p>These functions need to be named with this pattern:</p>
127<pre><code>hook_&lt;plugin_name&gt;_&lt;hook_name&gt;</code></pre> 130<pre><code>hook_&lt;plugin_name&gt;_&lt;hook_name&gt;($data, $conf)</code></pre>
131<p>Parameters:</p>
132<ul>
133<li>data: see <a href="https://github.com/shaarli/Shaarli/wiki/Plugin-System#plugins-data">$data section</a><a href=".html"></a></li>
134<li>conf: the <code>ConfigManager</code> instance.</li>
135</ul>
128<p>For exemple, if my plugin want to add data to the header, this function is needed:</p> 136<p>For exemple, if my plugin want to add data to the header, this function is needed:</p>
129<pre><code>hook_demo_plugin_render_header()</code></pre> 137<pre><code>hook_demo_plugin_render_header</code></pre>
130<p>If this function is declared, and the plugin enabled, it will be called every time Shaarli is rendering the header.</p> 138<p>If this function is declared, and the plugin enabled, it will be called every time Shaarli is rendering the header.</p>
131<h3 id="plugins-data">Plugin's data</h3> 139<h3 id="plugins-data">Plugin's data</h3>
132<h4 id="parameters">Parameters</h4> 140<h4 id="parameters">Parameters</h4>
@@ -159,6 +167,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
159<ul> 167<ul>
160<li><code>description</code>: plugin description</li> 168<li><code>description</code>: plugin description</li>
161<li><code>parameters</code>: user parameter names, separated by a <code>;</code>.</li> 169<li><code>parameters</code>: user parameter names, separated by a <code>;</code>.</li>
170<li><code>parameter.&lt;PARAMETER_NAME&gt;</code>: add a text description the specified parameter.</li>
162</ul> 171</ul>
163<blockquote> 172<blockquote>
164<p>Note: In PHP, <code>parse_ini_file()</code> seems to want strings to be between by quotes <code>&quot;</code> in the ini file.</p> 173<p>Note: In PHP, <code>parse_ini_file()</code> seems to want strings to be between by quotes <code>&quot;</code> in the ini file.</p>
@@ -209,16 +218,28 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
209</tr> 218</tr>
210<tr class="even"> 219<tr class="even">
211<td><a href="#render_tagcloud">render_tagcloud</a></td> 220<td><a href="#render_tagcloud">render_tagcloud</a></td>
212<td style="text-align: center;">Allow to add content at the top and bottom of the page.</td> 221<td style="text-align: center;">Allow to add content at the top and bottom of the page, and after all tags.</td>
213</tr> 222</tr>
214<tr class="odd"> 223<tr class="odd">
224<td><a href="#render_taglist">render_taglist</a></td>
225<td style="text-align: center;">Allow to add content at the top and bottom of the page, and after all tags.</td>
226</tr>
227<tr class="even">
215<td><a href="#render_daily">render_daily</a></td> 228<td><a href="#render_daily">render_daily</a></td>
216<td style="text-align: center;">Allow to add content at the top and bottom of the page, the bottom of each link and to alter data.</td> 229<td style="text-align: center;">Allow to add content at the top and bottom of the page, the bottom of each link and to alter data.</td>
217</tr> 230</tr>
231<tr class="odd">
232<td><a href="#render_feed">render_feed</a></td>
233<td style="text-align: center;">Allow to do add tags in RSS and ATOM feeds.</td>
234</tr>
218<tr class="even"> 235<tr class="even">
219<td><a href="#savelink">savelink</a></td> 236<td><a href="#save_link">save_link</a></td>
220<td style="text-align: center;">Allow to alter the link being saved in the datastore.</td> 237<td style="text-align: center;">Allow to alter the link being saved in the datastore.</td>
221</tr> 238</tr>
239<tr class="odd">
240<td><a href="#delete_link">delete_link</a></td>
241<td style="text-align: center;">Allow to do an action before a link is deleted from the datastore.</td>
242</tr>
222</tbody> 243</tbody>
223</table> 244</table>
224<h4 id="render_header">render_header</h4> 245<h4 id="render_header">render_header</h4>
@@ -376,17 +397,41 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
376<li><p><code>plugin_start_zone</code>: before displaying the template content.</p></li> 397<li><p><code>plugin_start_zone</code>: before displaying the template content.</p></li>
377<li><p><code>plugin_end_zone</code>: after displaying the template content.</p></li> 398<li><p><code>plugin_end_zone</code>: after displaying the template content.</p></li>
378</ul> 399</ul>
400<p>For each tag, the following placeholder can be used:</p>
401<ul>
402<li><code>tag_plugin</code>: after each tag</li>
403</ul>
379<p><img src="http://i.imgur.com/vHmyT3a.png" alt="plugin_start_end_zone_example" /><a href=".html"></a></p> 404<p><img src="http://i.imgur.com/vHmyT3a.png" alt="plugin_start_end_zone_example" /><a href=".html"></a></p>
405<h4 id="render_taglist">render_taglist</h4>
406<p>Triggered when taglist is displayed.</p>
407<p>Allow to add content at the top and bottom of the page.</p>
408<h5 id="data-8">Data</h5>
409<p><code>$data</code> is an array containing:</p>
410<ul>
411<li><code>_LOGGEDIN_</code>: true if user is logged in, false otherwise.</li>
412<li>All templates data.</li>
413</ul>
414<h5 id="template-placeholders-8">Template placeholders</h5>
415<p>Items can be displayed in templates by adding an entry in <code>$data['&lt;placeholder&gt;']</code> array.<a href=".html"></a></p>
416<p>List of placeholders:</p>
417<ul>
418<li><p><code>plugin_start_zone</code>: before displaying the template content.</p></li>
419<li><p><code>plugin_end_zone</code>: after displaying the template content.</p></li>
420</ul>
421<p>For each tag, the following placeholder can be used:</p>
422<ul>
423<li><code>tag_plugin</code>: after each tag</li>
424</ul>
380<h4 id="render_daily">render_daily</h4> 425<h4 id="render_daily">render_daily</h4>
381<p>Triggered when tagcloud is displayed.</p> 426<p>Triggered when tagcloud is displayed.</p>
382<p>Allow to add content at the top and bottom of the page, the bottom of each link and to alter data.</p> 427<p>Allow to add content at the top and bottom of the page, the bottom of each link and to alter data.</p>
383<h5 id="data-8">Data</h5> 428<h5 id="data-9">Data</h5>
384<p><code>$data</code> is an array containing:</p> 429<p><code>$data</code> is an array containing:</p>
385<ul> 430<ul>
386<li><code>_LOGGEDIN_</code>: true if user is logged in, false otherwise.</li> 431<li><code>_LOGGEDIN_</code>: true if user is logged in, false otherwise.</li>
387<li>All templates data, including links.</li> 432<li>All templates data, including links.</li>
388</ul> 433</ul>
389<h5 id="template-placeholders-8">Template placeholders</h5> 434<h5 id="template-placeholders-9">Template placeholders</h5>
390<p>Items can be displayed in templates by adding an entry in <code>$data['&lt;placeholder&gt;']</code> array.<a href=".html"></a></p> 435<p>Items can be displayed in templates by adding an entry in <code>$data['&lt;placeholder&gt;']</code> array.<a href=".html"></a></p>
391<p>List of placeholders:</p> 436<p>List of placeholders:</p>
392<ul> 437<ul>
@@ -397,18 +442,57 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
397<li><p><code>plugin_start_zone</code>: before displaying the template content.</p></li> 442<li><p><code>plugin_start_zone</code>: before displaying the template content.</p></li>
398<li><p><code>plugin_end_zone</code>: after displaying the template content.</p></li> 443<li><p><code>plugin_end_zone</code>: after displaying the template content.</p></li>
399</ul> 444</ul>
400<h4 id="savelink">savelink</h4> 445<h4 id="render_feed">render_feed</h4>
446<p>Triggered when the ATOM or RSS feed is displayed.</p>
447<p>Allow to add tags in the feed, either in the header or for each items. Items (links) can also be altered before being rendered.</p>
448<h5 id="data-10">Data</h5>
449<p><code>$data</code> is an array containing:</p>
450<ul>
451<li><code>_LOGGEDIN_</code>: true if user is logged in, false otherwise.</li>
452<li><code>_PAGE_</code>: containing either <code>rss</code> or <code>atom</code>.</li>
453<li>All templates data, including links.</li>
454</ul>
455<h5 id="template-placeholders-10">Template placeholders</h5>
456<p>Tags can be added in feeds by adding an entry in <code>$data['&lt;placeholder&gt;']</code> array.<a href=".html"></a></p>
457<p>List of placeholders:</p>
458<ul>
459<li><code>feed_plugins_header</code>: used as a header tag in the feed.</li>
460</ul>
461<p>For each links:</p>
462<ul>
463<li><code>feed_plugins</code>: additional tag for every link entry.</li>
464</ul>
465<h4 id="save_link">save_link</h4>
401<p>Triggered when a link is save (new link or edit).</p> 466<p>Triggered when a link is save (new link or edit).</p>
402<p>Allow to alter the link being saved in the datastore.</p> 467<p>Allow to alter the link being saved in the datastore.</p>
403<h5 id="data-9">Data</h5> 468<h5 id="data-11">Data</h5>
404<p><code>$data</code> is an array containing the link being saved:</p> 469<p><code>$data</code> is an array containing the link being saved:</p>
405<ul> 470<ul>
471<li>id</li>
406<li>title</li> 472<li>title</li>
407<li>url</li> 473<li>url</li>
474<li>shorturl</li>
408<li>description</li> 475<li>description</li>
409<li>linkdate</li>
410<li>private</li> 476<li>private</li>
411<li>tags</li> 477<li>tags</li>
478<li>created</li>
479<li>updated</li>
480</ul>
481<h4 id="delete_link">delete_link</h4>
482<p>Triggered when a link is deleted.</p>
483<p>Allow to execute any action before the link is actually removed from the datastore</p>
484<h5 id="data-12">Data</h5>
485<p><code>$data</code> is an array containing the link being saved:</p>
486<ul>
487<li>id</li>
488<li>title</li>
489<li>url</li>
490<li>shorturl</li>
491<li>description</li>
492<li>private</li>
493<li>tags</li>
494<li>created</li>
495<li>updated</li>
412</ul> 496</ul>
413<h2 id="guide-for-template-designer">Guide for template designer</h2> 497<h2 id="guide-for-template-designer">Guide for template designer</h2>
414<h3 id="plugin-administration">Plugin administration</h3> 498<h3 id="plugin-administration">Plugin administration</h3>
@@ -537,5 +621,14 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
537 {$value} 621 {$value}
538 {/loop} 622 {/loop}
539<span class="kw">&lt;/div&gt;</span></code></pre></div> 623<span class="kw">&lt;/div&gt;</span></code></pre></div>
624<p><strong>feed.atom.xml</strong> and <strong>feed.rss.xml</strong>:</p>
625<p>In headers tags section:</p>
626<div class="sourceCode"><pre class="sourceCode xml"><code class="sourceCode xml">{loop=&quot;$feed_plugins_header&quot;}
627 {$value}
628{/loop}</code></pre></div>
629<p>After each entry:</p>
630<div class="sourceCode"><pre class="sourceCode xml"><code class="sourceCode xml">{loop=&quot;$value.feed_plugins&quot;}
631 {$value}
632{/loop}</code></pre></div>
540</body> 633</body>
541</html> 634</html>
diff --git a/doc/Plugin-System.md b/doc/Plugin-System.md
index 623627dd..addd792d 100644
--- a/doc/Plugin-System.md
+++ b/doc/Plugin-System.md
@@ -1,6 +1,4 @@
1#Plugin System 1#Plugin System
2> Note: Plugin current status - in development (not merged into master).
3
4[**I am a developer.** Developer API.](#developer-api)[](.html) 2[**I am a developer.** Developer API.](#developer-api)[](.html)
5 3
6[**I am a template designer.** Guide for template designer.](#guide-for-template-designer)[](.html) 4[**I am a template designer.** Guide for template designer.](#guide-for-template-designer)[](.html)
@@ -30,6 +28,14 @@ You should have the following tree view:
30| |---| demo_plugin.php 28| |---| demo_plugin.php
31``` 29```
32 30
31### Plugin initialization
32
33At the beginning of Shaarli execution, all enabled plugins are loaded. At this point, the plugin system looks for an `init()` function to execute and run it if it exists. This function must be named this way, and takes the `ConfigManager` as parameter.
34
35 <plugin_name>_init($conf)
36
37This function can be used to create initial data, load default settings, etc. But also to set *plugin errors*. If the initialization function returns an array of strings, they will be understand as errors, and displayed in the header to logged in users.
38
33### Understanding hooks 39### Understanding hooks
34 40
35A plugin is a set of functions. Each function will be triggered by the plugin system at certain point in Shaarli execution. 41A plugin is a set of functions. Each function will be triggered by the plugin system at certain point in Shaarli execution.
@@ -37,12 +43,17 @@ A plugin is a set of functions. Each function will be triggered by the plugin sy
37These functions need to be named with this pattern: 43These functions need to be named with this pattern:
38 44
39``` 45```
40hook_<plugin_name>_<hook_name> 46hook_<plugin_name>_<hook_name>($data, $conf)
41``` 47```
42 48
49Parameters:
50
51 - data: see [$data section](https://github.com/shaarli/Shaarli/wiki/Plugin-System#plugins-data)[](.html)
52 - conf: the `ConfigManager` instance.
53
43For exemple, if my plugin want to add data to the header, this function is needed: 54For exemple, if my plugin want to add data to the header, this function is needed:
44 55
45 hook_demo_plugin_render_header() 56 hook_demo_plugin_render_header
46 57
47If this function is declared, and the plugin enabled, it will be called every time Shaarli is rendering the header. 58If this function is declared, and the plugin enabled, it will be called every time Shaarli is rendering the header.
48 59
@@ -98,6 +109,7 @@ Each file contain two keys:
98 109
99 * `description`: plugin description 110 * `description`: plugin description
100 * `parameters`: user parameter names, separated by a `;`. 111 * `parameters`: user parameter names, separated by a `;`.
112 * `parameter.<PARAMETER_NAME>`: add a text description the specified parameter.
101 113
102> Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file. 114> Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file.
103 115
@@ -118,9 +130,13 @@ If it's still not working, please [open an issue](https://github.com/shaarli/Sha
118| [render_editlink](#render_editlink) | Allow to add fields in the form, or display elements. |[](.html) 130| [render_editlink](#render_editlink) | Allow to add fields in the form, or display elements. |[](.html)
119| [render_tools](#render_tools) | Allow to add content at the end of the page. |[](.html) 131| [render_tools](#render_tools) | Allow to add content at the end of the page. |[](.html)
120| [render_picwall](#render_picwall) | Allow to add content at the top and bottom of the page. |[](.html) 132| [render_picwall](#render_picwall) | Allow to add content at the top and bottom of the page. |[](.html)
121| [render_tagcloud](#render_tagcloud) | Allow to add content at the top and bottom of the page. |[](.html) 133| [render_tagcloud](#render_tagcloud) | Allow to add content at the top and bottom of the page, and after all tags. |[](.html)
134| [render_taglist](#render_taglist) | Allow to add content at the top and bottom of the page, and after all tags. |[](.html)
122| [render_daily](#render_daily) | Allow to add content at the top and bottom of the page, the bottom of each link and to alter data. |[](.html) 135| [render_daily](#render_daily) | Allow to add content at the top and bottom of the page, the bottom of each link and to alter data. |[](.html)
123| [savelink](#savelink) | Allow to alter the link being saved in the datastore. |[](.html) 136| [render_feed](#render_feed) | Allow to do add tags in RSS and ATOM feeds. |[](.html)
137| [save_link](#save_link) | Allow to alter the link being saved in the datastore. |[](.html)
138| [delete_link](#delete_link) | Allow to do an action before a link is deleted from the datastore. |[](.html)
139
124 140
125 141
126#### render_header 142#### render_header
@@ -330,8 +346,40 @@ List of placeholders:
330 346
331 * `plugin_end_zone`: after displaying the template content. 347 * `plugin_end_zone`: after displaying the template content.
332 348
349For each tag, the following placeholder can be used:
350
351 * `tag_plugin`: after each tag
352
333![plugin_start_end_zone_example](http://i.imgur.com/vHmyT3a.png)[](.html) 353![plugin_start_end_zone_example](http://i.imgur.com/vHmyT3a.png)[](.html)
334 354
355
356#### render_taglist
357
358Triggered when taglist is displayed.
359
360Allow to add content at the top and bottom of the page.
361
362##### Data
363
364`$data` is an array containing:
365
366 * `_LOGGEDIN_`: true if user is logged in, false otherwise.
367 * All templates data.
368
369##### Template placeholders
370
371Items can be displayed in templates by adding an entry in `$data['<placeholder>']` array.[](.html)
372
373List of placeholders:
374
375 * `plugin_start_zone`: before displaying the template content.
376
377 * `plugin_end_zone`: after displaying the template content.
378
379For each tag, the following placeholder can be used:
380
381 * `tag_plugin`: after each tag
382
335#### render_daily 383#### render_daily
336 384
337Triggered when tagcloud is displayed. 385Triggered when tagcloud is displayed.
@@ -359,7 +407,33 @@ List of placeholders:
359 407
360 * `plugin_end_zone`: after displaying the template content. 408 * `plugin_end_zone`: after displaying the template content.
361 409
362#### savelink 410#### render_feed
411
412Triggered when the ATOM or RSS feed is displayed.
413
414Allow to add tags in the feed, either in the header or for each items. Items (links) can also be altered before being rendered.
415
416##### Data
417
418`$data` is an array containing:
419
420 * `_LOGGEDIN_`: true if user is logged in, false otherwise.
421 * `_PAGE_`: containing either `rss` or `atom`.
422 * All templates data, including links.
423
424##### Template placeholders
425
426Tags can be added in feeds by adding an entry in `$data['<placeholder>']` array.[](.html)
427
428List of placeholders:
429
430 * `feed_plugins_header`: used as a header tag in the feed.
431
432For each links:
433
434 * `feed_plugins`: additional tag for every link entry.
435
436#### save_link
363 437
364Triggered when a link is save (new link or edit). 438Triggered when a link is save (new link or edit).
365 439
@@ -369,12 +443,36 @@ Allow to alter the link being saved in the datastore.
369 443
370`$data` is an array containing the link being saved: 444`$data` is an array containing the link being saved:
371 445
446 * id
447 * title
448 * url
449 * shorturl
450 * description
451 * private
452 * tags
453 * created
454 * updated
455
456
457#### delete_link
458
459Triggered when a link is deleted.
460
461Allow to execute any action before the link is actually removed from the datastore
462
463##### Data
464
465`$data` is an array containing the link being saved:
466
467 * id
372 * title 468 * title
373 * url 469 * url
470 * shorturl
374 * description 471 * description
375 * linkdate
376 * private 472 * private
377 * tags 473 * tags
474 * created
475 * updated
378 476
379## Guide for template designer 477## Guide for template designer
380 478
@@ -595,3 +693,19 @@ Bottom:
595 {/loop} 693 {/loop}
596</div> 694</div>
597``` 695```
696
697**feed.atom.xml** and **feed.rss.xml**:
698
699In headers tags section:
700```xml
701{loop="$feed_plugins_header"}
702 {$value}
703{/loop}
704```
705
706After each entry:
707```xml
708{loop="$value.feed_plugins"}
709 {$value}
710{/loop}
711```
diff --git a/doc/Plugins.html b/doc/Plugins.html
index 435a836f..08ce8a86 100644
--- a/doc/Plugins.html
+++ b/doc/Plugins.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -145,7 +147,6 @@ Example:</p>
145<li><a href="https://github.com/shaarli/Shaarli/blob/master/plugins/markdown/README.md"><code>markdown</code></a>: Render shaare description with Markdown syntax.<a href=".html"></a></li> 147<li><a href="https://github.com/shaarli/Shaarli/blob/master/plugins/markdown/README.md"><code>markdown</code></a>: Render shaare description with Markdown syntax.<a href=".html"></a></li>
146<li><a href="https://github.com/shaarli/Shaarli/blob/master/plugins/playvideos/README.md"><code>playvideos</code></a>: Add a button in the toolbar allowing to watch all videos.<a href=".html"></a></li> 148<li><a href="https://github.com/shaarli/Shaarli/blob/master/plugins/playvideos/README.md"><code>playvideos</code></a>: Add a button in the toolbar allowing to watch all videos.<a href=".html"></a></li>
147<li><code>qrcode</code>: For each link, add a QRCode icon.</li> 149<li><code>qrcode</code>: For each link, add a QRCode icon.</li>
148<li><code>readityourself</code>: For each link, add a ReadItYourself icon to save the shaared URL</li>
149<li><a href="https://github.com/shaarli/Shaarli/blob/master/plugins/wallabag/README.md"><code>wallabag</code></a>: For each link, add a Wallabag icon to save it in your instance.<a href=".html"></a></li> 150<li><a href="https://github.com/shaarli/Shaarli/blob/master/plugins/wallabag/README.md"><code>wallabag</code></a>: For each link, add a Wallabag icon to save it in your instance.<a href=".html"></a></li>
150</ul> 151</ul>
151<h4 id="third-party-plugins">Third party plugins</h4> 152<h4 id="third-party-plugins">Third party plugins</h4>
diff --git a/doc/Plugins.md b/doc/Plugins.md
index 81167fcf..e3192a60 100644
--- a/doc/Plugins.md
+++ b/doc/Plugins.md
@@ -67,7 +67,6 @@ Usage of each plugin is documented in it's README file:
67 * [`markdown`](https://github.com/shaarli/Shaarli/blob/master/plugins/markdown/README.md): Render shaare description with Markdown syntax.[](.html) 67 * [`markdown`](https://github.com/shaarli/Shaarli/blob/master/plugins/markdown/README.md): Render shaare description with Markdown syntax.[](.html)
68 * [`playvideos`](https://github.com/shaarli/Shaarli/blob/master/plugins/playvideos/README.md): Add a button in the toolbar allowing to watch all videos.[](.html) 68 * [`playvideos`](https://github.com/shaarli/Shaarli/blob/master/plugins/playvideos/README.md): Add a button in the toolbar allowing to watch all videos.[](.html)
69 * `qrcode`: For each link, add a QRCode icon. 69 * `qrcode`: For each link, add a QRCode icon.
70 * `readityourself`: For each link, add a ReadItYourself icon to save the shaared URL
71 * [`wallabag`](https://github.com/shaarli/Shaarli/blob/master/plugins/wallabag/README.md): For each link, add a Wallabag icon to save it in your instance.[](.html) 70 * [`wallabag`](https://github.com/shaarli/Shaarli/blob/master/plugins/wallabag/README.md): For each link, add a Wallabag icon to save it in your instance.[](.html)
72 71
73 72
diff --git a/doc/REST-API.html b/doc/REST-API.html
new file mode 100644
index 00000000..d14c98c9
--- /dev/null
+++ b/doc/REST-API.html
@@ -0,0 +1,169 @@
1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <meta name="generator" content="pandoc">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
7 <title>Shaarli – REST API</title>
8 <style type="text/css">code{white-space: pre;}</style>
9 <style type="text/css">
10div.sourceCode { overflow-x: auto; }
11table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
12 margin: 0; padding: 0; vertical-align: baseline; border: none; }
13table.sourceCode { width: 100%; line-height: 100%; }
14td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
15td.sourceCode { padding-left: 5px; }
16code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
17code > span.dt { color: #902000; } /* DataType */
18code > span.dv { color: #40a070; } /* DecVal */
19code > span.bn { color: #40a070; } /* BaseN */
20code > span.fl { color: #40a070; } /* Float */
21code > span.ch { color: #4070a0; } /* Char */
22code > span.st { color: #4070a0; } /* String */
23code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
24code > span.ot { color: #007020; } /* Other */
25code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
26code > span.fu { color: #06287e; } /* Function */
27code > span.er { color: #ff0000; font-weight: bold; } /* Error */
28code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
29code > span.cn { color: #880000; } /* Constant */
30code > span.sc { color: #4070a0; } /* SpecialChar */
31code > span.vs { color: #4070a0; } /* VerbatimString */
32code > span.ss { color: #bb6688; } /* SpecialString */
33code > span.im { } /* Import */
34code > span.va { color: #19177c; } /* Variable */
35code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
36code > span.op { color: #666666; } /* Operator */
37code > span.bu { } /* BuiltIn */
38code > span.ex { } /* Extension */
39code > span.pp { color: #bc7a00; } /* Preprocessor */
40code > span.at { color: #7d9029; } /* Attribute */
41code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
42code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
43code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
44code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
45 </style>
46 <link rel="stylesheet" href="github-markdown.css">
47 <!--[if lt IE 9]>
48 <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
49 <![endif]-->
50</head>
51<body>
52<div id="local-sidebar">
53<ul>
54<li><a href="Home.html">Home</a></li>
55<li>Setup
56<ul>
57<li><a href="Download-and-Installation.html">Download and Installation</a></li>
58<li><a href="Upgrade-and-migration.html">Upgrade and migration</a></li>
59<li><a href="Server-requirements.html">Server requirements</a></li>
60<li><a href="Server-configuration.html">Server configuration</a></li>
61<li><a href="Server-security.html">Server security</a></li>
62<li><a href="Shaarli-configuration.html">Shaarli configuration</a></li>
63<li><a href="Plugins.html">Plugins</a></li>
64</ul></li>
65<li><a href="Docker.html">Docker</a></li>
66<li><a href="Usage.html">Usage</a>
67<ul>
68<li><a href="Sharing-button.html">Sharing button</a> (bookmarklet)</li>
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
73</ul></li>
74<li>How To
75<ul>
76<li><a href="Backup,-restore,-import-and-export.html">Backup, restore, import and export</a></li>
77<li><a href="Copy-an-existing-installation-over-SSH-and-serve-it-locally.html">Copy an existing installation over SSH and serve it locally</a></li>
78<li><a href="Create-and-serve-multiple-Shaarlis-(farm).html">Create and serve multiple Shaarlis (farm)</a></li>
79<li><a href="Download-CSS-styles-from-an-OPML-list.html">Download CSS styles from an OPML list</a></li>
80<li><a href="Datastore-hacks.html">Datastore hacks</a></li>
81</ul></li>
82<li><a href="Troubleshooting.html">Troubleshooting</a></li>
83<li><a href="Development.html">Development</a>
84<ul>
85<li><a href="GnuPG-signature.html">GnuPG signature</a></li>
86<li><a href="Coding-guidelines.html">Coding guidelines</a></li>
87<li><a href="Directory-structure.html">Directory structure</a></li>
88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
89<li><a href="Plugin-System.html">Plugin System</a></li>
90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
92<li><a href="Security.html">Security</a></li>
93<li><a href="Static-analysis.html">Static analysis</a></li>
94<li><a href="Theming.html">Theming</a></li>
95<li><a href="Unit-tests.html">Unit tests</a></li>
96</ul></li>
97<li>About
98<ul>
99<li><a href="FAQ.html">FAQ</a></li>
100<li><a href="Community-&amp;-Related-software.html">Community &amp; Related software</a></li>
101</ul></li>
102</ul>
103</div>
104<h1 id="rest-api">REST API</h1>
105<h2 id="usage">Usage</h2>
106<p>See the <a href="http://shaarli.github.io/api-documentation/">REST API documentation</a>.<a href=".html"></a></p>
107<h2 id="authentication">Authentication</h2>
108<p>All requests to Shaarli's API must include a JWT token to verify their authenticity.</p>
109<p>This token has to be included as an HTTP header called <code>Authentication: Bearer &lt;jwt token&gt;</code>.</p>
110<p>JWT resources :</p>
111<ul>
112<li><a href="https://jwt.io">jwt.io</a> (including a list of client per language).<a href=".html"></a></li>
113<li>RFC : <a href="https://tools.ietf.org/html/rfc7519" class="uri">https://tools.ietf.org/html/rfc7519</a></li>
114<li><a href="https://float-middle.com/json-web-tokens-jwt-vs-sessions/" class="uri">https://float-middle.com/json-web-tokens-jwt-vs-sessions/</a></li>
115<li>HackerNews thread: <a href="https://news.ycombinator.com/item?id=11929267" class="uri">https://news.ycombinator.com/item?id=11929267</a></li>
116</ul>
117<h3 id="shaarli-jwt-token">Shaarli JWT Token</h3>
118<p>JWT tokens are composed by three parts, separated by a dot <code>.</code> and encoded in base64:</p>
119<pre><code>[header].[payload].[signature][](.html)</code></pre>
120<h4 id="header">Header</h4>
121<p>Shaarli only allow one hash algorithm, so the header will always be the same:</p>
122<div class="sourceCode"><pre class="sourceCode json"><code class="sourceCode json"><span class="fu">{</span>
123 <span class="dt">&quot;typ&quot;</span><span class="fu">:</span> <span class="st">&quot;JWT&quot;</span><span class="fu">,</span>
124 <span class="dt">&quot;alg&quot;</span><span class="fu">:</span> <span class="st">&quot;HS512&quot;</span>
125<span class="fu">}</span></code></pre></div>
126<p>Encoded in base64, it gives:</p>
127<pre><code>ewogICAgICAgICJ0eXAiOiAiSldUIiwKICAgICAgICAiYWxnIjogIkhTNTEyIgogICAgfQ==</code></pre>
128<h4 id="payload">Payload</h4>
129<p><strong>Validity duration</strong></p>
130<p>To avoid infinite token validity, JWT tokens must include their creation date in UNIX timestamp format (timezone independant - UTC) under the key <code>iat</code> (issued at). This token will be accepted during 9 minutes.</p>
131<div class="sourceCode"><pre class="sourceCode json"><code class="sourceCode json"><span class="fu">{</span>
132 <span class="dt">&quot;iat&quot;</span><span class="fu">:</span> <span class="dv">1468663519</span>
133<span class="fu">}</span></code></pre></div>
134<p>See <a href="https://tools.ietf.org/html/rfc7519#section-4.1.6">RFC reference</a>.<a href=".html"></a></p>
135<h4 id="signature">Signature</h4>
136<p>The signature authenticate the token validity. It contains the base64 of the header and the body, separated by a dot <code>.</code>, hashed in SHA512 with the API secret available in Shaarli administration page.</p>
137<p>Signature example with PHP:</p>
138<div class="sourceCode"><pre class="sourceCode php"><code class="sourceCode php"><span class="kw">$content</span> = <span class="fu">base64_encode</span><span class="ot">(</span><span class="kw">$header</span><span class="ot">)</span> . <span class="st">&#39;.&#39;</span> . <span class="fu">base64_encode</span><span class="ot">(</span><span class="kw">$payload</span><span class="ot">);</span>
139<span class="kw">$signature</span> = <span class="fu">hash_hmac</span><span class="ot">(</span><span class="st">&#39;sha512&#39;</span><span class="ot">,</span> <span class="kw">$content</span><span class="ot">,</span> <span class="kw">$secret</span><span class="ot">);</span></code></pre></div>
140<h3 id="complete-example">Complete example</h3>
141<h4 id="php">PHP</h4>
142<div class="sourceCode"><pre class="sourceCode php"><code class="sourceCode php"><span class="kw">function</span> generateToken<span class="ot">(</span><span class="kw">$secret</span><span class="ot">)</span> {
143 <span class="kw">$header</span> = <span class="fu">base64_encode</span><span class="ot">(</span><span class="st">&#39;{</span>
144<span class="st"> &quot;typ&quot;: &quot;JWT&quot;,</span>
145<span class="st"> &quot;alg&quot;: &quot;HS512&quot;</span>
146<span class="st"> }&#39;</span><span class="ot">);</span>
147 <span class="kw">$payload</span> = <span class="fu">base64_encode</span><span class="ot">(</span><span class="st">&#39;{</span>
148<span class="st"> &quot;iat&quot;: &#39;</span>. <span class="fu">time</span><span class="ot">()</span> .<span class="st">&#39;</span>
149<span class="st"> }&#39;</span><span class="ot">);</span>
150 <span class="kw">$signature</span> = <span class="fu">hash_hmac</span><span class="ot">(</span><span class="st">&#39;sha512&#39;</span><span class="ot">,</span> <span class="kw">$header</span> .<span class="st">&#39;.&#39;</span>. <span class="kw">$payload</span> <span class="ot">,</span> <span class="kw">$secret</span><span class="ot">);</span>
151 <span class="kw">return</span> <span class="kw">$header</span> .<span class="st">&#39;.&#39;</span>. <span class="kw">$payload</span> .<span class="st">&#39;.&#39;</span>. <span class="kw">$signature</span><span class="ot">;</span>
152}
153
154<span class="kw">$secret</span> = <span class="st">&#39;mysecret&#39;</span><span class="ot">;</span>
155<span class="kw">$token</span> = generateToken<span class="ot">(</span><span class="kw">$secret</span><span class="ot">);</span>
156<span class="fu">echo</span> <span class="kw">$token</span><span class="ot">;</span></code></pre></div>
157<blockquote>
158<p><code>ewogICAgICAgICJ0eXAiOiAiSldUIiwKICAgICAgICAiYWxnIjogIkhTNTEyIgogICAgfQ==.ewogICAgICAgICJpYXQiOiAxNDY4NjY3MDQ3CiAgICB9.1d2c54fa947daf594fdbf7591796195652c8bc63bffad7f6a6db2a41c313f495a542cbfb595acade79e83f3810d709b4251d7b940bbc10b531a6e6134af63a68</code></p>
159</blockquote>
160<div class="sourceCode"><pre class="sourceCode php"><code class="sourceCode php"><span class="kw">$options</span> = <span class="ot">[[](</span>.html<span class="ot">)</span>
161 <span class="st">&#39;http&#39;</span> =&gt; <span class="ot">[[](</span>.html<span class="ot">)</span>
162 <span class="st">&#39;method&#39;</span> =&gt; <span class="st">&#39;GET&#39;</span><span class="ot">,</span>
163 <span class="st">&#39;jwt&#39;</span> =&gt; <span class="kw">$token</span><span class="ot">,</span>
164 <span class="ot">],</span>
165<span class="ot">];</span>
166<span class="kw">$context</span> = <span class="fu">stream_context_create</span><span class="ot">(</span><span class="kw">$options</span><span class="ot">);</span>
167<span class="fu">file_get_contents</span><span class="ot">(</span><span class="kw">$apiEndpoint</span><span class="ot">,</span> <span class="kw">false</span><span class="ot">,</span> <span class="kw">$context</span><span class="ot">);</span></code></pre></div>
168</body>
169</html>
diff --git a/doc/REST-API.md b/doc/REST-API.md
new file mode 100644
index 00000000..d7909978
--- /dev/null
+++ b/doc/REST-API.md
@@ -0,0 +1,105 @@
1#REST API
2## Usage
3
4See the [REST API documentation](http://shaarli.github.io/api-documentation/).[](.html)
5
6## Authentication
7
8All requests to Shaarli's API must include a JWT token to verify their authenticity.
9
10This token has to be included as an HTTP header called `Authentication: Bearer <jwt token>`.
11
12JWT resources :
13
14 * [jwt.io](https://jwt.io) (including a list of client per language).[](.html)
15 * RFC : https://tools.ietf.org/html/rfc7519
16 * https://float-middle.com/json-web-tokens-jwt-vs-sessions/
17 * HackerNews thread: https://news.ycombinator.com/item?id=11929267
18
19
20### Shaarli JWT Token
21
22JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64:
23
24```
25[header].[payload].[signature][](.html)
26```
27
28#### Header
29
30Shaarli only allow one hash algorithm, so the header will always be the same:
31
32```json
33{
34 "typ": "JWT",
35 "alg": "HS512"
36}
37```
38
39Encoded in base64, it gives:
40
41```
42ewogICAgICAgICJ0eXAiOiAiSldUIiwKICAgICAgICAiYWxnIjogIkhTNTEyIgogICAgfQ==
43```
44
45#### Payload
46
47**Validity duration**
48
49To avoid infinite token validity, JWT tokens must include their creation date in UNIX timestamp format (timezone independant - UTC) under the key `iat` (issued at). This token will be accepted during 9 minutes.
50
51```json
52{
53 "iat": 1468663519
54}
55```
56
57See [RFC reference](https://tools.ietf.org/html/rfc7519#section-4.1.6).[](.html)
58
59
60#### Signature
61
62The signature authenticate the token validity. It contains the base64 of the header and the body, separated by a dot `.`, hashed in SHA512 with the API secret available in Shaarli administration page.
63
64Signature example with PHP:
65
66```php
67$content = base64_encode($header) . '.' . base64_encode($payload);
68$signature = hash_hmac('sha512', $content, $secret);
69```
70
71
72### Complete example
73
74#### PHP
75
76```php
77function generateToken($secret) {
78 $header = base64_encode('{
79 "typ": "JWT",
80 "alg": "HS512"
81 }');
82 $payload = base64_encode('{
83 "iat": '. time() .'
84 }');
85 $signature = hash_hmac('sha512', $header .'.'. $payload , $secret);
86 return $header .'.'. $payload .'.'. $signature;
87}
88
89$secret = 'mysecret';
90$token = generateToken($secret);
91echo $token;
92```
93
94> `ewogICAgICAgICJ0eXAiOiAiSldUIiwKICAgICAgICAiYWxnIjogIkhTNTEyIgogICAgfQ==.ewogICAgICAgICJpYXQiOiAxNDY4NjY3MDQ3CiAgICB9.1d2c54fa947daf594fdbf7591796195652c8bc63bffad7f6a6db2a41c313f495a542cbfb595acade79e83f3810d709b4251d7b940bbc10b531a6e6134af63a68`
95
96```php
97$options = [[](.html)
98 'http' => [[](.html)
99 'method' => 'GET',
100 'jwt' => $token,
101 ],
102];
103$context = stream_context_create($options);
104file_get_contents($apiEndpoint, false, $context);
105```
diff --git a/doc/RSS-feeds.html b/doc/RSS-feeds.html
index 0f332b3d..0ebfecc6 100644
--- a/doc/RSS-feeds.html
+++ b/doc/RSS-feeds.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Release-Shaarli.html b/doc/Release-Shaarli.html
index 0d9fa3e1..fa690c7c 100644
--- a/doc/Release-Shaarli.html
+++ b/doc/Release-Shaarli.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -192,6 +194,8 @@ $ <span class="fu">git</span> verify-tag f7762cf803f03f5caf4b8078359a63783d0090c
192<span class="ex">gpg</span>: Signature made Thu 30 Jul 2015 11:46:34 CEST using RSA key ID 4100DF6F 194<span class="ex">gpg</span>: Signature made Thu 30 Jul 2015 11:46:34 CEST using RSA key ID 4100DF6F
193<span class="ex">gpg</span>: Good signature from <span class="st">&quot;VirtualTam &lt;virtualtam@flibidi.net&gt;&quot;</span> [ultimate][](.html)</code></pre></div> 195<span class="ex">gpg</span>: Good signature from <span class="st">&quot;VirtualTam &lt;virtualtam@flibidi.net&gt;&quot;</span> [ultimate][](.html)</code></pre></div>
194<h2 id="publish-the-github-release">Publish the GitHub release</h2> 196<h2 id="publish-the-github-release">Publish the GitHub release</h2>
197<h3 id="update-release-badges">Update release badges</h3>
198<p>Update <code>README.md</code> so version badges display and point to the newly released Shaarli version(s).</p>
195<h3 id="create-a-github-release-from-a-git-tag">Create a GitHub release from a Git tag</h3> 199<h3 id="create-a-github-release-from-a-git-tag">Create a GitHub release from a Git tag</h3>
196<p>From the previously drafted release:</p> 200<p>From the previously drafted release:</p>
197<ul> 201<ul>
diff --git a/doc/Release-Shaarli.md b/doc/Release-Shaarli.md
index 556a96ee..ced58853 100644
--- a/doc/Release-Shaarli.md
+++ b/doc/Release-Shaarli.md
@@ -103,6 +103,9 @@ gpg: Good signature from "VirtualTam <virtualtam@flibidi.net>" [ultimate][](.htm
103``` 103```
104 104
105## Publish the GitHub release 105## Publish the GitHub release
106### Update release badges
107Update `README.md` so version badges display and point to the newly released Shaarli version(s).
108
106### Create a GitHub release from a Git tag 109### Create a GitHub release from a Git tag
107From the previously drafted release: 110From the previously drafted release:
108- edit the release notes (if needed) 111- edit the release notes (if needed)
diff --git a/doc/Security.html b/doc/Security.html
index cec20590..12b46fa9 100644
--- a/doc/Security.html
+++ b/doc/Security.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Server-configuration.html b/doc/Server-configuration.html
index 2f1c25b5..0e6b220a 100644
--- a/doc/Server-configuration.html
+++ b/doc/Server-configuration.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -196,6 +198,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
196<h3 id="htaccess">.htaccess</h3> 198<h3 id="htaccess">.htaccess</h3>
197<p>Shaarli use <code>.htaccess</code> Apache files to deny access to files that shouldn't be directly accessed (datastore, config, etc.). You need the directive <code>AllowOverride All</code> in your virtual host configuration for them to work.</p> 199<p>Shaarli use <code>.htaccess</code> Apache files to deny access to files that shouldn't be directly accessed (datastore, config, etc.). You need the directive <code>AllowOverride All</code> in your virtual host configuration for them to work.</p>
198<p><strong>Warning</strong>: If you use Apache 2.2 or lower, you need <a href="https://httpd.apache.org/docs/current/mod/mod_version.html">mod_version</a> to be installed and enabled.<a href=".html"></a></p> 200<p><strong>Warning</strong>: If you use Apache 2.2 or lower, you need <a href="https://httpd.apache.org/docs/current/mod/mod_version.html">mod_version</a> to be installed and enabled.<a href=".html"></a></p>
201<p>Apache module <code>mod_rewrite</code> <strong>must</strong> be enabled to use the REST API. URL rewriting rules for the Slim microframework are stated in the root <code>.htaccess</code> file.</p>
199<h2 id="lighthttpd">LightHttpd</h2> 202<h2 id="lighthttpd">LightHttpd</h2>
200<h2 id="nginx">Nginx</h2> 203<h2 id="nginx">Nginx</h2>
201<h3 id="foreword">Foreword</h3> 204<h3 id="foreword">Foreword</h3>
@@ -296,11 +299,14 @@ http {
296 error_log /var/log/nginx/error.log; 299 error_log /var/log/nginx/error.log;
297 300
298 location /shaarli/ { 301 location /shaarli/ {
302 try_files $uri /shaarli/index.php$is_args$args;
299 access_log /var/log/nginx/shaarli.access.log; 303 access_log /var/log/nginx/shaarli.access.log;
300 error_log /var/log/nginx/shaarli.error.log; 304 error_log /var/log/nginx/shaarli.error.log;
301 } 305 }
302 306
303 location ~ (index)\.php$ { 307 location ~ (index)\.php$ {
308 try_files $uri =404;
309 fastcgi_split_path_info ^(.+\.php)(/.+)$;
304 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; 310 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
305 fastcgi_index index.php; 311 fastcgi_index index.php;
306 include fastcgi.conf; 312 include fastcgi.conf;
@@ -335,6 +341,10 @@ location ~ ~$ {
335}</code></pre> 341}</code></pre>
336<pre class="nginx"><code># /etc/nginx/php.conf 342<pre class="nginx"><code># /etc/nginx/php.conf
337location ~ (index)\.php$ { 343location ~ (index)\.php$ {
344 # Slim - split URL path into (script_filename, path_info)
345 try_files $uri =404;
346 fastcgi_split_path_info ^(.+\.php)(/.+)$;
347
338 # filter and proxy PHP requests to PHP-FPM 348 # filter and proxy PHP requests to PHP-FPM
339 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; 349 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
340 fastcgi_index index.php; 350 fastcgi_index index.php;
@@ -367,6 +377,9 @@ http {
367 server_name my.first.domain.org; 377 server_name my.first.domain.org;
368 378
369 location /shaarli/ { 379 location /shaarli/ {
380 # Slim - rewrite URLs
381 try_files $uri /shaarli/index.php$is_args$args;
382
370 access_log /var/log/nginx/shaarli.access.log; 383 access_log /var/log/nginx/shaarli.access.log;
371 error_log /var/log/nginx/shaarli.error.log; 384 error_log /var/log/nginx/shaarli.error.log;
372 } 385 }
@@ -425,6 +438,9 @@ http {
425 ssl_certificate_key /home/john/ssl/localhost.key; 438 ssl_certificate_key /home/john/ssl/localhost.key;
426 439
427 location /shaarli/ { 440 location /shaarli/ {
441 # Slim - rewrite URLs
442 try_files $uri /index.php$is_args$args;
443
428 access_log /var/log/nginx/shaarli.access.log; 444 access_log /var/log/nginx/shaarli.access.log;
429 error_log /var/log/nginx/shaarli.error.log; 445 error_log /var/log/nginx/shaarli.error.log;
430 } 446 }
diff --git a/doc/Server-configuration.md b/doc/Server-configuration.md
index df10feb2..81cc1a72 100644
--- a/doc/Server-configuration.md
+++ b/doc/Server-configuration.md
@@ -107,6 +107,8 @@ See [Server-side TLS](https://wiki.mozilla.org/Security/Server_Side_TLS#Apache)
107Shaarli use `.htaccess` Apache files to deny access to files that shouldn't be directly accessed (datastore, config, etc.). You need the directive `AllowOverride All` in your virtual host configuration for them to work. 107Shaarli use `.htaccess` Apache files to deny access to files that shouldn't be directly accessed (datastore, config, etc.). You need the directive `AllowOverride All` in your virtual host configuration for them to work.
108 108
109**Warning**: If you use Apache 2.2 or lower, you need [mod_version](https://httpd.apache.org/docs/current/mod/mod_version.html) to be installed and enabled.[](.html) 109**Warning**: If you use Apache 2.2 or lower, you need [mod_version](https://httpd.apache.org/docs/current/mod/mod_version.html) to be installed and enabled.[](.html)
110
111Apache module `mod_rewrite` **must** be enabled to use the REST API. URL rewriting rules for the Slim microframework are stated in the root `.htaccess` file.
110 112
111## LightHttpd 113## LightHttpd
112 114
@@ -218,11 +220,14 @@ http {
218 error_log /var/log/nginx/error.log; 220 error_log /var/log/nginx/error.log;
219 221
220 location /shaarli/ { 222 location /shaarli/ {
223 try_files $uri /shaarli/index.php$is_args$args;
221 access_log /var/log/nginx/shaarli.access.log; 224 access_log /var/log/nginx/shaarli.access.log;
222 error_log /var/log/nginx/shaarli.error.log; 225 error_log /var/log/nginx/shaarli.error.log;
223 } 226 }
224 227
225 location ~ (index)\.php$ { 228 location ~ (index)\.php$ {
229 try_files $uri =404;
230 fastcgi_split_path_info ^(.+\.php)(/.+)$;
226 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; 231 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
227 fastcgi_index index.php; 232 fastcgi_index index.php;
228 include fastcgi.conf; 233 include fastcgi.conf;
@@ -261,6 +266,10 @@ location ~ ~$ {
261```nginx 266```nginx
262# /etc/nginx/php.conf 267# /etc/nginx/php.conf
263location ~ (index)\.php$ { 268location ~ (index)\.php$ {
269 # Slim - split URL path into (script_filename, path_info)
270 try_files $uri =404;
271 fastcgi_split_path_info ^(.+\.php)(/.+)$;
272
264 # filter and proxy PHP requests to PHP-FPM 273 # filter and proxy PHP requests to PHP-FPM
265 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; 274 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
266 fastcgi_index index.php; 275 fastcgi_index index.php;
@@ -299,6 +308,9 @@ http {
299 server_name my.first.domain.org; 308 server_name my.first.domain.org;
300 309
301 location /shaarli/ { 310 location /shaarli/ {
311 # Slim - rewrite URLs
312 try_files $uri /shaarli/index.php$is_args$args;
313
302 access_log /var/log/nginx/shaarli.access.log; 314 access_log /var/log/nginx/shaarli.access.log;
303 error_log /var/log/nginx/shaarli.error.log; 315 error_log /var/log/nginx/shaarli.error.log;
304 } 316 }
@@ -361,6 +373,9 @@ http {
361 ssl_certificate_key /home/john/ssl/localhost.key; 373 ssl_certificate_key /home/john/ssl/localhost.key;
362 374
363 location /shaarli/ { 375 location /shaarli/ {
376 # Slim - rewrite URLs
377 try_files $uri /index.php$is_args$args;
378
364 access_log /var/log/nginx/shaarli.access.log; 379 access_log /var/log/nginx/shaarli.access.log;
365 error_log /var/log/nginx/shaarli.error.log; 380 error_log /var/log/nginx/shaarli.error.log;
366 } 381 }
diff --git a/doc/Server-requirements.html b/doc/Server-requirements.html
index 2c2545bb..79d74118 100644
--- a/doc/Server-requirements.html
+++ b/doc/Server-requirements.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
@@ -83,26 +85,31 @@
83</thead> 85</thead>
84<tbody> 86<tbody>
85<tr class="odd"> 87<tr class="odd">
88<td style="text-align: center;">7.1</td>
89<td style="text-align: center;">Supported (v0.9.x)</td>
90<td style="text-align: center;">✅</td>
91</tr>
92<tr class="even">
86<td style="text-align: center;">7.0</td> 93<td style="text-align: center;">7.0</td>
87<td style="text-align: center;">Supported</td> 94<td style="text-align: center;">Supported</td>
88<td style="text-align: center;">✅</td> 95<td style="text-align: center;">✅</td>
89</tr> 96</tr>
90<tr class="even"> 97<tr class="odd">
91<td style="text-align: center;">5.6</td> 98<td style="text-align: center;">5.6</td>
92<td style="text-align: center;">Supported</td> 99<td style="text-align: center;">Supported</td>
93<td style="text-align: center;">✅</td> 100<td style="text-align: center;">✅</td>
94</tr> 101</tr>
95<tr class="odd"> 102<tr class="even">
96<td style="text-align: center;">5.5</td> 103<td style="text-align: center;">5.5</td>
97<td style="text-align: center;">EOL: 2016-07-10</td> 104<td style="text-align: center;">EOL: 2016-07-10</td>
98<td style="text-align: center;">✅</td> 105<td style="text-align: center;">✅</td>
99</tr> 106</tr>
100<tr class="even"> 107<tr class="odd">
101<td style="text-align: center;">5.4</td> 108<td style="text-align: center;">5.4</td>
102<td style="text-align: center;">EOL: 2015-09-14</td> 109<td style="text-align: center;">EOL: 2015-09-14</td>
103<td style="text-align: center;">✅ (up to Shaarli 0.8.x)</td> 110<td style="text-align: center;">✅ (up to Shaarli 0.8.x)</td>
104</tr> 111</tr>
105<tr class="odd"> 112<tr class="even">
106<td style="text-align: center;">5.3</td> 113<td style="text-align: center;">5.3</td>
107<td style="text-align: center;">EOL: 2014-08-14</td> 114<td style="text-align: center;">EOL: 2014-08-14</td>
108<td style="text-align: center;">✅ (up to Shaarli 0.8.x)</td> 115<td style="text-align: center;">✅ (up to Shaarli 0.8.x)</td>
@@ -130,6 +137,16 @@ download and install third-party PHP dependencies.</p>
130<td style="text-align: center;">All</td> 137<td style="text-align: center;">All</td>
131<td>Import bookmarks from Netscape files<a href=".html"></a></td> 138<td>Import bookmarks from Netscape files<a href=".html"></a></td>
132</tr> 139</tr>
140<tr class="even">
141<td><a href="https://packagist.org/packages/erusev/parsedown"><code>erusev/parsedown</code></a></td>
142<td style="text-align: center;">All</td>
143<td>Parse MarkDown syntax for the MarkDown plugin<a href=".html"></a></td>
144</tr>
145<tr class="odd">
146<td><a href="https://packagist.org/packages/slim/slim"><code>slim/slim</code></a></td>
147<td style="text-align: center;">All</td>
148<td>Handle routes and middleware for the REST API<a href=".html"></a></td>
149</tr>
133</tbody> 150</tbody>
134</table> 151</table>
135<h3 id="extensions">Extensions</h3> 152<h3 id="extensions">Extensions</h3>
diff --git a/doc/Server-requirements.md b/doc/Server-requirements.md
index 4962193e..07e70ab3 100644
--- a/doc/Server-requirements.md
+++ b/doc/Server-requirements.md
@@ -10,6 +10,7 @@
10### Supported versions 10### Supported versions
11Version | Status | Shaarli compatibility 11Version | Status | Shaarli compatibility
12:---:|:---:|:---: 12:---:|:---:|:---:
137.1 | Supported (v0.9.x) | :white_check_mark:
137.0 | Supported | :white_check_mark: 147.0 | Supported | :white_check_mark:
145.6 | Supported | :white_check_mark: 155.6 | Supported | :white_check_mark:
155.5 | EOL: 2016-07-10 | :white_check_mark: 165.5 | EOL: 2016-07-10 | :white_check_mark:
@@ -26,6 +27,8 @@ download and install third-party PHP dependencies.
26Library | Required? | Usage 27Library | Required? | Usage
27---|:---:|--- 28---|:---:|---
28[`shaarli/netscape-bookmark-parser`](https://packagist.org/packages/shaarli/netscape-bookmark-parser) | All | Import bookmarks from Netscape files[](.html) 29[`shaarli/netscape-bookmark-parser`](https://packagist.org/packages/shaarli/netscape-bookmark-parser) | All | Import bookmarks from Netscape files[](.html)
30[`erusev/parsedown`](https://packagist.org/packages/erusev/parsedown) | All | Parse MarkDown syntax for the MarkDown plugin[](.html)
31[`slim/slim`](https://packagist.org/packages/slim/slim) | All | Handle routes and middleware for the REST API[](.html)
29 32
30### Extensions 33### Extensions
31Extension | Required? | Usage 34Extension | Required? | Usage
diff --git a/doc/Server-security.html b/doc/Server-security.html
index 3551deff..4f7ff468 100644
--- a/doc/Server-security.html
+++ b/doc/Server-security.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Shaarli-configuration.html b/doc/Shaarli-configuration.html
index 6d717c65..c696c97c 100644
--- a/doc/Shaarli-configuration.html
+++ b/doc/Shaarli-configuration.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -169,6 +171,7 @@ It might be useful if your IP adress often changes.<br />
169<h3 id="resources">Resources</h3> 171<h3 id="resources">Resources</h3>
170<p><strong>data_dir</strong>: Data directory.<br /> 172<p><strong>data_dir</strong>: Data directory.<br />
171<strong>datastore</strong>: Shaarli's links database file path.<br /> 173<strong>datastore</strong>: Shaarli's links database file path.<br />
174<strong>history</strong>: Shaarli's operation history file path.<br />
172<strong>updates</strong>: File path for the ran updates file.<br /> 175<strong>updates</strong>: File path for the ran updates file.<br />
173<strong>log</strong>: Log file path.<br /> 176<strong>log</strong>: Log file path.<br />
174<strong>update_check</strong>: Last update check file path.<br /> 177<strong>update_check</strong>: Last update check file path.<br />
diff --git a/doc/Shaarli-configuration.md b/doc/Shaarli-configuration.md
index 4a783c0e..25647cb7 100644
--- a/doc/Shaarli-configuration.md
+++ b/doc/Shaarli-configuration.md
@@ -70,6 +70,7 @@ It might be useful if your IP adress often changes.
70 70
71**data_dir**: Data directory. 71**data_dir**: Data directory.
72**datastore**: Shaarli's links database file path. 72**datastore**: Shaarli's links database file path.
73**history**: Shaarli's operation history file path.
73**updates**: File path for the ran updates file. 74**updates**: File path for the ran updates file.
74**log**: Log file path. 75**log**: Log file path.
75**update_check**: Last update check file path. 76**update_check**: Last update check file path.
diff --git a/doc/Sharing-button.html b/doc/Sharing-button.html
index 93710efe..f3682f8c 100644
--- a/doc/Sharing-button.html
+++ b/doc/Sharing-button.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Static-analysis.html b/doc/Static-analysis.html
index d964e917..a95d195e 100644
--- a/doc/Static-analysis.html
+++ b/doc/Static-analysis.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Theming.html b/doc/Theming.html
index 7cbf7aef..6b5dac35 100644
--- a/doc/Theming.html
+++ b/doc/Theming.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -100,42 +102,56 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
100</ul> 102</ul>
101</div> 103</div>
102<h1 id="theming">Theming</h1> 104<h1 id="theming">Theming</h1>
103<h2 id="user-css">User CSS</h2> 105<h2 id="foreword">Foreword</h2>
106<p>There are two ways of customizing how Shaarli looks:</p>
107<ol>
108<li>by using a custom CSS to override Shaarli's CSS</li>
109<li>by using a full theme that provides its own RainTPL templates, CSS and Javascript resources</li>
110</ol>
111<h2 id="custom-css">Custom CSS</h2>
112<p>Shaarli's appearance can be modified by adding CSS rules to:</p>
104<ul> 113<ul>
105<li>Shaarli's apparence can be modified by editing CSS rules in <code>inc/user.css</code>. This file allows to override rules defined in the main <code>inc/shaarli.css</code> (only add changed rules), or define a whole new theme.</li> 114<li>Shaarli &lt; <code>v0.9.0</code>: <code>inc/user.css</code></li>
106<li>Do not edit <code>inc/shaarli.css</code>! Your changes would be overriden when updating Shaarli.</li> 115<li>Shaarli &gt;= <code>v0.9.0</code>: <code>data/user.css</code></li>
107<li>Some themes are available at <a href="https://github.com/shaarli/shaarli-themes" class="uri">https://github.com/shaarli/shaarli-themes</a>.</li>
108</ul> 116</ul>
109<p>See also:</p> 117<p>This file allows overriding rules defined in the template CSS files (only add changed rules), or define a whole new theme.</p>
110<ul> 118<p><strong>Note</strong>: Do not edit <code>tpl/default/css/shaarli.css</code>! Your changes would be overridden when updating Shaarli.</p>
111<li><a href="Download-CSS-styles-from-an-OPML-list.html">Download CSS styles from an OPML list</a></li> 119<p>See also <a href="Download-CSS-styles-from-an-OPML-list.html">Download CSS styles from an OPML list</a></p>
112</ul> 120<h2 id="themes">Themes</h2>
113<h2 id="raintpl-template">RainTPL template</h2>
114<p><em>WARNING - This feature is currently being worked on and will be improved in the next releases. Experimental.</em></p> 121<p><em>WARNING - This feature is currently being worked on and will be improved in the next releases. Experimental.</em></p>
122<p>Installation:</p>
115<ul> 123<ul>
116<li>Find the template you'd like to install (see the list of <a href="available-templates%7CTheming#community-themes--templates.html">available templates|Theming#community-themes--templates</a>)</li> 124<li>find a theme you'd like to install</li>
117<li>Find it's git clone URL or download the zip archive for the template.</li> 125<li>copy or clone the theme folder under <code>tpl/&lt;a_sweet_theme&gt;</code></li>
118<li>In your Shaarli <code>tpl/</code> directory, run <code>git clone https://url/of/my-template/</code> or unpack the zip archive. 126<li>enable the theme:
119<ul> 127<ul>
120<li>There should now be a <code>my-template/</code> directory under the <code>tpl/</code> dir, containing directly all the template files.</li> 128<li>Shaarli &lt; <code>v0.9.0</code>: edit <code>data/config.json.php</code> and set the value of <code>raintpl_tpl</code> to the new theme name:<br />
129<code>&quot;raintpl_tpl&quot;: &quot;tpl\/my-template\/&quot;</code></li>
130<li>Shaarli &gt;= <code>v0.9.0</code>: select the theme through the <em>Tools</em> page</li>
121</ul></li> 131</ul></li>
122<li><p>Edit <code>data/config.json.php</code> to have Shaarli use this template, in <code>&quot;resource&quot;</code> e.g.</p>
123<div class="sourceCode"><pre class="sourceCode json"><code class="sourceCode json"><span class="er">&quot;raintpl_tpl&quot;:</span> <span class="er">&quot;tpl\/my-template\/&quot;,</span></code></pre></div></li>
124</ul> 132</ul>
125<h2 id="community-themes-templates">Community themes &amp; templates</h2> 133<h2 id="community-css-themes">Community CSS &amp; themes</h2>
134<h3 id="custom-css-1">Custom CSS</h3>
135<ul>
136<li><a href="https://github.com/mrjovanovic/serious-theme-shaarli">mrjovanovic/serious-theme-shaarli</a> - A serious theme for Shaarli<a href=".html"></a></li>
137<li><a href="https://github.com/shaarli/shaarli-themes">shaarli/shaarli-themes</a><a href=".html"></a></li>
138</ul>
139<h3 id="themes-1">Themes</h3>
126<ul> 140<ul>
127<li><a href="https://github.com/AkibaTech/Shaarli---SuperHero-Theme">AkibaTech/Shaarli Superhero Theme</a> - A template/theme for Shaarli<a href=".html"></a></li> 141<li><a href="https://github.com/AkibaTech/Shaarli---SuperHero-Theme">AkibaTech/Shaarli Superhero Theme</a> - A template/theme for Shaarli<a href=".html"></a></li>
128<li><a href="https://github.com/alexisju/albinomouse-template">alexisju/albinomouse-template</a> - A full template for Shaarli<a href=".html"></a></li> 142<li><a href="https://github.com/alexisju/albinomouse-template">alexisju/albinomouse-template</a> - A full template for Shaarli<a href=".html"></a></li>
129<li><a href="https://github.com/ArthurHoaro/shaarli-launch">ArthurHoaro/shaarli-launch</a> - Customizable Shaarli theme.<a href=".html"></a></li> 143<li><a href="https://github.com/ArthurHoaro/shaarli-launch">ArthurHoaro/shaarli-launch</a> - Customizable Shaarli theme<a href=".html"></a></li>
130<li><a href="https://github.com/dhoko/ShaarliTemplate">dhoko/ShaarliTemplate</a> - A template/theme for Shaarli<a href=".html"></a></li> 144<li><a href="https://github.com/dhoko/ShaarliTemplate">dhoko/ShaarliTemplate</a> - A template/theme for Shaarli<a href=".html"></a></li>
131<li><a href="https://github.com/kalvn/shaarli-blocks">kalvn/shaarli-blocks</a> - A template/theme for Shaarli<a href=".html"></a></li> 145<li><a href="https://github.com/kalvn/shaarli-blocks">kalvn/shaarli-blocks</a> - A template/theme for Shaarli<a href=".html"></a></li>
132<li><a href="https://github.com/kalvn/Shaarli-Material">kalvn/Shaarli-Material</a> - A theme (template) based on Google's Material Design for Shaarli, the superfast delicious clone.<a href=".html"></a></li> 146<li><a href="https://github.com/kalvn/Shaarli-Material">kalvn/Shaarli-Material</a> - A theme (template) based on Google's Material Design for Shaarli, the superfast delicious clone<a href=".html"></a></li>
133<li><a href="https://github.com/ManufacturaInd/shaarli-2004licious-theme">ManufacturaInd/shaarli-2004licious-theme</a> - A template/theme as a humble homage to the early looks of the del.icio.us site.<a href=".html"></a></li> 147<li><a href="https://github.com/ManufacturaInd/shaarli-2004licious-theme">ManufacturaInd/shaarli-2004licious-theme</a> - A template/theme as a humble homage to the early looks of the del.icio.us site<a href=".html"></a></li>
148</ul>
149<h3 id="shaarli-forks">Shaarli forks</h3>
150<ul>
134<li><a href="https://github.com/misterair/limonade">misterair/Limonade</a> - A fork of (legacy) Shaarli with a new template<a href=".html"></a></li> 151<li><a href="https://github.com/misterair/limonade">misterair/Limonade</a> - A fork of (legacy) Shaarli with a new template<a href=".html"></a></li>
135<li><a href="https://github.com/mrjovanovic/serious-theme-shaarli">mrjovanovic/serious-theme-shaarli</a> - A serious theme for SHaarli.<a href=".html"></a></li>
136<li><a href="https://github.com/vivienhaese/shaarlitheme">vivienhaese/shaarlitheme</a> - A Shaarli fork meant to be run in an openshift instance<a href=".html"></a></li> 152<li><a href="https://github.com/vivienhaese/shaarlitheme">vivienhaese/shaarlitheme</a> - A Shaarli fork meant to be run in an openshift instance<a href=".html"></a></li>
137</ul> 153</ul>
138<h3 id="example-installation-albinomouse-template">Example installation: AlbinoMouse template</h3> 154<h2 id="example-installation-albinomouse-theme">Example installation: AlbinoMouse theme</h2>
139<p>With the following configuration:</p> 155<p>With the following configuration:</p>
140<ul> 156<ul>
141<li>Apache 2 / PHP 5.6</li> 157<li>Apache 2 / PHP 5.6</li>
diff --git a/doc/Theming.md b/doc/Theming.md
index a21899c2..23877e5d 100644
--- a/doc/Theming.md
+++ b/doc/Theming.md
@@ -1,39 +1,51 @@
1#Theming 1#Theming
2## User CSS 2## Foreword
3There are two ways of customizing how Shaarli looks:
3 4
4- Shaarli's apparence can be modified by editing CSS rules in `inc/user.css`. This file allows to override rules defined in the main `inc/shaarli.css` (only add changed rules), or define a whole new theme. 51. by using a custom CSS to override Shaarli's CSS
5- Do not edit `inc/shaarli.css`! Your changes would be overriden when updating Shaarli. 62. by using a full theme that provides its own RainTPL templates, CSS and Javascript resources
6- Some themes are available at https://github.com/shaarli/shaarli-themes.
7 7
8See also: 8## Custom CSS
9- [Download CSS styles from an OPML list](Download-CSS-styles-from-an-OPML-list.html) 9Shaarli's appearance can be modified by adding CSS rules to:
10- Shaarli < `v0.9.0`: `inc/user.css`
11- Shaarli >= `v0.9.0`: `data/user.css`
10 12
11## RainTPL template 13This file allows overriding rules defined in the template CSS files (only add changed rules), or define a whole new theme.
12 14
15**Note**: Do not edit `tpl/default/css/shaarli.css`! Your changes would be overridden when updating Shaarli.
16
17See also [Download CSS styles from an OPML list](Download-CSS-styles-from-an-OPML-list.html)
18
19## Themes
13_WARNING - This feature is currently being worked on and will be improved in the next releases. Experimental._ 20_WARNING - This feature is currently being worked on and will be improved in the next releases. Experimental._
14 21
15- Find the template you'd like to install (see the list of [available templates|Theming#community-themes--templates](available-templates|Theming#community-themes--templates.html)) 22Installation:
16- Find it's git clone URL or download the zip archive for the template. 23- find a theme you'd like to install
17- In your Shaarli `tpl/` directory, run `git clone https://url/of/my-template/` or unpack the zip archive. 24- copy or clone the theme folder under `tpl/<a_sweet_theme>`
18 - There should now be a `my-template/` directory under the `tpl/` dir, containing directly all the template files. 25- enable the theme:
19- Edit `data/config.json.php` to have Shaarli use this template, in `"resource"` e.g. 26 - Shaarli < `v0.9.0`: edit `data/config.json.php` and set the value of `raintpl_tpl` to the new theme name:
20```json 27 `"raintpl_tpl": "tpl\/my-template\/"`
21"raintpl_tpl": "tpl\/my-template\/", 28 - Shaarli >= `v0.9.0`: select the theme through the _Tools_ page
22```
23 29
24## Community themes & templates 30## Community CSS & themes
31### Custom CSS
32- [mrjovanovic/serious-theme-shaarli](https://github.com/mrjovanovic/serious-theme-shaarli) - A serious theme for Shaarli[](.html)
33- [shaarli/shaarli-themes](https://github.com/shaarli/shaarli-themes)[](.html)
34
35### Themes
25- [AkibaTech/Shaarli Superhero Theme](https://github.com/AkibaTech/Shaarli---SuperHero-Theme) - A template/theme for Shaarli[](.html) 36- [AkibaTech/Shaarli Superhero Theme](https://github.com/AkibaTech/Shaarli---SuperHero-Theme) - A template/theme for Shaarli[](.html)
26- [alexisju/albinomouse-template](https://github.com/alexisju/albinomouse-template) - A full template for Shaarli[](.html) 37- [alexisju/albinomouse-template](https://github.com/alexisju/albinomouse-template) - A full template for Shaarli[](.html)
27- [ArthurHoaro/shaarli-launch](https://github.com/ArthurHoaro/shaarli-launch) - Customizable Shaarli theme.[](.html) 38- [ArthurHoaro/shaarli-launch](https://github.com/ArthurHoaro/shaarli-launch) - Customizable Shaarli theme[](.html)
28- [dhoko/ShaarliTemplate](https://github.com/dhoko/ShaarliTemplate) - A template/theme for Shaarli[](.html) 39- [dhoko/ShaarliTemplate](https://github.com/dhoko/ShaarliTemplate) - A template/theme for Shaarli[](.html)
29- [kalvn/shaarli-blocks](https://github.com/kalvn/shaarli-blocks) - A template/theme for Shaarli[](.html) 40- [kalvn/shaarli-blocks](https://github.com/kalvn/shaarli-blocks) - A template/theme for Shaarli[](.html)
30- [kalvn/Shaarli-Material](https://github.com/kalvn/Shaarli-Material) - A theme (template) based on Google's Material Design for Shaarli, the superfast delicious clone.[](.html) 41- [kalvn/Shaarli-Material](https://github.com/kalvn/Shaarli-Material) - A theme (template) based on Google's Material Design for Shaarli, the superfast delicious clone[](.html)
31- [ManufacturaInd/shaarli-2004licious-theme](https://github.com/ManufacturaInd/shaarli-2004licious-theme) - A template/theme as a humble homage to the early looks of the del.icio.us site.[](.html) 42- [ManufacturaInd/shaarli-2004licious-theme](https://github.com/ManufacturaInd/shaarli-2004licious-theme) - A template/theme as a humble homage to the early looks of the del.icio.us site[](.html)
43
44### Shaarli forks
32- [misterair/Limonade](https://github.com/misterair/limonade) - A fork of (legacy) Shaarli with a new template[](.html) 45- [misterair/Limonade](https://github.com/misterair/limonade) - A fork of (legacy) Shaarli with a new template[](.html)
33- [mrjovanovic/serious-theme-shaarli](https://github.com/mrjovanovic/serious-theme-shaarli) - A serious theme for SHaarli.[](.html)
34- [vivienhaese/shaarlitheme](https://github.com/vivienhaese/shaarlitheme) - A Shaarli fork meant to be run in an openshift instance[](.html) 46- [vivienhaese/shaarlitheme](https://github.com/vivienhaese/shaarlitheme) - A Shaarli fork meant to be run in an openshift instance[](.html)
35 47
36### Example installation: AlbinoMouse template 48## Example installation: AlbinoMouse theme
37With the following configuration: 49With the following configuration:
38- Apache 2 / PHP 5.6 50- Apache 2 / PHP 5.6
39- user sites are enabled, e.g. `/home/user/public_html/somedir` is served as `http://localhost/~user/somedir` 51- user sites are enabled, e.g. `/home/user/public_html/somedir` is served as `http://localhost/~user/somedir`
diff --git a/doc/Troubleshooting.html b/doc/Troubleshooting.html
index ed1c6f09..f43e6ed3 100644
--- a/doc/Troubleshooting.html
+++ b/doc/Troubleshooting.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -172,7 +174,7 @@ Search for <code>failed</code> in this file to look for unauthorized login attem
172<li>If you have the error <code>Warning: file_get_contents() [function.file-get-contents]: URL file-access is disabled in the server configuration in /…/index.php on line xxx</code>, it means that your host has disabled the ability to fetch a file by HTTP in the php config (Typically in 1and1 hosting). Bad host. Change host. Or comment the following lines:<a href=".html"></a></li> 174<li>If you have the error <code>Warning: file_get_contents() [function.file-get-contents]: URL file-access is disabled in the server configuration in /…/index.php on line xxx</code>, it means that your host has disabled the ability to fetch a file by HTTP in the php config (Typically in 1and1 hosting). Bad host. Change host. Or comment the following lines:<a href=".html"></a></li>
173</ul> 175</ul>
174<div class="sourceCode"><pre class="sourceCode php"><code class="sourceCode php"><span class="co">//list($status,$headers,$data) = getHTTP($url,4); // Short timeout to keep the application responsive.</span> 176<div class="sourceCode"><pre class="sourceCode php"><code class="sourceCode php"><span class="co">//list($status,$headers,$data) = getHTTP($url,4); // Short timeout to keep the application responsive.</span>
175<span class="co">// FIXME: Decode charset according to charset specified in either 1) HTTP response headers or 2) &lt;head&gt; in html </span> 177<span class="co">// </span><span class="al">FIXME</span><span class="co">: Decode charset according to charset specified in either 1) HTTP response headers or 2) &lt;head&gt; in html </span>
176<span class="co">//if (strpos($status,&#39;200 OK&#39;)) $title=html_extract_title($data);</span></code></pre></div> 178<span class="co">//if (strpos($status,&#39;200 OK&#39;)) $title=html_extract_title($data);</span></code></pre></div>
177<ul> 179<ul>
178<li>On hosts which forbid outgoing HTTP requests (such as free.fr), some thumbnails will not work.</li> 180<li>On hosts which forbid outgoing HTTP requests (such as free.fr), some thumbnails will not work.</li>
diff --git a/doc/Unit-tests.html b/doc/Unit-tests.html
index 266fd33a..09611463 100644
--- a/doc/Unit-tests.html
+++ b/doc/Unit-tests.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -208,5 +210,17 @@ DBTest.php on line 79 and defined
208<li>a detailed HTML report with metrics for tested code</li> 210<li>a detailed HTML report with metrics for tested code</li>
209<li>to open it in a web browser: <code>firefox coverage/index.html &amp;</code></li> 211<li>to open it in a web browser: <code>firefox coverage/index.html &amp;</code></li>
210</ul> 212</ul>
213<h3 id="executing-specific-tests">Executing specific tests</h3>
214<p>Add a <a href="https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.group"><code>@group</code></a> annotation in a test class or method comment:<a href=".html"></a></p>
215<div class="sourceCode"><pre class="sourceCode php"><code class="sourceCode php"><span class="co">/**</span>
216<span class="co"> * Netscape bookmark import</span>
217<span class="co"> * </span><span class="an">@group</span><span class="co"> WIP</span>
218<span class="co"> */</span>
219<span class="kw">class</span> BookmarkImportTest <span class="kw">extends</span> PHPUnit_Framework_TestCase
220{
221 <span class="ot">[</span><span class="st">...</span><span class="ot">][](</span>.html<span class="ot">)</span>
222}</code></pre></div>
223<p>To run all tests annotated with <code>@group WIP</code>:</p>
224<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="ex">vendor/bin/phpunit</span> --group WIP tests/</code></pre></div>
211</body> 225</body>
212</html> 226</html>
diff --git a/doc/Unit-tests.md b/doc/Unit-tests.md
index f2888780..0942ad38 100644
--- a/doc/Unit-tests.md
+++ b/doc/Unit-tests.md
@@ -126,3 +126,22 @@ If Xdebug has been installed and activated, two coverage reports will be generat
126* a summary in the console 126* a summary in the console
127* a detailed HTML report with metrics for tested code 127* a detailed HTML report with metrics for tested code
128 * to open it in a web browser: `firefox coverage/index.html &` 128 * to open it in a web browser: `firefox coverage/index.html &`
129
130### Executing specific tests
131Add a [`@group`](https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.group) annotation in a test class or method comment:[](.html)
132
133```php
134/**
135 * Netscape bookmark import
136 * @group WIP
137 */
138class BookmarkImportTest extends PHPUnit_Framework_TestCase
139{
140 [...][](.html)
141}
142```
143
144To run all tests annotated with `@group WIP`:
145```bash
146$ vendor/bin/phpunit --group WIP tests/
147```
diff --git a/doc/Upgrade-and-migration.html b/doc/Upgrade-and-migration.html
index a5b041d5..667215ab 100644
--- a/doc/Upgrade-and-migration.html
+++ b/doc/Upgrade-and-migration.html
@@ -69,6 +69,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li> 70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li> 71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
72</ul></li> 73</ul></li>
73<li>How To 74<li>How To
74<ul> 75<ul>
@@ -87,6 +88,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
87<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
88<li><a href="Plugin-System.html">Plugin System</a></li> 89<li><a href="Plugin-System.html">Plugin System</a></li>
89<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
90<li><a href="Security.html">Security</a></li> 92<li><a href="Security.html">Security</a></li>
91<li><a href="Static-analysis.html">Static analysis</a></li> 93<li><a href="Static-analysis.html">Static analysis</a></li>
92<li><a href="Theming.html">Theming</a></li> 94<li><a href="Theming.html">Theming</a></li>
@@ -101,12 +103,16 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
101</div> 103</div>
102<h1 id="upgrade-and-migration">Upgrade and migration</h1> 104<h1 id="upgrade-and-migration">Upgrade and migration</h1>
103<h2 id="preparation">Preparation</h2> 105<h2 id="preparation">Preparation</h2>
106<h3 id="note-your-current-version">Note your current version</h3>
107<p>If anything goes wrong, it's important for us to know which version you're upgrading from.<br />
108The current version is present in the <code>version.php</code> file.</p>
104<h3 id="backup-your-data">Backup your data</h3> 109<h3 id="backup-your-data">Backup your data</h3>
105<p>Shaarli stores all user data under the <code>data</code> directory:</p> 110<p>Shaarli stores all user data under the <code>data</code> directory:</p>
106<ul> 111<ul>
107<li><code>data/config.php</code> - main configuration file</li> 112<li><code>data/config.php</code> - main configuration file</li>
108<li><code>data/datastore.php</code> - bookmarked links</li> 113<li><code>data/datastore.php</code> - bookmarked links</li>
109<li><code>data/ipbans.php</code> - banned IP addresses</li> 114<li><code>data/ipbans.php</code> - banned IP addresses</li>
115<li><code>data/updates.txt</code> - contains all automatic update to the configuration and datastore files already run</li>
110</ul> 116</ul>
111<p>See <a href="Shaarli-configuration.html">Shaarli configuration</a> for more information about Shaarli resources.</p> 117<p>See <a href="Shaarli-configuration.html">Shaarli configuration</a> for more information about Shaarli resources.</p>
112<p>It is recommended to backup this repository <em>before</em> starting updating/upgrading Shaarli:</p> 118<p>It is recommended to backup this repository <em>before</em> starting updating/upgrading Shaarli:</p>
@@ -125,15 +131,11 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
125</ul></li> 131</ul></li>
126<li>check or restore the <code>data</code> directory</li> 132<li>check or restore the <code>data</code> directory</li>
127</ul> 133</ul>
128<h2 id="upgrading-from-release-archives">Upgrading from release archives</h2> 134<h2 id="recommended-upgrading-from-release-archives">Recommended : Upgrading from release archives</h2>
129<p>All tagged revisions can be downloaded as tarballs or ZIP archives from the <a href="https://github.com/shaarli/Shaarli/releases">releases</a> page.<a href=".html"></a></p> 135<p>All tagged revisions can be downloaded as tarballs or ZIP archives from the <a href="https://github.com/shaarli/Shaarli/releases">releases</a> page.<a href=".html"></a></p>
130<p>We <em>recommend</em> using the releases from the <code>stable</code> branch, which are available as:</p> 136<p>We recommend that you use the latest release tarball with the <code>-full</code> suffix. It contains the dependencies, please read <a href="Download-and-installation.html">Download and installation</a> for <code>git</code> complete instructions.</p>
131<ul> 137<p>Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the <code>data</code> directory!</p>
132<li>gzipped tarball - <a href="https://github.com/shaarli/Shaarli/archive/stable.tar.gz" class="uri">https://github.com/shaarli/Shaarli/archive/stable.tar.gz</a></li> 138<p>After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to <code>data/config.json.php</code> (see <a href="Shaarli-configuration.html">Shaarli configuration</a> for more details).</p>
133<li>ZIP archive - <a href="https://github.com/shaarli/Shaarli/archive/stable.zip" class="uri">https://github.com/shaarli/Shaarli/archive/stable.zip</a></li>
134</ul>
135<p>Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the contents of the <code>data</code> directory!</p>
136<p>After upgrading, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to <code>data/config.php</code> (see <a href="Shaarli-configuration.html">Shaarli configuration</a> for more details).</p>
137<h2 id="upgrading-with-git">Upgrading with Git</h2> 139<h2 id="upgrading-with-git">Upgrading with Git</h2>
138<h3 id="updating-a-community-shaarli">Updating a community Shaarli</h3> 140<h3 id="updating-a-community-shaarli">Updating a community Shaarli</h3>
139<p>If you have installed Shaarli from the <a href="Download#clone-with-git-recommended">community Git repository</a>, simply <a href="https://www.git-scm.com/docs/git-pull">pull new changes</a> from your local clone:<a href=".html"></a></p> 141<p>If you have installed Shaarli from the <a href="Download#clone-with-git-recommended">community Git repository</a>, simply <a href="https://www.git-scm.com/docs/git-pull">pull new changes</a> from your local clone:<a href=".html"></a></p>
@@ -149,7 +151,7 @@ $ <span class="fu">git</span> pull
149 <span class="ex">tests/Url/UrlTest.php</span> <span class="kw">|</span> <span class="ex">1</span> + 151 <span class="ex">tests/Url/UrlTest.php</span> <span class="kw">|</span> <span class="ex">1</span> +
150 <span class="ex">3</span> files changed, 3 insertions(+), <span class="ex">1</span> deletion(-)</code></pre></div> 152 <span class="ex">3</span> files changed, 3 insertions(+), <span class="ex">1</span> deletion(-)</code></pre></div>
151<p>Shaarli &gt;= <code>v0.8.x</code>: install/update third-party PHP dependencies using <a href="https://getcomposer.org/">Composer</a>:<a href=".html"></a></p> 153<p>Shaarli &gt;= <code>v0.8.x</code>: install/update third-party PHP dependencies using <a href="https://getcomposer.org/">Composer</a>:<a href=".html"></a></p>
152<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="ex">composer</span> update --no-dev 154<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="ex">composer</span> install --no-dev
153 155
154<span class="ex">Loading</span> composer repositories with package information 156<span class="ex">Loading</span> composer repositories with package information
155<span class="ex">Updating</span> dependencies 157<span class="ex">Updating</span> dependencies
@@ -214,7 +216,7 @@ $ <span class="fu">git</span> branch -vv
214 <span class="ex">master</span> 029f75f [sebsauvage/master] Update README.md[](.html) 216 <span class="ex">master</span> 029f75f [sebsauvage/master] Update README.md[](.html)
215<span class="ex">*</span> stable 890afc3 [origin/stable] Merge pull request <span class="co">#509 from ArthurHoaro/v0.6.5[](.html)</span></code></pre></div> 217<span class="ex">*</span> stable 890afc3 [origin/stable] Merge pull request <span class="co">#509 from ArthurHoaro/v0.6.5[](.html)</span></code></pre></div>
216<p>Shaarli &gt;= <code>v0.8.x</code>: install/update third-party PHP dependencies using <a href="https://getcomposer.org/">Composer</a>:<a href=".html"></a></p> 218<p>Shaarli &gt;= <code>v0.8.x</code>: install/update third-party PHP dependencies using <a href="https://getcomposer.org/">Composer</a>:<a href=".html"></a></p>
217<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="ex">composer</span> update --no-dev 219<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="ex">composer</span> install --no-dev
218 220
219<span class="ex">Loading</span> composer repositories with package information 221<span class="ex">Loading</span> composer repositories with package information
220<span class="ex">Updating</span> dependencies 222<span class="ex">Updating</span> dependencies
@@ -238,5 +240,20 @@ $ <span class="fu">git</span> gc
238<span class="ex">Total</span> 3317 (delta 2050), <span class="ex">reused</span> 3301 (delta 2034)<span class="ex">to</span></code></pre></div> 240<span class="ex">Total</span> 3317 (delta 2050), <span class="ex">reused</span> 3301 (delta 2034)<span class="ex">to</span></code></pre></div>
239<h4 id="step-3-configuration">Step 3: configuration</h4> 241<h4 id="step-3-configuration">Step 3: configuration</h4>
240<p>After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to <code>data/config.php</code> (see <a href="Shaarli-configuration.html">Shaarli configuration</a> for more details).</p> 242<p>After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to <code>data/config.php</code> (see <a href="Shaarli-configuration.html">Shaarli configuration</a> for more details).</p>
243<h2 id="troubleshooting">Troubleshooting</h2>
244<p>If the solutions provided here doesn't work, please open an issue specifying which version you're upgrading from and to.</p>
245<h3 id="you-must-specify-an-integer-as-a-key">You must specify an integer as a key</h3>
246<p>In <code>v0.8.1</code> we changed how link keys are handled (from timestamps to incremental integers).<br />
247Take a look at <code>data/updates.txt</code> content.</p>
248<h4 id="updates.txt-contains-updatemethoddatastoreids"><code>updates.txt</code> contains <code>updateMethodDatastoreIds</code></h4>
249<p>Try to delete it and refresh your page while being logged in.</p>
250<h4 id="updates.txt-doesnt-exists-or-doesnt-contain-updatemethoddatastoreids"><code>updates.txt</code> doesn't exists or doesn't contain <code>updateMethodDatastoreIds</code></h4>
251<ol>
252<li>Create <code>data/updates.txt</code> if it doesn't exist.</li>
253<li>Paste this string in the update file <code>;updateMethodRenameDashTags;</code></li>
254<li>Login to Shaarli.</li>
255<li>Delete the update file.</li>
256<li>Refresh.</li>
257</ol>
241</body> 258</body>
242</html> 259</html>
diff --git a/doc/Upgrade-and-migration.md b/doc/Upgrade-and-migration.md
index 0bc33824..d36eb862 100644
--- a/doc/Upgrade-and-migration.md
+++ b/doc/Upgrade-and-migration.md
@@ -1,11 +1,17 @@
1#Upgrade and migration 1#Upgrade and migration
2## Preparation 2## Preparation
3### Note your current version
4
5If anything goes wrong, it's important for us to know which version you're upgrading from.
6The current version is present in the `version.php` file.
7
3### Backup your data 8### Backup your data
4 9
5Shaarli stores all user data under the `data` directory: 10Shaarli stores all user data under the `data` directory:
6- `data/config.php` - main configuration file 11- `data/config.php` - main configuration file
7- `data/datastore.php` - bookmarked links 12- `data/datastore.php` - bookmarked links
8- `data/ipbans.php` - banned IP addresses 13- `data/ipbans.php` - banned IP addresses
14- `data/updates.txt` - contains all automatic update to the configuration and datastore files already run
9 15
10See [Shaarli configuration](Shaarli-configuration.html) for more information about Shaarli resources. 16See [Shaarli configuration](Shaarli-configuration.html) for more information about Shaarli resources.
11 17
@@ -22,16 +28,14 @@ As all user data is kept under `data`, this is the only directory you need to wo
22 - update - see the following sections 28 - update - see the following sections
23- check or restore the `data` directory 29- check or restore the `data` directory
24 30
25## Upgrading from release archives 31## Recommended : Upgrading from release archives
26All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page.[](.html) 32All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page.[](.html)
27 33
28We _recommend_ using the releases from the `stable` branch, which are available as: 34We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation.html) for `git` complete instructions.
29- gzipped tarball - https://github.com/shaarli/Shaarli/archive/stable.tar.gz
30- ZIP archive - https://github.com/shaarli/Shaarli/archive/stable.zip
31 35
32Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the contents of the `data` directory! 36Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
33 37
34After upgrading, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration.html) for more details). 38After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli-configuration.html) for more details).
35 39
36## Upgrading with Git 40## Upgrading with Git
37### Updating a community Shaarli 41### Updating a community Shaarli
@@ -54,7 +58,7 @@ Fast-forward
54Shaarli >= `v0.8.x`: install/update third-party PHP dependencies using [Composer](https://getcomposer.org/):[](.html) 58Shaarli >= `v0.8.x`: install/update third-party PHP dependencies using [Composer](https://getcomposer.org/):[](.html)
55 59
56```bash 60```bash
57$ composer update --no-dev 61$ composer install --no-dev
58 62
59Loading composer repositories with package information 63Loading composer repositories with package information
60Updating dependencies 64Updating dependencies
@@ -129,7 +133,7 @@ $ git branch -vv
129Shaarli >= `v0.8.x`: install/update third-party PHP dependencies using [Composer](https://getcomposer.org/):[](.html) 133Shaarli >= `v0.8.x`: install/update third-party PHP dependencies using [Composer](https://getcomposer.org/):[](.html)
130 134
131```bash 135```bash
132$ composer update --no-dev 136$ composer install --no-dev
133 137
134Loading composer repositories with package information 138Loading composer repositories with package information
135Updating dependencies 139Updating dependencies
@@ -159,3 +163,24 @@ Total 3317 (delta 2050), reused 3301 (delta 2034)to
159 163
160#### Step 3: configuration 164#### Step 3: configuration
161After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration.html) for more details). 165After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration.html) for more details).
166
167## Troubleshooting
168
169If the solutions provided here doesn't work, please open an issue specifying which version you're upgrading from and to.
170
171### You must specify an integer as a key
172
173In `v0.8.1` we changed how link keys are handled (from timestamps to incremental integers).
174Take a look at `data/updates.txt` content.
175
176#### `updates.txt` contains `updateMethodDatastoreIds`
177
178Try to delete it and refresh your page while being logged in.
179
180#### `updates.txt` doesn't exists or doesn't contain `updateMethodDatastoreIds`
181
182 1. Create `data/updates.txt` if it doesn't exist.
183 2. Paste this string in the update file `;updateMethodRenameDashTags;`
184 3. Login to Shaarli.
185 4. Delete the update file.
186 5. Refresh.
diff --git a/doc/Usage.html b/doc/Usage.html
index 63f21d93..b5855881 100644
--- a/doc/Usage.html
+++ b/doc/Usage.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/Versioning-and-Branches.html b/doc/Versioning-and-Branches.html
new file mode 100644
index 00000000..4dfe4a91
--- /dev/null
+++ b/doc/Versioning-and-Branches.html
@@ -0,0 +1,156 @@
1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <meta name="generator" content="pandoc">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
7 <title>Shaarli – Versioning and Branches</title>
8 <style type="text/css">code{white-space: pre;}</style>
9 <style type="text/css">
10div.sourceCode { overflow-x: auto; }
11table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
12 margin: 0; padding: 0; vertical-align: baseline; border: none; }
13table.sourceCode { width: 100%; line-height: 100%; }
14td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
15td.sourceCode { padding-left: 5px; }
16code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
17code > span.dt { color: #902000; } /* DataType */
18code > span.dv { color: #40a070; } /* DecVal */
19code > span.bn { color: #40a070; } /* BaseN */
20code > span.fl { color: #40a070; } /* Float */
21code > span.ch { color: #4070a0; } /* Char */
22code > span.st { color: #4070a0; } /* String */
23code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
24code > span.ot { color: #007020; } /* Other */
25code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
26code > span.fu { color: #06287e; } /* Function */
27code > span.er { color: #ff0000; font-weight: bold; } /* Error */
28code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
29code > span.cn { color: #880000; } /* Constant */
30code > span.sc { color: #4070a0; } /* SpecialChar */
31code > span.vs { color: #4070a0; } /* VerbatimString */
32code > span.ss { color: #bb6688; } /* SpecialString */
33code > span.im { } /* Import */
34code > span.va { color: #19177c; } /* Variable */
35code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
36code > span.op { color: #666666; } /* Operator */
37code > span.bu { } /* BuiltIn */
38code > span.ex { } /* Extension */
39code > span.pp { color: #bc7a00; } /* Preprocessor */
40code > span.at { color: #7d9029; } /* Attribute */
41code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
42code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
43code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
44code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
45 </style>
46 <link rel="stylesheet" href="github-markdown.css">
47 <!--[if lt IE 9]>
48 <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
49 <![endif]-->
50</head>
51<body>
52<div id="local-sidebar">
53<ul>
54<li><a href="Home.html">Home</a></li>
55<li>Setup
56<ul>
57<li><a href="Download-and-Installation.html">Download and Installation</a></li>
58<li><a href="Upgrade-and-migration.html">Upgrade and migration</a></li>
59<li><a href="Server-requirements.html">Server requirements</a></li>
60<li><a href="Server-configuration.html">Server configuration</a></li>
61<li><a href="Server-security.html">Server security</a></li>
62<li><a href="Shaarli-configuration.html">Shaarli configuration</a></li>
63<li><a href="Plugins.html">Plugins</a></li>
64</ul></li>
65<li><a href="Docker.html">Docker</a></li>
66<li><a href="Usage.html">Usage</a>
67<ul>
68<li><a href="Sharing-button.html">Sharing button</a> (bookmarklet)</li>
69<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
70<li><a href="Firefox-share.html">Firefox share</a></li>
71<li><a href="RSS-feeds.html">RSS feeds</a></li>
72<li><a href="REST-API.html">REST API</a></li>
73</ul></li>
74<li>How To
75<ul>
76<li><a href="Backup,-restore,-import-and-export.html">Backup, restore, import and export</a></li>
77<li><a href="Copy-an-existing-installation-over-SSH-and-serve-it-locally.html">Copy an existing installation over SSH and serve it locally</a></li>
78<li><a href="Create-and-serve-multiple-Shaarlis-(farm).html">Create and serve multiple Shaarlis (farm)</a></li>
79<li><a href="Download-CSS-styles-from-an-OPML-list.html">Download CSS styles from an OPML list</a></li>
80<li><a href="Datastore-hacks.html">Datastore hacks</a></li>
81</ul></li>
82<li><a href="Troubleshooting.html">Troubleshooting</a></li>
83<li><a href="Development.html">Development</a>
84<ul>
85<li><a href="GnuPG-signature.html">GnuPG signature</a></li>
86<li><a href="Coding-guidelines.html">Coding guidelines</a></li>
87<li><a href="Directory-structure.html">Directory structure</a></li>
88<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
89<li><a href="Plugin-System.html">Plugin System</a></li>
90<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
91<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
92<li><a href="Security.html">Security</a></li>
93<li><a href="Static-analysis.html">Static analysis</a></li>
94<li><a href="Theming.html">Theming</a></li>
95<li><a href="Unit-tests.html">Unit tests</a></li>
96</ul></li>
97<li>About
98<ul>
99<li><a href="FAQ.html">FAQ</a></li>
100<li><a href="Community-&amp;-Related-software.html">Community &amp; Related software</a></li>
101</ul></li>
102</ul>
103</div>
104<h1 id="versioning-and-branches">Versioning and Branches</h1>
105<p>[<strong>WORK IN PROGRESS</strong>][](.html)</p>
106<p>It's important to understand how Shaarli branches work, especially if you're maintaining a 3rd party tools for Shaarli (theme, plugin, etc.), to be sure stay compatible.</p>
107<h2 id="master-branch"><code>master</code> branch</h2>
108<p>The <code>master</code> branch is the development branch. Any new change MUST go through this branch using Pull Requests.</p>
109<p>Remarks:</p>
110<ul>
111<li>This branch shouldn't be used for production as it isn't necessary stable.</li>
112<li>3rd party aren't required to be compatible with the latest changes.</li>
113<li>Official plugins, themes and libraries (contained within Shaarli organization repos) must be compatible with the master branch.</li>
114<li>The version in this branch is always <code>dev</code>.</li>
115</ul>
116<h2 id="v0.x-branch"><code>v0.x</code> branch</h2>
117<p>This <code>v0.x</code> branch, points to the latest <code>v0.x.y</code> release.</p>
118<p>Explanation:</p>
119<p>When a new version is released, it might contains a major bug which isn't detected right away. For example, a new PHP version is released, containing backward compatibility issue which doesn't work with Shaarli.</p>
120<p>In this case, the issue is fixed in the <code>master</code> branch, and the fix is backported the to the <code>v0.x</code> branch. Then a new release is made from the <code>v0.x</code> branch.</p>
121<p>This workflow allow us to fix any major bug detected, without having to release bleeding edge feature too soon.</p>
122<h2 id="latest-branch"><code>latest</code> branch</h2>
123<p>This branch point the latest release. It recommended to use it to get the latest tested changes.</p>
124<h2 id="stable-branch"><code>stable</code> branch</h2>
125<p>The <code>stable</code> branch doesn't contain any major bug, and is one major digit version behind the latest release.</p>
126<p>For example, the current latest release is <code>v0.8.3</code>, the stable branch is an alias to the latest <code>v0.7.x</code> release. When the <code>v0.9.0</code> version will be released, the stable will move to the latest <code>v0.8.x</code> release.</p>
127<p>Remarks:</p>
128<ul>
129<li>Shaarli release pace isn't fast, and the stable branch might be a few months behind the latest release.</li>
130</ul>
131<h2 id="releases">Releases</h2>
132<p>Releases are always made from the latest <code>v0.x</code> branch.</p>
133<p>Note that for every release, we manually generate a tarball which contains all Shaarli dependencies, making Shaarli's installation only one step.</p>
134<h2 id="advices-on-3rd-party-git-repos-workflow">Advices on 3rd party git repos workflow</h2>
135<h3 id="versioning">Versioning</h3>
136<p>Any time a new Shaarli release is published, you should publish a new release of your repo if the changes affected you since the latest release (take a look at the <a href="https://github.com/shaarli/Shaarli/releases">changelog</a> (<em>Draft</em> means not released yet) and the commit log (like <a href="https://github.com/shaarli/Shaarli/commits/master/tpl/default"><code>tpl</code> folder</a> for themes)). You can either:<a href=".html"></a></p>
137<ul>
138<li>use the Shaarli version number, with your repo version. For example, if Shaarli <code>v0.8.3</code> is released, publish a <code>v0.8.3-1</code> release, where <code>v0.8.3</code> states Shaarli compatibility and <code>-1</code> is your own version digit for the current Shaarli version.</li>
139<li>use your own versioning scheme, and state Shaarli compatibility in the release description.</li>
140</ul>
141<p>Using this, any user will be able to pick the release matching his own Shaarli version.</p>
142<h3 id="major-bugfix-backport-releases">Major bugfix backport releases</h3>
143<p>To be able to support backported fixes, it recommended to use our workflow:</p>
144<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="co"># In master, fix the major bug</span>
145<span class="fu">git</span> commit -m <span class="st">&quot;Katastrophe&quot;</span>
146<span class="fu">git</span> push origin master
147<span class="co"># Get your commit hash</span>
148<span class="fu">git</span> log --format=<span class="st">&quot;%H&quot;</span> -n 1
149<span class="co"># Create a new branch from your latest release, let&#39;s say v0.8.2-1 (the tag name)</span>
150<span class="fu">git</span> checkout -b katastrophe v0.8.2-1
151<span class="co"># Backport the fix commit to your brand new branch</span>
152<span class="fu">git</span> cherry-pick <span class="op">&lt;</span>fix commit hash<span class="op">&gt;</span>
153<span class="fu">git</span> push origin katastrophe
154<span class="co"># Then you just have to make a new release from the `katastrophe` branch tagged `v0.8.3-1`</span></code></pre></div>
155</body>
156</html>
diff --git a/doc/Versioning-and-Branches.md b/doc/Versioning-and-Branches.md
new file mode 100644
index 00000000..bbc7719e
--- /dev/null
+++ b/doc/Versioning-and-Branches.md
@@ -0,0 +1,76 @@
1#Versioning and Branches
2[**WORK IN PROGRESS**][](.html)
3
4It's important to understand how Shaarli branches work, especially if you're maintaining a 3rd party tools for Shaarli (theme, plugin, etc.), to be sure stay compatible.
5
6## `master` branch
7
8The `master` branch is the development branch. Any new change MUST go through this branch using Pull Requests.
9
10Remarks:
11
12 * This branch shouldn't be used for production as it isn't necessary stable.
13 * 3rd party aren't required to be compatible with the latest changes.
14 * Official plugins, themes and libraries (contained within Shaarli organization repos) must be compatible with the master branch.
15 * The version in this branch is always `dev`.
16
17## `v0.x` branch
18
19This `v0.x` branch, points to the latest `v0.x.y` release.
20
21Explanation:
22
23When a new version is released, it might contains a major bug which isn't detected right away. For example, a new PHP version is released, containing backward compatibility issue which doesn't work with Shaarli.
24
25In this case, the issue is fixed in the `master` branch, and the fix is backported the to the `v0.x` branch. Then a new release is made from the `v0.x` branch.
26
27This workflow allow us to fix any major bug detected, without having to release bleeding edge feature too soon.
28
29## `latest` branch
30
31This branch point the latest release. It recommended to use it to get the latest tested changes.
32
33## `stable` branch
34
35The `stable` branch doesn't contain any major bug, and is one major digit version behind the latest release.
36
37For example, the current latest release is `v0.8.3`, the stable branch is an alias to the latest `v0.7.x` release. When the `v0.9.0` version will be released, the stable will move to the latest `v0.8.x` release.
38
39Remarks:
40
41 * Shaarli release pace isn't fast, and the stable branch might be a few months behind the latest release.
42
43## Releases
44
45Releases are always made from the latest `v0.x` branch.
46
47Note that for every release, we manually generate a tarball which contains all Shaarli dependencies, making Shaarli's installation only one step.
48
49## Advices on 3rd party git repos workflow
50
51### Versioning
52
53Any time a new Shaarli release is published, you should publish a new release of your repo if the changes affected you since the latest release (take a look at the [changelog](https://github.com/shaarli/Shaarli/releases) (*Draft* means not released yet) and the commit log (like [`tpl` folder](https://github.com/shaarli/Shaarli/commits/master/tpl/default) for themes)). You can either:[](.html)
54
55 - use the Shaarli version number, with your repo version. For example, if Shaarli `v0.8.3` is released, publish a `v0.8.3-1` release, where `v0.8.3` states Shaarli compatibility and `-1` is your own version digit for the current Shaarli version.
56 - use your own versioning scheme, and state Shaarli compatibility in the release description.
57
58Using this, any user will be able to pick the release matching his own Shaarli version.
59
60### Major bugfix backport releases
61
62To be able to support backported fixes, it recommended to use our workflow:
63
64```bash
65# In master, fix the major bug
66git commit -m "Katastrophe"
67git push origin master
68# Get your commit hash
69git log --format="%H" -n 1
70# Create a new branch from your latest release, let's say v0.8.2-1 (the tag name)
71git checkout -b katastrophe v0.8.2-1
72# Backport the fix commit to your brand new branch
73git cherry-pick <fix commit hash>
74git push origin katastrophe
75# Then you just have to make a new release from the `katastrophe` branch tagged `v0.8.3-1`
76```
diff --git a/doc/_Footer.html b/doc/_Footer.html
index e8a62d2a..09473a38 100644
--- a/doc/_Footer.html
+++ b/doc/_Footer.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/_Sidebar.html b/doc/_Sidebar.html
index bb6dad93..d3f94560 100644
--- a/doc/_Sidebar.html
+++ b/doc/_Sidebar.html
@@ -32,6 +32,7 @@
32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 32<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
33<li><a href="Firefox-share.html">Firefox share</a></li> 33<li><a href="Firefox-share.html">Firefox share</a></li>
34<li><a href="RSS-feeds.html">RSS feeds</a></li> 34<li><a href="RSS-feeds.html">RSS feeds</a></li>
35<li><a href="REST-API.html">REST API</a></li>
35</ul></li> 36</ul></li>
36<li>How To 37<li>How To
37<ul> 38<ul>
@@ -50,6 +51,7 @@
50<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 51<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
51<li><a href="Plugin-System.html">Plugin System</a></li> 52<li><a href="Plugin-System.html">Plugin System</a></li>
52<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 53<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
54<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
53<li><a href="Security.html">Security</a></li> 55<li><a href="Security.html">Security</a></li>
54<li><a href="Static-analysis.html">Static analysis</a></li> 56<li><a href="Static-analysis.html">Static analysis</a></li>
55<li><a href="Theming.html">Theming</a></li> 57<li><a href="Theming.html">Theming</a></li>
@@ -82,6 +84,7 @@
82<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 84<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
83<li><a href="Firefox-share.html">Firefox share</a></li> 85<li><a href="Firefox-share.html">Firefox share</a></li>
84<li><a href="RSS-feeds.html">RSS feeds</a></li> 86<li><a href="RSS-feeds.html">RSS feeds</a></li>
87<li><a href="REST-API.html">REST API</a></li>
85</ul></li> 88</ul></li>
86<li>How To 89<li>How To
87<ul> 90<ul>
@@ -100,6 +103,7 @@
100<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 103<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
101<li><a href="Plugin-System.html">Plugin System</a></li> 104<li><a href="Plugin-System.html">Plugin System</a></li>
102<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 105<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
106<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
103<li><a href="Security.html">Security</a></li> 107<li><a href="Security.html">Security</a></li>
104<li><a href="Static-analysis.html">Static analysis</a></li> 108<li><a href="Static-analysis.html">Static analysis</a></li>
105<li><a href="Theming.html">Theming</a></li> 109<li><a href="Theming.html">Theming</a></li>
diff --git a/doc/_Sidebar.md b/doc/_Sidebar.md
index 1778e3a3..8df2e565 100644
--- a/doc/_Sidebar.md
+++ b/doc/_Sidebar.md
@@ -14,6 +14,7 @@
14 - [Browsing and Searching](Browsing-and-Searching.html) 14 - [Browsing and Searching](Browsing-and-Searching.html)
15 - [Firefox share](Firefox-share.html) 15 - [Firefox share](Firefox-share.html)
16 - [RSS feeds](RSS-feeds.html) 16 - [RSS feeds](RSS-feeds.html)
17 - [REST API](REST-API.html)
17- How To 18- How To
18 - [Backup, restore, import and export](Backup,-restore,-import-and-export.html) 19 - [Backup, restore, import and export](Backup,-restore,-import-and-export.html)
19 - [Copy an existing installation over SSH and serve it locally](Copy-an-existing-installation-over-SSH-and-serve-it-locally.html) 20 - [Copy an existing installation over SSH and serve it locally](Copy-an-existing-installation-over-SSH-and-serve-it-locally.html)
@@ -28,6 +29,7 @@
28 - [3rd party libraries](3rd-party-libraries.html) 29 - [3rd party libraries](3rd-party-libraries.html)
29 - [Plugin System](Plugin-System.html) 30 - [Plugin System](Plugin-System.html)
30 - [Release Shaarli](Release-Shaarli.html) 31 - [Release Shaarli](Release-Shaarli.html)
32 - [Versioning and Branches](Versioning-and-Branches.html)
31 - [Security](Security.html) 33 - [Security](Security.html)
32 - [Static analysis](Static-analysis.html) 34 - [Static analysis](Static-analysis.html)
33 - [Theming](Theming.html) 35 - [Theming](Theming.html)
diff --git a/doc/sidebar.html b/doc/sidebar.html
index 4dad0161..478840d1 100644
--- a/doc/sidebar.html
+++ b/doc/sidebar.html
@@ -18,6 +18,7 @@
18<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li> 18<li><a href="Browsing-and-Searching.html">Browsing and Searching</a></li>
19<li><a href="Firefox-share.html">Firefox share</a></li> 19<li><a href="Firefox-share.html">Firefox share</a></li>
20<li><a href="RSS-feeds.html">RSS feeds</a></li> 20<li><a href="RSS-feeds.html">RSS feeds</a></li>
21<li><a href="REST-API.html">REST API</a></li>
21</ul></li> 22</ul></li>
22<li>How To 23<li>How To
23<ul> 24<ul>
@@ -36,6 +37,7 @@
36<li><a href="3rd-party-libraries.html">3rd party libraries</a></li> 37<li><a href="3rd-party-libraries.html">3rd party libraries</a></li>
37<li><a href="Plugin-System.html">Plugin System</a></li> 38<li><a href="Plugin-System.html">Plugin System</a></li>
38<li><a href="Release-Shaarli.html">Release Shaarli</a></li> 39<li><a href="Release-Shaarli.html">Release Shaarli</a></li>
40<li><a href="Versioning-and-Branches.html">Versioning and Branches</a></li>
39<li><a href="Security.html">Security</a></li> 41<li><a href="Security.html">Security</a></li>
40<li><a href="Static-analysis.html">Static analysis</a></li> 42<li><a href="Static-analysis.html">Static analysis</a></li>
41<li><a href="Theming.html">Theming</a></li> 43<li><a href="Theming.html">Theming</a></li>
diff --git a/index.php b/index.php
index 5c21c2f6..61b71129 100644
--- a/index.php
+++ b/index.php
@@ -62,6 +62,7 @@ require_once 'application/CachedPage.php';
62require_once 'application/config/ConfigPlugin.php'; 62require_once 'application/config/ConfigPlugin.php';
63require_once 'application/FeedBuilder.php'; 63require_once 'application/FeedBuilder.php';
64require_once 'application/FileUtils.php'; 64require_once 'application/FileUtils.php';
65require_once 'application/History.php';
65require_once 'application/HttpUtils.php'; 66require_once 'application/HttpUtils.php';
66require_once 'application/Languages.php'; 67require_once 'application/Languages.php';
67require_once 'application/LinkDB.php'; 68require_once 'application/LinkDB.php';
@@ -224,27 +225,6 @@ function setup_login_state($conf)
224} 225}
225$userIsLoggedIn = setup_login_state($conf); 226$userIsLoggedIn = setup_login_state($conf);
226 227
227/**
228 * PubSubHubbub protocol support (if enabled) [UNTESTED]
229 * (Source: http://aldarone.fr/les-flux-rss-shaarli-et-pubsubhubbub/ )
230 *
231 * @param ConfigManager $conf Configuration Manager instance.
232 */
233function pubsubhub($conf)
234{
235 $pshUrl = $conf->get('config.PUBSUBHUB_URL');
236 if (!empty($pshUrl))
237 {
238 include_once './publisher.php';
239 $p = new Publisher($pshUrl);
240 $topic_url = array (
241 index_url($_SERVER).'?do=atom',
242 index_url($_SERVER).'?do=rss'
243 );
244 $p->publish_update($topic_url);
245 }
246}
247
248// ------------------------------------------------------------------------------------------ 228// ------------------------------------------------------------------------------------------
249// Session management 229// Session management
250 230
@@ -473,34 +453,6 @@ if (isset($_POST['login']))
473} 453}
474 454
475// ------------------------------------------------------------------------------------------ 455// ------------------------------------------------------------------------------------------
476// Misc utility functions:
477
478// Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
479function return_bytes($val)
480{
481 $val = trim($val); $last=strtolower($val[strlen($val)-1]);
482 switch($last)
483 {
484 case 'g': $val *= 1024;
485 case 'm': $val *= 1024;
486 case 'k': $val *= 1024;
487 }
488 return $val;
489}
490
491// Try to determine max file size for uploads (POST).
492// Returns an integer (in bytes)
493function getMaxFileSize()
494{
495 $size1 = return_bytes(ini_get('post_max_size'));
496 $size2 = return_bytes(ini_get('upload_max_filesize'));
497 // Return the smaller of two:
498 $maxsize = min($size1,$size2);
499 // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000)
500 return $maxsize;
501}
502
503// ------------------------------------------------------------------------------------------
504// Token management for XSRF protection 456// Token management for XSRF protection
505// Token should be used in any form which acts on data (create,update,delete,import...). 457// Token should be used in any form which acts on data (create,update,delete,import...).
506if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. 458if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session.
@@ -695,9 +647,11 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
695 647
696 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); 648 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
697 $data = array( 649 $data = array(
650 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
698 'linksToDisplay' => $linksToDisplay, 651 'linksToDisplay' => $linksToDisplay,
699 'cols' => $columns, 652 'cols' => $columns,
700 'day' => $dayDate->getTimestamp(), 653 'day' => $dayDate->getTimestamp(),
654 'dayDate' => $dayDate,
701 'previousday' => $previousday, 655 'previousday' => $previousday,
702 'nextday' => $nextday, 656 'nextday' => $nextday,
703 ); 657 );
@@ -732,7 +686,7 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
732 * @param PluginManager $pluginManager Plugin Manager instance, 686 * @param PluginManager $pluginManager Plugin Manager instance,
733 * @param LinkDB $LINKSDB 687 * @param LinkDB $LINKSDB
734 */ 688 */
735function renderPage($conf, $pluginManager, $LINKSDB) 689function renderPage($conf, $pluginManager, $LINKSDB, $history)
736{ 690{
737 $updater = new Updater( 691 $updater = new Updater(
738 read_updates_file($conf->get('resource.updates')), 692 read_updates_file($conf->get('resource.updates')),
@@ -753,7 +707,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
753 die($e->getMessage()); 707 die($e->getMessage());
754 } 708 }
755 709
756 $PAGE = new PageBuilder($conf); 710 $PAGE = new PageBuilder($conf, $LINKSDB);
757 $PAGE->assign('linkcount', count($LINKSDB)); 711 $PAGE->assign('linkcount', count($LINKSDB));
758 $PAGE->assign('privateLinkcount', count_private($LINKSDB)); 712 $PAGE->assign('privateLinkcount', count_private($LINKSDB));
759 $PAGE->assign('plugin_errors', $pluginManager->getErrors()); 713 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
@@ -836,7 +790,9 @@ function renderPage($conf, $pluginManager, $LINKSDB)
836 // -------- Tag cloud 790 // -------- Tag cloud
837 if ($targetPage == Router::$PAGE_TAGCLOUD) 791 if ($targetPage == Router::$PAGE_TAGCLOUD)
838 { 792 {
839 $tags= $LINKSDB->allTags(); 793 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
794 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
795 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
840 796
841 // We sort tags alphabetically, then choose a font size according to count. 797 // We sort tags alphabetically, then choose a font size according to count.
842 // First, find max value. 798 // First, find max value.
@@ -845,17 +801,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
845 $maxcount = max($maxcount, $value); 801 $maxcount = max($maxcount, $value);
846 } 802 }
847 803
848 // Sort tags alphabetically: case insensitive, support locale if available. 804 alphabetical_sort($tags, true, true);
849 uksort($tags, function($a, $b) {
850 // Collator is part of PHP intl.
851 if (class_exists('Collator')) {
852 $c = new Collator(setlocale(LC_COLLATE, 0));
853 if (!intl_is_failure(intl_get_error_code())) {
854 return $c->compare($a, $b);
855 }
856 }
857 return strcasecmp($a, $b);
858 });
859 805
860 $tagList = array(); 806 $tagList = array();
861 foreach($tags as $key => $value) { 807 foreach($tags as $key => $value) {
@@ -870,6 +816,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
870 } 816 }
871 817
872 $data = array( 818 $data = array(
819 'search_tags' => implode(' ', $filteringTags),
873 'tags' => $tagList, 820 'tags' => $tagList,
874 ); 821 );
875 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); 822 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn()));
@@ -878,7 +825,32 @@ function renderPage($conf, $pluginManager, $LINKSDB)
878 $PAGE->assign($key, $value); 825 $PAGE->assign($key, $value);
879 } 826 }
880 827
881 $PAGE->renderPage('tagcloud'); 828 $PAGE->renderPage('tag.cloud');
829 exit;
830 }
831
832 // -------- Tag cloud
833 if ($targetPage == Router::$PAGE_TAGLIST)
834 {
835 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
836 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
837 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
838
839 if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
840 alphabetical_sort($tags, false, true);
841 }
842
843 $data = [
844 'search_tags' => implode(' ', $filteringTags),
845 'tags' => $tags,
846 ];
847 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
848
849 foreach ($data as $key => $value) {
850 $PAGE->assign($key, $value);
851 }
852
853 $PAGE->renderPage('tag.list');
882 exit; 854 exit;
883 } 855 }
884 856
@@ -1151,6 +1123,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1151 $conf->set('api.secret', escape($_POST['apiSecret'])); 1123 $conf->set('api.secret', escape($_POST['apiSecret']));
1152 try { 1124 try {
1153 $conf->write(isLoggedIn()); 1125 $conf->write(isLoggedIn());
1126 $history->updateSettings();
1154 invalidateCaches($conf->get('resource.page_cache')); 1127 invalidateCaches($conf->get('resource.page_cache'));
1155 } 1128 }
1156 catch(Exception $e) { 1129 catch(Exception $e) {
@@ -1172,9 +1145,12 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1172 $PAGE->assign('theme', $conf->get('resource.theme')); 1145 $PAGE->assign('theme', $conf->get('resource.theme'));
1173 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl'))); 1146 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
1174 $PAGE->assign('redirector', $conf->get('redirector.url')); 1147 $PAGE->assign('redirector', $conf->get('redirector.url'));
1175 list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone')); 1148 list($continents, $cities) = generateTimeZoneData(
1176 $PAGE->assign('timezone_form', $timezone_form); 1149 timezone_identifiers_list(),
1177 $PAGE->assign('timezone_js',$timezone_js); 1150 $conf->get('general.timezone')
1151 );
1152 $PAGE->assign('continents', $continents);
1153 $PAGE->assign('cities', $cities);
1178 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false)); 1154 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
1179 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false)); 1155 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
1180 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false)); 1156 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
@@ -1191,7 +1167,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1191 if ($targetPage == Router::$PAGE_CHANGETAG) 1167 if ($targetPage == Router::$PAGE_CHANGETAG)
1192 { 1168 {
1193 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { 1169 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
1194 $PAGE->assign('tags', $LINKSDB->allTags()); 1170 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
1195 $PAGE->renderPage('changetag'); 1171 $PAGE->renderPage('changetag');
1196 exit; 1172 exit;
1197 } 1173 }
@@ -1211,6 +1187,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1211 unset($tags[array_search($needle,$tags)]); // Remove tag. 1187 unset($tags[array_search($needle,$tags)]); // Remove tag.
1212 $value['tags']=trim(implode(' ',$tags)); 1188 $value['tags']=trim(implode(' ',$tags));
1213 $LINKSDB[$key]=$value; 1189 $LINKSDB[$key]=$value;
1190 $history->updateLink($LINKSDB[$key]);
1214 } 1191 }
1215 $LINKSDB->save($conf->get('resource.page_cache')); 1192 $LINKSDB->save($conf->get('resource.page_cache'));
1216 echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?do=changetag\';</script>'; 1193 echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?do=changetag\';</script>';
@@ -1228,6 +1205,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1228 $tags[array_search($needle, $tags)] = trim($_POST['totag']); 1205 $tags[array_search($needle, $tags)] = trim($_POST['totag']);
1229 $value['tags'] = implode(' ', array_unique($tags)); 1206 $value['tags'] = implode(' ', array_unique($tags));
1230 $LINKSDB[$key] = $value; 1207 $LINKSDB[$key] = $value;
1208 $history->updateLink($LINKSDB[$key]);
1231 } 1209 }
1232 $LINKSDB->save($conf->get('resource.page_cache')); // Save to disk. 1210 $LINKSDB->save($conf->get('resource.page_cache')); // Save to disk.
1233 echo '<script>alert("Tag was renamed in '.count($linksToAlter).' links.");document.location=\'?searchtags='.urlencode(escape($_POST['totag'])).'\';</script>'; 1211 echo '<script>alert("Tag was renamed in '.count($linksToAlter).' links.");document.location=\'?searchtags='.urlencode(escape($_POST['totag'])).'\';</script>';
@@ -1262,11 +1240,13 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1262 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); 1240 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
1263 $updated = new DateTime(); 1241 $updated = new DateTime();
1264 $shortUrl = $LINKSDB[$id]['shorturl']; 1242 $shortUrl = $LINKSDB[$id]['shorturl'];
1243 $new = false;
1265 } else { 1244 } else {
1266 // New link 1245 // New link
1267 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); 1246 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
1268 $updated = null; 1247 $updated = null;
1269 $shortUrl = link_small_hash($created, $id); 1248 $shortUrl = link_small_hash($created, $id);
1249 $new = true;
1270 } 1250 }
1271 1251
1272 // Remove multiple spaces. 1252 // Remove multiple spaces.
@@ -1305,6 +1285,11 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1305 1285
1306 $LINKSDB[$id] = $link; 1286 $LINKSDB[$id] = $link;
1307 $LINKSDB->save($conf->get('resource.page_cache')); 1287 $LINKSDB->save($conf->get('resource.page_cache'));
1288 if ($new) {
1289 $history->addLink($link);
1290 } else {
1291 $history->updateLink($link);
1292 }
1308 1293
1309 // If we are called from the bookmarklet, we must close the popup: 1294 // If we are called from the bookmarklet, we must close the popup:
1310 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { 1295 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
@@ -1342,19 +1327,23 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1342 // -------- User clicked the "Delete" button when editing a link: Delete link from database. 1327 // -------- User clicked the "Delete" button when editing a link: Delete link from database.
1343 if ($targetPage == Router::$PAGE_DELETELINK) 1328 if ($targetPage == Router::$PAGE_DELETELINK)
1344 { 1329 {
1345 // We do not need to ask for confirmation:
1346 // - confirmation is handled by JavaScript
1347 // - we are protected from XSRF by the token.
1348
1349 if (! tokenOk($_GET['token'])) { 1330 if (! tokenOk($_GET['token'])) {
1350 die('Wrong token.'); 1331 die('Wrong token.');
1351 } 1332 }
1352 1333
1353 $id = intval(escape($_GET['lf_linkdate'])); 1334 if (strpos($_GET['lf_linkdate'], ' ') !== false) {
1354 $link = $LINKSDB[$id]; 1335 $ids = array_values(array_filter(preg_split('/\s+/', escape($_GET['lf_linkdate']))));
1355 $pluginManager->executeHooks('delete_link', $link); 1336 } else {
1356 unset($LINKSDB[$id]); 1337 $ids = [$_GET['lf_linkdate']];
1338 }
1339 foreach ($ids as $id) {
1340 $id = (int) escape($id);
1341 $link = $LINKSDB[$id];
1342 $pluginManager->executeHooks('delete_link', $link);
1343 unset($LINKSDB[$id]);
1344 }
1357 $LINKSDB->save($conf->get('resource.page_cache')); // save to disk 1345 $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
1346 $history->deleteLink($link);
1358 1347
1359 // If we are called from the bookmarklet, we must close the popup: 1348 // If we are called from the bookmarklet, we must close the popup:
1360 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; } 1349 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
@@ -1384,7 +1373,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1384 'link' => $link, 1373 'link' => $link,
1385 'link_is_new' => false, 1374 'link_is_new' => false,
1386 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 1375 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1387 'tags' => $LINKSDB->allTags(), 1376 'tags' => $LINKSDB->linksCountPerTag(),
1388 ); 1377 );
1389 $pluginManager->executeHooks('render_editlink', $data); 1378 $pluginManager->executeHooks('render_editlink', $data);
1390 1379
@@ -1453,7 +1442,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1453 'link_is_new' => $link_is_new, 1442 'link_is_new' => $link_is_new,
1454 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 1443 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1455 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), 1444 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
1456 'tags' => $LINKSDB->allTags(), 1445 'tags' => $LINKSDB->linksCountPerTag(),
1457 'default_private_links' => $conf->get('privacy.default_private_links', false), 1446 'default_private_links' => $conf->get('privacy.default_private_links', false),
1458 ); 1447 );
1459 $pluginManager->executeHooks('render_editlink', $data); 1448 $pluginManager->executeHooks('render_editlink', $data);
@@ -1515,7 +1504,22 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1515 1504
1516 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) { 1505 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1517 // Show import dialog 1506 // Show import dialog
1518 $PAGE->assign('maxfilesize', getMaxFileSize()); 1507 $PAGE->assign(
1508 'maxfilesize',
1509 get_max_upload_size(
1510 ini_get('post_max_size'),
1511 ini_get('upload_max_filesize'),
1512 false
1513 )
1514 );
1515 $PAGE->assign(
1516 'maxfilesizeHuman',
1517 get_max_upload_size(
1518 ini_get('post_max_size'),
1519 ini_get('upload_max_filesize'),
1520 true
1521 )
1522 );
1519 $PAGE->renderPage('import'); 1523 $PAGE->renderPage('import');
1520 exit; 1524 exit;
1521 } 1525 }
@@ -1525,7 +1529,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1525 // The file is too big or some form field may be missing. 1529 // The file is too big or some form field may be missing.
1526 echo '<script>alert("The file you are trying to upload is probably' 1530 echo '<script>alert("The file you are trying to upload is probably'
1527 .' bigger than what this webserver can accept (' 1531 .' bigger than what this webserver can accept ('
1528 .getMaxFileSize().' bytes).' 1532 .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').'
1529 .' Please upload in smaller chunks.");document.location=\'?do=' 1533 .' Please upload in smaller chunks.");document.location=\'?do='
1530 .Router::$PAGE_IMPORT .'\';</script>'; 1534 .Router::$PAGE_IMPORT .'\';</script>';
1531 exit; 1535 exit;
@@ -1537,7 +1541,8 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1537 $_POST, 1541 $_POST,
1538 $_FILES, 1542 $_FILES,
1539 $LINKSDB, 1543 $LINKSDB,
1540 $conf 1544 $conf,
1545 $history
1541 ); 1546 );
1542 echo '<script>alert("'.$status.'");document.location=\'?do=' 1547 echo '<script>alert("'.$status.'");document.location=\'?do='
1543 .Router::$PAGE_IMPORT .'\';</script>'; 1548 .Router::$PAGE_IMPORT .'\';</script>';
@@ -1577,6 +1582,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1577 $conf->set('general.enabled_plugins', save_plugin_config($_POST)); 1582 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
1578 } 1583 }
1579 $conf->write(isLoggedIn()); 1584 $conf->write(isLoggedIn());
1585 $history->updateSettings();
1580 } 1586 }
1581 catch (Exception $e) { 1587 catch (Exception $e) {
1582 error_log( 1588 error_log(
@@ -1592,6 +1598,13 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1592 exit; 1598 exit;
1593 } 1599 }
1594 1600
1601 // Get a fresh token
1602 if ($targetPage == Router::$GET_TOKEN) {
1603 header('Content-Type:text/plain');
1604 echo getToken($conf);
1605 exit;
1606 }
1607
1595 // -------- Otherwise, simply display search form and links: 1608 // -------- Otherwise, simply display search form and links:
1596 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); 1609 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
1597 exit; 1610 exit;
@@ -1694,7 +1707,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1694 'visibility' => ! empty($_SESSION['privateonly']) ? 'private' : '', 1707 'visibility' => ! empty($_SESSION['privateonly']) ? 'private' : '',
1695 'redirector' => $conf->get('redirector.url'), // Optional redirector URL. 1708 'redirector' => $conf->get('redirector.url'), // Optional redirector URL.
1696 'links' => $linkDisp, 1709 'links' => $linkDisp,
1697 'tags' => $LINKSDB->allTags(),
1698 ); 1710 );
1699 1711
1700 // If there is only a single link, we change on-the-fly the title of the page. 1712 // If there is only a single link, we change on-the-fly the title of the page.
@@ -1980,16 +1992,10 @@ function install($conf)
1980 exit; 1992 exit;
1981 } 1993 }
1982 1994
1983 // Display config form:
1984 list($timezone_form, $timezone_js) = generateTimeZoneForm();
1985 $timezone_html = '';
1986 if ($timezone_form != '') {
1987 $timezone_html = '<tr><td><b>Timezone:</b></td><td>'.$timezone_form.'</td></tr>';
1988 }
1989
1990 $PAGE = new PageBuilder($conf); 1995 $PAGE = new PageBuilder($conf);
1991 $PAGE->assign('timezone_html',$timezone_html); 1996 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
1992 $PAGE->assign('timezone_js',$timezone_js); 1997 $PAGE->assign('continents', $continents);
1998 $PAGE->assign('cities', $cities);
1993 $PAGE->renderPage('install'); 1999 $PAGE->renderPage('install');
1994 exit; 2000 exit;
1995} 2001}
@@ -2233,16 +2239,27 @@ $linkDb = new LinkDB(
2233 $conf->get('redirector.encode_url') 2239 $conf->get('redirector.encode_url')
2234); 2240);
2235 2241
2242try {
2243 $history = new History($conf->get('resource.history'));
2244} catch(Exception $e) {
2245 die($e->getMessage());
2246}
2247
2236$container = new \Slim\Container(); 2248$container = new \Slim\Container();
2237$container['conf'] = $conf; 2249$container['conf'] = $conf;
2238$container['plugins'] = $pluginManager; 2250$container['plugins'] = $pluginManager;
2251$container['history'] = $history;
2239$app = new \Slim\App($container); 2252$app = new \Slim\App($container);
2240 2253
2241// REST API routes 2254// REST API routes
2242$app->group('/api/v1', function() { 2255$app->group('/api/v1', function() {
2243 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo'); 2256 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
2244 $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks'); 2257 $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
2245 $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink'); 2258 $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
2259 $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
2260 $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink');
2261 $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink');
2262 $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory');
2246})->add('\Shaarli\Api\ApiMiddleware'); 2263})->add('\Shaarli\Api\ApiMiddleware');
2247 2264
2248$response = $app->run(true); 2265$response = $app->run(true);
@@ -2251,7 +2268,7 @@ $response = $app->run(true);
2251if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { 2268if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
2252 // We use UTF-8 for proper international characters handling. 2269 // We use UTF-8 for proper international characters handling.
2253 header('Content-Type: text/html; charset=utf-8'); 2270 header('Content-Type: text/html; charset=utf-8');
2254 renderPage($conf, $pluginManager, $linkDb); 2271 renderPage($conf, $pluginManager, $linkDb, $history);
2255} else { 2272} else {
2256 $app->respond($response); 2273 $app->respond($response);
2257} 2274}
diff --git a/plugins/readityourself/book-open.png b/plugins/readityourself/book-open.png
deleted file mode 100644
index 36513d7b..00000000
--- a/plugins/readityourself/book-open.png
+++ /dev/null
Binary files differ
diff --git a/plugins/readityourself/readityourself.html b/plugins/readityourself/readityourself.html
deleted file mode 100644
index 5e200715..00000000
--- a/plugins/readityourself/readityourself.html
+++ /dev/null
@@ -1 +0,0 @@
1<span><a href="%s?url=%s"><img class="linklist-plugin-icon" src="%s/readityourself/book-open.png" title="Read with Readityourself" alt="readityourself" /></a></span>
diff --git a/plugins/readityourself/readityourself.meta b/plugins/readityourself/readityourself.meta
deleted file mode 100644
index bd611dd0..00000000
--- a/plugins/readityourself/readityourself.meta
+++ /dev/null
@@ -1,2 +0,0 @@
1description="For each link, add a ReadItYourself icon to save the shaared URL."
2parameters=READITYOUSELF_URL; \ No newline at end of file
diff --git a/plugins/readityourself/readityourself.php b/plugins/readityourself/readityourself.php
deleted file mode 100644
index 961c5bda..00000000
--- a/plugins/readityourself/readityourself.php
+++ /dev/null
@@ -1,51 +0,0 @@
1<?php
2
3/**
4 * Plugin readityourself
5 */
6
7// If we're talking about https://github.com/memiks/readityourself
8// it seems kinda dead.
9// Not tested.
10
11/**
12 * Init function, return an error if the server is not set.
13 *
14 * @param $conf ConfigManager instance.
15 *
16 * @return array Eventual error.
17 */
18function readityourself_init($conf)
19{
20 $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
21 if (empty($riyUrl)) {
22 $error = 'Readityourself plugin error: '.
23 'Please define the "READITYOUSELF_URL" setting in the plugin administration page.';
24 return array($error);
25 }
26}
27
28/**
29 * Add readityourself icon to link_plugin when rendering linklist.
30 *
31 * @param mixed $data Linklist data.
32 * @param ConfigManager $conf Configuration Manager instance.
33 *
34 * @return mixed - linklist data with readityourself plugin.
35 */
36function hook_readityourself_render_linklist($data, $conf)
37{
38 $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
39 if (empty($riyUrl)) {
40 return $data;
41 }
42
43 $readityourself_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/readityourself/readityourself.html');
44
45 foreach ($data['links'] as &$value) {
46 $readityourself = sprintf($readityourself_html, $riyUrl, $value['url'], PluginManager::$PLUGINS_PATH);
47 $value['link_plugin'][] = $readityourself;
48 }
49
50 return $data;
51}
diff --git a/tests/FileUtilsTest.php b/tests/FileUtilsTest.php
new file mode 100644
index 00000000..d764e495
--- /dev/null
+++ b/tests/FileUtilsTest.php
@@ -0,0 +1,108 @@
1<?php
2
3require_once 'application/FileUtils.php';
4
5/**
6 * Class FileUtilsTest
7 *
8 * Test file utility class.
9 */
10class FileUtilsTest extends PHPUnit_Framework_TestCase
11{
12 /**
13 * @var string Test file path.
14 */
15 protected static $file = 'sandbox/flat.db';
16
17 /**
18 * Delete test file after every test.
19 */
20 public function tearDown()
21 {
22 @unlink(self::$file);
23 }
24
25 /**
26 * Test writeDB, then readDB with different data.
27 */
28 public function testSimpleWriteRead()
29 {
30 $data = ['blue', 'red'];
31 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
32 $this->assertTrue(startsWith(file_get_contents(self::$file), '<?php /*'));
33 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
34
35 $data = 0;
36 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
37 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
38
39 $data = null;
40 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
41 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
42
43 $data = false;
44 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
45 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
46 }
47
48 /**
49 * File not writable: raise an exception.
50 *
51 * @expectedException IOException
52 * @expectedExceptionMessage Error accessing "sandbox/flat.db"
53 */
54 public function testWriteWithoutPermission()
55 {
56 touch(self::$file);
57 chmod(self::$file, 0440);
58 FileUtils::writeFlatDB(self::$file, null);
59 }
60
61 /**
62 * Folder non existent: raise an exception.
63 *
64 * @expectedException IOException
65 * @expectedExceptionMessage Error accessing "nopefolder"
66 */
67 public function testWriteFolderDoesNotExist()
68 {
69 FileUtils::writeFlatDB('nopefolder/file', null);
70 }
71
72 /**
73 * Folder non writable: raise an exception.
74 *
75 * @expectedException IOException
76 * @expectedExceptionMessage Error accessing "sandbox"
77 */
78 public function testWriteFolderPermission()
79 {
80 chmod(dirname(self::$file), 0555);
81 try {
82 FileUtils::writeFlatDB(self::$file, null);
83 } catch (Exception $e) {
84 chmod(dirname(self::$file), 0755);
85 throw $e;
86 }
87 }
88
89 /**
90 * Read non existent file, use default parameter.
91 */
92 public function testReadNotExistentFile()
93 {
94 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
95 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
96 }
97
98 /**
99 * Read non readable file, use default parameter.
100 */
101 public function testReadNotReadable()
102 {
103 touch(self::$file);
104 chmod(self::$file, 0220);
105 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
106 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
107 }
108}
diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php
new file mode 100644
index 00000000..d3bef5a3
--- /dev/null
+++ b/tests/HistoryTest.php
@@ -0,0 +1,207 @@
1<?php
2
3require_once 'application/History.php';
4
5
6class HistoryTest extends PHPUnit_Framework_TestCase
7{
8 /**
9 * @var string History file path
10 */
11 protected static $historyFilePath = 'sandbox/history.php';
12
13 /**
14 * Delete history file.
15 */
16 public function tearDown()
17 {
18 @unlink(self::$historyFilePath);
19 }
20
21 /**
22 * Test that the history file is created if it doesn't exist.
23 */
24 public function testConstructLazyLoading()
25 {
26 new History(self::$historyFilePath);
27 $this->assertFileNotExists(self::$historyFilePath);
28 }
29
30 /**
31 * Test that the history file is created if it doesn't exist.
32 */
33 public function testAddEventCreateFile()
34 {
35 $history = new History(self::$historyFilePath);
36 $history->updateSettings();
37 $this->assertFileExists(self::$historyFilePath);
38 }
39
40 /**
41 * Not writable history file: raise an exception.
42 *
43 * @expectedException Exception
44 * @expectedExceptionMessage History file isn't readable or writable
45 */
46 public function testConstructNotWritable()
47 {
48 touch(self::$historyFilePath);
49 chmod(self::$historyFilePath, 0440);
50 $history = new History(self::$historyFilePath);
51 $history->updateSettings();
52 }
53
54 /**
55 * Not parsable history file: raise an exception.
56 *
57 * @expectedException Exception
58 * @expectedExceptionMessageRegExp /Could not parse history file/
59 */
60 public function testConstructNotParsable()
61 {
62 file_put_contents(self::$historyFilePath, 'not parsable');
63 $history = new History(self::$historyFilePath);
64 // gzinflate generates a warning
65 @$history->updateSettings();
66 }
67
68 /**
69 * Test add link event
70 */
71 public function testAddLink()
72 {
73 $history = new History(self::$historyFilePath);
74 $history->addLink(['id' => 0]);
75 $actual = $history->getHistory()[0];
76 $this->assertEquals(History::CREATED, $actual['event']);
77 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
78 $this->assertEquals(0, $actual['id']);
79
80 $history = new History(self::$historyFilePath);
81 $history->addLink(['id' => 1]);
82 $actual = $history->getHistory()[0];
83 $this->assertEquals(History::CREATED, $actual['event']);
84 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
85 $this->assertEquals(1, $actual['id']);
86
87 $history = new History(self::$historyFilePath);
88 $history->addLink(['id' => 'str']);
89 $actual = $history->getHistory()[0];
90 $this->assertEquals(History::CREATED, $actual['event']);
91 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
92 $this->assertEquals('str', $actual['id']);
93 }
94
95 /**
96 * Test updated link event
97 */
98 public function testUpdateLink()
99 {
100 $history = new History(self::$historyFilePath);
101 $history->updateLink(['id' => 1]);
102 $actual = $history->getHistory()[0];
103 $this->assertEquals(History::UPDATED, $actual['event']);
104 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
105 $this->assertEquals(1, $actual['id']);
106 }
107
108 /**
109 * Test delete link event
110 */
111 public function testDeleteLink()
112 {
113 $history = new History(self::$historyFilePath);
114 $history->deleteLink(['id' => 1]);
115 $actual = $history->getHistory()[0];
116 $this->assertEquals(History::DELETED, $actual['event']);
117 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
118 $this->assertEquals(1, $actual['id']);
119 }
120
121 /**
122 * Test updated settings event
123 */
124 public function testUpdateSettings()
125 {
126 $history = new History(self::$historyFilePath);
127 $history->updateSettings();
128 $actual = $history->getHistory()[0];
129 $this->assertEquals(History::SETTINGS, $actual['event']);
130 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
131 $this->assertEmpty($actual['id']);
132 }
133
134 /**
135 * Make sure that new items are stored at the beginning
136 */
137 public function testHistoryOrder()
138 {
139 $history = new History(self::$historyFilePath);
140 $history->updateLink(['id' => 1]);
141 $actual = $history->getHistory()[0];
142 $this->assertEquals(History::UPDATED, $actual['event']);
143 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
144 $this->assertEquals(1, $actual['id']);
145
146 $history->addLink(['id' => 1]);
147 $actual = $history->getHistory()[0];
148 $this->assertEquals(History::CREATED, $actual['event']);
149 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
150 $this->assertEquals(1, $actual['id']);
151 }
152
153 /**
154 * Re-read history from file after writing an event
155 */
156 public function testHistoryRead()
157 {
158 $history = new History(self::$historyFilePath);
159 $history->updateLink(['id' => 1]);
160 $history = new History(self::$historyFilePath);
161 $actual = $history->getHistory()[0];
162 $this->assertEquals(History::UPDATED, $actual['event']);
163 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
164 $this->assertEquals(1, $actual['id']);
165 }
166
167 /**
168 * Re-read history from file after writing an event and make sure that the order is correct
169 */
170 public function testHistoryOrderRead()
171 {
172 $history = new History(self::$historyFilePath);
173 $history->updateLink(['id' => 1]);
174 $history->addLink(['id' => 1]);
175
176 $history = new History(self::$historyFilePath);
177 $actual = $history->getHistory()[0];
178 $this->assertEquals(History::CREATED, $actual['event']);
179 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
180 $this->assertEquals(1, $actual['id']);
181
182 $actual = $history->getHistory()[1];
183 $this->assertEquals(History::UPDATED, $actual['event']);
184 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
185 $this->assertEquals(1, $actual['id']);
186 }
187
188 /**
189 * Test retention time: delete old entries.
190 */
191 public function testHistoryRententionTime()
192 {
193 $history = new History(self::$historyFilePath, 5);
194 $history->updateLink(['id' => 1]);
195 $this->assertEquals(1, count($history->getHistory()));
196 $arr = $history->getHistory();
197 $arr[0]['datetime'] = new DateTime('-1 hour');
198 FileUtils::writeFlatDB(self::$historyFilePath, $arr);
199
200 $history = new History(self::$historyFilePath, 60);
201 $this->assertEquals(1, count($history->getHistory()));
202 $this->assertEquals(1, $history->getHistory()[0]['id']);
203 $history->updateLink(['id' => 2]);
204 $this->assertEquals(1, count($history->getHistory()));
205 $this->assertEquals(2, $history->getHistory()[0]['id']);
206 }
207}
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php
index 1f62a34a..2523467d 100644
--- a/tests/LinkDBTest.php
+++ b/tests/LinkDBTest.php
@@ -101,7 +101,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
101 * Attempt to instantiate a LinkDB whereas the datastore is not writable 101 * Attempt to instantiate a LinkDB whereas the datastore is not writable
102 * 102 *
103 * @expectedException IOException 103 * @expectedException IOException
104 * @expectedExceptionMessageRegExp /Error accessing\nnull/ 104 * @expectedExceptionMessageRegExp /Error accessing "null"/
105 */ 105 */
106 public function testConstructDatastoreNotWriteable() 106 public function testConstructDatastoreNotWriteable()
107 { 107 {
@@ -297,7 +297,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
297 'sTuff' => 2, 297 'sTuff' => 2,
298 'ut' => 1, 298 'ut' => 1,
299 ), 299 ),
300 self::$publicLinkDB->allTags() 300 self::$publicLinkDB->linksCountPerTag()
301 ); 301 );
302 302
303 $this->assertEquals( 303 $this->assertEquals(
@@ -325,7 +325,34 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
325 'tag4' => 1, 325 'tag4' => 1,
326 'ut' => 1, 326 'ut' => 1,
327 ), 327 ),
328 self::$privateLinkDB->allTags() 328 self::$privateLinkDB->linksCountPerTag()
329 );
330 $this->assertEquals(
331 array(
332 'web' => 4,
333 'cartoon' => 2,
334 'gnu' => 1,
335 'dev' => 1,
336 'samba' => 1,
337 'media' => 1,
338 'html' => 1,
339 'w3c' => 1,
340 'css' => 1,
341 'Mercurial' => 1,
342 '.hidden' => 1,
343 'hashtag' => 1,
344 ),
345 self::$privateLinkDB->linksCountPerTag(['web'])
346 );
347 $this->assertEquals(
348 array(
349 'web' => 1,
350 'html' => 1,
351 'w3c' => 1,
352 'css' => 1,
353 'Mercurial' => 1,
354 ),
355 self::$privateLinkDB->linksCountPerTag(['web'], 'private')
329 ); 356 );
330 } 357 }
331 358
diff --git a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
index 5925a8e1..5fc1d1e8 100644
--- a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
+++ b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
@@ -34,6 +34,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
34 protected static $testDatastore = 'sandbox/datastore.php'; 34 protected static $testDatastore = 'sandbox/datastore.php';
35 35
36 /** 36 /**
37 * @var string History file path
38 */
39 protected static $historyFilePath = 'sandbox/history.php';
40
41 /**
37 * @var LinkDB private LinkDB instance 42 * @var LinkDB private LinkDB instance
38 */ 43 */
39 protected $linkDb = null; 44 protected $linkDb = null;
@@ -49,6 +54,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
49 protected $conf; 54 protected $conf;
50 55
51 /** 56 /**
57 * @var History instance.
58 */
59 protected $history;
60
61 /**
52 * @var string Save the current timezone. 62 * @var string Save the current timezone.
53 */ 63 */
54 protected static $defaultTimeZone; 64 protected static $defaultTimeZone;
@@ -73,6 +83,15 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
73 $this->linkDb = new LinkDB(self::$testDatastore, true, false); 83 $this->linkDb = new LinkDB(self::$testDatastore, true, false);
74 $this->conf = new ConfigManager('tests/utils/config/configJson'); 84 $this->conf = new ConfigManager('tests/utils/config/configJson');
75 $this->conf->set('resource.page_cache', $this->pagecache); 85 $this->conf->set('resource.page_cache', $this->pagecache);
86 $this->history = new History(self::$historyFilePath);
87 }
88
89 /**
90 * Delete history file.
91 */
92 public function tearDown()
93 {
94 @unlink(self::$historyFilePath);
76 } 95 }
77 96
78 public static function tearDownAfterClass() 97 public static function tearDownAfterClass()
@@ -89,7 +108,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
89 $this->assertEquals( 108 $this->assertEquals(
90 'File empty.htm (0 bytes) has an unknown file format.' 109 'File empty.htm (0 bytes) has an unknown file format.'
91 .' Nothing was imported.', 110 .' Nothing was imported.',
92 NetscapeBookmarkUtils::import(NULL, $files, NULL, $this->conf) 111 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
93 ); 112 );
94 $this->assertEquals(0, count($this->linkDb)); 113 $this->assertEquals(0, count($this->linkDb));
95 } 114 }
@@ -102,7 +121,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
102 $files = file2array('no_doctype.htm'); 121 $files = file2array('no_doctype.htm');
103 $this->assertEquals( 122 $this->assertEquals(
104 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.', 123 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
105 NetscapeBookmarkUtils::import(NULL, $files, NULL, $this->conf) 124 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
106 ); 125 );
107 $this->assertEquals(0, count($this->linkDb)); 126 $this->assertEquals(0, count($this->linkDb));
108 } 127 }
@@ -116,7 +135,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
116 $this->assertEquals( 135 $this->assertEquals(
117 'File internet_explorer_encoding.htm (356 bytes) was successfully processed:' 136 'File internet_explorer_encoding.htm (356 bytes) was successfully processed:'
118 .' 1 links imported, 0 links overwritten, 0 links skipped.', 137 .' 1 links imported, 0 links overwritten, 0 links skipped.',
119 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf) 138 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
120 ); 139 );
121 $this->assertEquals(1, count($this->linkDb)); 140 $this->assertEquals(1, count($this->linkDb));
122 $this->assertEquals(0, count_private($this->linkDb)); 141 $this->assertEquals(0, count_private($this->linkDb));
@@ -145,7 +164,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
145 $this->assertEquals( 164 $this->assertEquals(
146 'File netscape_nested.htm (1337 bytes) was successfully processed:' 165 'File netscape_nested.htm (1337 bytes) was successfully processed:'
147 .' 8 links imported, 0 links overwritten, 0 links skipped.', 166 .' 8 links imported, 0 links overwritten, 0 links skipped.',
148 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf) 167 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
149 ); 168 );
150 $this->assertEquals(8, count($this->linkDb)); 169 $this->assertEquals(8, count($this->linkDb));
151 $this->assertEquals(2, count_private($this->linkDb)); 170 $this->assertEquals(2, count_private($this->linkDb));
@@ -267,7 +286,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
267 $this->assertEquals( 286 $this->assertEquals(
268 'File netscape_basic.htm (482 bytes) was successfully processed:' 287 'File netscape_basic.htm (482 bytes) was successfully processed:'
269 .' 2 links imported, 0 links overwritten, 0 links skipped.', 288 .' 2 links imported, 0 links overwritten, 0 links skipped.',
270 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf) 289 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
271 ); 290 );
272 291
273 $this->assertEquals(2, count($this->linkDb)); 292 $this->assertEquals(2, count($this->linkDb));
@@ -312,7 +331,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
312 $this->assertEquals( 331 $this->assertEquals(
313 'File netscape_basic.htm (482 bytes) was successfully processed:' 332 'File netscape_basic.htm (482 bytes) was successfully processed:'
314 .' 2 links imported, 0 links overwritten, 0 links skipped.', 333 .' 2 links imported, 0 links overwritten, 0 links skipped.',
315 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 334 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
316 ); 335 );
317 $this->assertEquals(2, count($this->linkDb)); 336 $this->assertEquals(2, count($this->linkDb));
318 $this->assertEquals(1, count_private($this->linkDb)); 337 $this->assertEquals(1, count_private($this->linkDb));
@@ -356,7 +375,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
356 $this->assertEquals( 375 $this->assertEquals(
357 'File netscape_basic.htm (482 bytes) was successfully processed:' 376 'File netscape_basic.htm (482 bytes) was successfully processed:'
358 .' 2 links imported, 0 links overwritten, 0 links skipped.', 377 .' 2 links imported, 0 links overwritten, 0 links skipped.',
359 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 378 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
360 ); 379 );
361 $this->assertEquals(2, count($this->linkDb)); 380 $this->assertEquals(2, count($this->linkDb));
362 $this->assertEquals(0, count_private($this->linkDb)); 381 $this->assertEquals(0, count_private($this->linkDb));
@@ -380,7 +399,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
380 $this->assertEquals( 399 $this->assertEquals(
381 'File netscape_basic.htm (482 bytes) was successfully processed:' 400 'File netscape_basic.htm (482 bytes) was successfully processed:'
382 .' 2 links imported, 0 links overwritten, 0 links skipped.', 401 .' 2 links imported, 0 links overwritten, 0 links skipped.',
383 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 402 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
384 ); 403 );
385 $this->assertEquals(2, count($this->linkDb)); 404 $this->assertEquals(2, count($this->linkDb));
386 $this->assertEquals(2, count_private($this->linkDb)); 405 $this->assertEquals(2, count_private($this->linkDb));
@@ -406,7 +425,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
406 $this->assertEquals( 425 $this->assertEquals(
407 'File netscape_basic.htm (482 bytes) was successfully processed:' 426 'File netscape_basic.htm (482 bytes) was successfully processed:'
408 .' 2 links imported, 0 links overwritten, 0 links skipped.', 427 .' 2 links imported, 0 links overwritten, 0 links skipped.',
409 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 428 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
410 ); 429 );
411 $this->assertEquals(2, count($this->linkDb)); 430 $this->assertEquals(2, count($this->linkDb));
412 $this->assertEquals(2, count_private($this->linkDb)); 431 $this->assertEquals(2, count_private($this->linkDb));
@@ -426,7 +445,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
426 $this->assertEquals( 445 $this->assertEquals(
427 'File netscape_basic.htm (482 bytes) was successfully processed:' 446 'File netscape_basic.htm (482 bytes) was successfully processed:'
428 .' 2 links imported, 2 links overwritten, 0 links skipped.', 447 .' 2 links imported, 2 links overwritten, 0 links skipped.',
429 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 448 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
430 ); 449 );
431 $this->assertEquals(2, count($this->linkDb)); 450 $this->assertEquals(2, count($this->linkDb));
432 $this->assertEquals(0, count_private($this->linkDb)); 451 $this->assertEquals(0, count_private($this->linkDb));
@@ -452,7 +471,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
452 $this->assertEquals( 471 $this->assertEquals(
453 'File netscape_basic.htm (482 bytes) was successfully processed:' 472 'File netscape_basic.htm (482 bytes) was successfully processed:'
454 .' 2 links imported, 0 links overwritten, 0 links skipped.', 473 .' 2 links imported, 0 links overwritten, 0 links skipped.',
455 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 474 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
456 ); 475 );
457 $this->assertEquals(2, count($this->linkDb)); 476 $this->assertEquals(2, count($this->linkDb));
458 $this->assertEquals(0, count_private($this->linkDb)); 477 $this->assertEquals(0, count_private($this->linkDb));
@@ -473,7 +492,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
473 $this->assertEquals( 492 $this->assertEquals(
474 'File netscape_basic.htm (482 bytes) was successfully processed:' 493 'File netscape_basic.htm (482 bytes) was successfully processed:'
475 .' 2 links imported, 2 links overwritten, 0 links skipped.', 494 .' 2 links imported, 2 links overwritten, 0 links skipped.',
476 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 495 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
477 ); 496 );
478 $this->assertEquals(2, count($this->linkDb)); 497 $this->assertEquals(2, count($this->linkDb));
479 $this->assertEquals(2, count_private($this->linkDb)); 498 $this->assertEquals(2, count_private($this->linkDb));
@@ -497,7 +516,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
497 $this->assertEquals( 516 $this->assertEquals(
498 'File netscape_basic.htm (482 bytes) was successfully processed:' 517 'File netscape_basic.htm (482 bytes) was successfully processed:'
499 .' 2 links imported, 0 links overwritten, 0 links skipped.', 518 .' 2 links imported, 0 links overwritten, 0 links skipped.',
500 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 519 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
501 ); 520 );
502 $this->assertEquals(2, count($this->linkDb)); 521 $this->assertEquals(2, count($this->linkDb));
503 $this->assertEquals(0, count_private($this->linkDb)); 522 $this->assertEquals(0, count_private($this->linkDb));
@@ -507,7 +526,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
507 $this->assertEquals( 526 $this->assertEquals(
508 'File netscape_basic.htm (482 bytes) was successfully processed:' 527 'File netscape_basic.htm (482 bytes) was successfully processed:'
509 .' 0 links imported, 0 links overwritten, 2 links skipped.', 528 .' 0 links imported, 0 links overwritten, 2 links skipped.',
510 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 529 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
511 ); 530 );
512 $this->assertEquals(2, count($this->linkDb)); 531 $this->assertEquals(2, count($this->linkDb));
513 $this->assertEquals(0, count_private($this->linkDb)); 532 $this->assertEquals(0, count_private($this->linkDb));
@@ -526,7 +545,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
526 $this->assertEquals( 545 $this->assertEquals(
527 'File netscape_basic.htm (482 bytes) was successfully processed:' 546 'File netscape_basic.htm (482 bytes) was successfully processed:'
528 .' 2 links imported, 0 links overwritten, 0 links skipped.', 547 .' 2 links imported, 0 links overwritten, 0 links skipped.',
529 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 548 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
530 ); 549 );
531 $this->assertEquals(2, count($this->linkDb)); 550 $this->assertEquals(2, count($this->linkDb));
532 $this->assertEquals(0, count_private($this->linkDb)); 551 $this->assertEquals(0, count_private($this->linkDb));
@@ -553,7 +572,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
553 $this->assertEquals( 572 $this->assertEquals(
554 'File netscape_basic.htm (482 bytes) was successfully processed:' 573 'File netscape_basic.htm (482 bytes) was successfully processed:'
555 .' 2 links imported, 0 links overwritten, 0 links skipped.', 574 .' 2 links imported, 0 links overwritten, 0 links skipped.',
556 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 575 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
557 ); 576 );
558 $this->assertEquals(2, count($this->linkDb)); 577 $this->assertEquals(2, count($this->linkDb));
559 $this->assertEquals(0, count_private($this->linkDb)); 578 $this->assertEquals(0, count_private($this->linkDb));
@@ -578,7 +597,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
578 $this->assertEquals( 597 $this->assertEquals(
579 'File same_date.htm (453 bytes) was successfully processed:' 598 'File same_date.htm (453 bytes) was successfully processed:'
580 .' 3 links imported, 0 links overwritten, 0 links skipped.', 599 .' 3 links imported, 0 links overwritten, 0 links skipped.',
581 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf) 600 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history)
582 ); 601 );
583 $this->assertEquals(3, count($this->linkDb)); 602 $this->assertEquals(3, count($this->linkDb));
584 $this->assertEquals(0, count_private($this->linkDb)); 603 $this->assertEquals(0, count_private($this->linkDb));
@@ -595,4 +614,32 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
595 $this->linkDb[2]['id'] 614 $this->linkDb[2]['id']
596 ); 615 );
597 } 616 }
617
618 public function testImportCreateUpdateHistory()
619 {
620 $post = [
621 'privacy' => 'public',
622 'overwrite' => 'true',
623 ];
624 $files = file2array('netscape_basic.htm');
625 $nbLinks = 2;
626 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
627 $history = $this->history->getHistory();
628 $this->assertEquals($nbLinks, count($history));
629 foreach ($history as $value) {
630 $this->assertEquals(History::CREATED, $value['event']);
631 $this->assertTrue(new DateTime('-5 seconds') < $value['datetime']);
632 $this->assertTrue(is_int($value['id']));
633 }
634
635 // re-import as private, enable overwriting
636 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
637 $history = $this->history->getHistory();
638 $this->assertEquals($nbLinks * 2, count($history));
639 for ($i = 0 ; $i < $nbLinks ; $i++) {
640 $this->assertEquals(History::UPDATED, $history[$i]['event']);
641 $this->assertTrue(new DateTime('-5 seconds') < $history[$i]['datetime']);
642 $this->assertTrue(is_int($history[$i]['id']));
643 }
644 }
598} 645}
diff --git a/tests/TimeZoneTest.php b/tests/TimeZoneTest.php
index 2976d116..127fdc19 100644
--- a/tests/TimeZoneTest.php
+++ b/tests/TimeZoneTest.php
@@ -11,24 +11,45 @@ require_once 'application/TimeZone.php';
11class TimeZoneTest extends PHPUnit_Framework_TestCase 11class TimeZoneTest extends PHPUnit_Framework_TestCase
12{ 12{
13 /** 13 /**
14 * @var array of timezones
15 */
16 protected $installedTimezones;
17
18 public function setUp()
19 {
20 $this->installedTimezones = [
21 'Antarctica/Syowa',
22 'Europe/London',
23 'Europe/Paris',
24 'UTC'
25 ];
26 }
27
28 /**
14 * Generate a timezone selection form 29 * Generate a timezone selection form
15 */ 30 */
16 public function testGenerateTimeZoneForm() 31 public function testGenerateTimeZoneForm()
17 { 32 {
18 $generated = generateTimeZoneForm(); 33 $expected = [
34 'continents' => [
35 'Antarctica',
36 'Europe',
37 'UTC',
38 'selected' => '',
39 ],
40 'cities' => [
41 ['continent' => 'Antarctica', 'city' => 'Syowa'],
42 ['continent' => 'Europe', 'city' => 'London'],
43 ['continent' => 'Europe', 'city' => 'Paris'],
44 ['continent' => 'UTC', 'city' => 'UTC'],
45 'selected' => '',
46 ]
47 ];
19 48
20 // HTML form 49 list($continents, $cities) = generateTimeZoneData($this->installedTimezones);
21 $this->assertStringStartsWith('Continent:<select', $generated[0]);
22 $this->assertContains('selected="selected"', $generated[0]);
23 $this->assertStringEndsWith('</select><br />', $generated[0]);
24 50
25 // Javascript handler 51 $this->assertEquals($expected['continents'], $continents);
26 $this->assertStringStartsWith('<script>', $generated[1]); 52 $this->assertEquals($expected['cities'], $cities);
27 $this->assertContains(
28 '<option value=\"Bermuda\">Bermuda<\/option>',
29 $generated[1]
30 );
31 $this->assertStringEndsWith('</script>', $generated[1]);
32 } 53 }
33 54
34 /** 55 /**
@@ -36,28 +57,26 @@ class TimeZoneTest extends PHPUnit_Framework_TestCase
36 */ 57 */
37 public function testGenerateTimeZoneFormPreselected() 58 public function testGenerateTimeZoneFormPreselected()
38 { 59 {
39 $generated = generateTimeZoneForm('Antarctica/Syowa'); 60 $expected = [
40 61 'continents' => [
41 // HTML form 62 'Antarctica',
42 $this->assertStringStartsWith('Continent:<select', $generated[0]); 63 'Europe',
43 $this->assertContains( 64 'UTC',
44 'value="Antarctica" selected="selected"', 65 'selected' => 'Antarctica',
45 $generated[0] 66 ],
46 ); 67 'cities' => [
47 $this->assertContains( 68 ['continent' => 'Antarctica', 'city' => 'Syowa'],
48 'value="Syowa" selected="selected"', 69 ['continent' => 'Europe', 'city' => 'London'],
49 $generated[0] 70 ['continent' => 'Europe', 'city' => 'Paris'],
50 ); 71 ['continent' => 'UTC', 'city' => 'UTC'],
51 $this->assertStringEndsWith('</select><br />', $generated[0]); 72 'selected' => 'Syowa',
73 ]
74 ];
52 75
76 list($continents, $cities) = generateTimeZoneData($this->installedTimezones, 'Antarctica/Syowa');
53 77
54 // Javascript handler 78 $this->assertEquals($expected['continents'], $continents);
55 $this->assertStringStartsWith('<script>', $generated[1]); 79 $this->assertEquals($expected['cities'], $cities);
56 $this->assertContains(
57 '<option value=\"Bermuda\">Bermuda<\/option>',
58 $generated[1]
59 );
60 $this->assertStringEndsWith('</script>', $generated[1]);
61 } 80 }
62 81
63 /** 82 /**
diff --git a/tests/Updater/UpdaterTest.php b/tests/Updater/UpdaterTest.php
index 11b6444a..fed175df 100644
--- a/tests/Updater/UpdaterTest.php
+++ b/tests/Updater/UpdaterTest.php
@@ -470,46 +470,6 @@ $GLOBALS[\'privateLinkByDefault\'] = true;';
470 } 470 }
471 471
472 /** 472 /**
473 * Test updateMethodDefaultThemeVintage with the default theme enabled.
474 */
475 public function testSetDefaultThemeToVintage()
476 {
477 $sandboxConf = 'sandbox/config';
478 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
479 $this->conf = new ConfigManager($sandboxConf);
480
481 $this->conf->set('resource.theme', 'default');
482 $updater = new Updater([], [], $this->conf, true);
483 $this->assertTrue($updater->updateMethodDefaultThemeVintage());
484 $this->assertEquals('vintage', $this->conf->get('resource.theme'));
485
486 // reload from file
487 $this->conf = new ConfigManager($sandboxConf);
488 $this->assertEquals('vintage', $this->conf->get('resource.theme'));
489 }
490
491 /**
492 * Test updateMethodDefaultThemeVintage with custom theme enabled => nothing to do.
493 */
494 public function testSetDefaultThemeNothingToDo()
495 {
496 $sandboxConf = 'sandbox/config';
497 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
498 $this->conf = new ConfigManager($sandboxConf);
499
500 $theme = 'myawesometheme';
501 $this->conf->set('resource.theme', $theme);
502 $this->conf->write(true);
503 $updater = new Updater([], [], $this->conf, true);
504 $this->assertTrue($updater->updateMethodDefaultThemeVintage());
505 $this->assertEquals($theme, $this->conf->get('resource.theme'));
506
507 // reload from file
508 $this->conf = new ConfigManager($sandboxConf);
509 $this->assertEquals($theme, $this->conf->get('resource.theme'));
510 }
511
512 /**
513 * Test updateMethodEscapeMarkdown with markdown plugin enabled 473 * Test updateMethodEscapeMarkdown with markdown plugin enabled
514 * => setting markdown_escape set to false. 474 * => setting markdown_escape set to false.
515 */ 475 */
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index e70cc1ae..3d1aa653 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -4,6 +4,7 @@
4 */ 4 */
5 5
6require_once 'application/Utils.php'; 6require_once 'application/Utils.php';
7require_once 'application/Languages.php';
7require_once 'tests/utils/ReferenceSessionIdHashes.php'; 8require_once 'tests/utils/ReferenceSessionIdHashes.php';
8 9
9// Initialize reference data before PHPUnit starts a session 10// Initialize reference data before PHPUnit starts a session
@@ -326,4 +327,206 @@ class UtilsTest extends PHPUnit_Framework_TestCase
326 $this->assertFalse(format_date([])); 327 $this->assertFalse(format_date([]));
327 $this->assertFalse(format_date(null)); 328 $this->assertFalse(format_date(null));
328 } 329 }
330
331 /**
332 * Test is_integer_mixed with valid values
333 */
334 public function testIsIntegerMixedValid()
335 {
336 $this->assertTrue(is_integer_mixed(12));
337 $this->assertTrue(is_integer_mixed('12'));
338 $this->assertTrue(is_integer_mixed(-12));
339 $this->assertTrue(is_integer_mixed('-12'));
340 $this->assertTrue(is_integer_mixed(0));
341 $this->assertTrue(is_integer_mixed('0'));
342 $this->assertTrue(is_integer_mixed(0x0a));
343 }
344
345 /**
346 * Test is_integer_mixed with invalid values
347 */
348 public function testIsIntegerMixedInvalid()
349 {
350 $this->assertFalse(is_integer_mixed(true));
351 $this->assertFalse(is_integer_mixed(false));
352 $this->assertFalse(is_integer_mixed([]));
353 $this->assertFalse(is_integer_mixed(['test']));
354 $this->assertFalse(is_integer_mixed([12]));
355 $this->assertFalse(is_integer_mixed(new DateTime()));
356 $this->assertFalse(is_integer_mixed('0x0a'));
357 $this->assertFalse(is_integer_mixed('12k'));
358 $this->assertFalse(is_integer_mixed('k12'));
359 $this->assertFalse(is_integer_mixed(''));
360 }
361
362 /**
363 * Test return_bytes
364 */
365 public function testReturnBytes()
366 {
367 $this->assertEquals(2 * 1024, return_bytes('2k'));
368 $this->assertEquals(2 * 1024, return_bytes('2K'));
369 $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2m'));
370 $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2M'));
371 $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2g'));
372 $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2G'));
373 $this->assertEquals(374, return_bytes('374'));
374 $this->assertEquals(374, return_bytes(374));
375 $this->assertEquals(0, return_bytes('0'));
376 $this->assertEquals(0, return_bytes(0));
377 $this->assertEquals(-1, return_bytes('-1'));
378 $this->assertEquals(-1, return_bytes(-1));
379 $this->assertEquals('', return_bytes(''));
380 }
381
382 /**
383 * Test human_bytes
384 */
385 public function testHumanBytes()
386 {
387 $this->assertEquals('2kiB', human_bytes(2 * 1024));
388 $this->assertEquals('2kiB', human_bytes(strval(2 * 1024)));
389 $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2))));
390 $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2)))));
391 $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3))));
392 $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3)))));
393 $this->assertEquals('374B', human_bytes(374));
394 $this->assertEquals('374B', human_bytes('374'));
395 $this->assertEquals('232kiB', human_bytes(237481));
396 $this->assertEquals('Unlimited', human_bytes('0'));
397 $this->assertEquals('Unlimited', human_bytes(0));
398 $this->assertEquals('Setting not set', human_bytes(''));
399 }
400
401 /**
402 * Test get_max_upload_size with formatting
403 */
404 public function testGetMaxUploadSize()
405 {
406 $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k'));
407 $this->assertEquals('1MiB', get_max_upload_size('1m', '2m'));
408 $this->assertEquals('100B', get_max_upload_size(100, 100));
409 }
410
411 /**
412 * Test get_max_upload_size without formatting
413 */
414 public function testGetMaxUploadSizeRaw()
415 {
416 $this->assertEquals('1048576', get_max_upload_size(2097152, '1024k', false));
417 $this->assertEquals('1048576', get_max_upload_size('1m', '2m', false));
418 $this->assertEquals('100', get_max_upload_size(100, 100, false));
419 }
420
421 /**
422 * Test alphabetical_sort by value, not reversed, with php-intl.
423 */
424 public function testAlphabeticalSortByValue()
425 {
426 $arr = [
427 'zZz',
428 'éee',
429 'éae',
430 'eee',
431 'A',
432 'a',
433 'zzz',
434 ];
435 $expected = [
436 'a',
437 'A',
438 'éae',
439 'eee',
440 'éee',
441 'zzz',
442 'zZz',
443 ];
444
445 alphabetical_sort($arr);
446 $this->assertEquals($expected, $arr);
447 }
448
449 /**
450 * Test alphabetical_sort by value, reversed, with php-intl.
451 */
452 public function testAlphabeticalSortByValueReversed()
453 {
454 $arr = [
455 'zZz',
456 'éee',
457 'éae',
458 'eee',
459 'A',
460 'a',
461 'zzz',
462 ];
463 $expected = [
464 'zZz',
465 'zzz',
466 'éee',
467 'eee',
468 'éae',
469 'A',
470 'a',
471 ];
472
473 alphabetical_sort($arr, true);
474 $this->assertEquals($expected, $arr);
475 }
476
477 /**
478 * Test alphabetical_sort by keys, not reversed, with php-intl.
479 */
480 public function testAlphabeticalSortByKeys()
481 {
482 $arr = [
483 'zZz' => true,
484 'éee' => true,
485 'éae' => true,
486 'eee' => true,
487 'A' => true,
488 'a' => true,
489 'zzz' => true,
490 ];
491 $expected = [
492 'a' => true,
493 'A' => true,
494 'éae' => true,
495 'eee' => true,
496 'éee' => true,
497 'zzz' => true,
498 'zZz' => true,
499 ];
500
501 alphabetical_sort($arr, true, true);
502 $this->assertEquals($expected, $arr);
503 }
504
505 /**
506 * Test alphabetical_sort by keys, reversed, with php-intl.
507 */
508 public function testAlphabeticalSortByKeysReversed()
509 {
510 $arr = [
511 'zZz' => true,
512 'éee' => true,
513 'éae' => true,
514 'eee' => true,
515 'A' => true,
516 'a' => true,
517 'zzz' => true,
518 ];
519 $expected = [
520 'zZz' => true,
521 'zzz' => true,
522 'éee' => true,
523 'eee' => true,
524 'éae' => true,
525 'A' => true,
526 'a' => true,
527 ];
528
529 alphabetical_sort($arr, true, true);
530 $this->assertEquals($expected, $arr);
531 }
329} 532}
diff --git a/tests/api/ApiUtilsTest.php b/tests/api/ApiUtilsTest.php
index b4431d1b..62baf4c5 100644
--- a/tests/api/ApiUtilsTest.php
+++ b/tests/api/ApiUtilsTest.php
@@ -271,4 +271,82 @@ class ApiUtilsTest extends \PHPUnit_Framework_TestCase
271 271
272 $this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); 272 $this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl));
273 } 273 }
274
275 /**
276 * Test updateLink with valid data, and also unnecessary fields.
277 */
278 public function testUpdateLink()
279 {
280 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
281 $old = [
282 'id' => 12,
283 'url' => '?abc',
284 'shorturl' => 'abc',
285 'title' => 'Note',
286 'description' => '',
287 'tags' => '',
288 'private' => '',
289 'created' => $created,
290 ];
291
292 $new = [
293 'id' => 13,
294 'shorturl' => 'nope',
295 'url' => 'http://somewhere.else',
296 'title' => 'Le Cid',
297 'description' => 'Percé jusques au fond du cœur [...]',
298 'tags' => 'corneille rodrigue',
299 'private' => true,
300 'created' => 'creation',
301 'updated' => 'updation',
302 ];
303
304 $result = ApiUtils::updateLink($old, $new);
305 $this->assertEquals(12, $result['id']);
306 $this->assertEquals('http://somewhere.else', $result['url']);
307 $this->assertEquals('abc', $result['shorturl']);
308 $this->assertEquals('Le Cid', $result['title']);
309 $this->assertEquals('Percé jusques au fond du cœur [...]', $result['description']);
310 $this->assertEquals('corneille rodrigue', $result['tags']);
311 $this->assertEquals(true, $result['private']);
312 $this->assertEquals($created, $result['created']);
313 $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']);
314 }
315
316 /**
317 * Test updateLink with minimal data.
318 */
319 public function testUpdateLinkMinimal()
320 {
321 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
322 $old = [
323 'id' => 12,
324 'url' => '?abc',
325 'shorturl' => 'abc',
326 'title' => 'Note',
327 'description' => 'Interesting description!',
328 'tags' => 'doggo',
329 'private' => true,
330 'created' => $created,
331 ];
332
333 $new = [
334 'url' => '',
335 'title' => '',
336 'description' => '',
337 'tags' => '',
338 'private' => false,
339 ];
340
341 $result = ApiUtils::updateLink($old, $new);
342 $this->assertEquals(12, $result['id']);
343 $this->assertEquals('?abc', $result['url']);
344 $this->assertEquals('abc', $result['shorturl']);
345 $this->assertEquals('?abc', $result['title']);
346 $this->assertEquals('', $result['description']);
347 $this->assertEquals('', $result['tags']);
348 $this->assertEquals(false, $result['private']);
349 $this->assertEquals($created, $result['created']);
350 $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']);
351 }
274} 352}
diff --git a/tests/api/controllers/DeleteLinkTest.php b/tests/api/controllers/DeleteLinkTest.php
new file mode 100644
index 00000000..7d797137
--- /dev/null
+++ b/tests/api/controllers/DeleteLinkTest.php
@@ -0,0 +1,126 @@
1<?php
2
3
4namespace Shaarli\Api\Controllers;
5
6use Shaarli\Config\ConfigManager;
7use Slim\Container;
8use Slim\Http\Environment;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12class DeleteLinkTest extends \PHPUnit_Framework_TestCase
13{
14 /**
15 * @var string datastore to test write operations
16 */
17 protected static $testDatastore = 'sandbox/datastore.php';
18
19 /**
20 * @var string datastore to test write operations
21 */
22 protected static $testHistory = 'sandbox/history.php';
23
24 /**
25 * @var ConfigManager instance
26 */
27 protected $conf;
28
29 /**
30 * @var \ReferenceLinkDB instance.
31 */
32 protected $refDB = null;
33
34 /**
35 * @var \LinkDB instance.
36 */
37 protected $linkDB;
38
39 /**
40 * @var \History instance.
41 */
42 protected $history;
43
44 /**
45 * @var Container instance.
46 */
47 protected $container;
48
49 /**
50 * @var Links controller instance.
51 */
52 protected $controller;
53
54 /**
55 * Before each test, instantiate a new Api with its config, plugins and links.
56 */
57 public function setUp()
58 {
59 $this->conf = new ConfigManager('tests/utils/config/configJson');
60 $this->refDB = new \ReferenceLinkDB();
61 $this->refDB->write(self::$testDatastore);
62 $this->linkDB = new \LinkDB(self::$testDatastore, true, false);
63 $refHistory = new \ReferenceHistory();
64 $refHistory->write(self::$testHistory);
65 $this->history = new \History(self::$testHistory);
66 $this->container = new Container();
67 $this->container['conf'] = $this->conf;
68 $this->container['db'] = $this->linkDB;
69 $this->container['history'] = $this->history;
70
71 $this->controller = new Links($this->container);
72 }
73
74 /**
75 * After each test, remove the test datastore.
76 */
77 public function tearDown()
78 {
79 @unlink(self::$testDatastore);
80 @unlink(self::$testHistory);
81 }
82
83 /**
84 * Test DELETE link endpoint: the link should be removed.
85 */
86 public function testDeleteLinkValid()
87 {
88 $id = '41';
89 $this->assertTrue(isset($this->linkDB[$id]));
90 $env = Environment::mock([
91 'REQUEST_METHOD' => 'DELETE',
92 ]);
93 $request = Request::createFromEnvironment($env);
94
95 $response = $this->controller->deleteLink($request, new Response(), ['id' => $id]);
96 $this->assertEquals(204, $response->getStatusCode());
97 $this->assertEmpty((string) $response->getBody());
98
99 $this->linkDB = new \LinkDB(self::$testDatastore, true, false);
100 $this->assertFalse(isset($this->linkDB[$id]));
101
102 $historyEntry = $this->history->getHistory()[0];
103 $this->assertEquals(\History::DELETED, $historyEntry['event']);
104 $this->assertTrue(
105 (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime']
106 );
107 $this->assertEquals($id, $historyEntry['id']);
108 }
109
110 /**
111 * Test DELETE link endpoint: reach not existing ID.
112 *
113 * @expectedException Shaarli\Api\Exceptions\ApiLinkNotFoundException
114 */
115 public function testDeleteLink404()
116 {
117 $id = -1;
118 $this->assertFalse(isset($this->linkDB[$id]));
119 $env = Environment::mock([
120 'REQUEST_METHOD' => 'DELETE',
121 ]);
122 $request = Request::createFromEnvironment($env);
123
124 $this->controller->deleteLink($request, new Response(), ['id' => $id]);
125 }
126}
diff --git a/tests/api/controllers/GetLinkIdTest.php b/tests/api/controllers/GetLinkIdTest.php
index 45b18e6a..57528d5a 100644
--- a/tests/api/controllers/GetLinkIdTest.php
+++ b/tests/api/controllers/GetLinkIdTest.php
@@ -62,6 +62,7 @@ class GetLinkIdTest extends \PHPUnit_Framework_TestCase
62 $this->container = new Container(); 62 $this->container = new Container();
63 $this->container['conf'] = $this->conf; 63 $this->container['conf'] = $this->conf;
64 $this->container['db'] = new \LinkDB(self::$testDatastore, true, false); 64 $this->container['db'] = new \LinkDB(self::$testDatastore, true, false);
65 $this->container['history'] = null;
65 66
66 $this->controller = new Links($this->container); 67 $this->controller = new Links($this->container);
67 } 68 }
diff --git a/tests/api/controllers/GetLinksTest.php b/tests/api/controllers/GetLinksTest.php
index 10330cd9..84ae7f7a 100644
--- a/tests/api/controllers/GetLinksTest.php
+++ b/tests/api/controllers/GetLinksTest.php
@@ -61,6 +61,7 @@ class GetLinksTest extends \PHPUnit_Framework_TestCase
61 $this->container = new Container(); 61 $this->container = new Container();
62 $this->container['conf'] = $this->conf; 62 $this->container['conf'] = $this->conf;
63 $this->container['db'] = new \LinkDB(self::$testDatastore, true, false); 63 $this->container['db'] = new \LinkDB(self::$testDatastore, true, false);
64 $this->container['history'] = null;
64 65
65 $this->controller = new Links($this->container); 66 $this->controller = new Links($this->container);
66 } 67 }
diff --git a/tests/api/controllers/HistoryTest.php b/tests/api/controllers/HistoryTest.php
new file mode 100644
index 00000000..61046d97
--- /dev/null
+++ b/tests/api/controllers/HistoryTest.php
@@ -0,0 +1,216 @@
1<?php
2
3
4namespace Shaarli\Api\Controllers;
5
6
7use Shaarli\Config\ConfigManager;
8use Slim\Container;
9use Slim\Http\Environment;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13require_once 'tests/utils/ReferenceHistory.php';
14
15class HistoryTest extends \PHPUnit_Framework_TestCase
16{
17 /**
18 * @var string datastore to test write operations
19 */
20 protected static $testHistory = 'sandbox/history.php';
21
22 /**
23 * @var ConfigManager instance
24 */
25 protected $conf;
26
27 /**
28 * @var \ReferenceHistory instance.
29 */
30 protected $refHistory = null;
31
32 /**
33 * @var Container instance.
34 */
35 protected $container;
36
37 /**
38 * @var History controller instance.
39 */
40 protected $controller;
41
42 /**
43 * Before every test, instantiate a new Api with its config, plugins and links.
44 */
45 public function setUp()
46 {
47 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php');
48 $this->refHistory = new \ReferenceHistory();
49 $this->refHistory->write(self::$testHistory);
50 $this->container = new Container();
51 $this->container['conf'] = $this->conf;
52 $this->container['db'] = true;
53 $this->container['history'] = new \History(self::$testHistory);
54
55 $this->controller = new History($this->container);
56 }
57
58 /**
59 * After every test, remove the test datastore.
60 */
61 public function tearDown()
62 {
63 @unlink(self::$testHistory);
64 }
65
66 /**
67 * Test /history service without parameter.
68 */
69 public function testGetHistory()
70 {
71 $env = Environment::mock([
72 'REQUEST_METHOD' => 'GET',
73 ]);
74 $request = Request::createFromEnvironment($env);
75
76 $response = $this->controller->getHistory($request, new Response());
77 $this->assertEquals(200, $response->getStatusCode());
78 $data = json_decode((string) $response->getBody(), true);
79
80 $this->assertEquals($this->refHistory->count(), count($data));
81
82 $this->assertEquals(\History::DELETED, $data[0]['event']);
83 $this->assertEquals(
84 \DateTime::createFromFormat('Ymd_His', '20170303_121216')->format(\DateTime::ATOM),
85 $data[0]['datetime']
86 );
87 $this->assertEquals(124, $data[0]['id']);
88
89 $this->assertEquals(\History::SETTINGS, $data[1]['event']);
90 $this->assertEquals(
91 \DateTime::createFromFormat('Ymd_His', '20170302_121215')->format(\DateTime::ATOM),
92 $data[1]['datetime']
93 );
94 $this->assertNull($data[1]['id']);
95
96 $this->assertEquals(\History::UPDATED, $data[2]['event']);
97 $this->assertEquals(
98 \DateTime::createFromFormat('Ymd_His', '20170301_121214')->format(\DateTime::ATOM),
99 $data[2]['datetime']
100 );
101 $this->assertEquals(123, $data[2]['id']);
102
103 $this->assertEquals(\History::CREATED, $data[3]['event']);
104 $this->assertEquals(
105 \DateTime::createFromFormat('Ymd_His', '20170201_121214')->format(\DateTime::ATOM),
106 $data[3]['datetime']
107 );
108 $this->assertEquals(124, $data[3]['id']);
109
110 $this->assertEquals(\History::CREATED, $data[4]['event']);
111 $this->assertEquals(
112 \DateTime::createFromFormat('Ymd_His', '20170101_121212')->format(\DateTime::ATOM),
113 $data[4]['datetime']
114 );
115 $this->assertEquals(123, $data[4]['id']);
116 }
117
118 /**
119 * Test /history service with limit parameter.
120 */
121 public function testGetHistoryLimit()
122 {
123 $env = Environment::mock([
124 'REQUEST_METHOD' => 'GET',
125 'QUERY_STRING' => 'limit=1'
126 ]);
127 $request = Request::createFromEnvironment($env);
128
129 $response = $this->controller->getHistory($request, new Response());
130 $this->assertEquals(200, $response->getStatusCode());
131 $data = json_decode((string) $response->getBody(), true);
132
133 $this->assertEquals(1, count($data));
134
135 $this->assertEquals(\History::DELETED, $data[0]['event']);
136 $this->assertEquals(
137 \DateTime::createFromFormat('Ymd_His', '20170303_121216')->format(\DateTime::ATOM),
138 $data[0]['datetime']
139 );
140 $this->assertEquals(124, $data[0]['id']);
141 }
142
143 /**
144 * Test /history service with offset parameter.
145 */
146 public function testGetHistoryOffset()
147 {
148 $env = Environment::mock([
149 'REQUEST_METHOD' => 'GET',
150 'QUERY_STRING' => 'offset=4'
151 ]);
152 $request = Request::createFromEnvironment($env);
153
154 $response = $this->controller->getHistory($request, new Response());
155 $this->assertEquals(200, $response->getStatusCode());
156 $data = json_decode((string) $response->getBody(), true);
157
158 $this->assertEquals(1, count($data));
159
160 $this->assertEquals(\History::CREATED, $data[0]['event']);
161 $this->assertEquals(
162 \DateTime::createFromFormat('Ymd_His', '20170101_121212')->format(\DateTime::ATOM),
163 $data[0]['datetime']
164 );
165 $this->assertEquals(123, $data[0]['id']);
166 }
167
168 /**
169 * Test /history service with since parameter.
170 */
171 public function testGetHistorySince()
172 {
173 $env = Environment::mock([
174 'REQUEST_METHOD' => 'GET',
175 'QUERY_STRING' => 'since=2017-03-03T00:00:00%2B00:00'
176 ]);
177 $request = Request::createFromEnvironment($env);
178
179 $response = $this->controller->getHistory($request, new Response());
180 $this->assertEquals(200, $response->getStatusCode());
181 $data = json_decode((string) $response->getBody(), true);
182
183 $this->assertEquals(1, count($data));
184
185 $this->assertEquals(\History::DELETED, $data[0]['event']);
186 $this->assertEquals(
187 \DateTime::createFromFormat('Ymd_His', '20170303_121216')->format(\DateTime::ATOM),
188 $data[0]['datetime']
189 );
190 $this->assertEquals(124, $data[0]['id']);
191 }
192
193 /**
194 * Test /history service with since parameter.
195 */
196 public function testGetHistorySinceOffsetLimit()
197 {
198 $env = Environment::mock([
199 'REQUEST_METHOD' => 'GET',
200 'QUERY_STRING' => 'since=2017-02-01T00:00:00%2B00:00&offset=1&limit=1'
201 ]);
202 $request = Request::createFromEnvironment($env);
203
204 $response = $this->controller->getHistory($request, new Response());
205 $this->assertEquals(200, $response->getStatusCode());
206 $data = json_decode((string) $response->getBody(), true);
207
208 $this->assertEquals(1, count($data));
209
210 $this->assertEquals(\History::SETTINGS, $data[0]['event']);
211 $this->assertEquals(
212 \DateTime::createFromFormat('Ymd_His', '20170302_121215')->format(\DateTime::ATOM),
213 $data[0]['datetime']
214 );
215 }
216}
diff --git a/tests/api/controllers/InfoTest.php b/tests/api/controllers/InfoTest.php
index 4beef3f7..e85eb281 100644
--- a/tests/api/controllers/InfoTest.php
+++ b/tests/api/controllers/InfoTest.php
@@ -54,6 +54,7 @@ class InfoTest extends \PHPUnit_Framework_TestCase
54 $this->container = new Container(); 54 $this->container = new Container();
55 $this->container['conf'] = $this->conf; 55 $this->container['conf'] = $this->conf;
56 $this->container['db'] = new \LinkDB(self::$testDatastore, true, false); 56 $this->container['db'] = new \LinkDB(self::$testDatastore, true, false);
57 $this->container['history'] = null;
57 58
58 $this->controller = new Info($this->container); 59 $this->controller = new Info($this->container);
59 } 60 }
diff --git a/tests/api/controllers/PostLinkTest.php b/tests/api/controllers/PostLinkTest.php
new file mode 100644
index 00000000..31954e39
--- /dev/null
+++ b/tests/api/controllers/PostLinkTest.php
@@ -0,0 +1,216 @@
1<?php
2
3namespace Shaarli\Api\Controllers;
4
5
6use Shaarli\Config\ConfigManager;
7use Slim\Container;
8use Slim\Http\Environment;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class PostLinkTest
14 *
15 * Test POST Link REST API service.
16 *
17 * @package Shaarli\Api\Controllers
18 */
19class PostLinkTest extends \PHPUnit_Framework_TestCase
20{
21 /**
22 * @var string datastore to test write operations
23 */
24 protected static $testDatastore = 'sandbox/datastore.php';
25
26 /**
27 * @var string datastore to test write operations
28 */
29 protected static $testHistory = 'sandbox/history.php';
30
31 /**
32 * @var ConfigManager instance
33 */
34 protected $conf;
35
36 /**
37 * @var \ReferenceLinkDB instance.
38 */
39 protected $refDB = null;
40
41 /**
42 * @var \History instance.
43 */
44 protected $history;
45
46 /**
47 * @var Container instance.
48 */
49 protected $container;
50
51 /**
52 * @var Links controller instance.
53 */
54 protected $controller;
55
56 /**
57 * Number of JSON field per link.
58 */
59 const NB_FIELDS_LINK = 9;
60
61 /**
62 * Before every test, instantiate a new Api with its config, plugins and links.
63 */
64 public function setUp()
65 {
66 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php');
67 $this->refDB = new \ReferenceLinkDB();
68 $this->refDB->write(self::$testDatastore);
69
70 $refHistory = new \ReferenceHistory();
71 $refHistory->write(self::$testHistory);
72 $this->history = new \History(self::$testHistory);
73
74 $this->container = new Container();
75 $this->container['conf'] = $this->conf;
76 $this->container['db'] = new \LinkDB(self::$testDatastore, true, false);
77 $this->container['history'] = new \History(self::$testHistory);
78
79 $this->controller = new Links($this->container);
80
81 $mock = $this->getMock('\Slim\Router', ['relativePathFor']);
82 $mock->expects($this->any())
83 ->method('relativePathFor')
84 ->willReturn('api/v1/links/1');
85
86 // affect @property-read... seems to work
87 $this->controller->getCi()->router = $mock;
88
89 // Used by index_url().
90 $this->controller->getCi()['environment'] = [
91 'SERVER_NAME' => 'domain.tld',
92 'SERVER_PORT' => 80,
93 'SCRIPT_NAME' => '/',
94 ];
95 }
96
97 /**
98 * After every test, remove the test datastore.
99 */
100 public function tearDown()
101 {
102 @unlink(self::$testDatastore);
103 @unlink(self::$testHistory);
104 }
105
106 /**
107 * Test link creation without any field: creates a blank note.
108 */
109 public function testPostLinkMinimal()
110 {
111 $env = Environment::mock([
112 'REQUEST_METHOD' => 'POST',
113 ]);
114
115 $request = Request::createFromEnvironment($env);
116
117 $response = $this->controller->postLink($request, new Response());
118 $this->assertEquals(201, $response->getStatusCode());
119 $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]);
120 $data = json_decode((string) $response->getBody(), true);
121 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
122 $this->assertEquals(43, $data['id']);
123 $this->assertRegExp('/[\w-_]{6}/', $data['shorturl']);
124 $this->assertEquals('http://domain.tld/?' . $data['shorturl'], $data['url']);
125 $this->assertEquals('?' . $data['shorturl'], $data['title']);
126 $this->assertEquals('', $data['description']);
127 $this->assertEquals([], $data['tags']);
128 $this->assertEquals(false, $data['private']);
129 $this->assertTrue(new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']));
130 $this->assertEquals('', $data['updated']);
131
132 $historyEntry = $this->history->getHistory()[0];
133 $this->assertEquals(\History::CREATED, $historyEntry['event']);
134 $this->assertTrue(
135 (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime']
136 );
137 $this->assertEquals(43, $historyEntry['id']);
138 }
139
140 /**
141 * Test link creation with all available fields.
142 */
143 public function testPostLinkFull()
144 {
145 $link = [
146 'url' => 'website.tld/test?foo=bar',
147 'title' => 'new entry',
148 'description' => 'shaare description',
149 'tags' => ['one', 'two'],
150 'private' => true,
151 ];
152 $env = Environment::mock([
153 'REQUEST_METHOD' => 'POST',
154 'CONTENT_TYPE' => 'application/json'
155 ]);
156
157 $request = Request::createFromEnvironment($env);
158 $request = $request->withParsedBody($link);
159 $response = $this->controller->postLink($request, new Response());
160
161 $this->assertEquals(201, $response->getStatusCode());
162 $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]);
163 $data = json_decode((string) $response->getBody(), true);
164 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
165 $this->assertEquals(43, $data['id']);
166 $this->assertRegExp('/[\w-_]{6}/', $data['shorturl']);
167 $this->assertEquals('http://' . $link['url'], $data['url']);
168 $this->assertEquals($link['title'], $data['title']);
169 $this->assertEquals($link['description'], $data['description']);
170 $this->assertEquals($link['tags'], $data['tags']);
171 $this->assertEquals(true, $data['private']);
172 $this->assertTrue(new \DateTime('2 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']));
173 $this->assertEquals('', $data['updated']);
174 }
175
176 /**
177 * Test link creation with an existing link (duplicate URL). Should return a 409 HTTP error and the existing link.
178 */
179 public function testPostLinkDuplicate()
180 {
181 $link = [
182 'url' => 'mediagoblin.org/',
183 'title' => 'new entry',
184 'description' => 'shaare description',
185 'tags' => ['one', 'two'],
186 'private' => true,
187 ];
188 $env = Environment::mock([
189 'REQUEST_METHOD' => 'POST',
190 'CONTENT_TYPE' => 'application/json'
191 ]);
192
193 $request = Request::createFromEnvironment($env);
194 $request = $request->withParsedBody($link);
195 $response = $this->controller->postLink($request, new Response());
196
197 $this->assertEquals(409, $response->getStatusCode());
198 $data = json_decode((string) $response->getBody(), true);
199 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
200 $this->assertEquals(7, $data['id']);
201 $this->assertEquals('IuWvgA', $data['shorturl']);
202 $this->assertEquals('http://mediagoblin.org/', $data['url']);
203 $this->assertEquals('MediaGoblin', $data['title']);
204 $this->assertEquals('A free software media publishing platform #hashtagOther', $data['description']);
205 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
206 $this->assertEquals(false, $data['private']);
207 $this->assertEquals(
208 \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130614_184135'),
209 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
210 );
211 $this->assertEquals(
212 \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130615_184230'),
213 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
214 );
215 }
216}
diff --git a/tests/api/controllers/PutLinkTest.php b/tests/api/controllers/PutLinkTest.php
new file mode 100644
index 00000000..8a562571
--- /dev/null
+++ b/tests/api/controllers/PutLinkTest.php
@@ -0,0 +1,222 @@
1<?php
2
3
4namespace Shaarli\Api\Controllers;
5
6
7use Shaarli\Config\ConfigManager;
8use Slim\Container;
9use Slim\Http\Environment;
10use Slim\Http\Request;
11use Slim\Http\Response;
12
13class PutLinkTest extends \PHPUnit_Framework_TestCase
14{
15 /**
16 * @var string datastore to test write operations
17 */
18 protected static $testDatastore = 'sandbox/datastore.php';
19
20 /**
21 * @var string datastore to test write operations
22 */
23 protected static $testHistory = 'sandbox/history.php';
24
25 /**
26 * @var ConfigManager instance
27 */
28 protected $conf;
29
30 /**
31 * @var \ReferenceLinkDB instance.
32 */
33 protected $refDB = null;
34
35 /**
36 * @var \History instance.
37 */
38 protected $history;
39
40 /**
41 * @var Container instance.
42 */
43 protected $container;
44
45 /**
46 * @var Links controller instance.
47 */
48 protected $controller;
49
50 /**
51 * Number of JSON field per link.
52 */
53 const NB_FIELDS_LINK = 9;
54
55 /**
56 * Before every test, instantiate a new Api with its config, plugins and links.
57 */
58 public function setUp()
59 {
60 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php');
61 $this->refDB = new \ReferenceLinkDB();
62 $this->refDB->write(self::$testDatastore);
63
64 $refHistory = new \ReferenceHistory();
65 $refHistory->write(self::$testHistory);
66 $this->history = new \History(self::$testHistory);
67
68 $this->container = new Container();
69 $this->container['conf'] = $this->conf;
70 $this->container['db'] = new \LinkDB(self::$testDatastore, true, false);
71 $this->container['history'] = new \History(self::$testHistory);
72
73 $this->controller = new Links($this->container);
74
75 // Used by index_url().
76 $this->controller->getCi()['environment'] = [
77 'SERVER_NAME' => 'domain.tld',
78 'SERVER_PORT' => 80,
79 'SCRIPT_NAME' => '/',
80 ];
81 }
82
83 /**
84 * After every test, remove the test datastore.
85 */
86 public function tearDown()
87 {
88 @unlink(self::$testDatastore);
89 @unlink(self::$testHistory);
90 }
91
92 /**
93 * Test link update without value: reset the link to default values
94 */
95 public function testPutLinkMinimal()
96 {
97 $env = Environment::mock([
98 'REQUEST_METHOD' => 'PUT',
99 ]);
100 $id = '41';
101 $request = Request::createFromEnvironment($env);
102
103 $response = $this->controller->putLink($request, new Response(), ['id' => $id]);
104 $this->assertEquals(200, $response->getStatusCode());
105 $data = json_decode((string) $response->getBody(), true);
106 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
107 $this->assertEquals($id, $data['id']);
108 $this->assertEquals('WDWyig', $data['shorturl']);
109 $this->assertEquals('http://domain.tld/?WDWyig', $data['url']);
110 $this->assertEquals('?WDWyig', $data['title']);
111 $this->assertEquals('', $data['description']);
112 $this->assertEquals([], $data['tags']);
113 $this->assertEquals(false, $data['private']);
114 $this->assertEquals(
115 \DateTime::createFromFormat('Ymd_His', '20150310_114651'),
116 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
117 );
118 $this->assertTrue(new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']));
119
120 $historyEntry = $this->history->getHistory()[0];
121 $this->assertEquals(\History::UPDATED, $historyEntry['event']);
122 $this->assertTrue(
123 (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime']
124 );
125 $this->assertEquals($id, $historyEntry['id']);
126 }
127
128 /**
129 * Test link update with new values
130 */
131 public function testPutLinkWithValues()
132 {
133 $env = Environment::mock([
134 'REQUEST_METHOD' => 'PUT',
135 'CONTENT_TYPE' => 'application/json'
136 ]);
137 $id = 41;
138 $update = [
139 'url' => 'http://somewhere.else',
140 'title' => 'Le Cid',
141 'description' => 'Percé jusques au fond du cœur [...]',
142 'tags' => ['corneille', 'rodrigue'],
143 'private' => true,
144 ];
145 $request = Request::createFromEnvironment($env);
146 $request = $request->withParsedBody($update);
147
148 $response = $this->controller->putLink($request, new Response(), ['id' => $id]);
149 $this->assertEquals(200, $response->getStatusCode());
150 $data = json_decode((string) $response->getBody(), true);
151 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
152 $this->assertEquals($id, $data['id']);
153 $this->assertEquals('WDWyig', $data['shorturl']);
154 $this->assertEquals('http://somewhere.else', $data['url']);
155 $this->assertEquals('Le Cid', $data['title']);
156 $this->assertEquals('Percé jusques au fond du cœur [...]', $data['description']);
157 $this->assertEquals(['corneille', 'rodrigue'], $data['tags']);
158 $this->assertEquals(true, $data['private']);
159 $this->assertEquals(
160 \DateTime::createFromFormat('Ymd_His', '20150310_114651'),
161 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
162 );
163 $this->assertTrue(new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']));
164 }
165
166 /**
167 * Test link update with an existing URL: 409 Conflict with the existing link as body
168 */
169 public function testPutLinkDuplicate()
170 {
171 $link = [
172 'url' => 'mediagoblin.org/',
173 'title' => 'new entry',
174 'description' => 'shaare description',
175 'tags' => ['one', 'two'],
176 'private' => true,
177 ];
178 $env = Environment::mock([
179 'REQUEST_METHOD' => 'PUT',
180 'CONTENT_TYPE' => 'application/json'
181 ]);
182
183 $request = Request::createFromEnvironment($env);
184 $request = $request->withParsedBody($link);
185 $response = $this->controller->putLink($request, new Response(), ['id' => 41]);
186
187 $this->assertEquals(409, $response->getStatusCode());
188 $data = json_decode((string) $response->getBody(), true);
189 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
190 $this->assertEquals(7, $data['id']);
191 $this->assertEquals('IuWvgA', $data['shorturl']);
192 $this->assertEquals('http://mediagoblin.org/', $data['url']);
193 $this->assertEquals('MediaGoblin', $data['title']);
194 $this->assertEquals('A free software media publishing platform #hashtagOther', $data['description']);
195 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
196 $this->assertEquals(false, $data['private']);
197 $this->assertEquals(
198 \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130614_184135'),
199 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
200 );
201 $this->assertEquals(
202 \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130615_184230'),
203 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
204 );
205 }
206
207 /**
208 * Test link update on non existent link => ApiLinkNotFoundException.
209 *
210 * @expectedException Shaarli\Api\Exceptions\ApiLinkNotFoundException
211 * @expectedExceptionMessage Link not found
212 */
213 public function testGetLink404()
214 {
215 $env = Environment::mock([
216 'REQUEST_METHOD' => 'PUT',
217 ]);
218 $request = Request::createFromEnvironment($env);
219
220 $this->controller->putLink($request, new Response(), ['id' => -1]);
221 }
222}
diff --git a/tests/languages/de/UtilsDeTest.php b/tests/languages/de/UtilsDeTest.php
index 545fa572..6c9c9adc 100644
--- a/tests/languages/de/UtilsDeTest.php
+++ b/tests/languages/de/UtilsDeTest.php
@@ -11,7 +11,16 @@ class UtilsDeTest extends UtilsTest
11 public function testDateFormat() 11 public function testDateFormat()
12 { 12 {
13 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 13 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
14 $this->assertRegExp('/1. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true)); 14 $this->assertRegExp('/1\. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true, true));
15 }
16
17 /**
18 * Test date_format() without time.
19 */
20 public function testDateFormatNoTime()
21 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertRegExp('/1\. Januar 2017/', format_date($date, false,true));
15 } 24 }
16 25
17 /** 26 /**
@@ -20,7 +29,16 @@ class UtilsDeTest extends UtilsTest
20 public function testDateFormatDefault() 29 public function testDateFormatDefault()
21 { 30 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 31 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, false)); 32 $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, true, false));
33 }
34
35 /**
36 * Test date_format() using builtin PHP function strftime without time.
37 */
38 public function testDateFormatDefaultNoTime()
39 {
40 $date = DateTime::createFromFormat('Ymd_His', '20170201_101112');
41 $this->assertEquals('01.02.2017', format_date($date, false, false));
24 } 42 }
25 43
26 /** 44 /**
diff --git a/tests/languages/en/UtilsEnTest.php b/tests/languages/en/UtilsEnTest.php
index 7c829ac7..d8680b2b 100644
--- a/tests/languages/en/UtilsEnTest.php
+++ b/tests/languages/en/UtilsEnTest.php
@@ -11,7 +11,16 @@ class UtilsEnTest extends UtilsTest
11 public function testDateFormat() 11 public function testDateFormat()
12 { 12 {
13 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 13 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
14 $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true)); 14 $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true, true));
15 }
16
17 /**
18 * Test date_format() without time.
19 */
20 public function testDateFormatNoTime()
21 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertRegExp('/January 1, 2017/', format_date($date, false, true));
15 } 24 }
16 25
17 /** 26 /**
@@ -20,7 +29,16 @@ class UtilsEnTest extends UtilsTest
20 public function testDateFormatDefault() 29 public function testDateFormatDefault()
21 { 30 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 31 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, false)); 32 $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, true, false));
33 }
34
35 /**
36 * Test date_format() using builtin PHP function strftime without time.
37 */
38 public function testDateFormatDefaultNoTime()
39 {
40 $date = DateTime::createFromFormat('Ymd_His', '20170201_101112');
41 $this->assertEquals('02/01/2017', format_date($date, false, false));
24 } 42 }
25 43
26 /** 44 /**
diff --git a/tests/languages/fr/UtilsFrTest.php b/tests/languages/fr/UtilsFrTest.php
index 45996ee2..0d50a878 100644
--- a/tests/languages/fr/UtilsFrTest.php
+++ b/tests/languages/fr/UtilsFrTest.php
@@ -15,12 +15,30 @@ class UtilsFrTest extends UtilsTest
15 } 15 }
16 16
17 /** 17 /**
18 * Test date_format() without time.
19 */
20 public function testDateFormatNoTime()
21 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertRegExp('/1 janvier 2017/', format_date($date, false, true));
24 }
25
26 /**
18 * Test date_format() using builtin PHP function strftime. 27 * Test date_format() using builtin PHP function strftime.
19 */ 28 */
20 public function testDateFormatDefault() 29 public function testDateFormatDefault()
21 { 30 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 31 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, false)); 32 $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, true, false));
33 }
34
35 /**
36 * Test date_format() using builtin PHP function strftime without time.
37 */
38 public function testDateFormatDefaultNoTime()
39 {
40 $date = DateTime::createFromFormat('Ymd_His', '20170201_101112');
41 $this->assertEquals('01/02/2017', format_date($date, false, false));
24 } 42 }
25 43
26 /** 44 /**
diff --git a/tests/plugins/PluginReadityourselfTest.php b/tests/plugins/PluginReadityourselfTest.php
deleted file mode 100644
index bbba9676..00000000
--- a/tests/plugins/PluginReadityourselfTest.php
+++ /dev/null
@@ -1,99 +0,0 @@
1<?php
2use Shaarli\Config\ConfigManager;
3
4/**
5 * PluginReadityourselfTest.php.php
6 */
7
8require_once 'plugins/readityourself/readityourself.php';
9
10/**
11 * Class PluginWallabagTest
12 * Unit test for the Wallabag plugin
13 */
14class PluginReadityourselfTest extends PHPUnit_Framework_TestCase
15{
16 /**
17 * Reset plugin path
18 */
19 public function setUp()
20 {
21 PluginManager::$PLUGINS_PATH = 'plugins';
22 }
23
24 /**
25 * Test Readityourself init without errors.
26 */
27 public function testReadityourselfInitNoError()
28 {
29 $conf = new ConfigManager('');
30 $conf->set('plugins.READITYOUSELF_URL', 'value');
31 $errors = readityourself_init($conf);
32 $this->assertEmpty($errors);
33 }
34
35 /**
36 * Test Readityourself init with errors.
37 */
38 public function testReadityourselfInitError()
39 {
40 $conf = new ConfigManager('');
41 $errors = readityourself_init($conf);
42 $this->assertNotEmpty($errors);
43 }
44
45 /**
46 * Test render_linklist hook.
47 */
48 public function testReadityourselfLinklist()
49 {
50 $conf = new ConfigManager('');
51 $conf->set('plugins.READITYOUSELF_URL', 'value');
52 $str = 'http://randomstr.com/test';
53 $data = array(
54 'title' => $str,
55 'links' => array(
56 array(
57 'url' => $str,
58 )
59 )
60 );
61
62 $data = hook_readityourself_render_linklist($data, $conf);
63 $link = $data['links'][0];
64 // data shouldn't be altered
65 $this->assertEquals($str, $data['title']);
66 $this->assertEquals($str, $link['url']);
67
68 // plugin data
69 $this->assertEquals(1, count($link['link_plugin']));
70 $this->assertNotFalse(strpos($link['link_plugin'][0], $str));
71 }
72
73 /**
74 * Test without config: nothing should happened.
75 */
76 public function testReadityourselfLinklistWithoutConfig()
77 {
78 $conf = new ConfigManager('');
79 $conf->set('plugins.READITYOUSELF_URL', null);
80 $str = 'http://randomstr.com/test';
81 $data = array(
82 'title' => $str,
83 'links' => array(
84 array(
85 'url' => $str,
86 )
87 )
88 );
89
90 $data = hook_readityourself_render_linklist($data, $conf);
91 $link = $data['links'][0];
92 // data shouldn't be altered
93 $this->assertEquals($str, $data['title']);
94 $this->assertEquals($str, $link['url']);
95
96 // plugin data
97 $this->assertArrayNotHasKey('link_plugin', $link);
98 }
99}
diff --git a/tests/utils/ReferenceHistory.php b/tests/utils/ReferenceHistory.php
new file mode 100644
index 00000000..75cbb326
--- /dev/null
+++ b/tests/utils/ReferenceHistory.php
@@ -0,0 +1,82 @@
1<?php
2
3/**
4 * Populates a reference history
5 */
6class ReferenceHistory
7{
8 private $count;
9
10 private $history = [];
11
12 /**
13 * Populates the test DB with reference data
14 */
15 public function __construct()
16 {
17 $this->addEntry(
18 History::DELETED,
19 DateTime::createFromFormat('Ymd_His', '20170303_121216'),
20 124
21 );
22
23 $this->addEntry(
24 History::SETTINGS,
25 DateTime::createFromFormat('Ymd_His', '20170302_121215')
26 );
27
28 $this->addEntry(
29 History::UPDATED,
30 DateTime::createFromFormat('Ymd_His', '20170301_121214'),
31 123
32 );
33
34 $this->addEntry(
35 History::CREATED,
36 DateTime::createFromFormat('Ymd_His', '20170201_121214'),
37 124
38 );
39
40 $this->addEntry(
41 History::CREATED,
42 DateTime::createFromFormat('Ymd_His', '20170101_121212'),
43 123
44 );
45 }
46
47 /**
48 * Adds a new history entry
49 *
50 * @param string $event Event identifier
51 * @param DateTime $datetime creation date
52 * @param int $id optional: related link ID
53 */
54 protected function addEntry($event, $datetime, $id = null)
55 {
56 $link = [
57 'event' => $event,
58 'datetime' => $datetime,
59 'id' => $id,
60 ];
61 $this->history[] = $link;
62 $this->count++;
63 }
64
65 /**
66 * Writes data to the datastore
67 *
68 * @param string $filename write history content to.
69 */
70 public function write($filename)
71 {
72 FileUtils::writeFlatDB($filename, $this->history);
73 }
74
75 /**
76 * Returns the number of links in the reference data
77 */
78 public function count()
79 {
80 return $this->count;
81 }
82}
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index 36d58c68..1f4b3063 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -56,7 +56,7 @@ class ReferenceLinkDB
56 0, 56 0,
57 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 57 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'),
58 'gnu media web .hidden hashtag', 58 'gnu media web .hidden hashtag',
59 null, 59 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130615_184230'),
60 'IuWvgA' 60 'IuWvgA'
61 ); 61 );
62 62
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html
index 8d263a16..49dd20d9 100644
--- a/tpl/default/changetag.html
+++ b/tpl/default/changetag.html
@@ -11,7 +11,7 @@
11 <h2 class="window-title">{"Manage tags"|t}</h2> 11 <h2 class="window-title">{"Manage tags"|t}</h2>
12 <form method="POST" action="#" name="changetag" id="changetag"> 12 <form method="POST" action="#" name="changetag" id="changetag">
13 <div> 13 <div>
14 <input type="text" name="fromtag" placeholder="{'Tag'|t}" 14 <input type="text" name="fromtag" placeholder="{'Tag'|t}" value="{$fromtag}"
15 list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1"> 15 list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1">
16 <datalist id="tagsList"> 16 <datalist id="tagsList">
17 {loop="$tags"}<option>{$key}</option>{/loop} 17 {loop="$tags"}<option>{$key}</option>{/loop}
@@ -31,6 +31,8 @@
31 <input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete"> 31 <input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete">
32 </div> 32 </div>
33 </form> 33 </form>
34
35 <p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p>
34 </div> 36 </div>
35</div> 37</div>
36{include="page.footer"} 38{include="page.footer"}
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index 12261487..76a1b9fd 100644
--- a/tpl/default/configure.html
+++ b/tpl/default/configure.html
@@ -34,7 +34,7 @@
34 <div class="pure-u-lg-{$ratioLabel} pure-u-1"> 34 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
35 <div class="form-label"> 35 <div class="form-label">
36 <label for="titleLink"> 36 <label for="titleLink">
37 <span class="label-name">{'Title link'|t}</span><br> 37 <span class="label-name">{'Home link'|t}</span><br>
38 <span class="label-desc">{'Default value'|t}: ?</span> 38 <span class="label-desc">{'Default value'|t}: ?</span>
39 </label> 39 </label>
40 </div> 40 </div>
@@ -73,15 +73,35 @@
73 <div class="pure-u-lg-{$ratioLabel} pure-u-1 "> 73 <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
74 <div class="form-label"> 74 <div class="form-label">
75 <label> 75 <label>
76 <span class="label-name">{'Timezone'|t}</span> 76 <span class="label-name">{'Timezone'|t}</span><br>
77 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span>
77 </label> 78 </label>
78 </div> 79 </div>
79 </div> 80 </div>
80 <div class="pure-u-lg-{$ratioInput} pure-u-1 "> 81 <div class="pure-u-lg-{$ratioInput} pure-u-1 ">
81 <div class="form-input"> 82 <div class="form-input">
82 {ignore}FIXME! too hackish, needs to be fixed upstream{/ignore} 83 <div class="timezone">
83 <div class="timezone" id="timezone-remove">{$timezone_form}</div> 84 <select id="continent" name="continent">
84 <div class="timezone" id="timezone-add"></div> 85 {loop="$continents"}
86 {if="$key !== 'selected'"}
87 <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
88 {$value}
89 </option>
90 {/if}
91 {/loop}
92 </select>
93 <select id="city" name="city">
94 {loop="$cities"}
95 {if="$key !== 'selected'"}
96 <option value="{$value.city}"
97 {if="$cities.selected === $value.city"}selected{/if}
98 data-continent="{$value.continent}">
99 {$value.city}
100 </option>
101 {/if}
102 {/loop}
103 </select>
104 </div>
85 </div> 105 </div>
86 </div> 106 </div>
87 </div> 107 </div>
diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css
index 8fcd13af..28920648 100644
--- a/tpl/default/css/shaarli.css
+++ b/tpl/default/css/shaarli.css
@@ -35,14 +35,29 @@ pre {
35} 35}
36 36
37@font-face { 37@font-face {
38 font-family: 'Roboto Slab'; 38 font-family: 'Roboto';
39 font-weight: 400; 39 font-weight: 400;
40 font-style: normal; 40 font-style: normal;
41 src: 41 src:
42 local('Fira Sans'), 42 local('Roboto'),
43 local('Fira-Sans-regular'), 43 local('Roboto-Regular'),
44 url('../fonts/Fira-Sans-regular.woff2') format('woff2'), 44 url('../fonts/Roboto-Regular.woff2') format('woff2'),
45 url('../fonts/Fira-Sans-regular.woff') format('woff'); 45 url('../fonts/Roboto-Regular.woff') format('woff');
46}
47
48@font-face {
49 font-family: 'Roboto';
50 font-weight: 700;
51 font-style: normal;
52 src:
53 local('Roboto'),
54 local('Roboto-Bold'),
55 url('../fonts/Roboto-Bold.woff2') format('woff2'),
56 url('../fonts/Roboto-Bold.woff') format('woff');
57}
58
59body, .pure-g [class*="pure-u"] {
60 font-family: Roboto, Arial, sans-serif;
46} 61}
47 62
48/** 63/**
@@ -68,10 +83,6 @@ pre {
68 .pure-u-xl-visible { display: inline-block !important; } 83 .pure-u-xl-visible { display: inline-block !important; }
69} 84}
70 85
71.pure-g [class*="pure-u"]{
72 font-family: Roboto Slab, Arial, sans-serif;
73}
74
75/** 86/**
76 * Make pure-extras alert closable. 87 * Make pure-extras alert closable.
77 */ 88 */
@@ -200,7 +211,7 @@ pre {
200 } 211 }
201} 212}
202 213
203#search, #search-linklist { 214#search, #search-linklist, #search-tagcloud {
204 text-align: center; 215 text-align: center;
205 width: 100%; 216 width: 100%;
206} 217}
@@ -223,6 +234,7 @@ pre {
223} 234}
224 235
225#search button, 236#search button,
237#search-tagcloud button,
226#search-linklist button { 238#search-linklist button {
227 background: transparent; 239 background: transparent;
228 border: none; 240 border: none;
@@ -240,6 +252,9 @@ pre {
240#search-linklist button:hover { 252#search-linklist button:hover {
241 color: #fff; 253 color: #fff;
242} 254}
255#search-tagcloud button:hover {
256 color: #d0d0d0;
257}
243 258
244#search-linklist { 259#search-linklist {
245 padding: 5px 0; 260 padding: 5px 0;
@@ -264,6 +279,19 @@ pre {
264 } 279 }
265} 280}
266 281
282.subheader-form a.button {
283 color: #f5f5f5;
284 font-weight: bold;
285 text-decoration: none;
286 border: 2px solid #f5f5f5;
287 border-radius: 5px;
288 padding: 3px 10px;
289}
290
291.linklist-item-editbuttons .delete-checkbox {
292 display: none;
293}
294
267#header-login-form input[type="text"], #header-login-form input[type="password"] { 295#header-login-form input[type="text"], #header-login-form input[type="password"] {
268 width: 200px; 296 width: 200px;
269} 297}
@@ -504,7 +532,6 @@ pre {
504 color: #252525; 532 color: #252525;
505 text-decoration: none; 533 text-decoration: none;
506 vertical-align: middle; 534 vertical-align: middle;
507 font-family: Roboto Slab, Arial, sans-serif;
508} 535}
509 536
510.linklist-item-title .linklist-link { 537.linklist-item-title .linklist-link {
@@ -560,7 +587,6 @@ pre {
560.linklist-item-description { 587.linklist-item-description {
561 position: relative; 588 position: relative;
562 padding: 10px; 589 padding: 10px;
563 font-family: Roboto Slab, Arial, sans-serif;
564 word-wrap: break-word; 590 word-wrap: break-word;
565 color: #252525; 591 color: #252525;
566 line-height: 1.3em; 592 line-height: 1.3em;
@@ -725,10 +751,11 @@ pre {
725.page-form a { 751.page-form a {
726 color: #1b926c; 752 color: #1b926c;
727 font-weight: bold; 753 font-weight: bold;
754 text-decoration: none;
728} 755}
729 756
730.page-form p { 757.page-form p {
731 padding: 0 10px; 758 padding: 5px 10px;
732 margin: 0; 759 margin: 0;
733} 760}
734 761
@@ -1044,7 +1071,7 @@ form[name="linkform"].page-form {
1044} 1071}
1045 1072
1046#cloudtag, #cloudtag a { 1073#cloudtag, #cloudtag a {
1047 color: #000; 1074 color: #252525;
1048 text-decoration: none; 1075 text-decoration: none;
1049} 1076}
1050 1077
@@ -1053,6 +1080,42 @@ form[name="linkform"].page-form {
1053} 1080}
1054 1081
1055/** 1082/**
1083 * TAG LIST
1084 */
1085#taglist {
1086 padding: 0 10px;
1087}
1088
1089#taglist a {
1090 color: #252525;
1091 text-decoration: none;
1092}
1093
1094#taglist .count {
1095 display: inline-block;
1096 width: 35px;
1097 text-align: right;
1098 color: #7f7f7f;
1099}
1100
1101#taglist .rename-tag-form {
1102 display: none;
1103}
1104
1105#taglist .delete-tag {
1106 color: #ac2925;
1107 display: none;
1108}
1109
1110#taglist .rename-tag {
1111 color: #0b5ea6;
1112}
1113
1114#taglist .validate-rename-tag {
1115 color: #1b926c;
1116}
1117
1118/**
1056 * Picture wall CSS 1119 * Picture wall CSS
1057 */ 1120 */
1058#picwall_container { 1121#picwall_container {
@@ -1201,3 +1264,16 @@ form[name="linkform"].page-form {
1201.pure-button { 1264.pure-button {
1202 -moz-user-select: auto; 1265 -moz-user-select: auto;
1203} 1266}
1267
1268.tag-sort {
1269 margin-top: 30px;
1270 text-align: center;
1271}
1272
1273.tag-sort a {
1274 display: inline-block;
1275 margin: 0 15px;
1276 color: white;
1277 text-decoration: none;
1278 font-weight: bold;
1279}
diff --git a/tpl/default/daily.html b/tpl/default/daily.html
index d8c91078..29d845d5 100644
--- a/tpl/default/daily.html
+++ b/tpl/default/daily.html
@@ -44,7 +44,7 @@
44 </div> 44 </div>
45 </div> 45 </div>
46 <div> 46 <div>
47 <h3 class="window-subtitle">{function="strftime('%A %d, %B %Y', $day)"}</h3> 47 <h3 class="window-subtitle">{function="format_date($dayDate, false)"}</h3>
48 48
49 <div id="plugin_zone_about_daily" class="plugin_zone"> 49 <div id="plugin_zone_about_daily" class="plugin_zone">
50 {loop="$daily_about_plugin"} 50 {loop="$daily_about_plugin"}
diff --git a/tpl/default/fonts/Fira-Sans-regular.woff b/tpl/default/fonts/Fira-Sans-regular.woff
deleted file mode 100644
index 014ac317..00000000
--- a/tpl/default/fonts/Fira-Sans-regular.woff
+++ /dev/null
Binary files differ
diff --git a/tpl/default/fonts/Fira-Sans-regular.woff2 b/tpl/default/fonts/Fira-Sans-regular.woff2
deleted file mode 100644
index bf3ad9a4..00000000
--- a/tpl/default/fonts/Fira-Sans-regular.woff2
+++ /dev/null
Binary files differ
diff --git a/tpl/default/fonts/Roboto-Bold.woff b/tpl/default/fonts/Roboto-Bold.woff
new file mode 100644
index 00000000..3d86753b
--- /dev/null
+++ b/tpl/default/fonts/Roboto-Bold.woff
Binary files differ
diff --git a/tpl/default/fonts/Roboto-Bold.woff2 b/tpl/default/fonts/Roboto-Bold.woff2
new file mode 100644
index 00000000..bd05e2ea
--- /dev/null
+++ b/tpl/default/fonts/Roboto-Bold.woff2
Binary files differ
diff --git a/tpl/default/fonts/Roboto-Regular.woff b/tpl/default/fonts/Roboto-Regular.woff
new file mode 100644
index 00000000..464d2062
--- /dev/null
+++ b/tpl/default/fonts/Roboto-Regular.woff
Binary files differ
diff --git a/tpl/default/fonts/Roboto-Regular.woff2 b/tpl/default/fonts/Roboto-Regular.woff2
new file mode 100644
index 00000000..f9661967
--- /dev/null
+++ b/tpl/default/fonts/Roboto-Regular.woff2
Binary files differ
diff --git a/tpl/default/import.html b/tpl/default/import.html
index e6e521e8..1f040685 100644
--- a/tpl/default/import.html
+++ b/tpl/default/import.html
@@ -18,6 +18,7 @@
18 <div class="center" id="import-field"> 18 <div class="center" id="import-field">
19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
20 <input type="file" name="filetoupload"> 20 <input type="file" name="filetoupload">
21 <p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p>
21 </div> 22 </div>
22 23
23 <div class="pure-g"> 24 <div class="pure-g">
diff --git a/tpl/default/includes.html b/tpl/default/includes.html
index 91c6ca3b..0350ef66 100644
--- a/tpl/default/includes.html
+++ b/tpl/default/includes.html
@@ -11,7 +11,7 @@
11<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css" /> 11<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css" />
12<link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" /> 12<link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
13<link type="text/css" rel="stylesheet" href="css/shaarli.css" /> 13<link type="text/css" rel="stylesheet" href="css/shaarli.css" />
14{if="is_file('inc/user.css')"} 14{if="is_file('data/user.css')"}
15 <link type="text/css" rel="stylesheet" href="data/user.css#" /> 15 <link type="text/css" rel="stylesheet" href="data/user.css#" />
16{/if} 16{/if}
17{loop="$plugins_includes.css_files"} 17{loop="$plugins_includes.css_files"}
diff --git a/tpl/default/install.html b/tpl/default/install.html
index 663397ac..164d453b 100644
--- a/tpl/default/install.html
+++ b/tpl/default/install.html
@@ -45,24 +45,22 @@
45 </div> 45 </div>
46 <div class="pure-u-lg-{$ratioInput} pure-u-1"> 46 <div class="pure-u-lg-{$ratioInput} pure-u-1">
47 <div class="form-input"> 47 <div class="form-input">
48 <input type="text" name="setpassword" id="password"> 48 <input type="password" name="setpassword" id="password">
49 </div> 49 </div>
50 </div> 50 </div>
51 </div> 51 </div>
52 52
53 <div class="pure-g"> 53 <div class="pure-g">
54 <div class="pure-u-lg-{$ratioLabel} pure-u-1 "> 54 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
55 <div class="form-label"> 55 <div class="form-label">
56 <label> 56 <label for="title">
57 <span class="label-name">{'Timezone'|t}</span> 57 <span class="label-name">{'Shaarli title'|t}</span>
58 </label> 58 </label>
59 </div> 59 </div>
60 </div> 60 </div>
61 <div class="pure-u-lg-{$ratioInput} pure-u-1 "> 61 <div class="pure-u-lg-{$ratioInput} pure-u-1">
62 <div class="form-input"> 62 <div class="form-input">
63 {ignore}FIXME! too hackish, needs to be fixed upstream{/ignore} 63 <input type="text" name="title" id="title" placeholder="{'My links'|t}">
64 <div class="timezone" id="timezone-remove">{$timezone_html}</div>
65 <div class="timezone" id="timezone-add"></div>
66 </div> 64 </div>
67 </div> 65 </div>
68 </div> 66 </div>
@@ -70,14 +68,36 @@
70 <div class="pure-g"> 68 <div class="pure-g">
71 <div class="pure-u-lg-{$ratioLabel} pure-u-1"> 69 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
72 <div class="form-label"> 70 <div class="form-label">
73 <label for="title"> 71 <label>
74 <span class="label-name">{'Shaarli title'|t}</span> 72 <span class="label-name">{'Timezone'|t}</span><br>
73 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span>
75 </label> 74 </label>
76 </div> 75 </div>
77 </div> 76 </div>
78 <div class="pure-u-lg-{$ratioInput} pure-u-1"> 77 <div class="pure-u-lg-{$ratioInput} pure-u-1">
79 <div class="form-input"> 78 <div class="form-input">
80 <input type="text" name="title" id="title" placeholder="{'My links'|t}"> 79 <div class="timezone">
80 <select id="continent" name="continent">
81 {loop="$continents"}
82 {if="$key !== 'selected'"}
83 <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
84 {$value}
85 </option>
86 {/if}
87 {/loop}
88 </select>
89 <select id="city" name="city">
90 {loop="$cities"}
91 {if="$key !== 'selected'"}
92 <option value="{$value.city}"
93 {if="$cities.selected === $value.city"}selected{/if}
94 data-continent="{$value.continent}">
95 {$value.city}
96 </option>
97 {/if}
98 {/loop}
99 </select>
100 </div>
81 </div> 101 </div>
82 </div> 102 </div>
83 </div> 103 </div>
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js
index 30d8ed6f..4ebb7815 100644
--- a/tpl/default/js/shaarli.js
+++ b/tpl/default/js/shaarli.js
@@ -76,9 +76,12 @@ window.onload = function () {
76 } 76 }
77 } 77 }
78 78
79 document.getElementById('menu-toggle').addEventListener('click', function (e) { 79 var menuToggle = document.getElementById('menu-toggle');
80 toggleMenu(); 80 if (menuToggle != null) {
81 }); 81 menuToggle.addEventListener('click', function (e) {
82 toggleMenu();
83 });
84 }
82 85
83 window.addEventListener(WINDOW_CHANGE_EVENT, closeMenu); 86 window.addEventListener(WINDOW_CHANGE_EVENT, closeMenu);
84 })(this, this.document); 87 })(this, this.document);
@@ -213,14 +216,14 @@ window.onload = function () {
213 /** 216 /**
214 * Autofocus text fields 217 * Autofocus text fields
215 */ 218 */
216 // ES6 syntax 219 var autofocusElements = document.querySelectorAll('.autofocus');
217 let autofocusElements = document.querySelectorAll('.autofocus'); 220 var breakLoop = false;
218 for (let autofocusElement of autofocusElements) { 221 [].forEach.call(autofocusElements, function(autofocusElement) {
219 if (autofocusElement.value == '') { 222 if (autofocusElement.value == '' && ! breakLoop) {
220 autofocusElement.focus(); 223 autofocusElement.focus();
221 break; 224 breakLoop = true;
222 } 225 }
223 } 226 });
224 227
225 /** 228 /**
226 * Handle sub menus/forms 229 * Handle sub menus/forms
@@ -255,10 +258,9 @@ window.onload = function () {
255 * Remove CSS target padding (for fixed bar) 258 * Remove CSS target padding (for fixed bar)
256 */ 259 */
257 if (location.hash != '') { 260 if (location.hash != '') {
258 var anchor = document.querySelector(location.hash); 261 var anchor = document.getElementById(location.hash.substr(1));
259 if (anchor != null) { 262 if (anchor != null) {
260 var padsize = anchor.clientHeight; 263 var padsize = anchor.clientHeight;
261 console.log(document.querySelector(location.hash).clientHeight);
262 this.window.scroll(0, this.window.scrollY - padsize); 264 this.window.scroll(0, this.window.scrollY - padsize);
263 anchor.style.paddingTop = 0; 265 anchor.style.paddingTop = 0;
264 } 266 }
@@ -300,21 +302,6 @@ window.onload = function () {
300 } 302 }
301 303
302 /** 304 /**
303 * TimeZome select
304 * FIXME! way too hackish
305 */
306 var toRemove = document.getElementById('timezone-remove');
307 if (toRemove != null) {
308 var firstSelect = toRemove.getElementsByTagName('select')[0];
309 var secondSelect = toRemove.getElementsByTagName('select')[1];
310 toRemove.parentNode.removeChild(toRemove);
311 var toAdd = document.getElementById('timezone-add');
312 var newTimezone = '<span class="timezone-continent">Continent ' + firstSelect.outerHTML + '</span>';
313 newTimezone += ' <span class="timezone-country">Country ' + secondSelect.outerHTML + '</span>';
314 toAdd.innerHTML = newTimezone;
315 }
316
317 /**
318 * Awesomplete trigger. 305 * Awesomplete trigger.
319 */ 306 */
320 var tags = document.getElementById('lf_tags'); 307 var tags = document.getElementById('lf_tags');
@@ -366,8 +353,256 @@ window.onload = function () {
366 } 353 }
367 }); 354 });
368 }); 355 });
356
357 var continent = document.getElementById('continent');
358 var city = document.getElementById('city');
359 if (continent != null && city != null) {
360 continent.addEventListener('change', function (event) {
361 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
362 });
363 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
364 }
365
366 /**
367 * Bulk actions
368 */
369 var linkCheckboxes = document.querySelectorAll('.delete-checkbox');
370 var bar = document.getElementById('actions');
371 [].forEach.call(linkCheckboxes, function(checkbox) {
372 checkbox.style.display = 'block';
373 checkbox.addEventListener('click', function(event) {
374 var count = 0;
375 var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
376 [].forEach.call(linkCheckedCheckboxes, function(checkbox) {
377 count++;
378 });
379 if (count == 0 && bar.classList.contains('open')) {
380 bar.classList.toggle('open');
381 } else if (count > 0 && ! bar.classList.contains('open')) {
382 bar.classList.toggle('open');
383 }
384 });
385 });
386
387 var deleteButton = document.getElementById('actions-delete');
388 var token = document.querySelector('input[type="hidden"][name="token"]');
389 if (deleteButton != null && token != null) {
390 deleteButton.addEventListener('click', function(event) {
391 event.preventDefault();
392
393 var links = [];
394 var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
395 [].forEach.call(linkCheckedCheckboxes, function(checkbox) {
396 links.push({
397 'id': checkbox.value,
398 'title': document.querySelector('.linklist-item[data-id="'+ checkbox.value +'"] .linklist-link').innerHTML
399 });
400 });
401
402 var message = 'Are you sure you want to delete '+ links.length +' links?\n';
403 message += 'This action is IRREVERSIBLE!\n\nTitles:\n';
404 var ids = '';
405 links.forEach(function(item) {
406 message += ' - '+ item['title'] +'\n';
407 ids += item['id'] +'+';
408 });
409
410 if (window.confirm(message)) {
411 window.location = '?delete_link&lf_linkdate='+ ids +'&token='+ token.value;
412 }
413 });
414 }
415
416 /**
417 * Tag list operations
418 *
419 * TODO: support error code in the backend for AJAX requests
420 */
421 var existingTags = document.querySelector('input[name="taglist"]').value.split(' ');
422 var awesomepletes = [];
423
424 // Display/Hide rename form
425 var renameTagButtons = document.querySelectorAll('.rename-tag');
426 [].forEach.call(renameTagButtons, function(rename) {
427 rename.addEventListener('click', function(event) {
428 event.preventDefault();
429 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
430 var form = block.querySelector('.rename-tag-form');
431 if (form.style.display == 'none' || form.style.display == '') {
432 form.style.display = 'block';
433 } else {
434 form.style.display = 'none';
435 }
436 block.querySelector('input').focus();
437 });
438 });
439
440 // Rename a tag with an AJAX request
441 var renameTagSubmits = document.querySelectorAll('.validate-rename-tag');
442 [].forEach.call(renameTagSubmits, function(rename) {
443 rename.addEventListener('click', function(event) {
444 event.preventDefault();
445 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
446 var input = block.querySelector('.rename-tag-input');
447 var totag = input.value.replace('/"/g', '\\"');
448 if (totag.trim() == '') {
449 return;
450 }
451 var fromtag = block.getAttribute('data-tag');
452 var token = document.getElementById('token').value;
453
454 xhr = new XMLHttpRequest();
455 xhr.open('POST', '?do=changetag');
456 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
457 xhr.onload = function() {
458 if (xhr.status !== 200) {
459 alert('An error occurred. Return code: '+ xhr.status);
460 location.reload();
461 } else {
462 block.setAttribute('data-tag', totag);
463 input.setAttribute('name', totag);
464 input.setAttribute('value', totag);
465 findParent(input, 'div', {'class': 'rename-tag-form'}).style.display = 'none';
466 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
467 block.querySelector('a.tag-link').setAttribute('href', '?searchtags='+ encodeURIComponent(totag));
468 block.querySelector('a.rename-tag').setAttribute('href', '?do=changetag&fromtag='+ encodeURIComponent(totag));
469
470 // Refresh awesomplete values
471 for (var key in existingTags) {
472 if (existingTags[key] == fromtag) {
473 existingTags[key] = totag;
474 }
475 }
476 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
477 }
478 };
479 xhr.send('renametag=1&fromtag='+ encodeURIComponent(fromtag) +'&totag='+ encodeURIComponent(totag) +'&token='+ token);
480 refreshToken();
481 });
482 });
483
484 // Validate input with enter key
485 var renameTagInputs = document.querySelectorAll('.rename-tag-input');
486 [].forEach.call(renameTagInputs, function(rename) {
487
488 rename.addEventListener('keypress', function(event) {
489 if (event.keyCode === 13) { // enter
490 findParent(event.target, 'div', {'class': 'tag-list-item'}).querySelector('.validate-rename-tag').click();
491 }
492 });
493 });
494
495 // Delete a tag with an AJAX query (alert popup confirmation)
496 var deleteTagButtons = document.querySelectorAll('.delete-tag');
497 [].forEach.call(deleteTagButtons, function(rename) {
498 rename.style.display = 'inline';
499 rename.addEventListener('click', function(event) {
500 event.preventDefault();
501 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
502 var tag = block.getAttribute('data-tag');
503 var token = document.getElementById('token').value;
504
505 if (confirm('Are you sure you want to delete the tag "'+ tag +'"?')) {
506 xhr = new XMLHttpRequest();
507 xhr.open('POST', '?do=changetag');
508 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
509 xhr.onload = function() {
510 block.remove();
511 };
512 xhr.send(encodeURI('deletetag=1&fromtag='+ tag +'&token='+ token));
513 refreshToken();
514 }
515 });
516 });
517
518 updateAwesompleteList('.rename-tag-input', document.querySelector('input[name="taglist"]').value.split(' '), awesomepletes);
369}; 519};
370 520
521/**
522 * Find a parent element according to its tag and its attributes
523 *
524 * @param element Element where to start the search
525 * @param tagName Expected parent tag name
526 * @param attributes Associative array of expected attributes (name=>value).
527 *
528 * @returns Found element or null.
529 */
530function findParent(element, tagName, attributes)
531{
532 while (element) {
533 if (element.tagName.toLowerCase() == tagName) {
534 var match = true;
535 for (var key in attributes) {
536 if (! element.hasAttribute(key)
537 || (attributes[key] != '' && element.getAttribute(key).indexOf(attributes[key]) == -1)
538 ) {
539 match = false;
540 break;
541 }
542 }
543
544 if (match) {
545 return element;
546 }
547 }
548 element = element.parentElement;
549 }
550 return null;
551}
552
553/**
554 * Ajax request to refresh the CSRF token.
555 */
556function refreshToken()
557{
558 var xhr = new XMLHttpRequest();
559 xhr.open('GET', '?do=token');
560 xhr.onload = function() {
561 var token = document.getElementById('token');
562 token.setAttribute('value', xhr.responseText);
563 };
564 xhr.send();
565}
566
567/**
568 * Update awesomplete list of tag for all elements matching the given selector
569 *
570 * @param selector CSS selector
571 * @param tags Array of tags
572 * @param instances List of existing awesomplete instances
573 */
574function updateAwesompleteList(selector, tags, instances)
575{
576 // First load: create Awesomplete instances
577 if (instances.length == 0) {
578 var elements = document.querySelectorAll(selector);
579 [].forEach.call(elements, function (element) {
580 instances.push(new Awesomplete(
581 element,
582 {'list': tags}
583 ));
584 });
585 } else {
586 // Update awesomplete tag list
587 for (var key in instances) {
588 instances[key].list = tags;
589 }
590 }
591 return instances;
592}
593
594/**
595 * html_entities in JS
596 *
597 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
598 */
599function htmlEntities(str)
600{
601 return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
602 return '&#'+i.charCodeAt(0)+';';
603 });
604}
605
371function activateFirefoxSocial(node) { 606function activateFirefoxSocial(node) {
372 var loc = location.href; 607 var loc = location.href;
373 var baseURL = loc.substring(0, loc.lastIndexOf("/")); 608 var baseURL = loc.substring(0, loc.lastIndexOf("/"));
@@ -391,3 +626,28 @@ function activateFirefoxSocial(node) {
391 var activate = new CustomEvent("ActivateSocialFeature"); 626 var activate = new CustomEvent("ActivateSocialFeature");
392 node.dispatchEvent(activate); 627 node.dispatchEvent(activate);
393} 628}
629
630/**
631 * Add the class 'hidden' to city options not attached to the current selected continent.
632 *
633 * @param cities List of <option> elements
634 * @param currentContinent Current selected continent
635 * @param reset Set to true to reset the selected value
636 */
637function hideTimezoneCities(cities, currentContinent) {
638 var first = true;
639 if (reset == null) {
640 reset = false;
641 }
642 [].forEach.call(cities, function (option) {
643 if (option.getAttribute('data-continent') != currentContinent) {
644 option.className = 'hidden';
645 } else {
646 option.className = '';
647 if (reset === true && first === true) {
648 option.setAttribute('selected', 'selected');
649 first = false;
650 }
651 }
652 });
653}
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html
index 57ef4567..6a4e14a6 100644
--- a/tpl/default/linklist.html
+++ b/tpl/default/linklist.html
@@ -15,6 +15,8 @@
15 {/if} 15 {/if}
16</div> 16</div>
17 17
18<input type="hidden" name="token" value="{$token}">
19
18<div id="search-linklist"> 20<div id="search-linklist">
19 21
20 <div class="pure-g"> 22 <div class="pure-g">
@@ -121,7 +123,7 @@
121 <div class="pure-u-lg-20-24 pure-u-22-24"> 123 <div class="pure-u-lg-20-24 pure-u-22-24">
122 {loop="links"} 124 {loop="links"}
123 <div class="anchor" id="{$value.shorturl}"></div> 125 <div class="anchor" id="{$value.shorturl}"></div>
124 <div class="linklist-item{if="$value.class"} {$value.class}{/if}"> 126 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
125 127
126 <div class="linklist-item-title"> 128 <div class="linklist-item-title">
127 {if="isLoggedIn()"} 129 {if="isLoggedIn()"}
@@ -129,6 +131,7 @@
129 {if="$value.private"} 131 {if="$value.private"}
130 <span class="label label-private">{'Private'|t}</span> 132 <span class="label label-private">{'Private'|t}</span>
131 {/if} 133 {/if}
134 <input type="checkbox" class="delete-checkbox" value="{$value.id}">
132 <!-- FIXME! JS translation --> 135 <!-- FIXME! JS translation -->
133 <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a> 136 <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a>
134 <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a> 137 <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html
index 77fc65dd..02fc7642 100644
--- a/tpl/default/page.footer.html
+++ b/tpl/default/page.footer.html
@@ -16,6 +16,9 @@
16 </div> 16 </div>
17 <div class="pure-u-2-24"></div> 17 <div class="pure-u-2-24"></div>
18</div> 18</div>
19
20<input type="hidden" name="token" value="{$token}" id="token" />
21
19{loop="$plugins_footer.endofpage"} 22{loop="$plugins_footer.endofpage"}
20 {$value} 23 {$value}
21{/loop} 24{/loop}
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html
index 9388ef79..6c71a718 100644
--- a/tpl/default/page.header.html
+++ b/tpl/default/page.header.html
@@ -122,6 +122,13 @@
122 </div> 122 </div>
123 </div> 123 </div>
124 </div> 124 </div>
125 <div id="actions" class="subheader-form">
126 <div class="pure-g">
127 <div class="pure-u-1">
128 <a href="" id="actions-delete" class="button">Delete</a>
129 </div>
130 </div>
131 </div>
125 {if="!isLoggedIn()"} 132 {if="!isLoggedIn()"}
126 <form method="post" name="loginform"> 133 <form method="post" name="loginform">
127 <div class="subheader-form" id="header-login-form"> 134 <div class="subheader-form" id="header-login-form">
diff --git a/tpl/default/tagcloud.html b/tpl/default/tag.cloud.html
index 53c31748..59aa2ee0 100644
--- a/tpl/default/tagcloud.html
+++ b/tpl/default/tag.cloud.html
@@ -6,12 +6,32 @@
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8 8
9{include="tag.sort"}
10
9<div class="pure-g"> 11<div class="pure-g">
10 <div class="pure-u-lg-1-6 pure-u-1-24"></div> 12 <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"> 13 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
12 {$countTags=count($tags)} 14 {$countTags=count($tags)}
13 <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2> 15 <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
14 16
17 <div id="search-tagcloud" class="pure-g">
18 <div class="pure-u-lg-1-4"></div>
19 <div class="pure-u-1 pure-u-lg-1-2">
20 <form method="GET">
21 <input type="hidden" name="do" value="tagcloud">
22 <input type="text" name="searchtags" placeholder="{'Filter by tag'|t}"
23 {if="!empty($search_tags)"}
24 value="{$search_tags}"
25 {/if}
26 autocomplete="off" data-multiple data-autofirst data-minChars="1"
27 data-list="{loop="$tags"}{$key}, {/loop}"
28 >
29 <button type="submit" class="search-button"><i class="fa fa-search"></i></button>
30 </form>
31 </div>
32 <div class="pure-u-lg-1-4"></div>
33 </div>
34
15 <div id="plugin_zone_start_tagcloud" class="plugin_zone"> 35 <div id="plugin_zone_start_tagcloud" class="plugin_zone">
16 {loop="$plugin_start_zone"} 36 {loop="$plugin_start_zone"}
17 {$value} 37 {$value}
@@ -21,7 +41,7 @@
21 <div id="cloudtag"> 41 <div id="cloudtag">
22 {loop="tags"} 42 {loop="tags"}
23 <a href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a 43 <a href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a
24 ><span class="count">{$value.count}</span> 44 ><a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
25 {loop="$value.tag_plugin"} 45 {loop="$value.tag_plugin"}
26 {$value} 46 {$value}
27 {/loop} 47 {/loop}
@@ -36,6 +56,8 @@
36 </div> 56 </div>
37</div> 57</div>
38 58
59{include="tag.sort"}
60
39{include="page.footer"} 61{include="page.footer"}
40</body> 62</body>
41</html> 63</html>
diff --git a/tpl/default/tag.list.html b/tpl/default/tag.list.html
new file mode 100644
index 00000000..62e2e7c6
--- /dev/null
+++ b/tpl/default/tag.list.html
@@ -0,0 +1,86 @@
1<!DOCTYPE html>
2<html>
3<head>
4 {include="includes"}
5</head>
6<body>
7{include="page.header"}
8
9{include="tag.sort"}
10
11<div class="pure-g">
12 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
13 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
14 {$countTags=count($tags)}
15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
16
17 <div id="search-tagcloud" class="pure-g">
18 <div class="pure-u-lg-1-4"></div>
19 <div class="pure-u-1 pure-u-lg-1-2">
20 <form method="GET">
21 <input type="hidden" name="do" value="taglist">
22 <input type="text" name="searchtags" placeholder="{'Filter by tag'|t}"
23 {if="!empty($search_tags)"}
24 value="{$search_tags}"
25 {/if}
26 autocomplete="off" data-multiple data-autofirst data-minChars="1"
27 data-list="{loop="$tags"}{$key}, {/loop}"
28 >
29 <button type="submit" class="search-button"><i class="fa fa-search"></i></button>
30 </form>
31 </div>
32 <div class="pure-u-lg-1-4"></div>
33 </div>
34
35 <div id="plugin_zone_start_tagcloud" class="plugin_zone">
36 {loop="$plugin_start_zone"}
37 {$value}
38 {/loop}
39 </div>
40
41 <div id="taglist">
42 {loop="tags"}
43 <div class="tag-list-item pure-g" data-tag="{$key}">
44 <div class="pure-u-1">
45 {if="isLoggedIn()===true"}
46 <a href="#" class="delete-tag"><i class="fa fa-trash"></i></a>&nbsp;&nbsp;
47 <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
48 <i class="fa fa-pencil-square-o {$key}"></i>
49 </a>
50 {/if}
51
52 <a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
53 <a href="?searchtags={$key|urlencode}" class="tag-link">{$key}</a>
54
55 {loop="$value.tag_plugin"}
56 {$value}
57 {/loop}
58 </div>
59 {if="isLoggedIn()===true"}
60 <div class="rename-tag-form pure-u-1">
61 <input type="text" name="{$key}" value="{$key}" class="rename-tag-input" />
62 <a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a>
63 </div>
64 {/if}
65 </div>
66 {/loop}
67 </div>
68
69 <div id="plugin_zone_end_tagcloud" class="plugin_zone">
70 {loop="$plugin_end_zone"}
71 {$value}
72 {/loop}
73 </div>
74 </div>
75</div>
76
77{if="isLoggedIn()===true"}
78 <input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
79{/if}
80
81{include="tag.sort"}
82
83{include="page.footer"}
84</body>
85</html>
86
diff --git a/tpl/default/tag.sort.html b/tpl/default/tag.sort.html
new file mode 100644
index 00000000..89acda0d
--- /dev/null
+++ b/tpl/default/tag.sort.html
@@ -0,0 +1,8 @@
1<div class="pure-g">
2 <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
3 {'Sort by:'|t}
4 <a href="?do=tagcloud" title="cloud">{'Cloud'|t}</a> &middot;
5 <a href="?do=taglist&sort=usage" title="cloud">{'Most used'|t}</a> &middot;
6 <a href="?do=taglist&sort=alpha" title="cloud">{'Alphabetical'|t}</a>
7 </div>
8</div> \ No newline at end of file
diff --git a/tpl/vintage/configure.html b/tpl/vintage/configure.html
index 704389c5..479284eb 100644
--- a/tpl/vintage/configure.html
+++ b/tpl/vintage/configure.html
@@ -4,7 +4,6 @@
4<body onload="document.configform.title.focus();"> 4<body onload="document.configform.title.focus();">
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 {$timezone_js}
8 <form method="POST" action="#" name="configform" id="configform"> 7 <form method="POST" action="#" name="configform" id="configform">
9 <input type="hidden" name="token" value="{$token}"> 8 <input type="hidden" name="token" value="{$token}">
10 <table id="configuration_table"> 9 <table id="configuration_table">
@@ -15,7 +14,7 @@
15 </tr> 14 </tr>
16 15
17 <tr> 16 <tr>
18 <td><b>Title link:</b></td> 17 <td><b>Home link:</b></td>
19 <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label 18 <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label
20 for="titleLink">(default value is: ?)</label></td> 19 for="titleLink">(default value is: ?)</label></td>
21 </tr> 20 </tr>
@@ -35,7 +34,28 @@
35 34
36 <tr> 35 <tr>
37 <td><b>Timezone:</b></td> 36 <td><b>Timezone:</b></td>
38 <td>{$timezone_form}</td> 37 <td>
38 <select id="continent" name="continent">
39 {loop="$continents"}
40 {if="$key !== 'selected'"}
41 <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
42 {$value}
43 </option>
44 {/if}
45 {/loop}
46 </select>
47 <select id="city" name="city">
48 {loop="$cities"}
49 {if="$key !== 'selected'"}
50 <option value="{$value.city}"
51 {if="$cities.selected === $value.city"}selected{/if}
52 data-continent="{$value.continent}">
53 {$value.city}
54 </option>
55 {/if}
56 {/loop}
57 </select>
58 </td>
39 </tr> 59 </tr>
40 60
41 <tr> 61 <tr>
diff --git a/tpl/vintage/css/shaarli.css b/tpl/vintage/css/shaarli.css
index 7ca567e7..9c72d993 100644
--- a/tpl/vintage/css/shaarli.css
+++ b/tpl/vintage/css/shaarli.css
@@ -41,6 +41,10 @@ strong {
41 font-weight: bold; 41 font-weight: bold;
42} 42}
43 43
44.hidden {
45 display: none;
46}
47
44/* Buttons */ 48/* Buttons */
45.bigbutton, #pageheader a.bigbutton { 49.bigbutton, #pageheader a.bigbutton {
46 background-color: #c0c0c0; 50 background-color: #c0c0c0;
diff --git a/tpl/vintage/import.html b/tpl/vintage/import.html
index 071e1160..bb9e4a56 100644
--- a/tpl/vintage/import.html
+++ b/tpl/vintage/import.html
@@ -5,7 +5,7 @@
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="uploaddiv"> 7 <div id="uploaddiv">
8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize} bytes). 8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize}).
9 <form method="POST" action="?do=import" enctype="multipart/form-data" 9 <form method="POST" action="?do=import" enctype="multipart/form-data"
10 name="uploadform" id="uploadform"> 10 name="uploadform" id="uploadform">
11 <input type="hidden" name="token" value="{$token}"> 11 <input type="hidden" name="token" value="{$token}">
diff --git a/tpl/vintage/install.html b/tpl/vintage/install.html
index 42874dcd..aca890d6 100644
--- a/tpl/vintage/install.html
+++ b/tpl/vintage/install.html
@@ -1,6 +1,6 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3<head>{include="includes"}{$timezone_js}</head> 3<head>{include="includes"}</head>
4<body onload="document.installform.setlogin.focus();"> 4<body onload="document.installform.setlogin.focus();">
5<div id="install"> 5<div id="install">
6 <h1>Shaarli</h1> 6 <h1>Shaarli</h1>
@@ -9,7 +9,31 @@
9 <table> 9 <table>
10 <tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr> 10 <tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr>
11 <tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr> 11 <tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr>
12 {$timezone_html} 12 <tr>
13 <td><b>Timezone:</b></td>
14 <td>
15 <select id="continent" name="continent">
16 {loop="$continents"}
17 {if="$key !== 'selected'"}
18 <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
19 {$value}
20 </option>
21 {/if}
22 {/loop}
23 </select>
24 <select id="city" name="city">
25 {loop="$cities"}
26 {if="$key !== 'selected'"}
27 <option value="{$value.city}"
28 {if="$cities.selected === $value.city"}selected{/if}
29 data-continent="{$value.continent}">
30 {$value.city}
31 </option>
32 {/if}
33 {/loop}
34 </select>
35 </td>
36 </tr>
13 <tr><td><b>Page title:</b></td><td><input type="text" name="title" size="30"></td></tr> 37 <tr><td><b>Page title:</b></td><td><input type="text" name="title" size="30"></td></tr>
14 <tr><td valign="top"><b>Update:</b></td><td> 38 <tr><td valign="top"><b>Update:</b></td><td>
15 <input type="checkbox" name="updateCheck" id="updateCheck" checked="checked"><label for="updateCheck">&nbsp;Notify me when a new release is ready</label></td> 39 <input type="checkbox" name="updateCheck" id="updateCheck" checked="checked"><label for="updateCheck">&nbsp;Notify me when a new release is ready</label></td>
diff --git a/tpl/vintage/js/shaarli.js b/tpl/vintage/js/shaarli.js
new file mode 100644
index 00000000..9bcc96fb
--- /dev/null
+++ b/tpl/vintage/js/shaarli.js
@@ -0,0 +1,32 @@
1window.onload = function () {
2 var continent = document.getElementById('continent');
3 var city = document.getElementById('city');
4 if (continent != null && city != null) {
5 continent.addEventListener('change', function(event) {
6 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
7 });
8 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
9 }
10};
11
12/**
13 * Add the class 'hidden' to city options not attached to the current selected continent.
14 *
15 * @param cities List of <option> elements
16 * @param currentContinent Current selected continent
17 * @param reset Set to true to reset the selected value
18 */
19function hideTimezoneCities(cities, currentContinent, reset = false) {
20 var first = true;
21 [].forEach.call(cities, function(option) {
22 if (option.getAttribute('data-continent') != currentContinent) {
23 option.className = 'hidden';
24 } else {
25 option.className = '';
26 if (reset === true && first === true) {
27 option.setAttribute('selected', 'selected');
28 first = false;
29 }
30 }
31 });
32}
diff --git a/tpl/vintage/page.footer.html b/tpl/vintage/page.footer.html
index 006d1d68..4ce0803a 100644
--- a/tpl/vintage/page.footer.html
+++ b/tpl/vintage/page.footer.html
@@ -26,6 +26,7 @@
26<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script> 26<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
27{/if} 27{/if}
28 28
29<script src="js/shaarli.js"></script>
29{loop="$plugins_footer.js_files"} 30{loop="$plugins_footer.js_files"}
30 <script src="{$value}#"></script> 31 <script src="{$value}#"></script>
31{/loop} 32{/loop}
diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html
index cce61ec4..8a58844e 100644
--- a/tpl/vintage/page.header.html
+++ b/tpl/vintage/page.header.html
@@ -1,5 +1,5 @@
1 1
2<div id="logo" title="Share your links !" onclick="document.location='?';"></div> 2<div id="logo" title="Share your links !" onclick="document.location='{$titleLink}';"></div>
3 3
4<div id="linkcount" class="nomobile"> 4<div id="linkcount" class="nomobile">
5 {if="!empty($linkcount)"}{$linkcount} links{/if}<br> 5 {if="!empty($linkcount)"}{$linkcount} links{/if}<br>
@@ -16,7 +16,7 @@
16{if="!empty($_GET['source']) && $_GET['source']=='bookmarklet'"} 16{if="!empty($_GET['source']) && $_GET['source']=='bookmarklet'"}
17 {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore} 17 {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore}
18{else} 18{else}
19<li><a href="?" class="nomobile">Home</a></li> 19<li><a href="{$titleLink}" class="nomobile">Home</a></li>
20 {if="isLoggedIn()"} 20 {if="isLoggedIn()"}
21 <li><a href="?do=logout">Logout</a></li> 21 <li><a href="?do=logout">Logout</a></li>
22 <li><a href="?do=tools">Tools</a></li> 22 <li><a href="?do=tools">Tools</a></li>
diff --git a/tpl/vintage/tagcloud.html b/tpl/vintage/tag.cloud.html
index 05e45273..d93bf4f9 100644
--- a/tpl/vintage/tagcloud.html
+++ b/tpl/vintage/tag.cloud.html
@@ -12,7 +12,7 @@
12 12
13 <div id="cloudtag"> 13 <div id="cloudtag">
14 {loop="$tags"} 14 {loop="$tags"}
15 <span class="count">{$value.count}</span><a 15 <a href="?addtag={$key|urlencode}" class="count">{$value.count}</a><a
16 href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a> 16 href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a>
17 {loop="$value.tag_plugin"} 17 {loop="$value.tag_plugin"}
18 {$value} 18 {$value}