aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/LinkDB.php4
-rw-r--r--application/Utils.php98
-rw-r--r--application/api/ApiUtils.php35
-rw-r--r--application/api/controllers/ApiController.php10
-rw-r--r--application/api/controllers/Links.php44
-rw-r--r--index.php78
-rw-r--r--plugins/readityourself/book-open.pngbin568 -> 0 bytes
-rw-r--r--plugins/readityourself/readityourself.html1
-rw-r--r--plugins/readityourself/readityourself.meta2
-rw-r--r--plugins/readityourself/readityourself.php51
-rw-r--r--tests/UtilsTest.php91
-rw-r--r--tests/api/controllers/PostLinkTest.php193
-rw-r--r--tests/languages/de/UtilsDeTest.php22
-rw-r--r--tests/languages/en/UtilsEnTest.php22
-rw-r--r--tests/languages/fr/UtilsFrTest.php20
-rw-r--r--tests/plugins/PluginReadityourselfTest.php99
-rw-r--r--tests/utils/ReferenceLinkDB.php2
-rw-r--r--tpl/default/configure.html4
-rw-r--r--tpl/default/css/shaarli.css31
-rw-r--r--tpl/default/daily.html2
-rw-r--r--tpl/default/fonts/Fira-Sans-regular.woffbin17100 -> 0 bytes
-rw-r--r--tpl/default/fonts/Fira-Sans-regular.woff2bin13836 -> 0 bytes
-rw-r--r--tpl/default/fonts/Roboto-Bold.woffbin0 -> 89584 bytes
-rw-r--r--tpl/default/fonts/Roboto-Bold.woff2bin0 -> 63320 bytes
-rw-r--r--tpl/default/fonts/Roboto-Regular.woffbin0 -> 89732 bytes
-rw-r--r--tpl/default/fonts/Roboto-Regular.woff2bin0 -> 63412 bytes
-rw-r--r--tpl/default/import.html1
-rw-r--r--tpl/default/install.html18
-rw-r--r--tpl/default/js/shaarli.js3
-rw-r--r--tpl/vintage/configure.html4
-rw-r--r--tpl/vintage/import.html2
31 files changed, 612 insertions, 225 deletions
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 4cee2af9..1e4d7ce8 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -144,10 +144,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess
144 if (!isset($value['id']) || empty($value['url'])) { 144 if (!isset($value['id']) || empty($value['url'])) {
145 die('Internal Error: A link should always have an id and URL.'); 145 die('Internal Error: A link should always have an id and URL.');
146 } 146 }
147 if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) { 147 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
148 die('You must specify an integer as a key.'); 148 die('You must specify an integer as a key.');
149 } 149 }
150 if (! empty($offset) && $offset !== $value['id']) { 150 if ($offset !== null && $offset !== $value['id']) {
151 die('Array offset and link ID must be equal.'); 151 die('Array offset and link ID must be equal.');
152 } 152 }
153 153
diff --git a/application/Utils.php b/application/Utils.php
index 5c077450..ab463af9 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -321,25 +321,117 @@ function normalize_spaces($string)
321 * otherwise default format '%c' will be returned. 321 * otherwise default format '%c' will be returned.
322 * 322 *
323 * @param DateTime $date to format. 323 * @param DateTime $date to format.
324 * @param bool $time Displays time if true.
324 * @param bool $intl Use international format if true. 325 * @param bool $intl Use international format if true.
325 * 326 *
326 * @return bool|string Formatted date, or false if the input is invalid. 327 * @return bool|string Formatted date, or false if the input is invalid.
327 */ 328 */
328function format_date($date, $intl = true) 329function format_date($date, $time = true, $intl = true)
329{ 330{
330 if (! $date instanceof DateTime) { 331 if (! $date instanceof DateTime) {
331 return false; 332 return false;
332 } 333 }
333 334
334 if (! $intl || ! class_exists('IntlDateFormatter')) { 335 if (! $intl || ! class_exists('IntlDateFormatter')) {
335 return strftime('%c', $date->getTimestamp()); 336 $format = $time ? '%c' : '%x';
337 return strftime($format, $date->getTimestamp());
336 } 338 }
337 339
338 $formatter = new IntlDateFormatter( 340 $formatter = new IntlDateFormatter(
339 setlocale(LC_TIME, 0), 341 setlocale(LC_TIME, 0),
340 IntlDateFormatter::LONG, 342 IntlDateFormatter::LONG,
341 IntlDateFormatter::LONG 343 $time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE
342 ); 344 );
343 345
344 return $formatter->format($date); 346 return $formatter->format($date);
345} 347}
348
349/**
350 * Check if the input is an integer, no matter its real type.
351 *
352 * PHP is a bit messy regarding this:
353 * - is_int returns false if the input is a string
354 * - ctype_digit returns false if the input is an integer or negative
355 *
356 * @param mixed $input value
357 *
358 * @return bool true if the input is an integer, false otherwise
359 */
360function is_integer_mixed($input)
361{
362 if (is_array($input) || is_bool($input) || is_object($input)) {
363 return false;
364 }
365 $input = strval($input);
366 return ctype_digit($input) || (startsWith($input, '-') && ctype_digit(substr($input, 1)));
367}
368
369/**
370 * Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
371 *
372 * @param string $val Size expressed in string.
373 *
374 * @return int Size expressed in bytes.
375 */
376function return_bytes($val)
377{
378 if (is_integer_mixed($val) || $val === '0' || empty($val)) {
379 return $val;
380 }
381 $val = trim($val);
382 $last = strtolower($val[strlen($val)-1]);
383 $val = intval(substr($val, 0, -1));
384 switch($last) {
385 case 'g': $val *= 1024;
386 case 'm': $val *= 1024;
387 case 'k': $val *= 1024;
388 }
389 return $val;
390}
391
392/**
393 * Return a human readable size from bytes.
394 *
395 * @param int $bytes value
396 *
397 * @return string Human readable size
398 */
399function human_bytes($bytes)
400{
401 if ($bytes === '') {
402 return t('Setting not set');
403 }
404 if (! is_integer_mixed($bytes)) {
405 return $bytes;
406 }
407 $bytes = intval($bytes);
408 if ($bytes === 0) {
409 return t('Unlimited');
410 }
411
412 $units = [t('B'), t('kiB'), t('MiB'), t('GiB')];
413 for ($i = 0; $i < count($units) && $bytes >= 1024; ++$i) {
414 $bytes /= 1024;
415 }
416
417 return round($bytes) . $units[$i];
418}
419
420/**
421 * Try to determine max file size for uploads (POST).
422 * Returns an integer (in bytes) or formatted depending on $format.
423 *
424 * @param mixed $limitPost post_max_size PHP setting
425 * @param mixed $limitUpload upload_max_filesize PHP setting
426 * @param bool $format Format max upload size to human readable size
427 *
428 * @return int|string max upload file size
429 */
430function get_max_upload_size($limitPost, $limitUpload, $format = true)
431{
432 $size1 = return_bytes($limitPost);
433 $size2 = return_bytes($limitUpload);
434 // Return the smaller of two:
435 $maxsize = min($size1, $size2);
436 return $format ? human_bytes($maxsize) : $maxsize;
437}
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index d4015865..b8155a34 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -12,7 +12,7 @@ class ApiUtils
12 /** 12 /**
13 * Validates a JWT token authenticity. 13 * Validates a JWT token authenticity.
14 * 14 *
15 * @param string $token JWT token extracted from the headers. 15 * @param string $token JWT token extracted from the headers.
16 * @param string $secret API secret set in the settings. 16 * @param string $secret API secret set in the settings.
17 * 17 *
18 * @throws ApiAuthorizationException the token is not valid. 18 * @throws ApiAuthorizationException the token is not valid.
@@ -50,7 +50,7 @@ class ApiUtils
50 /** 50 /**
51 * Format a Link for the REST API. 51 * Format a Link for the REST API.
52 * 52 *
53 * @param array $link Link data read from the datastore. 53 * @param array $link Link data read from the datastore.
54 * @param string $indexUrl Shaarli's index URL (used for relative URL). 54 * @param string $indexUrl Shaarli's index URL (used for relative URL).
55 * 55 *
56 * @return array Link data formatted for the REST API. 56 * @return array Link data formatted for the REST API.
@@ -77,4 +77,35 @@ class ApiUtils
77 } 77 }
78 return $out; 78 return $out;
79 } 79 }
80
81 /**
82 * Convert a link given through a request, to a valid link for LinkDB.
83 *
84 * If no URL is provided, it will generate a local note URL.
85 * If no title is provided, it will use the URL as title.
86 *
87 * @param array $input Request Link.
88 * @param bool $defaultPrivate Request Link.
89 *
90 * @return array Formatted link.
91 */
92 public static function buildLinkFromRequest($input, $defaultPrivate)
93 {
94 $input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : '';
95 if (isset($input['private'])) {
96 $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
97 } else {
98 $private = $defaultPrivate;
99 }
100
101 $link = [
102 'title' => ! empty($input['title']) ? $input['title'] : $input['url'],
103 'url' => $input['url'],
104 'description' => ! empty($input['description']) ? $input['description'] : '',
105 'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '',
106 'private' => $private,
107 'created' => new \DateTime(),
108 ];
109 return $link;
110 }
80} 111}
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php
index 1dd47f17..f35b923a 100644
--- a/application/api/controllers/ApiController.php
+++ b/application/api/controllers/ApiController.php
@@ -51,4 +51,14 @@ abstract class ApiController
51 $this->jsonStyle = null; 51 $this->jsonStyle = null;
52 } 52 }
53 } 53 }
54
55 /**
56 * Get the container.
57 *
58 * @return Container
59 */
60 public function getCi()
61 {
62 return $this->ci;
63 }
54} 64}
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index d4f1a09c..0db10fd0 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -97,11 +97,53 @@ class Links extends ApiController
97 */ 97 */
98 public function getLink($request, $response, $args) 98 public function getLink($request, $response, $args)
99 { 99 {
100 if (! isset($this->linkDb[$args['id']])) { 100 if (!isset($this->linkDb[$args['id']])) {
101 throw new ApiLinkNotFoundException(); 101 throw new ApiLinkNotFoundException();
102 } 102 }
103 $index = index_url($this->ci['environment']); 103 $index = index_url($this->ci['environment']);
104 $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index); 104 $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index);
105
105 return $response->withJson($out, 200, $this->jsonStyle); 106 return $response->withJson($out, 200, $this->jsonStyle);
106 } 107 }
108
109 /**
110 * Creates a new link from posted request body.
111 *
112 * @param Request $request Slim request.
113 * @param Response $response Slim response.
114 *
115 * @return Response response.
116 */
117 public function postLink($request, $response)
118 {
119 $data = $request->getParsedBody();
120 $link = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
121 // duplicate by URL, return 409 Conflict
122 if (! empty($link['url']) && ! empty($dup = $this->linkDb->getLinkFromUrl($link['url']))) {
123 return $response->withJson(
124 ApiUtils::formatLink($dup, index_url($this->ci['environment'])),
125 409,
126 $this->jsonStyle
127 );
128 }
129
130 $link['id'] = $this->linkDb->getNextId();
131 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
132
133 // note: general relative URL
134 if (empty($link['url'])) {
135 $link['url'] = '?' . $link['shorturl'];
136 }
137
138 if (empty($link['title'])) {
139 $link['title'] = $link['url'];
140 }
141
142 $this->linkDb[$link['id']] = $link;
143 $this->linkDb->save($this->conf->get('resource.page_cache'));
144 $out = ApiUtils::formatLink($link, index_url($this->ci['environment']));
145 $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $link['id']]);
146 return $response->withAddedHeader('Location', $redirect)
147 ->withJson($out, 201, $this->jsonStyle);
148 }
107} 149}
diff --git a/index.php b/index.php
index 5497a23e..e392e501 100644
--- a/index.php
+++ b/index.php
@@ -432,7 +432,7 @@ if (isset($_POST['login']))
432 // Optional redirect after login: 432 // Optional redirect after login:
433 if (isset($_GET['post'])) { 433 if (isset($_GET['post'])) {
434 $uri = '?post='. urlencode($_GET['post']); 434 $uri = '?post='. urlencode($_GET['post']);
435 foreach (array('description', 'source', 'title') as $param) { 435 foreach (array('description', 'source', 'title', 'tags') as $param) {
436 if (!empty($_GET[$param])) { 436 if (!empty($_GET[$param])) {
437 $uri .= '&'.$param.'='.urlencode($_GET[$param]); 437 $uri .= '&'.$param.'='.urlencode($_GET[$param]);
438 } 438 }
@@ -461,7 +461,7 @@ if (isset($_POST['login']))
461 $redir = '&username='. $_POST['login']; 461 $redir = '&username='. $_POST['login'];
462 if (isset($_GET['post'])) { 462 if (isset($_GET['post'])) {
463 $redir .= '&post=' . urlencode($_GET['post']); 463 $redir .= '&post=' . urlencode($_GET['post']);
464 foreach (array('description', 'source', 'title') as $param) { 464 foreach (array('description', 'source', 'title', 'tags') as $param) {
465 if (!empty($_GET[$param])) { 465 if (!empty($_GET[$param])) {
466 $redir .= '&' . $param . '=' . urlencode($_GET[$param]); 466 $redir .= '&' . $param . '=' . urlencode($_GET[$param]);
467 } 467 }
@@ -473,34 +473,6 @@ if (isset($_POST['login']))
473} 473}
474 474
475// ------------------------------------------------------------------------------------------ 475// ------------------------------------------------------------------------------------------
476// Misc utility functions:
477
478// Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
479function return_bytes($val)
480{
481 $val = trim($val); $last=strtolower($val[strlen($val)-1]);
482 switch($last)
483 {
484 case 'g': $val *= 1024;
485 case 'm': $val *= 1024;
486 case 'k': $val *= 1024;
487 }
488 return $val;
489}
490
491// Try to determine max file size for uploads (POST).
492// Returns an integer (in bytes)
493function getMaxFileSize()
494{
495 $size1 = return_bytes(ini_get('post_max_size'));
496 $size2 = return_bytes(ini_get('upload_max_filesize'));
497 // Return the smaller of two:
498 $maxsize = min($size1,$size2);
499 // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000)
500 return $maxsize;
501}
502
503// ------------------------------------------------------------------------------------------
504// Token management for XSRF protection 476// Token management for XSRF protection
505// Token should be used in any form which acts on data (create,update,delete,import...). 477// Token should be used in any form which acts on data (create,update,delete,import...).
506if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. 478if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session.
@@ -695,9 +667,11 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
695 667
696 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); 668 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
697 $data = array( 669 $data = array(
670 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
698 'linksToDisplay' => $linksToDisplay, 671 'linksToDisplay' => $linksToDisplay,
699 'cols' => $columns, 672 'cols' => $columns,
700 'day' => $dayDate->getTimestamp(), 673 'day' => $dayDate->getTimestamp(),
674 'dayDate' => $dayDate,
701 'previousday' => $previousday, 675 'previousday' => $previousday,
702 'nextday' => $nextday, 676 'nextday' => $nextday,
703 ); 677 );
@@ -1044,7 +1018,13 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1044 // Show login screen, then redirect to ?post=... 1018 // Show login screen, then redirect to ?post=...
1045 if (isset($_GET['post'])) 1019 if (isset($_GET['post']))
1046 { 1020 {
1047 header('Location: ?do=login&post='.urlencode($_GET['post']).(!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').(!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').(!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')); // Redirect to login page, then back to post link. 1021 header( // Redirect to login page, then back to post link.
1022 'Location: ?do=login&post='.urlencode($_GET['post']).
1023 (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
1024 (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
1025 (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
1026 (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
1027 );
1048 exit; 1028 exit;
1049 } 1029 }
1050 1030
@@ -1141,7 +1121,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1141 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks'])); 1121 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
1142 $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); 1122 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1143 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks'])); 1123 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
1144 $conf->set('api.enabled', !empty($_POST['apiEnabled'])); 1124 $conf->set('api.enabled', !empty($_POST['enableApi']));
1145 $conf->set('api.secret', escape($_POST['apiSecret'])); 1125 $conf->set('api.secret', escape($_POST['apiSecret']));
1146 try { 1126 try {
1147 $conf->write(isLoggedIn()); 1127 $conf->write(isLoggedIn());
@@ -1248,7 +1228,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1248 } 1228 }
1249 1229
1250 // lf_id should only be present if the link exists. 1230 // lf_id should only be present if the link exists.
1251 $id = !empty($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId(); 1231 $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
1252 // Linkdate is kept here to: 1232 // Linkdate is kept here to:
1253 // - use the same permalink for notes as they're displayed when creating them 1233 // - use the same permalink for notes as they're displayed when creating them
1254 // - let users hack creation date of their posts 1234 // - let users hack creation date of their posts
@@ -1321,9 +1301,13 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1321 // -------- User clicked the "Cancel" button when editing a link. 1301 // -------- User clicked the "Cancel" button when editing a link.
1322 if (isset($_POST['cancel_edit'])) 1302 if (isset($_POST['cancel_edit']))
1323 { 1303 {
1304 $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false;
1305 if (! isset($LINKSDB[$id])) {
1306 header('Location: ?');
1307 }
1324 // If we are called from the bookmarklet, we must close the popup: 1308 // If we are called from the bookmarklet, we must close the popup:
1325 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; } 1309 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
1326 $link = $LINKSDB[(int) escape($_POST['lf_id'])]; 1310 $link = $LINKSDB[$id];
1327 $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' ); 1311 $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
1328 // Scroll to the link which has been edited. 1312 // Scroll to the link which has been edited.
1329 $returnurl .= '#'. $link['shorturl']; 1313 $returnurl .= '#'. $link['shorturl'];
@@ -1508,7 +1492,22 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1508 1492
1509 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) { 1493 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1510 // Show import dialog 1494 // Show import dialog
1511 $PAGE->assign('maxfilesize', getMaxFileSize()); 1495 $PAGE->assign(
1496 'maxfilesize',
1497 get_max_upload_size(
1498 ini_get('post_max_size'),
1499 ini_get('upload_max_filesize'),
1500 false
1501 )
1502 );
1503 $PAGE->assign(
1504 'maxfilesizeHuman',
1505 get_max_upload_size(
1506 ini_get('post_max_size'),
1507 ini_get('upload_max_filesize'),
1508 true
1509 )
1510 );
1512 $PAGE->renderPage('import'); 1511 $PAGE->renderPage('import');
1513 exit; 1512 exit;
1514 } 1513 }
@@ -1518,7 +1517,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1518 // The file is too big or some form field may be missing. 1517 // The file is too big or some form field may be missing.
1519 echo '<script>alert("The file you are trying to upload is probably' 1518 echo '<script>alert("The file you are trying to upload is probably'
1520 .' bigger than what this webserver can accept (' 1519 .' bigger than what this webserver can accept ('
1521 .getMaxFileSize().' bytes).' 1520 .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').'
1522 .' Please upload in smaller chunks.");document.location=\'?do=' 1521 .' Please upload in smaller chunks.");document.location=\'?do='
1523 .Router::$PAGE_IMPORT .'\';</script>'; 1522 .Router::$PAGE_IMPORT .'\';</script>';
1524 exit; 1523 exit;
@@ -2227,9 +2226,10 @@ $app = new \Slim\App($container);
2227 2226
2228// REST API routes 2227// REST API routes
2229$app->group('/api/v1', function() { 2228$app->group('/api/v1', function() {
2230 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo'); 2229 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
2231 $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks'); 2230 $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
2232 $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink'); 2231 $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
2232 $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
2233})->add('\Shaarli\Api\ApiMiddleware'); 2233})->add('\Shaarli\Api\ApiMiddleware');
2234 2234
2235$response = $app->run(true); 2235$response = $app->run(true);
diff --git a/plugins/readityourself/book-open.png b/plugins/readityourself/book-open.png
deleted file mode 100644
index 36513d7b..00000000
--- a/plugins/readityourself/book-open.png
+++ /dev/null
Binary files differ
diff --git a/plugins/readityourself/readityourself.html b/plugins/readityourself/readityourself.html
deleted file mode 100644
index 5e200715..00000000
--- a/plugins/readityourself/readityourself.html
+++ /dev/null
@@ -1 +0,0 @@
1<span><a href="%s?url=%s"><img class="linklist-plugin-icon" src="%s/readityourself/book-open.png" title="Read with Readityourself" alt="readityourself" /></a></span>
diff --git a/plugins/readityourself/readityourself.meta b/plugins/readityourself/readityourself.meta
deleted file mode 100644
index bd611dd0..00000000
--- a/plugins/readityourself/readityourself.meta
+++ /dev/null
@@ -1,2 +0,0 @@
1description="For each link, add a ReadItYourself icon to save the shaared URL."
2parameters=READITYOUSELF_URL; \ No newline at end of file
diff --git a/plugins/readityourself/readityourself.php b/plugins/readityourself/readityourself.php
deleted file mode 100644
index 961c5bda..00000000
--- a/plugins/readityourself/readityourself.php
+++ /dev/null
@@ -1,51 +0,0 @@
1<?php
2
3/**
4 * Plugin readityourself
5 */
6
7// If we're talking about https://github.com/memiks/readityourself
8// it seems kinda dead.
9// Not tested.
10
11/**
12 * Init function, return an error if the server is not set.
13 *
14 * @param $conf ConfigManager instance.
15 *
16 * @return array Eventual error.
17 */
18function readityourself_init($conf)
19{
20 $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
21 if (empty($riyUrl)) {
22 $error = 'Readityourself plugin error: '.
23 'Please define the "READITYOUSELF_URL" setting in the plugin administration page.';
24 return array($error);
25 }
26}
27
28/**
29 * Add readityourself icon to link_plugin when rendering linklist.
30 *
31 * @param mixed $data Linklist data.
32 * @param ConfigManager $conf Configuration Manager instance.
33 *
34 * @return mixed - linklist data with readityourself plugin.
35 */
36function hook_readityourself_render_linklist($data, $conf)
37{
38 $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
39 if (empty($riyUrl)) {
40 return $data;
41 }
42
43 $readityourself_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/readityourself/readityourself.html');
44
45 foreach ($data['links'] as &$value) {
46 $readityourself = sprintf($readityourself_html, $riyUrl, $value['url'], PluginManager::$PLUGINS_PATH);
47 $value['link_plugin'][] = $readityourself;
48 }
49
50 return $data;
51}
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index e70cc1ae..d6a0aad5 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -4,6 +4,7 @@
4 */ 4 */
5 5
6require_once 'application/Utils.php'; 6require_once 'application/Utils.php';
7require_once 'application/Languages.php';
7require_once 'tests/utils/ReferenceSessionIdHashes.php'; 8require_once 'tests/utils/ReferenceSessionIdHashes.php';
8 9
9// Initialize reference data before PHPUnit starts a session 10// Initialize reference data before PHPUnit starts a session
@@ -326,4 +327,94 @@ class UtilsTest extends PHPUnit_Framework_TestCase
326 $this->assertFalse(format_date([])); 327 $this->assertFalse(format_date([]));
327 $this->assertFalse(format_date(null)); 328 $this->assertFalse(format_date(null));
328 } 329 }
330
331 /**
332 * Test is_integer_mixed with valid values
333 */
334 public function testIsIntegerMixedValid()
335 {
336 $this->assertTrue(is_integer_mixed(12));
337 $this->assertTrue(is_integer_mixed('12'));
338 $this->assertTrue(is_integer_mixed(-12));
339 $this->assertTrue(is_integer_mixed('-12'));
340 $this->assertTrue(is_integer_mixed(0));
341 $this->assertTrue(is_integer_mixed('0'));
342 $this->assertTrue(is_integer_mixed(0x0a));
343 }
344
345 /**
346 * Test is_integer_mixed with invalid values
347 */
348 public function testIsIntegerMixedInvalid()
349 {
350 $this->assertFalse(is_integer_mixed(true));
351 $this->assertFalse(is_integer_mixed(false));
352 $this->assertFalse(is_integer_mixed([]));
353 $this->assertFalse(is_integer_mixed(['test']));
354 $this->assertFalse(is_integer_mixed([12]));
355 $this->assertFalse(is_integer_mixed(new DateTime()));
356 $this->assertFalse(is_integer_mixed('0x0a'));
357 $this->assertFalse(is_integer_mixed('12k'));
358 $this->assertFalse(is_integer_mixed('k12'));
359 $this->assertFalse(is_integer_mixed(''));
360 }
361
362 /**
363 * Test return_bytes
364 */
365 public function testReturnBytes()
366 {
367 $this->assertEquals(2 * 1024, return_bytes('2k'));
368 $this->assertEquals(2 * 1024, return_bytes('2K'));
369 $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2m'));
370 $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2M'));
371 $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2g'));
372 $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2G'));
373 $this->assertEquals(374, return_bytes('374'));
374 $this->assertEquals(374, return_bytes(374));
375 $this->assertEquals(0, return_bytes('0'));
376 $this->assertEquals(0, return_bytes(0));
377 $this->assertEquals(-1, return_bytes('-1'));
378 $this->assertEquals(-1, return_bytes(-1));
379 $this->assertEquals('', return_bytes(''));
380 }
381
382 /**
383 * Test human_bytes
384 */
385 public function testHumanBytes()
386 {
387 $this->assertEquals('2kiB', human_bytes(2 * 1024));
388 $this->assertEquals('2kiB', human_bytes(strval(2 * 1024)));
389 $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2))));
390 $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2)))));
391 $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3))));
392 $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3)))));
393 $this->assertEquals('374B', human_bytes(374));
394 $this->assertEquals('374B', human_bytes('374'));
395 $this->assertEquals('232kiB', human_bytes(237481));
396 $this->assertEquals('Unlimited', human_bytes('0'));
397 $this->assertEquals('Unlimited', human_bytes(0));
398 $this->assertEquals('Setting not set', human_bytes(''));
399 }
400
401 /**
402 * Test get_max_upload_size with formatting
403 */
404 public function testGetMaxUploadSize()
405 {
406 $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k'));
407 $this->assertEquals('1MiB', get_max_upload_size('1m', '2m'));
408 $this->assertEquals('100B', get_max_upload_size(100, 100));
409 }
410
411 /**
412 * Test get_max_upload_size without formatting
413 */
414 public function testGetMaxUploadSizeRaw()
415 {
416 $this->assertEquals('1048576', get_max_upload_size(2097152, '1024k', false));
417 $this->assertEquals('1048576', get_max_upload_size('1m', '2m', false));
418 $this->assertEquals('100', get_max_upload_size(100, 100, false));
419 }
329} 420}
diff --git a/tests/api/controllers/PostLinkTest.php b/tests/api/controllers/PostLinkTest.php
new file mode 100644
index 00000000..3ed7bcb0
--- /dev/null
+++ b/tests/api/controllers/PostLinkTest.php
@@ -0,0 +1,193 @@
1<?php
2
3namespace Shaarli\Api\Controllers;
4
5
6use Shaarli\Config\ConfigManager;
7use Slim\Container;
8use Slim\Http\Environment;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Class PostLinkTest
14 *
15 * Test POST Link REST API service.
16 *
17 * @package Shaarli\Api\Controllers
18 */
19class PostLinkTest extends \PHPUnit_Framework_TestCase
20{
21 /**
22 * @var string datastore to test write operations
23 */
24 protected static $testDatastore = 'sandbox/datastore.php';
25
26 /**
27 * @var 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 Links controller instance.
43 */
44 protected $controller;
45
46 /**
47 * Number of JSON field per link.
48 */
49 const NB_FIELDS_LINK = 9;
50
51 /**
52 * Before every 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.json.php');
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
64 $this->controller = new Links($this->container);
65
66 $mock = $this->getMock('\Slim\Router', ['relativePathFor']);
67 $mock->expects($this->any())
68 ->method('relativePathFor')
69 ->willReturn('api/v1/links/1');
70
71 // affect @property-read... seems to work
72 $this->controller->getCi()->router = $mock;
73
74 // Used by index_url().
75 $this->controller->getCi()['environment'] = [
76 'SERVER_NAME' => 'domain.tld',
77 'SERVER_PORT' => 80,
78 'SCRIPT_NAME' => '/',
79 ];
80 }
81
82 /**
83 * After every test, remove the test datastore.
84 */
85 public function tearDown()
86 {
87 @unlink(self::$testDatastore);
88 }
89
90 /**
91 * Test link creation without any field: creates a blank note.
92 */
93 public function testPostLinkMinimal()
94 {
95 $env = Environment::mock([
96 'REQUEST_METHOD' => 'POST',
97 ]);
98
99 $request = Request::createFromEnvironment($env);
100
101 $response = $this->controller->postLink($request, new Response());
102 $this->assertEquals(201, $response->getStatusCode());
103 $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]);
104 $data = json_decode((string) $response->getBody(), true);
105 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
106 $this->assertEquals(43, $data['id']);
107 $this->assertRegExp('/[\w-_]{6}/', $data['shorturl']);
108 $this->assertEquals('http://domain.tld/?' . $data['shorturl'], $data['url']);
109 $this->assertEquals('?' . $data['shorturl'], $data['title']);
110 $this->assertEquals('', $data['description']);
111 $this->assertEquals([], $data['tags']);
112 $this->assertEquals(false, $data['private']);
113 $this->assertTrue(new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']));
114 $this->assertEquals('', $data['updated']);
115 }
116
117 /**
118 * Test link creation with all available fields.
119 */
120 public function testPostLinkFull()
121 {
122 $link = [
123 'url' => 'website.tld/test?foo=bar',
124 'title' => 'new entry',
125 'description' => 'shaare description',
126 'tags' => ['one', 'two'],
127 'private' => true,
128 ];
129 $env = Environment::mock([
130 'REQUEST_METHOD' => 'POST',
131 'CONTENT_TYPE' => 'application/json'
132 ]);
133
134 $request = Request::createFromEnvironment($env);
135 $request = $request->withParsedBody($link);
136 $response = $this->controller->postLink($request, new Response());
137
138 $this->assertEquals(201, $response->getStatusCode());
139 $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]);
140 $data = json_decode((string) $response->getBody(), true);
141 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
142 $this->assertEquals(43, $data['id']);
143 $this->assertRegExp('/[\w-_]{6}/', $data['shorturl']);
144 $this->assertEquals('http://' . $link['url'], $data['url']);
145 $this->assertEquals($link['title'], $data['title']);
146 $this->assertEquals($link['description'], $data['description']);
147 $this->assertEquals($link['tags'], $data['tags']);
148 $this->assertEquals(true, $data['private']);
149 $this->assertTrue(new \DateTime('2 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']));
150 $this->assertEquals('', $data['updated']);
151 }
152
153 /**
154 * Test link creation with an existing link (duplicate URL). Should return a 409 HTTP error and the existing link.
155 */
156 public function testPostLinkDuplicate()
157 {
158 $link = [
159 'url' => 'mediagoblin.org/',
160 'title' => 'new entry',
161 'description' => 'shaare description',
162 'tags' => ['one', 'two'],
163 'private' => true,
164 ];
165 $env = Environment::mock([
166 'REQUEST_METHOD' => 'POST',
167 'CONTENT_TYPE' => 'application/json'
168 ]);
169
170 $request = Request::createFromEnvironment($env);
171 $request = $request->withParsedBody($link);
172 $response = $this->controller->postLink($request, new Response());
173
174 $this->assertEquals(409, $response->getStatusCode());
175 $data = json_decode((string) $response->getBody(), true);
176 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
177 $this->assertEquals(7, $data['id']);
178 $this->assertEquals('IuWvgA', $data['shorturl']);
179 $this->assertEquals('http://mediagoblin.org/', $data['url']);
180 $this->assertEquals('MediaGoblin', $data['title']);
181 $this->assertEquals('A free software media publishing platform #hashtagOther', $data['description']);
182 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
183 $this->assertEquals(false, $data['private']);
184 $this->assertEquals(
185 \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130614_184135'),
186 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
187 );
188 $this->assertEquals(
189 \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130615_184230'),
190 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
191 );
192 }
193}
diff --git a/tests/languages/de/UtilsDeTest.php b/tests/languages/de/UtilsDeTest.php
index 545fa572..6c9c9adc 100644
--- a/tests/languages/de/UtilsDeTest.php
+++ b/tests/languages/de/UtilsDeTest.php
@@ -11,7 +11,16 @@ class UtilsDeTest extends UtilsTest
11 public function testDateFormat() 11 public function testDateFormat()
12 { 12 {
13 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 13 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
14 $this->assertRegExp('/1. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true)); 14 $this->assertRegExp('/1\. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true, true));
15 }
16
17 /**
18 * Test date_format() without time.
19 */
20 public function testDateFormatNoTime()
21 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertRegExp('/1\. Januar 2017/', format_date($date, false,true));
15 } 24 }
16 25
17 /** 26 /**
@@ -20,7 +29,16 @@ class UtilsDeTest extends UtilsTest
20 public function testDateFormatDefault() 29 public function testDateFormatDefault()
21 { 30 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 31 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, false)); 32 $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, true, false));
33 }
34
35 /**
36 * Test date_format() using builtin PHP function strftime without time.
37 */
38 public function testDateFormatDefaultNoTime()
39 {
40 $date = DateTime::createFromFormat('Ymd_His', '20170201_101112');
41 $this->assertEquals('01.02.2017', format_date($date, false, false));
24 } 42 }
25 43
26 /** 44 /**
diff --git a/tests/languages/en/UtilsEnTest.php b/tests/languages/en/UtilsEnTest.php
index 7c829ac7..d8680b2b 100644
--- a/tests/languages/en/UtilsEnTest.php
+++ b/tests/languages/en/UtilsEnTest.php
@@ -11,7 +11,16 @@ class UtilsEnTest extends UtilsTest
11 public function testDateFormat() 11 public function testDateFormat()
12 { 12 {
13 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 13 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
14 $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true)); 14 $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true, true));
15 }
16
17 /**
18 * Test date_format() without time.
19 */
20 public function testDateFormatNoTime()
21 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertRegExp('/January 1, 2017/', format_date($date, false, true));
15 } 24 }
16 25
17 /** 26 /**
@@ -20,7 +29,16 @@ class UtilsEnTest extends UtilsTest
20 public function testDateFormatDefault() 29 public function testDateFormatDefault()
21 { 30 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 31 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, false)); 32 $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, true, false));
33 }
34
35 /**
36 * Test date_format() using builtin PHP function strftime without time.
37 */
38 public function testDateFormatDefaultNoTime()
39 {
40 $date = DateTime::createFromFormat('Ymd_His', '20170201_101112');
41 $this->assertEquals('02/01/2017', format_date($date, false, false));
24 } 42 }
25 43
26 /** 44 /**
diff --git a/tests/languages/fr/UtilsFrTest.php b/tests/languages/fr/UtilsFrTest.php
index 45996ee2..0d50a878 100644
--- a/tests/languages/fr/UtilsFrTest.php
+++ b/tests/languages/fr/UtilsFrTest.php
@@ -15,12 +15,30 @@ class UtilsFrTest extends UtilsTest
15 } 15 }
16 16
17 /** 17 /**
18 * Test date_format() without time.
19 */
20 public function testDateFormatNoTime()
21 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertRegExp('/1 janvier 2017/', format_date($date, false, true));
24 }
25
26 /**
18 * Test date_format() using builtin PHP function strftime. 27 * Test date_format() using builtin PHP function strftime.
19 */ 28 */
20 public function testDateFormatDefault() 29 public function testDateFormatDefault()
21 { 30 {
22 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); 31 $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
23 $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, false)); 32 $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, true, false));
33 }
34
35 /**
36 * Test date_format() using builtin PHP function strftime without time.
37 */
38 public function testDateFormatDefaultNoTime()
39 {
40 $date = DateTime::createFromFormat('Ymd_His', '20170201_101112');
41 $this->assertEquals('01/02/2017', format_date($date, false, false));
24 } 42 }
25 43
26 /** 44 /**
diff --git a/tests/plugins/PluginReadityourselfTest.php b/tests/plugins/PluginReadityourselfTest.php
deleted file mode 100644
index bbba9676..00000000
--- a/tests/plugins/PluginReadityourselfTest.php
+++ /dev/null
@@ -1,99 +0,0 @@
1<?php
2use Shaarli\Config\ConfigManager;
3
4/**
5 * PluginReadityourselfTest.php.php
6 */
7
8require_once 'plugins/readityourself/readityourself.php';
9
10/**
11 * Class PluginWallabagTest
12 * Unit test for the Wallabag plugin
13 */
14class PluginReadityourselfTest extends PHPUnit_Framework_TestCase
15{
16 /**
17 * Reset plugin path
18 */
19 public function setUp()
20 {
21 PluginManager::$PLUGINS_PATH = 'plugins';
22 }
23
24 /**
25 * Test Readityourself init without errors.
26 */
27 public function testReadityourselfInitNoError()
28 {
29 $conf = new ConfigManager('');
30 $conf->set('plugins.READITYOUSELF_URL', 'value');
31 $errors = readityourself_init($conf);
32 $this->assertEmpty($errors);
33 }
34
35 /**
36 * Test Readityourself init with errors.
37 */
38 public function testReadityourselfInitError()
39 {
40 $conf = new ConfigManager('');
41 $errors = readityourself_init($conf);
42 $this->assertNotEmpty($errors);
43 }
44
45 /**
46 * Test render_linklist hook.
47 */
48 public function testReadityourselfLinklist()
49 {
50 $conf = new ConfigManager('');
51 $conf->set('plugins.READITYOUSELF_URL', 'value');
52 $str = 'http://randomstr.com/test';
53 $data = array(
54 'title' => $str,
55 'links' => array(
56 array(
57 'url' => $str,
58 )
59 )
60 );
61
62 $data = hook_readityourself_render_linklist($data, $conf);
63 $link = $data['links'][0];
64 // data shouldn't be altered
65 $this->assertEquals($str, $data['title']);
66 $this->assertEquals($str, $link['url']);
67
68 // plugin data
69 $this->assertEquals(1, count($link['link_plugin']));
70 $this->assertNotFalse(strpos($link['link_plugin'][0], $str));
71 }
72
73 /**
74 * Test without config: nothing should happened.
75 */
76 public function testReadityourselfLinklistWithoutConfig()
77 {
78 $conf = new ConfigManager('');
79 $conf->set('plugins.READITYOUSELF_URL', null);
80 $str = 'http://randomstr.com/test';
81 $data = array(
82 'title' => $str,
83 'links' => array(
84 array(
85 'url' => $str,
86 )
87 )
88 );
89
90 $data = hook_readityourself_render_linklist($data, $conf);
91 $link = $data['links'][0];
92 // data shouldn't be altered
93 $this->assertEquals($str, $data['title']);
94 $this->assertEquals($str, $link['url']);
95
96 // plugin data
97 $this->assertArrayNotHasKey('link_plugin', $link);
98 }
99}
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index 36d58c68..1f4b3063 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -56,7 +56,7 @@ class ReferenceLinkDB
56 0, 56 0,
57 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 57 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'),
58 'gnu media web .hidden hashtag', 58 'gnu media web .hidden hashtag',
59 null, 59 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130615_184230'),
60 'IuWvgA' 60 'IuWvgA'
61 ); 61 );
62 62
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index fd8ee9c2..7469ab59 100644
--- a/tpl/default/configure.html
+++ b/tpl/default/configure.html
@@ -206,7 +206,7 @@
206 <div class="pure-g"> 206 <div class="pure-g">
207 <div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile}"> 207 <div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile}">
208 <div class="form-label"> 208 <div class="form-label">
209 <label for="apiEnabled"> 209 <label for="enableApi">
210 <span class="label-name">{'Enable REST API'|t}</span><br> 210 <span class="label-name">{'Enable REST API'|t}</span><br>
211 <span class="label-desc">{'Allow third party software to use Shaarli such as mobile application'|t}</span> 211 <span class="label-desc">{'Allow third party software to use Shaarli such as mobile application'|t}</span>
212 </label> 212 </label>
@@ -214,7 +214,7 @@
214 </div> 214 </div>
215 <div class="pure-u-lg-{$ratioInput} pure-u-{$ratioInputMobile}"> 215 <div class="pure-u-lg-{$ratioInput} pure-u-{$ratioInputMobile}">
216 <div class="form-input"> 216 <div class="form-input">
217 <input type="checkbox" name="apiEnabled" id="apiEnabled" 217 <input type="checkbox" name="enableApi" id="enableApi"
218 {if="$api_enabled"}checked{/if}/> 218 {if="$api_enabled"}checked{/if}/>
219 </div> 219 </div>
220 </div> 220 </div>
diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css
index 8fcd13af..73fade5f 100644
--- a/tpl/default/css/shaarli.css
+++ b/tpl/default/css/shaarli.css
@@ -35,14 +35,29 @@ pre {
35} 35}
36 36
37@font-face { 37@font-face {
38 font-family: 'Roboto Slab'; 38 font-family: 'Roboto';
39 font-weight: 400; 39 font-weight: 400;
40 font-style: normal; 40 font-style: normal;
41 src: 41 src:
42 local('Fira Sans'), 42 local('Roboto'),
43 local('Fira-Sans-regular'), 43 local('Roboto-Regular'),
44 url('../fonts/Fira-Sans-regular.woff2') format('woff2'), 44 url('../fonts/Roboto-Regular.woff2') format('woff2'),
45 url('../fonts/Fira-Sans-regular.woff') format('woff'); 45 url('../fonts/Roboto-Regular.woff') format('woff');
46}
47
48@font-face {
49 font-family: 'Roboto';
50 font-weight: 700;
51 font-style: normal;
52 src:
53 local('Roboto'),
54 local('Roboto-Bold'),
55 url('../fonts/Roboto-Bold.woff2') format('woff2'),
56 url('../fonts/Roboto-Bold.woff') format('woff');
57}
58
59body, .pure-g [class*="pure-u"] {
60 font-family: Roboto, Arial, sans-serif;
46} 61}
47 62
48/** 63/**
@@ -68,10 +83,6 @@ pre {
68 .pure-u-xl-visible { display: inline-block !important; } 83 .pure-u-xl-visible { display: inline-block !important; }
69} 84}
70 85
71.pure-g [class*="pure-u"]{
72 font-family: Roboto Slab, Arial, sans-serif;
73}
74
75/** 86/**
76 * Make pure-extras alert closable. 87 * Make pure-extras alert closable.
77 */ 88 */
@@ -504,7 +515,6 @@ pre {
504 color: #252525; 515 color: #252525;
505 text-decoration: none; 516 text-decoration: none;
506 vertical-align: middle; 517 vertical-align: middle;
507 font-family: Roboto Slab, Arial, sans-serif;
508} 518}
509 519
510.linklist-item-title .linklist-link { 520.linklist-item-title .linklist-link {
@@ -560,7 +570,6 @@ pre {
560.linklist-item-description { 570.linklist-item-description {
561 position: relative; 571 position: relative;
562 padding: 10px; 572 padding: 10px;
563 font-family: Roboto Slab, Arial, sans-serif;
564 word-wrap: break-word; 573 word-wrap: break-word;
565 color: #252525; 574 color: #252525;
566 line-height: 1.3em; 575 line-height: 1.3em;
diff --git a/tpl/default/daily.html b/tpl/default/daily.html
index d8c91078..29d845d5 100644
--- a/tpl/default/daily.html
+++ b/tpl/default/daily.html
@@ -44,7 +44,7 @@
44 </div> 44 </div>
45 </div> 45 </div>
46 <div> 46 <div>
47 <h3 class="window-subtitle">{function="strftime('%A %d, %B %Y', $day)"}</h3> 47 <h3 class="window-subtitle">{function="format_date($dayDate, false)"}</h3>
48 48
49 <div id="plugin_zone_about_daily" class="plugin_zone"> 49 <div id="plugin_zone_about_daily" class="plugin_zone">
50 {loop="$daily_about_plugin"} 50 {loop="$daily_about_plugin"}
diff --git a/tpl/default/fonts/Fira-Sans-regular.woff b/tpl/default/fonts/Fira-Sans-regular.woff
deleted file mode 100644
index 014ac317..00000000
--- a/tpl/default/fonts/Fira-Sans-regular.woff
+++ /dev/null
Binary files differ
diff --git a/tpl/default/fonts/Fira-Sans-regular.woff2 b/tpl/default/fonts/Fira-Sans-regular.woff2
deleted file mode 100644
index bf3ad9a4..00000000
--- a/tpl/default/fonts/Fira-Sans-regular.woff2
+++ /dev/null
Binary files differ
diff --git a/tpl/default/fonts/Roboto-Bold.woff b/tpl/default/fonts/Roboto-Bold.woff
new file mode 100644
index 00000000..3d86753b
--- /dev/null
+++ b/tpl/default/fonts/Roboto-Bold.woff
Binary files differ
diff --git a/tpl/default/fonts/Roboto-Bold.woff2 b/tpl/default/fonts/Roboto-Bold.woff2
new file mode 100644
index 00000000..bd05e2ea
--- /dev/null
+++ b/tpl/default/fonts/Roboto-Bold.woff2
Binary files differ
diff --git a/tpl/default/fonts/Roboto-Regular.woff b/tpl/default/fonts/Roboto-Regular.woff
new file mode 100644
index 00000000..464d2062
--- /dev/null
+++ b/tpl/default/fonts/Roboto-Regular.woff
Binary files differ
diff --git a/tpl/default/fonts/Roboto-Regular.woff2 b/tpl/default/fonts/Roboto-Regular.woff2
new file mode 100644
index 00000000..f9661967
--- /dev/null
+++ b/tpl/default/fonts/Roboto-Regular.woff2
Binary files differ
diff --git a/tpl/default/import.html b/tpl/default/import.html
index e6e521e8..1f040685 100644
--- a/tpl/default/import.html
+++ b/tpl/default/import.html
@@ -18,6 +18,7 @@
18 <div class="center" id="import-field"> 18 <div class="center" id="import-field">
19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
20 <input type="file" name="filetoupload"> 20 <input type="file" name="filetoupload">
21 <p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p>
21 </div> 22 </div>
22 23
23 <div class="pure-g"> 24 <div class="pure-g">
diff --git a/tpl/default/install.html b/tpl/default/install.html
index c5052a26..99aca193 100644
--- a/tpl/default/install.html
+++ b/tpl/default/install.html
@@ -7,6 +7,8 @@
7 7
8{$ratioLabel='1-4'} 8{$ratioLabel='1-4'}
9{$ratioInput='3-4'} 9{$ratioInput='3-4'}
10{$ratioLabelMobile='7-8'}
11{$ratioInputMobile='1-8'}
10 12
11<form method="POST" action="#" name="installform" id="installform"> 13<form method="POST" action="#" name="installform" id="installform">
12<div class="pure-g"> 14<div class="pure-g">
@@ -118,6 +120,22 @@
118 </div> 120 </div>
119 </div> 121 </div>
120 122
123 <div class="pure-g">
124 <div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile}">
125 <div class="form-label">
126 <label for="enableApi">
127 <span class="label-name">{'Enable REST API'|t}</span><br>
128 <span class="label-desc">{'Allow third party software to use Shaarli such as mobile application'|t}</span>
129 </label>
130 </div>
131 </div>
132 <div class="pure-u-lg-{$ratioInput} pure-u-{$ratioInputMobile}">
133 <div class="form-input">
134 <input type="checkbox" name="enableApi" id="enableApi" checked />
135 </div>
136 </div>
137 </div>
138
121 <div class="center"> 139 <div class="center">
122 <input type="submit" value="{'Install'|t}" name="Save"> 140 <input type="submit" value="{'Install'|t}" name="Save">
123 </div> 141 </div>
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js
index 714420b7..4d47fcd0 100644
--- a/tpl/default/js/shaarli.js
+++ b/tpl/default/js/shaarli.js
@@ -258,10 +258,9 @@ window.onload = function () {
258 * Remove CSS target padding (for fixed bar) 258 * Remove CSS target padding (for fixed bar)
259 */ 259 */
260 if (location.hash != '') { 260 if (location.hash != '') {
261 var anchor = document.querySelector(location.hash); 261 var anchor = document.getElementById(location.hash.substr(1));
262 if (anchor != null) { 262 if (anchor != null) {
263 var padsize = anchor.clientHeight; 263 var padsize = anchor.clientHeight;
264 console.log(document.querySelector(location.hash).clientHeight);
265 this.window.scroll(0, this.window.scrollY - padsize); 264 this.window.scroll(0, this.window.scrollY - padsize);
266 anchor.style.paddingTop = 0; 265 anchor.style.paddingTop = 0;
267 } 266 }
diff --git a/tpl/vintage/configure.html b/tpl/vintage/configure.html
index 61907ddf..7adc7545 100644
--- a/tpl/vintage/configure.html
+++ b/tpl/vintage/configure.html
@@ -117,9 +117,9 @@
117 <tr> 117 <tr>
118 <td valign="top"><b>Enable REST API</b></td> 118 <td valign="top"><b>Enable REST API</b></td>
119 <td> 119 <td>
120 <input type="checkbox" name="apiEnabled" id="apiEnabled" 120 <input type="checkbox" name="enableApi" id="enableApi"
121 {if="$api_enabled"}checked{/if}/> 121 {if="$api_enabled"}checked{/if}/>
122 <label for="apiEnabled">&nbsp;Allow third party software to use Shaarli such as mobile application.</label> 122 <label for="enableApi">&nbsp;Allow third party software to use Shaarli such as mobile application.</label>
123 </td> 123 </td>
124 </tr> 124 </tr>
125 <tr> 125 <tr>
diff --git a/tpl/vintage/import.html b/tpl/vintage/import.html
index 071e1160..bb9e4a56 100644
--- a/tpl/vintage/import.html
+++ b/tpl/vintage/import.html
@@ -5,7 +5,7 @@
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="uploaddiv"> 7 <div id="uploaddiv">
8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize} bytes). 8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize}).
9 <form method="POST" action="?do=import" enctype="multipart/form-data" 9 <form method="POST" action="?do=import" enctype="multipart/form-data"
10 name="uploadform" id="uploadform"> 10 name="uploadform" id="uploadform">
11 <input type="hidden" name="token" value="{$token}"> 11 <input type="hidden" name="token" value="{$token}">