Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

+344 -164
View File
@@ -11,64 +11,102 @@
namespace IPS\Dispatcher;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateInterval;
use DomainException;
use IPS\Application;
use IPS\DateTime;
use IPS\Db;
use IPS\Developer;
use IPS\Helpers\Menu\Link;
use IPS\Http\Url;
use IPS\Lang;
use IPS\Member;
use IPS\Application\Module;
use IPS\Output;
use IPS\Platform\Bridge;
use IPS\Request;
use IPS\Session;
use IPS\Settings;
use IPS\Theme;
use UnderflowException;
use function array_merge;
use function count;
use function defined;
use function in_array;
use function is_array;
use function substr;
use const IPS\CIC;
use const IPS\DEMO_MODE;
use const IPS\ENFORCE_ACCESS;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Admin CP Dispatcher
*/
class _Admin extends \IPS\Dispatcher\Standard
class Admin extends Standard
{
/**
* Controller Location
*/
public $controllerLocation = 'admin';
public string $controllerLocation = 'admin';
/**
* @brief Cached Menu
*/
protected $menu = NULL;
protected ?array $menu = NULL;
/**
* @brief Search Keywords
*/
public $searchKeywords = array();
public array $searchKeywords = array();
/**
* @brief ACP Restrictions (for search keyword editing)
*/
public $moduleRestrictions = array();
public array $moduleRestrictions = array();
/**
* @brief ACP Restriction for the current menu item (for search keyword editing)
*/
public $menuRestriction = NULL;
public ?string $menuRestriction = NULL;
const PLATFORM_PERMISSION_UPGRADE_NEEDED = -1;
const PLATFORM_PERMISSION_FUTURE_EXPIRES = -2;
/**
* Cache built URL map
* @var null
*/
public static mixed $permissionUrls = NULL;
/**
* Init
*
* @return void
*/
public function init()
public function init() : void
{
\IPS\Output::i()->sidebar['appmenu'] = '';
Output::i()->sidebar['appmenu'] = '';
/* Sync stuff when in developer mode */
if ( \IPS\IN_DEV )
{
\IPS\Developer::sync();
Developer::sync();
}
if ( \IPS\Member::loggedIn()->member_id )
if ( Member::loggedIn()->member_id )
{
/* Build the menu */
$menu = $this->buildMenu();
/* Do we need to figure out the default? */
if ( !isset( \IPS\Request::i()->app ) )
if ( !isset( Request::i()->app ) )
{
foreach ( $menu['tabs'] as $app => $appData )
{
@@ -77,7 +115,7 @@ class _Admin extends \IPS\Dispatcher\Standard
parse_str( $menu['defaults'][ $app ], $defaultQueryString );
foreach ( $defaultQueryString as $k => $v )
{
\IPS\Request::i()->$k = $v;
Request::i()->$k = $v;
}
break;
}
@@ -89,54 +127,66 @@ class _Admin extends \IPS\Dispatcher\Standard
static::baseCss();
static::baseJs();
/* Loader extension */
foreach( Application::allExtensions( 'core', 'Loader' ) as $loader )
{
foreach( $loader->js() as $js )
{
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, $js );
}
foreach( $loader->css() as $css )
{
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, $css );
}
}
/* Stuff needed for output */
if ( !\IPS\Request::i()->isAjax() )
if ( !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' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'core/core.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' ) );
Output::i()->globalControllers[] = 'core.admin.core.app';
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin.js' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'jquery/jquery-ui.js', 'core', 'interface' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'jquery/jquery-touchpunch.js', 'core', 'interface' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'jquery/jquery.menuaim.js', 'core', 'interface' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'jquery/jquery.nestedSortable.js', 'core', 'interface' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_core.js', 'core', 'front' ) );
if ( \IPS\Member::loggedIn()->member_id )
if ( 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', 'core_overview_dashboard' );
\IPS\Output::i()->sidebar['mobilenav'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->mobileNavigation( $menu, 'core' );
Output::i()->sidebar['sidebar'] = Theme::i()->getTemplate( 'global', 'core' )->sidebar( array(), 'core_overview' );
Output::i()->sidebar['appmenu'] = Theme::i()->getTemplate( 'global', 'core' )->appmenu( $menu, 'core', 'core_overview_dashboard' );
Output::i()->sidebar['mobilenav'] = Theme::i()->getTemplate( 'global', 'core' )->mobileNavigation( $menu, 'core' );
Output::i()->sidebar['quickLinks'] = $this->_getAcpQuickLinks();
}
}
/* 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 )
if( ( !Member::loggedIn()->member_id or !Member::loggedIn()->isAdmin() )
and ( Request::i()->module !== 'system' or Request::i()->controller !== 'login' )
and ( !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 = Url::PROTOCOL_HTTP;
if ( substr( Settings::i()->base_url, 0, 5 ) == 'https' )
{
$protocol = \IPS\Http\Url::PROTOCOL_HTTPS;
$protocol = Url::PROTOCOL_HTTPS;
}
$url = \IPS\Http\Url::internal( "app=core&module=system&controller=login", 'admin', NULL, array(), $protocol );
$url = Url::internal( "app=core&module=system&controller=login", 'admin', NULL, array(), $protocol );
if ( \IPS\Session::i()->error )
if ( Session::i()->error and is_object( Session::i()->error ) )
{
$url = $url->setQueryString( 'error', \IPS\Session::i()->error->getMessage() );
$url = $url->setQueryString( 'error', Session::i()->error );
}
if( !\IPS\Request::i()->isAjax() )
if( !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'] ) )
@@ -150,7 +200,7 @@ class _Admin extends \IPS\Dispatcher\Standard
$url = $url->setQueryString( 'ref', base64_encode( $previous ) );
}
\IPS\Output::i()->redirect( $url );
Output::i()->redirect( $url );
}
/* Init */
@@ -158,65 +208,75 @@ class _Admin extends \IPS\Dispatcher\Standard
{
parent::init();
}
catch ( \DomainException $e )
catch ( DomainException $e )
{
\IPS\Output::i()->error( $e->getMessage(), '2S100/' . $e->getCode(), $e->getCode() === 4 ? 403 : 404, '' );
Output::i()->error( $e->getMessage(), '2S100/' . $e->getCode(), $e->getCode() === 4 ? 403 : 404, '' );
}
/* Loader extension - check for redirects */
foreach( Application::allExtensions( 'core', 'Loader' ) as $loader )
{
/* First come first serve here */
if( $redirect = $loader->checkForRedirect() )
{
Output::i()->redirect( $redirect );
}
}
/* Unless there is a flag telling us we have specifically added CSRF checks, assume any AdminCP action which contains more than app/module/controller/id (i.e. anything with "do") requires CSRF-protection */
if ( !isset( $this->classname::$csrfProtected ) and array_diff( array_keys( \IPS\Request::i()->url()->queryString ), array( 'app', 'module', 'controller', 'id' ) ) )
if ( !isset( $this->classname::$csrfProtected ) and array_diff( array_keys( Request::i()->url()->queryString ), array( 'app', 'module', 'controller', 'id' ) ) )
{
\IPS\Session::i()->csrfCheck();
Session::i()->csrfCheck();
}
\IPS\Db::i()->readWriteSeparation = FALSE;
Db::i()->readWriteSeparation = FALSE;
if ( isset( $this->classname::$allowRWSeparation ) and $this->classname::$allowRWSeparation )
{
\IPS\Db::i()->readWriteSeparation = TRUE;
Db::i()->readWriteSeparation = TRUE;
}
/* 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' ) ) )
if ( \IPS\RECOVERY_MODE AND !in_array( $this->controller, array( 'recovery', 'login' ) ) )
{
\IPS\Output::i()->error( 'recovery_mode_remove_constant', '1S107/3', 403, '' );
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' ) )
!in_array( $this->controller, array( 'login', 'language', 'theme', 'livesearch', 'editor', 'ajax' ) )
) and
/* Every admin can view and manage his own acp notification */
( $this->module->key !== 'overview' or $this->controller !== 'notifications' ) and
(
$this->module->key !== 'members' or
$this->controller !== 'members' or
!\in_array( \IPS\Request::i()->do, array( 'adminDetails', 'adminEmail', 'adminPassword' ) )
!in_array( 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 )
!Member::loggedIn()->hasAcpRestriction( $this->application, ( $this->application->directory === 'core' and $this->module->key === 'system' and $this->controller === 'upgrade' ) ? Application\Module::get( 'core', 'overview', 'admin' ) : $this->module )
)
{
\IPS\Output::i()->error( 'no_module_permission', '2S107/1', 403, '' );
Output::i()->error( 'no_module_permission', '2S107/1', 403, '' );
}
if( ! \IPS\Application::appIsEnabled( $this->application->directory ) )
if( ! Application::appIsEnabled( $this->application->directory ) )
{
\IPS\Output::i()->error( 'requested_route_404', '2S107/5', 404, '' );
Output::i()->error( 'requested_route_404', '2S107/5', 404, '' );
}
/* Support is not available for demos */
if ( \IPS\DEMO_MODE AND $this->module->application === 'core' AND $this->module->key === 'support' )
if ( DEMO_MODE AND $this->module->application === 'core' AND $this->module->key === 'support' )
{
\IPS\Output::i()->error( 'demo_mode_function_blocked', '1S107/4', 403, '' );
Output::i()->error( 'demo_mode_function_blocked', '1S107/4', 403, '' );
}
/* ACP search keywords */
if ( \IPS\IN_DEV )
{
foreach ( \IPS\Db::i()->select( '*', 'core_acp_search_index' ) as $word )
foreach ( 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'];
@@ -245,7 +305,7 @@ class _Admin extends \IPS\Dispatcher\Standard
{
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 ) ) )
if ( $menuItem['restriction'] and $menuItem['controller'] == $this->controller and ( !$menuItem['do'] or ( isset( Request::i()->do ) and $menuItem['do'] == Request::i()->do ) ) )
{
$this->menuRestriction = $menuItem['restriction'];
}
@@ -254,10 +314,10 @@ class _Admin extends \IPS\Dispatcher\Standard
}
/* More stuff needed for output */
if ( !\IPS\Request::i()->isAjax() )
if ( !Request::i()->isAjax() )
{
/* Menu and base navigation */
if ( \IPS\Member::loggedIn()->member_id )
if ( Member::loggedIn()->member_id )
{
/* Work out what tab we're on */
$currentTab = NULL;
@@ -288,23 +348,45 @@ class _Admin extends \IPS\Dispatcher\Standard
$additionalChecksPass = TRUE;
if( isset( $item['menu_checks'] ) AND \is_array( $item['menu_checks'] ) )
/* If we set a do=value in the menu, add that to the list of menu checks */
if( isset( $item['do'] ) and $item['do'] )
{
if( isset( $item['menu_checks']['do'] ) and !is_array( $item['menu_checks']['do'] ) )
{
$item['menu_checks']['do'] = array( $item['menu_checks']['do'] );
}
$item['menu_checks']['do'][] = $item['do'];
}
if( isset( $item['menu_checks'] ) AND is_array( $item['menu_checks'] ) )
{
foreach( $item['menu_checks'] as $key => $value )
{
if( !isset( \IPS\Request::i()->$key ) OR \IPS\Request::i()->$key != $value )
{
$additionalChecksPass = FALSE;
break;
}
if( isset( Request::i()->$key ) )
{
if( is_array( $value ) and !in_array( Request::i()->$key, $value ) )
{
$additionalChecksPass = false;
break;
}
elseif( !is_array( $value ) and Request::i()->$key != $value )
{
$additionalChecksPass = false;
break;
}
}
else
{
$additionalChecksPass = false;
break;
}
}
}
if ( $additionalChecksPass === TRUE and ( $item['controller'] === $this->controller or ( isset( $item['subcontrollers'] ) and \in_array( $this->controller, explode( ",", $item['subcontrollers'] ) ) ) ) )
if ( $additionalChecksPass === TRUE and ( $item['controller'] === $this->controller or ( isset( $item['subcontrollers'] ) and in_array( $this->controller, explode( ",", $item['subcontrollers'] ) ) ) ) )
{
$controllerForKey = ( isset( $item['menu_controller'] ) ) ? $item['menu_controller'] : $item['controller'];
$currentItem = $this->application->directory . "_" . $moduleKey . "_" . $controllerForKey;
$currentItem = $item['activemenuitem'] ?? ( $this->application->directory . "_" . $moduleKey . "_" . $controllerForKey );
if( $currentTab != $item['tab'] )
{
@@ -323,10 +405,10 @@ class _Admin extends \IPS\Dispatcher\Standard
/* Display */
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 );
Output::i()->sidebar['sidebar'] = 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, $currentItem );
\IPS\Output::i()->sidebar['mobilenav'] = \IPS\Theme::i()->getTemplate( 'global', 'core' )->mobileNavigation( $menu, $currentTab, $currentItem );
Output::i()->sidebar['appmenu'] = Theme::i()->getTemplate( 'global', 'core' )->appmenu( $menu, $currentTab, $currentItem );
Output::i()->sidebar['mobilenav'] = Theme::i()->getTemplate( 'global', 'core' )->mobileNavigation( $menu, $currentTab, $currentItem );
}
}
}
@@ -334,23 +416,23 @@ class _Admin extends \IPS\Dispatcher\Standard
/**
* Build Menu
*
* @param bool $rebuild If TRUE, will rebuild
* @return array
* @param bool $rebuild If TRUE, will rebuild
* @return array|null
*/
public function buildMenu( $rebuild=FALSE )
public function buildMenu(bool $rebuild=FALSE ): ?array
{
$acpTabOrder = $this->_getAcpTabOrder();
if ( $this->menu === NULL or $rebuild === TRUE )
{
$this->menu = array( 'tabs' => array(), 'defaults' => array() );
foreach ( \IPS\Application::applications() as $app )
$this->menu = array( 'tabs' => array(), 'defaults' => array(), 'badges' );
foreach ( Application::applications() as $app )
{
if ( \IPS\Application::appIsEnabled( $app->directory ) and \IPS\Application::load( $app->directory )->canAccess() )
if ( Application::appIsEnabled( $app->directory ) and 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 )
@@ -361,18 +443,18 @@ class _Admin extends \IPS\Dispatcher\Standard
foreach ( $appMenu as $moduleKey => $items )
{
foreach ( $items as $itemKey => $item )
{
if ( isset( $item['callback'] ) and !eval( $item['callback'] ) )
{
continue;
}
$moduleUrl = ( isset( $item['module_url'] ) ) ? $item['module_url'] : $moduleKey;
$moduleToCheck = ( isset( $item['restriction_module'] ) ) ? $item['restriction_module'] : $moduleKey;
if ( \IPS\Member::loggedIn()->hasAcpRestriction( $app, $moduleToCheck ) )
{
foreach ( $items as $itemKey => $item )
{
if ( isset( $item['callback'] ) and !eval( $item['callback'] ) )
{
continue;
}
$moduleUrl = ( isset( $item['module_url'] ) ) ? $item['module_url'] : $moduleKey;
$moduleToCheck = ( isset( $item['restriction_module'] ) ) ? $item['restriction_module'] : $moduleKey;
if ( Member::loggedIn()->hasAcpRestriction( $app, $moduleToCheck ) )
{
if( !$item['restriction'] )
{
$canAccess = TRUE;
@@ -392,7 +474,7 @@ class _Admin extends \IPS\Dispatcher\Standard
foreach( $restrictions as $restrictionKey )
{
if( \IPS\Member::loggedIn()->hasAcpRestriction( $app, $moduleToCheck, $restrictionKey ) )
if( Member::loggedIn()->hasAcpRestriction( $app, $moduleToCheck, $restrictionKey ) )
{
$canAccess = TRUE;
break;
@@ -400,28 +482,60 @@ class _Admin extends \IPS\Dispatcher\Standard
}
}
if ( $canAccess )
{
$this->menu['tabs'][ $item['tab'] ][ "{$app->directory}_{$moduleKey}" ][ $itemKey ] = "app={$app->directory}&module={$moduleUrl}&controller={$item['controller']}" . ( $item['do'] ? "&do={$item['do']}" : '' );
}
}
}
if( isset( $item['activemenuitem'] ) and $item['activemenuitem'] )
{
$groupKey = substr( $item['activemenuitem'], 0, strrpos( $item['activemenuitem'], '_' ) );
$itemKey = substr( $item['activemenuitem'], strrpos( $item['activemenuitem'], '_' ) + 1 );
}
else
{
$groupKey = $app->directory . "_" . $moduleKey;
}
if ( $canAccess )
{
$this->menu['tabs'][ $item['tab'] ][ "{$groupKey}" ][ $itemKey ] = "app={$app->directory}&module={$moduleUrl}&controller={$item['controller']}" . ( $item['do'] ? "&do={$item['do']}" : '' );
}
/* Custom badge */
if( isset( $item['badge'] ) and $item['badge'] )
{
$this->menu['badges'][ "{$groupKey}_{$itemKey}" ] = $item['badge'];
}
}
}
}
}
}
}
/* Developer Center */
if( \IPS\IN_DEV )
{
$currentApp = Request::i()->appKey ?? 'core';
foreach( $this->menu['tabs']['developer'] as $module => $controllers )
{
foreach( $controllers as $key => $url )
{
parse_str( $url, $params );
$params['module'] = 'developer';
$params['appKey'] = $currentApp;
$this->menu['tabs']['developer'][ $module ][ $key ] = http_build_query( $params );
}
}
}
if ( $acpTabOrder !== NULL )
{
$_apps = array_keys( $acpTabOrder );
uksort( $this->menu['tabs'], function($a, $b) use ( $_apps )
{
if( !\in_array( $a, $_apps ) )
if( !in_array( $a, $_apps ) )
{
return 1;
}
if( !\in_array( $b, $_apps ) )
if( !in_array( $b, $_apps ) )
{
return -1;
}
@@ -437,12 +551,12 @@ class _Admin extends \IPS\Dispatcher\Standard
{
uksort( $this->menu['tabs'][ $app ], function($a, $b) use ( $submenu )
{
if( !\in_array( $a, $submenu ) )
if( !in_array( $a, $submenu ) )
{
return 1;
}
if( !\in_array( $b, $submenu ) )
if( !in_array( $b, $submenu ) )
{
return -1;
}
@@ -475,47 +589,71 @@ class _Admin extends \IPS\Dispatcher\Standard
}
}
return $this->menu;
return Bridge::i()->alterAcpMenu( $this->menu );
}
/**
* Build an array of quick links for the top menu
*
* @return array|null
*/
protected function _getAcpQuickLinks() : ?array
{
$links = [];
if( Member::loggedIn()->hasAcpRestriction( 'core', 'support', 'get_support' ) )
{
$links[] = new Link( Url::internal( "app=core&module=support&controller=support&_new=1" ), 'support' );
$links[] = new Link( Url::internal( "app=core&module=support&controller=systemLogs" ), 'system_logs' );
$links[] = new Link( Url::internal( "app=core&module=support&controller=support&do=clearCaches" )->csrf(), 'health_clear_caches_button', dataAttributes: [ 'data-role' => 'clearCaches' ] );
}
if( Member::loggedIn()->hasAcpRestriction( 'core', 'settings', 'advanced_manage_tasks' ) )
{
$links[] = new Link( Url::internal( "app=core&module=settings&controller=advanced&do=tasks" ), 'task_manager' );
}
return count( $links ) ? $links : null;
}
/**
* @brief Cached ACP tab order
*/
protected $acpTabOrder = NULL;
protected ?array $acpTabOrder = NULL;
/**
* Figure out the ACP tab order
*
* @return array
* @return array|null
*/
public function _getAcpTabOrder()
public function _getAcpTabOrder(): ?array
{
if( $this->acpTabOrder !== NULL )
{
return $this->acpTabOrder;
}
if ( isset( \IPS\Request::i()->cookie['acpTabs'] ) AND !\IPS\Settings::i()->acp_menu_cookie_rebuild )
if ( isset( Request::i()->cookie['acpTabs'] ) AND ! Settings::i()->acp_menu_cookie_rebuild )
{
$this->acpTabOrder = json_decode( \IPS\Request::i()->cookie['acpTabs'], TRUE );
$this->acpTabOrder = json_decode( Request::i()->cookie['acpTabs'], TRUE );
}
else
{
if ( \IPS\Settings::i()->acp_menu_cookie_rebuild )
if ( Settings::i()->acp_menu_cookie_rebuild )
{
\IPS\Settings::i()->changeValues( array( 'acp_menu_cookie_rebuild' => 0 ) );
Settings::i()->changeValues( array( 'acp_menu_cookie_rebuild' => 0 ) );
}
try
{
$this->acpTabOrder = json_decode( \IPS\Db::i()->select( 'data', 'core_acp_tab_order', array( 'id=?', \IPS\Member::loggedIn()->member_id ) )->first(), TRUE );
$this->acpTabOrder = json_decode( Db::i()->select( 'data', 'core_acp_tab_order', array( 'id=?', Member::loggedIn()->member_id ) )->first(), TRUE );
}
catch( \UnderflowException $ex )
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 ) );
Request::i()->setCookie( 'acpTabs', json_encode( $this->acpTabOrder ) );
}
return $this->acpTabOrder;
@@ -524,60 +662,24 @@ class _Admin extends \IPS\Dispatcher\Standard
/**
* Display a link in a custom format
*
* @param string $url The URL from the menu system
* @param string $url The URL from the menu system
* @return string|null
*/
public function acpMenuCustom( $url ): ?string
public function acpMenuCustom( string $url ): ?string
{
if ( !\IPS\Application::appIsEnabled( 'cloud' ) AND $url == 'app=core&module=smartcommunity&controller=cloud' )
{
$url = \IPS\Http\Url::internal( $url, 'admin' );
$title = \IPS\Member::loggedIn()->language()->addToStack( "menu__core_smartcommunity_features" );
return <<<EOF
<a href='{$url}' style="opacity:30%">{$title} <i class="fa fa-lock"></i></a>
EOF;
}
return NULL;
return Bridge::i()->acpMenuCustom( $url );
}
/**
* Do we have permission to use this module?
*
* @param \IPS\Application $app Application
* @param \IPS\Module|string $module Module
* @param Application $app Application
* @param string|Module $module Module
* @return bool
*/
public function hasPermission( $app, $module )
public function hasPermission( Application $app, Module|string $module ): bool
{
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)
* @param boolean $return Return boolean (true/false) instead of throwing an error
* @return void
*/
public function checkAcpPermission( $key, $app=NULL, $module=NULL, $return=FALSE )
{
if ( !\IPS\Member::loggedIn()->hasAcpRestriction( ( $app ?: $this->application ), ( $module ?: $this->module ), $key ) )
{
if ( $return )
{
return FALSE;
}
\IPS\Output::i()->error( 'no_module_permission', '2S107/2', 403, '' );
}
if ( $return )
{
return TRUE;
}
return Member::loggedIn()->hasAcpRestriction( $app, $module );
}
/**
@@ -587,23 +689,23 @@ EOF;
*/
final public static function showSwitchLink(): bool
{
if ( \IPS\CIC )
if ( CIC )
{
return false;
}
/* Don't show if installation was less than a month ago */
if ( time() < ( (int) \IPS\Settings::i()->board_start + ( 86400 * 30 ) ) )
if ( time() < ( (int) Settings::i()->board_start + ( 86400 * 30 ) ) )
{
return false;
}
if ( ! isset( \IPS\Request::i()->cookie['acpLinkSnooze'] ) )
if ( ! isset( Request::i()->cookie['acpLinkSnooze'] ) )
{
return true;
}
$value = json_decode( \IPS\Request::i()->cookie['acpLinkSnooze'], true );
$value = json_decode( Request::i()->cookie['acpLinkSnooze'], true );
if ( $value['hits'] < 3 )
{
@@ -632,4 +734,82 @@ EOF;
return true;
}
/**
* Base CSS
*
* @return void
*/
public static function baseCss() : void
{
parent::baseCss();
/* Stuff for output */
if ( !Request::i()->isAjax() )
{
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'core/core.css', 'core', 'admin' ) );
}
if ( count( Lang::languages() ) > 1 )
{
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'flags.css', 'core', 'global' ) );
}
}
/**
* Finish
*
* @return void
*/
public function finish() : void
{
if( !Request::i()->isAjax() )
{
$dismiss = [];
$updated = false;
if ( isset( Request::i()->cookie['acpDeprecations'] ) )
{
$dismiss = json_decode( Request::i()->cookie['acpDeprecations'], TRUE );
if ( ! is_array( $dismiss ) )
{
$dismiss = [];
}
}
if ( isset( Request::i()->deprecationDismiss ) )
{
$updated = true;
$dismiss[ Request::i()->deprecationDismiss ] = DateTime::create()->add( new DateInterval( 'P1M' ) )->getTimestamp();
}
foreach ( $dismiss as $key => $value )
{
if ( $value < time() )
{
$updated = true;
unset( $dismiss[ $key ] );
}
}
if ( $updated )
{
Request::i()->setCookie( 'acpDeprecations', json_encode( $dismiss ), DateTime::create()->add( new DateInterval( 'P1Y' ) ) );
}
if ( isset( Request::i()->deprecationDismiss ) )
{
Output::i()->redirect( Request::i()->url()->stripQueryString( 'deprecationDismiss' ) );
}
}
/* Loader Extension */
foreach( Application::allExtensions( 'core', 'Loader' ) as $loader )
{
$loader->onFinish();
}
parent::finish();
}
}
+114 -88
View File
@@ -11,51 +11,77 @@
namespace IPS\Dispatcher;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateInterval;
use DomainException;
use IPS\Api\Exception as ApiException;
use IPS\Api\Key;
use IPS\Api\OAuthClient;
use IPS\Application;
use IPS\Data\Store;
use IPS\DateTime;
use IPS\Db;
use IPS\Db\Exception;
use IPS\Dispatcher;
use IPS\Http\Url;
use IPS\Lang;
use IPS\Log;
use IPS\Output;
use IPS\Request;
use IPS\Settings;
use OutOfRangeException;
use UnderflowException;
use function count;
use function defined;
use function in_array;
use function intval;
use const IPS\OAUTH_REQUIRES_HTTPS;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* @brief API Dispatcher
*/
class _Api extends \IPS\Dispatcher
class Api extends Dispatcher
{
/**
* @brief Controller Location
*/
public $controllerLocation = 'api';
public string $controllerLocation = 'api';
/**
* @brief Path
*/
public $path = NULL;
public ?string $path = NULL;
/**
* @brief Raw API Key
*/
public $rawApiKey = NULL;
public ?string $rawApiKey = NULL;
/**
* @brief Raw Access Token
*/
public $rawAccessToken = NULL;
public ?string $rawAccessToken = NULL;
/**
* @brief API Key Object
*/
public $apiKey = NULL;
public ?Key $apiKey = NULL;
/**
* @brief Access Token Details
*/
public $accessToken = NULL;
public ?array $accessToken = NULL;
/**
* @brief Language
*/
public $language = NULL;
public ?Lang $language = NULL;
/**
* Can the Response be cached?
@@ -68,9 +94,9 @@ class _Api extends \IPS\Dispatcher
* Init
*
* @return void
* @throws \DomainException
* @throws DomainException
*/
public function init()
public function init() : void
{
try
{
@@ -87,7 +113,7 @@ class _Api extends \IPS\Dispatcher
$this->_setAccessToken();
if ( !$this->accessToken['scope'] or !json_decode( $this->accessToken['scope'] ) )
{
throw new \IPS\Api\Exception( 'NO_SCOPES', '3S290/B', 401, 'insufficient_scope' );
throw new ApiException( 'NO_SCOPES', '3S290/B', 401, 'insufficient_scope' );
}
}
elseif ( $this->rawApiKey )
@@ -96,32 +122,32 @@ class _Api extends \IPS\Dispatcher
}
else
{
throw new \IPS\Api\Exception( 'NO_API_KEY', '2S290/6', 401 );
throw new ApiException( 'NO_API_KEY', '2S290/6', 401 );
}
/* Set other data */
$this->_setLanguage();
/* We don't want to cache any output for Zapier Requests */
if( \IPS\Request::i()->isZapier() )
if( Request::i()->isZapier() )
{
$this->cacheResponse = FALSE;
}
}
catch ( \IPS\Api\Exception $e )
catch ( ApiException $e )
{
/* Build response */
$response = json_encode( array( 'errorCode' => $e->exceptionCode, 'errorMessage' => $e->getMessage() ), JSON_PRETTY_PRINT );
/* Do we need to log this? */
if ( $this->rawApiKey !== 'test' and \in_array( $e->exceptionCode, array( '2S290/8', '2S290/B', '3S290/7', '3S290/9' ) ) )
if ( $this->rawApiKey !== 'test' and in_array( $e->exceptionCode, array( '2S290/8', '2S290/B', '3S290/7', '3S290/9' ) ) )
{
$this->_log( $response, $e->getCode(), \in_array( $e->exceptionCode, array( '3S290/7', '3S290/9', '3S290/B' ) ) );
$this->_log( (array)$response, $e->getCode(), in_array($e->exceptionCode, array('3S290/7', '3S290/9', '3S290/B')));
}
/* Output */
$this->_respond( $response, $e->getCode(), $e->oauthError );
$this->_respond($response, $e->getCode(), $e->oauthError);
}
}
@@ -130,23 +156,23 @@ class _Api extends \IPS\Dispatcher
*
* @return void
*/
protected function _setPath()
protected function _setPath() : void
{
/* Decode URL */
if ( \IPS\Settings::i()->use_friendly_urls and \IPS\Settings::i()->htaccess_mod_rewrite and mb_substr( \IPS\Request::i()->url()->data[ \IPS\Http\Url::COMPONENT_PATH ], -14 ) !== '/api/index.php' )
if ( Settings::i()->use_friendly_urls and Settings::i()->htaccess_mod_rewrite and mb_substr( Request::i()->url()->data[ Url::COMPONENT_PATH ], -14 ) !== '/api/index.php' )
{
/* We are using Mod Rewrite URL's, so look in the path */
$this->path = mb_substr( \IPS\Request::i()->url()->data[ \IPS\Http\Url::COMPONENT_PATH ], mb_strpos( \IPS\Request::i()->url()->data[ \IPS\Http\Url::COMPONENT_PATH ], '/api/' ) + 5 );
$this->path = mb_substr( Request::i()->url()->data[ Url::COMPONENT_PATH ], mb_strpos( Request::i()->url()->data[ Url::COMPONENT_PATH ], '/api/' ) + 5 );
/* nginx won't convert the 'fake' query string to $_GET params, so do this now */
if ( ! empty( \IPS\Request::i()->url()->data[ \IPS\Http\Url::COMPONENT_QUERY ] ) )
if ( ! empty( Request::i()->url()->data[ Url::COMPONENT_QUERY ] ) )
{
parse_str( \IPS\Request::i()->url()->data[ \IPS\Http\Url::COMPONENT_QUERY ], $params );
parse_str( Request::i()->url()->data[ Url::COMPONENT_QUERY ], $params );
foreach ( $params as $k => $v )
{
if ( ! isset( \IPS\Request::i()->$k ) )
if ( ! isset( Request::i()->$k ) )
{
\IPS\Request::i()->$k = $v;
Request::i()->$k = $v;
}
}
}
@@ -154,7 +180,7 @@ class _Api extends \IPS\Dispatcher
else
{
/* Otherwise we are not, so we need the query string instead, which is actually easier */
$this->path = \IPS\Request::i()->url()->data[ \IPS\Http\Url::COMPONENT_QUERY ];
$this->path = Request::i()->url()->data[ Url::COMPONENT_QUERY ];
/* However, if we passed any actual query string arguments, we need to strip those */
if( mb_strpos( $this->path, '&' ) )
@@ -170,11 +196,11 @@ class _Api extends \IPS\Dispatcher
* @note OAuth requires Access Tokens only be transmitted over TLS, so if the request isn't secure, we ignore OAuth credentials
* @return void
*/
public function _setRawCredentials()
public function _setRawCredentials() : void
{
if ( $authorizationHeader = \IPS\Request::i()->authorizationHeader() )
if ( $authorizationHeader = Request::i()->authorizationHeader() )
{
if ( mb_substr( $authorizationHeader, 0, 7 ) === 'Bearer ' and ( !\IPS\OAUTH_REQUIRES_HTTPS or \IPS\Request::i()->isSecure() ) )
if ( mb_substr( $authorizationHeader, 0, 7 ) === 'Bearer ' and ( !OAUTH_REQUIRES_HTTPS or Request::i()->isSecure() ) )
{
$this->rawAccessToken = mb_substr( $authorizationHeader, 7 );
}
@@ -193,39 +219,39 @@ class _Api extends \IPS\Dispatcher
* Check the IP Address isn't banned
*
* @return void
* @throws \IPS\Api\Exception
* @throws ApiException
*/
protected function _checkIpAddressIsAllowed()
protected function _checkIpAddressIsAllowed() : void
{
/* Check the IP address is banned */
if ( \IPS\Request::i()->ipAddressIsBanned() )
if ( Request::i()->ipAddressIsBanned() )
{
throw new \IPS\Api\Exception( 'IP_ADDRESS_BANNED', '1S290/A', 403 );
throw new ApiException( 'IP_ADDRESS_BANNED', '1S290/A', 403 );
}
/* If we have tried to access the API with a bad key more than 10 times, ban the IP address */
if ( \IPS\Db::i()->select( 'COUNT(*)', 'core_api_logs', array( 'ip_address=? AND is_bad_key=1', \IPS\Request::i()->ipAddress() ) )->first() > 10 )
if ( Db::i()->select( 'COUNT(*)', 'core_api_logs', array( 'ip_address=? AND is_bad_key=1', Request::i()->ipAddress() ) )->first() > 10 )
{
/* Remove the flag from these logs so that if the admin unbans the IP we aren't immediately banned again */
\IPS\Db::i()->update( 'core_api_logs', array( 'is_bad_key' => 0 ), array( 'ip_address=?', \IPS\Request::i()->ipAddress() ) );
Db::i()->update( 'core_api_logs', array( 'is_bad_key' => 0 ), array( 'ip_address=?', Request::i()->ipAddress() ) );
/* Then insert the ban... */
\IPS\Db::i()->insert( 'core_banfilters', array(
Db::i()->insert( 'core_banfilters', array(
'ban_type' => 'ip',
'ban_content' => \IPS\Request::i()->ipAddress(),
'ban_content' => Request::i()->ipAddress(),
'ban_date' => time(),
'ban_reason' => 'API',
) );
unset( \IPS\Data\Store::i()->bannedIpAddresses );
unset( Store::i()->bannedIpAddresses );
/* And throw an error */
throw new \IPS\Api\Exception( 'IP_ADDRESS_BANNED', '1S290/C', 403 );
throw new ApiException( 'IP_ADDRESS_BANNED', '1S290/C', 403 );
}
/* If we have tried to access the API with a bad key more than once in the last 5 minutes, throw an error to prevent brute-forcing */
if ( \IPS\Db::i()->select( 'COUNT(*)', 'core_api_logs', array( 'ip_address=? AND is_bad_key=1 AND date>?', \IPS\Request::i()->ipAddress(), \IPS\DateTime::create()->sub( new \DateInterval( 'PT5M' ) )->getTimestamp() ) )->first() > 1 )
if ( Db::i()->select( 'COUNT(*)', 'core_api_logs', array( 'ip_address=? AND is_bad_key=1 AND date>?', Request::i()->ipAddress(), DateTime::create()->sub( new DateInterval( 'PT5M' ) )->getTimestamp() ) )->first() > 1 )
{
throw new \IPS\Api\Exception( 'TOO_MANY_REQUESTS_WITH_BAD_KEY', '1S290/D', 429 );
throw new ApiException( 'TOO_MANY_REQUESTS_WITH_BAD_KEY', '1S290/D', 429 );
}
}
@@ -234,20 +260,20 @@ class _Api extends \IPS\Dispatcher
*
* @return void
*/
public function _setApiKey()
public function _setApiKey() : void
{
try
{
$this->apiKey = \IPS\Api\Key::load( $this->rawApiKey );
$this->apiKey = Key::load( $this->rawApiKey );
if ( $this->apiKey->allowed_ips and !\in_array( \IPS\Request::i()->ipAddress(), explode( ',', $this->apiKey->allowed_ips ) ) )
if ( $this->apiKey->allowed_ips and !in_array( Request::i()->ipAddress(), explode( ',', $this->apiKey->allowed_ips ) ) )
{
throw new \IPS\Api\Exception( 'IP_ADDRESS_NOT_ALLOWED', '2S290/8', 403 );
throw new ApiException( 'IP_ADDRESS_NOT_ALLOWED', '2S290/8', 403 );
}
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_API_KEY', '3S290/7', 401 );
throw new ApiException( 'INVALID_API_KEY', '3S290/7', 401 );
}
}
@@ -256,15 +282,15 @@ class _Api extends \IPS\Dispatcher
*
* @return void
*/
public function _setAccessToken()
public function _setAccessToken() : void
{
try
{
$this->accessToken = \IPS\Api\OAuthClient::accessTokenDetails( $this->rawAccessToken );
$this->accessToken = OAuthClient::accessTokenDetails( $this->rawAccessToken );
}
catch ( \UnderflowException $e )
catch ( UnderflowException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ACCESS_TOKEN', '3S290/9', 401, 'invalid_token' );
throw new ApiException( 'INVALID_ACCESS_TOKEN', '3S290/9', 401, 'invalid_token' );
}
}
@@ -273,24 +299,24 @@ class _Api extends \IPS\Dispatcher
*
* @return Lang
*/
public function _setLanguage()
public function _setLanguage() : Lang
{
try
{
if ( isset( $_SERVER['HTTP_X_IPS_LANGUAGE'] ) )
{
$this->language = \IPS\Lang::load( \intval( $_SERVER['HTTP_X_IPS_LANGUAGE'] ) );
$this->language = Lang::load( intval( $_SERVER['HTTP_X_IPS_LANGUAGE'] ) );
}
else
{
$this->language = \IPS\Lang::load( \IPS\Lang::defaultLanguage() );
$this->language = Lang::load( Lang::defaultLanguage() );
}
return $this->language;
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_LANGUAGE', '2S290/9', 400, 'invalid_request' );
throw new ApiException( 'INVALID_LANGUAGE', '2S290/9', 400, 'invalid_request' );
}
}
@@ -299,7 +325,7 @@ class _Api extends \IPS\Dispatcher
*
* @return void
*/
public function run()
public function run() : void
{
$shouldLog = FALSE;
try
@@ -309,35 +335,35 @@ class _Api extends \IPS\Dispatcher
$app = array_shift( $pathBits );
if ( !preg_match( '/^[a-z0-9]+$/', $app ) )
{
throw new \IPS\Api\Exception( 'INVALID_APP', '3S290/3', 400 );
throw new ApiException( 'INVALID_APP', '3S290/3', 400 );
}
$controller = array_shift( $pathBits );
if ( !preg_match( '/^[a-z0-9]+$/', $controller ) )
{
throw new \IPS\Api\Exception( 'INVALID_CONTROLLER', '3S290/4', 400 );
throw new ApiException( 'INVALID_CONTROLLER', '3S290/4', 400 );
}
/* Load the app */
try
{
$app = \IPS\Application::load( $app );
$app = Application::load( $app );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_APP', '2S290/1', 404 );
throw new ApiException( 'INVALID_APP', '2S290/1', 404 );
}
/* Check it's enabled */
if ( !$app->enabled )
{
throw new \IPS\Api\Exception( 'APP_DISABLED', '1S290/2', 503 );
throw new ApiException( 'APP_DISABLED', '1S290/2', 503 );
}
/* Get the controller */
$class = 'IPS\\' . $app->directory . '\\api\\' . $controller;
if ( !class_exists( $class ) )
{
throw new \IPS\Api\Exception( 'INVALID_CONTROLLER', '2S290/5', 404 );
throw new ApiException( 'INVALID_CONTROLLER', '2S290/5', 404 );
}
/* Run it */
@@ -351,55 +377,55 @@ class _Api extends \IPS\Dispatcher
$output = $response->getOutput();
$this->language->parseOutputForDisplay( $output );
$this->_respond( json_encode( $output, JSON_PRETTY_PRINT ), $response->httpCode, NULL, $shouldLog, TRUE );
$this->_respond(json_encode($output, JSON_PRETTY_PRINT), $response->httpCode, NULL, $shouldLog);
}
catch ( \IPS\Api\Exception $e )
catch ( ApiException $e )
{
$this->_respond( json_encode( array( 'errorCode' => $e->exceptionCode, 'errorMessage' => $e->getMessage() ), JSON_PRETTY_PRINT ), $e->getCode(), $e->oauthError, $shouldLog );
$this->_respond(json_encode(array('errorCode' => $e->exceptionCode, 'errorMessage' => $e->getMessage()), JSON_PRETTY_PRINT), $e->getCode(), $e->oauthError, $shouldLog);
}
catch ( \Exception $e )
{
\IPS\Log::log( $e, 'api' );
Log::log( $e, 'api' );
$this->_respond( json_encode( array( 'errorCode' => 'EX' . $e->getCode(), 'errorMessage' => \IPS\IN_DEV ? $e->getMessage() : 'UNKNOWN_ERROR' ), JSON_PRETTY_PRINT ), 500 );
$this->_respond(json_encode(array('errorCode' => 'EX' . $e->getCode(), 'errorMessage' => \IPS\IN_DEV ? $e->getMessage() : 'UNKNOWN_ERROR'), JSON_PRETTY_PRINT), 500);
}
}
/**
* @brief Parameters to mask in logs per the controller
*/
protected $parametersToMask = NULL;
protected ?array $parametersToMask = NULL;
/**
* Log
*
* @param array $response Response to output
* @param int $httpResponseCode HTTP Response Code
* @param bool $isBadKey Was the ley invalid?
* @param array|string $response Response to output
* @param int $httpResponseCode HTTP Response Code
* @param bool $isBadKey Was the ley invalid?
* @return void
*/
protected function _log( $response, $httpResponseCode, $isBadKey=FALSE )
protected function _log( array|string $response, int $httpResponseCode, bool $isBadKey=FALSE ) : void
{
try
{
$_requestData = $_REQUEST;
if( $this->parametersToMask AND \count( $this->parametersToMask ) )
if( $this->parametersToMask AND count( $this->parametersToMask ) )
{
foreach( $_requestData as $k => $v )
{
if( \in_array( $k, $this->parametersToMask ) )
if( in_array( $k, $this->parametersToMask ) )
{
$_requestData[ $k ] = '******';
}
}
}
\IPS\Db::i()->insert( 'core_api_logs', array(
Db::i()->insert( 'core_api_logs', array(
'endpoint' => $this->path,
'method' => $_SERVER['REQUEST_METHOD'],
'api_key' => $this->rawApiKey,
'ip_address' => \IPS\Request::i()->ipAddress(),
'ip_address' => Request::i()->ipAddress(),
'request_data' => json_encode( $_requestData, JSON_PRETTY_PRINT ),
'response_code' => $httpResponseCode,
'response_output' => $response,
@@ -410,21 +436,21 @@ class _Api extends \IPS\Dispatcher
'access_token' => $this->rawAccessToken,
) );
}
catch ( \IPS\Db\Exception $e ) {}
catch ( Exception $e ) {}
}
/**
* Output response
*
* @param string $response Response to output
* @param int $httpResponseCode HTTP Response Code
* @param NULL|string $oauthError OAuth error
* @param bool $log Whether or not to log the response
* @param string $response Response to output
* @param int $httpResponseCode HTTP Response Code
* @param string|null $oauthError OAuth error
* @param bool $log Whether or not to log the response
* @return void
*/
protected function _respond( $response, $httpResponseCode, $oauthError=NULL, $log=FALSE )
protected function _respond( string $response, int $httpResponseCode, string $oauthError=NULL, bool $log=FALSE ) : void
{
$headers = $this->canBeCached() ? \IPS\Output::getCacheHeaders( time(), 60 ) : \IPS\Output::getNoCacheHeaders();
$headers = $this->canBeCached() ? Output::getCacheHeaders( time(), 60 ) : Output::getNoCacheHeaders();
if ( $this->rawAccessToken and $oauthError )
{
@@ -433,10 +459,10 @@ class _Api extends \IPS\Dispatcher
if ( $log )
{
$this->_log( $response, $httpResponseCode );
$this->_log($response, $httpResponseCode);
}
\IPS\Output::i()->sendOutput( $response, $httpResponseCode, 'application/json', $headers );
Output::i()->sendOutput( $response, $httpResponseCode, 'application/json', $headers );
}
/**
+31 -24
View File
@@ -11,50 +11,57 @@
namespace IPS\Dispatcher;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Application;
use IPS\Application\Module;
use IPS\Data\Store;
use IPS\Dispatcher;
use IPS\Request;
use IPS\Settings;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* @brief Build/Tools Dispatcher
*/
class _Build extends \IPS\Dispatcher
class Build extends Dispatcher
{
/**
* @brief Controller Location
*/
public $controllerLocation = 'front';
public string $controllerLocation = 'front';
/**
* @brief Application
*/
public $application = 'core';
public string $application = 'core';
/**
* @brief Module
*/
public $module = 'system';
public string $module = 'system';
/**
* @brief Step
*/
public $step = 1;
public int $step = 1;
/**
* Initiator
*
* @return void
*/
public function init()
public function init() : void
{
$modules = \IPS\Application\Module::modules();
$this->application = \IPS\Application::load('core');
$modules = Module::modules();
$this->application = Application::load('core');
$this->module = $modules['core']['front']['system'];
$this->controller = 'build';
return true;
}
/**
@@ -62,31 +69,31 @@ class _Build extends \IPS\Dispatcher
*
* @return void
*/
public function run()
public function run() : void
{
if ( isset( \IPS\Request::i()->force ) )
if ( isset( Request::i()->force ) )
{
if ( isset( \IPS\Data\Store::i()->builder_building ) )
if ( isset( Store::i()->builder_building ) )
{
unset( \IPS\Data\Store::i()->builder_building );
unset( Store::i()->builder_building );
}
}
else
{
if ( isset( \IPS\Data\Store::i()->builder_building ) and ! empty( \IPS\Data\Store::i()->builder_building ) )
if ( isset( Store::i()->builder_building ) and ! empty( Store::i()->builder_building ) )
{
/* We're currently rebuilding */
if ( time() - \IPS\Data\Store::i()->builder_building < 180 )
if ( time() - Store::i()->builder_building < 180 )
{
print "Builder is already running. To force a rebuild anyway, add &force=1 on the end of your URL";
exit();
}
}
\IPS\Data\Store::i()->builder_building = time();
Store::i()->builder_building = time();
}
\IPS\Settings::i()->changeValues( array( 'site_online' => 0 ) );
Settings::i()->changeValues( array( 'site_online' => 0 ) );
}
/**
@@ -94,13 +101,13 @@ class _Build extends \IPS\Dispatcher
*
* @return void
*/
public function buildDone()
public function buildDone() : void
{
if ( isset( \IPS\Data\Store::i()->builder_building ) )
if ( isset( Store::i()->builder_building ) )
{
unset( \IPS\Data\Store::i()->builder_building );
unset( Store::i()->builder_building );
}
\IPS\Settings::i()->changeValues( array( 'site_online' => 1 ) );
Settings::i()->changeValues( array( 'site_online' => 1 ) );
}
}
+97 -76
View File
@@ -11,26 +11,45 @@
namespace IPS\Dispatcher;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\core\DataLayer;
use IPS\Dispatcher;
use IPS\Http\Url;
use IPS\Log;
use IPS\Member;
use IPS\Node\Model;
use IPS\Output;
use IPS\Request;
use IPS\Settings;
use IPS\Theme;
use OutOfRangeException;
use function defined;
use function get_called_class;
use function intval;
use function mb_stristr;
use const IPS\ENFORCE_ACCESS;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Abstract class that Controllers should extend
*/
abstract class _Controller
abstract class Controller
{
/**
* @brief Base URL
*/
public $url;
public mixed $url;
/**
* @brief Is this for displaying "content"? Affects if advertisements may be shown
*/
public $isContentPage = TRUE;
public bool $isContentPage = TRUE;
/**
* @brief These properties are used to specify datalayer context properties.
@@ -39,21 +58,21 @@ abstract class _Controller
'community_area' => array( 'value' => 'search', 'odkUpdate' => 'true' )
* )
*/
public static $dataLayerContext = array();
public static array $dataLayerContext = array();
/**
* Constructor
*
* @param \IPS\Http\Url|NULL $url The base URL for this controller or NULL to calculate automatically
* @param Url|null $url The base URL for this controller or NULL to calculate automatically
* @return void
*/
public function __construct( $url=NULL )
public function __construct( mixed $url=NULL )
{
if ( $url === NULL )
{
$class = \get_called_class();
$class = get_called_class();
$exploded = explode( '\\', $class );
$this->url = \IPS\Http\Url::internal( "app={$exploded[1]}&module={$exploded[4]}&controller={$exploded[5]}", \IPS\Dispatcher::i()->controllerLocation );
$this->url = Url::internal( "app={$exploded[1]}&module={$exploded[4]}&controller={$exploded[5]}", Dispatcher::i()->controllerLocation );
}
else
{
@@ -64,12 +83,12 @@ abstract class _Controller
/**
* Force a specific method within a controller to execute. Useful for unit testing.
*
* @param null|string $method The specific method to call
* @param string|null $method The specific method to call
* @return mixed
*/
public function forceExecute( $method=NULL )
public function forceExecute( ?string $method=NULL ): mixed
{
if( \IPS\ENFORCE_ACCESS === true and $method !== null )
if( ENFORCE_ACCESS and $method !== null )
{
if ( method_exists( $this, $method ) )
{
@@ -77,11 +96,13 @@ abstract class _Controller
}
else
{
return $this->execute();
$this->execute();
return null;
}
}
return $this->execute();
$this->execute();
return null;
}
/**
@@ -89,26 +110,26 @@ abstract class _Controller
*
* @return void
*/
public function execute()
public function execute() : void
{
if ( !empty( static::$dataLayerContext ) AND \IPS\Settings::i()->core_datalayer_enabled AND !\IPS\Request::i()->isAjax() )
if ( !empty( static::$dataLayerContext ) AND Settings::i()->core_datalayer_enabled AND !Request::i()->isAjax() )
{
foreach ( static::$dataLayerContext as $property => $data )
{
\IPS\core\DataLayer::i()->addContextProperty( $property, $data['value'], $data['odkUpdate'] ?? false );
DataLayer::i()->addContextProperty( $property, $data['value'], $data['odkUpdate'] ?? false );
}
}
if( \IPS\Request::i()->do and preg_match( '/^[a-zA-Z\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', \IPS\Request::i()->do ) )
if( Request::i()->do and preg_match( '/^[a-zA-Z\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', Request::i()->do ) )
{
if ( method_exists( $this, \IPS\Request::i()->do ) or method_exists( $this, '__call' ) )
if ( method_exists( $this, Request::i()->do ) or method_exists( $this, '__call' ) )
{
$method = \IPS\Request::i()->do;
$method = Request::i()->do;
$this->$method();
}
else
{
\IPS\Output::i()->error( 'page_not_found', '2S106/1', 404, '' );
Output::i()->error( 'page_not_found', '2S106/1', 404, '' );
}
}
else
@@ -119,7 +140,7 @@ abstract class _Controller
}
else
{
\IPS\Output::i()->error( 'page_not_found', '2S106/2', 404, '' );
Output::i()->error( 'page_not_found', '2S106/2', 404, '' );
}
}
}
@@ -129,14 +150,14 @@ abstract class _Controller
*
* @return void
*/
protected function embed()
protected function embed() : void
{
$title = \IPS\Member::loggedIn()->language()->addToStack('error_title');
$title = Member::loggedIn()->language()->addToStack('error_title');
$content = null;
if( !isset( static::$contentModel ) )
if( !isset( static::$contentModel ) OR !in_array( 'IPS\Content\Embeddable', class_implements( static::$contentModel ) ) )
{
$output = \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedUnavailable();
$output = Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedUnavailable();
}
else
{
@@ -145,131 +166,131 @@ abstract class _Controller
$class = static::$contentModel;
$params = array();
if( \IPS\Request::i()->embedComment )
if( Request::i()->embedComment )
{
$commentClass = $class::$commentClass;
if ( isset( $class::$archiveClass ) )
$commentClass = $class::$commentClass;
if ( isset( $class::$archiveClass ) )
{
$item = $class::load( Request::i()->id );
if ( $item->isArchived() )
{
$item = $class::load( \IPS\Request::i()->id );
if ( $item->isArchived() )
{
$commentClass = $class::$archiveClass;
}
$commentClass = $class::$archiveClass;
}
}
try
{
$content = $commentClass::load( \IPS\Request::i()->embedComment );
$content = $commentClass::load( Request::i()->embedComment );
$title = $content->item()->mapped('title');
}
/* Comment wasn't found, but we can still link to the item */
catch( \OutOfRangeException $e )
catch( OutOfRangeException $e )
{
$type = $commentClass::$title;
$output = \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedCommentUnavailable( $type, $item );
$output = Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedCommentUnavailable( $type, $item );
goto embedOutput;
}
}
elseif( \IPS\Request::i()->embedReview )
elseif( Request::i()->embedReview )
{
$reviewClass = $class::$reviewClass;
$reviewClass = $class::$reviewClass;
$item = $class::load( Request::i()->id );
try
{
$content = $reviewClass::load( \IPS\Request::i()->embedReview );
$title = $content->item()->mapped('title');
$content = $reviewClass::load( Request::i()->embedReview );
$title = $content->item()->mapped('title');
}
/* Review wasn't found, but we can still link to the item */
catch( \OutOfRangeException $e )
catch( OutOfRangeException $e )
{
$type = $reviewClass::$title;
$output = \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedCommentUnavailable( $type, $item );
$output = Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedCommentUnavailable( $type, $item );
goto embedOutput;
}
}
else
{
if ( isset( \IPS\Request::i()->page ) and \IPS\Request::i()->page > 1 )
if ( isset( Request::i()->page ) and Request::i()->page > 1 )
{
$params['page'] = \intval( \IPS\Request::i()->page );
$params['page'] = intval( Request::i()->page );
}
if ( isset( \IPS\Request::i()->embedDo ) )
if ( isset( Request::i()->embedDo ) )
{
$params['do'] = \IPS\Request::i()->embedDo;
$params['do'] = Request::i()->embedDo;
}
$content = $class::load( \IPS\Request::i()->id );
$title = $content instanceof \IPS\Node\Model ? $content->_title : $content->mapped( 'title' );
$content = $class::load( Request::i()->id );
$title = $content instanceof Model ? $content->_title : $content->mapped( 'title' );
}
$output = $this->getEmbedOutput( $content, $params );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
$output = \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedUnavailable();
$output = Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedUnavailable();
}
catch( \Exception $e )
catch( Exception $e )
{
\IPS\Log::log( $e, 'embed_error' );
$output = \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedNoPermission();
Log::log( $e, 'embed_error' );
$output = Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedNoPermission();
}
}
embedOutput:
/* Make sure our iframe contents get the necessary elements and JS */
$js = array(
\IPS\Output::i()->js( 'js/commonEmbedHandler.js', 'core', 'interface' ),
\IPS\Output::i()->js( 'js/internalEmbedHandler.js', 'core', 'interface' )
Output::i()->js( 'js/commonEmbedHandler.js', 'core', 'interface' ),
Output::i()->js( 'js/internalEmbedHandler.js', 'core', 'interface' )
);
\IPS\Output::i()->base = '_parent';
Output::i()->base = '_parent';
/* We need to keep any embed.css files that have been specified so that we can re-add them after we re-fetch the css framework */
$embedCss = array();
foreach( \IPS\Output::i()->cssFiles as $cssFile )
foreach( Output::i()->cssFiles as $cssFile )
{
if( \mb_stristr( $cssFile, 'embed.css' ) )
if( mb_stristr( $cssFile, 'embed.css' ) )
{
$embedCss[] = $cssFile;
}
}
/* We need to reset the included CSS files because by this point the responsive files are already in the output CSS array */
\IPS\Output::i()->cssFiles = array();
\IPS\Output::i()->responsive = FALSE;
\IPS\Dispatcher\Front::baseCss();
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, $embedCss );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/embeds.css', 'core', 'front' ) );
Output::i()->cssFiles = array();
Output::i()->responsive = FALSE;
Front::baseCss();
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, $embedCss );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'styles/embeds.css', 'core', 'front' ) );
/* Seo Stuffs */
\IPS\Output::i()->title = $title;
Output::i()->title = $title;
if( $content !== NULL )
{
\IPS\Output::i()->linkTags['canonical'] = (string) $content->url();
Output::i()->linkTags['canonical'] = (string) $content->url();
}
else
{
\IPS\Output::i()->metaTags['robots'] = 'noindex';
Output::i()->metaTags['robots'] = 'noindex';
}
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->embedInternal( $output, $js ), 200, 'text/html' );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core', 'front' )->embedInternal( $output, $js ) );
}
/**
* Returns the content item or the proper error message
*
* @param \IPS\Content $content
* @param object $content
* @param array $params
*
* @return string
*/
protected function getEmbedOutput( $content, array $params = array() )
protected function getEmbedOutput( object $content, array $params = array() ): string
{
if ( !$content->canView() )
{
$output = \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedNoPermission();
$output = Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedNoPermission();
}
else if ( !( $content instanceof \IPS\Content\Embeddable ) )
else if ( !in_array( 'IPS\Content\Embeddable', class_implements( $content ) ) )
{
$output = \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedUnavailable();
$output = Theme::i()->getTemplate( 'embed', 'core', 'global' )->embedUnavailable();
}
else
{
+127 -36
View File
@@ -11,29 +11,45 @@
namespace IPS;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DomainException;
use ErrorException;
use IPS\Application\Module;
use IPS\Dispatcher\Controller;
use IPS\Platform\Bridge;
use RuntimeException;
use function defined;
use function dirname;
use function get_called_class;
use function strstr;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Dispatcher
*
* @property Application $application
* @property Module $module
* @property string $controller
*/
abstract class _Dispatcher
abstract class Dispatcher
{
/**
* @brief Singleton Instance
*/
protected static $instance = NULL;
protected static mixed $instance = NULL;
/**
* Check if a dispatcher instance is available
*
* @return static
* @return bool
* @note This should be used sparingly, primarily for gateway scripts that do not need a dispatcher but still use the framework
*/
public static function hasInstance()
public static function hasInstance(): bool
{
return ( static::$instance !== NULL );
}
@@ -43,15 +59,15 @@ abstract class _Dispatcher
*
* @return static
*/
public static function i()
public static function i(): static
{
if( static::$instance === NULL )
{
$class = \get_called_class();
$class = get_called_class();
if( $class == 'IPS\\Dispatcher' )
{
throw new \RuntimeException( "Only subclasses of Dispatcher can be instantiated" );
throw new RuntimeException( "Only subclasses of Dispatcher can be instantiated" );
}
static::$instance = new $class;
@@ -60,13 +76,13 @@ abstract class _Dispatcher
{
$_redirect = FALSE;
if ( !file_exists( \IPS\SITE_FILES_PATH . '/conf_global.php' ) )
if ( !file_exists( SITE_FILES_PATH . '/conf_global.php' ) )
{
$_redirect = TRUE;
}
else
{
require \IPS\SITE_FILES_PATH . '/conf_global.php';
require SITE_FILES_PATH . '/conf_global.php';
if( !isset( $INFO['sql_database'] ) )
{
@@ -77,7 +93,7 @@ abstract class _Dispatcher
/* This looks weird, but there was a period of time where "installed" was misspelled as "instaled" on Community in the Cloud after install finished. So, if that is present, assume we're okay. */
if ( !isset( $INFO['instaled'] ) )
{
if( isset( $_SERVER['SERVER_PROTOCOL'] ) and \strstr( $_SERVER['SERVER_PROTOCOL'], '/1.0' ) !== false )
if( isset( $_SERVER['SERVER_PROTOCOL'] ) and strstr( $_SERVER['SERVER_PROTOCOL'], '/1.0' ) !== false )
{
header( "HTTP/1.0 503 Service Unavailable" );
}
@@ -86,7 +102,7 @@ abstract class _Dispatcher
header( "HTTP/1.1 503 Service Unavailable" );
}
require \IPS\ROOT_PATH . '/' . \IPS\CP_DIRECTORY . '/install/installing.html';
require ROOT_PATH . '/admin/install/installing.html';
exit;
}
}
@@ -95,14 +111,14 @@ abstract class _Dispatcher
if( $_redirect === TRUE )
{
/* conf_global.php does not exist, forward to installer - we'll do this manually to avoid any code in Output.php that anticipates the installation already being complete (such as setting CSP header in __construct()) */
$url = ( \IPS\Request::i()->isSecure() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . rtrim( \dirname( $_SERVER['SCRIPT_NAME'] ), '/' );
$url = ( Request::i()->isSecure() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . rtrim( dirname( $_SERVER['SCRIPT_NAME'] ), '/' );
header( "HTTP/1.1 307 Temporary Redirect" );
foreach( \IPS\Output::getNoCacheHeaders() as $headerKey => $headerValue )
foreach(Output::getNoCacheHeaders() as $headerKey => $headerValue )
{
header( "{$headerKey}: {$headerValue}" );
}
header( "Location: {$url}/" . \IPS\CP_DIRECTORY . "/install/" );
header( "Location: {$url}/admin/install/" );
exit;
}
}
@@ -116,37 +132,37 @@ abstract class _Dispatcher
/**
* @brief Controller Classname
*/
protected $classname;
protected ?string $classname = NULL;
/**
* @brief Controller instance
*/
public $dispatcherController;
public mixed $dispatcherController = NULL;
/**
* Init
*
* @return void
* @throws \DomainException
* @throws DomainException
*/
abstract public function init();
abstract public function init() : void;
/**
* Run
*
* @return void
*/
public function run()
public function run() : void
{
/* Init class */
if( !class_exists( $this->classname ) )
{
\IPS\Output::i()->error( 'page_doesnt_exist', '2S100/1', 404 );
Output::i()->error( 'page_doesnt_exist', '2S100/1', 404 );
}
$this->dispatcherController = new $this->classname;
if( !( $this->dispatcherController instanceof \IPS\Dispatcher\Controller ) )
if( !( $this->dispatcherController instanceof Controller ) )
{
\IPS\Output::i()->error( 'page_not_found', '5S100/3', 500, '' );
Output::i()->error( 'page_not_found', '5S100/3', 500, '' );
}
/* Execute */
@@ -154,23 +170,26 @@ abstract class _Dispatcher
$this->finish();
}
/**
* Finish
*
* @return void
* @return void
* @throws ErrorException
*/
public function finish()
public function finish() : void
{
Bridge::i()->dispatcherFinish();
/* If we're still here - output */
if ( \IPS\Request::i()->isAjax() )
if ( Request::i()->isAjax() )
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( \IPS\Output::i()->output ), 200, 'text/html' );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( Output::i()->output ), 200, 'text/html' );
}
else
{
/* Just prefetch this to save a query later */
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, $this->getLocationData() ), 200, 'text/html' );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( Output::i()->title, Output::i()->output, $this->getLocationData() ), 200, 'text/html' );
}
}
@@ -178,15 +197,87 @@ abstract class _Dispatcher
* Get an array of data representing the user's current location
* This gets passed to the templates in order to apply some attributes to the body tag
*
* @return array
* @return array{app:string,module:string,controller:string,id?:int|null}
*/
public function getLocationData()
public function getLocationData(): array
{
return array(
'app' => \IPS\Dispatcher::i()->application->directory,
'module' => \IPS\Dispatcher::i()->module->key,
'controller' => \IPS\Dispatcher::i()->controller,
'id' => \IPS\Request::i()->id ? (int) \IPS\Request::i()->id : NULL
'app' => Dispatcher::i()->application->directory,
'module' => Dispatcher::i()->module->key,
'controller' => Dispatcher::i()->controller,
'id' => Request::i()->id ? (int) Request::i()->id : NULL
);
}
/**
* Check the current location to determine if it's a match
*
* @param string $location
* @param string|null $app
* @param string|null $module
* @param string|null $controller
* @return bool
*/
public static function checkLocation( string $location, ?string $app = null, ?string $module = null, ?string $controller = null ) : bool
{
/* If we have no instance it's always false */
if( !static::hasInstance() )
{
return false;
}
/* This is required */
if( static::i()->controllerLocation != $location )
{
return false;
}
/* If we specified an application, check that first */
if( $app !== null )
{
if( isset( static::i()->application ) )
{
if( static::i()->application->directory != $app )
{
return false;
}
}
elseif( isset( Request::i()->app ) and Request::i()->app != $app )
{
return false;
}
}
/* Repeat for module */
if( $module !== null )
{
if( isset( static::i()->module ) )
{
if( static::i()->module->key != $module )
{
return false;
}
}
elseif( isset( Request::i()->module ) and Request::i()->module != $module )
{
return false;
}
}
/* And finally the controller */
if( $controller !== null )
{
if( isset( static::i()->controller ) and static::i()->controller != $controller )
{
return false;
}
elseif( isset( Request::i()->controller ) and Request::i()->controller != $controller )
{
return false;
}
}
/* Still here? All good */
return true;
}
}
+23 -19
View File
@@ -11,28 +11,36 @@
namespace IPS\Dispatcher;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DomainException;
use IPS\Output;
use IPS\Request;
use IPS\Session;
use IPS\Theme;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Dispatcher that doesn't really dispatch but sets things up for external scripts like Pages external blocks
*/
class _External extends \IPS\Dispatcher\Standard
class External extends Standard
{
/**
* Controller Location
*/
public $controllerLocation = 'front';
public string $controllerLocation = 'front';
/**
* Init
*
* @return void
*/
public function init()
public function init() : void
{
/* Base CSS */
static::baseCss();
@@ -46,11 +54,11 @@ class _External extends \IPS\Dispatcher\Standard
parent::init();
/* Don't update sessions for this hit as it will wipe location data */
\IPS\Session::i()->noUpdate();
Session::i()->noUpdate();
}
catch ( \DomainException $e )
catch ( DomainException $e )
{
\IPS\Output::i()->error( $e->getMessage(), '2S100/' . $e->getCode(), $e->getCode() === 4 ? 403 : 404, '' );
Output::i()->error( $e->getMessage(), '2S100/' . $e->getCode(), $e->getCode() === 4 ? 403 : 404, '' );
}
}
@@ -59,15 +67,15 @@ class _External extends \IPS\Dispatcher\Standard
*
* @return void
*/
protected static function baseJs()
protected static function baseJs() : void
{
parent::baseJs();
/* Stuff for output */
if ( !\IPS\Request::i()->isAjax() )
if ( !Request::i()->isAjax() )
{
\IPS\Output::i()->globalControllers[] = 'core.front.core.app';
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front.js' ) );
Output::i()->globalControllers[] = 'core.front.core.app';
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'front.js' ) );
}
}
@@ -76,18 +84,14 @@ class _External extends \IPS\Dispatcher\Standard
*
* @return void
*/
public static function baseCss()
public static function baseCss() : void
{
parent::baseCss();
/* Stuff for output */
if ( !\IPS\Request::i()->isAjax() )
if ( !Request::i()->isAjax() )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'core.css', 'core', 'front' ) );
if ( \IPS\Theme::i()->settings['responsive'] )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'core_responsive.css', 'core', 'front' ) );
}
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'core.css', 'core', 'front' ) );
}
}
}
File diff suppressed because it is too large. Load diff
+39 -25
View File
@@ -12,32 +12,46 @@
namespace IPS\Dispatcher;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DomainException;
use Exception;
use IPS\Api\GraphQL as GraphQLApi;
use IPS\Api\OAuthClient;
use IPS\IPS;
use IPS\Member;
use IPS\Request;
use OutOfRangeException;
use function defined;
use function in_array;
use function is_array;
use const IPS\DEBUG_GRAPHQL;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/* Register our GraphQL library */
\IPS\IPS::$PSR0Namespaces['GraphQL'] = \IPS\ROOT_PATH . "/system/3rd_party/graphql-php";
IPS::$PSR0Namespaces['GraphQL'] = \IPS\ROOT_PATH . "/system/3rd_party/graphql-php";
/**
* @brief API Dispatcher
*/
class _GraphQL extends Api
class GraphQL extends Api
{
/**
* @brief Path
*/
public $path = '/graphql';
public ?string $path = '/graphql';
/**
* Init
*
* @return void
* @throws \DomainException
* @throws DomainException
*/
public function init()
public function init() : void
{
try
{
@@ -52,27 +66,27 @@ class _GraphQL extends Api
if ( $this->rawAccessToken )
{
$this->_setAccessToken();
$client = \IPS\Api\OAuthClient::load( $this->accessToken['client_id'] );
$client = OAuthClient::load( $this->accessToken['client_id'] );
if( $client->api_access == 'rest' )
{
throw new \IPS\Api\Exception( 'NO_GRAPHQL_ACCESS', '3S426_graphql/1', 403 );
}
\IPS\Member::$loggedInMember = \IPS\Member::load( $this->accessToken['member_id'] );
Member::$loggedInMember = Member::load( $this->accessToken['member_id'] );
}
else
{
$client = \IPS\Api\OAuthClient::load( $this->rawApiKey );
$client = OAuthClient::load( $this->rawApiKey );
/* Check that the API key has access to the GraphQL API */
if( $client->api_access == 'rest' )
{
throw new \IPS\Api\Exception( 'NO_GRAPHQL_ACCESS', '3S426_graphql/1', 403 );
}
\IPS\Member::$loggedInMember = new \IPS\Member;
Member::$loggedInMember = new Member;
}
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_API_KEY', '3S290_graphql/7', 401 );
}
@@ -89,9 +103,9 @@ class _GraphQL extends Api
$response = json_encode( array( 'errors' => array( array( 'message' => $e->getMessage(), 'id' => $e->exceptionCode ) ) ), JSON_PRETTY_PRINT );
/* Do we need to log this? */
if ( \in_array( $e->exceptionCode, array( '2S290_graphql/8', '2S290_graphql/B', '3S290_graphql/7', '3S290_graphql/9' ) ) )
if ( in_array( $e->exceptionCode, array( '2S290_graphql/8', '2S290_graphql/B', '3S290_graphql/7', '3S290_graphql/9' ) ) )
{
$this->_log( $response, $e->getCode(), \in_array( $e->exceptionCode, array( '3S290/7', '3S290/9', '3S290/B' ) ) );
$this->_log( (array)$response, $e->getCode(), in_array( $e->exceptionCode, array( '3S290/7', '3S290/9', '3S290/B' ) ) );
}
/* Output */
@@ -104,26 +118,26 @@ class _GraphQL extends Api
*
* @return void
*/
public function run()
public function run() : void
{
try
{
/* Work out the query (can either use a JSON-encoded or form-encoded) body */
$query = NULL;
$variables = [];
if( \IPS\Request::i()->query )
if( Request::i()->query )
{
$query = \IPS\Request::i()->query;
$query = Request::i()->query;
if( isset( \IPS\Request::i()->variables ) )
if( isset( Request::i()->variables ) )
{
if( \is_array( \IPS\Request::i()->variables ) )
if( is_array( Request::i()->variables ) )
{
$variables = \IPS\Request::i()->variables;
$variables = Request::i()->variables;
}
else
{
$variables = json_decode( \IPS\Request::i()->variables, TRUE );
$variables = json_decode( Request::i()->variables, TRUE );
}
}
}
@@ -141,15 +155,15 @@ class _GraphQL extends Api
$this->_respond( json_encode( array( 'errors' => array( array( 'message' => 'NO_QUERY' ) ) ), JSON_PRETTY_PRINT ), 400 );
}
$output = \IPS\Api\GraphQL::execute( $query, $variables );
$output = GraphQLApi::execute( $query, $variables );
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $output );
Member::loggedIn()->language()->parseOutputForDisplay( $output );
$this->_respond( json_encode( $output, JSON_PRETTY_PRINT ), 200 );
}
catch ( \Exception $e )
catch ( Exception $e )
{
$response = json_encode( array( 'errors' => array( array( 'message' => ( \IPS\IN_DEV OR \IPS\DEBUG_GRAPHQL ) ? $e->getMessage() : 'UNKNOWN_ERROR', 'id' => $e->getCode() ) ) ), JSON_PRETTY_PRINT );
$response = json_encode( array( 'errors' => array( array( 'message' => ( \IPS\IN_DEV OR DEBUG_GRAPHQL ) ? $e->getMessage() : 'UNKNOWN_ERROR', 'id' => $e->getCode() ) ) ), JSON_PRETTY_PRINT );
$this->_respond( $response, 500 );
}
}
+89 -48
View File
@@ -11,38 +11,62 @@
namespace IPS\Dispatcher;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Data\Cache;
use IPS\Data\Store;
use IPS\Db;
use IPS\Dispatcher;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Lang;
use IPS\Member;
use IPS\Member\Group;
use IPS\Output;
use IPS\Request;
use IPS\Settings;
use IPS\Theme;
use function defined;
use const IPS\COOKIE_BYPASS_SSLONLY;
use const IPS\COOKIE_DOMAIN;
use const IPS\COOKIE_PATH;
use const IPS\COOKIE_PREFIX;
use const IPS\SITE_FILES_PATH;
use const IPS\STORE_CONFIG;
use const IPS\STORE_METHOD;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* @brief Installer/Upgrader Dispatcher
*/
class _Setup extends \IPS\Dispatcher
class Setup extends Dispatcher
{
/**
* @brief Controller Location
*/
public $controllerLocation = 'setup';
public string $controllerLocation = 'setup';
/**
* @brief Install or upgrade
*/
public $setupLocation = 'install';
public string $setupLocation = 'install';
/**
* @brief Step
*/
public $step = 1;
public int $step = 1;
/**
* Initiator
*
* @return void
*/
public function init()
public function init() : void
{
}
@@ -51,7 +75,7 @@ class _Setup extends \IPS\Dispatcher
*
* @return array
*/
protected function returnSteps()
protected function returnSteps(): array
{
if( $this->setupLocation == 'upgrade' )
{
@@ -83,14 +107,31 @@ class _Setup extends \IPS\Dispatcher
/**
* Set location (install or upgrade)
*
* @param string $location 'install' or 'upgrade'
* @return \IPS\Dispatcher\Setup
* @param string $location 'install' or 'upgrade'
* @return Setup
*/
public function setLocation( $location )
public function setLocation(string $location ): Setup
{
$this->setupLocation = $location;
$steps = $this->returnSteps();
$this->classname = 'IPS\core\modules\setup\\' . $location . '\\' . ( isset( \IPS\Request::i()->controller ) ? \IPS\Request::i()->controller : $steps[1] );
$currentStep = ( isset( Request::i()->controller ) ? Request::i()->controller : $steps[1] );
$this->classname = 'IPS\core\modules\setup\\' . $location . '\\' . $currentStep;
/* If we are upgrading and just starting, check the current version.
If we are upgrading from a version prior to v5, disable all non-1st party apps. */
if( $location == 'upgrade' and $currentStep == 'login' )
{
$currentBaseVersion = Db::i()->select( 'app_long_version', 'core_applications', [ 'app_directory=?', 'core' ] )->first();
/* If we are upgrading from a version earlier than v5, disable and lock anything that isn't 1st party */
if( $currentBaseVersion < 5000016 )
{
Db::i()->update( 'core_applications', [ 'app_enabled' => 0, 'app_requires_manual_intervention' => 1 ], Db::i()->in( 'app_directory', IPS::$ipsApps, true ) );
Cache::i()->clearAll();
Store::i()->clearAll();
}
}
return $this;
}
@@ -99,7 +140,7 @@ class _Setup extends \IPS\Dispatcher
*
* @return void
*/
public function run()
public function run() : void
{
/* Installer checks */
if( $this->setupLocation == 'install' )
@@ -110,7 +151,7 @@ class _Setup extends \IPS\Dispatcher
{
rename( \IPS\ROOT_PATH . '/conf_global.dist.php', \IPS\ROOT_PATH . '/conf_global.php' );
}
catch ( \Exception $e ) { }
catch ( Exception $e ) { }
if ( !file_exists( \IPS\ROOT_PATH . '/conf_global.php' ) )
{
@@ -118,115 +159,115 @@ class _Setup extends \IPS\Dispatcher
{
file_put_contents( \IPS\ROOT_PATH. '/conf_global.php', '' );
}
catch ( \Exception $e ) { }
catch ( Exception $e ) { }
}
if ( !file_exists( \IPS\ROOT_PATH . '/conf_global.php' ) )
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Member::loggedIn()->language()->addToStack( 'installation_error' ), '', true, \IPS\ROOT_PATH ), 500, 'text/html', array(), FALSE, FALSE, FALSE );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( Member::loggedIn()->language()->addToStack( 'installation_error' ), '', true, \IPS\ROOT_PATH ), 500, 'text/html', array(), FALSE, FALSE, FALSE );
}
}
require \IPS\ROOT_PATH . '/conf_global.php';
if ( isset( $INFO ) and isset( $INFO['installed'] ) )
{
$upgradeUrl = new \IPS\Http\Url( \IPS\Settings::i()->base_url . \IPS\CP_DIRECTORY . '/upgrade/' );
\IPS\Output::i()->redirect( $upgradeUrl );
$upgradeUrl = new Url( Settings::i()->base_url . 'admin/upgrade/' );
Output::i()->redirect( $upgradeUrl );
}
}
/* Upgrader Checks */
else
{
if ( !file_exists( \IPS\SITE_FILES_PATH . '/conf_global.php' ) )
if ( !file_exists( SITE_FILES_PATH . '/conf_global.php' ) )
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Member::loggedIn()->language()->addToStack('upgrade_error'), '', \IPS\Member::loggedIn()->language()->addToStack('no_conf_global'), \IPS\ROOT_PATH ), 200, 'text/html', array(), FALSE );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( Member::loggedIn()->language()->addToStack('upgrade_error'), '', Member::loggedIn()->language()->addToStack('no_conf_global'), \IPS\ROOT_PATH ), 200, 'text/html', array(), FALSE );
}
if ( \IPS\STORE_METHOD === 'FileSystem' )
if ( STORE_METHOD === 'FileSystem' )
{
$config = json_decode( \IPS\STORE_CONFIG, TRUE );
$config = json_decode( STORE_CONFIG, TRUE );
$path = str_replace( '{root}', \IPS\ROOT_PATH, $config['path'] );
if ( !is_dir( $path ) or !is_writable( $path ) )
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Member::loggedIn()->language()->addToStack('upgrade_error'), '', \IPS\Member::loggedIn()->language()->addToStack( 'create_conf_global', FALSE, array( 'sprintf' => array( $path ) ) ), \IPS\ROOT_PATH ), 200, 'text/html', array(), FALSE );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( Member::loggedIn()->language()->addToStack('upgrade_error'), '', Member::loggedIn()->language()->addToStack( 'create_conf_global', FALSE, array( 'sprintf' => array( $path ) ) ), \IPS\ROOT_PATH ), 200, 'text/html', array(), FALSE );
}
}
require \IPS\SITE_FILES_PATH . '/conf_global.php';
require SITE_FILES_PATH . '/conf_global.php';
if ( !isset( $INFO ) )
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Member::loggedIn()->language()->addToStack('upgrade_error'), '', \IPS\Member::loggedIn()->language()->addToStack('bad_conf_global'), \IPS\ROOT_PATH ), 200, 'text/html', array(), FALSE );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( Member::loggedIn()->language()->addToStack('upgrade_error'), '', Member::loggedIn()->language()->addToStack('bad_conf_global'), \IPS\ROOT_PATH ), 200, 'text/html', array(), FALSE );
}
/* Fix languages if necessary */
if( \IPS\Db::i()->checkForTable( 'core_sys_lang' ) AND !\IPS\Db::i()->checkForColumn( 'core_sys_lang', 'lang_order' ) )
if( Db::i()->checkForTable( 'core_sys_lang' ) AND !Db::i()->checkForColumn( 'core_sys_lang', 'lang_order' ) )
{
\IPS\Lang::languages( \IPS\Db::i()->select( '*', 'core_sys_lang' ) );
Lang::languages( Db::i()->select( '*', 'core_sys_lang' ) );
}
/* Fix members if necessary */
if( !\IPS\Db::i()->checkForTable( 'core_members' ) )
if( !Db::i()->checkForTable( 'core_members' ) )
{
\IPS\Member::$databaseTable = 'members';
Member::$databaseTable = 'members';
if ( !\IPS\Db::i()->checkForTable( 'core_groups' ) )
if ( !Db::i()->checkForTable( 'core_groups' ) )
{
\IPS\Member\Group::$databaseTable = 'groups';
Group::$databaseTable = 'groups';
}
/* re-arrange 3.x mapping to 4.x code */
$bits = \IPS\Member::$bitOptions;
$bits = Member::$bitOptions;
unset( $bits['members_bitoptions']['members_bitoptions2'] );
\IPS\Member::$bitOptions = $bits;
Member::$bitOptions = $bits;
}
}
\IPS\Settings::i()->base_url = ( \IPS\Request::i()->isSecure() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . mb_substr( $_SERVER['SCRIPT_NAME'], 0, -mb_strlen( \IPS\CP_DIRECTORY . '/' . $this->setupLocation . '/index.php' ) );
Settings::i()->base_url = ( Request::i()->isSecure() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . mb_substr( $_SERVER['SCRIPT_NAME'], 0, -mb_strlen( 'admin/' . $this->setupLocation . '/index.php' ) );
$this->step = array_search( \IPS\Request::i()->controller, $this->returnSteps() );
$this->step = array_search( Request::i()->controller, $this->returnSteps() );
session_name( ( \IPS\COOKIE_PREFIX !== NULL ) ? \IPS\COOKIE_PREFIX . 'IPSSessionSetup' : 'IPSSessionSetup' );
session_name( ( COOKIE_PREFIX !== NULL ) ? COOKIE_PREFIX . 'IPSSessionSetup' : 'IPSSessionSetup' );
$currentCookieParams = session_get_cookie_params();
session_set_cookie_params(
86400 * 14,
( \IPS\COOKIE_PATH !== NULL ) ? \IPS\COOKIE_PATH : $currentCookieParams['path'],
( \IPS\COOKIE_DOMAIN !== NULL ) ? \IPS\COOKIE_DOMAIN : $currentCookieParams['domain'],
( \IPS\COOKIE_BYPASS_SSLONLY !== TRUE ) ? ( mb_substr( \IPS\Settings::i()->base_url, 0, 5 ) == 'https' ) : $currentCookieParams['secure'],
( COOKIE_PATH !== NULL ) ? COOKIE_PATH : $currentCookieParams['path'],
( COOKIE_DOMAIN !== NULL ) ? COOKIE_DOMAIN : $currentCookieParams['domain'],
( !COOKIE_BYPASS_SSLONLY ) ? ( mb_substr( Settings::i()->base_url, 0, 5 ) == 'https' ) : $currentCookieParams['secure'],
TRUE
);
if( !@session_start() )
{
\IPS\Output::i()->error( \IPS\Member::loggedIn()->language()->addToStack( 'session_no_good', FALSE, array( 'sprintf' => array( \IPS\IPS::$lastError ? \IPS\IPS::$lastError->getMessage() : NULL ) ) ), '4S109/5', 500, '' );
Output::i()->error( Member::loggedIn()->language()->addToStack( 'session_no_good', FALSE, array( 'sprintf' => array( IPS::$lastError?->getMessage() ) ) ), '4S109/5', 500, '' );
}
if( $this->classname != 'IPS\core\modules\setup\upgrade\done' and $this->setupLocation != 'install' and $this->step > 1 AND ( !isset( $_SESSION['uniqueKey'] ) OR $_SESSION['uniqueKey'] != \IPS\Request::i()->key ) )
if( $this->classname != 'IPS\core\modules\setup\upgrade\done' and $this->setupLocation != 'install' and $this->step > 1 AND ( !isset( $_SESSION['uniqueKey'] ) OR $_SESSION['uniqueKey'] != Request::i()->key ) )
{
\IPS\Output::i()->error( 'upgrade_session_error', '3S109/4', 403, '' );
Output::i()->error( 'upgrade_session_error', '3S109/4', 403, '' );
}
/* Init class */
if( !class_exists( $this->classname ) )
{
\IPS\Output::i()->error( 'page_doesnt_exist', '2S100/1', 404 );
Output::i()->error( 'page_doesnt_exist', '2S100/1', 404 );
}
$controller = new $this->classname;
if( !( $controller instanceof \IPS\Dispatcher\Controller ) )
if( !( $controller instanceof Controller) )
{
\IPS\Output::i()->error( 'page_not_found', '5S100/3', 500, '' );
Output::i()->error( 'page_not_found', '5S100/3', 500, '' );
}
/* Execute */
$controller->execute();
/* If we're still here - output */
if ( \IPS\Request::i()->isAjax() )
if ( Request::i()->isAjax() )
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( \IPS\Output::i()->output ), 200, 'text/html', array(), FALSE, FALSE, TRUE, FALSE );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( Output::i()->output ), 200, 'text/html', array(), FALSE, FALSE, TRUE, FALSE );
}
else
{
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output ), 200, 'text/html', array(), FALSE, FALSE, TRUE, FALSE );
Output::i()->sendOutput( Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( Output::i()->title, Output::i()->output ), 200, 'text/html', array(), FALSE, FALSE, TRUE, FALSE );
}
}
+151 -102
View File
@@ -11,56 +11,80 @@
namespace IPS\Dispatcher;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DomainException;
use IPS\Application;
use IPS\Application\Module;
use IPS\Data\Cache;
use IPS\Data\Cache\None;
use IPS\Data\Store;
use IPS\Db\Exception;
use IPS\Dispatcher;
use IPS\Http\Url;
use IPS\Lang;
use IPS\Member;
use IPS\Output;
use IPS\Platform\Bridge;
use IPS\Request;
use IPS\Settings;
use IPS\Task;
use IPS\Theme;
use OutOfRangeException;
use function count;
use function defined;
use function intval;
use function is_array;
use const IPS\CIC2;
use const IPS\SITE_FILES_PATH;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Standard Dispatcher
*/
abstract class _Standard extends \IPS\Dispatcher
abstract class Standard extends Dispatcher
{
/**
* Application
*/
public $application;
public ?Application $application = NULL;
/**
* Module
*/
public $module;
public ?Module $module = NULL;
/**
* Controller
*/
public $controller;
public ?string $controller = NULL;
/**
* @brief Check access permissions
*/
public $checkGenericPermissions = TRUE;
public bool $checkGenericPermissions = TRUE;
public array $platformFeatureExpires = [];
/**
* Base CSS
*
* @return void
*/
public static function baseCss()
public static function baseCss() : void
{
if ( !\IPS\Request::i()->isAjax() )
if ( !Request::i()->isAjax() )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'framework.css', 'core', 'global' ) );
if ( \IPS\Theme::i()->settings['responsive'] )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'responsive.css', 'core', 'global' ) );
}
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'framework/framework.css', 'core', 'global' ) );
}
if ( \count( \IPS\Lang::languages() ) > 1 )
if ( count( Lang::languages() ) > 1 )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'flags.css', 'core', 'global' ) );
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'flags.css', 'core', 'global' ) );
}
}
@@ -69,28 +93,26 @@ abstract class _Standard extends \IPS\Dispatcher
*
* @return void
*/
protected static function baseJs()
protected static function baseJs() : void
{
/* Stuff for output */
if ( !\IPS\Request::i()->isAjax() )
if ( !Request::i()->isAjax() )
{
/* JS */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'library.js' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'js_lang_' . \IPS\Member::loggedIn()->language()->id . '.js' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'framework.js' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_core.js', 'core', 'global' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'plugins.js', 'core', 'plugins' ) );
\IPS\Output::i()->jsVars['date_format'] = mb_strtolower( \IPS\Member::loggedIn()->language()->preferredDateFormat() );
\IPS\Output::i()->jsVars['date_first_day'] = 0;
\IPS\Output::i()->jsVars['ipb_url_filter_option'] = \IPS\Settings::i()->ipb_url_filter_option;
\IPS\Output::i()->jsVars['url_filter_any_action'] = \IPS\Settings::i()->url_filter_any_action;
\IPS\Output::i()->jsVars['bypass_profanity'] = \intval( \IPS\Member::loggedIn()->group['g_bypass_badwords'] );
\IPS\Output::i()->jsVars['emoji_style'] = ( \IPS\Settings::i()->getFromConfGlobal('sql_utf8mb4') === TRUE ) ? \IPS\Settings::i()->emoji_style : 'disabled';
\IPS\Output::i()->jsVars['emoji_shortcodes'] = (bool) \IPS\Settings::i()->emoji_shortcodes;
\IPS\Output::i()->jsVars['emoji_ascii'] = (bool) \IPS\Settings::i()->emoji_ascii;
\IPS\Output::i()->jsVars['emoji_cache'] = (int) \IPS\Settings::i()->emoji_cache;
\IPS\Output::i()->jsVars['image_jpg_quality'] = (int) \IPS\Settings::i()->image_jpg_quality ?: 85;
\IPS\Output::i()->jsVars['cloud2'] = (bool) \IPS\CIC2;
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'library.js' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, [ Output\Javascript::getLanguageUrl( Member::loggedIn()->language() ) ] );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'framework.js' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'global_core.js', 'core', 'global' ) );
Output::i()->jsVars['date_format'] = mb_strtolower( Member::loggedIn()->language()->preferredDateFormat() );
Output::i()->jsVars['date_first_day'] = 0;
Output::i()->jsVars['ipb_url_filter_option'] = Settings::i()->ipb_url_filter_option;
Output::i()->jsVars['url_filter_any_action'] = Settings::i()->url_filter_any_action;
Output::i()->jsVars['bypass_profanity'] = intval( Member::loggedIn()->group['g_bypass_badwords'] );
Output::i()->jsVars['emoji_style'] = ( Settings::i()->getFromConfGlobal('sql_utf8mb4') === TRUE ) ? Settings::i()->emoji_style : 'disabled';
Output::i()->jsVars['emoji_shortcodes'] = (bool) Settings::i()->emoji_shortcodes;
Output::i()->jsVars['emoji_cache'] = (int) Settings::i()->emoji_cache;
Output::i()->jsVars['image_jpg_quality'] = (int) Settings::i()->image_jpg_quality ?: 85;
Output::i()->jsVars['cloud2'] = (bool) CIC2;
}
}
@@ -99,15 +121,15 @@ abstract class _Standard extends \IPS\Dispatcher
*
* @return void
*/
public function finish()
public function finish() : void
{
/* If we're still here - output */
if ( ! \IPS\Request::i()->isAjax() )
if ( ! Request::i()->isAjax() )
{
/* Load all models for this app and location */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'app.js', \IPS\Dispatcher::i()->application->directory ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'app.js', Dispatcher::i()->application->directory ) );
/* Map.js must come last as it will always have the correct file names */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'map.js' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'map.js' ) );
}
parent::finish();
@@ -116,53 +138,53 @@ abstract class _Standard extends \IPS\Dispatcher
/**
* @brief Initialize tasks
*/
protected $runTasks = FALSE;
protected bool $runTasks = FALSE;
/**
* Init
*
* @return void
* @throws \DomainException
* @throws DomainException
*/
public function init()
public function init() : void
{
$this->runTasks = (bool) \IPS\Member::loggedIn()->member_id;
$this->runTasks = (bool) Member::loggedIn()->member_id;
/* Force HTTPs and correct domain (e.g. not "www." if that's not in the base URL) */
if ( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' )
{
$baseUrl = new \IPS\Http\Url( \IPS\Settings::i()->base_url );
$newUrl = \IPS\Request::i()->url();
$baseUrl = new Url( Settings::i()->base_url );
$newUrl = Request::i()->url();
if( $baseUrl->data['scheme'] === 'https' and \IPS\Request::i()->url()->data['scheme'] !== 'https' )
if( $baseUrl->data['scheme'] === 'https' and Request::i()->url()->data['scheme'] !== 'https' )
{
$newUrl = $newUrl->setScheme('https');
}
if( $baseUrl->data['host'] !== \IPS\Request::i()->url()->data['host'] )
if( $baseUrl->data['host'] !== Request::i()->url()->data['host'] )
{
$newUrl = $newUrl->setHost( $baseUrl->data['host'] );
}
if( $newUrl != \IPS\Request::i()->url() )
if( $newUrl != Request::i()->url() )
{
\IPS\Output::i()->redirect( $newUrl );
Output::i()->redirect( $newUrl );
}
}
/* Set locale */
\IPS\Member::loggedIn()->language()->setLocale();
Member::loggedIn()->language()->setLocale();
/* Set Application */
if ( isset( \IPS\Request::i()->app ) )
if ( isset( Request::i()->app ) )
{
try
{
$this->application = \IPS\Application::load( \IPS\Request::i()->app );
$this->application = Application::load( Request::i()->app );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
$applications = \IPS\Application::applications();
$applications = Application::applications();
foreach( $applications as $application )
{
@@ -176,13 +198,13 @@ abstract class _Standard extends \IPS\Dispatcher
{
$this->application = array_shift( $applications );
}
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'app.js' ) );
throw new \DomainException( 'requested_route_404', 5 );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'app.js' ) );
throw new DomainException( 'requested_route_404', 5 );
}
}
else
{
$applications = \IPS\Application::applications();
$applications = Application::applications();
foreach( $applications as $application )
{
@@ -199,11 +221,11 @@ abstract class _Standard extends \IPS\Dispatcher
}
/* Init Application */
if( $this->checkGenericPermissions === TRUE AND !$this->application->canAccess( \IPS\Member::loggedIn() ) AND $this->controllerLocation != 'admin' )
if( $this->checkGenericPermissions === TRUE AND !$this->application->canAccess( Member::loggedIn() ) AND $this->controllerLocation != 'admin' )
{
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'app.js' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'app.js' ) );
$message = $this->application->disabled_message ?: 'generic_offline_message';
throw new \DomainException( $message, 4 );
throw new DomainException( $message, 4 );
}
if ( method_exists( $this->application, 'init' ) )
{
@@ -211,16 +233,16 @@ abstract class _Standard extends \IPS\Dispatcher
}
/* Set Module */
if ( isset( \IPS\Request::i()->module ) )
if ( isset( Request::i()->module ) )
{
try
{
$this->module = \IPS\Application\Module::get( $this->application->directory, \IPS\Request::i()->module, static::i()->controllerLocation );
$this->module = Module::get( $this->application->directory, Request::i()->module, static::i()->controllerLocation );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'app.js' ) );
throw new \DomainException( 'requested_route_404', 6 );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'app.js' ) );
throw new DomainException( 'requested_route_404', 6 );
}
}
else
@@ -229,54 +251,49 @@ abstract class _Standard extends \IPS\Dispatcher
}
/* Set controller */
$this->controller = isset( \IPS\Request::i()->controller ) ? \IPS\Request::i()->controller : $this->module->default_controller;
$this->controller = isset( Request::i()->controller ) ? Request::i()->controller : $this->module->default_controller;
if( \is_array( $this->controller ) )
if( is_array( $this->controller ) )
{
$this->controller = NULL;
throw new \DomainException( 'requested_route_404', 7 );
throw new DomainException( 'requested_route_404', 7 );
}
/* Set classname */
$this->classname = 'IPS\\' . $this->application->directory . '\\modules\\' . $this->controllerLocation . '\\' . $this->module->key . '\\' . $this->controller;
/* Base Templates, CSS and JS */
if ( !\IPS\Request::i()->isAjax() )
if ( !Request::i()->isAjax() )
{
/* Templates */
if ( \IPS\Data\Cache::i() instanceof \IPS\Data\Cache\None )
if ( Cache::i() instanceof None )
{
\IPS\Data\Store::i()->templateLoad[] = array( 'core', $this->controllerLocation, 'global' );
\IPS\Data\Store::i()->templateLoad[] = array( 'core', 'global', 'global' );
\IPS\Data\Store::i()->templateLoad[] = array( 'core', 'global', 'forms' );
\IPS\Data\Store::i()->templateLoad[] = array( 'core', $this->controllerLocation, 'forms' );
Store::i()->templateLoad[] = array( 'core', $this->controllerLocation, 'global' );
Store::i()->templateLoad[] = array( 'core', 'global', 'global' );
Store::i()->templateLoad[] = array( 'core', 'global', 'forms' );
Store::i()->templateLoad[] = array( 'core', $this->controllerLocation, 'forms' );
$templateLoad = array();
if ( ! \IPS\Theme::isUsingTemplateDiskCache() )
foreach ( Store::i()->templateLoad as $data )
{
foreach ( \IPS\Data\Store::i()->templateLoad as $data )
{
$templateLoad[] = 'template_' . \IPS\Theme::i()->id . '_' . \IPS\Theme::makeBuiltTemplateLookupHash( $data[0], $data[1], $data[2] ) . '_' . $data[2];
}
\IPS\Data\Store::i()->loadIntoMemory( $templateLoad );
$templateLoad[] = 'template_' . Theme::i()->id . '_' . Theme::makeBuiltTemplateLookupHash( $data[0], $data[1], $data[2] ) . '_' . $data[2];
}
Store::i()->loadIntoMemory( $templateLoad );
}
/* App JS */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'app.js' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'app.js' ) );
/* App CSS */
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( $this->application->directory . '.css', $this->application->directory, $this->controllerLocation ) );
if ( \IPS\Theme::i()->settings['responsive'] )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( $this->application->directory . '_responsive.css', $this->application->directory, $this->controllerLocation ) );
}
$currentApp = $this->application;
$currentApp::outputCss();
/* VLE */
if ( \IPS\Lang::vleActive() )
if ( Lang::vleActive() )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'customization/visuallanguage.css', 'core', 'admin' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_customization.js', 'core', 'global' ) );
\IPS\Output::i()->globalControllers[] = 'core.global.customization.visualLang';
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'customization/visuallanguage.css', 'core', 'admin' ) );
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'global_customization.js', 'core', 'global' ) );
Output::i()->globalControllers[] = 'core.global.customization.visualLang';
}
}
}
@@ -286,7 +303,7 @@ abstract class _Standard extends \IPS\Dispatcher
*
* @return void
*/
protected function setDefaultModule()
protected function setDefaultModule() : void
{
$modules = $this->application->modules( static::i()->controllerLocation );
foreach( $modules as $module )
@@ -308,13 +325,8 @@ abstract class _Standard extends \IPS\Dispatcher
* @brief Enable destruct method
* @note Disable to avoid an automatic database connection
*/
public $destruct = true;
public bool $destruct = true;
/**
* @brief Flag to check if we are in the destructor
*/
public bool $inDestructor = false;
/**
* Destructor
* Runs tasks
@@ -324,23 +336,60 @@ abstract class _Standard extends \IPS\Dispatcher
public function __destruct()
{
/* If you visit and you are redirected to installer, this code should not run */
if( !file_exists( \IPS\SITE_FILES_PATH . '/conf_global.php' ) OR !$this->destruct )
if( !file_exists( SITE_FILES_PATH . '/conf_global.php' ) OR !$this->destruct )
{
return;
}
if ( $this->runTasks and \IPS\Settings::i()->task_use_cron == 'normal' and !\IPS\Request::i()->isAjax() )
if ( $this->runTasks and Settings::i()->task_use_cron == 'normal' and !Request::i()->isAjax() )
{
$this->inDestructor = true;
try
{
$task = \IPS\Task::queued();
$task = Task::queued();
if ( $task )
{
$task->runAndLog();
}
}
catch( \IPS\Db\Exception $e ) { }
catch( Exception $e ) { }
}
}
/**
* Check ACP Permission
*
* @param string $key Permission Key
* @param string|Application|null $app Application (NULL will default to current)
* @param string|Module|null $module Module (NULL will default to current)
* @param boolean $return Return boolean (true/false) instead of throwing an error
* @return int|bool|null
*/
public function checkAcpPermission( string $key, string|Application $app=NULL, Module|string $module=NULL, bool $return=FALSE ): int|bool|null
{
$result = Bridge::i()->checkPlatformPermission( $key, $app, $module, $return );
if ( is_int( $result ) )
{
return $result;
}
if ( !Member::loggedIn()->hasAcpRestriction( ( $app ?: $this->application ), ( $module ?: $this->module ), $key ) )
{
if ( $return )
{
return FALSE;
}
Output::i()->error( 'no_module_permission', '2S107/2', 403, '' );
}
if ( $return )
{
return TRUE;
}
return NULL;
}
}