/** calculate and validate the node_id to display
 *
 * this tries to determine a valid node to display based on the node the user
 * requested and the area that the user may or may not have requested.
 *
 * Basic assumption is that the visitor has indeed view access to
 * area $area_id. This means that the user is allowed to see the nodes
 * in this area that are not under embargo (and not expired).
 * We do have a complete overview of all nodes in this area in the array $tree.
 * (See {@link tree_build()} for more information about the tree structure)
 *
 * The parameter $requested_node is either an integer, indicating the user
 * explicitly specified a node number in the page request, or null, indicating
 * that the user did not explicitly specify a node. In the latter case
 * the user may or may not have explicitly requested an area.
 *
 * There are several cases we need to handle
 * - if no node is explicitly requested, we need to identify the default page in the area
 * - if the node is under embargo the node does not exist (from the POV of the user)
 * - if the requested node is a section, we need to identify the default page in that section
 *
 * @param array &$tree a reference to the complete tree in area $area_id
 * @param int $area_id the area where we are looking for a node
 * @param int|null $requested_node the node_id the user requested or NULL if none was specified
 * @return bool|int FALSE if no suitable node found or a valid $node_id
 */
function calculate_node_id(&$tree, $area_id, $requested_node)
{
    if (is_null($requested_node)) {
        return calculate_default_page($tree, $tree[0]['first_child_id']);
    }
    $node_id = intval($requested_node);
    if (!isset($tree[$node_id])) {
        logger("calculate_node_id(): weird: node '{$node_id}' not set in tree for area '{$area_id}'", WLOG_DEBUG);
        return FALSE;
    }
    if (is_under_embargo($tree, $node_id) || is_expired($node_id, $tree)) {
        return FALSE;
    }
    return $tree[$node_id]['is_page'] ? $node_id : calculate_default_page($tree, $tree[$node_id]['first_child_id']);
}
 /** 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 {@link 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).
  *
  * @param int $node_id the node we have to change
  * @todo this routine could be improved by refactoring it; it is too long!
  * @return void results are returned as output in $this->output
  * @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
  */
 function save_node($node_id)
 {
     global $USER, $WAS_SCRIPT_NAME;
     // 0 -- prepare some useful information
     $is_advanced = intval($_POST['dialog']) == DIALOG_NODE_EDIT_ADVANCED ? TRUE : FALSE;
     $is_page = db_bool_is(TRUE, $this->tree[$node_id]['record']['is_page']);
     $viewonly = db_bool_is(TRUE, $this->tree[$node_id]['record']['is_readonly']);
     $embargo = is_under_embargo($this->tree, $node_id);
     // 1 -- perhaps make node read-write again + quit
     if ($viewonly) {
         $anode = array('{NODE_FULL_NAME}' => $this->node_full_name($node_id));
         if ($is_advanced && !isset($_POST['node_is_readonly'])) {
             $fields = array('is_readonly' => FALSE);
             $where = array('node_id' => $node_id);
             $retval = db_update('nodes', $fields, $where);
             logger(sprintf(__CLASS__ . ': set node %s%s to readwrite: %s', $this->node_full_name($node_id), $embargo ? ' (embargo)' : '', $retval === FALSE ? 'failed: ' . db_errormessage() : 'success'));
             $message = t('node_no_longer_readonly', 'admin', $anode);
             $this->output->add_message($message);
             if (!$embargo) {
                 $nodes = $this->get_node_id_and_ancestors($node_id);
                 $this->queue_area_node_alert($this->area_id, $nodes, $message, $USER->full_name);
             }
             // update our cached version of this node, saving a trip to the database
             $this->tree[$node_id]['record']['is_readonly'] = SQL_FALSE;
         } else {
             $this->output->add_message(t('node_still_readonly', 'admin', $anode));
         }
         lock_release_node($node_id);
         $this->show_tree();
         $this->show_area_menu($this->area_id);
         return;
     }
     // 2 -- validate data
     $dialogdef = $is_advanced ? $this->get_dialogdef_edit_advanced_node($node_id, $is_page, $viewonly) : $this->get_dialogdef_edit_node($node_id, $is_page, $viewonly);
     if (!dialog_validate($dialogdef)) {
         // errors? show them to the user and edit again
         foreach ($dialogdef as $k => $item) {
             if (isset($item['errors']) && $item['errors'] > 0) {
                 $this->output->add_message($item['error_messages']);
             }
         }
         $anode = array('{NODE}' => strval($node_id), '{NODE_FULL_NAME}' => $this->node_full_name($node_id));
         if ($is_advanced) {
             $title = t($is_page ? 'edit_a_page_advanced_header' : 'edit_a_section_advanced_header', 'admin', $anode);
             $expl = t($is_page ? 'edit_page_advanced_explanation' : 'edit_section_advanced_explanation', 'admin', $anode);
         } else {
             $title = t($is_page ? 'edit_a_page_header' : 'edit_a_section_header', 'admin', $anode);
             $expl = t($is_page ? 'edit_page_explanation' : 'edit_section_explanation', 'admin', $anode);
         }
         $this->output->add_content('<h2>' . $title . '</h2>');
         $this->output->add_content($expl);
         $href = href($WAS_SCRIPT_NAME, array('job' => JOB_PAGEMANAGER, 'task' => TASK_SAVE_NODE, 'node' => $node_id));
         $this->output->add_content(dialog_quickform($href, $dialogdef));
         $this->output->set_funnel_mode(TRUE);
         // no distractions
         // Note that we also do NOT show the edit menu: we try to let the user concentrate
         // on the task at hand;  the only escape route is 'Cancel'...
         // Also note that we still have the record lock; that won't change because we
         // will be editing the page again. Cancel'ing will also release the lock.
         return;
     }
     // 3A -- prepare for update of node record - phase 1
     $now = strftime("%Y-%m-%d %T");
     $fields = array('mtime' => $now);
     $changed_parent = FALSE;
     $changed_sortorder = FALSE;
     $changed_module = FALSE;
     $changed_area = FALSE;
     $new_area_mass_move = FALSE;
     foreach ($dialogdef as $name => $item) {
         if (isset($item['viewonly']) && $item['viewonly']) {
             continue;
         }
         switch ($name) {
             // basic fields
             case 'node_title':
                 $fields['title'] = $item['value'];
                 $this->tree[$node_id]['record']['title'] = $item['value'];
                 // for full_name in message below
                 break;
             case 'node_link_text':
                 $fields['link_text'] = $item['value'];
                 $this->tree[$node_id]['record']['link_text'] = $item['value'];
                 // for full_name in message below
                 break;
             case 'node_parent_id':
                 if ($this->tree[$node_id]['parent_id'] != $item['value']) {
                     $parent_id = intval($item['value']);
                     // could be 0, indicating top level node (see below)
                     $changed_parent = TRUE;
                 }
                 break;
             case 'node_module_id':
                 if ($this->tree[$node_id]['record']['module_id'] != $item['value']) {
                     $fields['module_id'] = intval($item['value']);
                     $changed_module = TRUE;
                 }
                 break;
             case 'node_sort_after_id':
                 if ($this->tree[$node_id]['prev_sibling_id'] != $item['value']) {
                     $node_sort_after_id = $item['value'];
                     // deal with this after this foreach()
                     $changed_sortorder = TRUE;
                 }
                 break;
                 // advanced fields
             // advanced fields
             case 'node_area_id':
                 $new_area_id = intval($item['value']);
                 if ($this->tree[$node_id]['record']['area_id'] != $new_area_id) {
                     if ($is_page || $this->tree[$node_id]['first_child_id'] == 0) {
                         $fields['area_id'] = intval($item['value']);
                         $changed_area = TRUE;
                     } else {
                         $new_area_mass_move = TRUE;
                     }
                 }
                 break;
             case 'node_link_image':
                 $fields['link_image'] = $item['value'];
                 break;
             case 'node_link_image_width':
                 $fields['link_image_width'] = intval($item['value']);
                 break;
             case 'node_link_image_height':
                 $fields['link_image_height'] = intval($item['value']);
                 break;
             case 'node_is_hidden':
                 $fields['is_hidden'] = $item['value'] == 1 ? TRUE : FALSE;
                 break;
             case 'node_is_readonly':
                 $fields['is_readonly'] = $item['value'] == 1 ? TRUE : FALSE;
                 break;
             case 'node_embargo':
                 $fields['embargo'] = $item['value'];
                 if ($now < $fields['embargo']) {
                     $embargo = TRUE;
                 }
                 break;
             case 'node_expiry':
                 $fields['expiry'] = $item['value'];
                 break;
             case 'node_style':
                 $fields['style'] = $item['value'];
                 break;
         }
         // switch
     }
     // foreach
     // 3B -- prepare for update - phase 2 ('simple exceptions')
     if ($changed_area) {
         // a single node will be moved to the top level of the new area
         $parent_id = 0;
         $newtree = tree_build($new_area_id);
         $fields['sort_order'] = $this->calculate_new_sort_order($newtree, $new_area_id, $parent_id);
         unset($newtree);
         $fields['parent_id'] = $node_id;
         // $parent_id == $node_id means: top level
         $fields['is_default'] = FALSE;
         // otherwise the target section might end up with TWO defaults...
     } elseif ($changed_parent) {
         // the node will be moved to another section (or the top level)
         $fields['sort_order'] = $this->calculate_new_sort_order($this->tree, $this->area_id, $parent_id);
         $fields['parent_id'] = $parent_id == 0 ? $node_id : $parent_id;
         $fields['is_default'] = FALSE;
         // otherwise the target section might end up with TWO defaults...
     } elseif ($changed_sortorder) {
         // simply change the sort order
         $fields['sort_order'] = $this->calculate_updated_sort_order($node_id, $node_sort_after_id);
     }
     // 4A -- actually update the database for the pending 'simple' changes
     $errors = 0;
     if ($changed_module) {
         if (!$this->module_disconnect($this->area_id, $node_id, $this->tree[$node_id]['record']['module_id'])) {
             ++$errors;
         }
     }
     $where = array('node_id' => $node_id);
     if (db_update('nodes', $fields, $where) === FALSE) {
         logger(sprintf('%s.%s(): error saving node \'%d\'%s: %s', __CLASS__, __FUNCTION__, $node_id, $embargo ? ' (embargo)' : '', db_errormessage()));
         ++$errors;
     }
     if ($changed_module) {
         if (!$this->module_connect($this->area_id, $node_id, $fields['module_id'])) {
             ++$errors;
         }
     }
     if ($errors == 0) {
         logger(sprintf('%s.%s(): success saving node \'%d\'%s', __CLASS__, __FUNCTION__, $node_id, $embargo ? ' (embargo)' : ''), WLOG_DEBUG);
         $anode = array('{AREA}' => $this->area_id, '{NODE_FULL_NAME}' => $this->node_full_name($node_id));
         $this->output->add_message(t($is_page ? 'page_saved' : 'section_saved', 'admin', $anode));
         if (!$embargo) {
             $nodes = $this->get_node_id_and_ancestors($node_id);
             if ($changed_area) {
                 $areas = array($this->area_id, $fields['area_id']);
                 $anode['{NEWAREA}'] = $fields['area_id'];
                 $message = t('node_was_edited_and_moved', 'admin', $anode);
             } else {
                 $areas = $this->area_id;
                 $message = t('node_was_edited', 'admin', $anode);
             }
             $this->queue_area_node_alert($areas, $nodes, $message, $USER->full_name);
         }
     } else {
         $message = t('error_saving_node', 'admin');
         $this->output->add_message($message);
     }
     // 4B -- update the database for the special case of a mass-move
     if ($new_area_mass_move) {
         $this->save_node_new_area_mass_move($node_id, $new_area_id, $embargo);
     }
     // 5 -- clean up + show updated and re-read tree again
     lock_release_node($node_id);
     $this->build_cached_tree($this->area_id, TRUE);
     // force re-read of the tree structure
     $this->show_tree();
     $this->show_area_menu($this->area_id);
 }