aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
Diffstat (limited to 'application')
-rw-r--r--application/ApplicationUtils.php45
-rw-r--r--application/LinkDB.php4
-rw-r--r--application/PageBuilder.php2
-rw-r--r--application/TimeZone.php101
-rw-r--r--application/Updater.php44
-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
9 files changed, 308 insertions, 75 deletions
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php
index a0f482b0..85dcbeeb 100644
--- a/application/ApplicationUtils.php
+++ b/application/ApplicationUtils.php
@@ -4,9 +4,13 @@
4 */ 4 */
5class ApplicationUtils 5class ApplicationUtils
6{ 6{
7 /**
8 * @var string File containing the current version
9 */
10 public static $VERSION_FILE = 'shaarli_version.php';
11
7 private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; 12 private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
8 private static $GIT_BRANCHES = array('master', 'stable'); 13 private static $GIT_BRANCHES = array('latest', 'stable');
9 private static $VERSION_FILE = 'shaarli_version.php';
10 private static $VERSION_START_TAG = '<?php /* '; 14 private static $VERSION_START_TAG = '<?php /* ';
11 private static $VERSION_END_TAG = ' */ ?>'; 15 private static $VERSION_END_TAG = ' */ ?>';
12 16
@@ -29,6 +33,30 @@ class ApplicationUtils
29 return false; 33 return false;
30 } 34 }
31 35
36 return $data;
37 }
38
39 /**
40 * Retrieve the version from a remote URL or a file.
41 *
42 * @param string $remote URL or file to fetch.
43 * @param int $timeout For URLs fetching.
44 *
45 * @return bool|string The version or false if it couldn't be retrieved.
46 */
47 public static function getVersion($remote, $timeout = 2)
48 {
49 if (startsWith($remote, 'http')) {
50 if (($data = static::getLatestGitVersionCode($remote, $timeout)) === false) {
51 return false;
52 }
53 } else {
54 if (! is_file($remote)) {
55 return false;
56 }
57 $data = file_get_contents($remote);
58 }
59
32 return str_replace( 60 return str_replace(
33 array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), 61 array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL),
34 array('', '', ''), 62 array('', '', ''),
@@ -65,13 +93,10 @@ class ApplicationUtils
65 $isLoggedIn, 93 $isLoggedIn,
66 $branch='stable') 94 $branch='stable')
67 { 95 {
68 if (! $isLoggedIn) { 96 // Do not check versions for visitors
69 // Do not check versions for visitors 97 // Do not check if the user doesn't want to
70 return false; 98 // Do not check with dev version
71 } 99 if (! $isLoggedIn || empty($enableCheck) || $currentVersion === 'dev') {
72
73 if (empty($enableCheck)) {
74 // Do not check if the user doesn't want to
75 return false; 100 return false;
76 } 101 }
77 102
@@ -93,7 +118,7 @@ class ApplicationUtils
93 118
94 // Late Static Binding allows overriding within tests 119 // Late Static Binding allows overriding within tests
95 // See http://php.net/manual/en/language.oop5.late-static-bindings.php 120 // See http://php.net/manual/en/language.oop5.late-static-bindings.php
96 $latestVersion = static::getLatestGitVersionCode( 121 $latestVersion = static::getVersion(
97 self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE 122 self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE
98 ); 123 );
99 124
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 2fb15040..0d3c85bd 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -138,10 +138,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess
138 if (!isset($value['id']) || empty($value['url'])) { 138 if (!isset($value['id']) || empty($value['url'])) {
139 die('Internal Error: A link should always have an id and URL.'); 139 die('Internal Error: A link should always have an id and URL.');
140 } 140 }
141 if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) { 141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
142 die('You must specify an integer as a key.'); 142 die('You must specify an integer as a key.');
143 } 143 }
144 if (! empty($offset) && $offset !== $value['id']) { 144 if ($offset !== null && $offset !== $value['id']) {
145 die('Array offset and link ID must be equal.'); 145 die('Array offset and link ID must be equal.');
146 } 146 }
147 147
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index b133dee8..8e39455b 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -1,5 +1,7 @@
1<?php 1<?php
2 2
3use Shaarli\Config\ConfigManager;
4
3/** 5/**
4 * This class is in charge of building the final page. 6 * This class is in charge of building the final page.
5 * (This is basically a wrapper around RainTPL which pre-fills some fields.) 7 * (This is basically a wrapper around RainTPL which pre-fills some fields.)
diff --git a/application/TimeZone.php b/application/TimeZone.php
index 36a8fb12..c1869ef8 100644
--- a/application/TimeZone.php
+++ b/application/TimeZone.php
@@ -1,23 +1,42 @@
1<?php 1<?php
2/** 2/**
3 * Generates the timezone selection form and JavaScript. 3 * Generates a list of available timezone continents and cities.
4 * 4 *
5 * Note: 'UTC/UTC' is mapped to 'UTC' to form a valid option 5 * Two distinct array based on available timezones
6 * and the one selected in the settings:
7 * - (0) continents:
8 * + list of available continents
9 * + special key 'selected' containing the value of the selected timezone's continent
10 * - (1) cities:
11 * + list of available cities associated with their continent
12 * + special key 'selected' containing the value of the selected timezone's city (without the continent)
6 * 13 *
7 * Example: preselect Europe/Paris 14 * Example:
8 * list($htmlform, $js) = generateTimeZoneForm('Europe/Paris'); 15 * [
16 * [
17 * 'America',
18 * 'Europe',
19 * 'selected' => 'Europe',
20 * ],
21 * [
22 * ['continent' => 'America', 'city' => 'Toronto'],
23 * ['continent' => 'Europe', 'city' => 'Paris'],
24 * 'selected' => 'Paris',
25 * ],
26 * ];
9 * 27 *
28 * Notes:
29 * - 'UTC/UTC' is mapped to 'UTC' to form a valid option
30 * - a few timezone cities includes the country/state, such as Argentina/Buenos_Aires
31 * - these arrays are designed to build timezone selects in template files with any HTML structure
32 *
33 * @param array $installedTimeZones List of installed timezones as string
10 * @param string $preselectedTimezone preselected timezone (optional) 34 * @param string $preselectedTimezone preselected timezone (optional)
11 * 35 *
12 * @return array containing the generated HTML form and Javascript code 36 * @return array[] continents and cities
13 **/ 37 **/
14function generateTimeZoneForm($preselectedTimezone='') 38function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
15{ 39{
16 // Select the server timezone
17 if ($preselectedTimezone == '') {
18 $preselectedTimezone = date_default_timezone_get();
19 }
20
21 if ($preselectedTimezone == 'UTC') { 40 if ($preselectedTimezone == 'UTC') {
22 $pcity = $pcontinent = 'UTC'; 41 $pcity = $pcontinent = 'UTC';
23 } else { 42 } else {
@@ -27,62 +46,30 @@ function generateTimeZoneForm($preselectedTimezone='')
27 $pcity = substr($preselectedTimezone, $spos+1); 46 $pcity = substr($preselectedTimezone, $spos+1);
28 } 47 }
29 48
30 // The list is in the form 'Europe/Paris', 'America/Argentina/Buenos_Aires' 49 $continents = [];
31 // We split the list in continents/cities. 50 $cities = [];
32 $continents = array(); 51 foreach ($installedTimeZones as $tz) {
33 $cities = array();
34
35 // TODO: use a template to generate the HTML/Javascript form
36
37 foreach (timezone_identifiers_list() as $tz) {
38 if ($tz == 'UTC') { 52 if ($tz == 'UTC') {
39 $tz = 'UTC/UTC'; 53 $tz = 'UTC/UTC';
40 } 54 }
41 $spos = strpos($tz, '/'); 55 $spos = strpos($tz, '/');
42 56
43 if ($spos !== false) { 57 // Ignore invalid timezones
44 $continent = substr($tz, 0, $spos); 58 if ($spos === false) {
45 $city = substr($tz, $spos+1); 59 continue;
46 $continents[$continent] = 1;
47
48 if (!isset($cities[$continent])) {
49 $cities[$continent] = '';
50 }
51 $cities[$continent] .= '<option value="'.$city.'"';
52 if ($pcity == $city) {
53 $cities[$continent] .= ' selected="selected"';
54 }
55 $cities[$continent] .= '>'.$city.'</option>';
56 } 60 }
57 }
58
59 $continentsHtml = '';
60 $continents = array_keys($continents);
61 61
62 foreach ($continents as $continent) { 62 $continent = substr($tz, 0, $spos);
63 $continentsHtml .= '<option value="'.$continent.'"'; 63 $city = substr($tz, $spos+1);
64 if ($pcontinent == $continent) { 64 $cities[] = ['continent' => $continent, 'city' => $city];
65 $continentsHtml .= ' selected="selected"'; 65 $continents[$continent] = true;
66 }
67 $continentsHtml .= '>'.$continent.'</option>';
68 } 66 }
69 67
70 // Timezone selection form 68 $continents = array_keys($continents);
71 $timezoneForm = 'Continent:'; 69 $continents['selected'] = $pcontinent;
72 $timezoneForm .= '<select name="continent" id="continent" onChange="onChangecontinent();">'; 70 $cities['selected'] = $pcity;
73 $timezoneForm .= $continentsHtml.'</select>';
74 $timezoneForm .= '&nbsp;&nbsp;&nbsp;&nbsp;City:';
75 $timezoneForm .= '<select name="city" id="city">'.$cities[$pcontinent].'</select><br />';
76
77 // Javascript handler - updates the city list when the user selects a continent
78 $timezoneJs = '<script>';
79 $timezoneJs .= 'function onChangecontinent() {';
80 $timezoneJs .= 'document.getElementById("city").innerHTML =';
81 $timezoneJs .= ' citiescontinent[document.getElementById("continent").value]; }';
82 $timezoneJs .= 'var citiescontinent = '.json_encode($cities).';';
83 $timezoneJs .= '</script>';
84 71
85 return array($timezoneForm, $timezoneJs); 72 return [$continents, $cities];
86} 73}
87 74
88/** 75/**
diff --git a/application/Updater.php b/application/Updater.php
index efbfc832..0fb68c5a 100644
--- a/application/Updater.php
+++ b/application/Updater.php
@@ -396,6 +396,50 @@ class Updater
396 396
397 return true; 397 return true;
398 } 398 }
399
400 /**
401 * Update updates.check_updates_branch setting.
402 *
403 * If the current major version digit matches the latest branch
404 * major version digit, we set the branch to `latest`,
405 * otherwise we'll check updates on the `stable` branch.
406 *
407 * No update required for the dev version.
408 *
409 * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
410 *
411 * FIXME! This needs to be removed when we switch to first digit major version
412 * instead of the second one since the versionning process will change.
413 */
414 public function updateMethodCheckUpdateRemoteBranch()
415 {
416 if (shaarli_version === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
417 return true;
418 }
419
420 // Get latest branch major version digit
421 $latestVersion = ApplicationUtils::getLatestGitVersionCode(
422 'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
423 5
424 );
425 if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
426 return false;
427 }
428 $latestMajor = $matches[1];
429
430 // Get current major version digit
431 preg_match('/(\d+)\.\d+$/', shaarli_version, $matches);
432 $currentMajor = $matches[1];
433
434 if ($currentMajor === $latestMajor) {
435 $branch = 'latest';
436 } else {
437 $branch = 'stable';
438 }
439 $this->conf->set('updates.check_updates_branch', $branch);
440 $this->conf->write($this->isLoggedIn);
441 return true;
442 }
399} 443}
400 444
401/** 445/**
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}