summaryrefslogtreecommitdiff
path: root/setup/upgrade.php
diff options
context:
space:
mode:
Diffstat (limited to 'setup/upgrade.php')
-rw-r--r--setup/upgrade.php643
1 files changed, 643 insertions, 0 deletions
diff --git a/setup/upgrade.php b/setup/upgrade.php
new file mode 100644
index 0000000..46d108b
--- /dev/null
+++ b/setup/upgrade.php
@@ -0,0 +1,643 @@
+<?php
+// +----------------------------------------------------------------------
+// | PHP Source
+// +----------------------------------------------------------------------
+// | Copyright (C) 2006 by Cristian Rodriguez R <judas.iscariote@flyspray.org>
+// | Copyright (C) 2007 by Florian Florian Schmitz <floele@flyspray.org>
+// +----------------------------------------------------------------------
+// |
+// | Copyright: See COPYING file that comes with this distribution
+// +----------------------------------------------------------------------
+//
+
+@set_time_limit(0);
+//do it fast damn it
+ini_set('memory_limit', '64M');
+
+// define basic stuff first.
+define('IN_FS', 1);
+define('BASEDIR', dirname(__FILE__));
+define('APPLICATION_PATH', dirname(BASEDIR));
+define('OBJECTS_PATH', APPLICATION_PATH . '/includes');
+require_once OBJECTS_PATH . '/class.flyspray.php';
+define('CONFIG_PATH', Flyspray::get_config_path(APPLICATION_PATH));
+
+define('TEMPLATE_FOLDER', BASEDIR . '/templates/');
+$conf = @parse_ini_file(CONFIG_PATH, true) or die('Cannot open config file at ' . CONFIG_PATH);
+
+$borked = str_replace( 'a', 'b', array( -1 => -1 ) );
+
+if(!isset($borked[-1])) {
+ die("Flyspray cannot run here, sorry :-( \n PHP 4.4.x/5.0.x is buggy on your 64-bit system; you must upgrade to PHP 5.1.x\n" .
+ "or higher. ABORTING. (http://bugs.php.net/bug.php?id=34879 for details)\n");
+}
+
+require_once OBJECTS_PATH . '/fix.inc.php';
+require_once OBJECTS_PATH . '/class.gpc.php';
+require_once OBJECTS_PATH . '/i18n.inc.php';
+
+# fake objects for load_translation()
+class user{var $infos=array();}; class project{var $id=0;};
+$user=new user; $proj=new project;
+load_translations();
+
+// Use composer autoloader
+require dirname(__DIR__) . '/vendor/autoload.php';
+
+// Initialise DB
+require_once dirname(__DIR__) . '/vendor/adodb/adodb-php/adodb.inc.php';
+require_once dirname(__DIR__) . '/vendor/adodb/adodb-php/adodb-xmlschema03.inc.php';
+
+$db = new Database;
+$db->dbOpenFast($conf['database']);
+
+$webdir = dirname(htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'utf-8'));
+$baseurl = rtrim(Flyspray::absoluteURI($webdir),'/\\') . '/' ;
+
+// ---------------------------------------------------------------------
+// Application Web locations
+// ---------------------------------------------------------------------
+$fs = new Flyspray();
+
+define('APPLICATION_SETUP_INDEX', Flyspray::absoluteURI());
+define('UPGRADE_VERSION', Flyspray::base_version($fs->version));
+
+define('DOMAIN_HASH', md5($_SERVER['SERVER_NAME'] . (int) $_SERVER['SERVER_PORT']));
+define('CACHE_DIR', Flyspray::get_tmp_dir() . DIRECTORY_SEPARATOR . DOMAIN_HASH);
+
+// Get installed version
+$sql = $db->query('SELECT pref_value FROM {prefs} WHERE pref_name = ?', array('fs_ver'));
+$installed_version = $db->fetchOne($sql);
+
+$page = new Tpl;
+$page->assign('title', 'Upgrade ');
+$page->assign('short_version', UPGRADE_VERSION);
+
+if (!isset($conf['general']['syntax_plugin']) || !$conf['general']['syntax_plugin'] || $conf['general']['syntax_plugin'] == 'none') {
+ $page->assign('ask_for_conversion', true);
+} else {
+ $page->assign('ask_for_conversion', false);
+}
+
+//cleanup
+//the cache dir
+@rmdirr(sprintf('%s/cache/dokuwiki', APPLICATION_PATH));
+
+// ---------------------------------------------------------------------
+// Now the hard work
+// ---------------------------------------------------------------------
+
+// Find out which upgrades need to be run
+$folders = glob_compat(BASEDIR . '/upgrade/[0-9]*');
+usort($folders, 'version_compare'); // start with lowest version
+
+if (Post::val('upgrade')) {
+ $uplog=array();
+ $uplog[]="Start database transaction";
+ $db->dblink->StartTrans();
+ fix_duplicate_list_entries(true);
+ foreach ($folders as $folder) {
+ if (version_compare($installed_version, $folder, '<=')) {
+ $uplog[]="Start $installed_version to $folder";
+ $uplog[]= execute_upgrade_file($folder, $installed_version);
+ $installed_version = $folder;
+ $uplog[]="End $installed_version to $folder";
+ }
+ }
+
+ # maybe as Filter: $out=html2wiki($input, 'wikistyle'); and $out=wiki2html($input, 'wikistyle') ?
+ # No need for any filter, because dokuwiki format wouldn't be touched anyway. But maybe ask the user
+ # first and explain that html-formatting is now used instead of plain text on installations that didn't
+ # use dokuwiki format. Then, adding paragraph tags and line breaks might enhance readability.
+ // For testing, do not use yet, have to discuss this one with others.
+ if ((!isset($conf['general']['syntax_plugin']) || !$conf['general']['syntax_plugin'] || $conf['general']['syntax_plugin'] == 'none') && Post::val('yes_please_do_convert')) {
+ convert_old_entries('tasks', 'detailed_desc', 'task_id');
+ convert_old_entries('projects', 'intro_message', 'project_id');
+ convert_old_entries('projects', 'default_task', 'project_id');
+ convert_old_entries('comments', 'comment_text', 'comment_id');
+ $page->assign('conversion', true);
+ } else {
+ $page->assign('conversion', false);
+ }
+
+ // we should be done at this point
+ $db->query('UPDATE {prefs} SET pref_value = ? WHERE pref_name = ?', array($fs->version, 'fs_ver'));
+
+ // Fix the sequence in tasks table for PostgreSQL.
+ if ($db->dblink->dataProvider == 'postgres') {
+ $rslt = $db->query('SELECT MAX(task_id) FROM {tasks}');
+ $maxid = $db->fetchOne($rslt);
+ // The correct sequence should normally have a name containing at least both the table and column name in this format.
+ $rslt = $db->query('SELECT relname FROM pg_class WHERE NOT relname ~ \'pg_.*\' AND relname LIKE \'%' . $conf['database']['dbprefix'] . 'tasks_task_id%\' AND relkind = \'S\'');
+ if ($db->countRows($rslt) == 1) {
+ $seqname = $db->fetchOne($rslt);
+ $db->query('SELECT setval(?, ?)', array($seqname, $maxid));
+ }
+ }
+ // */
+ $db->dblink->completeTrans();
+ $installed_version = $fs->version;
+ $page->assign('done', true);
+ $page->assign('upgradelog', $uplog);
+}
+
+function execute_upgrade_file($folder, $installed_version)
+{
+ global $db, $page, $conf;
+ // At first the config file
+ $upgrade_path = BASEDIR . '/upgrade/' . $folder;
+ new ConfUpdater(CONFIG_PATH, $upgrade_path);
+
+ $upgrade_info = parse_ini_file($upgrade_path . '/upgrade.info', true);
+ // dev version upgrade?
+ if ($folder == Flyspray::base_version($installed_version)) {
+ $type = 'develupgrade';
+ } else {
+ $type = 'defaultupgrade';
+ }
+
+ // Next a mix of XML schema files and PHP upgrade scripts
+ if (!isset($upgrade_info[$type])) {
+ die('#1 Bad upgrade.info file.');
+ }
+
+ ksort($upgrade_info[$type]);
+ foreach ($upgrade_info[$type] as $file) {
+ if (substr($file, -4) == '.php') {
+ require_once $upgrade_path . '/' . $file;
+ }
+
+ if (substr($file, -4) == '.xml') {
+ $schema = new adoSchema($db->dblink);
+ $xml = file_get_contents($upgrade_path . '/' . $file);
+ // $xml = str_replace('<table name="', '<table name="' . $conf['database']['dbprefix'], $xml);
+ // Set the prefix for database objects ( before parsing)
+ $schema->setPrefix($conf['database']['dbprefix'], false);
+ $schema->parseSchemaString($xml);
+ $schema->executeSchema(null, true);
+ }
+ }
+
+ // Last but not least global prefs update
+ if (isset($upgrade_info['fsprefs'])) {
+ $sql = $db->query('SELECT pref_name FROM {prefs}');
+ $existing = $db->fetchCol($sql);
+ // Add what is missing
+ foreach ($upgrade_info['fsprefs'] as $name => $value) {
+ if (!in_array($name, $existing)) {
+ $db->query('INSERT INTO {prefs} (pref_name, pref_value) VALUES (?, ?)', array($name, $value));
+ }
+ }
+ // Delete what is too much
+ foreach ($existing as $name) {
+ if (!isset($upgrade_info['fsprefs'][$name])) {
+ $db->query('DELETE FROM {prefs} WHERE pref_name = ?', array($name));
+ }
+ }
+ }
+
+ $db->query('UPDATE {prefs} SET pref_value = ? WHERE pref_name = ?', array(basename($upgrade_path), 'fs_ver'));
+ #$page->assign('done', true);
+ return "Write ".basename($upgrade_path)." into table {prefs} fs_ver in database";
+}
+
+ /**
+ * Delete a file, or a folder and its contents
+ *
+ * @author Aidan Lister <aidan@php.net>
+ * @version 1.0.3
+ * @link http://aidanlister.com/repos/v/function.rmdirr.php
+ * @param string $dirname Directory to delete
+ * @return bool Returns TRUE on success, FALSE on failure
+ * @license Public Domain.
+ */
+
+ function rmdirr($dirname)
+ {
+ // Sanity check
+ if (!file_exists($dirname)) {
+ return false;
+ }
+
+ // Simple delete for a file
+ if (is_file($dirname) || is_link($dirname)) {
+ return unlink($dirname);
+ }
+
+ // Loop through the folder
+ $dir = dir($dirname);
+ while (false !== $entry = $dir->read()) {
+ // Skip pointers
+ if ($entry == '.' || $entry == '..') {
+ continue;
+ }
+ // Recurse
+ rmdirr($dirname . DIRECTORY_SEPARATOR . $entry);
+ }
+ // Clean up
+ $dir->close();
+ return rmdir($dirname);
+ }
+
+
+class ConfUpdater
+{
+ var $old_config = array();
+ var $new_config = array();
+
+ /**
+ * Reads the existing config file and updates it
+ * @param string $location
+ * @access public
+ * @return bool
+ */
+ function ConfUpdater($location, $upgrade_path)
+ {
+ if (!is_writable($location)) {
+ return false;
+ }
+
+ $this->old_config = parse_ini_file($location, true) or die('Aborting: Could not open config file at ' . $location);
+ $this->new_config = parse_ini_file($upgrade_path . '/flyspray.conf.php', true);
+ // Now we overwrite all values of the *default* file if there is one in the existing config
+ array_walk($this->new_config, array($this, '_merge_configs'));
+ // save custom attachment definitions
+ $this->new_config['attachments'] = $this->old_config['attachments'];
+ # first try to keep an existing oauth config on upgrades
+ $this->new_config['oauth'] = $this->old_config['oauth'];
+
+ $this->_write_config($location);
+ }
+
+ /**
+ * Callback function, merges config values
+ * @param array $settings
+ * @access private
+ * @return array
+ */
+ function _merge_configs(&$settings, $group)
+ {
+ foreach ($settings as $key => $value) {
+ if (isset($this->old_config[$group][$key])) {
+ $settings[$key] = $this->old_config[$group][$key];
+ }
+ // Upgrade to MySQLi if possible
+ if ($key == 'dbtype' && strtolower($settings[$key]) == 'mysql' && function_exists('mysqli_connect')) {
+
+ //mysqli is broken on 64bit systems in versions < 5.1 do not use it, tested, does not work.
+ if (php_uname('m') == 'x86_64' && version_compare(phpversion(), '5.1.0', '<')) {
+ continue;
+ }
+
+ $settings[$key] = 'mysqli';
+ }
+ //no matter what, change the randomization key on each upgrade as an extra security improvement.
+ if($key === 'cookiesalt') {
+ $settings[$key] = md5(uniqid(mt_rand(), true));
+ }
+ }
+ }
+
+ /**
+ * Writes the new config file to a given $location
+ * @param string $location
+ * @access private
+ */
+ function _write_config($location)
+ {
+ $new_config = "; <?php die( 'Do not access this page directly.' ); ?>\n\n";
+ foreach ($this->new_config as $group => $settings) {
+ $new_config .= "[{$group}]\n";
+ foreach ($settings as $key => $value) {
+ if (is_array($value)) {
+ foreach ($value as $_key => $_value) {
+ $new_config .= sprintf('%s="%s"', "{$key}[{$_key}]", addslashes($_value)). "\n";
+ }
+ } else {
+ $new_config .= sprintf('%s="%s"', $key, addslashes($value)). "\n";
+ }
+ }
+ $new_config .= "\n";
+ }
+
+ $fp = fopen($location, 'wb');
+ fwrite($fp, $new_config);
+ fclose($fp);
+ }
+}
+
+function postgresql_adodb() {
+ if (class_exists('ReflectionClass')) {
+ require_once dirname(__DIR__) . '/vendor/adodb/adodb-php/adodb-datadict.inc.php';
+ require_once dirname(__DIR__) . '/vendor/adodb/adodb-php/datadict/datadict-postgres.inc.php';
+ $refclass = new ReflectionClass('ADODB2_postgres');
+ $refmethod = $refclass->getMethod('ChangeTableSQL');
+ $implclass = $refmethod->getDeclaringClass();
+ if ($implclass->name === 'ADODB2_postgres') {
+ return true;
+ }
+ return false;
+ } else {
+ // Can't even do the test, hope the user is able to handle the situation him/serself.
+ return true;
+ }
+}
+
+$checks = $todo = array();
+$checks['version_compare'] = version_compare($installed_version, UPGRADE_VERSION) === -1;
+$checks['config_writable'] = is_writable(CONFIG_PATH);
+$checks['db_connect'] = (bool) $db->dblink;
+$checks['installed_version'] = version_compare($installed_version, '0.9.6') === 1;
+$todo['config_writable'] = 'Please make sure that the file at ' . CONFIG_PATH . ' is writable.';
+$todo['db_connect'] = 'Connection to the database could not be established. Check your config.';
+$todo['version_compare'] = 'No newer version than yours can be installed with this upgrader.';
+$todo['installed_version'] = 'An upgrade from Flyspray versions lower than 0.9.6 is not possible.
+ You will have to upgrade manually to at least 0.9.6, the scripts which do that are included in all Flyspray releases <= 0.9.8.';
+
+if ($conf['database']['dbtype'] == 'pgsql') {
+ $checks['postgresql_adodb'] = (bool) postgresql_adodb();
+ $todo['postgresql_adodb'] = 'You have a version of ADOdb that does not contain overridden version of method ChangeTableSQL for PostgreSQL. '
+ . 'Please copy setup/upgrade/1.0/datadict-postgres.inc.php to '
+ . 'vendor/adodb/adodb-php/datadict/ before proceeding with the upgrade process.';
+}
+
+$upgrade_possible = true;
+foreach ($checks as $check => $result) {
+ if ($result !== true) {
+ $upgrade_possible = false;
+ $page->assign('todo', $todo[$check]);
+ break;
+ }
+}
+
+if (isset($upgrade_info['options'])) {
+ // piece of HTML which adds user input, quick and dirty*/
+ $page->assign('upgrade_options', implode('', $upgrade_info['options']));
+}
+
+$page->assign('index', APPLICATION_SETUP_INDEX);
+$page->uses('checks', 'fs', 'upgrade_possible');
+$page->assign('installed_version', $installed_version);
+
+$page->display('upgrade.tpl');
+
+// Functions for checking and fixing possible duplicate entries
+// in database for those tables that now have a unique index.
+
+function fix_duplicate_list_entries($doit=true) {
+ global $db,$uplog;
+
+ // Categories need a bit more thinking. A real life example from
+ // my own database: A big project originally written (horrible!)
+ // in VB6, that I ported to .NET -environment. Categories:
+ // BackOfficer (main category)
+ // -> Reports (subcategory - should be allowed)
+ // BackOfficer.NET (main category)
+ // -> Reports (subcategory - should be allowed)
+ // -> Reports (I added a fake duplicate - should not be allowed)
+
+ $sql = $db->query('SELECT MIN(os_id) id, project_id, os_name
+ FROM {list_os}
+ GROUP BY project_id, os_name
+ HAVING COUNT(*) > 1');
+ $dups = $db->fetchAllArray($sql);
+ if (count($dups) > 0) {
+ if($doit){
+ fix_os_table($dups);
+ } else{
+ $uplog[]='<span class="warning">'.count($dups).' duplicate entries in {list_os}</span>';
+ }
+ }
+
+ $sql = $db->query('SELECT MIN(resolution_id) id, project_id, resolution_name
+ FROM {list_resolution}
+ GROUP BY project_id, resolution_name
+ HAVING COUNT(*) > 1');
+ $dups = $db->fetchAllArray($sql);
+ if (count($dups) > 0) {
+ if($doit){
+ fix_resolution_table($dups);
+ }else{
+ $uplog[]='<span class="warning">'.count($dups).' duplicate entries in {list_resolution}</span>';
+ }
+ }
+
+ $sql = $db->query('SELECT MIN(status_id) id, project_id, status_name
+ FROM {list_status}
+ GROUP BY project_id, status_name
+ HAVING COUNT(*) > 1');
+ $dups = $db->fetchAllArray($sql);
+ if (count($dups) > 0) {
+ if($doit){
+ fix_status_table($dups);
+ }else{
+ $uplog[]='<span class="warning">'.count($dups).' duplicate entries in {list_status}</span>';
+ }
+ }
+ $sql = $db->query('SELECT MIN(tasktype_id) id, project_id, tasktype_name
+ FROM {list_tasktype}
+ GROUP BY project_id, tasktype_name
+ HAVING COUNT(*) > 1');
+ $dups = $db->fetchAllArray($sql);
+ if (count($dups) > 0) {
+ if($doit){
+ fix_tasktype_table($dups);
+ }else{
+ $uplog[]='<span class="warning">'.count($dups).' duplicate entries in {list_tasktype}</span>';
+ }
+ }
+
+ $sql = $db->query('SELECT MIN(version_id) id, project_id, version_name
+ FROM {list_version}
+ GROUP BY project_id, version_name
+ HAVING COUNT(*) > 1');
+ $dups = $db->fetchAllArray($sql);
+ if (count($dups) > 0) {
+ if($doit){
+ fix_version_table($dups);
+ }else{
+ $uplog[]='<span class="warning">'.count($dups).' duplicate entries in {list_version}</span>';
+ }
+ }
+}
+
+function fix_os_table($dups) {
+ global $db;
+
+ foreach ($dups as $dup) {
+ $update_id = $dup['id'];
+
+ $sql = $db->query('SELECT os_id id
+ FROM {list_os}
+ WHERE project_id = ? AND os_name = ?',
+ array($dup['project_id'], $dup['os_name']));
+ $entries = $db->fetchAllArray($sql);
+ foreach ($entries as $entry) {
+ if ($entry['id'] == $update_id) {
+ continue;
+ }
+
+ $db->query('UPDATE {tasks}
+ SET operating_system = ?
+ WHERE operating_system = ?',
+ array($update_id, $entry['id']));
+ $db->query('DELETE FROM {list_os} WHERE os_id = ?', array($entry['id']));
+ }
+ }
+}
+
+function fix_resolution_table($dups) {
+ global $db;
+
+ foreach ($dups as $dup) {
+ $update_id = $dup['id'];
+
+ $sql = $db->query('SELECT resolution_id id
+ FROM {list_resolution}
+ WHERE project_id = ? AND resolution_name = ?',
+ array($dup['project_id'], $dup['resolution_name']));
+ $entries = $db->fetchAllArray($sql);
+ foreach ($entries as $entry) {
+ if ($entry['id'] == $update_id) {
+ continue;
+ }
+
+ $db->query('UPDATE {tasks}
+ SET resolution_reason = ?
+ WHERE resolution_reason = ?',
+ array($update_id, $entry['id']));
+ $db->query('DELETE FROM {list_resolution} WHERE resolution_id = ?', array($entry['id']));
+ }
+ }
+}
+
+function fix_status_table($dups) {
+ global $db;
+
+ foreach ($dups as $dup) {
+ $update_id = $dup['id'];
+
+ $sql = $db->query('SELECT status_id id
+ FROM {list_status}
+ WHERE project_id = ? AND status_name = ?',
+ array($dup['project_id'], $dup['status_name']));
+ $entries = $db->fetchAllArray($sql);
+ foreach ($entries as $entry) {
+ if ($entry['id'] == $update_id) {
+ continue;
+ }
+
+ $db->query('UPDATE {tasks}
+ SET item_status = ?
+ WHERE item_status = ?',
+ array($update_id, $entry['id']));
+ $db->query('DELETE FROM {list_status} WHERE status_id = ?', array($entry['id']));
+ }
+ }
+}
+
+function fix_tasktype_table($dups) {
+ global $db;
+
+ foreach ($dups as $dup) {
+ $update_id = $dup['id'];
+
+ $sql = $db->query('SELECT tasktype_id id
+ FROM {list_tasktype}
+ WHERE project_id = ? AND tasktype_name = ?',
+ array($dup['project_id'], $dup['tasktype_name']));
+ $entries = $db->fetchAllArray($sql);
+ foreach ($entries as $entry) {
+ if ($entry['id'] == $update_id) {
+ continue;
+ }
+
+ $db->query('UPDATE {tasks}
+ SET task_type = ?
+ WHERE task_type = ?',
+ array($update_id, $entry['id']));
+ $db->query('DELETE FROM {list_tasktype} WHERE tasktype_id = ?', array($entry['id']));
+ }
+ }
+}
+
+function fix_version_table($dups) {
+ global $db;
+
+ foreach ($dups as $dup) {
+ $update_id = $dup['id'];
+
+ $sql = $db->query('SELECT version_id id
+ FROM {list_version}
+ WHERE project_id = ? AND version_name = ?',
+ array($dup['project_id'], $dup['version_name']));
+ $entries = $db->fetchAllArray($sql);
+ foreach ($entries as $entry) {
+ if ($entry['id'] == $update_id) {
+ continue;
+ }
+
+ $db->query('UPDATE {tasks}
+ SET product_version = ?
+ WHERE product_version = ?',
+ array($update_id, $entry['id']));
+ $db->query('DELETE FROM {list_version} WHERE version_id = ?', array($entry['id']));
+ }
+ }
+}
+
+// Just a sketch on how database columns could be updated to the new format.
+// Not tested for errors or used anywhere yet.
+
+function convert_old_entries($table, $column, $key) {
+ global $db;
+
+ // Assuming that anything not beginning with < was made with older
+ // versions of flyspray. This will not catch neither those old entries
+ // where the user for some reason really added paragraph tags nor those
+ // made with development version before fixing ckeditors configuration
+ // settings. You can't have everything in a limited time frame, this
+ // should be just good enough.
+ $sql = $db->query("SELECT $key, $column "
+ . "FROM {". $table . "} "
+ . "WHERE $column NOT LIKE '<%'");
+ $entries = $db->fetchAllArray($sql);
+
+ # We should probably better use existing and proven filters for the conversions
+ # maybe this or existing dokuwiki functionality?
+ # $out=html2wiki($input, 'wikistyle'); and $out=wiki2html($input, 'wikistyle')
+
+ foreach ($entries as $entry) {
+ $id = $entry[$key];
+ $data = $entry[$column];
+
+ if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
+ $data = htmlspecialchars($data, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+ } else{
+ $data = htmlspecialchars($data, ENT_QUOTES , 'UTF-8');
+ }
+ // Convert two or more line breaks to paragrahs, Windows/Unix/Linux formats
+ $data = preg_replace('/(\h*\r?\n)+\h*\r?\n/', "</p><p>", $data);
+ // Data coming from Macs has only carriage returns, and couldn't say
+ // \r?\n? in the previous regex, it would also have matched nothing.
+ // Even a short word like "it" has three nothings in it, one before
+ // i, one between i and t and one after t...
+ $data = preg_replace('/(\h*\r)+\h*\r/', "</p><p>", $data);
+ // Remaining single line breaks
+ $data = preg_replace('/\h*\r?\n/', "<br/>", $data);
+ $data = preg_replace('/\h*\r/', "<br/>", $data);
+ // Remove final extra break, if the data to converted ended with a line break
+ $data = preg_replace('#<br/>$#', '', $data);
+ // Remove final extra paragraph tags, if the data to converted ended with
+ // more than one line breaks
+ $data = preg_replace('#</p><p>$#', '', $data);
+ // Enclose the whole in paragraph tags, so it looks
+ // the same as what ckeditor produces.
+ $data = '<p>' . $data . '</p>';
+
+ $db->query("UPDATE {". $table . "} "
+ . "SET $column = ?"
+ . "WHERE $key = ?",
+ array($data, $id));
+ }
+}