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)
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.
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.
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.
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...)
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
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.
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.
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.
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.
construct a dialog definition to show [OK] and [Cancel]
this constructs the dialogdef to show two buttons AND protect against CSRF.
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.
construct a clickable icon to delete this node (and underlying nodes too)
construct a clickable icon to edit this node
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)
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
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".
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).
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.
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
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.
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.
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.
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.
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.
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.
attempt to lock all node records in a subtree
this recursively walks the subtree starting at $node_id and attempts to
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.
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:
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.
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.
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...
(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.
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.
shorthand for constructing a readable page/section name with id, name and title
shorthand to determine whether the number of levels below section $node_id is greater than one
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.
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.
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.
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).
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.
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()).
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).
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 best solution I can think of is to:
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.
shorthand for determing whether a section is opened or closed
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).
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.
show a dialog to the user offering to forcefully unlock a node
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:
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
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).
show one or two clickable links to change the view of the tree
There are three different tree views:
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.
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).
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.
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().
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
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.
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.
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).
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.
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....
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
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.
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().
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.
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.
Documentation generated on Tue, 28 Jun 2016 19:11:06 +0200 by phpDocumentor 1.4.0