diff options
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 | */ |
328 | function format_date($date, $intl = true) | 329 | function 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 | */ | ||
360 | function 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 | */ | ||
376 | function 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 | */ | ||
399 | function 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 | */ | ||
430 | function 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 | } |
@@ -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. | ||
479 | function 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) | ||
493 | function 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...). |
506 | if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. | 478 | if (!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 @@ | |||
1 | description="For each link, add a ReadItYourself icon to save the shaared URL." | ||
2 | parameters=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 | */ | ||
18 | function 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 | */ | ||
36 | function 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 | ||
6 | require_once 'application/Utils.php'; | 6 | require_once 'application/Utils.php'; |
7 | require_once 'application/Languages.php'; | ||
7 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | 8 | require_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 | |||
3 | namespace Shaarli\Api\Controllers; | ||
4 | |||
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 | /** | ||
13 | * Class PostLinkTest | ||
14 | * | ||
15 | * Test POST Link REST API service. | ||
16 | * | ||
17 | * @package Shaarli\Api\Controllers | ||
18 | */ | ||
19 | class 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 | ||
2 | use Shaarli\Config\ConfigManager; | ||
3 | |||
4 | /** | ||
5 | * PluginReadityourselfTest.php.php | ||
6 | */ | ||
7 | |||
8 | require_once 'plugins/readityourself/readityourself.php'; | ||
9 | |||
10 | /** | ||
11 | * Class PluginWallabagTest | ||
12 | * Unit test for the Wallabag plugin | ||
13 | */ | ||
14 | class 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 | |||
59 | body, .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"> Allow third party software to use Shaarli such as mobile application.</label> | 122 | <label for="enableApi"> 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}"> |