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.
11 * @var array Updates which are already done.
13 protected $doneUpdates;
16 * @var LinkDB instance.
21 * @var ConfigManager $conf Configuration Manager instance.
26 * @var bool True if the user is logged in, false otherwise.
28 protected $isLoggedIn;
31 * @var ReflectionMethod[] List of current class methods.
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.
43 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
45 $this->doneUpdates
= $doneUpdates;
46 $this->linkDB
= $linkDB;
48 $this->isLoggedIn
= $isLoggedIn;
50 // Retrieve all update methods.
51 $class = new ReflectionClass($this);
52 $this->methods
= $class->getMethods();
56 * Run all new updates.
57 * Update methods have to start with 'updateMethod' and return true (on success).
59 * @return array An array containing ran updates.
61 * @throws UpdaterException If something went wrong.
63 public function update()
65 $updatesRan = array();
67 // If the user isn't logged in, exit without updating.
68 if ($this->isLoggedIn
!== true) {
72 if ($this->methods
=== null) {
73 throw new UpdaterException('Couldn\'t retrieve Updater class methods.');
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
)
85 $method->setAccessible(true);
86 $res = $method->invoke($this);
87 // Update method must return true to be considered processed.
89 $updatesRan[] = $method->getName();
91 } catch (Exception
$e) {
92 throw new UpdaterException($method, $e);
96 $this->doneUpdates
= array_merge($this->doneUpdates
, $updatesRan);
102 * @return array Updates methods already processed.
104 public function getDoneUpdates()
106 return $this->doneUpdates
;
110 * Move deprecated options.php to config.php.
112 * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
113 * options.php is not supported anymore.
115 public function updateMethodMergeDeprecatedConfigFile()
117 if (is_file($this->conf
->get('resource.data_dir') . '/options.php')) {
118 include $this->conf
->get('resource.data_dir') . '/options.php';
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);
128 $this->conf
->write($this->isLoggedIn
);
129 unlink($this->conf
->get('resource.data_dir').'/options.php');
136 * Move old configuration in PHP to the new config system in JSON format.
138 * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
139 * It will also convert legacy setting keys to the new ones.
141 public function updateMethodConfigToJson()
143 // JSON config already exists, nothing to do.
144 if ($this->conf
->getConfigIO() instanceof ConfigJson
) {
148 $configPhp = new ConfigPhp();
149 $configJson = new ConfigJson();
150 $oldConfig = $configPhp->read($this->conf
->getConfigFile() . '.php');
151 rename($this->conf
->getConfigFileExt(), $this->conf
->getConfigFile() . '.save.php');
152 $this->conf
->setConfigIO($configJson);
153 $this->conf
->reload();
155 $legacyMap = array_flip(ConfigPhp
::$LEGACY_KEYS_MAPPING);
156 foreach (ConfigPhp
::$ROOT_KEYS as $key) {
157 $this->conf
->set($legacyMap[$key], $oldConfig[$key]);
160 // Set sub config keys (config and plugins)
161 $subConfig = array('config', 'plugins');
162 foreach ($subConfig as $sub) {
163 foreach ($oldConfig[$sub] as $key => $value) {
164 if (isset($legacyMap[$sub .'.'. $key])) {
165 $configKey = $legacyMap[$sub .'.'. $key];
167 $configKey = $sub .'.'. $key;
169 $this->conf
->set($configKey, $value);
174 $this->conf
->write($this->isLoggedIn
);
176 } catch (IOException
$e) {
177 error_log($e->getMessage());
183 * Escape settings which have been manually escaped in every request in previous versions:
185 * - general.header_link
188 * @return bool true if the update is successful, false otherwise.
190 public function updateMethodEscapeUnescapedConfig()
193 $this->conf
->set('general.title', escape($this->conf
->get('general.title')));
194 $this->conf
->set('general.header_link', escape($this->conf
->get('general.header_link')));
195 $this->conf
->set('redirector.url', escape($this->conf
->get('redirector.url')));
196 $this->conf
->write($this->isLoggedIn
);
197 } catch (Exception
$e) {
198 error_log($e->getMessage());
205 * Update the database to use the new ID system, which replaces linkdate primary keys.
206 * Also, creation and update dates are now DateTime objects (done by LinkDB).
208 * Since this update is very sensitve (changing the whole database), the datastore will be
209 * automatically backed up into the file datastore.<datetime>.php.
211 * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
212 * which will be saved by this method.
214 * @return bool true if the update is successful, false otherwise.
216 public function updateMethodDatastoreIds()
218 // up to date database
219 if (isset($this->linkDB
[0])) {
223 $save = $this->conf
->get('resource.data_dir') .'/datastore.'. date('YmdHis') .'.php';
224 copy($this->conf
->get('resource.datastore'), $save);
227 foreach ($this->linkDB
as $offset => $value) {
229 unset($this->linkDB
[$offset]);
231 $links = array_reverse($links);
233 foreach ($links as $l) {
234 unset($l['linkdate']);
236 $this->linkDB
[$cpt++
] = $l;
239 $this->linkDB
->save($this->conf
->get('resource.page_cache'));
240 $this->linkDB
->reorder();
246 * Rename tags starting with a '-' to work with tag exclusion search.
248 public function updateMethodRenameDashTags()
250 $linklist = $this->linkDB
->filterSearch();
251 foreach ($linklist as $key => $link) {
252 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags
']);
253 $link['tags
'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags
'], true)));
254 $this->linkDB[$key] = $link;
256 $this->linkDB->save($this->conf->get('resource.page_cache
'));
261 * Initialize API settings:
262 * - api.enabled: true
263 * - api.secret: generated secret
265 public function updateMethodApiSettings()
267 if ($this->conf->exists('api
.secret
')) {
271 $this->conf->set('api
.enabled
', true);
275 $this->conf->get('credentials
.login
'),
276 $this->conf->get('credentials
.salt
')
279 $this->conf->write($this->isLoggedIn);
284 * New setting: theme name. If the default theme is used, nothing to do.
286 * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
287 * and the current theme is set as default in the theme setting.
289 * @return bool true if the update is successful, false otherwise.
291 public function updateMethodDefaultTheme()
293 // raintpl_tpl isn't the root template directory anymore
.
294 // We run the update only if this folder still contains the template files.
295 $tplDir = $this->conf
->get('resource.raintpl_tpl');
296 $tplFile = $tplDir . '/linklist.html';
297 if (! file_exists($tplFile)) {
301 $parent = dirname($tplDir);
302 $this->conf
->set('resource.raintpl_tpl', $parent);
303 $this->conf
->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
304 $this->conf
->write($this->isLoggedIn
);
306 // Dependency injection gore
307 RainTPL
::$tpl_dir = $tplDir;
313 * Move the file to inc/user.css to data/user.css.
315 * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
317 * @return bool true if the update is successful, false otherwise.
319 public function updateMethodMoveUserCss()
321 if (! is_file('inc/user.css')) {
325 return rename('inc/user.css', 'data/user.css');
330 * Class UpdaterException.
332 class UpdaterException
extends Exception
335 * @var string Method where the error occurred.
340 * @var Exception The parent exception.
347 * @param string $message Force the error message if set.
348 * @param string $method Method where the error occurred.
349 * @param Exception|bool $previous Parent exception.
351 public function __construct($message = '', $method = '', $previous = false)
353 $this->method
= $method;
354 $this->previous
= $previous;
355 $this->message
= $this->buildMessage($message);
359 * Build the exception error message.
361 * @param string $message Optional given error message.
363 * @return string The built error message.
365 private function buildMessage($message)
368 if (! empty($message)) {
369 $out .= $message . PHP_EOL
;
372 if (! empty($this->method
)) {
373 $out .= 'An error occurred while running the update '. $this->method
. PHP_EOL
;
376 if (! empty($this->previous
)) {
377 $out .= ' '. $this->previous
->getMessage();
385 * Read the updates file, and return already done updates.
387 * @param string $updatesFilepath Updates file path.
389 * @return array Already done update methods.
391 function read_updates_file($updatesFilepath)
393 if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
394 $content = file_get_contents($updatesFilepath);
395 if (! empty($content)) {
396 return explode(';', $content);
403 * Write updates file.
405 * @param string $updatesFilepath Updates file path.
406 * @param array $updates Updates array to write.
408 * @throws Exception Couldn't write version number.
410 function write_updates_file($updatesFilepath, $updates)
412 if (empty($updatesFilepath)) {
413 throw new Exception('Updates file path is not set, can\'t write updates.');
416 $res = file_put_contents($updatesFilepath, implode(';', $updates));
417 if ($res === false) {
418 throw new Exception('Unable to write updates in '. $updatesFilepath . '.');