diff --git a/libraries/src/Component/Router/Rules/NomenuRules.php b/libraries/src/Component/Router/Rules/NomenuRules.php index 5ddb16db03a61..c444e5a57d8c5 100644 --- a/libraries/src/Component/Router/Rules/NomenuRules.php +++ b/libraries/src/Component/Router/Rules/NomenuRules.php @@ -67,20 +67,171 @@ public function parse(&$segments, &$vars) { $active = $this->router->menu->getActive(); - if (!is_object($active)) + if ($active !== null) { - $views = $this->router->getViews(); + return; + } + + $views = $this->router->getViews(); + + if (!isset($views[$segments[0]])) + { + return; + } + + $vars['view'] = array_shift($segments); + $mainView = $views[$vars['view']]; + + // Create a temporary copy of vars to use in getId + $vars2 = $vars; + + // Total views in path + $totalViews = count($mainView->path); + + foreach ($mainView->path as $i => $element) + { + if (!$segments) + { + // Wrong URL, some segment missing + return; + } + + $view = $views[$element]; - if (isset($views[$segments[0]])) + if ($element !== $vars['view'] && $view->key) { - $vars['view'] = array_shift($segments); + $child = $views[$mainView->path[$i+1]]; - if (isset($views[$vars['view']]->key) && isset($segments[0])) + if ($child->nestable && $view->key === $child->key) { - $vars[$views[$vars['view']]->key] = preg_replace('/-/', ':', array_shift($segments), 1); + // Do not process this view, child will do it as they work on the same path elements + continue; } } + + // Remember parent key value from parent view + $parentId = $view->parent_key && isset($vars2[$view->parent->key]) ? $vars2[$view->parent->key] : null; + + // Generate function name + $func = array($this->router, 'get' . ucfirst($view->name) . 'Id'); + + while ($segments) + { + if ($view->nestable) + { + // Limit number of calls to getId() + if (count($segments) + $i >= $totalViews) + { + // If query has no key set, we assume 0. + if (!isset($vars2[$view->key])) + { + $vars2[$view->key] = 0; + } + + // Required for noIDs to get id from alias + if (is_callable($func)) + { + $key = call_user_func_array($func, array($segments[0], $vars2)); + + // Did we get a proper key? If not, we need to look in the next view + if ($key) + { + $vars2[$view->key] = $key; + array_shift($segments); + + // Found, go to the next segment + continue; + } + } + else + { + // The router is not complete. The getId() method is missing. + return; + } + } + + // Add parent key + if ($view->parent_key && isset($parentId)) + { + $vars2[$view->parent_key] = $parentId; + + // Do not unset own key + if ($view->key !== $view->parent->key) + { + unset($vars2[$view->parent->key]); + } + } + + if ($element !== $vars['view']) + { + // Key not found, jump to the next view + break; + } + + // Wrong URL + return; + } + + if (!$view->key) + { + if ($view->name === $segments[0]) + { + // Add parent key + if ($view->parent_key && isset($parentId)) + { + $vars2[$view->parent_key] = $parentId; + unset($vars2[$view->parent->key]); + } + + array_shift($segments); + + // Found, jump to the next view + break; + } + + // Wrong URL + return; + } + + // Required for noIDs to get id from alias + if (is_callable($func)) + { + // If query has no key set, we assume 0. + if (!isset($vars2[$view->key])) + { + $vars2[$view->key] = 0; + } + + // Hand the data over to the router specific method and see if there is a content item that fits + $key = call_user_func_array($func, array($segments[0], $vars2)); + + if ($key) + { + // Add parent key + if ($view->parent_key && isset($parentId)) + { + $vars2[$view->parent_key] = $parentId; + unset($vars2[$view->parent->key]); + } + + $vars2[$view->key] = $key; + array_shift($segments); + + // Found, jump to the next view + break; + } + + // Wrong URL + return; + } + + // The router is not complete. The getId() method is missing. + return; + } } + + // Copy all found variables + $vars = $vars2; } /** @@ -95,44 +246,82 @@ public function parse(&$segments, &$vars) */ public function build(&$query, &$segments) { - $menu_found = false; - if (isset($query['Itemid'])) { $item = $this->router->menu->getItem($query['Itemid']); if (!isset($query['option']) || ($item && $item->query['option'] === $query['option'])) { - $menu_found = true; + return; } } - if (!$menu_found && isset($query['view'])) + // Get the path from the view of the current URL and parse it + $path = array_reverse($this->router->getPath($query), true); + + // Check if specified view is known + if (!isset($query['view'], $path[$query['view']])) + { + return; + } + + // Get all views for this component + $views = $this->router->getViews(); + + $segments[] = $query['view']; + + // Requested view + $mainView = $views[$query['view']]; + + foreach ($mainView->path as $i => $element) { - $views = $this->router->getViews(); + $view = $views[$element]; + $ids = $path[$element]; - if (isset($views[$query['view']])) + if ($element !== $query['view'] && $view->key) { - $view = $views[$query['view']]; - $segments[] = $query['view']; + $child = $views[$mainView->path[$i+1]]; - if ($view->key && isset($query[$view->key])) + if ($child->nestable && $view->key === $child->key) + { + // Do not process this view, child will do it as they work on the same path elements + continue; + } + } + + if ($ids) + { + if ($view->nestable) { - if (is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Segment'))) + // Remove 1:root + array_pop($ids); + + foreach (array_reverse($ids, true) as $id => $segment) { - $result = call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); - $segments[] = str_replace(':', '-', array_shift($result)); + $segments[] = str_replace(':', '-', $segment); } - else + } + elseif ($ids === true) + { + if ($element !== $query['view']) { - $segments[] = str_replace(':', '-', $query[$view->key]); + $segments[] = $element; } - - unset($query[$views[$query['view']]->key]); } + else + { + $segments[] = str_replace(':', '-', current($ids)); + } + } - unset($query['view']); + if ($view->parent_key) + { + // Remove parent key from query + unset($query[$view->parent_key]); } } + + // Remove key and view from query + unset($query[$mainView->key], $query['view']); } } diff --git a/tests/unit/suites/libraries/cms/component/router/JComponentRouterViewTest.php b/tests/unit/suites/libraries/cms/component/router/JComponentRouterViewTest.php index 959f6cf74f519..eadbd86bf5990 100644 --- a/tests/unit/suites/libraries/cms/component/router/JComponentRouterViewTest.php +++ b/tests/unit/suites/libraries/cms/component/router/JComponentRouterViewTest.php @@ -139,14 +139,19 @@ public function casesGetPath() $cases[] = array(array('view' => 'categories'), array('categories' => array())); // View with parent and children - $cases[] = array(array('view' => 'category', 'id' => '9'), array('category' => array(9 => '9:uncategorised'), 'categories' => array(9 => '9:uncategorised'))); + $cases[] = array(array('view' => 'category', 'id' => '9'), + array( + 'category' => array(9 => '9:uncategorised', 0 => '1:root'), + 'categories' => array(9 => '9:uncategorised', 0 => '1:root') + ) + ); // View with parent, no children $cases[] = array(array('view' => 'article', 'id' => '42:question-for-everything', 'catid' => '9'), array( 'article' => array(42 => '42:question-for-everything'), - 'category' => array(9 => '9:uncategorised'), - 'categories' => array(9 => '9:uncategorised') + 'category' => array(9 => '9:uncategorised', 0 => '1:root'), + 'categories' => array(9 => '9:uncategorised', 0 => '1:root') ) ); @@ -156,11 +161,13 @@ public function casesGetPath() 'article' => array(42 => '42:question-for-everything'), 'category' => array(20 => '20:extensions', 19 => '19:joomla', - 14 => '14:sample-data-articles' + 14 => '14:sample-data-articles', + 0 => '1:root' ), 'categories' => array(20 => '20:extensions', 19 => '19:joomla', - 14 => '14:sample-data-articles' + 14 => '14:sample-data-articles', + 0 => '1:root' ) ) ); diff --git a/tests/unit/suites/libraries/cms/component/router/rules/JComponentRouterRulesNomenuTest.php b/tests/unit/suites/libraries/cms/component/router/rules/JComponentRouterRulesNomenuTest.php index 500fc69f96a16..a56178e2c8491 100644 --- a/tests/unit/suites/libraries/cms/component/router/rules/JComponentRouterRulesNomenuTest.php +++ b/tests/unit/suites/libraries/cms/component/router/rules/JComponentRouterRulesNomenuTest.php @@ -17,7 +17,7 @@ * @subpackage Component * @since 3.4 */ -class JComponentRouterRulesNomenuTest extends TestCase +class JComponentRouterRulesNomenuTest extends TestCaseDatabase { /** * Object under test @@ -27,6 +27,25 @@ class JComponentRouterRulesNomenuTest extends TestCase */ protected $object; + /** + * Gets the data set to be loaded into the database during setup + * + * @return PHPUnit_Extensions_Database_DataSet_CsvDataSet + * + * @since __DEPLOY_VERSION__ + */ + protected function getDataSet() + { + $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet(',', "'", '\\'); + + $dataSet->addTable('jos_categories', JPATH_TEST_DATABASE . '/jos_categories.csv'); + $dataSet->addTable('jos_content', JPATH_TEST_DATABASE . '/jos_content.csv'); + $dataSet->addTable('jos_extensions', JPATH_TEST_DATABASE . '/jos_extensions.csv'); + $dataSet->addTable('jos_menu', JPATH_TEST_DATABASE . '/jos_menu.csv'); + + return $dataSet; + } + /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. @@ -97,12 +116,43 @@ public function testParse() $this->assertEquals(array(), $segments); $this->assertEquals(array('option' => 'com_content', 'view' => 'featured'), $vars); - // Check if a view with ID is properly parsed - $segments = array('category', '23-the-question'); + /** + * Check if a view with exists ID is properly parsed + * Note: only segment from first category level is available for categories view + */ + $segments = array('categories', '14-sample-data-articles'); $vars = array('option' => 'com_content'); $this->object->parse($segments, $vars); $this->assertEquals(array(), $segments); - $this->assertEquals(array('option' => 'com_content', 'view' => 'category', 'id' => '23:the-question'), $vars); + $this->assertEquals(array('option' => 'com_content', 'view' => 'categories', 'id' => '14'), $vars); + + // Check if a view with exists ID is properly parsed + $segments = array('category', '14-sample-data-articles'); + $vars = array('option' => 'com_content'); + $this->object->parse($segments, $vars); + $this->assertEquals(array(), $segments); + $this->assertEquals(array('option' => 'com_content', 'view' => 'category', 'id' => '14'), $vars); + + // Check if a view with not exists ID is properly parsed + $segments = array('category', '1499-sample-data-articles'); + $vars = array('option' => 'com_content'); + $this->object->parse($segments, $vars); + $this->assertEquals(array('1499-sample-data-articles'), $segments); + $this->assertEquals(array('option' => 'com_content', 'view' => 'category'), $vars); + + // Check if a nested view with identifier is properly parsed + $segments = array('category', '14-sample-data-articles', '19-joomla', '20-extensions', '22-modules', '64-articles-modules'); + $vars = array('option' => 'com_content'); + $this->object->parse($segments, $vars); + $this->assertEquals(array(), $segments); + $this->assertEquals(array('option' => 'com_content', 'view' => 'category', 'id' => '64'), $vars); + + // Check if a single view with identifier in nested parent is properly parsed + $segments = array('article', '14-sample-data-articles', '19-joomla', '8-beginners'); + $vars = array('option' => 'com_content'); + $this->object->parse($segments, $vars); + $this->assertEquals(array(), $segments); + $this->assertEquals(array('option' => 'com_content', 'view' => 'article', 'id' => '8', 'catid' => '19'), $vars); // Check if a view that normally has an ID but which is missing is properly parsed $segments = array('category'); @@ -111,15 +161,57 @@ public function testParse() $this->assertEquals(array(), $segments); $this->assertEquals(array('option' => 'com_content', 'view' => 'category'), $vars); - // Test if the rule is properly skipped when a menu item is set + // Test noIDs $router = $this->object->get('router'); + $router->set('noIDs', true); + + /** + * Check if a view with exists ID is properly parsed + * Note: only segment from first category level is available for categories view now + */ + $segments = array('categories', 'sample-data-articles'); + $vars = array('option' => 'com_content'); + $this->object->parse($segments, $vars); + $this->assertEquals(array(), $segments); + $this->assertEquals(array('option' => 'com_content', 'view' => 'categories', 'id' => '14'), $vars); + + // Check if a view with exists ID is properly parsed + $segments = array('category', 'sample-data-articles'); + $vars = array('option' => 'com_content'); + $this->object->parse($segments, $vars); + $this->assertEquals(array(), $segments); + $this->assertEquals(array('option' => 'com_content', 'view' => 'category', 'id' => '14'), $vars); + + // Check if a view with not exists ID is properly parsed + $segments = array('category', 'unknown-sample-data-articles'); + $vars = array('option' => 'com_content'); + $this->object->parse($segments, $vars); + $this->assertEquals(array('unknown-sample-data-articles'), $segments); + $this->assertEquals(array('option' => 'com_content', 'view' => 'category'), $vars); + + // Check if a nested view with identifier is properly parsed + $segments = array('category', 'sample-data-articles', 'joomla', 'extensions', 'modules', 'articles-modules'); + $vars = array('option' => 'com_content'); + $this->object->parse($segments, $vars); + $this->assertEquals(array(), $segments); + $this->assertEquals(array('option' => 'com_content', 'view' => 'category', 'id' => '64'), $vars); + + // Check if a single view with identifier in nested parent is properly parsed + $segments = array('article', 'sample-data-articles', 'joomla', 'beginners'); + $vars = array('option' => 'com_content'); + $this->object->parse($segments, $vars); + $this->assertEquals(array(), $segments); + $this->assertEquals(array('option' => 'com_content', 'view' => 'article', 'id' => '8', 'catid' => '19'), $vars); + + // Test if the rule is properly skipped when a menu item is set $router->menu->expects($this->any()) ->method('getActive') ->will($this->returnValue(new stdClass)); - $segments = array('article', '42:the-answer'); + + $segments = array('category', 'sample-data-articles'); $vars = array('option' => 'com_content'); $this->object->parse($segments, $vars); - $this->assertEquals(array('article', '42:the-answer'), $segments); + $this->assertEquals(array('category', 'sample-data-articles'), $segments); $this->assertEquals(array('option' => 'com_content'), $vars); } @@ -159,5 +251,50 @@ public function testBuild() $this->object->build($query, $segments); $this->assertEquals(array('option' => 'com_content'), $query); $this->assertEquals(array('article', '42-the-answer'), $segments); + + // Test if a nested view with identifier is properly build + $query = array('option' => 'com_content', 'view' => 'category', 'id' => '64:articles-modules'); + $segments = array(); + $this->object->build($query, $segments); + $this->assertEquals(array('option' => 'com_content'), $query); + $this->assertEquals( + array('category', '14-sample-data-articles', '19-joomla', '20-extensions', '22-modules', '64-articles-modules'), + $segments + ); + + // Test if a single view with identifier in nested parent is properly build + $query = array('option' => 'com_content', 'view' => 'article', 'id' => '8:beginners', 'catid' => '19:joomla'); + $segments = array(); + $this->object->build($query, $segments); + $this->assertEquals(array('option' => 'com_content'), $query); + $this->assertEquals(array('article', '14-sample-data-articles', '19-joomla', '8-beginners'), $segments); + + // Test noIDs + $router = $this->object->get('router'); + $router->set('noIDs', true); + + // Test if a single view with identifier is properly build + $query = array('option' => 'com_content', 'view' => 'article', 'id' => '42:the-answer'); + $segments = array(); + $this->object->build($query, $segments); + $this->assertEquals(array('option' => 'com_content'), $query); + $this->assertEquals(array('article', 'the-answer'), $segments); + + // Test if a nested view with identifier is properly build + $query = array('option' => 'com_content', 'view' => 'category', 'id' => '64:articles-modules'); + $segments = array(); + $this->object->build($query, $segments); + $this->assertEquals(array('option' => 'com_content'), $query); + $this->assertEquals( + array('category', 'sample-data-articles', 'joomla', 'extensions', 'modules', 'articles-modules'), + $segments + ); + + // Test if a single view with identifier in nested parent is properly build + $query = array('option' => 'com_content', 'view' => 'article', 'id' => '8:beginners', 'catid' => '19:joomla'); + $segments = array(); + $this->object->build($query, $segments); + $this->assertEquals(array('option' => 'com_content'), $query); + $this->assertEquals(array('article', 'sample-data-articles', 'joomla', 'beginners'), $segments); } } diff --git a/tests/unit/suites/libraries/cms/component/router/stubs/JComponentRouterViewInspector.php b/tests/unit/suites/libraries/cms/component/router/stubs/JComponentRouterViewInspector.php index af46083206655..63f4788744fab 100644 --- a/tests/unit/suites/libraries/cms/component/router/stubs/JComponentRouterViewInspector.php +++ b/tests/unit/suites/libraries/cms/component/router/stubs/JComponentRouterViewInspector.php @@ -16,6 +16,8 @@ */ class JComponentRouterViewInspector extends JComponentRouterView { + protected $noIDs = false; + /** * Gets an attribute of the object * @@ -60,7 +62,18 @@ public function getCategorySegment($id, $query) if ($category) { - return array_reverse($category->getPath(), true); + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) + { + foreach ($path as &$segment) + { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; } return array(); @@ -91,8 +104,108 @@ public function getCategoriesSegment($id, $query) */ public function getArticleSegment($id, $query) { + if (!strpos($id, ':')) + { + $db = JFactory::getDbo(); + $dbquery = $db->getQuery(true); + $dbquery->select('alias') + ->from($dbquery->quoteName('#__content')) + ->where('id = ' . $dbquery->quote($id)); + $db->setQuery($dbquery); + + $id .= ':' . $db->loadResult(); + } + + if ($this->noIDs) + { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + return array((int) $id => $id); } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + * + * @since __DEPLOY_VERSION__ + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) + { + $category = JCategories::getInstance($this->getName())->get($query['id']); + + foreach ($category->getChildren() as $child) + { + if ($this->noIDs) + { + if ($child->alias == $segment) + { + return $child->id; + } + } + else + { + if ($child->id == (int) $segment) + { + return $child->id; + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + * + * @since __DEPLOY_VERSION__ + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for an article + * + * @param string $segment Segment of the article to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + * + * @since __DEPLOY_VERSION__ + */ + public function getArticleId($segment, $query) + { + if ($this->noIDs) + { + $db = JFactory::getDbo(); + $dbquery = $db->getQuery(true); + $dbquery->select($dbquery->qn('id')) + ->from($dbquery->qn('#__content')) + ->where('alias = ' . $dbquery->q($segment)) + ->where('catid = ' . $dbquery->q($query['id'])); + $db->setQuery($dbquery); + + return (int) $db->loadResult(); + } + + return (int) $segment; + } } /**