<?php
/**
* @brief Admin CP Dispatcher
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 18 Feb 2013
*/
namespace IPS\Dispatcher;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Admin CP Dispatcher
*/
class _Admin extends \IPS\Dispatcher\Standard
{
/**
* Controller Location
*/
public $controllerLocation = 'admin';
/**
* @brief Cached Menu
*/
protected $menu = NULL;
/**
* @brief Search Keywords
*/
public $searchKeywords = array();
/**
* @brief ACP Restrictions (for search keyword editing)
*/
public $moduleRestrictions = array();
/**
* @brief ACP Restriction for the current menu item (for search keyword editing)
*/
public $menuRestriction = NULL;
/**
* Init
*
* @return void
*/
public function init()
{
\IPS\Output::i()->sidebar['appmenu'] = '';
/* Disable sending of referrer to third party sites from AdminCP */
\IPS\Output::i()->sendHeader( "Referrer-Policy: origin-when-cross-origin" );
/* Sync stuff when in developer mode */
if ( \IPS\IN_DEV )
{
\IPS\Developer::sync();
}
if ( \IPS\Member::loggedIn()->member_id )
{
/* Build the menu */
$menu = $this->buildMenu();
/* Do we need to figure out the default? */
if ( !isset( \IPS\Request::i()->app ) )
{
foreach ( $menu['tabs'] as $app => $appData )
{
if ( isset( $menu['defaults'][ $app ] ) )
{
parse_str( $menu['defaults'][ $app ], $defaultQueryString );
foreach ( $defaultQueryString as $k => $v )
{
\IPS\Request::i()->$k = $v;
}
break;
}
}
}
}
/* Call parent */
static::baseCss();
static::baseJs();
/* Stuff needed for output */
if ( !\IPS\Request::i()->isAjax() )
{
/* Special grouped CSS files */
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'core.css', 'core', 'admin' ) );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'responsive.css', 'core', 'front' ) );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'responsive.css', 'core', 'admin' ) );
/* JS */
\IPS\Output::i()->globalControllers[] = 'core.admin.core.app';
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'admin.js' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-ui.js', 'core', 'interface' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-touchpunch.js', 'core', 'interface' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery.menuaim.js', 'core', 'interface' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery.nestedSortable.js', 'core', 'interface' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'admin_core.js', 'core', 'front' ) );
if ( \IPS\Member::loggedIn()->member_id )
{
/* These are just defaults in case we hit an immediate error, e.g. app or controller doesn't exist */
\IPS\Output::i()->sidebar['sidebar'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->sidebar( array(), 'core_overview' );
\IPS\Output::i()->sidebar['appmenu'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->appmenu( $menu, 'core' );
\IPS\Output::i()->sidebar['mobilenav'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->mobileNavigation( $menu, 'core' );
}
}
/* Check we're logged in and we have ACP access */
if( ( !\IPS\Member::loggedIn()->member_id or !\IPS\Member::loggedIn()->isAdmin() )
and ( \IPS\Request::i()->module !== 'system' or \IPS\Request::i()->controller !== 'login' )
and ( !\IPS\ENFORCE_ACCESS )
)
{
/* Make sure the right protocol is used. IIS, for example, does not like protocol relative URL's in redirects. (Ref: 970629) */
$protocol = \IPS\Http\Url::PROTOCOL_HTTP;
if ( \substr( \IPS\Settings::i()->base_url, 0, 5 ) == 'https' )
{
$protocol = \IPS\Http\Url::PROTOCOL_HTTPS;
}
$url = \IPS\Http\Url::internal( "app=core&module=system&controller=login", 'admin', NULL, array(), $protocol );
if ( \IPS\Session::i()->error )
{
$url = $url->setQueryString( 'error', \IPS\Session::i()->error->getMessage() );
}
if( !\IPS\Request::i()->isAjax() )
{
/* If someone calls this from command line, while it wouldn't work, the key won't be set */
if( isset( $_SERVER['QUERY_STRING'] ) )
{
$url = $url->setQueryString( 'ref', base64_encode( preg_replace( '!adsess=((\w){32}|&)!', "", $_SERVER['QUERY_STRING'] ) ) );
}
}
else if( isset( $_SERVER['HTTP_REFERER'] ) )
{
$previous = preg_replace( "/^(.+?)\/\?/", "", $_SERVER['HTTP_REFERER'] );
$url = $url->setQueryString( 'ref', base64_encode( preg_replace( '!adsess=.*?(&|$)!', "", $previous ) ) );
}
\IPS\Output::i()->redirect( $url );
}
/* Init */
try
{
parent::init();
}
catch ( \DomainException $e )
{
\IPS\Output::i()->error( $e->getMessage(), '2S100/' . $e->getCode(), $e->getCode() === 4 ? 403 : 404, '' );
}
/* If we are in recovery mode, but not actually doing the recovery process, or logging in, then we need them to remove the constant */
if ( \IPS\RECOVERY_MODE === TRUE AND !in_array( $this->controller, array( 'recovery', 'login' ) ) )
{
\IPS\Output::i()->error( 'recovery_mode_remove_constant', '1S107/3', 403, '' );
}
/* Permission Check */
if (
(
$this->module->key !== 'system' or
!in_array( $this->controller, array( 'login', 'language', 'theme', 'livesearch', 'editor', 'ajax' ) )
) and
(
$this->module->key !== 'members' or
$this->controller !== 'members' or
!in_array( \IPS\Request::i()->do, array( 'adminDetails', 'adminEmail', 'adminPassword' ) )
) and
/* This is slightly hacky, but the upgrader was moved to the system module, however the ACP restriction is still set to overview.
To avoid unintentionally removing restrictions via an upgrade by moving the restriction, we reference the overview module for the restriction check instead */
!\IPS\Member::loggedIn()->hasAcpRestriction( $this->application, ( $this->application->directory === 'core' and $this->module->key === 'system' and $this->controller === 'upgrade' ) ? \IPS\Application\Module::get( 'core', 'overview', 'admin' ) : $this->module )
)
{
\IPS\Output::i()->error( 'no_module_permission', '2S107/1', 403, '' );
}
/* ACP search keywords */
if ( \IPS\IN_DEV )
{
foreach ( \IPS\Db::i()->select( '*', 'core_acp_search_index' ) as $word )
{
$this->searchKeywords[ $word['url'] ]['lang_key'] = $word['lang_key'];
$this->searchKeywords[ $word['url'] ]['restriction'] = $word['restriction'];
$this->searchKeywords[ $word['url'] ]['keywords'][] = $word['keyword'];
}
$restrictions = array();
$file = \IPS\ROOT_PATH . "/applications/{$this->application->directory}/data/acprestrictions.json";
if ( file_exists( $file ) )
{
$restrictions = json_decode( file_get_contents( $file ), TRUE );
}
$this->moduleRestrictions[''] = 'acpmenu_norestriction';
if ( isset( $restrictions[ $this->module->key ] ) )
{
foreach ( $restrictions[ $this->module->key ] as $key => $values )
{
$this->moduleRestrictions[ $key ] = array_combine( $values, $values );
}
}
$appMenu = $this->application->acpMenu();
if ( isset( $appMenu[ $this->module->key ] ) )
{
foreach ( $appMenu[ $this->module->key ] as $menuItem )
{
if ( $menuItem['restriction'] and $menuItem['controller'] == $this->controller and ( !$menuItem['do'] or ( isset( \IPS\Request::i()->do ) and $menuItem['do'] == \IPS\Request::i()->do ) ) )
{
$this->menuRestriction = $menuItem['restriction'];
}
}
}
}
/* More stuff needed for output */
if ( !\IPS\Request::i()->isAjax() )
{
/* Menu and base navigation */
if ( \IPS\Member::loggedIn()->member_id )
{
/* Work out what tab we're on */
$currentTab = NULL;
foreach ( $this->application->acpMenu() as $moduleKey => $items )
{
if ( $moduleKey === $this->module->key )
{
foreach ( $items as $itemKey => $item )
{
if ( !$currentTab )
{
$currentTab = $item['tab'];
}
if ( $item['controller'] === $this->controller )
{
break;
}
}
}
}
if ( !$currentTab )
{
$currentTab = $this->application->directory;
}
/* Display */
if( $this->controller !== 'login' )
{
if ( isset( $menu['tabs'][ $currentTab ] ) )
{
\IPS\Output::i()->sidebar['sidebar'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->sidebar( $menu['tabs'][ $currentTab ], $this->application->directory . '_' . $this->module->key );
}
\IPS\Output::i()->sidebar['appmenu'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->appmenu( $menu, $currentTab );
\IPS\Output::i()->sidebar['mobilenav'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->mobileNavigation( $menu, $currentTab );
}
}
}
}
/**
* Build Menu
*
* @param bool If TRUE, will rebuild
* @return array
*/
public function buildMenu( $rebuild=FALSE )
{
$acpTabOrder = $this->_getAcpTabOrder();
if ( $this->menu === NULL or $rebuild === TRUE )
{
$this->menu = array( 'tabs' => array(), 'defaults' => array() );
foreach ( \IPS\Application::applications() as $app )
{
if ( \IPS\Application::appIsEnabled( $app->directory ) and \IPS\Application::load( $app->directory )->canAccess() )
{
$appMenu = $app->acpMenu();
if ( $acpTabOrder !== NULL and isset( $acpTabOrder[ $app->directory ] ) and $app->directory == 'nexus' )
{
uksort( $appMenu, function( $a, $b ) use ( $acpTabOrder, $app )
{
return array_search( "{$app->directory}_{$a}", $acpTabOrder[ $app->directory ] ) - array_search( "{$app->directory}_{$b}", $acpTabOrder[ $app->directory ] );
} );
}
foreach ( $appMenu as $moduleKey => $items )
{
foreach ( $items as $itemKey => $item )
{
$moduleToCheck = ( isset( $item['restriction_module'] ) ) ? $item['restriction_module'] : $moduleKey;
if ( \IPS\Member::loggedIn()->hasAcpRestriction( $app, $moduleToCheck ) )
{
if ( !$item['restriction'] or \IPS\Member::loggedIn()->hasAcpRestriction( $app, $moduleToCheck, $item['restriction'] ) )
{
$this->menu['tabs'][ $item['tab'] ][ "{$app->directory}_{$moduleKey}" ][ $itemKey ] = "app={$app->directory}&module={$moduleKey}&controller={$item['controller']}" . ( $item['do'] ? "&do={$item['do']}" : '' );
}
}
}
}
}
}
}
if ( $acpTabOrder !== NULL )
{
$_apps = array_keys( $acpTabOrder );
uksort( $this->menu['tabs'], function($a, $b) use ( $_apps )
{
return array_search( $a, $_apps ) - array_search( $b, $_apps );
} );
foreach( $acpTabOrder as $app => $submenu )
{
if ( !empty( $submenu ) )
{
if( isset( $this->menu['tabs'][ $app ] ) )
{
uksort( $this->menu['tabs'][ $app ], function($a, $b) use ( $submenu )
{
return array_search( $a, $submenu ) - array_search( $b, $submenu );
} );
}
if ( isset( $this->menu['defaults'] ) )
{
uksort( $this->menu['defaults'], function($a, $b) use ( $acpTabOrder )
{
return array_search( $a, $acpTabOrder );
} );
}
}
}
}
/* Now set the tab defaults */
foreach( $this->menu['tabs'] as $tab => $menu )
{
if ( !isset( $this->menu['defaults'][ $tab ] ) )
{
foreach( $menu as $group => $submenu )
{
$this->menu['defaults'][ $tab ] = array_values($submenu)[0];
break;
}
}
}
return $this->menu;
}
/**
* @brief Cached ACP tab order
*/
protected $acpTabOrder = NULL;
/**
* Figure out the ACP tab order
*
* @return array
*/
public function _getAcpTabOrder()
{
if( $this->acpTabOrder !== NULL )
{
return $this->acpTabOrder;
}
if ( isset( \IPS\Request::i()->cookie['acpTabs'] ) )
{
$this->acpTabOrder = json_decode( \IPS\Request::i()->cookie['acpTabs'], TRUE );
}
else
{
try
{
$this->acpTabOrder = json_decode( \IPS\Db::i()->select( 'data', 'core_acp_tab_order', array( 'id=?', \IPS\Member::loggedIn()->member_id ) )->first(), TRUE );
}
catch( \UnderflowException $ex )
{
$this->acpTabOrder = array( 'core' => array(), 'community' => array(), 'members' => array(), 'nexus' => array(), 'cms' => array(), 'stats' => array(), 'customization' => array() );
}
\IPS\Request::i()->setCookie( 'acpTabs', json_encode( $this->acpTabOrder ) );
}
return $this->acpTabOrder;
}
/**
* Do we have permission to use this module?
*
* @param \IPS\Application $app Application
* @param \IPS\Module|string $module Module
* @return bool
*/
public function hasPermission( $app, $module )
{
return \IPS\Member::loggedIn()->hasAcpRestriction( $app, $module );
}
/**
* Check ACP Permission
*
* @param string $key Permission Key
* @param \IPS\Application|null $app Application (NULL will default to current)
* @param \IPS\Module|string|null $module Module (NULL will default to current)
* @return void
*/
public function checkAcpPermission( $key, $app=NULL, $module=NULL )
{
if ( !\IPS\Member::loggedIn()->hasAcpRestriction( ( $app ?: $this->application ), ( $module ?: $this->module ), $key ) )
{
\IPS\Output::i()->error( 'no_module_permission', '2S107/2', 403, '' );
}
}
}