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 83)


	
			
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)
 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 &$modules)
 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, string $option)
 bool module_show_edit (int $node_id, int $module_id, bool $viewonly, bool $edit_again, string $option)
 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 save_node (int $node_id, bool $done)
 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, array $dialogdef)
 output show_dialog_force_unlock (int $node_id, array $lockinfo)
 void show_edit_menu (int $node_id, [bool $is_page = FALSE], [string $current_option = NULL], [string $current_submenu = NULL])
 void show_tree ()
 void show_tree_walk (int $node_id, array &$modules, [string $m = ''])
 void task_node_add (string $task)
 void task_node_edit (string $task, int $node_id)
 void task_save_newnode (string $task)
 void task_save_node ()
 void task_treeview ()
Variables
null|array $areas = NULL (line 88)
  • var: $areas holds all area records (for future reference) or NULL if not yet set
null|int $area_id = NULL (line 94)
  • var: $area_id indicates which tree is stored in $this-tree, or NULL if none yet
object|null $output = NULL (line 85)
  • var: $output collects the html output
null|array $tree = NULL (line 91)
  • var: $tree holds the complete tree for area $this->area_id or NULL if not yet set
Methods
Constructor PageManager (line 119)

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.

Note that it IS possible to perform a change of area, by specifying the new area_id in the command line. If this is the case, the request will not look at the 'old' area_id (from $_SESSION) but start from scratch in the 'new' area.

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

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 3907)

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 configuration flag $CFG->pagemanager_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.

  • return: the sort order for the new node AND maybe changed sort orders amongst the childeren of $parent_id
  • uses: $DB
  • uses: $CFG
int calculate_new_sort_order (array &$tree, int $area_id, int $parent_id)
  • 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)
calculate_updated_sort_order (line 3997)

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
  • todo: Clean up this code, it is very hairy
  • 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 4080)

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 2038)

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 2915)

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 3098)

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 3010)

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_dialogdef_force_unlock (line 4157)

construct a dialog definition to show [OK] and [Cancel]

this constructs the dialogdef to show two buttons AND protect against CSRF.

  • return: ready-to-use definition of dialog
array get_dialogdef_force_unlock ()
get_dialog_data_node (line 3221)

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 2636)

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: $CFG
void get_icon_delete (int $node_id)
  • int $node_id: the node to delete
get_icon_edit (line 2664)

construct a clickable icon to edit this node

  • return: results are returned as output in $this->output
  • uses: $WAS_SCRIPT_NAME
  • uses: $CFG
void get_icon_edit (int $node_id)
  • int $node_id: the node to edit
get_icon_home (line 2593)

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: $CFG
void get_icon_home (int $node_id)
  • int $node_id: the node of interest
get_icon_invisibility (line 2700)

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: $CFG
void get_icon_invisibility (int $node_id)
  • int $node_id: the node to edit
get_icon_page_preview (line 2761)

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: $CFG
void get_icon_page_preview (int $node_id)
  • int $node_id: the node to preview
get_icon_section (line 2804)

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: $CFG
void get_icon_section (int $node_id)
  • int $node_id: the section to open/close
get_link_node_edit (line 2842)

construct a clickable link to edit this page OR open/close this section

this generates an A tag which leads to editing the content of the node (node == page) OR opens/closes the section (node == section). Additional information displayed via the title attribute includes the node_id.

Note that a user is always allowed to open/close a section so in case of a section $user_has_permission is always TRUE.

  • return: results are returned as output in $this->output
  • uses: $WAS_SCRIPT_NAME
  • uses: $CFG
void get_link_node_edit (int $node_id, array &$modules)
  • int $node_id: the node for which to make the link
  • array &$modules: translates module_ids to readable text
get_node_id_and_ancestors (line 3840)

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 3445)

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 3480)

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 3296)

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 3342)

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 3392)

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: $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 3536)

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 3871)

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 4231)

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 4192)

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 4378)

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 4330)

(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, string $option)
  • 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
  • string $option: the selected submenu option or NULL if 'Content' option was selected
module_show_edit (line 4277)

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, string $option)
  • 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
  • string $option: the selected submenu option or NULL if 'Content' option was selected
node_full_name (line 3805)

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 3817)

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 3598)

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 3630)

does the user have the privilege to add a node to an 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 $section_id. If access is denied initially, the ancestors 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 iterating 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
  • bool $is_page: selects either page or section permissions
permission_delete_node (line 3751)

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

Note: Top-level nodes are a special case: a user needs to have area-wide permission to drop such a node. However, child-nodes can be deleted based on the (cascading) permissions of a top-level node.

  • return: TRUE if user can delete node, FALSE otherwise
  • 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 3680)

does the user have the privilege to edit node properties?

this checks the edit permissions for the specified node and the node's ancestors. If none are found initially, we check out the add permissions at the parent and parent's ancestors.

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 3712)

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 3727)

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
save_node (line 2169)

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).

Update 2014-04-29: we now distinguish between 'Save' and 'Done': in the latter case we show the tree once the data is saved, in the former we redo the edit dialog again. This allows for frequent saving without 'losing' the page you are editing (and having to look it up again in the tree).

  • 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, bool $done)
  • int $node_id: the node we have to change
  • bool $done: TRUE means return to treeview, FALSE is stay in edit mode (and keep lock!)
save_node_new_area_mass_move (line 2484)

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 3785)

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 1532)

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 1988)

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, array $dialogdef)
  • int $node_id: the page or the section to delete
  • array $dialogdef: defines the confirmation dialog
show_dialog_force_unlock (line 4117)

show a dialog to the user offering to forcefully unlock a node

  • return: written to $this->output
output show_dialog_force_unlock (int $node_id, array $lockinfo)
  • int $node_id: the locked node we want to grab
  • array $lockinfo: has information about the current locker
show_edit_menu (line 1597)

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).

Update 2014-05-05. Depending on the module there may be additional submenu items to show beneath the Content menu entry. These are defined via the record in the modules table. Note that this routine expects translations of the submenu items in the domain of the module. These translation keys are:

    &lt;option&gt;_title &lt;option&gt;_anchor
and they should be defined in the module's translations.

  • return: results are returned as output in $this->output
  • uses: $WAS_SCRIPT_NAME
  • uses: $CFG
void show_edit_menu (int $node_id, [bool $is_page = FALSE], [string $current_option = NULL], [string $current_submenu = 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)
  • string $current_option: the currently selected edit mode (basic, advanced or content)
  • string $current_submenu: currently selected content submenu option or NULL for just content
show_tree (line 1705)

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 1904)

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 1789)

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, array &$modules, [string $m = ''])
  • int $node_id: the first node of this tree level to show
  • array &$modules: translates module_ids to readable text
  • string $m: left margin for increased readability
task_force_unlock (line 1445)

forcefully obtain a lock on a node and release it immediately

this routine attempts to steal the lock on node $node_id if this node is locked by our user_id (but in another session) and releases it immediately. The effect is that the node is forcefully unlocked. This deals with the annoying problem of a crashed browser and a node that remains locked until that dead session times out. The safety precautions are that we can only grab a session that is currently in posession of the same user_id. IOW: it is not possible to unlock another user's lock.

  • return: node unlocked and perhaps feedback messages written to this->$output
void task_force_unlock ()
task_node_add (line 560)

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
void task_node_add (string $task)
  • string $task: identifies whether a page or a section should be added
task_node_delete (line 635)

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.....
void task_node_delete ()
task_node_edit (line 751)

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
void task_node_edit (string $task, int $node_id)
  • string $task: identifies whether the basic of advanced properties should be edited
  • int $node_id: identifies the node to edit
task_node_edit_content (line 845)

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.

Update 2014-05-05. There were two changes in the module interface, see also . One was the addition of a a [Done]-button and the changed meaning of the [Save] button. The other is about extra options in the edit menu. For pages it used to be Basic, Advanced or Content. We now have a way to add submenu options to the Content menu option. This is done via a comma delimited list of possible options for a module. This list is stored in the new options field in the modules table. If the list is empty there are no additional submenu options to show (in show_edit_menu() or to use. If there is at least one, the options are added to the edit menu and also conveyed via the command line (in PARAM_SUBMENU_OPTION). So, the effect is that there are now multiple admin edit screens per module, each with their own PARAM_SUBMENU_OPTION. It is up to the module to use that information and display the correct edit screen (and perform the correct save routine).

void task_node_edit_content ()
task_page_preview (line 979)

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: $CFG
void task_page_preview ()
task_save_content (line 1323)
void task_save_content ()
task_save_newnode (line 1082)

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 1211)
void task_save_node ()
task_set_default (line 465)

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 413)

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 362)

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 292)

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 328)

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 Tue, 28 Jun 2016 19:11:06 +0200 by phpDocumentor 1.4.0