diff options
Diffstat (limited to 'application')
-rw-r--r-- | application/FileUtils.php | 75 | ||||
-rw-r--r-- | application/History.php | 200 | ||||
-rw-r--r-- | application/LinkDB.php | 35 | ||||
-rw-r--r-- | application/NetscapeBookmarkUtils.php | 5 | ||||
-rw-r--r-- | application/PageBuilder.php | 2 | ||||
-rw-r--r-- | application/TimeZone.php | 101 | ||||
-rw-r--r-- | application/Utils.php | 90 | ||||
-rw-r--r-- | application/config/ConfigManager.php | 1 | ||||
-rw-r--r-- | application/exceptions/IOException.php | 22 |
9 files changed, 432 insertions, 99 deletions
diff --git a/application/FileUtils.php b/application/FileUtils.php index 6cac9825..b8ad8970 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php | |||
@@ -1,21 +1,76 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
3 | require_once 'exceptions/IOException.php'; | ||
4 | |||
2 | /** | 5 | /** |
3 | * Exception class thrown when a filesystem access failure happens | 6 | * Class FileUtils |
7 | * | ||
8 | * Utility class for file manipulation. | ||
4 | */ | 9 | */ |
5 | class IOException extends Exception | 10 | class FileUtils |
6 | { | 11 | { |
7 | private $path; | 12 | /** |
13 | * @var string | ||
14 | */ | ||
15 | protected static $phpPrefix = '<?php /* '; | ||
16 | |||
17 | /** | ||
18 | * @var string | ||
19 | */ | ||
20 | protected static $phpSuffix = ' */ ?>'; | ||
8 | 21 | ||
9 | /** | 22 | /** |
10 | * Construct a new IOException | 23 | * Write data into a file (Shaarli database format). |
24 | * The data is stored in a PHP file, as a comment, in compressed base64 format. | ||
25 | * | ||
26 | * The file will be created if it doesn't exist. | ||
27 | * | ||
28 | * @param string $file File path. | ||
29 | * @param string $content Content to write. | ||
30 | * | ||
31 | * @return int|bool Number of bytes written or false if it fails. | ||
11 | * | 32 | * |
12 | * @param string $path path to the resource that cannot be accessed | 33 | * @throws IOException The destination file can't be written. |
13 | * @param string $message Custom exception message. | ||
14 | */ | 34 | */ |
15 | public function __construct($path, $message = '') | 35 | public static function writeFlatDB($file, $content) |
16 | { | 36 | { |
17 | $this->path = $path; | 37 | if (is_file($file) && !is_writeable($file)) { |
18 | $this->message = empty($message) ? 'Error accessing' : $message; | 38 | // The datastore exists but is not writeable |
19 | $this->message .= PHP_EOL . $this->path; | 39 | throw new IOException($file); |
40 | } else if (!is_file($file) && !is_writeable(dirname($file))) { | ||
41 | // The datastore does not exist and its parent directory is not writeable | ||
42 | throw new IOException(dirname($file)); | ||
43 | } | ||
44 | |||
45 | return file_put_contents( | ||
46 | $file, | ||
47 | self::$phpPrefix.base64_encode(gzdeflate(serialize($content))).self::$phpSuffix | ||
48 | ); | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * Read data from a file containing Shaarli database format content. | ||
53 | * If the file isn't readable or doesn't exists, default data will be returned. | ||
54 | * | ||
55 | * @param string $file File path. | ||
56 | * @param mixed $default The default value to return if the file isn't readable. | ||
57 | * | ||
58 | * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails. | ||
59 | */ | ||
60 | public static function readFlatDB($file, $default = null) | ||
61 | { | ||
62 | // Note that gzinflate is faster than gzuncompress. | ||
63 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | ||
64 | if (is_readable($file)) { | ||
65 | return unserialize( | ||
66 | gzinflate( | ||
67 | base64_decode( | ||
68 | substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) | ||
69 | ) | ||
70 | ) | ||
71 | ); | ||
72 | } | ||
73 | |||
74 | return $default; | ||
20 | } | 75 | } |
21 | } | 76 | } |
diff --git a/application/History.php b/application/History.php new file mode 100644 index 00000000..f93b0356 --- /dev/null +++ b/application/History.php | |||
@@ -0,0 +1,200 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Class History | ||
5 | * | ||
6 | * Handle the history file tracing events in Shaarli. | ||
7 | * The history is stored as JSON in a file set by 'resource.history' setting. | ||
8 | * | ||
9 | * Available data: | ||
10 | * - event: event key | ||
11 | * - datetime: event date, in ISO8601 format. | ||
12 | * - id: event item identifier (currently only link IDs). | ||
13 | * | ||
14 | * Available event keys: | ||
15 | * - CREATED: new link | ||
16 | * - UPDATED: link updated | ||
17 | * - DELETED: link deleted | ||
18 | * - SETTINGS: the settings have been updated through the UI. | ||
19 | * | ||
20 | * Note: new events are put at the beginning of the file and history array. | ||
21 | */ | ||
22 | class History | ||
23 | { | ||
24 | /** | ||
25 | * @var string Action key: a new link has been created. | ||
26 | */ | ||
27 | const CREATED = 'CREATED'; | ||
28 | |||
29 | /** | ||
30 | * @var string Action key: a link has been updated. | ||
31 | */ | ||
32 | const UPDATED = 'UPDATED'; | ||
33 | |||
34 | /** | ||
35 | * @var string Action key: a link has been deleted. | ||
36 | */ | ||
37 | const DELETED = 'DELETED'; | ||
38 | |||
39 | /** | ||
40 | * @var string Action key: settings have been updated. | ||
41 | */ | ||
42 | const SETTINGS = 'SETTINGS'; | ||
43 | |||
44 | /** | ||
45 | * @var string History file path. | ||
46 | */ | ||
47 | protected $historyFilePath; | ||
48 | |||
49 | /** | ||
50 | * @var array History data. | ||
51 | */ | ||
52 | protected $history; | ||
53 | |||
54 | /** | ||
55 | * @var int History retention time in seconds (1 month). | ||
56 | */ | ||
57 | protected $retentionTime = 2678400; | ||
58 | |||
59 | /** | ||
60 | * History constructor. | ||
61 | * | ||
62 | * @param string $historyFilePath History file path. | ||
63 | * @param int $retentionTime History content rentention time in seconds. | ||
64 | * | ||
65 | * @throws Exception if something goes wrong. | ||
66 | */ | ||
67 | public function __construct($historyFilePath, $retentionTime = null) | ||
68 | { | ||
69 | $this->historyFilePath = $historyFilePath; | ||
70 | if ($retentionTime !== null) { | ||
71 | $this->retentionTime = $retentionTime; | ||
72 | } | ||
73 | } | ||
74 | |||
75 | /** | ||
76 | * Initialize: read history file. | ||
77 | * | ||
78 | * Allow lazy loading (don't read the file if it isn't necessary). | ||
79 | */ | ||
80 | protected function initialize() | ||
81 | { | ||
82 | $this->check(); | ||
83 | $this->read(); | ||
84 | } | ||
85 | |||
86 | /** | ||
87 | * Add Event: new link. | ||
88 | * | ||
89 | * @param array $link Link data. | ||
90 | */ | ||
91 | public function addLink($link) | ||
92 | { | ||
93 | $this->addEvent(self::CREATED, $link['id']); | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * Add Event: update existing link. | ||
98 | * | ||
99 | * @param array $link Link data. | ||
100 | */ | ||
101 | public function updateLink($link) | ||
102 | { | ||
103 | $this->addEvent(self::UPDATED, $link['id']); | ||
104 | } | ||
105 | |||
106 | /** | ||
107 | * Add Event: delete existing link. | ||
108 | * | ||
109 | * @param array $link Link data. | ||
110 | */ | ||
111 | public function deleteLink($link) | ||
112 | { | ||
113 | $this->addEvent(self::DELETED, $link['id']); | ||
114 | } | ||
115 | |||
116 | /** | ||
117 | * Add Event: settings updated. | ||
118 | */ | ||
119 | public function updateSettings() | ||
120 | { | ||
121 | $this->addEvent(self::SETTINGS); | ||
122 | } | ||
123 | |||
124 | /** | ||
125 | * Save a new event and write it in the history file. | ||
126 | * | ||
127 | * @param string $status Event key, should be defined as constant. | ||
128 | * @param mixed $id Event item identifier (e.g. link ID). | ||
129 | */ | ||
130 | protected function addEvent($status, $id = null) | ||
131 | { | ||
132 | if ($this->history === null) { | ||
133 | $this->initialize(); | ||
134 | } | ||
135 | |||
136 | $item = [ | ||
137 | 'event' => $status, | ||
138 | 'datetime' => (new DateTime())->format(DateTime::ATOM), | ||
139 | 'id' => $id !== null ? $id : '', | ||
140 | ]; | ||
141 | $this->history = array_merge([$item], $this->history); | ||
142 | $this->write(); | ||
143 | } | ||
144 | |||
145 | /** | ||
146 | * Check that the history file is writable. | ||
147 | * Create the file if it doesn't exist. | ||
148 | * | ||
149 | * @throws Exception if it isn't writable. | ||
150 | */ | ||
151 | protected function check() | ||
152 | { | ||
153 | if (! is_file($this->historyFilePath)) { | ||
154 | FileUtils::writeFlatDB($this->historyFilePath, []); | ||
155 | } | ||
156 | |||
157 | if (! is_writable($this->historyFilePath)) { | ||
158 | throw new Exception('History file isn\'t readable or writable'); | ||
159 | } | ||
160 | } | ||
161 | |||
162 | /** | ||
163 | * Read JSON history file. | ||
164 | */ | ||
165 | protected function read() | ||
166 | { | ||
167 | $this->history = FileUtils::readFlatDB($this->historyFilePath, []); | ||
168 | if ($this->history === false) { | ||
169 | throw new Exception('Could not parse history file'); | ||
170 | } | ||
171 | } | ||
172 | |||
173 | /** | ||
174 | * Write JSON history file and delete old entries. | ||
175 | */ | ||
176 | protected function write() | ||
177 | { | ||
178 | $comparaison = new DateTime('-'. $this->retentionTime . ' seconds'); | ||
179 | foreach ($this->history as $key => $value) { | ||
180 | if (DateTime::createFromFormat(DateTime::ATOM, $value['datetime']) < $comparaison) { | ||
181 | unset($this->history[$key]); | ||
182 | } | ||
183 | } | ||
184 | FileUtils::writeFlatDB($this->historyFilePath, array_values($this->history)); | ||
185 | } | ||
186 | |||
187 | /** | ||
188 | * Get the History. | ||
189 | * | ||
190 | * @return array | ||
191 | */ | ||
192 | public function getHistory() | ||
193 | { | ||
194 | if ($this->history === null) { | ||
195 | $this->initialize(); | ||
196 | } | ||
197 | |||
198 | return $this->history; | ||
199 | } | ||
200 | } | ||
diff --git a/application/LinkDB.php b/application/LinkDB.php index 4cee2af9..0d3c85bd 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php | |||
@@ -50,12 +50,6 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
50 | // Link date storage format | 50 | // Link date storage format |
51 | const LINK_DATE_FORMAT = 'Ymd_His'; | 51 | const LINK_DATE_FORMAT = 'Ymd_His'; |
52 | 52 | ||
53 | // Datastore PHP prefix | ||
54 | protected static $phpPrefix = '<?php /* '; | ||
55 | |||
56 | // Datastore PHP suffix | ||
57 | protected static $phpSuffix = ' */ ?>'; | ||
58 | |||
59 | // List of links (associative array) | 53 | // List of links (associative array) |
60 | // - key: link date (e.g. "20110823_124546"), | 54 | // - key: link date (e.g. "20110823_124546"), |
61 | // - value: associative array (keys: title, description...) | 55 | // - value: associative array (keys: title, description...) |
@@ -144,10 +138,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
144 | if (!isset($value['id']) || empty($value['url'])) { | 138 | if (!isset($value['id']) || empty($value['url'])) { |
145 | 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.'); |
146 | } | 140 | } |
147 | if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) { | 141 | if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { |
148 | die('You must specify an integer as a key.'); | 142 | die('You must specify an integer as a key.'); |
149 | } | 143 | } |
150 | if (! empty($offset) && $offset !== $value['id']) { | 144 | if ($offset !== null && $offset !== $value['id']) { |
151 | die('Array offset and link ID must be equal.'); | 145 | die('Array offset and link ID must be equal.'); |
152 | } | 146 | } |
153 | 147 | ||
@@ -295,16 +289,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
295 | return; | 289 | return; |
296 | } | 290 | } |
297 | 291 | ||
298 | // Read data | 292 | $this->links = FileUtils::readFlatDB($this->datastore, []); |
299 | // Note that gzinflate is faster than gzuncompress. | ||
300 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | ||
301 | $this->links = array(); | ||
302 | |||
303 | if (file_exists($this->datastore)) { | ||
304 | $this->links = unserialize(gzinflate(base64_decode( | ||
305 | substr(file_get_contents($this->datastore), | ||
306 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); | ||
307 | } | ||
308 | 293 | ||
309 | $toremove = array(); | 294 | $toremove = array(); |
310 | foreach ($this->links as $key => &$link) { | 295 | foreach ($this->links as $key => &$link) { |
@@ -361,19 +346,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
361 | */ | 346 | */ |
362 | private function write() | 347 | private function write() |
363 | { | 348 | { |
364 | if (is_file($this->datastore) && !is_writeable($this->datastore)) { | 349 | FileUtils::writeFlatDB($this->datastore, $this->links); |
365 | // The datastore exists but is not writeable | ||
366 | throw new IOException($this->datastore); | ||
367 | } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { | ||
368 | // The datastore does not exist and its parent directory is not writeable | ||
369 | throw new IOException(dirname($this->datastore)); | ||
370 | } | ||
371 | |||
372 | file_put_contents( | ||
373 | $this->datastore, | ||
374 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix | ||
375 | ); | ||
376 | |||
377 | } | 350 | } |
378 | 351 | ||
379 | /** | 352 | /** |
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php index ab346f81..bbfde138 100644 --- a/application/NetscapeBookmarkUtils.php +++ b/application/NetscapeBookmarkUtils.php | |||
@@ -95,10 +95,11 @@ class NetscapeBookmarkUtils | |||
95 | * @param array $files Server $_FILES parameters | 95 | * @param array $files Server $_FILES parameters |
96 | * @param LinkDB $linkDb Loaded LinkDB instance | 96 | * @param LinkDB $linkDb Loaded LinkDB instance |
97 | * @param ConfigManager $conf instance | 97 | * @param ConfigManager $conf instance |
98 | * @param History $history History instance | ||
98 | * | 99 | * |
99 | * @return string Summary of the bookmark import status | 100 | * @return string Summary of the bookmark import status |
100 | */ | 101 | */ |
101 | public static function import($post, $files, $linkDb, $conf) | 102 | public static function import($post, $files, $linkDb, $conf, $history) |
102 | { | 103 | { |
103 | $filename = $files['filetoupload']['name']; | 104 | $filename = $files['filetoupload']['name']; |
104 | $filesize = $files['filetoupload']['size']; | 105 | $filesize = $files['filetoupload']['size']; |
@@ -182,6 +183,7 @@ class NetscapeBookmarkUtils | |||
182 | $linkDb[$existingLink['id']] = $newLink; | 183 | $linkDb[$existingLink['id']] = $newLink; |
183 | $importCount++; | 184 | $importCount++; |
184 | $overwriteCount++; | 185 | $overwriteCount++; |
186 | $history->updateLink($newLink); | ||
185 | continue; | 187 | continue; |
186 | } | 188 | } |
187 | 189 | ||
@@ -193,6 +195,7 @@ class NetscapeBookmarkUtils | |||
193 | $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); | 195 | $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); |
194 | $linkDb[$newLink['id']] = $newLink; | 196 | $linkDb[$newLink['id']] = $newLink; |
195 | $importCount++; | 197 | $importCount++; |
198 | $history->addLink($newLink); | ||
196 | } | 199 | } |
197 | 200 | ||
198 | $linkDb->save($conf->get('resource.page_cache')); | 201 | $linkDb->save($conf->get('resource.page_cache')); |
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 | ||
3 | use 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 | **/ |
14 | function generateTimeZoneForm($preselectedTimezone='') | 38 | function 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 .= ' 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/Utils.php b/application/Utils.php index d6e06610..ab463af9 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -345,3 +345,93 @@ function format_date($date, $time = true, $intl = true) | |||
345 | 345 | ||
346 | return $formatter->format($date); | 346 | return $formatter->format($date); |
347 | } | 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/config/ConfigManager.php b/application/config/ConfigManager.php index 7bfbfc72..86a917fb 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php | |||
@@ -301,6 +301,7 @@ class ConfigManager | |||
301 | $this->setEmpty('resource.updates', 'data/updates.txt'); | 301 | $this->setEmpty('resource.updates', 'data/updates.txt'); |
302 | $this->setEmpty('resource.log', 'data/log.txt'); | 302 | $this->setEmpty('resource.log', 'data/log.txt'); |
303 | $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt'); | 303 | $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt'); |
304 | $this->setEmpty('resource.history', 'data/history.php'); | ||
304 | $this->setEmpty('resource.raintpl_tpl', 'tpl/'); | 305 | $this->setEmpty('resource.raintpl_tpl', 'tpl/'); |
305 | $this->setEmpty('resource.theme', 'default'); | 306 | $this->setEmpty('resource.theme', 'default'); |
306 | $this->setEmpty('resource.raintpl_tmp', 'tmp/'); | 307 | $this->setEmpty('resource.raintpl_tmp', 'tmp/'); |
diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php new file mode 100644 index 00000000..b563b23d --- /dev/null +++ b/application/exceptions/IOException.php | |||
@@ -0,0 +1,22 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Exception class thrown when a filesystem access failure happens | ||
5 | */ | ||
6 | class IOException extends Exception | ||
7 | { | ||
8 | private $path; | ||
9 | |||
10 | /** | ||
11 | * Construct a new IOException | ||
12 | * | ||
13 | * @param string $path path to the resource that cannot be accessed | ||
14 | * @param string $message Custom exception message. | ||
15 | */ | ||
16 | public function __construct($path, $message = '') | ||
17 | { | ||
18 | $this->path = $path; | ||
19 | $this->message = empty($message) ? 'Error accessing' : $message; | ||
20 | $this->message .= ' "' . $this->path .'"'; | ||
21 | } | ||
22 | } | ||