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