/** 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; }