|>|!=)\s*([\w\-]+)\s*'); class SM2_Formatter { var $output; var $flags; var $itemOpen; var $itemClose; var $menuOpen; var $menuClose; var $topItemOpen; var $topMenuOpen; var $isFirst; var $page; var $url; var $currSib; var $sibCount; var $currClass; var $prettyLevel; // output the data function output($aString) { if ($this->flags & SM2_BUFFER) { $this->output .= $aString; } else { echo $aString; } } // set the default values for all of our formatting items function set($aFlags, $aItemOpen, $aItemClose, $aMenuOpen, $aMenuClose, $aTopItemOpen, $aTopMenuOpen) { $this->flags = $aFlags; $this->itemOpen = is_string($aItemOpen) ? $aItemOpen : '[li][a][menu_title]'; $this->itemClose = is_string($aItemClose) ? $aItemClose : ''; $this->menuOpen = is_string($aMenuOpen) ? $aMenuOpen : '[ul]'; $this->menuClose = is_string($aMenuClose) ? $aMenuClose : ''; $this->topItemOpen = is_string($aTopItemOpen) ? $aTopItemOpen : $this->itemOpen; $this->topMenuOpen = is_string($aTopMenuOpen) ? $aTopMenuOpen : $this->menuOpen; } // initialize the state of the formatter before anything is output function initialize() { $this->output = ''; $this->prettyLevel = 0; if ($this->flags & SM2_PRETTY) { $this->output("\n"); } } // start a menu function startList(&$aPage, &$aUrl) { $currClass = ''; $currItem = $this->menuOpen; // use the top level menu open if this is the first menu if ($this->topMenuOpen) { $currItem = $this->topMenuOpen; $currClass .= ' menu-top'; $this->topMenuOpen = false; } // add the numbered menu class only if requested if (($this->flags & SM2_NUMCLASS) == SM2_NUMCLASS) { $currClass .= ' menu-'.$aPage['level']; } $this->prettyLevel += 1; // replace all keywords in the output if ($this->flags & SM2_PRETTY) { $this->output("\n".str_repeat(' ',$this->prettyLevel). $this->format($aPage, $aUrl, $currItem, $currClass)); } else { $this->output($this->format($aPage, $aUrl, $currItem, $currClass)); } $this->prettyLevel += 3; } // start an item within the menu function startItem(&$aPage, &$aUrl, $aCurrSib, $aSibCount) { // generate our class list $currClass = ''; if (($this->flags & SM2_NUMCLASS) == SM2_NUMCLASS) { $currClass .= ' menu-'.$aPage['level']; } if (array_key_exists('sm2_has_child', $aPage)) { // not set if false, so existence = true $currClass .= ' menu-expand'; } if (array_key_exists('sm2_is_curr', $aPage)) { $currClass .= ' menu-current'; } elseif (array_key_exists('sm2_is_parent', $aPage)) { // not set if false, so existence = true $currClass .= ' menu-parent'; } elseif (array_key_exists('sm2_is_sibling', $aPage)) { // not set if false, so existence = true $currClass .= ' menu-sibling'; } elseif (array_key_exists('sm2_child_level',$aPage)) { // not set if not a child $currClass .= ' menu-child'; if (($this->flags & SM2_NUMCLASS) == SM2_NUMCLASS) { $currClass .= ' menu-child-'.($aPage['sm2_child_level']-1); } } if ($aCurrSib == 1) { $currClass .= ' menu-first'; } if ($aCurrSib == $aSibCount) { $currClass .= ' menu-last'; } // use the top level item if this is the first item $currItem = $this->itemOpen; if ($this->topItemOpen) { $currItem = $this->topItemOpen; $this->topItemOpen = false; } // replace all keywords in the output if ($this->flags & SM2_PRETTY) { $this->output("\n".str_repeat(' ',$this->prettyLevel)); } $this->output($this->format($aPage, $aUrl, $currItem, $currClass, $aCurrSib, $aSibCount)); } // find and replace all keywords, setting the state variables first function format(&$aPage, &$aUrl, &$aCurrItem, &$aCurrClass, $aCurrSib = 0, $aSibCount = 0) { $this->page = &$aPage; $this->url = &$aUrl; $this->currClass = trim($aCurrClass); $this->currSib = $aCurrSib; $this->sibCount = $aSibCount; $item = $this->format2($aCurrItem); unset($this->page); unset($this->url); unset($this->currClass); return $item; } // find and replace all keywords function format2(&$aCurrItem) { if (!is_string($aCurrItem)) return ''; return preg_replace( '@\[('. 'a|ac|/a|li|/li|ul|/ul|menu_title|page_title|url|target|page_id|'. 'parent|level|sib|sibCount|class|description|keywords|'. SM2_CONDITIONAL. ')\]@e', '$this->replace("\1")', $aCurrItem); } // replace the keywords function replace($aMatch) { switch ($aMatch) { case 'a': return ''; case 'ac': return ''; case '/a': return ''; case 'li': return '
  • '; case '/li': return '
  • '; case 'ul': return ''; case 'url': return $this->url; case 'sib': return $this->currSib; case 'sibCount': return $this->sibCount; case 'class': return $this->currClass; default: if (array_key_exists($aMatch, $this->page)) { if ($this->flags & SM2_ESCAPE) { return htmlspecialchars($this->page[$aMatch], ENT_QUOTES); } else { return $this->page[$aMatch]; } } if (preg_match('/'.SM2_CONDITIONAL.'/', $aMatch, $rgMatches)) { return $this->replaceIf($rgMatches[1], $rgMatches[2], $rgMatches[3]); } } return "[$aMatch=UNKNOWN]"; } // conditional replacement function replaceIf(&$aExpression, &$aIfValue, &$aElseValue) { // evaluate all of the tests in the conditional (we don't do short-circuit // evaluation) and replace the string test with the boolean result $rgTests = preg_split('/(\|\||\&\&)/', $aExpression, -1, PREG_SPLIT_DELIM_CAPTURE); for ($n = 0; $n < count($rgTests); $n += 2) { if (preg_match('/'.SM2_COND_TERM.'/', $rgTests[$n], $rgMatches)) { $rgTests[$n] = $this->ifTest($rgMatches[1], $rgMatches[2], $rgMatches[3]); } else { error_log("show_menu2 error: conditional expression is invalid!"); $rgTests[$n] = false; } } // combine all test results for a final result $ok = $rgTests[0]; for ($n = 1; $n+1 < count($rgTests); $n += 2) { if ($rgTests[$n] == '||') { $ok = $ok || $rgTests[$n+1]; } else { $ok = $ok && $rgTests[$n+1]; } } // return the formatted expression if the test succeeded return $ok ? $this->format2($aIfValue) : $this->format2($aElseValue); } // conditional test function ifTest(&$aKey, &$aOperator, &$aValue) { global $wb; // find the correct operand $operand = false; switch($aKey) { case 'class': // we need to wrap the class names in spaces so we can test for a unique // class name that will not match prefixes or suffixes. Same must be done // for the value we are testing. $operand = " $this->currClass "; break; case 'sib': $operand = $this->currSib; if ($aValue == 'sibCount') { $aValue = $this->sibCount; } break; case 'sibCount': $operand = $this->sibCount; break; case 'level': $operand = $this->page['level']; switch ($aValue) { case 'root': $aValue = 0; break; case 'granny': $aValue = $wb->page['level']-2; break; case 'parent': $aValue = $wb->page['level']-1; break; case 'current': $aValue = $wb->page['level']; break; case 'child': $aValue = $wb->page['level']+1; break; } if ($aValue < 0) $aValue = 0; break; case 'id': $operand = $this->page['page_id']; switch ($aValue) { case 'parent': $aValue = $wb->page['parent']; break; case 'current': $aValue = $wb->page['page_id']; break; } break; default: return ''; } // do the test $ok = false; switch ($aOperator) { case '<': $ok = ($operand < $aValue); break; case '<=': $ok = ($operand <= $aValue); break; case '=': case '==': case '!=': if ($aKey == 'class') { $ok = strstr($operand, " $aValue ") !== FALSE; } else { $ok = ($operand == $aValue); } if ($aOperator == '!=') { $ok = !$ok; } break; case '>=': $ok = ($operand >= $aValue); case '>': $ok = ($operand > $aValue); } return $ok; } // finish the current menu item function finishItem() { if ($this->flags & SM2_PRETTY) { $this->output(str_repeat(' ',$this->prettyLevel).$this->itemClose); } else { $this->output($this->itemClose); } } // finish the current menu function finishList() { $this->prettyLevel -= 3; if ($this->flags & SM2_PRETTY) { $this->output("\n".str_repeat(' ',$this->prettyLevel).$this->menuClose."\n"); } else { $this->output($this->menuClose); } $this->prettyLevel -= 1; } // cleanup the state of the formatter after everything has been output function finalize() { if ($this->flags & SM2_PRETTY) { $this->output("\n"); } } // return the output function getOutput() { return $this->output; } }; function show_menu2( $aMenu = 0, $aStart = SM2_ROOT, $aMaxLevel = -1999, // SM2_CURR+1 $aFlags = SM2_TRIM, $aItemOpen = false, $aItemClose = false, $aMenuOpen = false, $aMenuClose = false, $aTopItemOpen = false, $aTopMenuOpen = false ) { global $wb; // ensure we have our group 1 flag, we don't check for the "exactly 1" part, but // we do ensure that they provide at least one. if (0 == ($aFlags & _SM2_GROUP_1)) { error_log("show_menu2 error: no flags from group 1 supplied! Exactly one flag is required!"); $aFlags |= SM2_TRIM; // default to TRIM } // fix up the menu number to default to the menu number // of the current page if no menu has been supplied if ($aMenu == 0) { $aMenu = $wb->page['menu'] == '' ? 1 : $wb->page['menu']; } // Set some of the $wb->page[] settings to defaults if not set $pageLevel = $wb->page['level'] == '' ? 0 : $wb->page['level']; $pageParent = $wb->page['parent'] == '' ? 0 : $wb->page['parent']; // handle search page results using the referrer ID $CURR_PAGE_ID = PAGE_ID; if ($CURR_PAGE_ID == 0 && defined(REFERRER_ID) && REFERRER_ID > 0) { $CURR_PAGE_ID = REFERRER_ID; } // adjust the start level and start page ID as necessary to // handle the special values that can be passed in as $aStart $aStartLevel = 0; if ($aStart < SM2_ROOT) { // SM2_CURR+N if ($aStart == SM2_CURR) { $aStartLevel = $pageLevel; $aStart = $pageParent; } else { $aStartLevel = $pageLevel + $aStart - SM2_CURR; $aStart = $CURR_PAGE_ID; } } elseif ($aStart < 0) { // SM2_ROOT+N $aStartLevel = $aStart - SM2_ROOT; $aStart = 0; } // we get the menu data once and store it in a global variable. This allows // multiple calls to show_menu2 in a single page with only a single call to // the database. If this variable exists, then we have already retrieved all // of the information and processed it, so we don't need to do it again. if (($aFlags & SM2_NOCACHE) != 0 || !array_key_exists('show_menu2_data', $GLOBALS) || !array_key_exists($aMenu, $GLOBALS['show_menu2_data'])) { global $database; // create an array of all parents of the current page. As the page_trail // doesn't include the theoretical root element 0, we add it ourselves. $rgCurrParents = split(',', '0,'.$wb->page['page_trail']); array_pop($rgCurrParents); // remove the current page $rgParent = array(); // if the caller wants all menus gathered together (e.g. for a sitemap) // then we don't limit our SQL query $menuLimitSql = ' AND menu = ' .$aMenu; if ($aMenu == SM2_ALLMENU) { $menuLimitSql = ''; } // we only load the description and keywords if we have been told to, // this cuts the memory load for pages that don't use them. Note that if // we haven't been told to load these fields the *FIRST TIME* show_menu2 // is called (i.e. where the database is loaded) then the info won't // exist anyhow. $fields = 'parent,page_id,menu_title,page_title,link,target,level,visibility,viewing_groups'; if (version_compare(WB_VERSION, '2.7', '>=')) { // WB 2.7+ $fields .= ',viewing_users'; } if ($aFlags & SM2_ALLINFO) { $fields = '*'; } // get this once for performance. We really should be calling only need to // call $wb->get_group_id() but that outputs a warning notice if the // groupid isn't set in the session, so we check it first here. $currGroup = array_key_exists('GROUP_ID', $_SESSION) ? ','.$wb->get_group_id().',' : 'NOGROUP'; // we request all matching rows from the database for the menu that we // are about to create it is cheaper for us to get everything we need // from the database once and create the menu from memory then make // multiple calls to the database. $sql = "SELECT $fields FROM ".TABLE_PREFIX. "pages WHERE $wb->extra_where_sql $menuLimitSql ". 'ORDER BY level ASC, position ASC'; $oRowset = $database->query($sql); if (is_object($oRowset) && $oRowset->numRows() > 0) { // create an in memory array of the database data based on the item's parent. // The array stores all elements in the correct display order. while ($page = $oRowset->fetchRow()) { // ignore all pages that the current user is not permitted to view if(version_compare(WB_VERSION, '2.7', '>=')) { // WB >= 2.7 // 1. all pages with no active sections (unless it is the top page) are ignored if(!$wb->page_is_active($page) && $page['link'] != $wb->default_link && !INTRO_PAGE) { continue; } // 2. all pages not visible to this user (unless always visible to registered users) are ignored if(!$wb->page_is_visible($page) && $page['visibility'] != 'registered') { continue; } } else { // WB < 2.7 // We can't do this in SQL as the viewing_groups column contains multiple // values which are hard to process correctly in SQL. Essentially we add the // following limit to the SQL query above: // (visibility <> "private" OR $wb->get_group_id() IN viewing_groups) if ($page['visibility'] == 'private' && false === strstr(",{$page['viewing_groups']},", $currGroup)) { continue; } } // ensure that we have an array entry in the table to add this to $idx = $page['parent']; if (!array_key_exists($idx, $rgParent)) { $rgParent[$idx] = array(); } // mark our current page as being on the current path if ($page['page_id'] == $CURR_PAGE_ID) { $page['sm2_is_curr'] = true; $page['sm2_on_curr_path'] = true; } // mark parents of the current page as such if (in_array($page['page_id'], $rgCurrParents)) { $page['sm2_is_parent'] = true; $page['sm2_on_curr_path'] = true; } // add the entry to the array $rgParent[$idx][] = $page; } } unset($oRowset); // mark all elements that are siblings of any element on the current path foreach ($rgCurrParents as $x) { if (array_key_exists($x, $rgParent)) { foreach (array_keys($rgParent[$x]) as $y) { $mark =& $rgParent[$x][$y]; $mark['sm2_path_sibling'] = true; unset($mark); } } } // mark all elements that have children and are siblings of the current page $parentId = $pageParent; foreach (array_keys($rgParent) as $x) { $childSet =& $rgParent[$x]; foreach (array_keys($childSet) as $y) { $mark =& $childSet[$y]; if (array_key_exists($mark['page_id'], $rgParent)) { $mark['sm2_has_child'] = true; } if ($mark['parent'] == $parentId && $mark['page_id'] != $CURR_PAGE_ID) { $mark['sm2_is_sibling'] = true; } unset($mark); } unset($childSet); } // mark all children of the current page. We don't do this when // $CURR_PAGE_ID is 0, as 0 is the parent of everything. // $CURR_PAGE_ID == 0 occurs on special pages like search results // when no referrer is available.s if ($CURR_PAGE_ID != 0) { sm2_mark_children($rgParent, $CURR_PAGE_ID, 1); } // store the complete processed menu data as a global. We don't // need to read this from the database anymore regardless of how // many menus are displayed on the same page. if (!array_key_exists('show_menu2_data', $GLOBALS)) { $GLOBALS['show_menu2_data'] = array(); } $GLOBALS['show_menu2_data'][$aMenu] =& $rgParent; unset($rgParent); } // adjust $aMaxLevel to the level number of the final level that // will be displayed. That is, we display all levels <= aMaxLevel. if ($aMaxLevel == SM2_ALL) { $aMaxLevel = 1000; } elseif ($aMaxLevel < 0) { // SM2_CURR+N $aMaxLevel += $pageLevel - SM2_CURR; } elseif ($aMaxLevel >= SM2_MAX) { // SM2_MAX+N $aMaxLevel += $aStartLevel - SM2_MAX; if ($aMaxLevel > $pageLevel) { $aMaxLevel = $pageLevel; } } else { // SM2_START+N $aMaxLevel += $aStartLevel - SM2_START; } // generate the menu $retval = false; if (array_key_exists($aStart, $GLOBALS['show_menu2_data'][$aMenu])) { $formatter = $aItemOpen; if (!is_object($aItemOpen)) { static $sm2formatter; if (!isset($sm2formatter)) { $sm2formatter = new SM2_Formatter; } $formatter = $sm2formatter; $formatter->set($aFlags, $aItemOpen, $aItemClose, $aMenuOpen, $aMenuClose, $aTopItemOpen, $aTopMenuOpen); } // display the menu $formatter->initialize(); sm2_recurse( $GLOBALS['show_menu2_data'][$aMenu], $aStart, // parent id to start displaying sub-menus $aStartLevel, $aMaxLevel, $aFlags, $formatter); $formatter->finalize(); // if we are returning something, get the data if (($aFlags & SM2_BUFFER) != 0) { $retval = $formatter->getOutput(); } } // clear the data if we aren't caching it if (($aFlags & SM2_NOCACHE) != 0) { unset($GLOBALS['show_menu2_data'][$aMenu]); } return $retval; } function sm2_mark_children(&$rgParent, $aStart, $aChildLevel) { if (array_key_exists($aStart, $rgParent)) { foreach (array_keys($rgParent[$aStart]) as $y) { $mark =& $rgParent[$aStart][$y]; $mark['sm2_child_level'] = $aChildLevel; $mark['sm2_on_curr_path'] = true; sm2_mark_children($rgParent, $mark['page_id'], $aChildLevel+1); } } } function sm2_recurse( &$rgParent, $aStart, $aStartLevel, $aMaxLevel, $aFlags, &$aFormatter ) { global $wb; // on entry to this function we know that there are entries for this // parent and all entries for that parent are being displayed. We also // need to check if any of the children need to be displayed too. $isListOpen = false; $currentLevel = $wb->page['level'] == '' ? 0 : $wb->page['level']; // get the number of siblings so we can check pass this and check if the // item is first or last $sibCount = count($rgParent[$aStart]); $currSib = 0; foreach ($rgParent[$aStart] as $page) { $currSib++; // skip any elements that are lower than the maximum level $pageLevel = $page['level']; if ($pageLevel > $aMaxLevel) { continue; } // this affects ONLY the top level if ($aStart == 0 && ($aFlags & SM2_CURRTREE)) { if (!array_key_exists('sm2_on_curr_path', $page)) { // not set if false, so existence = true continue; } $sibCount = 1; } // trim the tree as appropriate if ($aFlags & SM2_SIBLING) { // parents, and siblings and children of current only if (!array_key_exists('sm2_on_curr_path', $page) // not set if false, so existence = true && !array_key_exists('sm2_is_sibling', $page) // not set if false, so existence = true && !array_key_exists('sm2_child_level', $page)) { // not set if false, so existence = true continue; } } else if ($aFlags & SM2_TRIM) { // parents and siblings of parents if (!array_key_exists('sm2_on_curr_path', $page) // not set if false, so existence = true && !array_key_exists('sm2_path_sibling', $page)) { // not set if false, so existence = true continue; } } elseif ($aFlags & SM2_CRUMB) { // parents only if (!array_key_exists('sm2_on_curr_path', $page) // not set if false, so existence = true || array_key_exists('sm2_child_level', $page)) { // not set if false, so existence = true continue; } } // depth first traverse $nextParent = $page['page_id']; // display the current element if we have reached the start level if ($pageLevel >= $aStartLevel) { // massage the link into the correct form if(!INTRO_PAGE && $page['link'] == $wb->default_link) { $url = WB_URL; } else { $url = $wb->page_link($page['link']); } // we open the list only when we absolutely need to if (!$isListOpen) { $aFormatter->startList($page, $url); $isListOpen = true; } $aFormatter->startItem($page, $url, $currSib, $sibCount); } // display children as appropriate if ($pageLevel + 1 <= $aMaxLevel && array_key_exists('sm2_has_child', $page)) { // not set if false, so existence = true sm2_recurse( $rgParent, $nextParent, // parent id to start displaying sub-menus $aStartLevel, $aMaxLevel, $aFlags, $aFormatter); } // close the current element if appropriate if ($pageLevel >= $aStartLevel) { $aFormatter->finishItem($pageLevel, $page); } } // close the list if we opened one if ($isListOpen) { $aFormatter->finishList(); } } ?>