Class PageManager

Description

Page Manager

This class implements the Page Manager.

All the work is directed from the constructor, so it is enough to simply instantiate a new object and let the constructor do the work. The only thing needed is an output object (see AdminOutput).

Located in /program/lib/pagemanager.class.php (line 79)


	
			
Variable Summary
 null|array $areas
 null|int $area_id
 object|null $output
 null|array $tree
Method Summary
 void PageManager (object &$output)
 void build_cached_tree (int $area_id, [bool $force = FALSE])
 int calculate_new_sort_order (array &$tree, int $area_id, int $parent_id, [bool $at_end = FALSE])
 int calculate_updated_sort_order ( $node_id, int $after_id)
 void calc_home_id (int $node_id, bool|int 1)
 bool delete_node (int $node_id)
 array get_dialogdef_add_node (bool $is_page)
 array get_dialogdef_edit_advanced_node (int $node_id, bool $is_page, [bool $viewonly = FALSE], int $area_id)
 array get_dialogdef_edit_node (int $node_id, bool $is_page, [bool $viewonly = FALSE])
 void get_dialog_data_node (array &$dialogdef, int $node_id)
 void get_icon_delete (int $node_id)
 void get_icon_edit (int $node_id)
 void get_icon_home (int $node_id)
 void get_icon_invisibility (int $node_id)
 void get_icon_page_preview (int $node_id)
 void get_icon_section (int $node_id)
 void get_link_node_edit (int $node_id)
 array|bool get_module_records ()
 array get_node_id_and_ancestors (int $node_id)
 array get_options_area (int $node_id, bool $is_page)
 array get_options_parents (bool $is_page, [mixed $forbidden_id = NULL])
 void get_options_parents_walk (array &$options, bool $is_page, int $node_id, int|null $forbidden_id)
 array get_options_sort_order (int $node_id)
 bool lock_records_subtree (int $node_id, int $new_area_id)
 string message_from_lockinfo (array $lockinfo, int $node_id,  $is_page)
 bool module_connect (int $area_id, int $node_id, int $module_id)
 bool module_disconnect (int $area_id, int $node_id, int $module_id)
 bool|array module_load_admin (int $module_id)
 bool module_save (int $node_id, int $module_id, bool $viewonly, bool &$edit_again)
 bool module_show_edit (int $node_id, int $module_id, bool $viewonly, bool $edit_again)
 string node_full_name (int $node_id)
 bool node_has_grandchilderen (int $node_id)
 bool permission_add_any_node (bool $is_page)
 bool permission_add_node (int $section_id, bool $is_page)
 bool permission_delete_node (int $node_id,  $is_page)
 bool permission_edit_node (int $node_id, bool $is_page, [bool $check_content = FALSE])
 bool permission_edit_node_content (int $node_id)
 bool permission_set_default (bool $node_id)
 void queue_area_node_alert (mixed $areas, mixed $nodes,  $alert_message, [string $username = ''], string $message)
 void save_node (int $node_id)
 bool save_node_new_area_mass_move (int $node_id, int $new_area_id, bool $embargo)
 bool section_is_open (int $section_id)
 void show_area_menu ([int|null $current_area_id = NULL])
 void show_dialog_delete_node_confirm (int $node_id)
 void show_edit_menu (int $node_id, [bool $is_page = FALSE], [int $current_option = NULL])
 void show_tree ()
 void show_tree_walk (int $node_id, [string $m = ''])
 void task_node_add (string $task)
 void task_node_edit (string $task)
 void task_save_newnode (string $task)
 void task_save_node ()
 void task_treeview ()
Variables
null|array $areas = NULL (line 84)
  • var: $areas holds all area records (for future reference) or NULL if not yet set
null|int $area_id = NULL (line 90)
  • var: $area_id indicates which tree is stored in $this-tree, or NULL if none yet
object|null $output = NULL (line 81)
  • var: $output collects the html output
null|array $tree = NULL (line 87)
  • var: $tree holds the complete tree for area $this->area_id or NULL if not yet set
Methods
Constructor PageManager (line 111)

construct a PageManager object (called from /program/main_admin.php)

This initialises the PageManager, checks user permissions and finally dispatches the tasks. If the specified task is not recognised, the default task TASK_TREEVIEW is executed.

Note that allmost all commands act on the area contained in the SESSION-variable current_area_id. Also, we almost always need the tree of nodes in that area, so we read it once, _before_ dispatching the task at hand. This means that the current tree in the current area is ALWAYS available. This means that none of the other routines should have to worry about which area or reading the tree; this information is already available in $this->area_id and $this->tree.

  • return: results are returned as output in $this->output
void PageManager (object &$output)
  • object &$output: collects the html output
build_cached_tree (line 4004)

construct $this->tree for future reference

this constructs the tree of the area $area_id so all other routines can simply use that tree instead of passing it around again and again via function arguments.

  • return: a copy of the area tree is cached in $this->tree
void build_cached_tree (int $area_id, [bool $force = FALSE])
  • int $area_id: indicates which area to buffer if not already buffered
  • bool $force: re-read of the tree for area $area_id
calculate_new_sort_order (line 3821)

calculate a new sort order and at the same time make room for a node

this is used to calculate a new sort order number for a node that will be added to section $parent_id in area $area_id. Note that this could be another area than the current working area. The reference to the tree is necessary; we can't simply use $this->tree and $this->area_id.

Depending on the flag $at_end the node is added at the end of the section or at the beginning. In the latter case, the new sort order number is always 10 and all the existing nodes are renumbered in such a way that the second node in the section (originally the first one) gets sort order 20. By not using consecutive numbers it is possible to 'insert' nodes without touching anything. This is not used but it does no harm to have a sort order in steps of 10 instead of 1. (I think the database doesn't care much when executing/interpreting the ORDER BY clause).

Note that this routine not only calculates a sort order but it also manipulates the database and moves other nodes in the section around in order to make room.

Note that the feature $at_end is currently not used at all.

  • return: the sort order for the new node AND maybe changed sort orders amongst the childeren of $parent_id
  • uses: $DB
int calculate_new_sort_order (array &$tree, int $area_id, int $parent_id, [bool $at_end = FALSE])
  • array &$tree: reference to the tree in area $area_id
  • int $area_id: the area to look at
  • int $parent_id: the section where we need to make room (where a node is added/inserted)
  • bool $at_end: if TRUE a new node is added at the end, otherwise it is inserted at the beginning
calculate_updated_sort_order (line 3909)

calculate an updated sort order and also make space in the section for moving the node around

this calculates a new sort order for node node_id; the effect should be that node_id will sort AFTER node after_id. If after_id is 0 then node_id should become the first in the section.

Note that this routine not only calculates a sort order but it also manipulates the database and moves other nodes in the section around in order to make room.

There are several different cases possible: a. $after_id == 0 b. sort_order($after_id) < sort_order($node_id) c. sort_order($node_id) < sort_order($after_id) d. $after_id is the last node in this section e. $node_id == $after_id

Case e. should not happen but if it did it would yield a no-op. Case d is very similar to case c, so much even that both cases can be combined to just one.

Strategy for case a. $old_sort_order = sort_order($node_id); $new_sort_order = sort_order(first_child(parent_section($node_id))) $delta = $old_sort_order - sort_order(prev($node_id)) SET $sort_order += $delta WHERE $new_sort_order <= sort_order(node) <= $old_sort_order

In other words: node_id gets the sort_order value from the first node in the section, all nodes from the first upto position where node_id was originally move 'up' in such a way that the last in that range will end up with the sort order that node_id had originally.

Strategy for case b. $old_sort_order = sort_order($node_id) $new_sort_order = sort_order(next($after_id)) $delta = $old_sort_order - sort_order(prev($node_id)) SET $sort_order += $delta WHERE $new_sort_order <= sort_order(node) <= $old_sort_order

Note that a and b are also quite similar.

Strategy for case c. (and d.) $old_sort_order = sort_order($node_id) $new_sort_order = sort_order($after_id) $delta = $old_sort_order - sort_order(next($node_id)) (note that this is a negative value) SET $sort_order += $delta WHERE $old_sort_order <= sort_order(node) <= $new_sort_order

By mass-updating the other nodes, we hopefully don't disturb the other nodes, even while they might be locked. So there, the lock on the node is not absolute, we will change the record behind the back of another user holding a lock. On the other hand: messing up the sort order is less messy than messing with the actual content of a node. I'll take the risk. Worst case is that two processes will both update the sort order, perhaps yielding two nodes with the same sort_order value. Oh well, so be it. (There is this law by Pareto, something about 80 - 20. Mmmm...)

  • return: the new sort_order value for $node_id + other nodes in the section may have been moved
  • uses: $DB
int calculate_updated_sort_order ( $node_id, int $after_id)
  • int $after_id: the node AFTER which $node_id should be sorted (0 means: first in the section)
  • $node_id
calc_home_id (line 3981)

calculate the current default node on this level

this tries to find a sibling of the node $node_id that has the flag 'is_default' set to TRUE

void calc_home_id (int $node_id, bool|int 1)
  • int $node_id: the node of interest
  • bool|int 1: FALSE if no default node found, the default node_id otherwise
delete_node (line 1775)

workhorse routine for deleting a node, including childeren

This deletes the childeren (but not grandchilderen) of a section and the section itself OR simply the node itself. See function for more on this design decision.

This routine actually deletes nodes from the database, but only if these nodes do not have childeren AND if the nodes are not readonly. Furthermore, just before the child nodes are deleted, a lock on that node is obtained. This makes sure that a node that is currently being edited by another user is not deleted under her nose. Also, we do not delete nodes that have childeren because that would yield orphan nodes.

Any problems with deleting childeren are reported in messages via $this->output. If all childeren are deleted successfully, then $node_id is deleted. Success of the whole operation is indicated by returning TRUE, otherwise FALSE.

  • return: TRUE if all nodes successfully deleted, FALSE otherwise
bool delete_node (int $node_id)
  • int $node_id: the page or the section to delete
get_dialogdef_add_node (line 2646)

construct a dialog definition for adding a node (page or section)

the dialog for pages and sections are different in just a single field: the page has an extra module field.

Note that we set two default values: one for visibility and one for the default module id. For now we set the initial visitibility to 2 (hidden). The default module is 1, under the assumption that the first module in the system is the one used most: a plain page. I didn't consider it worthy enough to make this defaults configurable. However, the sort order in the get_options_modules() doesn't guarantee that the plain page module is the first in the list, so there.

  • return: with dialog definition keyed on item name
  • todo: should we make the defaults in this routine configurable? (I'm not convinced they should)
array get_dialogdef_add_node (bool $is_page)
  • bool $is_page: TRUE if the dialog is for a new page, FALSE is for a new section
get_dialogdef_edit_advanced_node (line 2825)

construct a dialog definition for editing advanced properties of a node (page or section)

this constructs a dialog to edit the advanced properties of a node. There is a slight difference between pages and sections: a section can have neither the 'target' property nor the 'href' property; that only makes sense for a page, so these input fields are not displayed for a section.

The readonly-property is a special case. Even if the parameter $viewonly is TRUE, the readonly-field is displayed as 'editable'. This is because this particular field is used to toggle the viewonly mode: if a node is readonly, it cannot be edited, except the removal of the readonly attribute.

  • return: with definition of the dialog
array get_dialogdef_edit_advanced_node (int $node_id, bool $is_page, [bool $viewonly = FALSE], int $area_id)
  • int $area_id: the area in which the node lives
  • int $node_id: the node that is to be edited
  • bool $is_page: TRUE if the dialog is for a page, FALSE is for a section
  • bool $viewonly: if TRUE, most fields are 'dimmed' (uneditable)
get_dialogdef_edit_node (line 2739)

construct a dialog definition for editing basic properties of an existing node (page or section)

the dialog for pages and sections is different in just a single field: the page has an extra module field.

Note that we return a keyed array using the name of the dialog field as a key. This makes it easier to reference an incoming field in the save routine.

  • return: with definition of the dialog keyed with fieldname
array get_dialogdef_edit_node (int $node_id, bool $is_page, [bool $viewonly = FALSE])
  • int $node_id: the node that is to be edited
  • bool $is_page: TRUE if the dialog is for a page, FALSE is for a section
  • bool $viewonly: if TRUE, all fields are 'dimmed' (uneditable) and there is no [Save] button
get_dialog_data_node (line 2958)

fill the node dialog with data from the database

this fills a node dialog with data from the database. The routine takes care of some data conversions, e.g. manipulating a boolean TRUE/FALSE so it fits in a checkbox type of widget, etc.

Note that the data is NOT specifically validated. This means that a dialog _could_ contain invalid values even when the user doesn't change anything. Or, to put it a different way: if the database contains garbage, the garbage is simply presented to the user. If the user subsequently tries to save the "garbage" the validation will catch her.

This routine is able to fill the values for both the 'basic' and the 'advanced' dialogs.

  • return: $dialogdef is filled with data from the database
void get_dialog_data_node (array &$dialogdef, int $node_id)
  • array &$dialogdef: the dialog definition
  • int $node_id: the node that needs to be edited
get_icon_delete (line 2354)

construct a clickable icon to delete this node (and underlying nodes too)

  • return: results are returned as output in $this->output
  • todo: should we display trash can icons for sections with non-empty subsections? there really is no point, because we eventually will not accept deletion of sections with grandchilderen in task_node_delete. Hmmmmm..... For now I just added the condition that access is denied when a section has grandchilderen. Need to refine this, later. Also, how about readonly nodes? Surely those cannot be deleted... should it not show in the icon?
  • uses: $WAS_SCRIPT_NAME
  • uses: $USER
  • uses: $CFG
void get_icon_delete (int $node_id)
  • int $node_id: the node to delete
get_icon_edit (line 2389)

construct a clickable icon to edit this node

  • return: results are returned as output in $this->output
  • todo: move permission check to a separate function permission_edit_node()
  • uses: $WAS_SCRIPT_NAME
  • uses: $USER
  • uses: $CFG
void get_icon_edit (int $node_id)
  • int $node_id: the node to edit
get_icon_home (line 2300)

construct a clickable icon to set the home page/section on this tree level

this constructs a clickable icon to change the default node on this level. it requires PERMISSION_NODE_EDIT_PAGE or PERMISSION_NODE_EDIT_SECTION for both the target default node AND the current default node (if any)

  • return: results are returned as output in $this->output
  • uses: $WAS_SCRIPT_NAME
  • uses: $USER
  • uses: $CFG
void get_icon_home (int $node_id)
  • int $node_id: the node of interest
get_icon_invisibility (line 2431)

construct a clickable icon to edit the advanced properties of this node

This icon has another purpose besides creating a link to the advanced properties: it also indicates wheter a node is 'invisible' or not. In this context 'invisible' means either

  • the node is under embargo until some time in the future, OR
  • the node is alreay expired some time in the past, OR
  • the node is hidden (ie. page is not visible in navigation but otherwise available).
Depending on the visibility a different icon is displayed.

  • return: results are returned as output in $this->output
  • uses: $WAS_SCRIPT_NAME
  • uses: $USER
  • uses: $CFG
void get_icon_invisibility (int $node_id)
  • int $node_id: the node to edit
get_icon_page_preview (line 2497)

construct a clickable icon to preview this node

this constructs an icon to preview the page. the user should have edit permissions OR edit content permissions, because you can see the page when you can edit it, so there's no point in preventing the preview in that case. See task_page_preview() for more information.

The preview is displayed in a separate window, either generated via a small routing in javascript or (if javascript disabled) via a target="_blank".

  • return: results are returned as output in $this->output
  • todo: if this is a public area, the user can see every page, except the expired/embargo'ed ones should we take that into account too? I'd say that is way over the top. How about pages in an intranet where the user has view privilege? Complicated. KISS: only show preview to those that can edit or edit content.
  • uses: $WAS_SCRIPT_NAME
  • uses: $USER
  • uses: $CFG
void get_icon_page_preview (int $node_id)
  • int $node_id: the node to preview
get_icon_section (line 2545)

construct a clickable icon to open/close this node

This is a toggle: if the node is closed the closed icon is shown, but the action in the A-tag is to open the icon (and vice versa).

  • return: results are returned as output in $this->output
  • uses: $WAS_SCRIPT_NAME
  • uses: $USER
  • uses: $CFG
void get_icon_section (int $node_id)
  • int $node_id: the section to open/close
get_link_node_edit (line 2589)

construct a clickable link to edit this node showing the page's title or link-text

this generates an A tag which leads to editing the properties (node == section) or content (node == page). Additional information displayed via the title attribute includes the node_id.

  • return: results are returned as output in $this->output
  • uses: $WAS_SCRIPT_NAME
  • uses: $USER
  • uses: $CFG
void get_link_node_edit (int $node_id)
  • int $node_id: the node for which to make the link
get_module_records (line 3699)

retrieve a list of all available module records

this returns a list of active module-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 module_id.

  • return: FALSE if no modules available or an array with module-records
array|bool get_module_records ()
get_node_id_and_ancestors (line 3752)

get an array with all ids of ancestors of node_id and node_id itself

note that the order of nodes is from top to bottom

  • return: an array with all ancestor ids, from top until node_id
array get_node_id_and_ancestors (int $node_id)
  • int $node_id: start at the youngest in the family
get_options_area (line 3186)

generate a list of areas for use in a dropdown list (for moving a node to another area)

this creates an array containing a list of areas to which the user is allowed to move a node. Permissions for moving a node is a combination of permissions for deleting a node from the current area, and adding a node to the target area. The current area $this->area_id is always in the list, because even if the user isn't allowed to move a node to somewhere else, she is at least allowed to leave the node in the area it currently is in. Therefore the option for the current area MUST be possible.

We sepcifically check for these permissions: PERMISSION_AREA_ADD_PAGE or PERMISSION_AREA_ADD_SECTION and not PERMISSION_NODE_ADD_PAGE or PERMISSION_NODE_ADD_SECTION because the target of the move is always the top level, and not some (sub)section.

  • return: ready for use as an options array in a listbox or radiobuttons
array get_options_area (int $node_id, bool $is_page)
  • int $node_id: the node for which we are building this picklist
  • bool $is_page: TRUE if this concerns a page, FALSE for a section
get_options_modules (line 3221)

fetch a list of available modules for inclusion on a page

this retrieves a list of modules that can be used as a list of options in a listbox or radiobuttons. Only the active modules are considered. The names of the modules that are displayed in the list are translated (retrieved from the modules language files). The list is ordered by that translated module name.

  • return: ready for use as an options array in a listbox or radiobuttons
array get_options_modules ()
get_options_parents (line 3036)

construct an options list of possible parent sections

this constructs an array suitable for a radio field or a listbox. If the user has the privilege, an option 'add to toplevel' is added too.

If $forbidden_id is not NULL, it identifies the subtree that should be excluded from the result. If it were not excluded, the user might choose a child section as the parent for a section, which would introduce endless loops or circular references. Excluding the 'own' subtree prevents that.

Note that the list is constructed using recursion: the actual work is is done in the routine get_options_parents_walk().

Also note that if $forbidden_id is not NULL, we interpret this as a request to generate a picklist of parents for that node. We make sure that we always add the current parent node to the list. This way the only option for a parent might be to keep the current one, which obviously should be one of the options.

array get_options_parents (bool $is_page, [mixed $forbidden_id = NULL])
  • bool $is_page: if TRUE check page permissions, else check section permissions
  • mixed $forbidden_id: identifies the subtree to EXclude from the results or NULL for all sections
get_options_parents_walk (line 3082)

workhorse for construction an options list of possible parent sections

This routine is called recursively in order to construct a list of possible parent sections in the same order as the main tree display (see show_tree()), but excluding the subtree starting at $forbidden_id.

The list of parents is collected in $options. This variable is passed by reference to save memory and also to keep the parents in the correct order.

Note that the options in the output array all have a parameter 'class' which can be used to detect how deep the nesting is. This can be visualised via wellchosen CSS-parameters, eg.

    option.level0 { margin-left: 0px; } option.level1 { margin-left: 20px; } option.level2 { margin-left: 40px; } ...
which provides the illusion of a tree-like structure, even in a listbox.

The current parent of node $forbidden_id is always included in the list of allowable parents because a node should be able to keep the current parent, always.

void get_options_parents_walk (array &$options, bool $is_page, int $node_id, int|null $forbidden_id)
  • array &$options: resulting array, output of this routine
  • bool $is_page: distinction between page (TRUE) or section (FALSE)
  • int $node_id: the subtree where we should start
  • int|null $forbidden_id: if not NULL the subtree to skip
get_options_sort_order (line 3133)

generate a list of siblings in a particular (sub)section used to select/change sort order via a list box

this constructs an (ordered) list of siblings of $node_id, but excluding $node_id itself. Also, an option 'sort at the top of the list' is included. This allows for selecting a sibling AFTER which $node_id should appear in the section. The special value for 'before all others' or 'at the top of the list' is 0, because that value cannot be used by a real node.

  • return: ready for use as an options array in a listbox or radiobuttons
  • uses: $USER
  • uses: $CFG
array get_options_sort_order (int $node_id)
  • int $node_id: the node for which the list of siblings must be constructed
lock_records_subtree (line 3402)

attempt to lock all node records in a subtree

this recursively walks the subtree starting at $node_id and attempts to

  • lock every node in the subtree, AND
  • write the new area_id in an auxiliary field of every node in the subtree
With at least two trips to the database (at least one for the lock and another one for writing the auxiliary field) per node, this is an expensive routine. Maybe it is possible to combine locking and writing the auxiliary field. However, in order to keep things readable I decided against that.

This routine returns FALSE if any of the nodes in the subtree could NOT be locked. If each and every node in the subtree is successfully locked, TRUE is returned.

Note that all these locks are reset/released the moment the actual move is done, by resetting both the locked_by field and the area_id field. That may hurt readability too, but less than combining lock + setting auxiliary field. See save_node_new_area_mass_move() for more information.

bool lock_records_subtree (int $node_id, int $new_area_id)
  • int $node_id: the node which we are going to move to $new_area_id
  • int $new_area_id: the area to which we want to move the subtree $node_id
message_from_lockinfo (line 3783)

construct a readable message from the lockinfo array

if an attempt to lock a record fails (see lock_record_node()), the array $lockinfo is filled with information about the user that has locked the record. The following information is available:

  • 'user_id': the numerical user_id of the user holding the lock
  • 'username': the userid of that user
  • 'full_name': the full name of that user
  • 'user_information': the IP-address from where that user is calling
  • 'ctime': the date/time that user logged in (c=create)
  • 'atime': the date/time that user last accessed the system (a=access)
  • 'ltime': the date/time that user actually locked the record (l=lock)
This routine tries to construct a more or less readable message which informs this user here about that other user holding the lock.

  • return: a message constructed from the lockinfo
string message_from_lockinfo (array $lockinfo, int $node_id,  $is_page)
  • array $lockinfo: contains information about another user that has obtained a record lock
  • int $node_id: the node that is locked
  • $is_page
module_connect (line 4076)

inform module $module_id that from now on it will be linked to page $node_id

this routine tells module $module_id that from now on it is associated with node $node_id in area $area_id.

This is done by a. loading the module's administrative interface (the admin-script file), and b. calling the function <modulename>_connect()

If something goes wrong (e.g. no module found, non-existing admin-script, undefined function <modulename>_connect()) FALSE is returned, otherwise the return value of function <modulename>_connect() is returned.

  • return: FALSE on failure, otherwise the result of <modulename>_connect()
  • todo: should we pass the area_id at all? What happens when a node is moved to another area without informing the module? Questions, questions, questions...
bool module_connect (int $area_id, int $node_id, int $module_id)
  • int $area_id: the area where $node_id resides
  • int $node_id: the node to which the module will be connected
  • int $module_id: the module that will be connected to node $node_id
module_disconnect (line 4038)

inform module $module_id that it is no longer linked to page $node_id

this routine tells module $module_id that it is no longer associated with node $node_id in area $area_id.

This is done by a. loading the module's administrative interface (the admin-script file), and b. calling the function <modulename>_disconnect()

If something goes wrong (e.g. no permissions, no module found, non-existing admin-script, undefined function <modulename>_disconnect()) FALSE is returned, otherwise the return value of function <modulename>_disconnect() is returned.

  • return: FALSE on failure, otherwise the result of <modulename>_disconnect()
  • todo: should we pass the area_id at all? What happens when a node is moved to another area without informing the module? Questions, questions, questions...
bool module_disconnect (int $area_id, int $node_id, int $module_id)
  • int $area_id: the area where $node_id resides
  • int $node_id: the node from which the module is disconnected
  • int $module_id: the module that will be disconnected from node $node_id
module_load_admin (line 4189)

load the admin interface of a module in core

this includes the 'admin'-part of a module via 'require_once()'. This routine first figures out if the admin-script file actually exists before the file is included. Also, we look at a very specific location, namely: /program/modules/<modulename>/<module_admin_script> where <modulename> is retrieved from the modules table in the database.

Note that if modulename would somehow be something like "../../../../../../etc/passwd\x00", we could be in trouble...

  • return: FALSE on error or an array with the module record from the modules table
  • todo: should we sanitise the modulename here? It is not user input, but it comes from the modules table in the database. However, if a module name would contain sequences of "../" we might be in trouble
bool|array module_load_admin (int $module_id)
  • int $module_id: indicates which module to load
module_save (line 4154)

(maybe) save the modified content of module $module_id connected to page $node_id

this saves the module data belonging to node $node_id.

If something goes wrong (e.g. no module found, non-existing admin-script, undefined function <modulename>_save()) FALSE is returned, otherwise the return value of function <modulename>_save() is returned.

  • return: FALSE on failure, otherwise the result of <modulename>_save()
bool module_save (int $node_id, int $module_id, bool $viewonly, bool &$edit_again)
  • int $node_id: the node to which the module is connected
  • int $module_id: the module that is connected to node $node_id
  • bool $viewonly: if TRUE, editing and thus saving is not allowed
  • bool &$edit_again: returns TRUE if more editing is required, FALSE otherwise
module_show_edit (line 4120)

show a dialog for editing the content of module $module_id linked to page $node_id

this loads the code for module $module_id and calls the appropriate routine for displaying a dialog

The parameter $viewonly can be used to indicate readonly access to the content. It is upto the called function to adhere to this flag, e.g. by just showing the content instead of letting the user modify it.

If the flag $edit_again is TRUE, this is not the first call to this routine, i.e. we have been here before but probably something went wrong when saving the data (e.g. en invalid date like 2008-02-31 was entered). This makes it possible to re-edit the content without starting from scratch again. If the flag is FALSE, the called routine is supposed to start with the data as it is currently stored in the database. Otherwise the current data is POST'ed by the user.

If something goes wrong (e.g. no module found, non-existing admin-script, undefined function <modulename>_show_edit()) FALSE is returned, otherwise the return value of function <modulename>_show_edit() is returned.

bool module_show_edit (int $node_id, int $module_id, bool $viewonly, bool $edit_again)
  • int $node_id: the node to which the module is connected
  • int $module_id: the module that is connected to node $node_id
  • bool $viewonly: if TRUE, editing is not allowed (but simply showing the content is allowed)
  • bool $edit_again: if TRUE, start with data from $_POST, otherwise read from database
node_full_name (line 3717)

shorthand for constructing a readable page/section name with id, name and title

  • return: constructed full name of page/section
string node_full_name (int $node_id)
  • int $node_id: get the full name of this node
node_has_grandchilderen (line 3729)

shorthand to determine whether the number of levels below section $node_id is greater than one

  • return: TRUE if section $node_id has a non-empty subsection, FALSE otherwise
bool node_has_grandchilderen (int $node_id)
  • int $node_id: the section to check for grandchilderen
permission_add_any_node (line 3464)

does the user have the privilege to add a node, any node to an area?

this routine returns TRUE if the current user has permission to add at least one node to the current area. This information is used to show or suppress the 'add a page' and 'add a section' links.

Note that pages and sections are treated separately; if a user is allowed to add a page it doesn't necessarily mean that she is allowed to add a section too.

Strategy: we first check the area-level (and implicit site-level) permissions to add a node, anywhere in an area including at the toplevel. If that doesn't work, we check for permissions to add a node to an existing section at the node level (and implicit at the area and site level too). If that doesn't work, we return FALSE.

Note that it is enough to stop the search at the first hit: we need only 1 hit for 'any', not all of them.

  • return: TRUE if user can at least add 1 section/page, FALSE otherwise
  • uses: $USER
bool permission_add_any_node (bool $is_page)
  • bool $is_page: selects either page or section permissions
permission_add_node (line 3496)

does the user have the privilege to add a node the area or a section?

this checks for permission to add a page or a section to the area at the toplevel or to the section $node_id. If access is denied initially, the upper sections are tested for the requested permission. I dubbed this cascading permissions (if a section allows for adding a page, any subsections inherit that permission). This routine is protected from endless loops by recursing at most MAXIMUM_ITERATIONS levels.

  • return: TRUE if user can add section/page, FALSE otherwise
  • uses: $USER
bool permission_add_node (int $section_id, bool $is_page)
  • int $section_id: is the section to examine or 0 for the area top level
  • bool $is_page: selects either page or section permissions
permission_delete_node (line 3635)

does the user have the privilege to delete a node from the area?

  • return: TRUE if user can delete node, FALSE otherwise
  • todo: we should also take the readonly flag into account (or should we?) when determining delete permissions
  • uses: $USER
bool permission_delete_node (int $node_id,  $is_page)
  • int $node_id: is the node to delete
  • $is_page
permission_edit_node (line 3561)

does the user have the privilege to edit node properties?

this checks the edit permissions for the specified node. If none are found initially, we check out the permissions of the parent section. If the user allowed to add a page/section in the parent section, we assume or imply that the user also has edit permissions if she also has edit permissions for the parent. even though the exact permission bits are not set for this particular new node. IOW: if a user is able to add a page/section it would be illogical not to be able to edit the new page/section. However, if the edit-permissions are not area-wide, there is no way you can add permissions to a particular node before it exists. (Can't do that in the account manager).

Note that a node can also have the readonly attribute set. This is more or less a tool to prevent accidental changes to a node's properties: a user can easy reset the readonly flag and change the node anyway. However, it requires two steps and hence at least _some_ thinking. Bottom line: we only look at the 'real' permissions here, and not the readonly flag. (Even better: edit privilege is required to reset the readonly flag so using that flag as extra permission would yield pages completely uneditable).

This routine is also used to check for content edit permissions. This is only possible for pages (not sections). By default this routine checks the regular permissions (edit properties/ edit advanced properties).

  • return: TRUE if user can edit node, FALSE otherwise
  • uses: $USER
bool permission_edit_node (int $node_id, bool $is_page, [bool $check_content = FALSE])
  • int $node_id: is the node to examine
  • bool $is_page: TRUE means we look at page permissions, not section
  • bool $check_content: TRUE means check edit content, else edit plain
permission_edit_node_content (line 3599)

does the user have the privilege to edit node content?

this is a wrapper around routine permission_edit_node(). We force is_page and check_content to TRUE.

  • return: TRUE if user can edit node content, FALSE otherwise
bool permission_edit_node_content (int $node_id)
  • int $node_id: is the node to examine
permission_set_default (line 3614)

does the user have the privilege to make node $node_id the default?

if a user has edit permission for the new default node and also in the existing default node (if any), the user is allowed to set the default to node $node_id. Note that once again we use cascading permissions. (See also permission_edit_node()).

  • return: TRUE if user has enough permissions, otherwise FALSE
bool permission_set_default (bool $node_id)
  • bool $node_id: is the tentative new default
queue_area_node_alert (line 3309)

add a message to message queue of 0 or more alerts

this adds $alert_message to the message buffers of 0 or more alert accounts The alerts that qualify to receive this addition via the alerts_areas_nodes table. The logic in that table is as follows:

  • the area_id must match the area_id(s) (specified in $areas) OR it must be 0 which acts as a wildcard for ALL areas
  • the node_id must match the node_id(s) specified in $nodes) OR it must be 0 which acts as a wildcard for ALL nodes
Also the account must be active and the flag for the area/node-combination must be TRUE.

As a rule this routine is called with a single area_id in $areas and a collection of node_id's in $nodes. The nodes follow the path up through the tree, in order to alert accounts that are only watching a section at a higher level.

Example: If user 'webmaster' adds new page, say 34, to subsection 8 in section 4 in area 1, you get something like this:

queue_area_node_alert(1,array(8,4,34),'node 34 added','webmaster');

The effect will be that all accounts with the following combinations of area A and node N have the message added to their buffers: A=0, N=1 - qualifies for all nodes in all areas A=1, N=0 - qualifies for all nodes in area 1 A=1, N=4 - qualifies for node 4 in area 1 A=1, N=8 - qualifies for node 8 in area 1

It is very well possible that no message is added at all if there is no alert account watching the specified area and node (using wildcards or otherwise).

Near the end of this routine, we check the queue with pending messages, and perhaps send out a few alerts. The number of messages that can be sent from here is limited; we don't want to steal too much time from an unsuspecting user. It is the task of cron.php to take care of eventually sending the queued messages. However, sending only a few messages won't be noticed. I hope.

Note that this routine adds a timestamp to the message and, if it is specified, the name of the user.

Also note that the messages are added to the buffer with the last message at the top, it means that the receiver will travel back in time reading the collection of messages. This is based on the assumption that the latest messages sometimes override a previous message and therefore should be read first.

  • uses: $DB;
void queue_area_node_alert (mixed $areas, mixed $nodes,  $alert_message, [string $username = ''], string $message)
  • mixed $areas: an array or a single int identifying the area(s) of interest
  • mixed $nodes: an array or a single int identifying the node(s) of interest
  • string $message: the message to add to the buffer of qualifying alert accounts
  • string $username: (optional) the name of the user that initiated the action
  • $alert_message
save_node (line 1900)

workhorse routing for saving modified node data to the database

this is the 'meat' in saving the modified node data. There are a lot of complicated things we need to take care of, including dealing with the readonly property (if a node is currently readonly, nothing should be changed whatsoever, except removing the readonly attribute) and with moving a non-empty section to another area. Especially the latter is not trivial to do, therefore it is being done in a separate routine (see save_node_new_area_mass_move()).

Note that we need to return the user to the edit dialog if the data entered is somehow incorrect. If everything is OK, we simply display the treeview and the area menu, as usual.

Another complication is dealing with a changed module. If the user decides to change the module, we need to inform the old module that it is no longer connected to this page and is effectively 'deleted'. Subsequently we have to tell the new module that it is in fact now added to this node. It is up to the module's code to deal with these removals and additions (for some modules it could boil down to a no-op).

Finally there is a complication with parent nodes and sort order. The sort order is specified by the user via selecting the node AFTER which this node should be positioned. However, this list of nodes is created based on the OLD parent of the node. If the node is moved to elsewhere in the tree, sorting after a node in another branch no longer makes sense. Therefore, if both the parent and the sort order are changed, the parent prevails (and the sort order information is discarded).

  • return: results are returned as output in $this->output
  • todo: this routine could be improved by refactoring it; it is too long!
  • todo: there is something wrong with embargo: should we check starting at parent or at node? this is not clear: it depends on basic/advanced and whether the embargo field changed. mmmm... safe choice: start at node_id for the time being
void save_node (int $node_id)
  • int $node_id: the node we have to change
save_node_new_area_mass_move (line 2190)

workhorse routine for moving a complete subtree to another area

this routine moves a subtree starting at section $node_id from area $this->area_id to area $new_area_id. This is a complicated operation because

  • the subtree may be (very) large
  • nodes in the subtree may be locked by other users
  • we MUST take an all or nothing approach because either ALL of the nodes or NONE of the nodes in the subtree must change the area_id. If area_id's are only changed partly, we will end up with orphan nodes because areas differ between a parent and corresponding offspring.
Here is my train of thoughts leading to my implementation.

The best solution I can think of is to:

  • lock all individual nodes in the subtree, and if successful,
  • update all these records with the appropriate area_id and at the same time unlocking these records by writing a NULL to the lock field.
This way we postpone the actual move to the new area until we are certain that we have all nodes involved in our hands. If we don't succeed in obtaining all the necessary locks, we have to abandon the operation, accept defeat, release all locks and return FALSE to our caller. If we do succeed, well, we indicate success by returning TRUE.

Mmmm....

Note that the user might have two browser windows open in the same session. This shouldn't happen, but there is no easy way to prevent the user to open more windows in the same session. This may lead to an undesirable result: if the user is editing another node in the same session in another window (totally unrelated to the move of the current subtree), that node might also be moved to the new area, introducing an orphan in the new area. Mmmmm. The best way to handle that problem is to use a special helper field, say auxilary_id, in the nodes table. That field could be used as follows (pseudo-code):

set auxiliary_id of $node_id to $new_area_id for all descendants of section $node_id do obtain lock on descendant set auxiliary_id to $new_area_id update nodes set area_id = new_area_id, auxiliary_id = NULL, locked_by = NULL where auxiliary_id = new_area_id AND locked_by = our_session_id Then we once again concentrate the actual work in a single UPDATE-statement.

It is a costly operation: at least 2 trips to the database per descendant.

Mmmmm...

Perhaps we can save (a lot) of trips to the database if we build on the assumption that usually there are more childeren in every section AND that usually the childeren are NOT locked. In that case the pseudo-code becomes:

for all descendants of section $node_id do if is_section($descendant) then SET auxiliary_id = $new_area_id, locked_by = $our_session_id WHERE locked_by IS NULL AND parent_id = $descendant; endif endfor SET area_id = new_area_id, auxiliary_id = NULL, locked_by = NULL WHERE auxiliary_id = new_area_id AND locked_by = $our_session_id

However, we might miss a descendant or two if it happens to be locked (by us, or by another session). That's no good.

Mmmmm...

I'm sure there's a better way, but for the time being I'll simply use brute force and my way through the subtree. If this really becomes a huge problem, we may want to refactor this routine.

bool save_node_new_area_mass_move (int $node_id, int $new_area_id, bool $embargo)
  • int $node_id: the node which we are going to move to $new_area_id
  • int $new_area_id: the area to which we want to move the subtree $node_id
  • bool $embargo: if TRUE, we cannot send alerts because the original tree is under embargo
section_is_open (line 3675)

shorthand for determing whether a section is opened or closed

  • return: TRUE if the node is 'open', FALSE otherwise
bool section_is_open (int $section_id)
  • int $section_id: the section of interest
show_area_menu (line 1311)

construct a clickable list of available areas for the current user

this iterates through all available areas in the areas table, and constructs a list of areas (as LI's in a UL) for which the current user has either administrative or view permissions. The latter shows in 'dimmed' form, because it is not allowed to view this area in pagemanager, but the area does exist and is available to the user (as a visitor rather than an administrator) so it should not be suppressed. If a user has neither view or admin permission, the area is suppressed. Note that every user has at least view permissions for a public area.

The current area is determined by parameter $current_area_id. This area gets the attribute 'class="current"' which makes it possible to emphasise the current working area in the menu (via CSS).

  • return: results are returned as output in $this->output
void show_area_menu ([int|null $current_area_id = NULL])
  • int|null $current_area_id: the current area
show_dialog_delete_node_confirm (line 1715)

display a list of 1 or more nodes to delete and ask user for confirmation of delete

this displays a confirmation question with a list of nodes that will be deleted. This list is either a single page or a single (empty) section OR a section with childeren (but not grandchilderen). See function task_node_delete() for more on this design decision. If the user presses Delete button, the nodes will be deleted, if the user presses Cancel then nothing is deleted.

  • return: results are returned as output in $this->output
void show_dialog_delete_node_confirm (int $node_id)
  • int $node_id: the page or the section to delete
show_edit_menu (line 1366)

construct a clickable list of edit variants (basic, advanced and maybe content)

this constructs a menu from where the user can navigate to edit basic properties of a node, advanced properties or even the content (for pages).

  • return: results are returned as output in $this->output
  • uses: $WAS_SCRIPT_NAME
  • uses: $USER
  • uses: $CFG
void show_edit_menu (int $node_id, [bool $is_page = FALSE], [int $current_option = NULL])
  • int $node_id: the current node (the node being edited)
  • bool $is_page: if TRUE display the link to edit content too (this is for pages only)
  • int $current_option: the currently selected edit mode (basic, advanced or content)
show_tree (line 1442)

create a tree-like list of nodes in the content area of $this->output

this constructs a tree-like view of the current area, with

  • a title
  • 0, 1 or 2 links to add a node
  • 0, 1 or 2 links to select a different tree view
  • all nodes that are currently show-able (depending on tree view mode)
If the tree is empty, only the links to add a node are displayed (if the user has permission to add). The individual nodes are displayed using recursion with show_tree_walk().

Note that the tree is constructed via nested UL's with LI's, all in name of 'graceful degradation': this interface still works if this program has no stylesheet whatsoever).

void show_tree ()
show_treeview_buttons (line 1632)

show one or two clickable links to change the view of the tree

There are three different tree views:

  • minimal: all sections are closed, only the top level nodes are shown
  • custom: 1 or more sections are closed and 1 or more sections are opened
  • maximal: all sections are opened, all nodes are shown
There is a fourth option:
  • none: there are no sections at all
The view can be set to either TREE_VIEW_MINIMAL, TREE_VIEW_CUSTOM or TREE_VIEW_MAXIMAL. The current setting is remembered in session variable 'tree_mode'. A list of customised nodes is kept in session variable expanded_nodes[], an array keyed with the node number an a value of either TRUE (section is 'open') or FALSE (section is 'closed'). An empty array implies all nodes are closed (ie. default value is FALSE).

In some cases TREE_VIEW_CUSTOM is equivalent to one of the other two, e.g. when the user closes the last section, the effect looks exactly like TREE_VIEW_MINIMAL. If the user manually opens all sections, the effect is the same as TREE_VIEW_MAXIMAL.

In this routine we want to show 0, 1 or 2 buttons that allow the user to switch to another viewmode, but only if the new mode(s) are different from the current one.

The equivalency between modes can be determined by counting the number of open and closed sections. Here is a truth table.

| N | open | closed | description
+---+------+--------+------------
| 0 |   0  |   0    | no sections at all, show 0 buttons (al modes are equivalent)
| 1 |   0  |  >=1   | all sections are closed, 'custom' is equivalent with 'minimal'
| 2 |  >=1 |   0    | all sections are opened, 'custom' is equivalent with 'maximal'
| 3 |  >=1 |  >=1   | some open, some closed, 'custom' is distinct from the other two modes

Case N=0 In this case there are no sections at all, so there is no point to show any button at all because all views are equivalent: all available pages (if any) live at the top level and they are always visible.

Case N=1 In this case 'minimal' and 'custom' are equivalent. That means that if the current view is either 'minimal' or 'custom', the only viable option would be to set the view to 'maximal'. If the current mode is 'maximal', the only viable option is 'minimal'. Only 1 toggle-like button needs to be displayed.

Case N=2 In this case 'custom' and 'maximal' are equivalent. That means that if the current view is either 'custom' or 'maximal', the only viable option would be to set the view to 'minimal'. If the current mode is 'minimal', the only viable option is 'maximal'. Only 1 toggle-like button needs to be displayed.

Case N=3 In this case 'custom' is a distinct mode somewhere between 'minimal' and 'maximal'. This means that there are always two other options to choose from: if current mode is 'minimal' the choices are 'custom' and 'maximal', if current mode is 'custom' the choices are 'maximal' and 'minimal', if current mode is 'maximal' the choices are 'minimal' and 'custom'. This means that two buttons need to be displayed.

Strategy: First we step through the tree and we count the 'open' and 'closed' sections. After that we determine whether N is 0,1,2 or 3 (see truthtable). After that we calculate which of the three buttons need to be displayed, depending on the current mode (obtained via the session variable 'tree_mode'). Subsequently the buttons are output to the 'content' area via $this->output.

  • return: results are returned as output in $this->output
  • uses: $WAS_SCRIPT_NAME
void show_treeview_buttons ()
show_tree_walk (line 1517)

display the specified node, optionally all subtrees, and subsequently all siblings

this routine displays the specified node, including clickable icons for setting the default, editing the node etc. from the current tree. After that, any subtrees of this node are displayed using recursion (but only if the section is 'opened'). This continues for all siblings of the specified node until there are no more (indicated by a sibling_id equal to zero).

void show_tree_walk (int $node_id, [string $m = ''])
  • int $node_id: the first node of this tree level to show
  • string $m: left margin for increased readability
task_node_add (line 516)

display a dialog to add a new page or section to the current area

this displays a dialog where the user can add a node to the current area. If the user has no permissions to add a node at all, the result is an error message and the tree view

The value of $task (which can be either TASK_ADD_PAGE or TASK_ADD_SECTION) determines which dialog to show.

Both dialogs are very similar (a page can have a module, a section cannot). The actual dialog is constructed based on a dialogdef, see the function get_dialogdef_add_node().

  • return: results are returned as output in $this->output
  • uses: $USER
void task_node_add (string $task)
  • string $task: identifies whether a page or a section should be added
task_node_delete (line 592)

delete one or more nodes from an area after user confirmation

this deals with deleting nodes from an area. There are two stages. Stage 1 is presenting the user with a list of selected nodes and offering the user the choice to confirm the delete or cancel the operation. Stage 2 is actually deleting the selected nodes (after the user confirmed the delete in stage 1), including the disconnection of pages and modules.

An important design decision was to limit the delete process to at most 1 tree level. This means the following. If a user attempts to delete a page, it is easy: after confirmation a single node is deleted from the database. If a user attempts to delete a section, it can be different. If the section is empty, i.e. there are no childeren, it is the same as deleting a page: only a single node record has to be deleted.

It becomes more dangerous if a section is filled, ie. has childeren. If all childeren are pages (or empty subsections), it is still relatively innocent because the worst case is that all pages in a section are deleted. If, however, the section contains subsections which in turn contain subsubsections, etc. the delete operation may become a little too powerful. If it would work that way (deleting a section implies _all_ nodes in the subtree), it is possible to delete a complete area in only a few keystrokes, no matter how many levels.

In order to prevent this mass deletion, we decided to limit the delete operation to at most a single level. In other words: the user can delete

  • a single page
  • a single empty section
  • a section with childeren but no grandchilderen
If a user attempts to delete a section with childeren and grandchilderen, an error message is displayed and nothing is deleted.

This forces the user to delete a complete tree a section at the time, hopefully preventing a 'oh no! what have I done' user experience.

We _always_ want the user to confirm the deletion of a node, even if it is just a single page.

Note that a page that is readonly will not be deleted.

  • return: results are returned as output in $this->output
  • todo: should we display trash can icons for sections with non-empty subsections in treeview? there really is no point, because we eventually will not accept deletion of sections with grandchilderen. Hmmmmm.....
  • uses: $USER
void task_node_delete ()
task_node_edit (line 677)

display a dialog where the user can edit basic or advanced properties of a node

this constructs a dialog and a menu where the user can edit the properties of a node. We check the user's permissions and if that works out we try to obtain a lock on the record. If that succeeds, we show the dialog (in funnel mode). If we don't get the lock, we inform the user about the other user who holds the lock. In case of error (e.g. no permissions or no lock) we fall back on displaying the area menu and the treeview.

Note: the lock is released once the user saves the node OR cancels the edit operation.

  • return: results are returned as output in $this->output
  • uses: $USER
void task_node_edit (string $task)
  • string $task: identifies whether the basic of advanced properties should be edited
task_node_edit_content (line 753)

display a dialog where the user can edit the contents of a node via a module

this effectively loads the module code associated with the specified node and subsequently calls the corresponding code in the module to display an edit dialog.

Just like the other edit routine (see task_node_edit()) the node is locked first. Also the user permissions are checked. If we don't get the lock, we inform the user about the other user who holds the lock. In case of error (e.g. no permissions or no lock or an error loading the module) we fall back on displaying the area menu and the treeview. In that process the lock may be released.

void task_node_edit_content ()
task_page_preview (line 882)

preview a page that is maybe still under embargo/already expired

if the user has permissions to preview the specified page, she is redirected to the regular site with a special one-time permission to view a page, even if that page is under embargo or already expired (which normally would prevent any user from viewing that page).

There are several ways to implement such a one-off permit, e.g. by setting a quasi-random string in the session and specifying that string as a parameter to index.php. If (in index.php) the string provided matches ths string in the session, the user is granted access. However, this leaves room for the user to manually change the node id to _any_ number, even a node that that user is not supposed to see.

Another solution might have been to simply include index.php. I decided against that; I don't want to have to deal with a mix of admin.php and index.php-code in the same run.

I took a slightly different approach, as follows. First I generate a quasi-random string of N (N=32) characters. (The lenght of 32 is an arbitrary choice.) This string is stored in the session variable. Then I store the requested node in the session variable, too. After that I calculate the md5sum of the combination of the random string and the node id. This yields a hash. This hash is passed on to index.php as the sole parameter.

Note that the quasi-random key never leaves the server: it is only stored in the session variables. Also, the node id is not one of the parameters of index.php, this too is only stored in the session variables.

Once index.php is processed, the specified md5sum is retrieved and a check is performed on the node id and the quasi-random string in the session variables in order to see if the hashes match. If this is the case, index.php can proceed to show the page preview. Note that there is no way for the user to manipulate the node id, because that number never travels to the user's browser in plain text.

Making a bookmark for the preview will use the hash, but the hash depends on a quasi-random string stored in the session. It means that when the session is terminated, the bookmarked page will no longer be visible, which is good. Also, whenever another page preview is requested, a new quasi-random string is generated, which also invalidates the bookmarked page.

The only thing that CAN happen is that the user saves the preview in a place where it can be seen by others. Also, the page will probably be cached in the user's browser.

With respect to permissions: I consider the preview privilege equivalent with edit permission: if the user is able to edit the node she can see the content of the node anyway. However, maybe we should look at different permissions. Put it on the todo-list.

  • return: results are returned as output in $this->output
  • todo: the check on permissions can be improved (is PERMISSION_XXXX_EDIT_NODE enough?)
  • todo: there is an issue with redirecting to another site: officially the url should be fully qualified (ie. $CFG->www). I use the shorthand, possibly without scheme and hostname ($CFG->www_short). This might pose a problem with picky browsers. See calculate_uri_shortcuts for more information.
  • uses: $USER
  • uses: $CFG
void task_page_preview ()
task_save_content (line 1200)
void task_save_content ()
task_save_newnode (line 986)

save a newly added node to the database

this validate and save the (minimal) data for a new node (section or page). First we check which button press brought us here; Cancel means we're done, else we need to validate the user input. This is done by setting up the same dialog structure as we did when presenting the user with a dialog in the first place. This ensures that WE determine which fields we need to look for in the _POST data. (If we simply were to look for fieldnames in the _POST array, we might be tricked in accepting random fieldnames. By starting from the dialog structure we make sure that we only look at fields that are part of the dialog; any other fields are ignored, minimising the risks of the user trying to trick us.)

The dialog structure is filled with the data POST'ed by the user and subsequently the data is validated against the rules in the dialog structure (eg. min length, min/max numerical values, etc). If one or more fields fail the tests, we redo the dialog, using the data from _POST as a new starting point. This makes that the user doesn't lose all other field contents if she makes a minor mistake in entering data for one field.

If all data from the dialog appears to be valid, it is copied to an array that will be used to actually insert a new record into the nodes table. This array also holds various other fields (not part of the dialog) with sensible default values. Interesting 'special' fields are 'sort_order' and 'is_hidden' and 'embargo'.

'sort_order' is calculated automatically from other sort orders in the same parent section. There are two ways to do it: always add a node at the end or the exact opposite: always add a node at the beginning. The jury is still out on which of the two is the best choice (see comments in the code below).

'is_hidden' and 'embargo' are calculated from the dialog field 'node_visibility'. The latter gives the user three options: 'visible', 'hidden' and 'embargo'. This translates to the following values for 'is_hidden' and 'embargo' (note that $now is the current time in the form 'yyyy-mm-dd hh:mm:ss'):

visible: is_hidden = FALSE, 'embargo' = $now hidden: is_hidden = TRUE, 'embargo' = $now embargo: is_hidden = TRUE, 'embargo' = '9999-12-31 23:59:59'

This makes sure that IF the user wants to create a 'secret' node, ie. under embargo until some time in the future, the new node is never visible until the user edits the node to make it visible. However, there is no need to manually add a date/time: we simply plug in the maximum value for a date/time, which effectively means 'forever'.

Finally, if the new node is saved, a message about this event is recorded in the logfile (even for new nodes under embargo). Also, if the node is NOT under embargo, an alert message is queued. Note that we do NOT send alerts on a page that is created under embargo. (There is a slight problem with this: once a user edits the node and sets the embargo to a more realistic value, e.g. next week, there is no practical way to inform the 'alert-watchers' about that fact: we cannot send an alert at the time that the embargo date is changed to 'next week' because the node is still under embargo. We don't have a handy opportunity to send alerts because the embargo date will eventually come around and the node will become visible automatically, without anyone being alerted to the fact. Mmmm....

  • return: results are returned as output in $this->output
  • todo: about 'sort_order': do we insert nodes at the end or the beginning of a parent section?
  • todo: how do we alert users that an embargo date has come around? Do we schedule alerts via cron?
void task_save_newnode (string $task)
  • string $task: disinguishes between saving a page or a section
task_save_node (line 1111)
void task_save_node ()
task_set_default (line 420)

make the selected node the default for this level

this sets a default node. First we make sure we have a valid environment and a node that belongs to the current area Then we check permissions and if the user is allowed to

  • set the default bit on the target node, AND
  • reset the default bit on the current default node
we actually
  • reset the default bit from the current default (if there is one), AND
  • set the default bit for the selected node.
Note: if the user sets the default node on the current default node, the default is reset and subsequently set again (two trips to the database), This also updates the mtime of the record.

  • return: results are returned as output in $this->output
  • uses: $USER
void task_set_default ()
task_subtree_collapse (line 368)

close the selected section and perhaps change the view mode

this closes the selected node, i.e. fold in the subtree starting at the selected node. This should only happen when the view mode is either maximal (all sections closed) or custom (some sections opened and some sections closed). It should never happen when mode is minimal.

The status of a node (opened or closed) is remembered in session variable 'expanded_nodes': an array keyed with node_id If the corresponding value is TRUE, the section is considered open, all other values (FALSE or element is non-existing) equate to closed. See also task_subtree_expand().

If the current mode is 'maximal', all sections are showed 'open'. When one of the sections is closed (via this routine), we change the mode to 'custom'. However, because the previous state was 'all sections are opened', we need to remember all the sections in the session variable 'expanded_nodes' and set them all to TRUE except the section that needs to be closed. We do this by constructing the complete tree of the area and adding an entry for every section and setting the value to TRUE, except the node that needs to be closed.

  • return: results are returned as output in $this->output
  • uses: $USER
void task_subtree_collapse ()
task_subtree_expand (line 317)

open the selected section and perhaps change the view mode

this opens the selected node, i.e. unfold 1 level of the subtree starting at the selected node. This should only happen when the view mode is either minimal (all sections closed) or custom (some sections opened and some sections closed). It should never happen when mode is maximal.

The status of a node (opened or closed) is remembered in session variable 'expanded_nodes': an array keyed with node_id If the corresponding value is TRUE, the section is considered open, all other values (FALSE or element is non-existing) equate to closed. See also task_subtree_collapse().

  • return: results are returned as output in $this->output
  • uses: $USER
void task_subtree_expand ()
task_treeview (line 247)

maybe change the current area and then show the tree and the menu for the current area

this routine switches to a new area if one is specified and subsequently displays the tree of the new area or the existing current area.

  • return: results are returned as output in $this->output
void task_treeview ()
task_treeview_set (line 283)

this sets the tree view to the specified mode

this is a simple routine to set the current view to one of the three possible views. The problem that sometimes 'custom' yields a view identical with 'maximal' or 'minimal' is dealt with when constructing the links to this routine task_treeview_set(). See show_treeview_buttons() for more information.

  • return: results are returned as output in $this->output
void task_treeview_set ()

Documentation generated on Wed, 11 May 2011 23:45:33 +0200 by phpDocumentor 1.4.0