aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.dockerignore3
-rw-r--r--.gitattributes1
-rw-r--r--.htaccess7
-rw-r--r--Dockerfile2
-rw-r--r--Dockerfile.armhf47
-rw-r--r--application/api/ApiUtils.php16
-rw-r--r--application/api/controllers/Links.php8
-rw-r--r--application/api/controllers/Tags.php161
-rw-r--r--application/api/exceptions/ApiTagNotFoundException.php32
-rw-r--r--doc/md/REST-API.md19
-rw-r--r--doc/md/Upgrade-and-migration.md4
-rw-r--r--doc/md/docker/reverse-proxy-configuration.md7
-rw-r--r--doc/md/guides/backup-restore-import-export.md (renamed from doc/md/Backup,-restore,-import-and-export.md)0
-rw-r--r--doc/md/guides/images/01-create-droplet-distro.jpgbin0 -> 20909 bytes
-rw-r--r--doc/md/guides/images/02-create-droplet-region.jpgbin0 -> 21603 bytes
-rw-r--r--doc/md/guides/images/03-create-droplet-size.jpgbin0 -> 20860 bytes
-rw-r--r--doc/md/guides/images/04-finalize.jpgbin0 -> 28233 bytes
-rw-r--r--doc/md/guides/images/05-droplet.jpgbin0 -> 11977 bytes
-rw-r--r--doc/md/guides/images/06-domain.jpgbin0 -> 4499 bytes
-rw-r--r--doc/md/guides/images/07-installation.jpgbin0 -> 42832 bytes
-rw-r--r--doc/md/guides/install-shaarli-with-debian9-and-docker.md257
-rw-r--r--doc/md/guides/various-hacks.md (renamed from doc/md/Various-hacks.md)0
-rw-r--r--docker-compose.yml61
-rw-r--r--index.php8
-rw-r--r--mkdocs.yml12
-rw-r--r--tests/api/controllers/history/HistoryTest.php (renamed from tests/api/controllers/HistoryTest.php)0
-rw-r--r--tests/api/controllers/info/InfoTest.php (renamed from tests/api/controllers/InfoTest.php)0
-rw-r--r--tests/api/controllers/links/DeleteLinkTest.php (renamed from tests/api/controllers/DeleteLinkTest.php)0
-rw-r--r--tests/api/controllers/links/GetLinkIdTest.php (renamed from tests/api/controllers/GetLinkIdTest.php)0
-rw-r--r--tests/api/controllers/links/GetLinksTest.php (renamed from tests/api/controllers/GetLinksTest.php)0
-rw-r--r--tests/api/controllers/links/PostLinkTest.php (renamed from tests/api/controllers/PostLinkTest.php)0
-rw-r--r--tests/api/controllers/links/PutLinkTest.php (renamed from tests/api/controllers/PutLinkTest.php)0
-rw-r--r--tests/api/controllers/tags/DeleteTagTest.php164
-rw-r--r--tests/api/controllers/tags/GetTagNameTest.php129
-rw-r--r--tests/api/controllers/tags/GetTagsTest.php209
-rw-r--r--tests/api/controllers/tags/PutTagTest.php209
-rw-r--r--tpl/default/pluginsadmin.html2
-rw-r--r--tpl/vintage/page.footer.html2
38 files changed, 1333 insertions, 27 deletions
diff --git a/.dockerignore b/.dockerignore
index a0d28dc6..96fd31c5 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -4,6 +4,9 @@
4.github 4.github
5tests 5tests
6 6
7# Docker Compose resources
8docker-compose.yml
9
7# Shaarli runtime resources 10# Shaarli runtime resources
8cache/* 11cache/*
9data/* 12data/*
diff --git a/.gitattributes b/.gitattributes
index 9d22f11b..9a92bc37 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -35,6 +35,7 @@ doc/**/*.json export-ignore
35doc/**/*.md export-ignore 35doc/**/*.md export-ignore
36.docker/ export-ignore 36.docker/ export-ignore
37.dockerignore export-ignore 37.dockerignore export-ignore
38docker-compose.* export-ignore
38Dockerfile* export-ignore 39Dockerfile* export-ignore
39Doxyfile export-ignore 40Doxyfile export-ignore
40Makefile export-ignore 41Makefile export-ignore
diff --git a/.htaccess b/.htaccess
index 7ba4744b..b238854c 100644
--- a/.htaccess
+++ b/.htaccess
@@ -14,3 +14,10 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
14RewriteCond %{REQUEST_FILENAME} !-f 14RewriteCond %{REQUEST_FILENAME} !-f
15RewriteCond %{REQUEST_FILENAME} !-d 15RewriteCond %{REQUEST_FILENAME} !-d
16RewriteRule ^ index.php [QSA,L] 16RewriteRule ^ index.php [QSA,L]
17
18<Limit GET POST PUT DELETE OPTIONS>
19 Require all granted
20</Limit>
21<LimitExcept GET POST PUT DELETE OPTIONS>
22 Require all denied
23</LimitExcept>
diff --git a/Dockerfile b/Dockerfile
index 6261e81b..d8921cee 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,7 +25,7 @@ RUN cd shaarli \
25 25
26# Stage 4: 26# Stage 4:
27# - Shaarli image 27# - Shaarli image
28FROM alpine:3.7 28FROM alpine:3.8
29LABEL maintainer="Shaarli Community" 29LABEL maintainer="Shaarli Community"
30 30
31RUN apk --update --no-cache add \ 31RUN apk --update --no-cache add \
diff --git a/Dockerfile.armhf b/Dockerfile.armhf
index 5dcc34aa..1185e2df 100644
--- a/Dockerfile.armhf
+++ b/Dockerfile.armhf
@@ -1,9 +1,38 @@
1FROM lsiobase/alpine.armhf:3.6 1# Stage 1:
2# - Copy Shaarli sources
3# - Build documentation
4FROM arm32v6/alpine:3.8 as docs
5ADD . /usr/src/app/shaarli
6RUN apk --update --no-cache add py2-pip \
7 && cd /usr/src/app/shaarli \
8 && pip install --no-cache-dir mkdocs \
9 && mkdocs build --clean
10
11# Stage 2:
12# - Resolve PHP dependencies with Composer
13FROM arm32v6/alpine:3.8 as composer
14COPY --from=docs /usr/src/app/shaarli /app/shaarli
15RUN apk --update --no-cache add php7-mbstring composer \
16 && cd /app/shaarli \
17 && composer --prefer-dist --no-dev install
18
19# Stage 3:
20# - Frontend dependencies
21FROM arm32v6/alpine:3.8 as node
22COPY --from=composer /app/shaarli /shaarli
23RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
24 && cd /shaarli \
25 && yarn install \
26 && yarn run build \
27 && rm -rf node_modules
28
29# Stage 4:
30# - Shaarli image
31FROM arm32v6/alpine:3.8
2LABEL maintainer="Shaarli Community" 32LABEL maintainer="Shaarli Community"
3 33
4RUN apk --update --no-cache add \ 34RUN apk --update --no-cache add \
5 ca-certificates \ 35 ca-certificates \
6 curl \
7 nginx \ 36 nginx \
8 php7 \ 37 php7 \
9 php7-ctype \ 38 php7-ctype \
@@ -15,7 +44,6 @@ RUN apk --update --no-cache add \
15 php7-json \ 44 php7-json \
16 php7-mbstring \ 45 php7-mbstring \
17 php7-openssl \ 46 php7-openssl \
18 php7-phar \
19 php7-session \ 47 php7-session \
20 php7-xml \ 48 php7-xml \
21 php7-zlib \ 49 php7-zlib \
@@ -25,22 +53,19 @@ COPY .docker/nginx.conf /etc/nginx/nginx.conf
25COPY .docker/php-fpm.conf /etc/php7/php-fpm.conf 53COPY .docker/php-fpm.conf /etc/php7/php-fpm.conf
26COPY .docker/services.d /etc/services.d 54COPY .docker/services.d /etc/services.d
27 55
28RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \ 56RUN rm -rf /etc/php7/php-fpm.d/www.conf \
29 && rm -rf /etc/php7/php-fpm.d/www.conf \
30 && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \ 57 && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
31 && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini 58 && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
32 59
33 60
34WORKDIR /var/www 61WORKDIR /var/www
35RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \ 62COPY --from=node /shaarli /var/www/shaarli
36 && mv Shaarli-master shaarli \ 63
37 && cd shaarli \ 64RUN chown -R nginx:nginx . \
38 && composer --prefer-dist --no-dev install \
39 && rm -rf ~/.composer \
40 && chown -R nginx:nginx . \
41 && ln -sf /dev/stdout /var/log/nginx/shaarli.access.log \ 65 && ln -sf /dev/stdout /var/log/nginx/shaarli.access.log \
42 && ln -sf /dev/stderr /var/log/nginx/shaarli.error.log 66 && ln -sf /dev/stderr /var/log/nginx/shaarli.error.log
43 67
68VOLUME /var/www/shaarli/cache
44VOLUME /var/www/shaarli/data 69VOLUME /var/www/shaarli/data
45 70
46EXPOSE 80 71EXPOSE 80
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index f154bb52..fc5ecaf1 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -134,4 +134,20 @@ class ApiUtils
134 134
135 return $oldLink; 135 return $oldLink;
136 } 136 }
137
138 /**
139 * Format a Tag for the REST API.
140 *
141 * @param string $tag Tag name
142 * @param int $occurrences Number of links using this tag
143 *
144 * @return array Link data formatted for the REST API.
145 */
146 public static function formatTag($tag, $occurences)
147 {
148 return [
149 'name' => $tag,
150 'occurrences' => $occurences,
151 ];
152 }
137} 153}
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index 3a9c0355..ffcfd4c7 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -68,16 +68,16 @@ class Links extends ApiController
68 } 68 }
69 69
70 // 'environment' is set by Slim and encapsulate $_SERVER. 70 // 'environment' is set by Slim and encapsulate $_SERVER.
71 $index = index_url($this->ci['environment']); 71 $indexUrl = index_url($this->ci['environment']);
72 72
73 $out = []; 73 $out = [];
74 $cpt = 0; 74 $index = 0;
75 foreach ($links as $link) { 75 foreach ($links as $link) {
76 if (count($out) >= $limit) { 76 if (count($out) >= $limit) {
77 break; 77 break;
78 } 78 }
79 if ($cpt++ >= $offset) { 79 if ($index++ >= $offset) {
80 $out[] = ApiUtils::formatLink($link, $index); 80 $out[] = ApiUtils::formatLink($link, $indexUrl);
81 } 81 }
82 } 82 }
83 83
diff --git a/application/api/controllers/Tags.php b/application/api/controllers/Tags.php
new file mode 100644
index 00000000..6dd78750
--- /dev/null
+++ b/application/api/controllers/Tags.php
@@ -0,0 +1,161 @@
1<?php
2
3namespace Shaarli\Api\Controllers;
4
5use Shaarli\Api\ApiUtils;
6use Shaarli\Api\Exceptions\ApiBadParametersException;
7use Shaarli\Api\Exceptions\ApiLinkNotFoundException;
8use Shaarli\Api\Exceptions\ApiTagNotFoundException;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class Tags
14 *
15 * REST API Controller: all services related to tags collection.
16 *
17 * @package Api\Controllers
18 */
19class Tags extends ApiController
20{
21 /**
22 * @var int Number of links returned if no limit is provided.
23 */
24 public static $DEFAULT_LIMIT = 'all';
25
26 /**
27 * Retrieve a list of tags, allowing different filters.
28 *
29 * @param Request $request Slim request.
30 * @param Response $response Slim response.
31 *
32 * @return Response response.
33 *
34 * @throws ApiBadParametersException Invalid parameters.
35 */
36 public function getTags($request, $response)
37 {
38 $visibility = $request->getParam('visibility');
39 $tags = $this->linkDb->linksCountPerTag([], $visibility);
40
41 // Return tags from the {offset}th tag, starting from 0.
42 $offset = $request->getParam('offset');
43 if (! empty($offset) && ! ctype_digit($offset)) {
44 throw new ApiBadParametersException('Invalid offset');
45 }
46 $offset = ! empty($offset) ? intval($offset) : 0;
47 if ($offset > count($tags)) {
48 return $response->withJson([], 200, $this->jsonStyle);
49 }
50
51 // limit parameter is either a number of links or 'all' for everything.
52 $limit = $request->getParam('limit');
53 if (empty($limit)) {
54 $limit = self::$DEFAULT_LIMIT;
55 }
56 if (ctype_digit($limit)) {
57 $limit = intval($limit);
58 } elseif ($limit === 'all') {
59 $limit = count($tags);
60 } else {
61 throw new ApiBadParametersException('Invalid limit');
62 }
63
64 $out = [];
65 $index = 0;
66 foreach ($tags as $tag => $occurrences) {
67 if (count($out) >= $limit) {
68 break;
69 }
70 if ($index++ >= $offset) {
71 $out[] = ApiUtils::formatTag($tag, $occurrences);
72 }
73 }
74
75 return $response->withJson($out, 200, $this->jsonStyle);
76 }
77
78 /**
79 * Return a single formatted tag by its name.
80 *
81 * @param Request $request Slim request.
82 * @param Response $response Slim response.
83 * @param array $args Path parameters. including the tag name.
84 *
85 * @return Response containing the link array.
86 *
87 * @throws ApiTagNotFoundException generating a 404 error.
88 */
89 public function getTag($request, $response, $args)
90 {
91 $tags = $this->linkDb->linksCountPerTag();
92 if (!isset($tags[$args['tagName']])) {
93 throw new ApiTagNotFoundException();
94 }
95 $out = ApiUtils::formatTag($args['tagName'], $tags[$args['tagName']]);
96
97 return $response->withJson($out, 200, $this->jsonStyle);
98 }
99
100 /**
101 * Rename a tag from the given name.
102 * If the new name provided matches an existing tag, they will be merged.
103 *
104 * @param Request $request Slim request.
105 * @param Response $response Slim response.
106 * @param array $args Path parameters. including the tag name.
107 *
108 * @return Response response.
109 *
110 * @throws ApiTagNotFoundException generating a 404 error.
111 * @throws ApiBadParametersException new tag name not provided
112 */
113 public function putTag($request, $response, $args)
114 {
115 $tags = $this->linkDb->linksCountPerTag();
116 if (! isset($tags[$args['tagName']])) {
117 throw new ApiTagNotFoundException();
118 }
119
120 $data = $request->getParsedBody();
121 if (empty($data['name'])) {
122 throw new ApiBadParametersException('New tag name is required in the request body');
123 }
124
125 $updated = $this->linkDb->renameTag($args['tagName'], $data['name']);
126 $this->linkDb->save($this->conf->get('resource.page_cache'));
127 foreach ($updated as $link) {
128 $this->history->updateLink($link);
129 }
130
131 $tags = $this->linkDb->linksCountPerTag();
132 $out = ApiUtils::formatTag($data['name'], $tags[$data['name']]);
133 return $response->withJson($out, 200, $this->jsonStyle);
134 }
135
136 /**
137 * Delete an existing tag by its name.
138 *
139 * @param Request $request Slim request.
140 * @param Response $response Slim response.
141 * @param array $args Path parameters. including the tag name.
142 *
143 * @return Response response.
144 *
145 * @throws ApiTagNotFoundException generating a 404 error.
146 */
147 public function deleteTag($request, $response, $args)
148 {
149 $tags = $this->linkDb->linksCountPerTag();
150 if (! isset($tags[$args['tagName']])) {
151 throw new ApiTagNotFoundException();
152 }
153 $updated = $this->linkDb->renameTag($args['tagName'], null);
154 $this->linkDb->save($this->conf->get('resource.page_cache'));
155 foreach ($updated as $link) {
156 $this->history->updateLink($link);
157 }
158
159 return $response->withStatus(204);
160 }
161}
diff --git a/application/api/exceptions/ApiTagNotFoundException.php b/application/api/exceptions/ApiTagNotFoundException.php
new file mode 100644
index 00000000..eed5afa5
--- /dev/null
+++ b/application/api/exceptions/ApiTagNotFoundException.php
@@ -0,0 +1,32 @@
1<?php
2
3namespace Shaarli\Api\Exceptions;
4
5
6use Slim\Http\Response;
7
8/**
9 * Class ApiTagNotFoundException
10 *
11 * Tag selected by name couldn't be found in the datastore, results in a 404 error.
12 *
13 * @package Shaarli\Api\Exceptions
14 */
15class ApiTagNotFoundException extends ApiException
16{
17 /**
18 * ApiLinkNotFoundException constructor.
19 */
20 public function __construct()
21 {
22 $this->message = 'Tag not found';
23 }
24
25 /**
26 * {@inheritdoc}
27 */
28 public function getApiResponse()
29 {
30 return $this->buildApiResponse(404);
31 }
32}
diff --git a/doc/md/REST-API.md b/doc/md/REST-API.md
index c016de56..11bd1cd2 100644
--- a/doc/md/REST-API.md
+++ b/doc/md/REST-API.md
@@ -152,3 +152,22 @@ See the reference API client:
152 152
153- [Documentation](http://python-shaarli-client.readthedocs.io/en/latest/) on ReadTheDocs 153- [Documentation](http://python-shaarli-client.readthedocs.io/en/latest/) on ReadTheDocs
154- [python-shaarli-client](https://github.com/shaarli/python-shaarli-client) on Github 154- [python-shaarli-client](https://github.com/shaarli/python-shaarli-client) on Github
155
156## Troubleshooting
157
158### Debug mode
159
160> This should never be used in a production environment.
161
162For security reasons, authentication issues will always return an `HTTP 401` error code without any detail.
163
164It is possible to enable the debug mode in `config.json.php`
165to get the actual error message in the HTTP response body with:
166
167```json
168{
169 "dev": {
170 "debug": true
171 }
172}
173```
diff --git a/doc/md/Upgrade-and-migration.md b/doc/md/Upgrade-and-migration.md
index 451ca36d..d5682a34 100644
--- a/doc/md/Upgrade-and-migration.md
+++ b/doc/md/Upgrade-and-migration.md
@@ -27,7 +27,7 @@ As all user data is kept under `data`, this is the only directory you need to wo
27 27
28- backup the `data` directory 28- backup the `data` directory
29- install or update Shaarli: 29- install or update Shaarli:
30 - fresh installation - see [Download and installation](Download-and-installation) 30 - fresh installation - see [Download and Installation](Download-and-Installation)
31 - update - see the following sections 31 - update - see the following sections
32- check or restore the `data` directory 32- check or restore the `data` directory
33 33
@@ -35,7 +35,7 @@ As all user data is kept under `data`, this is the only directory you need to wo
35 35
36All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page. 36All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page.
37 37
38We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation) for `git` complete instructions. 38We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and Installation](Download-and-Installation) for `git` complete instructions.
39 39
40Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! 40Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
41 41
diff --git a/doc/md/docker/reverse-proxy-configuration.md b/doc/md/docker/reverse-proxy-configuration.md
index 6066140e..e53c9422 100644
--- a/doc/md/docker/reverse-proxy-configuration.md
+++ b/doc/md/docker/reverse-proxy-configuration.md
@@ -13,12 +13,14 @@ This guide assumes that:
13 - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html) 13 - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
14 - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers) 14 - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)
15 15
16The following HTTP headers are set by using the `ProxyPass` directive: 16The following HTTP headers are set when the `ProxyPass` directive is set:
17 17
18- `X-Forwarded-For` 18- `X-Forwarded-For`
19- `X-Forwarded-Host` 19- `X-Forwarded-Host`
20- `X-Forwarded-Server` 20- `X-Forwarded-Server`
21 21
22The original `SERVER_NAME` can be sent to the proxied host by setting the [`ProxyPreserveHost`](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#ProxyPreserveHost) directive to `On`.
23
22```apache 24```apache
23<VirtualHost *:80> 25<VirtualHost *:80>
24 ServerName shaarli.domain.tld 26 ServerName shaarli.domain.tld
@@ -37,7 +39,8 @@ The following HTTP headers are set by using the `ProxyPass` directive:
37 CustomLog /var/log/apache2/shaarli-access.log combined 39 CustomLog /var/log/apache2/shaarli-access.log combined
38 40
39 RequestHeader set X-Forwarded-Proto "https" 41 RequestHeader set X-Forwarded-Proto "https"
40 42 ProxyPreserveHost On
43
41 ProxyPass / http://127.0.0.1:10080/ 44 ProxyPass / http://127.0.0.1:10080/
42 ProxyPassReverse / http://127.0.0.1:10080/ 45 ProxyPassReverse / http://127.0.0.1:10080/
43</VirtualHost> 46</VirtualHost>
diff --git a/doc/md/Backup,-restore,-import-and-export.md b/doc/md/guides/backup-restore-import-export.md
index bb790074..bb790074 100644
--- a/doc/md/Backup,-restore,-import-and-export.md
+++ b/doc/md/guides/backup-restore-import-export.md
diff --git a/doc/md/guides/images/01-create-droplet-distro.jpg b/doc/md/guides/images/01-create-droplet-distro.jpg
new file mode 100644
index 00000000..63682ba8
--- /dev/null
+++ b/doc/md/guides/images/01-create-droplet-distro.jpg
Binary files differ
diff --git a/doc/md/guides/images/02-create-droplet-region.jpg b/doc/md/guides/images/02-create-droplet-region.jpg
new file mode 100644
index 00000000..135a78be
--- /dev/null
+++ b/doc/md/guides/images/02-create-droplet-region.jpg
Binary files differ
diff --git a/doc/md/guides/images/03-create-droplet-size.jpg b/doc/md/guides/images/03-create-droplet-size.jpg
new file mode 100644
index 00000000..aa5b2fd2
--- /dev/null
+++ b/doc/md/guides/images/03-create-droplet-size.jpg
Binary files differ
diff --git a/doc/md/guides/images/04-finalize.jpg b/doc/md/guides/images/04-finalize.jpg
new file mode 100644
index 00000000..68ec0dc5
--- /dev/null
+++ b/doc/md/guides/images/04-finalize.jpg
Binary files differ
diff --git a/doc/md/guides/images/05-droplet.jpg b/doc/md/guides/images/05-droplet.jpg
new file mode 100644
index 00000000..44e93a1e
--- /dev/null
+++ b/doc/md/guides/images/05-droplet.jpg
Binary files differ
diff --git a/doc/md/guides/images/06-domain.jpg b/doc/md/guides/images/06-domain.jpg
new file mode 100644
index 00000000..5827dd93
--- /dev/null
+++ b/doc/md/guides/images/06-domain.jpg
Binary files differ
diff --git a/doc/md/guides/images/07-installation.jpg b/doc/md/guides/images/07-installation.jpg
new file mode 100644
index 00000000..42cc9f10
--- /dev/null
+++ b/doc/md/guides/images/07-installation.jpg
Binary files differ
diff --git a/doc/md/guides/install-shaarli-with-debian9-and-docker.md b/doc/md/guides/install-shaarli-with-debian9-and-docker.md
new file mode 100644
index 00000000..f1b26d47
--- /dev/null
+++ b/doc/md/guides/install-shaarli-with-debian9-and-docker.md
@@ -0,0 +1,257 @@
1_Last updated on 2018-07-01._
2
3## Goals
4- Getting a Virtual Private Server (VPS)
5- Running Shaarli:
6 - as a Docker container,
7 - using the Træfik reverse proxy,
8 - securized with TLS certificates from Let's Encrypt.
9
10
11The following components and tools will be used:
12
13- [Debian](https://www.debian.org/), a GNU/Linux distribution widely used in
14 server environments;
15- [Docker](https://docs.docker.com/engine/docker-overview/), an open platform
16 for developing, shipping, and running applications;
17- [Docker Compose](https://docs.docker.com/compose/), a tool for defining and
18 running multi-container Docker applications.
19
20
21More information can be found in the [Resources](#resources) section at the
22bottom of the guide.
23
24## Getting a Virtual Private Server
25For this guide, I went for the smallest VPS available from DigitalOcean,
26a Droplet with 1 CPU, 1 GiB RAM and 25 GiB SSD storage, which costs
27$5/month ($0.007/hour):
28
29- [Droplets Overview](https://www.digitalocean.com/docs/droplets/overview/)
30- [Pricing](https://www.digitalocean.com/pricing/)
31- [How to Create a Droplet from the DigitalOcean Control Panel](https://www.digitalocean.com/docs/droplets/how-to/create/)
32- [How to Add SSH Keys to Droplets](https://www.digitalocean.com/docs/droplets/how-to/add-ssh-keys/)
33- [Initial Server Setup with Debian 8](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-debian-8) (also applies to Debian 9)
34- [An Introduction to Securing your Linux VPS](https://www.digitalocean.com/community/tutorials/an-introduction-to-securing-your-linux-vps)
35
36### Creating a Droplet
37Select `Debian 9` as the Droplet distribution:
38
39<img src="../images/01-create-droplet-distro.jpg"
40 width="500px"
41 alt="Droplet distribution" />
42
43Choose a region that is geographically close to you:
44
45<img src="../images/02-create-droplet-region.jpg"
46 width="500px"
47 alt="Droplet region" />
48
49Choose a Droplet size that corresponds to your usage and budget:
50
51<img src="../images/03-create-droplet-size.jpg"
52 width="500px"
53 alt="Droplet size" />
54
55Finalize the Droplet creation:
56
57<img src="../images/04-finalize.jpg"
58 width="500px"
59 alt="Droplet finalization" />
60
61Droplet information is displayed on the Control Panel:
62
63<img src="../images/05-droplet.jpg"
64 width="500px"
65 alt="Droplet summary" />
66
67Once your VPS has been created, you will receive an e-mail with connection
68instructions.
69
70## Obtaining a domain name
71After creating your VPS, it will be reachable using its IP address; some hosting
72providers also create a DNS record, e.g. `ns4853142.ip-01-47-127.eu`.
73
74A domain name (DNS record) is required to obtain a certificate and setup HTTPS
75(HTTP with TLS encryption).
76
77Domain names can be obtained from registrars through hosting providers such as
78[Gandi](https://www.gandi.net/en/domain).
79
80Once you have your own domain, you need to create a new DNS record that points
81to your VPS' IP address:
82
83<img src="../images/06-domain.jpg"
84 width="650px"
85 alt="Domain configuration" />
86
87## Host setup
88Now's the time to connect to your freshly created VPS!
89
90```shell
91$ ssh root@188.166.85.8
92
93Linux stretch-shaarli-02 4.9.0-6-amd64 #1 SMP Debian 4.9.88-1+deb9u1 (2018-05-07) x86_64
94
95The programs included with the Debian GNU/Linux system are free software;
96the exact distribution terms for each program are described in the
97individual files in /usr/share/doc/*/copyright.
98
99Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
100permitted by applicable law.
101Last login: Sun Jul 1 11:20:18 2018 from <REDACTED>
102
103root@stretch-shaarli-02:~$
104```
105
106### Updating the system
107```shell
108root@stretch-shaarli-02:~$ apt update && apt upgrade -y
109```
110
111### Setting up Docker
112_The following instructions are from the
113[Get Docker CE for Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
114guide._
115
116Install package dependencies:
117
118```shell
119root@stretch-shaarli-02:~$ apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common
120```
121
122Add Docker's package repository GPG key:
123
124```shell
125root@stretch-shaarli-02:~$ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
126```
127
128Add Docker's package repository:
129
130```shell
131root@stretch-shaarli-02:~$ add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian stretch stable"
132```
133
134Update package lists and install Docker:
135
136```shell
137root@stretch-shaarli-02:~$ apt update && apt install -y docker-ce
138```
139
140Verify Docker is properly configured by running the `hello-world` image:
141
142```shell
143root@stretch-shaarli-02:~$ docker run hello-world
144```
145
146### Setting up Docker Compose
147_The following instructions are from the
148[Install Docker Compose](https://docs.docker.com/compose/install/)
149guide._
150
151Download the current version from the release page:
152
153```shell
154root@stretch-shaarli-02:~$ curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
155root@stretch-shaarli-02:~$ chmod +x /usr/local/bin/docker-compose
156```
157
158## Running Shaarli
159Shaarli comes with a configuration file for Docker Compose, that will setup:
160
161- a local Docker network
162- a Docker [volume](https://docs.docker.com/storage/volumes/) to store Shaarli data
163- a Docker [volume](https://docs.docker.com/storage/volumes/) to store Træfik TLS configuration and certificates
164- a [Shaarli](https://hub.docker.com/r/shaarli/shaarli/) instance
165- a [Træfik](https://hub.docker.com/_/traefik/) instance
166
167[Træfik](https://docs.traefik.io/) is a modern HTTP reverse proxy, with native
168support for Docker and [Let's Encrypt](https://letsencrypt.org/).
169
170### Compose configuration
171Create a new directory to store the configuration:
172
173```shell
174root@stretch-shaarli-02:~$ mkdir shaarli && cd shaarli
175root@stretch-shaarli-02:~/shaarli$
176```
177
178Download the current version of Shaarli's `docker-compose.yml`:
179
180```shell
181root@stretch-shaarli-02:~/shaarli$ curl -L https://raw.githubusercontent.com/shaarli/Shaarli/master/docker-compose.yml -o docker-compose.yml
182```
183
184Create the `.env` file and fill in your VPS and domain information (replace
185`<MY_SHAARLI_DOMAIN>` and `<MY_CONTACT_EMAIL>` with your actual information):
186
187```shell
188root@stretch-shaarli-02:~/shaarli$ vim .env
189```
190
191```shell
192SHAARLI_VIRTUAL_HOST=<MY_SHAARLI_DOMAIN>
193SHAARLI_LETSENCRYPT_EMAIL=<MY_CONTACT_EMAIL>
194```
195
196### Pull the Docker images
197```shell
198root@stretch-shaarli-02:~/shaarli$ docker-compose pull
199Pulling shaarli ... done
200Pulling traefik ... done
201```
202
203### Run!
204```shell
205root@stretch-shaarli-02:~/shaarli$ docker-compose up -d
206Creating network "shaarli_http-proxy" with the default driver
207Creating volume "shaarli_traefik-acme" with default driver
208Creating volume "shaarli_shaarli-data" with default driver
209Creating shaarli_shaarli_1 ... done
210Creating shaarli_traefik_1 ... done
211```
212
213## Conclusion
214Congratulations! Your Shaarli instance should be up and running, and available
215at `https://<MY_SHAARLI_DOMAIN>`.
216
217<img src="../images/07-installation.jpg"
218 width="500px"
219 alt="Shaarli installation page" />
220
221## Resources
222### Related Shaarli documentation
223- [Docker 101](../docker/docker-101.md)
224- [Shaarli images](../docker/shaarli-images.md)
225
226### Hosting providers
227- [DigitalOcean](https://www.digitalocean.com/)
228- [Gandi](https://www.gandi.net/en)
229- [OVH](https://www.ovh.co.uk/)
230- [RackSpace](https://www.rackspace.com/)
231- etc.
232
233### Domain Names and Registrars
234- [Introduction to the Domain Name System (DNS)](https://opensource.com/article/17/4/introduction-domain-name-system-dns)
235- [ICANN](https://www.icann.org/)
236- [Domain name registrar](https://en.wikipedia.org/wiki/Domain_name_registrar)
237- [OVH Domain Registration](https://www.ovh.co.uk/domains/)
238- [Gandi Domain Registration](https://www.gandi.net/en/domain)
239
240### HTTPS and Security
241- [Transport Layer Security](https://en.wikipedia.org/wiki/Transport_Layer_Security)
242- [Let's Encrypt](https://letsencrypt.org/)
243
244### Docker
245- [Docker Overview](https://docs.docker.com/engine/docker-overview/)
246- [Docker Documentation](https://docs.docker.com/)
247- [Get Docker CE for Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
248- [docker logs](https://docs.docker.com/engine/reference/commandline/logs/)
249- [Volumes](https://docs.docker.com/storage/volumes/)
250- [Install Docker Compose](https://docs.docker.com/compose/install/)
251- [docker-compose logs](https://docs.docker.com/compose/reference/logs/)
252
253### Træfik
254- [Getting Started](https://docs.traefik.io/)
255- [Docker backend](https://docs.traefik.io/configuration/backends/docker/)
256- [Let's Encrypt and Docker](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/)
257- [traefik](https://hub.docker.com/_/traefik/) Docker image
diff --git a/doc/md/Various-hacks.md b/doc/md/guides/various-hacks.md
index 0074ae9f..0074ae9f 100644
--- a/doc/md/Various-hacks.md
+++ b/doc/md/guides/various-hacks.md
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..e8ea4271
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,61 @@
1---
2# Shaarli - Docker Compose example configuration
3#
4# See:
5# - https://shaarli.readthedocs.io/en/master/docker/shaarli-images/
6# - https://shaarli.readthedocs.io/en/master/guides/install-shaarli-with-debian9-and-docker/
7#
8# Environment variables:
9# - SHAARLI_VIRTUAL_HOST Fully Qualified Domain Name for the Shaarli instance
10# - SHAARLI_LETSENCRYPT_EMAIL Contact email for certificate renewal
11version: '3'
12
13networks:
14 http-proxy:
15
16volumes:
17 traefik-acme:
18 shaarli-cache:
19 shaarli-data:
20
21services:
22 shaarli:
23 image: shaarli/shaarli:master
24 build: ./
25 networks:
26 - http-proxy
27 volumes:
28 - shaarli-cache:/var/www/shaarli/cache
29 - shaarli-data:/var/www/shaarli/data
30 labels:
31 traefik.domain: "${SHAARLI_VIRTUAL_HOST}"
32 traefik.backend: shaarli
33 traefik.frontend.rule: "Host:${SHAARLI_VIRTUAL_HOST}"
34
35 traefik:
36 image: traefik
37 command:
38 - "--defaultentrypoints=http,https"
39 - "--entrypoints=Name:http Address::80 Redirect.EntryPoint:https"
40 - "--entrypoints=Name:https Address::443 TLS"
41 - "--retry"
42 - "--docker"
43 - "--docker.domain=docker.localhost"
44 - "--docker.exposedbydefault=true"
45 - "--docker.watch=true"
46 - "--acme"
47 - "--acme.domains=${SHAARLI_VIRTUAL_HOST}"
48 - "--acme.email=${SHAARLI_LETSENCRYPT_EMAIL}"
49 - "--acme.entrypoint=https"
50 - "--acme.onhostrule=true"
51 - "--acme.storage=/acme/acme.json"
52 - "--acme.httpchallenge"
53 - "--acme.httpchallenge.entrypoint=http"
54 networks:
55 - http-proxy
56 ports:
57 - 80:80
58 - 443:443
59 volumes:
60 - /var/run/docker.sock:/var/run/docker.sock:ro
61 - traefik-acme:/acme
diff --git a/index.php b/index.php
index ac0baf7d..1480bbc5 100644
--- a/index.php
+++ b/index.php
@@ -1121,7 +1121,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1121 // Linkdate is kept here to: 1121 // Linkdate is kept here to:
1122 // - use the same permalink for notes as they're displayed when creating them 1122 // - use the same permalink for notes as they're displayed when creating them
1123 // - let users hack creation date of their posts 1123 // - let users hack creation date of their posts
1124 // See: https://shaarli.readthedocs.io/en/master/Various-hacks/#changing-the-timestamp-for-a-shaare 1124 // See: https://shaarli.readthedocs.io/en/master/guides/various-hacks/#changing-the-timestamp-for-a-shaare
1125 $linkdate = escape($_POST['lf_linkdate']); 1125 $linkdate = escape($_POST['lf_linkdate']);
1126 if (isset($LINKSDB[$id])) { 1126 if (isset($LINKSDB[$id])) {
1127 // Edit 1127 // Edit
@@ -1845,6 +1845,12 @@ $app->group('/api/v1', function() {
1845 $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); 1845 $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
1846 $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink'); 1846 $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink');
1847 $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink'); 1847 $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink');
1848
1849 $this->get('/tags', '\Shaarli\Api\Controllers\Tags:getTags')->setName('getTags');
1850 $this->get('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:getTag')->setName('getTag');
1851 $this->put('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:putTag')->setName('putTag');
1852 $this->delete('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:deleteTag')->setName('deleteTag');
1853
1848 $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory'); 1854 $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory');
1849})->add('\Shaarli\Api\ApiMiddleware'); 1855})->add('\Shaarli\Api\ApiMiddleware');
1850 1856
diff --git a/mkdocs.yml b/mkdocs.yml
index d03bc149..941fce3a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -5,7 +5,10 @@ site_description: The personal, minimalist, super-fast, database free, bookmarki
5theme: readthedocs 5theme: readthedocs
6docs_dir: doc/md 6docs_dir: doc/md
7site_dir: doc/html 7site_dir: doc/html
8strict: true 8# Disable strict mode until ReadTheDocs provides up-to-date MkDocs settings:
9# - https://github.com/shaarli/Shaarli/issues/1179
10# - https://github.com/rtfd/readthedocs.org/issues/4314
11# strict: true
9 12
10pages: 13pages:
11- Home: index.md 14- Home: index.md
@@ -27,9 +30,10 @@ pages:
27 - RSS feeds: RSS-feeds.md 30 - RSS feeds: RSS-feeds.md
28 - REST API: REST-API.md 31 - REST API: REST-API.md
29 - Community & Related software: Community-&-Related-software.md 32 - Community & Related software: Community-&-Related-software.md
30- How To: 33- Guides:
31 - Backup, restore, import and export: Backup,-restore,-import-and-export.md 34 - Install Shaarli on Debian 9 with Docker: guides/install-shaarli-with-debian9-and-docker.md
32 - Various hacks: Various-hacks.md 35 - Backup, restore, import and export: guides/backup-restore-import-export.md
36 - Various hacks: guides/various-hacks.md
33- Development: 37- Development:
34 - Development guidelines: Development-guidelines.md 38 - Development guidelines: Development-guidelines.md
35 - Continuous integration tools: Continuous-integration-tools.md 39 - Continuous integration tools: Continuous-integration-tools.md
diff --git a/tests/api/controllers/HistoryTest.php b/tests/api/controllers/history/HistoryTest.php
index 61046d97..61046d97 100644
--- a/tests/api/controllers/HistoryTest.php
+++ b/tests/api/controllers/history/HistoryTest.php
diff --git a/tests/api/controllers/InfoTest.php b/tests/api/controllers/info/InfoTest.php
index f7e63bfa..f7e63bfa 100644
--- a/tests/api/controllers/InfoTest.php
+++ b/tests/api/controllers/info/InfoTest.php
diff --git a/tests/api/controllers/DeleteLinkTest.php b/tests/api/controllers/links/DeleteLinkTest.php
index 7d797137..7d797137 100644
--- a/tests/api/controllers/DeleteLinkTest.php
+++ b/tests/api/controllers/links/DeleteLinkTest.php
diff --git a/tests/api/controllers/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php
index 57528d5a..57528d5a 100644
--- a/tests/api/controllers/GetLinkIdTest.php
+++ b/tests/api/controllers/links/GetLinkIdTest.php
diff --git a/tests/api/controllers/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php
index d22ed3bf..d22ed3bf 100644
--- a/tests/api/controllers/GetLinksTest.php
+++ b/tests/api/controllers/links/GetLinksTest.php
diff --git a/tests/api/controllers/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php
index 100a9170..100a9170 100644
--- a/tests/api/controllers/PostLinkTest.php
+++ b/tests/api/controllers/links/PostLinkTest.php
diff --git a/tests/api/controllers/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php
index 8a562571..8a562571 100644
--- a/tests/api/controllers/PutLinkTest.php
+++ b/tests/api/controllers/links/PutLinkTest.php
diff --git a/tests/api/controllers/tags/DeleteTagTest.php b/tests/api/controllers/tags/DeleteTagTest.php
new file mode 100644
index 00000000..e0787ce2
--- /dev/null
+++ b/tests/api/controllers/tags/DeleteTagTest.php
@@ -0,0 +1,164 @@
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 DeleteTagTest 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 Tags 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 Tags($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 tag endpoint: the tag should be removed.
85 */
86 public function testDeleteTagValid()
87 {
88 $tagName = 'gnu';
89 $tags = $this->linkDB->linksCountPerTag();
90 $this->assertTrue($tags[$tagName] > 0);
91 $env = Environment::mock([
92 'REQUEST_METHOD' => 'DELETE',
93 ]);
94 $request = Request::createFromEnvironment($env);
95
96 $response = $this->controller->deleteTag($request, new Response(), ['tagName' => $tagName]);
97 $this->assertEquals(204, $response->getStatusCode());
98 $this->assertEmpty((string) $response->getBody());
99
100 $this->linkDB = new \LinkDB(self::$testDatastore, true, false);
101 $tags = $this->linkDB->linksCountPerTag();
102 $this->assertFalse(isset($tags[$tagName]));
103
104 // 2 links affected
105 $historyEntry = $this->history->getHistory()[0];
106 $this->assertEquals(\History::UPDATED, $historyEntry['event']);
107 $this->assertTrue(
108 (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime']
109 );
110 $historyEntry = $this->history->getHistory()[1];
111 $this->assertEquals(\History::UPDATED, $historyEntry['event']);
112 $this->assertTrue(
113 (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime']
114 );
115 }
116
117 /**
118 * Test DELETE tag endpoint: the tag should be removed.
119 */
120 public function testDeleteTagCaseSensitivity()
121 {
122 $tagName = 'sTuff';
123 $tags = $this->linkDB->linksCountPerTag();
124 $this->assertTrue($tags[$tagName] > 0);
125 $env = Environment::mock([
126 'REQUEST_METHOD' => 'DELETE',
127 ]);
128 $request = Request::createFromEnvironment($env);
129
130 $response = $this->controller->deleteTag($request, new Response(), ['tagName' => $tagName]);
131 $this->assertEquals(204, $response->getStatusCode());
132 $this->assertEmpty((string) $response->getBody());
133
134 $this->linkDB = new \LinkDB(self::$testDatastore, true, false);
135 $tags = $this->linkDB->linksCountPerTag();
136 $this->assertFalse(isset($tags[$tagName]));
137 $this->assertTrue($tags[strtolower($tagName)] > 0);
138
139 $historyEntry = $this->history->getHistory()[0];
140 $this->assertEquals(\History::UPDATED, $historyEntry['event']);
141 $this->assertTrue(
142 (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime']
143 );
144 }
145
146 /**
147 * Test DELETE tag endpoint: reach not existing tag.
148 *
149 * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException
150 * @expectedExceptionMessage Tag not found
151 */
152 public function testDeleteLink404()
153 {
154 $tagName = 'nopenope';
155 $tags = $this->linkDB->linksCountPerTag();
156 $this->assertFalse(isset($tags[$tagName]));
157 $env = Environment::mock([
158 'REQUEST_METHOD' => 'DELETE',
159 ]);
160 $request = Request::createFromEnvironment($env);
161
162 $this->controller->deleteTag($request, new Response(), ['tagName' => $tagName]);
163 }
164}
diff --git a/tests/api/controllers/tags/GetTagNameTest.php b/tests/api/controllers/tags/GetTagNameTest.php
new file mode 100644
index 00000000..afac228e
--- /dev/null
+++ b/tests/api/controllers/tags/GetTagNameTest.php
@@ -0,0 +1,129 @@
1<?php
2
3namespace Shaarli\Api\Controllers;
4
5use Shaarli\Config\ConfigManager;
6
7use Slim\Container;
8use Slim\Http\Environment;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class GetTagNameTest
14 *
15 * Test getTag by tag name API service.
16 *
17 * @package Shaarli\Api\Controllers
18 */
19class GetTagNameTest 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 ConfigManager instance
28 */
29 protected $conf;
30
31 /**
32 * @var \ReferenceLinkDB instance.
33 */
34 protected $refDB = null;
35
36 /**
37 * @var Container instance.
38 */
39 protected $container;
40
41 /**
42 * @var Tags controller instance.
43 */
44 protected $controller;
45
46 /**
47 * Number of JSON fields per link.
48 */
49 const NB_FIELDS_TAG = 2;
50
51 /**
52 * Before each test, instantiate a new Api with its config, plugins and links.
53 */
54 public function setUp()
55 {
56 $this->conf = new ConfigManager('tests/utils/config/configJson');
57 $this->refDB = new \ReferenceLinkDB();
58 $this->refDB->write(self::$testDatastore);
59
60 $this->container = new Container();
61 $this->container['conf'] = $this->conf;
62 $this->container['db'] = new \LinkDB(self::$testDatastore, true, false);
63 $this->container['history'] = null;
64
65 $this->controller = new Tags($this->container);
66 }
67
68 /**
69 * After each test, remove the test datastore.
70 */
71 public function tearDown()
72 {
73 @unlink(self::$testDatastore);
74 }
75
76 /**
77 * Test basic getTag service: return gnu tag with 2 occurrences.
78 */
79 public function testGetTag()
80 {
81 $tagName = 'gnu';
82 $env = Environment::mock([
83 'REQUEST_METHOD' => 'GET',
84 ]);
85 $request = Request::createFromEnvironment($env);
86
87 $response = $this->controller->getTag($request, new Response(), ['tagName' => $tagName]);
88 $this->assertEquals(200, $response->getStatusCode());
89 $data = json_decode((string) $response->getBody(), true);
90 $this->assertEquals(self::NB_FIELDS_TAG, count($data));
91 $this->assertEquals($tagName, $data['name']);
92 $this->assertEquals(2, $data['occurrences']);
93 }
94
95 /**
96 * Test getTag service which is not case sensitive: occurrences with both sTuff and stuff
97 */
98 public function testGetTagNotCaseSensitive()
99 {
100 $tagName = 'sTuff';
101 $env = Environment::mock([
102 'REQUEST_METHOD' => 'GET',
103 ]);
104 $request = Request::createFromEnvironment($env);
105
106 $response = $this->controller->getTag($request, new Response(), ['tagName' => $tagName]);
107 $this->assertEquals(200, $response->getStatusCode());
108 $data = json_decode((string) $response->getBody(), true);
109 $this->assertEquals(self::NB_FIELDS_TAG, count($data));
110 $this->assertEquals($tagName, $data['name']);
111 $this->assertEquals(2, $data['occurrences']);
112 }
113
114 /**
115 * Test basic getTag service: get non existent tag => ApiTagNotFoundException.
116 *
117 * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException
118 * @expectedExceptionMessage Tag not found
119 */
120 public function testGetTag404()
121 {
122 $env = Environment::mock([
123 'REQUEST_METHOD' => 'GET',
124 ]);
125 $request = Request::createFromEnvironment($env);
126
127 $this->controller->getTag($request, new Response(), ['tagName' => 'nopenope']);
128 }
129}
diff --git a/tests/api/controllers/tags/GetTagsTest.php b/tests/api/controllers/tags/GetTagsTest.php
new file mode 100644
index 00000000..3fab31b0
--- /dev/null
+++ b/tests/api/controllers/tags/GetTagsTest.php
@@ -0,0 +1,209 @@
1<?php
2namespace Shaarli\Api\Controllers;
3
4use Shaarli\Config\ConfigManager;
5
6use Slim\Container;
7use Slim\Http\Environment;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class GetTagsTest
13 *
14 * Test get tag list REST API service.
15 *
16 * @package Shaarli\Api\Controllers
17 */
18class GetTagsTest extends \PHPUnit_Framework_TestCase
19{
20 /**
21 * @var string datastore to test write operations
22 */
23 protected static $testDatastore = 'sandbox/datastore.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 Container instance.
37 */
38 protected $container;
39
40 /**
41 * @var \LinkDB instance.
42 */
43 protected $linkDB;
44
45 /**
46 * @var Tags controller instance.
47 */
48 protected $controller;
49
50 /**
51 * Number of JSON field per link.
52 */
53 const NB_FIELDS_TAG = 2;
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');
61 $this->refDB = new \ReferenceLinkDB();
62 $this->refDB->write(self::$testDatastore);
63
64 $this->container = new Container();
65 $this->container['conf'] = $this->conf;
66 $this->linkDB = new \LinkDB(self::$testDatastore, true, false);
67 $this->container['db'] = $this->linkDB;
68 $this->container['history'] = null;
69
70 $this->controller = new Tags($this->container);
71 }
72
73 /**
74 * After every test, remove the test datastore.
75 */
76 public function tearDown()
77 {
78 @unlink(self::$testDatastore);
79 }
80
81 /**
82 * Test basic getTags service: returns all tags.
83 */
84 public function testGetTagsAll()
85 {
86 $tags = $this->linkDB->linksCountPerTag();
87 $env = Environment::mock([
88 'REQUEST_METHOD' => 'GET',
89 ]);
90 $request = Request::createFromEnvironment($env);
91
92 $response = $this->controller->getTags($request, new Response());
93 $this->assertEquals(200, $response->getStatusCode());
94 $data = json_decode((string) $response->getBody(), true);
95 $this->assertEquals(count($tags), count($data));
96
97 // Check order
98 $this->assertEquals(self::NB_FIELDS_TAG, count($data[0]));
99 $this->assertEquals('web', $data[0]['name']);
100 $this->assertEquals(4, $data[0]['occurrences']);
101 $this->assertEquals(self::NB_FIELDS_TAG, count($data[1]));
102 $this->assertEquals('cartoon', $data[1]['name']);
103 $this->assertEquals(3, $data[1]['occurrences']);
104 // Case insensitive
105 $this->assertEquals(self::NB_FIELDS_TAG, count($data[5]));
106 $this->assertEquals('sTuff', $data[5]['name']);
107 $this->assertEquals(2, $data[5]['occurrences']);
108 // End
109 $this->assertEquals(self::NB_FIELDS_TAG, count($data[count($data) - 1]));
110 $this->assertEquals('w3c', $data[count($data) - 1]['name']);
111 $this->assertEquals(1, $data[count($data) - 1]['occurrences']);
112 }
113
114 /**
115 * Test getTags service with offset and limit parameter:
116 * limit=1 and offset=1 should return only the second tag, cartoon with 3 occurrences
117 */
118 public function testGetTagsOffsetLimit()
119 {
120 $env = Environment::mock([
121 'REQUEST_METHOD' => 'GET',
122 'QUERY_STRING' => 'offset=1&limit=1'
123 ]);
124 $request = Request::createFromEnvironment($env);
125 $response = $this->controller->getTags($request, new Response());
126 $this->assertEquals(200, $response->getStatusCode());
127 $data = json_decode((string) $response->getBody(), true);
128 $this->assertEquals(1, count($data));
129 $this->assertEquals(self::NB_FIELDS_TAG, count($data[0]));
130 $this->assertEquals('cartoon', $data[0]['name']);
131 $this->assertEquals(3, $data[0]['occurrences']);
132 }
133
134 /**
135 * Test getTags with limit=all (return all tags).
136 */
137 public function testGetTagsLimitAll()
138 {
139 $tags = $this->linkDB->linksCountPerTag();
140 $env = Environment::mock([
141 'REQUEST_METHOD' => 'GET',
142 'QUERY_STRING' => 'limit=all'
143 ]);
144 $request = Request::createFromEnvironment($env);
145 $response = $this->controller->getTags($request, new Response());
146 $this->assertEquals(200, $response->getStatusCode());
147 $data = json_decode((string) $response->getBody(), true);
148 $this->assertEquals(count($tags), count($data));
149 }
150
151 /**
152 * Test getTags service with offset and limit parameter:
153 * limit=1 and offset=1 should not return any tag
154 */
155 public function testGetTagsOffsetTooHigh()
156 {
157 $env = Environment::mock([
158 'REQUEST_METHOD' => 'GET',
159 'QUERY_STRING' => 'offset=100'
160 ]);
161 $request = Request::createFromEnvironment($env);
162 $response = $this->controller->getTags($request, new Response());
163 $this->assertEquals(200, $response->getStatusCode());
164 $data = json_decode((string) $response->getBody(), true);
165 $this->assertEmpty(count($data));
166 }
167
168 /**
169 * Test getTags with visibility parameter set to private
170 */
171 public function testGetTagsVisibilityPrivate()
172 {
173 $tags = $this->linkDB->linksCountPerTag([], 'private');
174 $env = Environment::mock([
175 'REQUEST_METHOD' => 'GET',
176 'QUERY_STRING' => 'visibility=private'
177 ]);
178 $request = Request::createFromEnvironment($env);
179 $response = $this->controller->getTags($request, new Response());
180 $this->assertEquals(200, $response->getStatusCode());
181 $data = json_decode((string) $response->getBody(), true);
182 $this->assertEquals(count($tags), count($data));
183 $this->assertEquals(self::NB_FIELDS_TAG, count($data[0]));
184 $this->assertEquals('Mercurial', $data[0]['name']);
185 $this->assertEquals(1, $data[0]['occurrences']);
186 }
187
188 /**
189 * Test getTags with visibility parameter set to public
190 */
191 public function testGetTagsVisibilityPublic()
192 {
193 $tags = $this->linkDB->linksCountPerTag([], 'public');
194 $env = Environment::mock(
195 [
196 'REQUEST_METHOD' => 'GET',
197 'QUERY_STRING' => 'visibility=public'
198 ]
199 );
200 $request = Request::createFromEnvironment($env);
201 $response = $this->controller->getTags($request, new Response());
202 $this->assertEquals(200, $response->getStatusCode());
203 $data = json_decode((string)$response->getBody(), true);
204 $this->assertEquals(count($tags), count($data));
205 $this->assertEquals(self::NB_FIELDS_TAG, count($data[0]));
206 $this->assertEquals('web', $data[0]['name']);
207 $this->assertEquals(3, $data[0]['occurrences']);
208 }
209}
diff --git a/tests/api/controllers/tags/PutTagTest.php b/tests/api/controllers/tags/PutTagTest.php
new file mode 100644
index 00000000..6f7dec22
--- /dev/null
+++ b/tests/api/controllers/tags/PutTagTest.php
@@ -0,0 +1,209 @@
1<?php
2
3
4namespace Shaarli\Api\Controllers;
5
6
7use Shaarli\Api\Exceptions\ApiBadParametersException;
8use Shaarli\Config\ConfigManager;
9use Slim\Container;
10use Slim\Http\Environment;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class PutTagTest extends \PHPUnit_Framework_TestCase
15{
16 /**
17 * @var string datastore to test write operations
18 */
19 protected static $testDatastore = 'sandbox/datastore.php';
20
21 /**
22 * @var string datastore to test write operations
23 */
24 protected static $testHistory = 'sandbox/history.php';
25
26 /**
27 * @var ConfigManager instance
28 */
29 protected $conf;
30
31 /**
32 * @var \ReferenceLinkDB instance.
33 */
34 protected $refDB = null;
35
36 /**
37 * @var \History instance.
38 */
39 protected $history;
40
41 /**
42 * @var Container instance.
43 */
44 protected $container;
45
46 /**
47 * @var \LinkDB instance.
48 */
49 protected $linkDB;
50
51 /**
52 * @var Tags controller instance.
53 */
54 protected $controller;
55
56 /**
57 * Number of JSON field per link.
58 */
59 const NB_FIELDS_TAG = 2;
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->linkDB = new \LinkDB(self::$testDatastore, true, false);
77 $this->container['db'] = $this->linkDB;
78 $this->container['history'] = $this->history;
79
80 $this->controller = new Tags($this->container);
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 tags update
94 */
95 public function testPutLinkValid()
96 {
97 $env = Environment::mock([
98 'REQUEST_METHOD' => 'PUT',
99 ]);
100 $tagName = 'gnu';
101 $update = ['name' => $newName = 'newtag'];
102 $request = Request::createFromEnvironment($env);
103 $request = $request->withParsedBody($update);
104
105 $response = $this->controller->putTag($request, new Response(), ['tagName' => $tagName]);
106 $this->assertEquals(200, $response->getStatusCode());
107 $data = json_decode((string) $response->getBody(), true);
108 $this->assertEquals(self::NB_FIELDS_TAG, count($data));
109 $this->assertEquals($newName, $data['name']);
110 $this->assertEquals(2, $data['occurrences']);
111
112 $tags = $this->linkDB->linksCountPerTag();
113 $this->assertNotTrue(isset($tags[$tagName]));
114 $this->assertEquals(2, $tags[$newName]);
115
116 $historyEntry = $this->history->getHistory()[0];
117 $this->assertEquals(\History::UPDATED, $historyEntry['event']);
118 $this->assertTrue(
119 (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime']
120 );
121 $historyEntry = $this->history->getHistory()[1];
122 $this->assertEquals(\History::UPDATED, $historyEntry['event']);
123 $this->assertTrue(
124 (new \DateTime())->add(\DateInterval::createFromDateString('-5 seconds')) < $historyEntry['datetime']
125 );
126 }
127
128 /**
129 * Test tag update with an existing tag: they should be merged
130 */
131 public function testPutTagMerge()
132 {
133 $tagName = 'gnu';
134 $newName = 'w3c';
135
136 $tags = $this->linkDB->linksCountPerTag();
137 $this->assertEquals(1, $tags[$newName]);
138 $this->assertEquals(2, $tags[$tagName]);
139
140 $env = Environment::mock([
141 'REQUEST_METHOD' => 'PUT',
142 ]);
143 $update = ['name' => $newName];
144 $request = Request::createFromEnvironment($env);
145 $request = $request->withParsedBody($update);
146
147 $response = $this->controller->putTag($request, new Response(), ['tagName' => $tagName]);
148 $this->assertEquals(200, $response->getStatusCode());
149 $data = json_decode((string) $response->getBody(), true);
150 $this->assertEquals(self::NB_FIELDS_TAG, count($data));
151 $this->assertEquals($newName, $data['name']);
152 $this->assertEquals(3, $data['occurrences']);
153
154 $tags = $this->linkDB->linksCountPerTag();
155 $this->assertNotTrue(isset($tags[$tagName]));
156 $this->assertEquals(3, $tags[$newName]);
157 }
158
159 /**
160 * Test tag update with an empty new tag name => ApiBadParametersException
161 *
162 * @expectedException Shaarli\Api\Exceptions\ApiBadParametersException
163 * @expectedExceptionMessage New tag name is required in the request body
164 */
165 public function testPutTagEmpty()
166 {
167 $tagName = 'gnu';
168 $newName = '';
169
170 $tags = $this->linkDB->linksCountPerTag();
171 $this->assertEquals(2, $tags[$tagName]);
172
173 $env = Environment::mock([
174 'REQUEST_METHOD' => 'PUT',
175 ]);
176 $request = Request::createFromEnvironment($env);
177
178 $env = Environment::mock([
179 'REQUEST_METHOD' => 'PUT',
180 ]);
181 $update = ['name' => $newName];
182 $request = Request::createFromEnvironment($env);
183 $request = $request->withParsedBody($update);
184
185 try {
186 $this->controller->putTag($request, new Response(), ['tagName' => $tagName]);
187 } catch (ApiBadParametersException $e) {
188 $tags = $this->linkDB->linksCountPerTag();
189 $this->assertEquals(2, $tags[$tagName]);
190 throw $e;
191 }
192 }
193
194 /**
195 * Test tag update on non existent tag => ApiTagNotFoundException.
196 *
197 * @expectedException Shaarli\Api\Exceptions\ApiTagNotFoundException
198 * @expectedExceptionMessage Tag not found
199 */
200 public function testPutTag404()
201 {
202 $env = Environment::mock([
203 'REQUEST_METHOD' => 'PUT',
204 ]);
205 $request = Request::createFromEnvironment($env);
206
207 $this->controller->putTag($request, new Response(), ['tagName' => 'nopenope']);
208 }
209}
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html
index 8f2597df..82041972 100644
--- a/tpl/default/pluginsadmin.html
+++ b/tpl/default/pluginsadmin.html
@@ -117,7 +117,7 @@
117 117
118 <div class="center more"> 118 <div class="center more">
119 {"More plugins available"|t} 119 {"More plugins available"|t}
120 <a href="doc/Community-&-Related-software.html#third-party-plugins">{"in the documentation"|t}</a>. 120 <a href="doc/html/Community-&-Related-software/#third-party-plugins">{"in the documentation"|t}</a>.
121 </div> 121 </div>
122 <div class="center"> 122 <div class="center">
123 <input type="submit" value="{'Save'|t}" name="save"> 123 <input type="submit" value="{'Save'|t}" name="save">
diff --git a/tpl/vintage/page.footer.html b/tpl/vintage/page.footer.html
index f409721e..a3380841 100644
--- a/tpl/vintage/page.footer.html
+++ b/tpl/vintage/page.footer.html
@@ -1,7 +1,7 @@
1<div id="footer"> 1<div id="footer">
2 <strong><a href="https://github.com/shaarli/Shaarli">Shaarli</a></strong> 2 <strong><a href="https://github.com/shaarli/Shaarli">Shaarli</a></strong>
3 - The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community 3 - The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community
4 - <a href="doc/Home.html" rel="nofollow">Help/documentation</a> 4 - <a href="doc/html/index.html" rel="nofollow">Help/documentation</a>
5 {loop="$plugins_footer.text"} 5 {loop="$plugins_footer.text"}
6 {$value} 6 {$value}
7 {/loop} 7 {/loop}