shell bypass 403
<?php
/**
* @package Joomla.Administrator
* @subpackage com_menus
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Menus\Administrator\Model;
use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Object\CMSObject;
use Joomla\Component\Menus\Administrator\Helper\MenusHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Menu Item Types Model for Menus.
*
* @since 1.6
*/
class MenutypesModel extends BaseDatabaseModel
{
/**
* A reverse lookup of the base link URL to Title
*
* @var array
*/
protected $rlu = [];
/**
* Method to auto-populate the model state.
*
* This method should only be called once per instantiation and is designed
* to be called on the first call to the getState() method unless the model
* configuration flag to ignore the request is set.
*
* @return void
*
* @note Calling getState in this method will result in recursion.
* @since 3.0.1
*/
protected function populateState()
{
parent::populateState();
$clientId = Factory::getApplication()->getInput()->get('client_id', 0);
$this->state->set('client_id', $clientId);
}
/**
* Method to get the reverse lookup of the base link URL to Title
*
* @return array Array of reverse lookup of the base link URL to Title
*
* @since 1.6
*/
public function getReverseLookup()
{
if (empty($this->rlu)) {
$this->getTypeOptions();
}
return $this->rlu;
}
/**
* Method to get the available menu item type options.
*
* @return array Array of groups with menu item types.
*
* @since 1.6
*/
public function getTypeOptions()
{
$lang = Factory::getLanguage();
$list = [];
// Get the list of components.
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select(
[
$db->quoteName('name'),
$db->quoteName('element', 'option'),
]
)
->from($db->quoteName('#__extensions'))
->where(
[
$db->quoteName('type') . ' = ' . $db->quote('component'),
$db->quoteName('enabled') . ' = 1',
]
)
->order($db->quoteName('name') . ' ASC');
$db->setQuery($query);
$components = $db->loadObjectList();
foreach ($components as $component) {
$options = $this->getTypeOptionsByComponent($component->option);
if ($options) {
$list[$component->name] = $options;
// Create the reverse lookup for link-to-name.
foreach ($options as $option) {
if (isset($option->request)) {
$this->addReverseLookupUrl($option);
if (isset($option->request['option'])) {
$componentLanguageFolder = JPATH_ADMINISTRATOR . '/components/' . $option->request['option'];
$lang->load($option->request['option'] . '.sys', JPATH_ADMINISTRATOR)
|| $lang->load($option->request['option'] . '.sys', $componentLanguageFolder);
}
}
}
}
}
// Allow a system plugin to insert dynamic menu types to the list shown in menus:
Factory::getApplication()->triggerEvent('onAfterGetMenuTypeOptions', [&$list, $this]);
return $list;
}
/**
* Method to create the reverse lookup for link-to-name.
* (can be used from onAfterGetMenuTypeOptions handlers)
*
* @param CMSObject $option Object with request array or string and title public variables
*
* @return void
*
* @since 3.1
*/
public function addReverseLookupUrl($option)
{
$this->rlu[MenusHelper::getLinkKey($option->request)] = $option->get('title');
}
/**
* Get menu types by component.
*
* @param string $component Component URL option.
*
* @return array
*
* @since 1.6
*/
protected function getTypeOptionsByComponent($component)
{
$options = [];
$client = ApplicationHelper::getClientInfo($this->getState('client_id'));
$mainXML = $client->path . '/components/' . $component . '/metadata.xml';
if (is_file($mainXML)) {
$options = $this->getTypeOptionsFromXml($mainXML, $component);
}
if (empty($options)) {
$options = $this->getTypeOptionsFromMvc($component);
}
if ($client->id == 1 && empty($options)) {
$options = $this->getTypeOptionsFromManifest($component);
}
return $options;
}
/**
* Get the menu types from an XML file
*
* @param string $file File path
* @param string $component Component option as in URL
*
* @return array|boolean
*
* @since 1.6
*/
protected function getTypeOptionsFromXml($file, $component)
{
$options = [];
// Attempt to load the xml file.
if (!$xml = simplexml_load_file($file)) {
return false;
}
// Look for the first menu node off of the root node.
if (!$menu = $xml->xpath('menu[1]')) {
return false;
} else {
$menu = $menu[0];
}
// If we have no options to parse, just add the base component to the list of options.
if (!empty($menu['options']) && $menu['options'] == 'none') {
// Create the menu option for the component.
$o = new CMSObject();
$o->title = (string) $menu['name'];
$o->description = (string) $menu['msg'];
$o->request = ['option' => $component];
$options[] = $o;
return $options;
}
// Look for the first options node off of the menu node.
if (!$optionsNode = $menu->xpath('options[1]')) {
return false;
} else {
$optionsNode = $optionsNode[0];
}
// Make sure the options node has children.
if (!$children = $optionsNode->children()) {
return false;
}
// Process each child as an option.
foreach ($children as $child) {
if ($child->getName() == 'option') {
// Create the menu option for the component.
$o = new CMSObject();
$o->title = (string) $child['name'];
$o->description = (string) $child['msg'];
$o->request = ['option' => $component, (string) $optionsNode['var'] => (string) $child['value']];
$options[] = $o;
} elseif ($child->getName() == 'default') {
// Create the menu option for the component.
$o = new CMSObject();
$o->title = (string) $child['name'];
$o->description = (string) $child['msg'];
$o->request = ['option' => $component];
$options[] = $o;
}
}
return $options;
}
/**
* Get menu types from MVC
*
* @param string $component Component option like in URLs
*
* @return array|boolean
*
* @since 1.6
*/
protected function getTypeOptionsFromMvc($component)
{
$options = [];
$views = [];
foreach ($this->getFolders($component) as $path) {
if (!is_dir($path)) {
continue;
}
$views = array_merge($views, Folder::folders($path, '.', false, true));
}
foreach ($views as $viewPath) {
$view = basename($viewPath);
// Ignore private views.
if (strpos($view, '_') !== 0) {
// Determine if a metadata file exists for the view.
$file = $viewPath . '/metadata.xml';
if (is_file($file)) {
// Attempt to load the xml file.
if ($xml = simplexml_load_file($file)) {
// Look for the first view node off of the root node.
if ($menu = $xml->xpath('view[1]')) {
$menu = $menu[0];
// If the view is hidden from the menu, discard it and move on to the next view.
if (!empty($menu['hidden']) && $menu['hidden'] == 'true') {
unset($xml);
continue;
}
// Do we have an options node or should we process layouts?
// Look for the first options node off of the menu node.
if ($optionsNode = $menu->xpath('options[1]')) {
$optionsNode = $optionsNode[0];
// Make sure the options node has children.
if ($children = $optionsNode->children()) {
// Process each child as an option.
foreach ($children as $child) {
if ($child->getName() == 'option') {
// Create the menu option for the component.
$o = new CMSObject();
$o->title = (string) $child['name'];
$o->description = (string) $child['msg'];
$o->request = ['option' => $component, 'view' => $view, (string) $optionsNode['var'] => (string) $child['value']];
$options[] = $o;
} elseif ($child->getName() == 'default') {
// Create the menu option for the component.
$o = new CMSObject();
$o->title = (string) $child['name'];
$o->description = (string) $child['msg'];
$o->request = ['option' => $component, 'view' => $view];
$options[] = $o;
}
}
}
} else {
$options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view));
}
}
unset($xml);
}
} else {
$options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view));
}
}
}
return $options;
}
/**
* Get menu types from Component manifest
*
* @param string $component Component option like in URLs
*
* @return array|boolean
*
* @since 3.7.0
*/
protected function getTypeOptionsFromManifest($component)
{
// Load the component manifest
$fileName = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml';
if (!is_file($fileName)) {
return false;
}
if (!($manifest = simplexml_load_file($fileName))) {
return false;
}
// Check for a valid XML root tag.
if ($manifest->getName() != 'extension') {
return false;
}
$options = [];
// Start with the component root menu.
$rootMenu = $manifest->administration->menu;
// If the menu item doesn't exist or is hidden do nothing.
if (!$rootMenu || in_array((string) $rootMenu['hidden'], ['true', 'hidden'])) {
return $options;
}
// Create the root menu option.
$ro = new \stdClass();
$ro->title = (string) trim($rootMenu);
$ro->description = '';
$ro->request = ['option' => $component];
// Process submenu options.
$submenu = $manifest->administration->submenu;
if (!$submenu) {
return $options;
}
foreach ($submenu->menu as $child) {
$attributes = $child->attributes();
$o = new \stdClass();
$o->title = (string) trim($child);
$o->description = '';
if ((string) $attributes->link) {
parse_str((string) $attributes->link, $request);
} else {
$request = [];
$request['option'] = $component;
$request['act'] = (string) $attributes->act;
$request['task'] = (string) $attributes->task;
$request['controller'] = (string) $attributes->controller;
$request['view'] = (string) $attributes->view;
$request['layout'] = (string) $attributes->layout;
$request['sub'] = (string) $attributes->sub;
}
$o->request = array_filter($request, 'strlen');
$options[] = new CMSObject($o);
// Do not repeat the default view link (index.php?option=com_abc).
if (count($o->request) == 1) {
$ro = null;
}
}
if ($ro) {
$options[] = new CMSObject($ro);
}
return $options;
}
/**
* Get the menu types from component layouts
*
* @param string $component Component option as in URLs
* @param string $view Name of the view
*
* @return array
*
* @since 1.6
*/
protected function getTypeOptionsFromLayouts($component, $view)
{
$options = [];
$layouts = [];
$layoutNames = [];
$lang = Factory::getLanguage();
$client = ApplicationHelper::getClientInfo($this->getState('client_id'));
// Get the views for this component.
foreach ($this->getFolders($component) as $folder) {
$path = $folder . '/' . $view . '/tmpl';
if (!is_dir($path)) {
$path = $folder . '/' . $view;
}
if (!is_dir($path)) {
continue;
}
$layouts = array_merge($layouts, Folder::files($path, '.xml$', false, true));
}
// Build list of standard layout names
foreach ($layouts as $layout) {
// Ignore private layouts.
if (strpos(basename($layout), '_') === false) {
// Get the layout name.
$layoutNames[] = basename($layout, '.xml');
}
}
// Get the template layouts
// @todo: This should only search one template -- the current template for this item (default of specified)
$folders = Folder::folders($client->path . '/templates', '', false, true);
// Array to hold association between template file names and templates
$templateName = [];
foreach ($folders as $folder) {
if (is_dir($folder . '/html/' . $component . '/' . $view)) {
$template = basename($folder);
$lang->load('tpl_' . $template . '.sys', $client->path)
|| $lang->load('tpl_' . $template . '.sys', $client->path . '/templates/' . $template);
$templateLayouts = Folder::files($folder . '/html/' . $component . '/' . $view, '.xml$', false, true);
foreach ($templateLayouts as $layout) {
// Get the layout name.
$templateLayoutName = basename($layout, '.xml');
// Add to the list only if it is not a standard layout
if (array_search($templateLayoutName, $layoutNames) === false) {
$layouts[] = $layout;
// Set template name array so we can get the right template for the layout
$templateName[$layout] = basename($folder);
}
}
}
}
// Process the found layouts.
foreach ($layouts as $layout) {
// Ignore private layouts.
if (strpos(basename($layout), '_') === false) {
$file = $layout;
// Get the layout name.
$layout = basename($layout, '.xml');
// Create the menu option for the layout.
$o = new CMSObject();
$o->title = ucfirst($layout);
$o->description = '';
$o->request = ['option' => $component, 'view' => $view];
// Only add the layout request argument if not the default layout.
if ($layout != 'default') {
// If the template is set, add in format template:layout so we save the template name
$o->request['layout'] = isset($templateName[$file]) ? $templateName[$file] . ':' . $layout : $layout;
}
// Load layout metadata if it exists.
if (is_file($file)) {
// Attempt to load the xml file.
if ($xml = simplexml_load_file($file)) {
// Look for the first view node off of the root node.
if ($menu = $xml->xpath('layout[1]')) {
$menu = $menu[0];
// If the view is hidden from the menu, discard it and move on to the next view.
if (!empty($menu['hidden']) && $menu['hidden'] == 'true') {
unset($xml);
unset($o);
continue;
}
// Populate the title and description if they exist.
if (!empty($menu['title'])) {
$o->title = trim((string) $menu['title']);
}
if (!empty($menu->message[0])) {
$o->description = trim((string) $menu->message[0]);
}
}
}
}
// Add the layout to the options array.
$options[] = $o;
}
}
return $options;
}
/**
* Get the folders with template files for the given component.
*
* @param string $component Component option as in URLs
*
* @return array
*
* @since 4.0.0
*/
private function getFolders($component)
{
$client = ApplicationHelper::getClientInfo($this->getState('client_id'));
if (!is_dir($client->path . '/components/' . $component)) {
return [];
}
$folders = Folder::folders($client->path . '/components/' . $component, '^view[s]?$', false, true);
$folders = array_merge($folders, Folder::folders($client->path . '/components/' . $component, '^tmpl?$', false, true));
if (!$folders) {
return [];
}
return $folders;
}
}