]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - application/Updater.php
Add a persistent 'shorturl' key to all links
[github/shaarli/Shaarli.git] / application / Updater.php
1 <?php
2
3 /**
4 * Class Updater.
5 * Used to update stuff when a new Shaarli's version is reached.
6 * Update methods are ran only once, and the stored in a JSON file.
7 */
8 class Updater
9 {
10 /**
11 * @var array Updates which are already done.
12 */
13 protected $doneUpdates;
14
15 /**
16 * @var LinkDB instance.
17 */
18 protected $linkDB;
19
20 /**
21 * @var ConfigManager $conf Configuration Manager instance.
22 */
23 protected $conf;
24
25 /**
26 * @var bool True if the user is logged in, false otherwise.
27 */
28 protected $isLoggedIn;
29
30 /**
31 * @var ReflectionMethod[] List of current class methods.
32 */
33 protected $methods;
34
35 /**
36 * Object constructor.
37 *
38 * @param array $doneUpdates Updates which are already done.
39 * @param LinkDB $linkDB LinkDB instance.
40 * @param ConfigManager $conf Configuration Manager instance.
41 * @param boolean $isLoggedIn True if the user is logged in.
42 */
43 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
44 {
45 $this->doneUpdates = $doneUpdates;
46 $this->linkDB = $linkDB;
47 $this->conf = $conf;
48 $this->isLoggedIn = $isLoggedIn;
49
50 // Retrieve all update methods.
51 $class = new ReflectionClass($this);
52 $this->methods = $class->getMethods();
53 }
54
55 /**
56 * Run all new updates.
57 * Update methods have to start with 'updateMethod' and return true (on success).
58 *
59 * @return array An array containing ran updates.
60 *
61 * @throws UpdaterException If something went wrong.
62 */
63 public function update()
64 {
65 $updatesRan = array();
66
67 // If the user isn't logged in, exit without updating.
68 if ($this->isLoggedIn !== true) {
69 return $updatesRan;
70 }
71
72 if ($this->methods == null) {
73 throw new UpdaterException('Couldn\'t retrieve Updater class methods.');
74 }
75
76 foreach ($this->methods as $method) {
77 // Not an update method or already done, pass.
78 if (! startsWith($method->getName(), 'updateMethod')
79 || in_array($method->getName(), $this->doneUpdates)
80 ) {
81 continue;
82 }
83
84 try {
85 $method->setAccessible(true);
86 $res = $method->invoke($this);
87 // Update method must return true to be considered processed.
88 if ($res === true) {
89 $updatesRan[] = $method->getName();
90 }
91 } catch (Exception $e) {
92 throw new UpdaterException($method, $e);
93 }
94 }
95
96 $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan);
97
98 return $updatesRan;
99 }
100
101 /**
102 * @return array Updates methods already processed.
103 */
104 public function getDoneUpdates()
105 {
106 return $this->doneUpdates;
107 }
108
109 /**
110 * Move deprecated options.php to config.php.
111 *
112 * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
113 * options.php is not supported anymore.
114 */
115 public function updateMethodMergeDeprecatedConfigFile()
116 {
117 if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
118 include $this->conf->get('resource.data_dir') . '/options.php';
119
120 // Load GLOBALS into config
121 $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
122 $allowedKeys[] = 'config';
123 foreach ($GLOBALS as $key => $value) {
124 if (in_array($key, $allowedKeys)) {
125 $this->conf->set($key, $value);
126 }
127 }
128 $this->conf->write($this->isLoggedIn);
129 unlink($this->conf->get('resource.data_dir').'/options.php');
130 }
131
132 return true;
133 }
134
135 /**
136 * Rename tags starting with a '-' to work with tag exclusion search.
137 */
138 public function updateMethodRenameDashTags()
139 {
140 $linklist = $this->linkDB->filterSearch();
141 foreach ($linklist as $key => $link) {
142 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
143 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
144 $this->linkDB[$key] = $link;
145 }
146 $this->linkDB->save($this->conf->get('resource.page_cache'));
147 return true;
148 }
149
150 /**
151 * Move old configuration in PHP to the new config system in JSON format.
152 *
153 * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
154 * It will also convert legacy setting keys to the new ones.
155 */
156 public function updateMethodConfigToJson()
157 {
158 // JSON config already exists, nothing to do.
159 if ($this->conf->getConfigIO() instanceof ConfigJson) {
160 return true;
161 }
162
163 $configPhp = new ConfigPhp();
164 $configJson = new ConfigJson();
165 $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
166 rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
167 $this->conf->setConfigIO($configJson);
168 $this->conf->reload();
169
170 $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
171 foreach (ConfigPhp::$ROOT_KEYS as $key) {
172 $this->conf->set($legacyMap[$key], $oldConfig[$key]);
173 }
174
175 // Set sub config keys (config and plugins)
176 $subConfig = array('config', 'plugins');
177 foreach ($subConfig as $sub) {
178 foreach ($oldConfig[$sub] as $key => $value) {
179 if (isset($legacyMap[$sub .'.'. $key])) {
180 $configKey = $legacyMap[$sub .'.'. $key];
181 } else {
182 $configKey = $sub .'.'. $key;
183 }
184 $this->conf->set($configKey, $value);
185 }
186 }
187
188 try{
189 $this->conf->write($this->isLoggedIn);
190 return true;
191 } catch (IOException $e) {
192 error_log($e->getMessage());
193 return false;
194 }
195 }
196
197 /**
198 * Escape settings which have been manually escaped in every request in previous versions:
199 * - general.title
200 * - general.header_link
201 * - redirector.url
202 *
203 * @return bool true if the update is successful, false otherwise.
204 */
205 public function updateMethodEscapeUnescapedConfig()
206 {
207 try {
208 $this->conf->set('general.title', escape($this->conf->get('general.title')));
209 $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
210 $this->conf->set('redirector.url', escape($this->conf->get('redirector.url')));
211 $this->conf->write($this->isLoggedIn);
212 } catch (Exception $e) {
213 error_log($e->getMessage());
214 return false;
215 }
216 return true;
217 }
218
219 /**
220 * Update the database to use the new ID system, which replaces linkdate primary keys.
221 * Also, creation and update dates are now DateTime objects (done by LinkDB).
222 *
223 * Since this update is very sensitve (changing the whole database), the datastore will be
224 * automatically backed up into the file datastore.<datetime>.php.
225 *
226 * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
227 * which will be saved by this method.
228 *
229 * @return bool true if the update is successful, false otherwise.
230 */
231 public function updateMethodDatastoreIds()
232 {
233 // up to date database
234 if (isset($this->linkDB[0])) {
235 return true;
236 }
237
238 $save = $this->conf->get('resource.data_dir') .'/datastore.'. date('YmdHis') .'.php';
239 copy($this->conf->get('resource.datastore'), $save);
240
241 $links = array();
242 foreach ($this->linkDB as $offset => $value) {
243 $links[] = $value;
244 unset($this->linkDB[$offset]);
245 }
246 $links = array_reverse($links);
247 $cpt = 0;
248 foreach ($links as $l) {
249 unset($l['linkdate']);
250 $l['id'] = $cpt;
251 $this->linkDB[$cpt++] = $l;
252 }
253
254 $this->linkDB->save($this->conf->get('resource.page_cache'));
255 $this->linkDB->reorder();
256
257 return true;
258 }
259 }
260
261 /**
262 * Class UpdaterException.
263 */
264 class UpdaterException extends Exception
265 {
266 /**
267 * @var string Method where the error occurred.
268 */
269 protected $method;
270
271 /**
272 * @var Exception The parent exception.
273 */
274 protected $previous;
275
276 /**
277 * Constructor.
278 *
279 * @param string $message Force the error message if set.
280 * @param string $method Method where the error occurred.
281 * @param Exception|bool $previous Parent exception.
282 */
283 public function __construct($message = '', $method = '', $previous = false)
284 {
285 $this->method = $method;
286 $this->previous = $previous;
287 $this->message = $this->buildMessage($message);
288 }
289
290 /**
291 * Build the exception error message.
292 *
293 * @param string $message Optional given error message.
294 *
295 * @return string The built error message.
296 */
297 private function buildMessage($message)
298 {
299 $out = '';
300 if (! empty($message)) {
301 $out .= $message . PHP_EOL;
302 }
303
304 if (! empty($this->method)) {
305 $out .= 'An error occurred while running the update '. $this->method . PHP_EOL;
306 }
307
308 if (! empty($this->previous)) {
309 $out .= ' '. $this->previous->getMessage();
310 }
311
312 return $out;
313 }
314 }
315
316 /**
317 * Read the updates file, and return already done updates.
318 *
319 * @param string $updatesFilepath Updates file path.
320 *
321 * @return array Already done update methods.
322 */
323 function read_updates_file($updatesFilepath)
324 {
325 if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
326 $content = file_get_contents($updatesFilepath);
327 if (! empty($content)) {
328 return explode(';', $content);
329 }
330 }
331 return array();
332 }
333
334 /**
335 * Write updates file.
336 *
337 * @param string $updatesFilepath Updates file path.
338 * @param array $updates Updates array to write.
339 *
340 * @throws Exception Couldn't write version number.
341 */
342 function write_updates_file($updatesFilepath, $updates)
343 {
344 if (empty($updatesFilepath)) {
345 throw new Exception('Updates file path is not set, can\'t write updates.');
346 }
347
348 $res = file_put_contents($updatesFilepath, implode(';', $updates));
349 if ($res === false) {
350 throw new Exception('Unable to write updates in '. $updatesFilepath . '.');
351 }
352 }