WordPress.org

Make WordPress Core

Opened 3 years ago

Last modified 10 months ago

#28801 new defect (bug)

Walker::walk makes an incorrect assumption if $top_level_elements is empty.

Reported by: rob-castlegate Owned by:
Milestone: Awaiting Review Priority: normal
Severity: normal Version: 3.8
Component: General Keywords: has-patch needs-testing needs-unit-tests 2nd-opinion
Focuses: Cc:

Description

A colleague of mine was generating a sidebar sub-navigation for one of his projects. The subnavigation contained second-level and third-level navigation elements.

The problem my colleague was having was that occasionally third-level elements would not be nested underneath their parent element (also in the list of elements) on some pages.

My colleague was calling wp_list_pages with an array of page IDs that he wanted to render in the sub-navigation, wp_list_pages then turned the list of page IDs into a list of Page objects, and it sorted the page objects by their 'menu_order' attribute; the third-level navigational elements all had their 'menu_order' set to 0, whereas the second-level navigational elements all had 'menu_order' set to something more than 0 - causing the third-level elements to be the first elements in the list.

wp_list_pages later made a call to Walker::walk, passing along that list of pages. Here is a relevant code snippet from Walker::walk:

/*
		 * When none of the elements is top level.
		 * Assume the first one must be root of the sub elements.
		 */
		if ( empty($top_level_elements) ) {

			$first = array_slice( $elements, 0, 1 );
			$root = $first[0];

			$top_level_elements = array();
			$children_elements  = array();
			foreach ( $elements as $e) {
				if ( $root->$parent_field == $e->$parent_field )
					$top_level_elements[] = $e;
				else
					$children_elements[ $e->$parent_field ][] = $e;
			}
		}

The bug is this code's assumption that the first item in $elements is a suitable root-element for the entire list (sentence emboldened for anybody not wanting to read the wall of text). wp_list_pages ordered our list by 'menu_order' which put our 3rd-level elements at the top of the list - causing a 3rd-level element to be treated as the navigation's root.

I wrote up a quick fix for this (I'm not sure if it's the best fix, I'm not overly experienced in Wordpress), and for our project we'll use wp_list_pages with a custom walker class that implements my fix.

Here is the patch of my fix:

Index: public_html/wp-includes/class-wp-walker.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- public_html/wp-includes/class-wp-walker.php	(date 1404915904000)
+++ public_html/wp-includes/class-wp-walker.php	(revision )
@@ -217,12 +217,34 @@
 
 		/*
 		 * When none of the elements is top level.
-		 * Assume the first one must be root of the sub elements.
+		 * ~~Assume the first one must be root of the sub elements.~~ Disregard - RJ CGIT 2014-07-09
+		 *
+		 * ----------
+		 *
+		 * Modified by Rob Jackson, Castlegate IT; 2014-07-09:
+		 *  Do not assume the first element is root, instead loop through the elements
+		 *  until we find one whose parent is _not_ in the list of elements.  If that fails,
+		 *  just fall back to the default behaviour of using the first element.
 		 */
 		if ( empty($top_level_elements) ) {
+            $root = false;
+            $element_ids = array_map(function($element){ return $element->ID; }, $elements);
 
+            foreach($elements as $element)
+            {
+                if (!in_array($element->post_parent, $element_ids))
+                {
+                    $root = $element;
+                    break;
+                }
+            }
+            unset($element);
+
+            if ($root === false)
+            {
-			$first = array_slice( $elements, 0, 1 );
-			$root = $first[0];
+                $first = array_slice( $elements, 0, 1 );
+                $root = $first[0];
+            }
 
 			$top_level_elements = array();
 			$children_elements  = array();

Kind regards,
Rob

Attachments (1)

class-wp-walker.php.patch (1.4 KB) - added by ross_ritchey 10 months ago.
Handle the case when there are no top elements and there is an element with a non-existant parent

Download all attachments as: .zip

Change History (5)

#1 @tellyworth
3 years ago

  • Version changed from trunk to 3.8

Appears to apply to 3.8, perhaps earlier.

#2 @chriscct7
22 months ago

  • Focuses administration added
  • Keywords needs-patch added

#3 @chriscct7
22 months ago

  • Focuses administration removed

@ross_ritchey
10 months ago

Handle the case when there are no top elements and there is an element with a non-existant parent

#4 @ross_ritchey
10 months ago

  • Keywords has-patch needs-testing needs-unit-tests 2nd-opinion added; needs-patch removed

Added a patch that handles this a bit differently. Instead of adding a new foreach I added a third conditional to the original foreach.

To do this I had to move the $root declaration out of the fallback if, which adds a small amount of performance overhead when there are top level elements.

Honestly not sure right now if this is better or worse than having a third foreach.

Note: See TracTickets for help on using tickets.