/** 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;
}
/** construct a dialog definition for the workshop configuration
 *
 * this generates an array which defines the dialog for workshop configuration.
 * There are a few plain fields that simply go into the appropriate workshops
 * record and the save and cancel button. However, there may also be items
 * related to ACLs. These fields are used to define the user's roles and the
 * should be presented in a table. We abuse the field names for this purpose:
 * if the first 3 characters are 'acl' we put the widgets in an HTML-table, otherwise
 * it is just an ordinary widget.
 *
 * Note that in the case of a single simple user without any aquaintances (ie. a user
 * that is not member of any group) the user is not able to add herself to the list
 * of authorised users. What is the point of having a _collaborative_ workshop when
 * you are the only one to collaborate with? (However, it would be fairly easy to force/add
 * an entry for this user if the tmp table would turn out empty. Maybe later....?)
 * @param object &$output collects the html output (if any)
 * @param int $viewonly if TRUE the Save button is not displayed and values cannot be changed
 * @param int $module_id indicates the id of the crew module in the database (needed for ACL)
 * @param int $area_id indicates the area where node_id lives (needed for ACL)
 * @param int $node_id indicates which page we are loooking at (needed for ACL)
 * @param int $user_id indicates the current user (needed for ACL)
 * @return array dialog definition
 */
function crew_get_dialogdef(&$output, $viewonly, $module_id, $area_id, $node_id, $user_id)
{
    global $DB, $USER;
    static $dialogdef = NULL;
    if (!is_null($dialogdef)) {
        // recycle
        return $dialogdef;
    }
    $visibilities = array('2' => array('option' => t('visibility_world_label', 'm_crew'), 'title' => t('visibility_world_title', 'm_crew')), '1' => array('option' => t('visibility_all_label', 'm_crew'), 'title' => t('visibility_all_title', 'm_crew')), '0' => array('option' => t('visibility_workers_label', 'm_crew'), 'title' => t('visibility_workers_title', 'm_crew')));
    $roles = array(ACL_ROLE_NONE => array('option' => t('acl_role_none_option', 'admin'), 'title' => t('acl_role_none_title', 'admin')), CREW_ACL_ROLE_READONLY => array('option' => t('crew_acl_role_readonly_option', 'm_crew'), 'title' => t('crew_acl_role_readonly_title', 'm_crew')), CREW_ACL_ROLE_READWRITE => array('option' => t('crew_acl_role_readwrite_option', 'm_crew'), 'title' => t('crew_acl_role_readwrite_title', 'm_crew')), ACL_ROLE_GURU => array('option' => t('acl_role_guru_option', 'admin'), 'title' => t('acl_role_guru_title', 'admin')));
    // 1 -- plain & simple fields
    // make a fresh start with data from the database
    $dialogdef = array('header' => array('type' => F_ALPHANUMERIC, 'name' => 'header', 'minlength' => 0, 'maxlength' => 240, 'columns' => 30, 'label' => t('header_label', 'm_crew'), 'title' => t('header_title', 'm_crew'), 'viewonly' => $viewonly, 'value' => '', 'old_value' => ''), 'introduction' => array('type' => F_ALPHANUMERIC, 'name' => 'introduction', 'minlength' => 0, 'maxlength' => 32768, 'columns' => 50, 'rows' => 10, 'label' => t('introduction_label', 'm_crew'), 'title' => t('introduction_title', 'm_crew'), 'viewonly' => $viewonly, 'value' => '', 'old_value' => ''), 'visibility' => array('type' => F_RADIO, 'name' => 'visibility', 'value' => 0, 'old_value' => 0, 'options' => $visibilities, 'viewonly' => $viewonly, 'title' => t('visibility_title', 'm_crew'), 'label' => t('visibility_label', 'm_crew')));
    $table = 'workshops';
    $fields = array('header', 'introduction', 'visibility');
    $where = array('node_id' => intval($node_id));
    if (($record = db_select_single_record($table, $fields, $where)) === FALSE) {
        logger(sprintf('%s(): error retrieving CREW configuration: %s', __FUNCTION__, db_errormessage()));
        $output->add_message(t('error_retrieving_data', 'admin'));
    } else {
        foreach ($record as $name => $value) {
            $dialogdef[$name]['value'] = $dialogdef[$name]['old_value'] = $value;
        }
    }
    $sql = sprintf('DROP TEMPORARY TABLE IF EXISTS %screw_tmp', $DB->prefix);
    $retval = $DB->exec($sql);
    if ($USER->has_job_permissions(JOB_PERMISSION_ACCOUNTMANAGER)) {
        // Allow $USER to set/edit any user's permission because she is already able
        // to manipulate useraccounts, _all_ useraccounts. We are sure that $USER is a
        // valid user with at least JOB_PERMISSION_STARTCENTER or else we would not be here.
        $sql = sprintf('CREATE TEMPORARY TABLE %screw_tmp ' . 'SELECT u.acl_id, u.username, u.full_name, amn.permissions_modules ' . 'FROM %susers u ' . 'LEFT JOIN %sacls_modules_nodes amn ' . 'ON amn.acl_id = u.acl_id AND amn.module_id = %d AND amn.node_id = %d ' . 'ORDER BY u.full_name', $DB->prefix, $DB->prefix, $DB->prefix, $module_id, $node_id);
    } else {
        // Only allow $USER to set permissions for all her acquaintances, ie. all users
        // that are members of the group(s) that $USER is a also member of.
        $sql = sprintf('CREATE TEMPORARY TABLE %screw_tmp ' . 'SELECT DISTINCT u.acl_id, u.username, u.full_name, amn.permissions_modules ' . 'FROM %susers u ' . 'INNER JOIN %susers_groups_capacities ugc1 USING(user_id) ' . 'INNER JOIN %susers_groups_capacities ugc2 USING(group_id) ' . 'LEFT JOIN %sacls_modules_nodes amn ' . 'ON amn.acl_id = u.acl_id AND amn.module_id = %d AND amn.node_id = %d ' . 'WHERE ugc2.user_id = %d ' . 'ORDER BY u.full_name', $DB->prefix, $DB->prefix, $DB->prefix, $DB->prefix, $DB->prefix, $module_id, $node_id, $user_id);
    }
    $retval = $DB->exec($sql);
    // at this point we have a temporary table with all 'editable' accounts
    // we first add those to the dialogdef.
    $table = 'crew_tmp';
    $fields = '*';
    $where = '';
    $order = array('full_name', 'username');
    if (($records = db_select_all_records($table, $fields, $where, $order)) === FALSE) {
        logger(sprintf('%s(): error retrieving elegible CREW-members: %s', __FUNCTION__, db_errormessage()));
        $output->add_message(t('error_retrieving_data', 'admin'));
    } else {
        foreach ($records as $record) {
            $acl_id = intval($record['acl_id']);
            $name = 'acl_rw_' . $acl_id;
            $dialogdef[$name] = array('type' => F_LISTBOX, 'name' => $name, 'value' => is_null($record['permissions_modules']) ? 0 : $record['permissions_modules'], 'old_value' => $record['permissions_modules'], 'acl_id' => $acl_id, 'options' => $roles, 'viewonly' => $viewonly, 'title' => $record['username'], 'label' => $record['full_name']);
        }
    }
    // the next step is to generate a list of any OTHER accounts that happen to have
    // permissions for this module on this node other than ACL_ROLE_NONE.
    // This list consists of a few UNIONs that effectively yields all accounts that
    // somehow have a non-0 permissions_modules, either global (acls), any node for
    // this module (acls_modules), any node within this area (acls_modules_area),
    // any node that is an ancestor of node_id (acls_modules_nodes) OR this specific
    // node for a user that is NOT an acquaintance (ie. who is not in the temp table).
    // Note that we don't check the ancestors (parents) when node happens to be at
    // the top level within the area, ie. when parent is 0. We also peek inside
    // 'acls_areas' and 'acls_nodes'. Pfew, complicated...
    // All these OTHER accounts cannot be manipulated by $USER because all accounts
    // would then be in the temp table, so there.
    // Since there may be more records for the same user (or rather acl_id), we need
    // to drill down the results. As all permissions are additive we can simply OR
    // these together per acl_id/user which yields a single combined role for that user.
    $tree = tree_build($area_id);
    $ancestors = array();
    for ($next_id = $tree[$node_id]['parent_id']; $next_id; $next_id = $tree[$next_id]['parent_id']) {
        $ancestors[] = $next_id;
    }
    unset($tree);
    $sql = empty($ancestors) ? '' : sprintf('SELECT u.acl_id, u.username, u.full_name, amn.permissions_modules  ' . 'FROM %susers u INNER JOIN %sacls_modules_nodes amn USING (acl_id) ' . 'WHERE amn.permissions_modules <> 0 AND amn.module_id = %d AND amn.node_id IN (%s)', $DB->prefix, $DB->prefix, $module_id, join(',', $ancestors)) . ' UNION ' . sprintf('SELECT u.acl_id, u.username, u.full_name, an.permissions_modules  ' . 'FROM %susers u INNER JOIN %sacls_nodes an USING (acl_id) ' . 'WHERE an.permissions_modules <> 0 AND an.node_id IN (%s)', $DB->prefix, $DB->prefix, join(',', $ancestors)) . ' UNION ';
    $sql .= sprintf('SELECT u.acl_id, u.username, u.full_name, a.permissions_modules ' . 'FROM %susers u INNER JOIN %sacls a USING (acl_id) ' . 'WHERE a.permissions_modules <> 0', $DB->prefix, $DB->prefix) . ' UNION ' . sprintf('SELECT u.acl_id, u.username, u.full_name, am.permissions_modules  ' . 'FROM %susers u INNER JOIN %sacls_modules am USING (acl_id) ' . 'WHERE am.permissions_modules <> 0 AND am.module_id = %d', $DB->prefix, $DB->prefix, $module_id) . ' UNION ' . sprintf('SELECT u.acl_id, u.username, u.full_name, aa.permissions_modules  ' . 'FROM %susers u INNER JOIN %sacls_areas aa USING (acl_id) ' . 'WHERE aa.permissions_modules <> 0 AND aa.area_id = %d', $DB->prefix, $DB->prefix, $area_id) . ' UNION ' . sprintf('SELECT u.acl_id, u.username, u.full_name, ama.permissions_modules  ' . 'FROM %susers u INNER JOIN %sacls_modules_areas ama USING (acl_id) ' . 'WHERE ama.permissions_modules <> 0 AND ama.module_id = %d AND ama.area_id = %d', $DB->prefix, $DB->prefix, $module_id, $area_id) . ' UNION ' . sprintf('SELECT u.acl_id, u.username, u.full_name, an.permissions_modules  ' . 'FROM %susers u INNER JOIN %sacls_nodes an USING (acl_id) ' . 'WHERE an.permissions_modules <> 0 AND an.node_id = %d', $DB->prefix, $DB->prefix, $node_id) . ' UNION ' . sprintf('SELECT u.acl_id, u.username, u.full_name, amn.permissions_modules  ' . 'FROM %susers u INNER JOIN %sacls_modules_nodes amn USING (acl_id) ' . 'LEFT JOIN %screw_tmp tmp USING(acl_id) ' . 'WHERE amn.permissions_modules <> 0 AND amn.module_id = %d AND amn.node_id = %d ' . 'AND tmp.acl_id IS NULL ', $DB->prefix, $DB->prefix, $DB->prefix, $module_id, $node_id) . 'ORDER BY full_name, acl_id';
    if (($result = $DB->query($sql)) === FALSE) {
        logger(sprintf('%s(): error retrieving other account names: %s', __FUNCTION__, db_errormessage()));
        $output->add_message(t('error_retrieving_data', 'admin'));
    } else {
        if ($result->num_rows > 0) {
            $records = array();
            while (($record = $result->fetch_row_assoc()) !== FALSE) {
                $acl_id = intval($record['acl_id']);
                if (isset($records[$acl_id])) {
                    $records[$acl_id]['permissions_modules'] |= intval($record['permissions_modules']);
                } else {
                    $records[$acl_id] = $record;
                }
            }
            $result->close();
            foreach ($records as $acl_id => $record) {
                $name = 'acl_ro_' . $acl_id;
                $dialogdef[$name] = array('type' => F_LISTBOX, 'name' => $name, 'value' => is_null($record['permissions_modules']) ? 0 : $record['permissions_modules'], 'options' => $roles, 'viewonly' => TRUE, 'title' => $record['username'], 'label' => $record['full_name']);
            }
        }
    }
    if (!$viewonly) {
        $dialogdef['button_save'] = dialog_buttondef(BUTTON_SAVE);
    }
    $dialogdef['button_cancel'] = dialog_buttondef(BUTTON_CANCEL);
    return $dialogdef;
}
/** 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);
}
 /** retrieve a list of all available theme records
  *
  * this returns a list of active theme-records or FALSE if none are are available
  * The list is cached via a static variable so we don't have to go to the
  * database more than once for this.
  * Note that the returned array is keyed with theme_id.
  *
  * @param bool $forced if TRUE forces reread from database (resets the cache)
  * @return array|bool FALSE if no themes available or an array with theme-records
  */
 function get_theme_records($forced = FALSE)
 {
     static $records = NULL;
     if ($records === NULL || $forced) {
         $tablename = 'themes';
         $fields = '*';
         $where = array('is_active' => TRUE);
         $order = array('theme_id');
         $records = db_select_all_records($tablename, $fields, $where, $order, 'theme_id');
     }
     return $records;
 }
/** retrieve all configuration data for this mailpage
 *
 * this retrieves all configuration data for this mailpage,
 * i.e. both the general parameters (header/intro/etc.) and
 * the full list of configured destination addresses.
 * Here is a quick reminder of the structure in $config.
 * <pre>
 * 'node_id'
 * 'header'
 * 'introduction'
 * 'message'
 * 'addresses' = ['mailpage_address_id','node_id','sort_order','name','email','description','thankyou'],...
 * </pre>
 *
 * @param int $node_id identifies the page
 * @return bool|array configuration data in a (nested) array or FALSE on error
 */
function mailpage_view_get_config($node_id)
{
    // 1 -- generic configuration
    $table = 'mailpages';
    $fields = array('node_id', 'header', 'introduction', 'message');
    $where = array('node_id' => intval($node_id));
    if (($config = db_select_single_record($table, $fields, $where)) === FALSE) {
        logger(sprintf('%s(): error retrieving configuration: %s', __FUNCTION__, db_errormessage()));
        return FALSE;
    }
    // 2 -- fetch all configured destinations
    $table = 'mailpages_addresses';
    $fields = '*';
    $where = array('node_id' => intval($node_id));
    $order = array('sort_order', 'mailpage_address_id');
    if (($records = db_select_all_records($table, $fields, $where, $order)) === FALSE) {
        logger(sprintf('%s(): error retrieving addresses: %s', __FUNCTION__, db_errormessage()));
        return FALSE;
    }
    $config['addresses'] = $records;
    // simple 0-based array, not keyed by mailpage_address_id
    return $config;
}
/** show the (visually almost) empty page and load or continue with the JS popup window
 *
 * this routine is responsible for showing an 'empty' page and maybe for generating
 * a JS popup window (if $first==TRUE). The 'empty' page contains only a form with
 * single textarea. However, this textarea is not displayed (display:none) so the
 * casual user sees nothing (but obviously without CSS it is a different matter).
 * This textarea is used by the CREW code to store the edited document before
 * submitting the form. Since there are no buttons of any kind, it is completely
 * up to the JS code to generate the necessary DOM elements that are required to
 * successfully save the document.
 *
 * If $first is TRUE, we have to setup the popup window. This is quite complicated
 * because generate the necessary JS-code at runtime using JS. One of the reasons
 * is that I want to set the correct translations in the popup window. There may
 * be an easier way.
 *
 * The Websocket protocol is used to talk to the Websocket server which is configured
 * for this site. This setting can be manipulated using the Module Manager. In order
 * to authenticate ourselves against the websocket server we use the following mechanism.
 * There are a few important variables used in authenticating:
 *
 *  - $origin: this is the website's hostname as seen by the user's browser
 *  - $request_uri: a string that uniquely identifies the node within the origin
 *  - $full_name: the full name of the current user (ie. $USER->full_name) 
 *  - $username: the (short) name/userid of the curent user (ie. $USER->username)
 *  - $request_date: the current time (GMT) in the format "yyyy-mm-dd hh:mm:ss".
 *
 * and also
 *
 *  - $secret_key: a secret shared with the Websocket server
 *  - $location: the URL of the Websocket server
 *
 * The authentication works as follows. The variables $origin, $request_uri, $full_name,
 * $username and $request_date are concatenated in a $message. Then the $message and
 * the $secret_key are used to calculate a hashed message authentication code (HMAC)
 * according to RFC2104 (see function {@see hmac()} in waslib.php).
 *
 * When connecting to the Websocket server the parameters $request_uri, $full_name,
 * $username and $request_date are sent, together with the HMAC. The server then
 * calculates the HMAC too and if it matches the HMAC that was sent, access is
 * granted.
 *
 * Note that the variable $origin is only used here to calculate the HMAC; it is
 * not sent to the Websocket server like the other parameters. Instead we use the
 * Origin as seen by the user's web browser. Obviously the two should match or else
 * authentication fails. This way we check the browser's idea of where the web page
 * is located. Also note that we made the current date/time part of the HMAC. That
 * is done to prevent replay-attacks (the other variables are quasi-static between
 * CREW editing sessions). It is up to the Websocket server to determine if the
 * timestamp is (still) valid or not. This depends on a certain clock synchronisation
 * between the webserver and the Websocket server.
 *
 * Also note that the shared secret never leaves the webserver, only the hashed
 * message is sent from webserver to Websocket server. However, the secret has to
 * be the same on both ends.
 *
 * @param object &$theme collects the (html) output
 * @param int $module_id identifies the crew module (need that for getting module properties)
 * @param bool $first if TRUE we generate code to generate a popup
 * @return bool TRUE on success+output generated via $theme, FALSE otherwise
 */
function crew_view_show_edit(&$theme, $module_id, $first = FALSE)
{
    global $USER, $WAS_SCRIPT_NAME, $CFG;
    // 1A -- fetch the latest version of the document (we always need that)...
    $node_id = intval($theme->node_record['node_id']);
    if (($record = crew_view_get_workshop_data($node_id)) === FALSE) {
        $theme->add_message(t('error_retrieving_workshop_data', 'm_crew'));
        return FALSE;
    }
    // 1B -- and tell the user the date/time/user of latest update in content area
    $params = array('{USERNAME}' => is_null($record['username']) ? $record['muser_id'] : $record['username'], '{FULL_NAME}' => is_null($record['full_name']) ? $record['muser_id'] : $record['full_name'], '{DATIM}' => $record['mtime']);
    $attr = array('class' => 'crew_datim');
    $theme->add_content(html_tag('p', $attr, t('last_updated_by', 'm_crew', $params)));
    // 1C -- prepare a hidden textarea with the current document text
    /* <noscript>requires javascript</noscript>
     * <div>
     *   <form>
     *     <textarea>$document</textarea>
     *   </form>
     * </div>
     */
    $theme->add_content(html_tag('noscript', '', t('crew_requires_js_and_ws', 'm_crew')));
    $attr = array('id' => 'crew_start_edit', 'style' => 'display: none;');
    $theme->add_content(html_tag('div', $attr));
    $href = was_node_url($theme->node_record);
    $attr = array('id' => 'frmEdit');
    $theme->add_content(html_form($href, 'post', $attr));
    $attr = array('id' => 'txtText', 'rows' => 10, 'cols' => 80, 'name' => 'text');
    $theme->add_content(html_tag('textarea', $attr, htmlspecialchars($record['document'])));
    $theme->add_content(html_form_close());
    $theme->add_content(html_tag_close('div'));
    // At this point we're done IF this was a repeat call.
    // If it was the first call we need to do some more, like popping up the edit window
    if (!$first) {
        return TRUE;
    }
    // Still here, so this is the first time
    // 2 -- prepare all information for popup
    // 2A -- which skin?
    $dialogdef = crew_view_dialogdef();
    if (!dialog_validate($dialogdef)) {
        // somehow an error; default to first skin
        $value = '0';
    } else {
        $value = $dialogdef['skin']['value'];
    }
    $skin = $dialogdef['skin']['options'][$value]['css'];
    // 2B -- which location,origin,secret (from module_properties)
    $table = 'modules_properties';
    $fields = array('name', 'value');
    $where = array('module_id' => $module_id);
    $order = array('sort_order');
    $keyfield = 'name';
    if (($properties = db_select_all_records($table, $fields, $where, $order, $keyfield)) === FALSE) {
        logger(sprintf('%s(): module properties error: %s', __FUNCTION__, db_errormessage()));
        $theme->add_message(t('error_retrieving_workshop_data', 'm_crew'));
        return FALSE;
    }
    $org = $properties['origin']['value'];
    $loc = $properties['location']['value'];
    $secret = $properties['secret']['value'];
    // 2C -- prepare variables for and perform hmac calculation
    $workshop = trim($record['header']);
    if (empty($workshop)) {
        $workshop = trim($node_record['link_text']);
    }
    $uri = sprintf('%s/%d/%s', $WAS_SCRIPT_NAME, $node_id, friendly_bookmark($workshop));
    $name = $USER->full_name;
    $nick = $USER->username;
    $datim = gmstrftime('%Y-%m-%d %T');
    $hmac_key = $secret;
    $hmac_msg = $org . $uri . $name . $nick . $datim;
    $sig = hmac($hmac_key, $hmac_msg);
    $progcrew = $CFG->progwww_short . '/modules/crew';
    $css = $progcrew . '/' . $skin;
    if ($CFG->debug || !file_exists($CFG->progdir . '/modules/crew/crew.min.js')) {
        $js = $progcrew . '/crew.js';
    } else {
        $js = $progcrew . '/crew.min.js';
    }
    $theme->add_content(html_tag('script'));
    $theme->add_content(crew_screen($loc, $nick, $name, $uri, $workshop, $org, $datim, $sig, $css, $js, $progcrew));
    $theme->add_content(html_tag_close('script'));
    return TRUE;
}
 /** retrieve phrases from the database for specified domain and language
  *
  * @param string $full_domain text domain to look for
  * @param string $language_key the language to look for
  * @return array associative array with phrase_keys and translations
  */
 function get_phrases_from_database($full_domain, $language_key)
 {
     $phrases = array();
     $table = 'phrases';
     $fields = array('phrase_key', 'phrase_text');
     $where = array('language_key' => $language_key, 'domain' => $full_domain);
     $records = db_select_all_records($table, $fields, $where);
     if ($records !== FALSE) {
         foreach ($records as $record) {
             $phrases[$record['phrase_key']] = $record['phrase_text'];
         }
     }
     unset($records);
     return $phrases;
 }
 /** constructor for the configuration assistant
  *
  * This stores the parameters, sets defaults when applicable and subsequently reads 
  * selected config parameters into the $this->records for future reference.
  *
  * @param string $table the table where the configuration parameters are stored
  * @param string $keyfield the field that uniquely identifies the configuration parameters
  * @param string $prefix is prepended for every translation/language key and the also dialog item name
  * @param string $domain the language domain where to look for translations (default: 'admin')
  * @param mixed $where a whereclause (without 'WHERE') or an array with conditions
  * @param array $dialogdef_hidden additional fields for inclusion in dialog definition
  * @return void object setup and data buffered in object
  */
 function ConfigAssistant($table, $keyfield, $prefix = '', $domain = '', $where = '', $dialogdef_hidden = '')
 {
     $this->table = $table;
     $this->keyfield = $keyfield;
     if (!in_array($keyfield, $this->fields)) {
         // we need to have this field for db_select_all_records()
         $this->fields[] = $keyfield;
     }
     $this->prefix = empty($prefix) ? '' : $prefix;
     $this->language_domain = empty($domain) ? 'admin' : $domain;
     $this->where = $where;
     $this->dialogdef_hidden = $dialogdef_hidden;
     $this->records = db_select_all_records($this->table, $this->fields, $this->where, 'sort_order', $this->keyfield);
     if ($this->records === FALSE) {
         logger('configassistant: could not retrieve config data from database: ' . db_errormessage());
     }
 }
 /** generate a list of (virtual) directories for users this user can access
  *
  * This generates a list of (virtual) user directories for which this
  * user has access permissions. The list is ordered by full name.
  *
  * @return array list of available user directories for this user
  * @uses $USER;
  * @uses $CFG;
  */
 function get_entries_users()
 {
     global $USER, $CFG;
     $entries = array();
     if ($USER->has_job_permissions(JOB_PERMISSION_ACCOUNTMANAGER)) {
         $table = 'users';
         $fields = array('user_id', 'username', 'full_name', 'is_active', 'path');
         $where = '';
         $order = array('full_name', 'username');
         if (($users = db_select_all_records($table, $fields, $where, $order, 'user_id')) === FALSE) {
             logger(sprintf('%s.%s(): cannot retrieve users list: %s', __CLASS__, __FUNCTION__, db_errormessage()));
             $users = array();
         }
     } else {
         $users = array(array('user_id' => $USER->user_id, 'username' => $USER->username, 'full_name' => $USER->full_name, 'is_active' => TRUE, 'path' => $USER->path));
     }
     if (count($users) > 0) {
         foreach ($users as $user_id => $user) {
             $name = $user['path'];
             $path = '/users/' . $name;
             $vname = $user['full_name'];
             $vpath = t('filemanager_users', 'admin') . '/' . $vname;
             $entries[$name] = array('name' => $name, 'path' => $path, 'vname' => $vname, 'vpath' => $vpath, 'mtime' => filemtime($CFG->datadir . $path), 'size' => 0, 'is_file' => FALSE, 'title' => t('filemanager_navigate_to', 'admin', array('{DIRECTORY}' => $vpath)));
         }
     }
     return $entries;
 }
/** retrieve an array with node records straight from the database
 *
 * this routine constructs a list of 0, 1 or more node records based
 * on the $node_id provided by the caller. The node records are
 * retrieved from the database.
 *
 * This routine takes care of the showstoppers like embargo and
 * expiry and also access permissions to the area. We can not
 * be sure if the user actually has access to a page until we
 * are have checked to area in which the node $node_id resides.
 * This is an extra test compared to 
 * {@link aggregator_view_get_node_from_tree()} above.
 *
 * @param int $node_id identifies page or section to evaluate
 * @param array &$config points to the aggregator configuration
 * @param array &$modules points to array with supported modules
 * @return array ordered list of nodes to aggregate (could be empty)
 */
function aggregator_view_get_node_from_db($node_id, &$config, &$modules)
{
    global $USER;
    $nodes = array();
    $table = 'nodes';
    $fields = '*';
    $order = $config['reverse_order'] ? 'sort_order DESC' : 'sort_order';
    $where = array('node_id' => intval($node_id));
    if (($record = db_select_single_record($table, $fields, $where)) === FALSE) {
        logger(sprintf('%s(): error retrieving node record: %s', __FUNCTION__, db_errormessage()));
        return $nodes;
    }
    $now = strftime("%Y-%m-%d %T");
    // don't show expired nodes or nodes under embargo
    if ($now < $record['embargo'] || $record['expiry'] < $now) {
        return $nodes;
    }
    // don't show private or inactive areas to random strangers
    $areas = get_area_records();
    $area_id = intval($record['area_id']);
    if (db_bool_is(FALSE, $areas[$area_id]['is_active']) || db_bool_is(TRUE, $areas[$area_id]['is_private']) && !$USER->has_intranet_permissions(ACL_ROLE_INTRANET_ACCESS, $area_id)) {
        return $nodes;
    }
    // if it was but a plain page we're done (even if not htmlpage or snapshots)
    if (db_bool_is(TRUE, $record['is_page'])) {
        $module_id = intval($record['module_id']);
        if (isset($modules[$module_id])) {
            $nodes[] = $record;
        }
        return $nodes;
    }
    // mmm, must have been a section (but at least in the correct area)
    // go get the pages in this section in this area
    $where = array('parent_id' => $node_id, 'area_id' => $area_id, 'is_page' => TRUE);
    if (($records = db_select_all_records($table, $fields, $where, $order)) === FALSE) {
        logger(sprintf('%s(): error retrieving node records: %s', __FUNCTION__, db_errormessage()));
        return $nodes;
    }
    $counter = 0;
    foreach ($records as $record) {
        // don't show expired nodes or nodes under embargo
        if ($now < $record['embargo'] || $record['expiry'] < $now) {
            continue;
        }
        $module_id = intval($record['module_id']);
        if (isset($modules[$module_id])) {
            $nodes[] = $record;
            if (++$counter >= $config['items']) {
                break;
            }
        }
    }
    return $nodes;
}
 /** build a tree of all nodes in an area
  *
  * this routine constructs a tree-structure of all nodes in area $area_id in much
  * the same way as {@link tree_build()} does. However, in this routine we keep the
  * cargo limited to a minimum: the fields we retrieve from the nodes table and
  * store in the tree are:
  *  - node_id
  *  - parent_id
  *  - is_page
  *  - title
  *  - link_text
  *  - module_id
  * Also, the tree is not cached because that does not make sense here: we only
  * use it to construct a dialogdef and that is a one-time operation too.
  *
  * @parameter int $area_id the area for which to build the tree
  * @param int $acl_id the primary acl_id (used for both users and groups)
  * @param array|null $related_acls an array with related acls for this user keyed by 'acl_id' or NULL for group acls
  * @return array ready to use tree structure w/ permissions
  */
 function tree_build($area_id)
 {
     // 1 -- Start with 'special' node 0 is root of the tree
     $tree = array(0 => array('node_id' => 0, 'parent_id' => 0, 'prev_sibling_id' => 0, 'next_sibling_id' => 0, 'first_child_id' => 0, 'is_page' => FALSE, 'title' => '', 'link_text' => '', 'module_id' => 0));
     $where = array('area_id' => intval($area_id));
     $order = array('CASE WHEN (parent_id = node_id) THEN 0 ELSE parent_id END', 'sort_order', 'node_id');
     $fields = array('node_id', 'parent_id', 'is_page', 'title', 'link_text', 'module_id');
     $records = db_select_all_records('nodes', $fields, $where, $order, 'node_id');
     // 2 -- step through all node records and copy the relevant fields
     if ($records !== FALSE) {
         foreach ($records as $record) {
             $node_id = intval($record['node_id']);
             $parent_id = intval($record['parent_id']);
             $is_page = db_bool_is(TRUE, $record['is_page']);
             if ($parent_id == $node_id) {
                 // top level
                 $parent_id = 0;
             }
             $tree[$node_id] = array('node_id' => $node_id, 'parent_id' => $parent_id, 'prev_sibling_id' => 0, 'next_sibling_id' => 0, 'first_child_id' => 0, 'is_page' => $is_page, 'title' => $record['title'], 'link_text' => $record['link_text'], 'module_id' => intval($record['module_id']));
         }
     }
     unset($records);
     // free memory
     // 3 -- step through all collected records and add links to childeren and siblings
     $prev_node_id = 0;
     $sort_order = 0;
     foreach ($tree as $node_id => $node) {
         $parent_id = $node['parent_id'];
         if (!isset($tree[$parent_id])) {
             logger("aclmanager: node '{$node_id}' is an orphan because parent '{$parent_id}' does not exist in tree[]");
         } elseif ($parent_id == $tree[$prev_node_id]['parent_id']) {
             $tree[$prev_node_id]['next_sibling_id'] = $node_id;
             $tree[$node_id]['prev_sibling_id'] = $prev_node_id;
         } else {
             $tree[$parent_id]['first_child_id'] = $node_id;
         }
         $prev_node_id = $node_id;
     }
     // 4 -- 'root node' 0 is a special case, the top level nodes are in fact childeren, not siblings
     $tree[0]['first_child_id'] = $tree[0]['next_sibling_id'];
     $tree[0]['next_sibling_id'] = 0;
     // 5 -- done!
     return $tree;
 }
 /** retrieve acl-data from table into a sparse array
  *
  * @param string $table name of the table which holds the acls
  * @return array zero or more elements with permissions
  */
 function fetch_acls_from_table($table)
 {
     $where = $this->where_acl_id();
     $a = array();
     switch ($table) {
         case 'acls_areas':
             $fields = array('permissions_intranet', 'permissions_modules', 'permissions_nodes');
             $keys = array('area_id');
             break;
         case 'acls_nodes':
             $fields = array('permissions_modules', 'permissions_nodes');
             $keys = array('node_id');
             break;
         case 'acls_modules':
             $fields = array('permissions_modules');
             $keys = array('module_id');
             break;
         case 'acls_modules_areas':
             $fields = array('permissions_modules');
             $keys = array('module_id', 'area_id');
             break;
         case 'acls_modules_nodes':
             $fields = array('permissions_modules');
             $keys = array('module_id', 'node_id');
             break;
         default:
             logger(sprintf("%s(): unknown table '%s'; cannot retrieve acls", __FUNCTION__, $table));
             return array();
             // empty array equates to: no access
             break;
     }
     $records = db_select_all_records($table, '*', $where);
     if ($records === FALSE) {
         logger(sprintf("%s(): cannot get acls from '%s'; %s'", __FUNCTION__, $table, db_errormessage()));
         return array();
         // empty array equates to: no access
     }
     if (sizeof($keys) == 1) {
         $key = $keys[0];
         if (sizeof($fields) > 1) {
             // acls_areas, acls_nodes
             foreach ($records as $record) {
                 $k = intval($record[$key]);
                 foreach ($fields as $f) {
                     if (($v = intval($record[$f])) != 0) {
                         $a[$k][$f] = isset($a[$k][$f]) ? $a[$k][$f] | $v : $v;
                     }
                 }
             }
         } else {
             // acls_modules
             $field = $fields[0];
             foreach ($records as $record) {
                 $k = intval($record[$key]);
                 if (($v = intval($record[$field])) != 0) {
                     $a[$k] = isset($a[$k]) ? $a[$k] | $v : $v;
                 }
             }
         }
     } else {
         // acls_modules_areas, acls_modules_nodes
         $field = $fields[0];
         foreach ($records as $record) {
             if (($v = intval($record[$field])) != 0) {
                 $k0 = intval($record[$keys[0]]);
                 $k1 = intval($record[$keys[1]]);
                 $a[$k0][$k1] = isset($a[$k0][$k1]) ? $a[$k0][$k1] | $v : $v;
             }
         }
     }
     unset($records);
     return $a;
 }
/** retrieve current list of addresses in an array (could be empty)
 *
 * this retrieves the addresses associated with $node_id from the
 * database. As an important side effect all entries' sort order values
 * are renumbered. This means that we always have a neat set of sort
 * order numbers 10, 20, ...
 *
 * On error we return an empty array as a sort of best effort.
 * The error is logged though.
 *
 * @param int $node_id indicates page
 * @return array records from the database with sort order renumbered OR an empty array
 */
function mailpage_get_addresses($node_id)
{
    $table = 'mailpages_addresses';
    $keyfield = 'mailpage_address_id';
    $fields = '*';
    $where = array('node_id' => intval($node_id));
    $order = array('sort_order', $keyfield);
    if (($records = db_select_all_records($table, $fields, $where, $order, $keyfield)) === FALSE) {
        logger(sprintf('%s(): error retrieving addresses: %s', __FUNCTION__, db_errormessage()));
        return array();
    }
    // side-effect: renumber the sort order of the addresses if not a nice list of 10, 20, ...
    $sort_order = 0;
    foreach ($records as $k => $record) {
        $sort_order += 10;
        if ($record['sort_order'] == $sort_order) {
            continue;
        }
        $records[$k]['sort_order'] = $sort_order;
        $where = array($keyfield => intval($record[$keyfield]));
        $fields = array('sort_order' => $sort_order);
        if (db_update($table, $fields, $where) === FALSE) {
            logger(sprintf('%s(): error renumbering addresses: %s', __FUNCTION__, db_errormessage()));
        }
    }
    return $records;
}
/** create a few sections and pages
 *
 * this constructs a complete public area with some pages and sections
 * and also the 'frugal' theme is configured for this area.
 * The information about the nodes (including the assigned node_id) is
 * copied to $config['demo_nodes'] for the caller's perusal.
 *
 * @param array &$messages used to return (error) messages to caller
 * @param array &$config pertinent information about the site; receives copy of nodes array on return
 * @param array &$tr translations of demodata texts
 * @return bool TRUE on success + data entered into database, FALSE on error
 */
function demodata_sections_pages(&$messages, &$config, &$tr)
{
    $retval = TRUE;
    // 0 -- setup essential information
    $table = 'modules';
    $fields = array('module_id', 'name');
    $where = '';
    $order = '';
    $keyfield = 'name';
    if (($records = db_select_all_records($table, $fields, $where, $order, $keyfield)) === FALSE) {
        // if we cannot determine the module_id's there is no point to stay here and 'pollute' the database with nonsense
        $messages[] = $tr['error'] . ' ' . db_errormessage();
        return FALSE;
    }
    $htmlpage_id = intval($records['htmlpage']['module_id']);
    $sitemap_id = intval($records['sitemap']['module_id']);
    $mailpage_id = intval($records['mailpage']['module_id']);
    $redirect_id = intval($records['redirect']['module_id']);
    $replace = $config['demo_replace'];
    $year = intval($replace['{YEAR}']);
    $nodes = array('welcome' => array('parent_id' => 'welcome', 'is_page' => TRUE, 'is_default' => TRUE, 'title' => $tr['welcome_title'], 'link_text' => $tr['welcome_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'schoolinfo' => array('parent_id' => 'schoolinfo', 'is_page' => FALSE, 'title' => $tr['schoolinfo_title'], 'link_text' => $tr['schoolinfo_link_text'], 'sort_order' => 20), 'aboutus' => array('parent_id' => 'schoolinfo', 'is_page' => TRUE, 'title' => $tr['aboutus_title'], 'link_text' => $tr['aboutus_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'schoolterms1' => array('parent_id' => 'schoolinfo', 'is_page' => TRUE, 'title' => strtr($tr['schoolterms_title'], array('{SCHOOLYEAR}' => $replace['{LAST_SCHOOLYEAR}'])), 'link_text' => strtr($tr['schoolterms_link_text'], array('{SCHOOLYEAR}' => $replace['{LAST_SCHOOLYEAR}'])), 'embargo' => sprintf('%04d-08-01 00:00:00', $year - 1), 'expiry' => sprintf('%04d-08-01 00:00:00', $year), 'sort_order' => 20, 'module_id' => $htmlpage_id), 'schoolterms2' => array('parent_id' => 'schoolinfo', 'is_page' => TRUE, 'title' => strtr($tr['schoolterms_title'], array('{SCHOOLYEAR}' => $replace['{THIS_SCHOOLYEAR}'])), 'link_text' => strtr($tr['schoolterms_link_text'], array('{SCHOOLYEAR}' => $replace['{THIS_SCHOOLYEAR}'])), 'embargo' => sprintf('%04d-08-01 00:00:00', $year), 'expiry' => sprintf('%04d-08-01 00:00:00', $year + 1), 'sort_order' => 30, 'module_id' => $htmlpage_id), 'schoolterms3' => array('parent_id' => 'schoolinfo', 'is_page' => TRUE, 'title' => strtr($tr['schoolterms_title'], array('{SCHOOLYEAR}' => $replace['{NEXT_SCHOOLYEAR}'])), 'link_text' => strtr($tr['schoolterms_link_text'], array('{SCHOOLYEAR}' => $replace['{NEXT_SCHOOLYEAR}'])), 'embargo' => sprintf('%04d-08-01 00:00:00', $year + 1), 'expiry' => sprintf('%04d-08-01 00:00:00', $year + 2), 'sort_order' => 40, 'module_id' => $htmlpage_id), 'contact' => array('parent_id' => 'schoolinfo', 'is_page' => TRUE, 'title' => $tr['contact_title'], 'link_text' => $tr['contact_link_text'], 'sort_order' => 50, 'module_id' => $mailpage_id), 'news' => array('parent_id' => 'news', 'is_page' => FALSE, 'title' => $tr['news_title'], 'link_text' => $tr['news_link_text'], 'sort_order' => 30), 'latestnews' => array('parent_id' => 'news', 'is_page' => TRUE, 'title' => $tr['latestnews_title'], 'link_text' => $tr['latestnews_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'latestnewsletter' => array('parent_id' => 'news', 'is_page' => TRUE, 'title' => $tr['latestnewsletter_title'], 'link_text' => $tr['latestnewsletter_link_text'], 'sort_order' => 20, 'module_id' => $htmlpage_id), 'newsarchive' => array('parent_id' => 'news', 'is_page' => FALSE, 'title' => $tr['newsarchive_title'], 'link_text' => $tr['newsarchive_link_text'], 'sort_order' => 30), 'oldnews' => array('parent_id' => 'newsarchive', 'is_page' => TRUE, 'title' => $tr['oldnews_title'], 'link_text' => $tr['oldnews_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'oldnewsletters' => array('parent_id' => 'newsarchive', 'is_page' => TRUE, 'title' => $tr['oldnewsletters_title'], 'link_text' => $tr['oldnewsletters_link_text'], 'sort_order' => 20, 'module_id' => $htmlpage_id), 'search' => array('parent_id' => 'search', 'is_page' => FALSE, 'title' => $tr['search_title'], 'link_text' => $tr['search_link_text'], 'sort_order' => 40), 'searchbox' => array('parent_id' => 'search', 'is_page' => TRUE, 'title' => $tr['searchbox_title'], 'link_text' => $tr['searchbox_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'sitemap' => array('parent_id' => 'search', 'is_page' => TRUE, 'title' => $tr['sitemap_title'], 'link_text' => $tr['sitemap_link_text'], 'sort_order' => 20, 'module_id' => $sitemap_id), 'mypage' => array('parent_id' => 'mypage', 'is_page' => TRUE, 'title' => $tr['mypage_title'], 'link_text' => $tr['mypage_link_text'], 'sort_order' => 50, 'module_id' => $htmlpage_id), 'quicktop' => array('parent_id' => 'quicktop', 'is_page' => FALSE, 'is_hidden' => TRUE, 'title' => $tr['quicktop_title'], 'link_text' => $tr['quicktop_link_text'], 'sort_order' => 60), 'about' => array('parent_id' => 'quicktop', 'is_page' => TRUE, 'title' => $tr['about_title'], 'link_text' => $tr['about_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'redirect' => array('parent_id' => 'quicktop', 'is_page' => TRUE, 'title' => $tr['contact_title'], 'link_text' => $tr['contact_link_text'], 'sort_order' => 20, 'module_id' => $redirect_id), 'quickbottom' => array('parent_id' => 'quickbottom', 'is_page' => FALSE, 'is_hidden' => TRUE, 'title' => $tr['quickbottom_title'], 'link_text' => $tr['quickbottom_link_text'], 'sort_order' => 70), 'disclaimer' => array('parent_id' => 'quickbottom', 'is_page' => TRUE, 'title' => $tr['disclaimer_title'], 'link_text' => $tr['disclaimer_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'login' => array('parent_id' => 'quickbottom', 'is_page' => TRUE, 'title' => $tr['login_title'], 'link_text' => $tr['login_link_text'], 'sort_order' => 20, 'module_id' => $htmlpage_id), 'intranet' => array('parent_id' => 'intranet', 'is_page' => TRUE, 'is_default' => TRUE, 'title' => $tr['intranet_title'], 'link_text' => $tr['intranet_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'meetings' => array('parent_id' => 'meetings', 'is_page' => FALSE, 'title' => $tr['meetings_title'], 'link_text' => $tr['meetings_link_text'], 'sort_order' => 20), 'roster' => array('parent_id' => 'meetings', 'is_page' => TRUE, 'title' => $tr['roster_title'], 'link_text' => $tr['roster_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'minutes' => array('parent_id' => 'meetings', 'is_page' => FALSE, 'title' => strtr($tr['minutes_title'], array('{SCHOOLYEAR}' => $replace['{LAST_SCHOOLYEAR}'])), 'link_text' => strtr($tr['minutes_link_text'], array('{SCHOOLYEAR}' => $replace['{LAST_SCHOOLYEAR}'])), 'sort_order' => 20), 'minutes1' => array('parent_id' => 'minutes', 'is_page' => TRUE, 'title' => $tr['minutes1_title'], 'link_text' => $tr['minutes1_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'minutes2' => array('parent_id' => 'minutes', 'is_page' => TRUE, 'title' => $tr['minutes2_title'], 'link_text' => $tr['minutes2_link_text'], 'sort_order' => 20, 'module_id' => $htmlpage_id), 'minutes3' => array('parent_id' => 'minutes', 'is_page' => TRUE, 'title' => $tr['minutes3_title'], 'link_text' => $tr['minutes3_link_text'], 'sort_order' => 30, 'module_id' => $htmlpage_id), 'minutes4' => array('parent_id' => 'minutes', 'is_page' => TRUE, 'title' => $tr['minutes4_title'], 'link_text' => $tr['minutes4_link_text'], 'sort_order' => 40, 'module_id' => $htmlpage_id), 'newminutes' => array('parent_id' => 'meetings', 'is_page' => FALSE, 'title' => strtr($tr['minutes_title'], array('{SCHOOLYEAR}' => $replace['{THIS_SCHOOLYEAR}'])), 'link_text' => strtr($tr['minutes_link_text'], array('{SCHOOLYEAR}' => $replace['{THIS_SCHOOLYEAR}'])), 'sort_order' => 30), 'minutes5' => array('parent_id' => 'newminutes', 'is_page' => TRUE, 'title' => $tr['minutes1_title'], 'link_text' => $tr['minutes1_link_text'], 'sort_order' => 10, 'module_id' => $htmlpage_id), 'downloads' => array('parent_id' => 'downloads', 'is_page' => TRUE, 'title' => $tr['downloads_title'], 'link_text' => $tr['downloads_link_text'], 'sort_order' => 30, 'module_id' => $htmlpage_id));
    $now = strftime('%Y-%m-%d %T');
    $user_id = $config['user_id'];
    $area_id = $config['demo_areas']['public']['area_id'];
    foreach ($nodes as $node => $fields) {
        if ($node == 'intranet') {
            // the nodes that follow are in another area
            $area_id = $config['demo_areas']['private']['area_id'];
        }
        $fields['area_id'] = $area_id;
        $fields['ctime'] = $now;
        $fields['mtime'] = $now;
        $fields['atime'] = $now;
        $fields['owner_id'] = $user_id;
        // Note: this is the reason we don't have a FK (parent_id) referencing nodes(node_id): 0 is an invalid value
        if ($fields['parent_id'] == $node) {
            // parent points to self, use 0 as a sentinel
            $fields['parent_id'] = 0;
        } else {
            // plug in the node_id of the parent node (which we already processed)
            $fields['parent_id'] = $nodes[$fields['parent_id']]['node_id'];
        }
        if (($node_id = db_insert_into_and_get_id('nodes', $fields, 'node_id')) === FALSE) {
            $messages[] = $tr['error'] . ' ' . db_errormessage();
            $retval = FALSE;
        }
        $node_id = intval($node_id);
        $fields['node_id'] = $node_id;
        if ($fields['parent_id'] == 0) {
            // parent points to self, adjust the 0 in the database
            $fields['parent_id'] = $node_id;
            if (db_update('nodes', array('parent_id' => $node_id), array('node_id' => $node_id)) === FALSE) {
                $messages[] = $tr['error'] . ' ' . db_errormessage();
                $retval = FALSE;
            }
        }
        $nodes[$node] = $fields;
        // Fill pages with actual content (sort of)
        if ($fields['is_page']) {
            switch ($fields['module_id']) {
                case $htmlpage_id:
                    $htmlpage_fields = array('node_id' => $node_id, 'version' => 1, 'page_data' => strtr($tr[$node . '_content'], $replace), 'ctime' => $now, 'cuser_id' => $user_id, 'mtime' => $now, 'muser_id' => $user_id);
                    if (db_insert_into('htmlpages', $htmlpage_fields) === FALSE) {
                        $messages[] = $tr['error'] . ' ' . db_errormessage();
                        $retval = FALSE;
                    }
                    break;
                case $sitemap_id:
                    $sitemap_fields = array('node_id' => $node_id, 'header' => $tr['sitemap_title'], 'introduction' => strtr($tr[$node . '_content'], $replace), 'scope' => 1, 'ctime' => $now, 'cuser_id' => $user_id, 'mtime' => $now, 'muser_id' => $user_id);
                    if (db_insert_into('sitemaps', $sitemap_fields) === FALSE) {
                        $messages[] = $tr['error'] . ' ' . db_errormessage();
                        $retval = FALSE;
                    }
                    break;
                case $redirect_id:
                    $redirect_fields = array('link_href' => sprintf($config['friendly_url'] ? '%s/%d' : '%s?node=%d', $replace['{INDEX_URL}'], $nodes['contact']['node_id']));
                    if (db_update('nodes', $redirect_fields, array('node_id' => $node_id)) === FALSE) {
                        $messages[] = $tr['error'] . ' ' . db_errormessage();
                        $retval = FALSE;
                    }
                    break;
                case $mailpage_id:
                    $mailpage_fields = array('node_id' => $node_id, 'header' => $tr[$node . '_header'], 'introduction' => strtr($tr[$node . '_introduction'], $replace), 'message' => '', 'ctime' => $now, 'cuser_id' => $user_id, 'mtime' => $now, 'muser_id' => $user_id);
                    if (db_insert_into('mailpages', $mailpage_fields) === FALSE) {
                        $messages[] = $tr['error'] . ' ' . db_errormessage();
                        $retval = FALSE;
                    }
                    $mailpage_addresses = array(array('node_id' => $node_id, 'sort_order' => 10, 'name' => $tr['contact_name1'], 'email' => $config['replyto'], 'description' => $tr['contact_description1'], 'thankyou' => $tr['contact_thankyou1']), array('node_id' => $node_id, 'sort_order' => 20, 'name' => $tr['contact_name2'], 'email' => $config['user_email'], 'description' => $tr['contact_description2'], 'thankyou' => $tr['contact_thankyou2']));
                    foreach ($mailpage_addresses as $mailpage_fields) {
                        if (db_insert_into('mailpages_addresses', $mailpage_fields) === FALSE) {
                            $messages[] = $tr['error'] . ' ' . db_errormessage();
                            $retval = FALSE;
                        }
                    }
                    break;
                default:
                    $messages[] = 'Internal error: unknown module ' . $field['module_id'];
                    break;
            }
        }
    }
    // Now plug in the correct values for quicktop/quickbottom in the theme
    $theme_updates = array(array('fields' => array('value' => strval($nodes['quicktop']['node_id'])), 'where' => array('area_id' => $config['demo_areas']['public']['area_id'], 'name' => 'quicktop_section_id')), array('fields' => array('value' => strval($nodes['quickbottom']['node_id'])), 'where' => array('area_id' => $config['demo_areas']['public']['area_id'], 'name' => 'quickbottom_section_id')));
    foreach ($theme_updates as $theme_update) {
        if (db_update('themes_areas_properties', $theme_update['fields'], $theme_update['where']) === FALSE) {
            $messages[] = $tr['error'] . ' ' . db_errormessage();
            $retval = FALSE;
        }
    }
    $config['demo_nodes'] = $nodes;
    return $retval;
}
 /** manipulate the current state if indicator(s) for 'open' and 'closed' areas
  * 
  * this manipulates the current state of 'open' and 'closed' areas in $areas_open.
  * If $area_id is NULL, we don't have to do anything but simply return the current state.
  * If $area_id is 0 (zero), we need to toggle all areas at once (area_id = 0 implies the site level toggle)
  * If $area_id is an integer, it is assumed to be a valid area_id and that area should be toggled.
  *
  * @param array|bool $areas_open current state of indicator(s) for 'open' and 'closed' areas
  * @param int|null $area_id the area to expand/collapse or NULL if nothing needs to be done
  * @return array|bool new state of indicator(s) for 'open' and 'closed' areas
  */
 function areas_expand_collapse($areas_open, $area_id)
 {
     // 0 -- anything to do?
     if (!is_int($area_id)) {
         return $areas_open;
     }
     // 1 -- toggle site-level?
     if ($area_id == 0) {
         $areas_open = is_array($areas_open) || $areas_open === TRUE ? FALSE : TRUE;
         return $areas_open;
     }
     // 2 -- still here? must be individual area then
     // 2A -- old: every area closed; new: a single area opened
     if ($areas_open === FALSE) {
         $areas_open = array($area_id => TRUE);
         return $areas_open;
     }
     // 2B -- old: some open, some closed
     if (is_array($areas_open)) {
         $areas_open[$area_id] = isset($areas_open[$area_id]) && $areas_open[$area_id] ? FALSE : TRUE;
         // if this is the last one set to FALSE, all areas are now 'closed' and we should return FALSE and no array
         if ($areas_open[$area_id]) {
             return $areas_open;
         } else {
             foreach ($areas_open as $k => $v) {
                 if ($v) {
                     return $areas_open;
                     // there was at least 1 other area 'open', so stick to an array
                 }
             }
             // still here? then all areas were closed: return FALSE;
             return FALSE;
         }
     }
     // 2C -- old: all opened; new: a single area is closed
     // At this point we start with all areas opened, and only area area_id must be closed.
     // That means that we have to create an array of areas and set every area's value to TRUE,
     // except area area_id.
     $records = db_select_all_records('areas', 'area_id', '', '', 'area_id');
     if ($records === FALSE) {
         logger(sprintf('%s.%s(): cannot retrieve areas. Mmmm...', __CLASS__, __FUNCTION__), WLOG_DEBUG);
         return TRUE;
     }
     $open_areas = array();
     foreach ($records as $k => $v) {
         $open_areas[$k] = TRUE;
     }
     $open_areas[$area_id] = FALSE;
     unset($records);
     return $open_areas;
 }
 /** return an ordered list of translation domains
  *
  * this constructs a list of language domains, grouped by
  * 'program','modules','themes' or 'install'. This array is the basis
  * for validating full domains (in $_POST'ed data) and also to construct
  * a menu.
  *
  * Note that we use the translations from the files themselves in the
  * current language to construct this list. Every translatefile should have
  * at least the string 'translatetool_title' and 'translatetool_description'.
  * Currently the sort order is based on the (internal) name of the modules.
  * This should do the trick for translators: the order of files to translate
  * in the menu does not depend on the translation of the module- or theme-title.
  * (In the page manager and elsewhere it may be different).
  *
  * @return array contains list of displayable titles and descriptions, keyed by full_domain
  */
 function get_domains()
 {
     global $CFG;
     $domains = array();
     // 1 -- Straightforward list of files for the core program itself
     $domains['was'] = array('grouping' => 'program', 'title' => t('translatetool_title', 'was'), 'description' => t('translatetool_description', 'was'));
     $domains['loginlib'] = array('grouping' => 'program', 'title' => t('translatetool_title', 'loginlib'), 'description' => t('translatetool_description', 'loginlib'));
     $domains['admin'] = array('grouping' => 'program', 'title' => t('translatetool_title', 'admin'), 'description' => t('translatetool_description', 'admin'));
     // 2 -- A tricky list of modules and themes which re-uses the tablename as grouping parameter
     $where = '';
     $order = 'name';
     $field = 'name';
     foreach (array('modules' => 'm_', 'themes' => 't_') as $table => $prefix) {
         if (($records = db_select_all_records($table, $field, $where, $order, $field)) === FALSE) {
             continue;
         }
         foreach ($records as $name => $record) {
             $domains[$prefix . $name] = array('grouping' => $table, 'title' => t('translatetool_title', $prefix . $name), 'description' => t('translatetool_description', $prefix . $name));
         }
     }
     // 3 -- Straightforward list of installation translations (located elsewhere in the /program directory tree)
     $domains['i_install'] = array('grouping' => 'install', 'title' => t('translatetool_title', 'i_install', '', $CFG->progdir . '/install/languages'), 'description' => t('translatetool_description', 'i_install', '', $CFG->progdir . '/install/languages'));
     $domains['i_demodata'] = array('grouping' => 'install', 'title' => t('translatetool_title', 'i_demodata', '', $CFG->progdir . '/install/languages'), 'description' => t('translatetool_description', 'i_demodata', '', $CFG->progdir . '/install/languages'));
     return $domains;
 }