diff options
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 |
5 | tests | 5 | tests |
6 | 6 | ||
7 | # Docker Compose resources | ||
8 | docker-compose.yml | ||
9 | |||
7 | # Shaarli runtime resources | 10 | # Shaarli runtime resources |
8 | cache/* | 11 | cache/* |
9 | data/* | 12 | data/* |
diff --git a/.gitattributes b/.gitattributes index 9d22f11b..9a92bc37 100644 --- a/.gitattributes +++ b/.gitattributes | |||
@@ -35,6 +35,7 @@ doc/**/*.json export-ignore | |||
35 | doc/**/*.md export-ignore | 35 | doc/**/*.md export-ignore |
36 | .docker/ export-ignore | 36 | .docker/ export-ignore |
37 | .dockerignore export-ignore | 37 | .dockerignore export-ignore |
38 | docker-compose.* export-ignore | ||
38 | Dockerfile* export-ignore | 39 | Dockerfile* export-ignore |
39 | Doxyfile export-ignore | 40 | Doxyfile export-ignore |
40 | Makefile export-ignore | 41 | Makefile export-ignore |
@@ -14,3 +14,10 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] | |||
14 | RewriteCond %{REQUEST_FILENAME} !-f | 14 | RewriteCond %{REQUEST_FILENAME} !-f |
15 | RewriteCond %{REQUEST_FILENAME} !-d | 15 | RewriteCond %{REQUEST_FILENAME} !-d |
16 | RewriteRule ^ index.php [QSA,L] | 16 | RewriteRule ^ 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> | ||
@@ -25,7 +25,7 @@ RUN cd shaarli \ | |||
25 | 25 | ||
26 | # Stage 4: | 26 | # Stage 4: |
27 | # - Shaarli image | 27 | # - Shaarli image |
28 | FROM alpine:3.7 | 28 | FROM alpine:3.8 |
29 | LABEL maintainer="Shaarli Community" | 29 | LABEL maintainer="Shaarli Community" |
30 | 30 | ||
31 | RUN apk --update --no-cache add \ | 31 | RUN 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 @@ | |||
1 | FROM lsiobase/alpine.armhf:3.6 | 1 | # Stage 1: |
2 | # - Copy Shaarli sources | ||
3 | # - Build documentation | ||
4 | FROM arm32v6/alpine:3.8 as docs | ||
5 | ADD . /usr/src/app/shaarli | ||
6 | RUN 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 | ||
13 | FROM arm32v6/alpine:3.8 as composer | ||
14 | COPY --from=docs /usr/src/app/shaarli /app/shaarli | ||
15 | RUN 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 | ||
21 | FROM arm32v6/alpine:3.8 as node | ||
22 | COPY --from=composer /app/shaarli /shaarli | ||
23 | RUN 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 | ||
31 | FROM arm32v6/alpine:3.8 | ||
2 | LABEL maintainer="Shaarli Community" | 32 | LABEL maintainer="Shaarli Community" |
3 | 33 | ||
4 | RUN apk --update --no-cache add \ | 34 | RUN 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 | |||
25 | COPY .docker/php-fpm.conf /etc/php7/php-fpm.conf | 53 | COPY .docker/php-fpm.conf /etc/php7/php-fpm.conf |
26 | COPY .docker/services.d /etc/services.d | 54 | COPY .docker/services.d /etc/services.d |
27 | 55 | ||
28 | RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \ | 56 | RUN 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 | ||
34 | WORKDIR /var/www | 61 | WORKDIR /var/www |
35 | RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \ | 62 | COPY --from=node /shaarli /var/www/shaarli |
36 | && mv Shaarli-master shaarli \ | 63 | |
37 | && cd shaarli \ | 64 | RUN 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 | ||
68 | VOLUME /var/www/shaarli/cache | ||
44 | VOLUME /var/www/shaarli/data | 69 | VOLUME /var/www/shaarli/data |
45 | 70 | ||
46 | EXPOSE 80 | 71 | EXPOSE 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 | |||
3 | namespace Shaarli\Api\Controllers; | ||
4 | |||
5 | use Shaarli\Api\ApiUtils; | ||
6 | use Shaarli\Api\Exceptions\ApiBadParametersException; | ||
7 | use Shaarli\Api\Exceptions\ApiLinkNotFoundException; | ||
8 | use Shaarli\Api\Exceptions\ApiTagNotFoundException; | ||
9 | use Slim\Http\Request; | ||
10 | use 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 | */ | ||
19 | class 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 | |||
3 | namespace Shaarli\Api\Exceptions; | ||
4 | |||
5 | |||
6 | use 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 | */ | ||
15 | class 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 | |||
162 | For security reasons, authentication issues will always return an `HTTP 401` error code without any detail. | ||
163 | |||
164 | It is possible to enable the debug mode in `config.json.php` | ||
165 | to 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 | ||
36 | All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page. | 36 | All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page. |
37 | 37 | ||
38 | We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation) for `git` complete instructions. | 38 | We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and Installation](Download-and-Installation) for `git` complete instructions. |
39 | 39 | ||
40 | Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! | 40 | Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! |
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 | ||
16 | The following HTTP headers are set by using the `ProxyPass` directive: | 16 | The 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 | ||
22 | The 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 | |||
11 | The 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 | |||
21 | More information can be found in the [Resources](#resources) section at the | ||
22 | bottom of the guide. | ||
23 | |||
24 | ## Getting a Virtual Private Server | ||
25 | For this guide, I went for the smallest VPS available from DigitalOcean, | ||
26 | a 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 | ||
37 | Select `Debian 9` as the Droplet distribution: | ||
38 | |||
39 | <img src="../images/01-create-droplet-distro.jpg" | ||
40 | width="500px" | ||
41 | alt="Droplet distribution" /> | ||
42 | |||
43 | Choose 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 | |||
49 | Choose 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 | |||
55 | Finalize the Droplet creation: | ||
56 | |||
57 | <img src="../images/04-finalize.jpg" | ||
58 | width="500px" | ||
59 | alt="Droplet finalization" /> | ||
60 | |||
61 | Droplet information is displayed on the Control Panel: | ||
62 | |||
63 | <img src="../images/05-droplet.jpg" | ||
64 | width="500px" | ||
65 | alt="Droplet summary" /> | ||
66 | |||
67 | Once your VPS has been created, you will receive an e-mail with connection | ||
68 | instructions. | ||
69 | |||
70 | ## Obtaining a domain name | ||
71 | After creating your VPS, it will be reachable using its IP address; some hosting | ||
72 | providers also create a DNS record, e.g. `ns4853142.ip-01-47-127.eu`. | ||
73 | |||
74 | A domain name (DNS record) is required to obtain a certificate and setup HTTPS | ||
75 | (HTTP with TLS encryption). | ||
76 | |||
77 | Domain names can be obtained from registrars through hosting providers such as | ||
78 | [Gandi](https://www.gandi.net/en/domain). | ||
79 | |||
80 | Once you have your own domain, you need to create a new DNS record that points | ||
81 | to your VPS' IP address: | ||
82 | |||
83 | <img src="../images/06-domain.jpg" | ||
84 | width="650px" | ||
85 | alt="Domain configuration" /> | ||
86 | |||
87 | ## Host setup | ||
88 | Now's the time to connect to your freshly created VPS! | ||
89 | |||
90 | ```shell | ||
91 | $ ssh root@188.166.85.8 | ||
92 | |||
93 | Linux stretch-shaarli-02 4.9.0-6-amd64 #1 SMP Debian 4.9.88-1+deb9u1 (2018-05-07) x86_64 | ||
94 | |||
95 | The programs included with the Debian GNU/Linux system are free software; | ||
96 | the exact distribution terms for each program are described in the | ||
97 | individual files in /usr/share/doc/*/copyright. | ||
98 | |||
99 | Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent | ||
100 | permitted by applicable law. | ||
101 | Last login: Sun Jul 1 11:20:18 2018 from <REDACTED> | ||
102 | |||
103 | root@stretch-shaarli-02:~$ | ||
104 | ``` | ||
105 | |||
106 | ### Updating the system | ||
107 | ```shell | ||
108 | root@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/) | ||
114 | guide._ | ||
115 | |||
116 | Install package dependencies: | ||
117 | |||
118 | ```shell | ||
119 | root@stretch-shaarli-02:~$ apt install -y apt-transport-https ca-certificates curl gnupg2 software-properties-common | ||
120 | ``` | ||
121 | |||
122 | Add Docker's package repository GPG key: | ||
123 | |||
124 | ```shell | ||
125 | root@stretch-shaarli-02:~$ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add - | ||
126 | ``` | ||
127 | |||
128 | Add Docker's package repository: | ||
129 | |||
130 | ```shell | ||
131 | root@stretch-shaarli-02:~$ add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian stretch stable" | ||
132 | ``` | ||
133 | |||
134 | Update package lists and install Docker: | ||
135 | |||
136 | ```shell | ||
137 | root@stretch-shaarli-02:~$ apt update && apt install -y docker-ce | ||
138 | ``` | ||
139 | |||
140 | Verify Docker is properly configured by running the `hello-world` image: | ||
141 | |||
142 | ```shell | ||
143 | root@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/) | ||
149 | guide._ | ||
150 | |||
151 | Download the current version from the release page: | ||
152 | |||
153 | ```shell | ||
154 | root@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 | ||
155 | root@stretch-shaarli-02:~$ chmod +x /usr/local/bin/docker-compose | ||
156 | ``` | ||
157 | |||
158 | ## Running Shaarli | ||
159 | Shaarli 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 | ||
168 | support for Docker and [Let's Encrypt](https://letsencrypt.org/). | ||
169 | |||
170 | ### Compose configuration | ||
171 | Create a new directory to store the configuration: | ||
172 | |||
173 | ```shell | ||
174 | root@stretch-shaarli-02:~$ mkdir shaarli && cd shaarli | ||
175 | root@stretch-shaarli-02:~/shaarli$ | ||
176 | ``` | ||
177 | |||
178 | Download the current version of Shaarli's `docker-compose.yml`: | ||
179 | |||
180 | ```shell | ||
181 | root@stretch-shaarli-02:~/shaarli$ curl -L https://raw.githubusercontent.com/shaarli/Shaarli/master/docker-compose.yml -o docker-compose.yml | ||
182 | ``` | ||
183 | |||
184 | Create 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 | ||
188 | root@stretch-shaarli-02:~/shaarli$ vim .env | ||
189 | ``` | ||
190 | |||
191 | ```shell | ||
192 | SHAARLI_VIRTUAL_HOST=<MY_SHAARLI_DOMAIN> | ||
193 | SHAARLI_LETSENCRYPT_EMAIL=<MY_CONTACT_EMAIL> | ||
194 | ``` | ||
195 | |||
196 | ### Pull the Docker images | ||
197 | ```shell | ||
198 | root@stretch-shaarli-02:~/shaarli$ docker-compose pull | ||
199 | Pulling shaarli ... done | ||
200 | Pulling traefik ... done | ||
201 | ``` | ||
202 | |||
203 | ### Run! | ||
204 | ```shell | ||
205 | root@stretch-shaarli-02:~/shaarli$ docker-compose up -d | ||
206 | Creating network "shaarli_http-proxy" with the default driver | ||
207 | Creating volume "shaarli_traefik-acme" with default driver | ||
208 | Creating volume "shaarli_shaarli-data" with default driver | ||
209 | Creating shaarli_shaarli_1 ... done | ||
210 | Creating shaarli_traefik_1 ... done | ||
211 | ``` | ||
212 | |||
213 | ## Conclusion | ||
214 | Congratulations! Your Shaarli instance should be up and running, and available | ||
215 | at `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 | ||
11 | version: '3' | ||
12 | |||
13 | networks: | ||
14 | http-proxy: | ||
15 | |||
16 | volumes: | ||
17 | traefik-acme: | ||
18 | shaarli-cache: | ||
19 | shaarli-data: | ||
20 | |||
21 | services: | ||
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 | ||
@@ -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 | ||
@@ -5,7 +5,10 @@ site_description: The personal, minimalist, super-fast, database free, bookmarki | |||
5 | theme: readthedocs | 5 | theme: readthedocs |
6 | docs_dir: doc/md | 6 | docs_dir: doc/md |
7 | site_dir: doc/html | 7 | site_dir: doc/html |
8 | strict: 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 | ||
10 | pages: | 13 | pages: |
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 | |||
4 | namespace Shaarli\Api\Controllers; | ||
5 | |||
6 | use Shaarli\Config\ConfigManager; | ||
7 | use Slim\Container; | ||
8 | use Slim\Http\Environment; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | class 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 | |||
3 | namespace Shaarli\Api\Controllers; | ||
4 | |||
5 | use Shaarli\Config\ConfigManager; | ||
6 | |||
7 | use Slim\Container; | ||
8 | use Slim\Http\Environment; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | /** | ||
13 | * Class GetTagNameTest | ||
14 | * | ||
15 | * Test getTag by tag name API service. | ||
16 | * | ||
17 | * @package Shaarli\Api\Controllers | ||
18 | */ | ||
19 | class 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 | ||
2 | namespace Shaarli\Api\Controllers; | ||
3 | |||
4 | use Shaarli\Config\ConfigManager; | ||
5 | |||
6 | use Slim\Container; | ||
7 | use Slim\Http\Environment; | ||
8 | use Slim\Http\Request; | ||
9 | use Slim\Http\Response; | ||
10 | |||
11 | /** | ||
12 | * Class GetTagsTest | ||
13 | * | ||
14 | * Test get tag list REST API service. | ||
15 | * | ||
16 | * @package Shaarli\Api\Controllers | ||
17 | */ | ||
18 | class 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 | |||
4 | namespace Shaarli\Api\Controllers; | ||
5 | |||
6 | |||
7 | use Shaarli\Api\Exceptions\ApiBadParametersException; | ||
8 | use Shaarli\Config\ConfigManager; | ||
9 | use Slim\Container; | ||
10 | use Slim\Http\Environment; | ||
11 | use Slim\Http\Request; | ||
12 | use Slim\Http\Response; | ||
13 | |||
14 | class 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} |