/** connect this module to a node
 *
 * this makes the link between the node $node_id in area $area_id and this module.
 * In this case we simply link a data container to node $node_id in a 1-to-1 relation.
 * Note that we might decide lateron to keep versions of pages around, e.g. by inserting
 * new records every save rather than updating the existing.
 * 
 * @param object &$output collects the html output (if any)
 * @param int $area_id the area in which $node_id resides
 * @param int $node_id the node to which we need to connect
 * @param array $module the module record straight from the database
 * @return bool TRUE on success, FALSE otherwise
 */
function htmlpage_connect(&$output, $area_id, $node_id, $module)
{
    global $USER;
    $now = strftime('%Y-%m-%d %T');
    $fields = array('node_id' => intval($node_id), 'version' => 1, 'page_data' => '', 'ctime' => $now, 'cuser_id' => $USER->user_id, 'mtime' => $now, 'muser_id' => $USER->user_id);
    $retval = db_insert_into('htmlpages', $fields);
    return $retval == 1 ? TRUE : FALSE;
}
/** install the theme
 *
 * this routine performs the necessary actions to make this theme usable.
 * More specific, this routine adds a handful of default values into the
 * themes_properties table. Once a theme is actually used in an area, these
 * defaults are copied from the themes_properties table to the
 * themes_areas_properties table for the selected area. The user can subsequently
 * edit these properties in the Area Manager.
 *
 * @param array &$messages collects the (error) messages
 * @param int $theme_id the key for this theme in the themes table
 * @return bool TRUE on success + output via $messages, FALSE otherwise
 */
function sophia_install(&$messages, $theme_id)
{
    $now = strftime('%Y-%m-%d %T');
    $properties = array(array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'quicktop_section_id', 'type' => 'i', 'value' => '0', 'sort_order' => 10, 'extra' => 'minvalue=0', 'description' => 'indicates which section to use for the quicklinks at the top of the page')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'quickbottom_section_id', 'type' => 'i', 'value' => '0', 'sort_order' => 20, 'extra' => 'minvalue=0', 'description' => 'indicates which section to use for the quicklinks at the bottom of the page')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'logo_image', 'type' => 's', 'value' => 'program/graphics/waslogo-284x71.png', 'sort_order' => 30, 'extra' => 'maxlength=255', 'description' => 'the URL of the logo file or a path relative to the directory holding index.php')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'logo_width', 'type' => 'i', 'value' => '284', 'sort_order' => 40, 'extra' => 'minvalue=0;maxvalue=2048', 'description' => 'the width of the logo in pixels')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'logo_height', 'type' => 'i', 'value' => '71', 'sort_order' => 50, 'extra' => 'minvalue=0;maxvalue=1536', 'description' => 'the height of the logo in pixels')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'show_breadcrumb_trail', 'type' => 'b', 'value' => '1', 'sort_order' => 60, 'description' => 'this enables/disables the display of the breadcrumb trail in the navigation')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'style_usage_static', 'type' => 'b', 'value' => '1', 'sort_order' => 70, 'extra' => '', 'description' => 'if TRUE this includes the static stylesheet')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'stylesheet', 'type' => 's', 'value' => 'program/themes/sophia/style.css', 'sort_order' => 80, 'extra' => 'maxlength=255', 'description' => 'the URL of the stylesheet or a path relative to the directory holding index.php')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'style_usage_area', 'type' => 'b', 'value' => '1', 'sort_order' => 90, 'extra' => '', 'description' => 'if TRUE this includes the additional style information')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'style', 'type' => 's', 'value' => "", 'sort_order' => 100, 'extra' => 'maxlength=65535;rows=10;columns=70', 'description' => 'additional style information that will be processed AFTER the static stylesheet file')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'style_usage_node', 'type' => 'b', 'value' => '1', 'sort_order' => 110, 'extra' => '', 'description' => 'if TRUE this allows for addition style information from individual sections/pages')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'header_text', 'type' => 's', 'value' => '', 'sort_order' => 120, 'extra' => 'maxlength=255', 'description' => 'additional plain text added to the page header')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'left_top_html', 'type' => 's', 'value' => "", 'sort_order' => 130, 'extra' => 'maxlength=65535;rows=10;columns=70', 'description' => 'additional free form html above the menu')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'left_bottom_html', 'type' => 's', 'value' => "", 'sort_order' => 140, 'extra' => 'maxlength=65535;rows=10;columns=70', 'description' => 'additional free form html below the menu')), array('table' => 'themes_properties', 'fields' => array('theme_id' => $theme_id, 'name' => 'footer_text', 'type' => 's', 'value' => '', 'sort_order' => 150, 'extra' => 'maxlength=255', 'description' => 'additional plain text added to the page footer')));
    $retval = TRUE;
    // assume success
    foreach ($properties as $property) {
        if (db_insert_into($property['table'], $property['fields']) === FALSE) {
            $messages[] = __FUNCTION__ . '(): ' . db_errormessage();
            $retval = FALSE;
        }
    }
    return $retval;
}
/** connect this module to a node
 *
 * this makes the link between the node $node_id in area $area_id and this module.
 * In this case we simply link a single record to node $node_id in a
 * 1-to-1 relation.
 *
 * Note that we set the parameters to more or less reasonable values.
 * It is up to the user to configure the aggregator with appropriate settings.
 * 
 * @param object &$output collects the html output (if any)
 * @param int $area_id the area in which $node_id resides
 * @param int $node_id the node to which we need to connect
 * @param array $module the module record straight from the database
 * @return bool TRUE on success, FALSE otherwise
 */
function aggregator_connect(&$output, $area_id, $node_id, $module)
{
    global $USER;
    $now = strftime('%Y-%m-%d %T');
    $fields = array('node_id' => intval($node_id), 'header' => '', 'introduction' => '', 'node_list' => '', 'items' => 10, 'reverse_order' => FALSE, 'htmlpage_length' => 2, 'snapshots_width' => 512, 'snapshots_height' => 120, 'snapshots_visible' => 3, 'snapshots_showtime' => 5, 'ctime' => $now, 'cuser_id' => $USER->user_id, 'mtime' => $now, 'muser_id' => $USER->user_id);
    $retval = db_insert_into('aggregator', $fields);
    if ($retval !== 1) {
        logger(sprintf('%s(): cannot connect aggregator to node \'%d\': %s', __FUNCTION__, $node_id, db_errormessage()));
        $retval = FALSE;
    } else {
        $retval = TRUE;
    }
    return $retval;
}
/** connect this module to a node
 *
 * this makes the link between the node $node_id in area $area_id and this module.
 * In this case we simply link a single 'scope' parameter to node $node_id in a
 * 1-to-1 relation.
 *
 * Note that we set the parameter 'scope' to 0. This implies a 'small' map.
 * It is up to the user to configure the node to use medium (scope=1) or large (scope=2) map.
 * 
 * @param object &$output collects the html output (if any)
 * @param int $area_id the area in which $node_id resides
 * @param int $node_id the node to which we need to connect
 * @param array $module the module record straight from the database
 * @return bool TRUE on success, FALSE otherwise
 */
function sitemap_connect(&$output, $area_id, $node_id, $module)
{
    global $USER;
    $now = strftime('%Y-%m-%d %T');
    $fields = array('node_id' => intval($node_id), 'header' => '', 'introduction' => '', 'scope' => 0, 'ctime' => $now, 'cuser_id' => $USER->user_id, 'mtime' => $now, 'muser_id' => $USER->user_id);
    $retval = db_insert_into('sitemaps', $fields);
    if ($retval !== 1) {
        logger(sprintf('%s(): cannot connect sitemap to node \'%d\': %s', __FUNCTION__, $node_id, db_errormessage()));
        $retval = FALSE;
    } else {
        $retval = TRUE;
    }
    return $retval;
}
/** install the module
 *
 * this routine installs the module. For this module there
 * are a few properties that need to be stored in the main
 * modules_properties table.  The specific table for this module is
 * already  created based on the tabledefs); see install/crew_tabledefs.php.
 *
 * Note that the record for this module is already created in the
 * modules table; the pkey is $module_id.
 *
 * @param array &$messages collects the (error) messages
 * @param int $module_id the key for this module in the modules table
 * @return bool TRUE on success + output via $messages, FALSE otherwise
 */
function crew_install(&$messages, $module_id)
{
    $properties = array('origin' => array('type' => 's', 'value' => isset($_SERVER['HTTP_HOST']) ? 'http://' . $_SERVER['HTTP_HOST'] : '', 'extra' => 'maxlength=255', 'description' => 'this must match the origin as seen by the browser of the CREW-user'), 'location' => array('type' => 's', 'value' => '', 'extra' => 'maxlength=255', 'description' => 'this is the location (URL) of the websocket server'), 'secret' => array('type' => 's', 'value' => '', 'extra' => 'maxlength=255', 'description' => 'this shared secret is necessary to access the websocket server'));
    $retval = TRUE;
    // assume success
    $table = 'modules_properties';
    $sort_order = 0;
    foreach ($properties as $name => $property) {
        $property['module_id'] = $module_id;
        $property['name'] = $name;
        $sort_order += 10;
        $property['sort_order'] = $sort_order;
        if (db_insert_into($table, $property) === FALSE) {
            $messages[] = __FUNCTION__ . '(): ' . db_errormessage();
            $retval = FALSE;
        }
    }
    return $retval;
}
/** connect this module to a node
 *
 * this makes the link between the node $node_id in area $area_id and this module.
 * In this case we simply link a single 'variant' parameter to node $node_id in a
 * 1-to-1 relation.
 *
 * Note that we set the parameter 'variant' to 1. This equates to the variant
 * where the visitor starts with the title, the optional introductory text and the
 * thumbnail overview. It is up to the user to configure the node to use other
 * variants, eg. start at the first picture full-size or display a slide show.
 * Also note that we start off with an (arbitrary) dimension for the full-size
 * snapshots. This is a per-node setting (as opposed to the systemwide setting
 * for thumbnail dimensions).
 * Finally, we do a little heuristics here by plugging in the current directory
 * from the filemanager. This is dirty, but we might assume that the user
 * uploaded the files to a directory just before adding this snapshots node.
 * In that case there is no need to change anything re: path.
 * If the user did NOT upload files, we plug in the name of the directory
 * which is associated with area $area_id.
 * 
 * @param object &$output collects the html output (if any)
 * @param int $area_id the area in which $node_id resides
 * @param int $node_id the node to which we need to connect
 * @param array $module the module record straight from the database
 * @return bool TRUE on success, FALSE otherwise
 */
function snapshots_connect(&$output, $area_id, $node_id, $module)
{
    global $USER;
    $now = strftime('%Y-%m-%d %T');
    $areas = get_area_records();
    $path = isset($_SESSION['current_directory']) ? $_SESSION['current_directory'] : (isset($areas[$area_id]) ? '/areas/' . $areas[$area_id]['path'] : '');
    unset($areas);
    $fields = array('node_id' => intval($node_id), 'header' => '', 'introduction' => '', 'snapshots_path' => $path, 'variant' => 1, 'dimension' => 512, 'ctime' => $now, 'cuser_id' => $USER->user_id, 'mtime' => $now, 'muser_id' => $USER->user_id);
    $retval = db_insert_into('snapshots', $fields);
    if ($retval !== 1) {
        logger(sprintf('%s(): cannot connect snapshots to node \'%d\': %s', __FUNCTION__, $node_id, db_errormessage()));
        $retval = FALSE;
    } else {
        $retval = TRUE;
    }
    return $retval;
}
 /** save the changed roles in the dialog to the corresponding tables 'acls'
  *
  * this interprets the data from the current dialog and saves the changed roles
  * accordingly. Note that the information about tables and fields etc. is all
  * contained in the dialogdef so we can use this generic save_data() routine.
  *
  * @return bool FALSE on error, TRUE otherwise
  */
 function save_data_permissions()
 {
     if (is_null($this->dialogdef)) {
         logger("save_data_permissions(): huh? dialogdef not set? cannot cope with that", WLOG_DEBUG);
         return FALSE;
     }
     if (!dialog_validate($this->dialogdef)) {
         // there were errors, show them to the user return error
         foreach ($this->dialogdef as $k => $item) {
             if (isset($item['errors']) && $item['errors'] > 0) {
                 $this->output->add_message($item['error_messages']);
             }
         }
         return FALSE;
     }
     // At this point we have valid values in $this->dialogdef
     // Also, we have the old values in our hands so we can sense the difference
     $errors = 0;
     foreach ($this->dialogdef as $k => $item) {
         if (!isset($item['name']) || !isset($item['table_name']) || $item['type'] == F_SUBMIT || isset($item['hidden']) && $item['hidden'] || intval($item['old_value']) == intval($item['value'])) {
             // skip unchanged fields
             continue;
         }
         // At this point we DO have an associated data table
         // and we DO have a changed value. Now we need to
         // save the changes in the database. We first try to
         // update an existing record.
         $table = $item['table_name'];
         $field = $item['table_field'];
         $where = $item['table_where'];
         $value = intval($item['value']);
         $failed = FALSE;
         $rows = db_update($table, array($field => $value), $where);
         if ($rows === FALSE) {
             // oops, failed
             $failed = TRUE;
         } elseif ($rows == 0) {
             // apparently there was no record yet. Go add one.
             if ($table == 'acls') {
                 // this should not happen
                 logger(sprintf("aclmanager: weird! no record acl_id='%d' in 'acls'?", $where['acl_id']));
                 $failed = TRUE;
             } else {
                 $fields = $where;
                 $fields[$field] = $value;
                 if (db_insert_into($table, $fields) === FALSE) {
                     logger(sprintf("aclmanager: cannot insert record in %s: %s", $table, db_errormessage()));
                     $failed = TRUE;
                 }
             }
         }
         if ($failed) {
             $message = t('acl_error_saving_field', 'admin', array('{FIELD}' => isset($item['label']) ? str_replace('~', '', $item['label']) : $item['name']));
             ++$errors;
             ++$this->dialogdef[$k]['errors'];
             $this->dialogdef[$k]['error_messages'][] = $message;
             $this->output->add_message($message);
         }
     }
     if ($errors == 0) {
         $this->output->add_message(t('success_saving_data', 'admin'));
         $retval = TRUE;
     } else {
         $this->output->add_message(t('errors_saving_data', 'admin', array('{ERRORS}' => $errors)));
         $retval = FALSE;
     }
     return $retval;
 }
 /** fill tables in database via include()'ing a file with tabledata
  *
  * @param string $filename contains the table definitions
  * @return bool TRUE on success, FALSE otherwise + messages written to $this->messages
  * @uses $DB
  */
 function insert_tabledata($filename)
 {
     global $DB;
     $retval = TRUE;
     // assume success
     $tabledata = array();
     if (!file_exists($filename)) {
         $this->messages[] = $this->t('error_file_not_found', array('{FILENAME}' => $filename));
         return FALSE;
     } else {
         include $filename;
     }
     foreach ($tabledata as $data) {
         if (db_insert_into($data['table'], $data['fields']) === FALSE) {
             $params = array('{TABLENAME}' => $DB->prefix . $data['table'], '{ERRNO}' => $DB->errno, '{ERROR}' => $DB->error);
             $this->messages[] = $this->t('error_insert_into_table', $params);
             $retval = FALSE;
         }
     }
     return $retval;
 }
/** create a new session in the session table, return the unique sessionkey
 *
 * this creates generates a new unique session key and stores it in a new record
 * in the sessions table. Additional information is recorded in the new record too:
 * the user_id and auxiliary information. This information makes that a session
 * can always be linked to a particular user (which is handy when dealing with
 * locked pages, etc.). This routine attempts to create a unique session key
 * a number of times. If it doesn't work out, the routine returns FALSE.
 *
 * the optional parameter $user_information can be used to store additional
 * information about this user, e.g. the IP-address. This is useful for generating
 * messages like 'Node xxx is currently locked by user YYYY logged in from ZZZZ'.
 *
 * Note that the generation of a unique session key is salted with both the main
 * url of this website and the special salt that was recorded once during installation
 * time. Also, pseudo-random data is added via rand(). Hopefully this will be
 * hard to guess, even though we use md5() to condense this (semi-)random information
 * into only 128 bits.
 *
 * @param int link to the users table, identifies the user that started the session
 * @param string (optional) auxiliary information about the user, e.g. the IP-address
 * @return bool|string FALSE on error, the unique session key ('token') on success
 * @uses $CFG
 * @uses $DB
 * @todo should we also record the IP-address of the user in the session record?
 *       In a way this is a case of information leak, even though it is only between
 *       authenticated users. Mmmm...
 */
function dbsession_create($user_id, $user_information = '')
{
    global $CFG, $DB;
    $salt = $CFG->www;
    if (isset($CFG->salt)) {
        $salt .= $CFG->salt;
    }
    $current_time = strftime('%Y-%m-%d %T');
    // current date/time as string yyyy-mm-dd hh:mm:ss
    for ($tries = 5; $tries > 0; --$tries) {
        $new_sessionkey = md5($salt . uniqid('', TRUE) . rand());
        $retval = db_insert_into('sessions', array('session_key' => $new_sessionkey, 'session_data' => '', 'user_id' => $user_id, 'user_information' => $user_information, 'ctime' => $current_time, 'atime' => $current_time));
        if ($retval !== FALSE) {
            return $new_sessionkey;
        }
    }
    return FALSE;
}
 /** save the new group/capacity for the selected user
  *
  * this adds a record to the users_groups_capacities table, indicating
  * the group membership and the corresponding capacity for the user.
  *
  * @return void output written to browser via $this->output
  * @uses $WAS_SCRIPT_NAME
  */
 function user_groupsave()
 {
     global $WAS_SCRIPT_NAME;
     //
     // 0 -- sanity check
     //
     $user_id = get_parameter_int('user', NULL);
     if (is_null($user_id)) {
         logger("usermanager->user_groupsave(): unspecified parameter user");
         $this->output->add_message(t('error_invalid_parameters', 'admin'));
         $this->users_overview();
         return;
     }
     //
     // 1 -- bail out if the user pressed cancel button
     //
     if (isset($_POST['button_cancel'])) {
         $this->output->add_message(t('cancelled', 'admin'));
         $this->user_groups();
         return;
     }
     //
     // 2 -- make sure the data is valid
     //
     $dialogdef = $this->get_dialogdef_add_usergroup($user_id);
     if (!dialog_validate($dialogdef)) {
         foreach ($dialogdef as $k => $item) {
             if (isset($item['errors']) && $item['errors'] > 0) {
                 $this->output->add_message($item['error_messages']);
             }
         }
         $params = $this->get_user_names($user_id);
         $this->output->add_content('<h2>' . t('usermanager_user_groupadd_header', 'admin', $params) . '</h2>');
         $this->output->add_content(t('usermanager_user_groupadd_explanation', 'admin', $params));
         $href = href($WAS_SCRIPT_NAME, $this->a_params(TASK_USER_GROUPSAVE, $user_id));
         $this->output->add_content(dialog_quickform($href, $dialogdef));
         $this->show_menu_user($user_id, TASK_USER_GROUPS);
         $parameters = $this->a_params(TASK_USER_GROUPADD, $user_id);
         $anchor = t('usermanager_user_groups_add', 'admin');
         $attributes = array('title' => t('usermanager_user_groups_add_title', 'admin'));
         $this->output->add_breadcrumb($WAS_SCRIPT_NAME, $parameters, $attributes, $anchor);
         return;
     }
     //
     // 3 -- save the selected group/capacity to this user account
     //
     $key = $dialogdef['user_group_capacity']['value'];
     list($group_id, $capacity_code) = explode(':', $key);
     $group_id = intval($group_id);
     $capacity_code = intval($capacity_code);
     if ($group_id == 0 || $capacity_code == 0) {
         // the key '0:0' is used to indicate 'no more groups'; pretend that the user cancelled add group membership
         $this->output->add_message(t('cancelled', 'admin'));
         $this->user_groups();
         return;
     }
     $errors = 0;
     // assume all goes well, for now...
     $fields = array('user_id' => $user_id, 'group_id' => $group_id, 'capacity_code' => $capacity_code);
     $table = 'users_groups_capacities';
     if (db_insert_into($table, $fields) === FALSE) {
         // Mmmm, weird. Perhaps there was already a record for $user_id,$group_id with another $capacity_code?
         logger("usermanager: weird: add membership for user '{$user_id}' and group '{$group_id}' failed: " . db_errormessage());
         ++$errors;
         $where = array('user_id' => $user_id, 'group_id' => $group_id);
         if (db_delete($table, $where) === FALSE) {
             logger("usermanager: add membership double-fault, giving up: " . db_errormessage());
             ++$errors;
         } elseif (db_insert_into($table, $fields) === FALSE) {
             logger("usermanager: add membership failed again, giving up: " . db_errormessage());
             ++$errors;
         } else {
             logger("usermanager: add membership for user '{$user_id}' and group '{$group_id}' succeeded, finally");
             $errors = 0;
             // OK. Forget the initial error.
         }
     }
     if ($errors == 0) {
         $this->output->add_message(t('success_saving_data', 'admin'));
     } else {
         $this->output->add_message(t('errors_saving_data', 'admin', array('{ERRORS}' => $errors)));
     }
     $this->user_groups();
 }
/** create a few alerts
 *
 *
 * @param array &$messages used to return (error) messages to caller
 * @param array &$config pertinent information about the site
 * @param array &$tr translations of demodata texts
 * @return bool TRUE on success + data entered into database, FALSE on error
 */
function demodata_alerts(&$messages, &$config, &$tr)
{
    $retval = TRUE;
    $now = strftime("%Y-%m-%d %T");
    $email = $config['user_email'];
    $alerts = array('webmaster' => array('full_name' => $config['user_full_name'], 'email' => $email, 'cron_interval' => 1440, 'cron_next' => $now, 'messages' => 1, 'message_buffer' => $now . "\n" . $tr['alerts_initial_load'] . "\n" . $tr['alerts_every_1440_minutes'] . "\n" . $tr['alerts_all_areas'] . "\n" . $tr['alerts_email_address'] . "\n" . $email . " (" . $config['user_full_name'] . ")\n", 'is_active' => TRUE), 'acackl' => array('full_name' => 'Amelia Cackle', 'email' => $email, 'cron_interval' => 60, 'cron_next' => $now, 'messages' => 1, 'message_buffer' => $now . "\n" . $tr['alerts_initial_load'] . "\n" . $tr['alerts_every_60_minutes'] . "\n" . $tr['alerts_private_area'] . "\n" . $tr['alerts_email_address'] . "\n" . $email . " (Amelia Cackle)\n", 'is_active' => TRUE));
    foreach ($alerts as $alert => $fields) {
        if (($alert_id = db_insert_into_and_get_id('alerts', $fields, 'alert_id')) === FALSE) {
            $messages[] = $tr['error'] . ' ' . db_errormessage();
            $retval = FALSE;
        }
        $alerts[$alert]['alert_id'] = intval($alert_id);
    }
    $alerts_areas_nodes = array('webmaster' => array('alert_id' => $alerts['webmaster']['alert_id'], 'area_id' => 0, 'node_id' => 0, 'flag' => TRUE), 'acackl' => array('alert_id' => $alerts['acackl']['alert_id'], 'area_id' => $config['demo_areas']['private']['area_id'], 'node_id' => 0, 'flag' => TRUE));
    foreach ($alerts_areas_nodes as $fields) {
        if (db_insert_into('alerts_areas_nodes', $fields) === FALSE) {
            $messages[] = $tr['error'] . ' ' . db_errormessage();
            $retval = FALSE;
        }
    }
    return $retval;
}
 /** add a group/capacity and corresponding acl to the database
  *
  * @param int $group_id group of interest
  * @param int $capacity_code the capacity
  * @param int $sort_order
  * @return bool TRUE on success, FALSE otherwise
  */
 function add_group_capacity($group_id, $capacity_code, $sort_order)
 {
     static $fields_acl = array('permissions_intranet' => ACL_ROLE_NONE, 'permissions_modules' => ACL_ROLE_NONE, 'permissions_jobs' => ACL_ROLE_NONE, 'permissions_nodes' => ACL_ROLE_NONE);
     //
     // 1 -- create an acl (with no permissions whatsoever) and remember the new acl_id
     //
     $new_acl_id = db_insert_into_and_get_id('acls', $fields_acl, 'acl_id');
     if ($new_acl_id === FALSE) {
         logger(sprintf("%s.%s(): adding new acl for group/capacity '%d/%d' failed: %s", __CLASS__, __FUNCTION__, $group_id, $capacity_code, db_errormessage()));
         return FALSE;
     }
     //
     // 2 -- subsequently add a new group-capacity record pointing to this new acl
     //
     $fields = array('group_id' => intval($group_id), 'capacity_code' => intval($capacity_code), 'sort_order' => intval($sort_order), 'acl_id' => $new_acl_id);
     if (db_insert_into('groups_capacities', $fields) === FALSE) {
         logger(sprintf("%s.%s(): adding new record for group/capacity '%d/%d' failed: %s", __CLASS__, __FUNCTION__, $group_id, $capacity_code, db_errormessage()));
         $retval = db_delete('acls', array('acl_id' => $new_acl_id));
         logger(sprintf("%s.%s(): removing freshly created acl '%d': %s", __CLASS__, __FUNCTION__, $new_acl_id, $retval !== FALSE ? 'success' : 'failure: ' . db_errormessage()));
         return FALSE;
     }
     logger(sprintf("%s.%s(): success adding group/capacity '%d/%d' with acl_id='%d'", __CLASS__, __FUNCTION__, $group_id, $capacity_code, $new_acl_id), WLOG_DEBUG);
     return TRUE;
 }
/** add demonstration data to the system
 *
 * this routine adds to the existing set of demonstration data as specified
 * in $config. Here we add a few nodes as follows:
 *
 * 'snapshots0': a section containing one page connected to the snapshots_module
 * 'snapshots1': a page in section 'snapshots0' acting as an example set of snapshots
 *
 * The section 'snapshots0' is added at the bottom of the demo-section 'news'
 * Maybe not the best place, but at least we don't add yet another top level
 * menu item.
 *
 * Note
 * If the module is installed via the Install Wizard, this routine is
 * called. However, if a module is installed as an additional module
 * after installation, the {$module}_demodata() routine is never called.
 * This is because the only time you know that demodata is installed is
 * when the Install Wizard runs. If we're called from admin.php, the
 * webmaster may have already deleted existing (core) demodata so you
 * never can be sure what to expect. To put it another way: it is hard
 * to re-construct $config when we're NOT the Instal Wizard.
 *
 * The array $config contains the following information.
 *
 * <code>
 * $config['language_key']   => install language code (eg. 'en')
 * $config['dir']            => path to CMS Root Directory (eg. /home/httpd/htdocs)
 * $config['www']            => URL of CMS Root Directory (eg. http://exemplum.eu)
 * $config['progdir']        => path to program directory (eg. /home/httpd/htdocs/program)
 * $config['progwww']        => URL of program directory (eg. http://exemplum.eu/program)
 * $config['datadir']        => path to data directory (eg. /home/httpd/wasdata/a1b2c3d4e5f6)
 * $config['user_username']  => userid of webmaster (eg. wblader)
 * $config['user_full_name'] => full name of webmaster (eg. Wilhelmina Bladergroen)
 * $config['user_email']     => email of webmaster (eg. w.bladergroen@exemplum.eu)
 * $config['user_id']        => numerical user_id (usually 1)
 * $config['demo_salt']      => password salt for all demodata accounts
 * $config['demo_password']  => password for all demodata accounts
 * $config['demo_areas']     => array with demo area data
 * $config['demo_groups']    => array with demo group data
 * $config['demo_users']     => array with demo user data
 * $config['demo_nodes']     => array with demo node data
 * $config['demo_string']    => array with demo strings from /program/install/languages/LL/demodata.php
 * $config['demo_replace']   => array with search/replace pairs to 'jazz up' the demo strings
 * </code>
 *
 * With this information, we can add a demonstration configuration for the public area,
 * which shows off the possibilities. Note that we add our own additions to the array
 * $config so other modules and themes can determine the correct status quo w.r.t. the
 * demodata nodes etc.
 *
 * @param array &$messages collects the (error) messages
 * @param int $module_id the key for this module in the modules table
 * @param array $configuration pertinent data for the new website + demodata foundation
 * @param array $manifest a copy of the manifest for this module
 * @return bool TRUE on success + output via $messages, FALSE otherwise
 */
function snapshots_demodata(&$messages, $module_id, $config, $manifest)
{
    global $DB;
    $retval = TRUE;
    // assume success
    // 0 -- get hold of the module-specific translations in $string[]
    $string = array();
    $language_key = $config['language_key'];
    $filename = dirname(__FILE__) . '/languages/' . $language_key . '/snapshots.php';
    if (!file_exists($filename)) {
        $filename = dirname(__FILE__) . '/languages/en/snapshots.php';
    }
    @(include $filename);
    if (empty($string)) {
        $messages[] = 'Internal error: no translations in ' . $filename;
        return FALSE;
    }
    // 1A -- prepare for addition of a few nodes
    $sort_order = 0;
    $parent_id = $config['demo_nodes']['news']['node_id'];
    foreach ($config['demo_nodes'] as $node => $fields) {
        if ($fields['parent_id'] == $parent_id && $fields['node_id'] != $parent_id) {
            $sort_order = max($sort_order, $fields['sort_order']);
        }
    }
    $sort_order += 10;
    // place the snapshots-section at the end of the parent section
    $nodes = array('snapshots0' => array('parent_id' => $parent_id, 'is_page' => FALSE, 'title' => strtr($string['snapshots0_title'], $config['demo_replace']), 'link_text' => strtr($string['snapshots0_link_text'], $config['demo_replace']), 'sort_order' => $sort_order), 'snapshots1' => array('parent_id' => 0, 'is_page' => TRUE, 'is_default' => TRUE, 'title' => strtr($string['snapshots1_title'], $config['demo_replace']), 'link_text' => strtr($string['snapshots1_link_text'], $config['demo_replace']), 'sort_order' => 10, 'module_id' => $module_id, 'style' => "div.thumbnail_image a:hover img { border: 5px solid #00FF00; }\n"));
    // 1B -- actually add the necessary nodes
    $now = strftime('%Y-%m-%d %T');
    $user_id = $config['user_id'];
    $area_id = $config['demo_areas']['public']['area_id'];
    foreach ($nodes as $node => $fields) {
        $fields['area_id'] = $area_id;
        $fields['ctime'] = $now;
        $fields['mtime'] = $now;
        $fields['atime'] = $now;
        $fields['owner_id'] = $user_id;
        if ($fields['parent_id'] == 0) {
            $fields['parent_id'] = $config['demo_nodes']['snapshots0']['node_id'];
            // plug in real node_id of 'our' section
        }
        if (($node_id = db_insert_into_and_get_id('nodes', $fields, 'node_id')) === FALSE) {
            $messages[] = $config['demo_string']['error'] . db_errormessage();
            $retval = FALSE;
        }
        $node_id = intval($node_id);
        $fields['node_id'] = $node_id;
        $config['demo_nodes'][$node] = $fields;
    }
    // 2 -- Simulate a series of snapshots in the exemplum area data storage
    // 2A -- copy from module demodata directory to exemplum path
    $pictures = array("allium.jpg", "calendula.jpg", "cynara.jpg", "lagos.jpg", "lavandula.jpg", "mentha.jpg", "nepeta.jpg", "ocimum.jpg", "origanum.jpg", "petroselinum.jpg", "salvia.jpg", "thymus.jpg");
    $thumb_prefix = 'zz_thumb_';
    $path_snapshots = '/areas/' . $config['demo_areas']['public']['path'] . '/snapshots';
    // relative to $datadir
    $fullpath_source = dirname(__FILE__) . '/install/demodata';
    $fullpath_target = $config['datadir'] . $path_snapshots;
    if (@mkdir($fullpath_target, 0700) === FALSE) {
        $messages[] = $config['demo_string']['error'] . "mkdir('{$fullpath_target}')";
        $retval = FALSE;
    } else {
        @touch($fullpath_target . '/index.html');
        // try to "protect" directory
        foreach ($pictures as $picture) {
            $filenames = array($picture, $thumb_prefix . $picture);
            foreach ($filenames as $filename) {
                if (!@copy($fullpath_source . '/' . $filename, $fullpath_target . '/' . $filename)) {
                    $messages[] = $config['demo_string']['error'] . "copy('{$path_snapshots}/{$filename}')";
                    $retval = FALSE;
                }
            }
        }
    }
    $fields = array('node_id' => $config['demo_nodes']['snapshots1']['node_id'], 'header' => strtr($string['snapshots1_header'], $config['demo_replace']), 'introduction' => strtr($string['snapshots1_introduction'], $config['demo_replace']), 'snapshots_path' => $path_snapshots, 'variant' => 1, 'dimension' => 512, 'ctime' => $now, 'cuser_id' => $user_id, 'mtime' => $now, 'muser_id' => $user_id);
    if (db_insert_into('snapshots', $fields) === FALSE) {
        $messages[] = $config['demo_string']['error'] . db_errormessage();
        $retval = FALSE;
    }
    return $retval;
}
/** a simple function to log information to the database 'for future reference'
 *
 * This adds a message to the table log_messages, including a time, the remote address
 * and (of course) a message. See also the standard PHP-function syslog(). We use the
 * existing symbolic constants for priority. Default value is WLOG_INFO.
 *
 * Note that messages with a priority WLOG_DEBUG are only written to the log
 * if the global parameter $CFG->debug is TRUE. All other messages are simply
 * logged, no further questions asked.
 *
 * If the caller does not provide a user_id, this routine attempts to
 * read the user_id from the global $_SESSION array, i.e. we try to link
 * events to a particular user if possible.
 *
 * Note that with a field definition of varchar(150) there is room to store either
 * an IPv4 address (max 15 bytes) or a full-blown IPv6 address (39-47 bytes, see RFC3989) or
 * even twice a complete reverse DNS address (see {@link update_core_2011092100()}).
 *
 * See also {@link task_logview()} for a rant on the difference between LOG_DEBUG and LOG_INFO.
 *
 * @param string $message the message to write to the log
 * @param int $priority loglevel, see PHP-function syslog() for a list of predefined constants
 * @return bool FALSE on error, TRUE on success
 * @uses $CFG
 * @todo should we make this configurable and maybe log directly to syslog 
 *       (with automatic logrotate) or do we want to keep this 'self-contained'
 *       (the webmaster can read the table, but not the machine's syslog)?
 */
function logger($message, $priority = WLOG_INFO, $user_id = '')
{
    global $CFG;
    if ($priority == WLOG_DEBUG && !$CFG->debug) {
        return TRUE;
    }
    // Try to link this information to a particular user
    // if possible (only when a session exists) and if no
    // user_id was provided by the caller.
    if (empty($user_id)) {
        if (isset($_SESSION['user_id'])) {
            $user_id = $_SESSION['user_id'];
        }
    }
    $fields = array('datim' => strftime('%Y-%m-%d %T'), 'remote_addr' => (string) $_SERVER['REMOTE_ADDR'], 'priority' => intval($priority), 'user_id' => empty($user_id) ? NULL : intval($user_id), 'message' => (string) $message);
    $retval = FALSE === db_insert_into('log_messages', $fields) ? FALSE : TRUE;
    return $retval;
}
/** save the modified content data of this module linked to node $node_id
 *
 * this validates and saves the data that was submitted by the user.
 * If validation fails, or storing the data doesn't work, the flag $edit_again
 * is set to TRUE and the return value is FALSE.
 *
 * If the user has cancelled the operation, the flag $edit_again is set to FALSE
 * and the return value is also FALSE.
 *
 * If the modified data is stored successfully, the return value is TRUE (and
 * the value of $edit_again is a don't care).
 *
 * Here is a summary of return values.
 *
 *  - retval = TRUE ==> data saved successfully
 *  - retval = FALSE && edit_again = TRUE ==> re-edit the data, show the edit dialog again
 *  - retval = FALSE && edit_again = FALSE ==> cancelled, do nothing
 *
 * @param object &$output collects the html output (if any)
 * @param int $area_id the area in which $node_id resides
 * @param int $node_id the node to which the content is connected
 * @param array $module the module record straight from the database
 * @param bool $viewonly if TRUE, editing and hence saving is not allowed
 * @param bool &$edit_again set to TRUE if we need to edit the content again, FALSE otherwise
 * @return bool TRUE on success + output stored via $output, FALSE otherwise
 */
function crew_save(&$output, $area_id, $node_id, $module, $viewonly, &$edit_again)
{
    global $USER;
    $retval = TRUE;
    // assume success
    $module_id = intval($module['module_id']);
    $node_id = intval($node_id);
    // 1 -- bail out if cancelled or viewonly
    if (isset($_POST['button_cancel']) || $viewonly) {
        $edit_again = FALSE;
        return FALSE;
    }
    // 2 -- redo if invalid data was submitted
    $dialogdef = crew_get_dialogdef($output, $viewonly, $module_id, $area_id, $node_id, $USER->user_id);
    if (!dialog_validate($dialogdef)) {
        // there were errors, show them to the user and ask caller to do it again
        foreach ($dialogdef as $k => $item) {
            if (isset($item['errors']) && $item['errors'] > 0) {
                $output->add_message($item['error_messages']);
            }
        }
        $edit_again = TRUE;
        return FALSE;
    }
    // 3 -- actually save the new (plain) settings (always)
    $now = strftime('%Y-%m-%d %T');
    $table = 'workshops';
    $fields = array('header' => $dialogdef['header']['value'], 'introduction' => $dialogdef['introduction']['value'], 'visibility' => $dialogdef['visibility']['value'], 'mtime' => $now, 'muser_id' => $USER->user_id);
    $where = array('node_id' => intval($node_id));
    if (db_update($table, $fields, $where) === FALSE) {
        logger(sprintf('%s(): error saving config value: %s', __FUNCTION__, db_errormessage()));
        $edit_again = TRUE;
        $retval = FALSE;
    }
    // 4 -- save or delete the changed ACLs and maybe add new ones too
    $table = 'acls_modules_nodes';
    $where = array('acl_id' => 0, 'node_id' => $node_id, 'module_id' => $module_id);
    $fields = $where;
    foreach ($dialogdef as $k => $item) {
        if (!isset($item['acl_id'])) {
            continue;
        }
        $acl_id = intval($item['acl_id']);
        $value = intval($item['value']);
        $old_value = intval($item['old_value']);
        $dbretval = TRUE;
        // assume success
        if ($value != 0) {
            if (is_null($item['old_value'])) {
                // need to add a new record
                $fields['permissions_modules'] = $value;
                $fields['acl_id'] = $acl_id;
                $dbretval = db_insert_into($table, $fields);
            } else {
                if ($value != $old_value) {
                    // need to update existing record
                    $where['acl_id'] = $acl_id;
                    $dbretval = db_update($table, array('permissions_modules' => $value), $where);
                }
            }
        } else {
            if (!is_null($item['old_value'])) {
                // delete existing record because the value is now 0
                $where['acl_id'] = $acl_id;
                $dbretval = db_delete($table, $where);
            }
        }
        if ($dbretval === FALSE) {
            $messages[] = __FUNCTION__ . '(): ' . db_errormessage();
            $edit_again = TRUE;
            $retval = FALSE;
        }
    }
    return $retval;
}
/** save the modified content data of this module linked to node $node_id
 *
 * this validates and saves the data that was submitted by the user.
 *
 * See also {@link mailpage_show_edit()} for the complications of having a single
 * routine to deal with two different dialogs.
 * If validation of dialog 1 fails, or storing the data doesn't work,
 * the flag $edit_again is set to TRUE and the return value is FALSE.
 * Validation and storage of data from dialog 2 _always_ returns $edit_again
 * TRUE because we want to return in dialog #1 after finishing dialog #2.
 *
 * If the user has cancelled the operation, the flag $edit_again is set to FALSE
 * and the return value is also FALSE.
 *
 * If the modified data is stored successfully, the return value is TRUE (and
 * the value of $edit_again is a don't care). Note that this also only applies
 * to the main dialoag (dialog #1).
 *
 * Here is a summary of return values.
 *
 *  - retval = TRUE ==> data saved successfully
 *  - retval = FALSE && edit_again = TRUE ==> re-edit the data, show the edit dialog again
 *  - retval = FALSE && edit_again = FALSE ==> cancelled, do nothing
 *
 * @param object &$output collects the html output (if any)
 * @param int $area_id the area in which $node_id resides
 * @param int $node_id the node to which the content is connected
 * @param array $module the module record straight from the database
 * @param bool $viewonly if TRUE, editing and hence saving is not allowed
 * @param bool &$edit_again set to TRUE if we need to edit the content again, FALSE otherwise
 * @return bool TRUE on success + output stored via $output, FALSE otherwise
 */
function mailpage_save(&$output, $area_id, $node_id, $module, $viewonly, &$edit_again)
{
    global $USER;
    $node_id = intval($node_id);
    $addresses = mailpage_get_addresses($node_id);
    $sort_order = 10 * (1 + sizeof($addresses));
    // $addresses are always renumbered so this is the first largest sord_order
    $address_id = get_parameter_int('address', NULL);
    if (is_null($address_id)) {
        // main config needs to be saved
        $edit_again = FALSE;
        // assume we do NOT need to edit again
        // 1 -- bail out if cancelled or viewonly
        if (isset($_POST['button_cancel']) || $viewonly) {
            return FALSE;
        }
        // 2 -- redo if invalid data was submitted
        $dialogdef = mailpage_get_dialogdef_config($output, $viewonly, $node_id);
        if (!mailpage_dialog_validate($dialogdef, $node_id, $addresses)) {
            // there were errors, show them to the user and do it again
            foreach ($dialogdef as $k => $item) {
                if (isset($item['errors']) && $item['errors'] > 0) {
                    $output->add_message($item['error_messages']);
                }
            }
            $edit_again = TRUE;
            return FALSE;
        }
        // 3 -- actually save the settings
        $retval = TRUE;
        // assume success
        $now = strftime('%Y-%m-%d %T');
        $table = 'mailpages';
        $fields = array('header' => trim($dialogdef['header']['value']), 'introduction' => trim($dialogdef['introduction']['value']), 'message' => trim($dialogdef['message']['value']), 'mtime' => $now, 'muser_id' => $USER->user_id);
        $where = array('node_id' => $node_id);
        if (db_update($table, $fields, $where) === FALSE) {
            logger(sprintf('%s(): error saving config values: %s', __FUNCTION__, db_errormessage()));
            $edit_again = TRUE;
            $retval = FALSE;
            $output->add_message(t('error_saving_data', 'm_mailpage'));
        }
        return $retval;
    }
    //
    // At this point we need to either save a new record, update an existing record,
    // delete an existing record or simply cancel and return to the main config dialog.
    // The logic depends on the submit button that was used and the value of $address_id.
    //
    $dialogdef = mailpage_get_dialogdef_address($output, $viewonly, $node_id, $address_id, $sort_order);
    if (!dialog_validate($dialogdef, $node_id, $addresses)) {
        // there were errors, show them to the user and do it again
        foreach ($dialogdef as $k => $item) {
            if (isset($item['errors']) && $item['errors'] > 0) {
                $output->add_message($item['error_messages']);
            }
        }
        $edit_again = TRUE;
        return FALSE;
    }
    $edit_again = TRUE;
    // we abuse this flag to return to the main config dialog instead of page mgr
    if (isset($_POST['button_cancel']) || $viewonly) {
        return FALSE;
    }
    $table = 'mailpages_addresses';
    $fields = array('node_id' => $node_id, 'sort_order' => intval($dialogdef['sort_order']['value']), 'name' => trim($dialogdef['name']['value']), 'email' => trim($dialogdef['email']['value']), 'description' => trim($dialogdef['description']['value']), 'thankyou' => trim($dialogdef['thankyou']['value']));
    if ($address_id <= 0) {
        // new record needs to be saved.
        if (db_insert_into($table, $fields) === FALSE) {
            logger(sprintf('%s(): error adding address: %s', __FUNCTION__, db_errormessage()));
            $output->add_message(t('error_saving_data', 'm_mailpage'));
        }
    } elseif (isset($addresses[$address_id])) {
        // OK, that is an existing record
        $where = array('mailpage_address_id' => $address_id);
        if (isset($_POST['button_save'])) {
            // Go save the record
            if (db_update($table, $fields, $where) === FALSE) {
                logger(sprintf('%s(): error updating address: %s', __FUNCTION__, db_errormessage()));
                $output->add_message(t('error_saving_data', 'm_mailpage'));
            }
        } elseif (isset($_POST['button_delete'])) {
            // Go delete this record
            if (db_delete($table, $where) === FALSE) {
                logger(sprintf('%s(): error deleting address: %s', __FUNCTION__, db_errormessage()));
                $output->add_message(t('error_deleting_data', 'm_mailpage'));
            }
        }
    }
    return FALSE;
    // Dirty trick to return to the main config dialog
}
/** perform actual update to version 2011093000
 *
 * this is yet another substantial change in the database: after we (finally)
 * standardised on UTF-8 the last time (see {@link update_core_2011051100()}
 * a number of problems occurred with new installations.
 *
 * This specifically occurs with MySQL (currently the only supported database).
 * In all their wisdom Oracle decided to change the default database engine from
 * MyISAM to InnoDB in MySQL version 5.5.5. Bad move to do that somewhere in a
 * sub-sub-release. Anyway. New installations with the default InnoDB engine
 * AND with the 4-byte utf8mb4 character set  (available since sub-sub-release 5.5.3) 
 * now generate serious trouble, because
 *
 *  - there is a hard-coded limit of 767 bytes for a key (index) in InnoDB, and
 *  - every utf8mb4 character counts as four bytes never mind the actual content.
 *
 * Note: the limit of 767 bytes stems from a utf8 (or utf8mb3 as it is now called)
 * string of max. 255 characters and 1 16-bit string length. 255 * 3 + 2 = 767 bytes.
 * I wonder why UTF-8 wasn't implemented correctly (ie. with 1 to 4 bytes) to begin with and
 * the key limit increased to 4 * 255 + 2 = 1022 bytes. The limited UTF-8 support
 * (only the BMP) now poses substantial problems. Yet another reason to start
 * looking for an alternative database solution. BTW: the key limit in MyISAM
 * is 1000 bytes.
 *
 * These two conditions (InnoDB and utf8mb4) limit the length of a key (index) to
 * 767 bytes / 4 bytes-per-char = 191 utf8mb4 characters. As it happens, some
 * tables in WebsiteAtSchool used keyfields like varchar(240) and even varchar(255).
 * These key sizes fail in InnoDB/utf8mb4 and the latter even fails with
 * MyISAM/utf8mb4 because 255 * 4 + 2 = 1022 bytes > 1000 bytes. What a mess...
 *
 * So there you have it: all keys MUST be shortened to 191 characters max. in order
 * to prevent stupid error messages about key too long. The alternative (forcing
 * another character set such as 'ascii' or 'latin1' for some fields) doesn't cut
 * it IMHO.
 *
 * *sigh*
 *
 * We still have a choice of exactly one database driver: MySQL.
 * Therefore the upgrade we do here can be more or less
 * MySQL-specific (so much for database-independency), as it has to be,
 * because the syntax of ALTER TABLE is -- unsuprisingly -- MySQL-specific.
 *
 * The good news is that we are still in beta, so a major change in the data
 * definition is less painful than with hundreds of production servers...
 *
 * Another issue is the use of foreign keys. We used to have a FK in the
 * nodes tabledef along the lines of this construct:
 * FOREIGN KEY parentnode (parent_id) REFERENCES nodes (node_id);
 * Upto now this could not possibly have worked with InnoDB because
 * adding a node would at the top level of an area would not satisfy this
 * constraint. Since MyISAM silently ignores any foreign key definition
 * it 'simply works' in that case. So, because this FK must be removed from
 * earlier installations we need to DROP the FOREIGN KEY. However, since
 * the whole program never installed using InnoDB, there is no need to drop
 * this foreign key that wasn't even recorded (in a MyISAM database) in the
 * first place. The same applies to a number of other FK's too: these are
 * now removed from the various tabledefs but do no need to be DROPped in
 * this update routine.
 *
 * What needs to be done here?
 *
 * For existing tables some fields must be shortened from varchar(255) or
 * varchar(240) to something like varchar(191) or even less. This MUST be
 * done for key (index) fields. However, while we are at it some more fields
 * SHOULD (or COULD) be shortened too. Here is what we do.
 *
 * <code>
 * for all affected table.fields do
 *    if a record exists with current data length > proposed new length then
 *        tell the user about it
 *    endif
 * next
 * if there were data length errors then
 *     tell the user about manually fixing it
 *     bail out with result FALSE (= not upgraded)
 * endif
 * for all affected table.fields do
 *     change field definition to new length
 *     if errors
 *         tell the user about it (but carry on)
 *     endif
 * next
 * return results (TRUE on success, FALSE on 1 or more errors)
 * </code>
 *
 * Below is a discussion of all affected fields and the rationale for
 * picking the new lengths less than 191 characters.
 *
 * <code>
 * config.name: varchar(240) => varchar(80)
 * modules_properties.name: varchar(240) => varchar(80)
 * themes_properties.name: varchar(240) => varchar(80)
 * themes_areas_properties.name: varchar(240) => varchar(80)
 * users_properties.name: varchar(240) => varchar(80)
 * users_properties.section: varchar(240) => varchar(80)
 * </code>
 * 
 * Currently the longest parameter name in use is 27 characters, so I
 * have to admit that the arbitrary size of 240 is a little bit too much.
 * I'll reduce these fields to a size of 80, which seems a little more
 * realistic. As an additional bonus, this allows for a compound key
 * using 'section' and 'name' in users_properties while staying within
 * the limit of 767 bytes or 191 characters.
 *
 * <code>
 * areas.path: varchar(240) => varchar(60)
 * groups.path: varchar(240) => varchar(60)
 * users.path: varchar(240) => varchar(60)
 * </code>
 *
 * and
 *
 * <code>
 * groups.groupname: varchar(255) => varchar(60)
 * users.username: varchar(255) => varchar(60)
 * </code>
 * 
 * The length of username or groupname was arbitrary set to 255.
 * Different systems have different limits, e.g. 8, 14, 15, 16,
 * 20, 32, 64 or 128. Since W@S is a stand-alone system we are more
 * or less free to choose whatever we want (as long as it is less
 * than 191 of course).
 *
 * Since a username or groupname is only used to distinguish one user
 * from another but at the same time giving at least some readability,
 * a length of 255 is way too long. An arbitrary but hopefully more
 * realistic choice is 60 characters. 
 *
 * The path for a user or group is derived from the corresponding
 * name so it makes sense to make both fields the same length.
 *
 * <code>
 * log_messages.remote_addr: varchar(255) => varchar(150)
 * login_failures.remote_addr: varchar(255) => varchar(150)
 * </code>
 * 
 * A remote address of type IPv4 generally looks like this: 'ddd.ddd.ddd.ddd' => length 15
 * It is not so easy to determine the length of an IPv6 address, because many valid variants exist.
 * 'xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx' => length 39
 * '0000:0000:0000:0000:0000:0000:ddd.ddd.ddd.ddd' => length 45
 * '[0000:0000:0000:0000:0000:0000:ddd.ddd.ddd.ddd'] => length 47 (RFC3989)
 * 
 * Adding to the complexity and confusion are link-local addresses with
 * zone indices: a percent-sign followed by an interface number
 * (e.g. '%1') or interface name (e.g. '%eth0') appended to the raw
 * address. This adds 2 or 5 or even more characters to the address.
 * And then we of course have the reverse DNS-variant like
 * 'x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.ip6.arpa.' => length 73
 * or the special Microsoft trick to shoehorn a literal address in a UNC path:
 * 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx.ipv6-literal.net' => length 56 or
 * 'xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxxs1.ipv6-literal.net' => length 58+ (with zone index)
 * 
 * Of course there several 'simplifications' such as omitting leading
 * zeros in the hexquads and replacing the longest sequece of 0-hexquads
 * with '::' that add to the confusion. RFC5952 adds the definition of a 'canonical
 * representation' of IPv6 addresses to the party. Mmmm, see http://xkcd.com/927
 * 
 * My conclusion is: this whole IPv6-idea suffers from the Second System Syndrome
 * (see F. Brooks' Mythical Man Month) and unfortunately we have to deal with it.
 * 
 * *sigh*
 * 
 * I will reduce the length of these fields from 255 to 150 for no other
 * reason than that it is 10 times the length of a dotted-decimal IPv4
 * address and sufficient to accomodate a reverse DNS address twice (2 x
 * 73 = 146).
 * 
 * <code>
 * sessions.session_key: varchar(255) => varchar(172)
 * </code>
 * 
 * This field stores a session key, currently constructed using md5()
 * which yields a string with 32 (lowercase) hexadecimal characters.  In
 * the future a different digest could be used to provice a session_key,
 * e.g. SHA-1 (40 hexdigits) or SHA-512 (128 hexdigits). Another option
 * would be to use a UUID: 128 bits represented in 32 hexdigits in the
 * form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (string of 36 bytes).
 * Alternatively, the SHA-512 could be encoded in base64 yielding a
 * string of 512 / 6 = 86 bytes. In this context, a field of size 255
 * seems a little over the top, not to mention problematic with 4-byte
 * UTF-8 characters combined with the infamous MySQL / InnoDB-limit of
 * 767 bytes for keyfields. I guess I will settle for a field size of
 * 172 characters which is not too much for InnoDB keys + utf8mb4 and
 * exactly enough to store a 1024 bit number in base64.
 *
 * @param object &$output collects the html output
 * @return bool TRUE on success, FALSE otherwise
 */
function update_core_2011093000(&$output)
{
    global $CFG, $DB;
    // 0 -- get outta here when already upgraded
    $version = 2011093000;
    if ($CFG->version >= $version) {
        return TRUE;
    }
    // List of column definitions keyed by 'tablename:fieldname' copied from (new) tabledefs
    $alterdefs = array('areas:path' => array('name' => 'path', 'type' => 'varchar', 'length' => 60, 'notnull' => TRUE, 'comment' => 'the place to store user uploaded files etc., relative to CFG->datadir/areas'), 'config:name' => array('name' => 'name', 'type' => 'varchar', 'length' => 80, 'notnull' => TRUE, 'comment' => 'the name of the global configuration parameter'), 'groups:groupname' => array('name' => 'groupname', 'type' => 'varchar', 'length' => 60, 'notnull' => TRUE, 'comment' => 'the short groupname, must be unique too'), 'groups:path' => array('name' => 'path', 'type' => 'varchar', 'length' => 60, 'notnull' => TRUE, 'comment' => 'the place (subdirectory) to store files for this group, relative to CFG->datadir/groups'), 'log_messages:remote_addr' => array('name' => 'remote_addr', 'type' => 'varchar', 'length' => 150, 'notnull' => TRUE, 'comment' => 'IP-address of the visitor'), 'login_failures:remote_addr' => array('name' => 'remote_addr', 'type' => 'varchar', 'length' => 150, 'notnull' => TRUE, 'comment' => 'IP-address of the visitor that failed the login attempt/is blocked'), 'modules_properties:name' => array('name' => 'name', 'type' => 'varchar', 'length' => 80, 'notnull' => TRUE, 'comment' => 'the name of the configuration parameter'), 'sessions:session_key' => array('name' => 'session_key', 'type' => 'varchar', 'length' => 172, 'default' => '', 'comment' => 'contains the unique identifier (\'token\') which is stored in the user\'s cookie'), 'themes_areas_properties:name' => array('name' => 'name', 'type' => 'varchar', 'length' => 80, 'notnull' => TRUE, 'comment' => 'the name of the configuration parameter'), 'themes_properties:name' => array('name' => 'name', 'type' => 'varchar', 'length' => 80, 'notnull' => TRUE, 'comment' => 'the name of the configuration parameter'), 'users:path' => array('name' => 'path', 'type' => 'varchar', 'length' => 60, 'notnull' => TRUE, 'comment' => 'the place (subdirectory) to store files for this user, relative to CFG->datadir/users'), 'users:username' => array('name' => 'username', 'type' => 'varchar', 'length' => 60, 'notnull' => TRUE, 'comment' => 'the account name, must be unique too'), 'users_properties:name' => array('name' => 'name', 'type' => 'varchar', 'length' => 80, 'notnull' => TRUE, 'comment' => 'the name of the configuration parameter'), 'users_properties:section' => array('name' => 'section', 'type' => 'varchar', 'length' => 80, 'notnull' => TRUE, 'comment' => 'keeps related properties grouped together, e.g. in a separate tab'), 'nodes:module_id' => array('name' => 'module_id', 'type' => 'int', 'notnull' => FALSE, 'default' => NULL, 'comment' => 'this connects to the module generating actual node content; NULL for sections'));
    //
    // 1 -- check existing data for strings that are too long (only the varchar fields)
    //
    $errors = 0;
    foreach ($alterdefs as $table_field => $fielddef) {
        if ($fielddef['type'] != 'varchar') {
            continue;
        }
        list($table, $field) = explode(':', $table_field);
        $length = $fielddef['length'];
        $where = sprintf('CHAR_LENGTH(%s) > %d', $field, $length);
        if (($records = db_select_all_records($table, $field, $where)) === FALSE) {
            $msg = sprintf('%s(): cannot retrieve data from table \'%s\' field \'%s\': %s', __FUNCTION__, $table, $field, db_errormessage());
            logger($msg);
            $output->add_message(htmlspecialchars($msg));
            $output->add_message(t('update_core_error', 'admin', array('{VERSION}' => strval($version))));
            return FALSE;
        }
        if (sizeof($records) <= 0) {
            continue;
        }
        $params = array('{TABLE}' => $table, '{FIELD}' => $field, '{LENGTH}' => $length, '{CONTENT}' => '');
        foreach ($records as $record) {
            ++$errors;
            logger(sprintf('%s(): content of table \'%s\' field \'%s\' longer than %d: \'%s\'', __FUNCTION__, $table, $field, $length, $record[$field]));
            $params['{CONTENT}'] = $record[$field];
            $output->add_message(t('update_field_value_too_long', 'admin', $params));
        }
    }
    if ($errors > 0) {
        logger(sprintf('%s(): number of errors encoutered: %d; bailing out for manual correction', __FUNCTION__, $errors));
        $output->add_message(t('update_please_correct_field_value_manually', 'admin', array('{ERRORS}' => $errors)));
        $msg = t('update_core_error', 'admin', array('{VERSION}' => strval($version)));
        $output->add_message($msg);
        $output->add_popup_bottom($msg);
        // attract some more attention
        return FALSE;
    }
    //
    // 2 -- actually change the table definitions (both varchar and int)
    //
    $overtime = max(intval(ini_get('max_execution_time')), 30);
    // additional processing time in seconds
    foreach ($alterdefs as $table_field => $fielddef) {
        list($table, $field) = explode(':', $table_field);
        $sql = sprintf('ALTER TABLE `%s%s` CHANGE %s %s', $DB->prefix, $table, $field, $DB->column_definition($fielddef));
        if ($DB->exec($sql) === FALSE) {
            $msg = sprintf('%s(): cannot alter \'%s\' with \'%s\': %d/%s; bailing out', __FUNCTION__, $table, $sql, $DB->errno, $DB->error);
            logger($msg);
            $output->add_message(htmlspecialchars($msg));
            $output->add_message(t('update_core_error', 'admin', array('{VERSION}' => strval($version))));
            return FALSE;
        } else {
            if ($fielddef['type'] == 'varchar') {
                $msg = sprintf('changed type to varchar(%d)', $fielddef['length']);
            } else {
                $msg = 'changed \'notnull\' and \'default\' properties';
                // there is only nodes.modules_id here...
            }
            logger(sprintf('%s(): alter table \'%s\' field \'%s\': %s', __FUNCTION__, $table, $field, $msg), WLOG_DEBUG);
        }
        @set_time_limit($overtime);
        // try to get additional processing time after every processed table
    }
    //
    // 3 -- adjust existing data for nodes.module_id
    //
    $table = 'nodes';
    $fields = array('module_id' => NULL);
    $where = array('module_id' => 0);
    if (($retval = db_update($table, $fields, $where)) === FALSE) {
        $msg = sprintf('%s(): cannot update \'%s\': %d/%s; bailing out', __FUNCTION__, $table, $sql, $DB->errno, $DB->error);
        logger($msg);
        $output->add_message(htmlspecialchars($msg));
        $output->add_message(t('update_core_error', 'admin', array('{VERSION}' => strval($version))));
        return FALSE;
    } else {
        logger(sprintf('%s(): update field \'nodes.module_id\': %d rows affected', __FUNCTION__, $retval), WLOG_DEBUG);
    }
    //
    // 4 -- add new config option and attempt to fix existing data suffering from sort_order bug in page manager
    //
    // 4A -- add a new sort option to the CFG
    $retval = TRUE;
    // assume success
    if (!isset($CFG->pagemanager_at_end)) {
        $table = 'config';
        $fields = array('name' => 'pagemanager_at_end', 'type' => 'b', 'value' => '0', 'sort_order' => 240, 'extra' => '', 'description' => 'sort order position within section for new nodes: TRUE is at the end  - USER-defined');
        if (db_insert_into($table, $fields) === FALSE) {
            $msg = sprintf("%s(): cannot add config option 'pagemanager_at_end': %s", __FUNCTION__, db_errormessage());
            logger($msg);
            $output->add_message(htmlspecialchars($msg));
            $output->add_message(t('update_core_error', 'admin', array('{VERSION}' => strval($version))));
            return FALSE;
        } else {
            logger(sprintf("%s(): success adding option 'pagemanager_at_end' to configuration table", __FUNCTION__));
        }
    } else {
        logger(sprintf("%s(): option 'pagemanager_at_end' already set in configuration table", __FUNCTION__));
    }
    // 4B -- attempt to update sort_orders in nodes that are obviously wrong
    $table = 'nodes';
    $fields = array('area_id', 'CASE WHEN node_id=parent_id THEN 0 ELSE parent_id END AS section', 'node_id', 'sort_order');
    $where = '';
    $order = array('area_id', 'CASE WHEN node_id = parent_id THEN 0 ELSE parent_id END', 'sort_order');
    $keyfield = 'node_id';
    if (($records = db_select_all_records($table, $fields, $where, $order, $keyfield)) === FALSE) {
        $msg = sprintf('%s(): cannot retrieve sort orders in nodes; skipping: %s', __FUNCTION__, db_errormessage());
        logger($msg);
        $output->add_message(htmlspecialchars($msg));
        $output->add_message(t('update_core_error', 'admin', array('{VERSION}' => strval($version))));
        return FALSE;
    }
    $count = 0;
    $area_id = 0;
    $section = 0;
    foreach ($records as $node_id => $record) {
        if ($area_id != $record['area_id'] || $section != $record['section']) {
            $area_id = $record['area_id'];
            $section = $record['section'];
            $sort_order = $record['sort_order'] + 10;
        } else {
            if ($sort_order != $record['sort_order']) {
                $fields = array('sort_order' => intval($sort_order));
                $where = array('node_id' => intval($node_id));
                if (db_update($table, $fields, $where) === FALSE) {
                    $msg = sprintf("%s(): sort order error in node '%d': %s", __FUNCTION__, $node_id, db_errormessage());
                    logger($msg);
                    $output->add_message(htmlspecialchars($msg));
                    $output->add_message(t('update_core_error', 'admin', array('{VERSION}' => strval($version))));
                    return FALSE;
                }
                logger(sprintf('%s(): success updating sort_order from %d => %d in area %d, section %d, node %d', __FUNCTION__, $record['sort_order'], $sort_order, $area_id, $section, $node_id));
                ++$count;
            }
            $sort_order += 10;
        }
    }
    logger(sprintf('%s(): success updating sort orders in nodes table; count = %d', __FUNCTION__, $count));
    //
    // 5 -- all done: bump version in database
    //
    return update_core_version($output, $version);
}
/** add 1 point to score for a particular IP-address and failed procedure, return the new score
 *
 * This records a login failure in a table and returns the the number
 * of failures for the specified procedure in the past T1 minutes.
 *
 * @param string $remote_addr the remote IP-address that is the origin of the failure
 * @param int $procedure indicates in which procedure the user failed
 * @param string $username extra information, could be useful for troubleshooting afterwards
 * @return int the current score
 */
function login_failure_increment($remote_addr, $procedure, $username = '')
{
    global $CFG;
    // this array used to validate $procedure _and_ to make a human readable description with logger()
    static $procedure_names = array(LOGIN_PROCEDURE_NORMAL => 'normal login', LOGIN_PROCEDURE_CHANGE_PASSWORD => 'change password', LOGIN_PROCEDURE_SEND_LAISSEZ_PASSER => 'send laissez passer', LOGIN_PROCEDURE_SEND_BYPASS => 'send bypass');
    $retval = 0;
    $procedure = intval($procedure);
    if (isset($procedure_names[$procedure])) {
        $now = strftime('%Y-%m-%d %T');
        $retval = db_insert_into('login_failures', array('remote_addr' => $remote_addr, 'datim' => $now, 'failed_procedure' => $procedure, 'points' => 1, 'username' => $username));
        if ($retval !== FALSE) {
            $minutes = intval($CFG->login_failures_interval);
            $interval_begin = strftime('%Y-%m-%d %T', time() - $minutes * 60);
            $where = 'remote_addr = ' . db_escape_and_quote($remote_addr) . ' AND failed_procedure = ' . $procedure . ' AND ' . db_escape_and_quote($interval_begin) . ' < datim';
            $record = db_select_single_record('login_failures', 'SUM(points) AS score', $where);
            if ($record !== FALSE) {
                $retval = intval($record['score']);
            } else {
                logger('could not calculate failure score', WLOG_DEBUG);
            }
        } else {
            logger('could not increment failure count', WLOG_DEBUG);
        }
        logger('login: failed; procedure=' . $procedure_names[$procedure] . ', count=' . $retval . ', username=\'' . $username . '\'');
    } else {
        logger('internal error: unknown procedure', WLOG_DEBUG);
    }
    return $retval;
}
/** upgrade the theme
 *
 * this routine performs an upgrade to the installed theme.
 *
 * Note that the initial version of this 'rosalina' theme does
 * not need any upgrade at all because there never was an earlier
 * version (well, duh).
 *
 * However, if there was to be a newer version of this theme, this
 * routine is THE place to bring the database up to date compared with
 * the existing version. For example, if an additional property 'foobar'
 * was to be added to the theme configuration, it could be added
 * to the themes_properties table with a suitable (default) value,
 * Any existing areas with this theme could have their configuration
 * updated with this additional foobar property, e.g.
 * INSERT INTO themes_properties: foobar
 * for all areas in themes_areas_properties with theme_id = $theme_id do
 *     INSERT INTO themes_areas_properties: foobar
 * etcetera,
 *
 * The current version of the theme could be determined by consulting
 * the databse (db_select_single_record(themes,'*','theme_id = $theme_id')
 * etcetera.
 *
 * Note that it is the responbabilty of the caller to correctly store
 * the data from the manifest in the themes table. You should not do
 * this here, in this routine.
 *
 * Currently this is a quick and dirty routine to
 *  - update changed sort_order in the existing settings, OR
 *  - add fields that were not available in the current settings
 *
 * In the future we could make it more sophisticated by
 * updating the themes_areas_properties too. Oh well. KISS
 *
 * @param array &$messages collects the (error) messages
 * @param int $theme_id the key for this theme in the themes table
 * @return bool TRUE on success + output via $messages, FALSE otherwise
 * @uses rosalina_get_properties()
 * @todo maybe make this a little less quick and dirty?
 */
function rosalina_upgrade(&$messages, $theme_id)
{
    $retval = TRUE;
    $theme_id = intval($theme_id);
    $table = 'themes_properties';
    // 1 -- fetch current settings
    $where = array('theme_id' => $theme_id);
    $fields = array('name', 'sort_order');
    $order = 'sort_order';
    $keyfield = 'name';
    if (($settings = db_select_all_records($table, $fields, $where, $order, $keyfield)) === FALSE) {
        $messages[] = sprintf('%s(): cannot get settings from %s: %s', __FUNCTION__, $table, db_errormessage());
        $retval = FALSE;
        $settings = array();
        // step 3 below expects an array
    }
    // 2 -- get new properties
    $properties = rosalina_get_properties();
    // 3 -- selectively update settings or add as new
    $sort_order = 0;
    foreach ($properties as $name => $property) {
        $sort_order += 10;
        if (isset($settings[$name])) {
            // existing property, maybe update sort order
            if ($sort_order != $settings[$name]['sort_order']) {
                $fields = array('sort_order' => $sort_order);
                $where = array('theme_id' => $theme_id, 'name' => $name);
                if (db_update($table, $fields, $where) === FALSE) {
                    $messages[] = __FUNCTION__ . '(): ' . db_errormessage();
                    $retval = FALSE;
                }
            }
            // else do_not_touch_existing_setting()
        } else {
            $property['theme_id'] = $theme_id;
            $property['name'] = $name;
            $property['sort_order'] = $sort_order;
            if (db_insert_into($table, $property) === FALSE) {
                $messages[] = __FUNCTION__ . '(): ' . db_errormessage();
                $retval = FALSE;
            }
        }
    }
    return $retval;
}
 /** save the newly added language to the database
  *
  * This saves the essential information of a new language to the database,
  * using sensible defaults for the other fields. Also, a data directory
  * is created in $CFG->datadir
  *
  * If something goes wrong, the user can redo the dialog, otherwise we
  * return to the languages overview, with the newly added language in the
  * list, too.
  *
  * Apart from the standard checks the following checks are done:
  *  - the language key should be an acceptable directory name
  *  - the language key should be lowercase
  *  - the language key should not exist already (in $this->languages)
  *  - the directory should not yet exist
  *  - the directory must be created here and now
  *
  * @return void results are returned as output in $this->output
  * @uses $WAS_SCRIPT_NAME
  * @uses $CFG
  * @uses $USER
  * @uses $LANGUAGE
  */
 function language_savenew()
 {
     global $WAS_SCRIPT_NAME, $USER, $CFG, $LANGUAGE;
     // 1 -- bail out if user pressed cancel button
     if (isset($_POST['button_cancel'])) {
         $this->output->add_message(t('cancelled', 'admin'));
         $this->languages_overview();
         return;
     }
     // 2 -- validate the data; check for generic errors (string too short, number too small, etc)
     $dialogdef = $this->get_dialogdef_language();
     $invalid = dialog_validate($dialogdef) ? FALSE : TRUE;
     // 3 -- additional validation & massaging of the language key
     $fname = isset($dialogdef['language_key']['label']) ? $dialogdef['language_key']['label'] : 'language_key';
     $params = array('{FIELD}' => str_replace('~', '', $fname));
     // 3A -- additional check: the language key doubles as a directory name AND should be lowercase
     $path = $dialogdef['language_key']['value'];
     $languagedata_directory = strtolower(sanitise_filename($path));
     if ($path != $languagedata_directory) {
         // User probably entered a few 'illegal' characters. This is no good
         $dialogdef['language_key']['value'] = $languagedata_directory;
         // 'Help' user with a better proposition?
         ++$dialogdef['language_key']['errors'];
         $params['{VALUE}'] = htmlspecialchars($path);
         $dialogdef['language_key']['error_messages'][] = t('validate_bad_filename', '', $params);
         $invalid = TRUE;
     }
     // 3B -- additional check: unique language key name entered
     if (isset($this->languages[$languagedata_directory])) {
         // Oops, already exists
         ++$dialogdef['language_key']['errors'];
         $params['{VALUE}'] = $languagedata_directory;
         $dialogdef['language_key']['error_messages'][] = t('validate_not_unique', '', $params);
         $invalid = TRUE;
     }
     // 3C -- additional check: can we create said directory?
     $languagedata_full_path = $CFG->datadir . '/languages/' . $languagedata_directory;
     $languagedata_directory_created = @mkdir($languagedata_full_path, 0700);
     if ($languagedata_directory_created) {
         @touch($languagedata_full_path . '/index.html');
         // "protect" the newly created directory from prying eyes
     } else {
         // Mmmm, failed; probably already exists then. Oh well. Go flag error.
         ++$dialogdef['language_key']['errors'];
         $params['{VALUE}'] = '/languages/' . $languagedata_directory;
         $dialogdef['language_key']['error_messages'][] = t('validate_already_exists', '', $params);
         $invalid = TRUE;
     }
     // 3E -- if there were any errors go redo dialog while keeping data already entered
     if ($invalid) {
         if ($languagedata_directory_created) {
             // Only get rid of the directory _we_ created
             @unlink($languagedata_full_path . '/index.html');
             @rmdir($languagedata_full_path);
         }
         // there were errors, show them to the user and do it again
         foreach ($dialogdef as $k => $item) {
             if (isset($item['errors']) && $item['errors'] > 0) {
                 $this->output->add_message($item['error_messages']);
             }
         }
         $this->output->add_content('<h2>' . t('translatetool_add_language_header', 'admin') . '</h2>');
         $this->output->add_content(t('translatetool_add_language_explanation', 'admin'));
         $href = href($WAS_SCRIPT_NAME, $this->a_param(TRANSLATETOOL_CHORE_LANGUAGE_SAVE_NEW));
         $this->output->add_content(dialog_quickform($href, $dialogdef));
         return;
     }
     // 4 -- go save the new language
     $language_key = $dialogdef['language_key']['value'];
     $language_name = $dialogdef['language_name']['value'];
     $parent_key = $dialogdef['parent_language_key']['value'] == '--' ? NULL : $dialogdef['parent_language_key']['value'];
     $is_active = $dialogdef['language_is_active']['value'] == '1' ? TRUE : FALSE;
     $fields = array('language_key' => $language_key, 'parent_language_key' => $parent_key, 'language_name' => $language_name, 'version' => 0, 'manifest' => '', 'is_core' => FALSE, 'is_active' => $is_active, 'dialect_in_database' => FALSE, 'dialect_in_file' => FALSE);
     if (db_insert_into('languages', $fields) === FALSE) {
         if ($languagedata_directory_created) {
             // Only get rid of the file/directory _we_ created
             @unlink($languagedata_full_path . '/index.html');
             @rmdir($languagedata_full_path);
         }
         logger(sprintf('%s.%s(): saving new language \'%s\' failed: %s', __CLASS__, __FUNCTION__, htmlspecialchars($language_key), db_errormessage()));
         $this->output->add_message(t('translatetool_language_savenew_failure', 'admin'));
     } else {
         $params = array('{LANGUAGE_KEY}' => $language_key, '{LANGUAGE_NAME}' => $language_name);
         $this->output->add_message(t('translatetool_language_savenew_success', 'admin', $params));
         logger(sprintf("%s.%s(): success saving new language '%s' (%s) with data directory /languages/%s", __CLASS__, __FUNCTION__, $language_name, $language_key, $languagedata_directory));
         $this->languages = $LANGUAGE->retrieve_languages(TRUE);
         // TRUE means force reread from database after add
     }
     $this->languages_overview();
 }