Version 4.7.0

This commit is contained in:
Neo committed 2025-12-19 16:00:35 -08:00
1 parent 517a5e1f70
commit 8ba7d43898
1933 files changed
+65613 -11332

No files matched your search

+216 -85
View File
@@ -191,83 +191,15 @@ class _Front extends \IPS\Dispatcher\Standard
/* Do we need more info from the member or do they need to validate? */
/* This controllers should always be accessible, no matter if the member is awaiting validation or needs to set up the email or name */
$legalControllers = array( 'privacy', 'contact', 'terms', 'embed', 'metatags', 'store', 'checkout', 'subscriptions' );
/* These controllers should always be accessible, no matter if the member is awaiting validation or needs to set up the email or name */
$legalControllers = array( 'privacy', 'contact', 'terms', 'embed', 'metatags', 'subscriptions', 'serviceworker', 'settings' );
/* Do we need more info from the member or do they need to validate? */
if( \IPS\Member::loggedIn()->member_id and $this->controller !== 'language' and $this->controller !== 'theme' and $this->controller !== 'ajax' and !\in_array( $this->controller, $legalControllers ) )
{
/* Need their name or email... */
if( ( \IPS\Member::loggedIn()->real_name === '' or !\IPS\Member::loggedIn()->email ) and $this->controller !== 'register' and $this->controller !== 'login' )
if ( $url = static::doMemberCheck() )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=complete' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
/* Need them to validate... */
elseif( \IPS\Member::loggedIn()->members_bitoptions['validating'] and $this->controller !== 'register' and $this->controller !== 'login' and $this->controller != 'redirect' )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=validating', 'front', 'register' ) );
}
/* Need them to reconfirm terms/privacy policy... */
elseif ( ( \IPS\Member::loggedIn()->members_bitoptions['must_reaccept_privacy'] or \IPS\Member::loggedIn()->members_bitoptions['must_reaccept_terms'] ) and $this->controller !== 'register' and $this->controller !== 'ajax' )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=reconfirm', 'front', 'register' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
/* Have required profile actions that need completing */
else if ( \IPS\Settings::i()->quick_register AND !\IPS\Member::loggedIn()->members_bitoptions['profile_completed'] AND !\in_array( $this->controller, array( 'register', 'login', 'redirect', 'ajax', 'settings', 'checkout', 'pixabay', 'editor' ) ) AND $completion = \IPS\Member::loggedIn()->profileCompletion() AND \count( $completion['required'] ) )
{
foreach( $completion['required'] AS $id => $completed )
{
if ( $completed === FALSE )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=core&module=system&controller=register&do=finish&_new=1", 'front', 'register' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
}
}
/* Need to set up MFA... */
if ( !\in_array( $this->controller, array( 'register', 'login', 'redirect', 'ajax', 'settings', 'embed' ) ) )
{
$haveAcceptableHandlers = FALSE;
$haveConfiguredHandler = FALSE;
foreach ( \IPS\MFA\MFAHandler::handlers() as $key => $handler )
{
if ( $handler->isEnabled() and $handler->memberCanUseHandler( \IPS\Member::loggedIn() ) )
{
$haveAcceptableHandlers = TRUE;
if ( $handler->memberHasConfiguredHandler( \IPS\Member::loggedIn() ) )
{
$haveConfiguredHandler = TRUE;
break;
}
}
}
if ( !$haveConfiguredHandler and $haveAcceptableHandlers )
{
if ( \IPS\Settings::i()->mfa_required_groups == '*' or \IPS\Member::loggedIn()->inGroup( explode( ',', \IPS\Settings::i()->mfa_required_groups ) ) )
{
if ( \IPS\Settings::i()->mfa_required_prompt === 'immediate' )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&do=initialMfa', 'front', 'settings' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
}
elseif ( \IPS\Settings::i()->mfa_optional_prompt === 'immediate' and !\IPS\Member::loggedIn()->members_bitoptions['security_questions_opt_out'] )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&do=initialMfa', 'front', 'settings' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
}
}
/* Need to reset password */
if ( $this->controller !== 'settings' AND ( !isset( \IPS\Request::i()->area ) OR \IPS\Request::i()->area !== 'password' ) )
{
foreach( \IPS\Login::methods() AS $method )
{
if ( $url = $method->forcePasswordResetUrl( \IPS\Member::loggedIn(), \IPS\Request::i()->url() ) )
{
\IPS\Output::i()->redirect( $url );
}
}
\IPS\Output::i()->redirect( $url );
}
}
@@ -287,7 +219,10 @@ class _Front extends \IPS\Dispatcher\Standard
{
\IPS\Output::i()->error( 'requested_route_404', '2S160/5', 404, '' );
}
/* Set up isAnonymous variable for realtime */
\IPS\Output::i()->jsVars['isAnonymous'] = (bool) \IPS\Member::loggedIn()->isOnlineAnonymously();
/* Stuff for output */
if ( !\IPS\Request::i()->isAjax() )
{
@@ -356,7 +291,7 @@ class _Front extends \IPS\Dispatcher\Standard
if ( \IPS\Settings::i()->use_friendly_urls )
{
$url = \IPS\Request::i()->url();
/* Redirect to the "correct" friendly URL if there is one */
if ( !\IPS\Request::i()->isAjax() and mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' and !\IPS\ENFORCE_ACCESS )
{
@@ -368,13 +303,23 @@ class _Front extends \IPS\Dispatcher\Standard
$correctUrl = $url->correctFriendlyUrl();
}
/* If they are accessing "index.php/whatever", we want "index.php?/whatever */
if ( !( $correctUrl instanceof \IPS\Http\Url ) and $url instanceof \IPS\Http\Url\Internal and mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], '/index.php/' ) !== FALSE )
if ( !( $correctUrl instanceof \IPS\Http\Url ) and $url instanceof \IPS\Http\Url\Internal )
{
$pathFromBaseUrl = ltrim( mb_substr( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], mb_strlen( \IPS\Http\Url::internal('')->data[ \IPS\Http\Url::COMPONENT_PATH ] ) ), '/' );
if ( mb_substr( $pathFromBaseUrl, 0, 10 ) === 'index.php/' )
/* If they are accessing "index.php/whatever", we want "index.php?/whatever */
if ( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], '/index.php/' ) !== FALSE )
{
$correctUrl = \IPS\Http\Url\Friendly::friendlyUrlFromComponent( 0, trim( mb_substr( $pathFromBaseUrl, 10 ), '/' ), $url->queryString );
if ( mb_substr( $pathFromBaseUrl, 0, 10 ) === 'index.php/' )
{
$correctUrl = \IPS\Http\Url\Friendly::friendlyUrlFromComponent( 0, trim( mb_substr( $pathFromBaseUrl, 10 ), '/' ), $url->queryString );
}
}
else
{
/* If necessary, return any special cases like the robots.txt file */
$this->customResponse( $pathFromBaseUrl );
}
}
@@ -451,16 +396,20 @@ class _Front extends \IPS\Dispatcher\Standard
protected function checkCached()
{
/* If this is a guest and there's a full cached page, we can serve that */
if( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' and !isset( \IPS\Request::i()->csrfKey ) and !isset( \IPS\Request::i()->_mfaLogin ) and !\IPS\Session\Front::loggedIn() and !isset( \IPS\Request::i()->cookie['noCache'] ) and \IPS\CACHE_PAGE_TIMEOUT )
if( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' AND !isset( \IPS\Request::i()->csrfKey ) AND !isset( \IPS\Request::i()->_mfaLogin ) AND !\IPS\Session\Front::loggedIn() AND !isset( \IPS\Request::i()->cookie['noCache'] ) AND \IPS\CACHE_PAGE_TIMEOUT )
{
/* Check whether a 304 Not modified response is appropriate */
if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) AND ( new \IPS\DateTime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )->getTimestamp() > ( time() - \IPS\CACHE_PAGE_TIMEOUT ) )
try
{
/* This is intentionally using PHP methods to avoid database connections */
$this->destruct = false;
header('HTTP/1.1 304 Not Modified' );
exit;
if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) AND ( new \IPS\DateTime( \rtrim( $_SERVER['HTTP_IF_MODIFIED_SINCE'], ';' ) ) )->getTimestamp() > ( time() - \IPS\CACHE_PAGE_TIMEOUT ) )
{
/* This is intentionally using PHP methods to avoid database connections */
$this->destruct = false;
header('HTTP/1.1 304 Not Modified' );
exit;
}
}
catch ( \Exception $e ) { }
/* Only check the output cache if it is enabled */
if( empty( \IPS\OUTPUT_CACHE_METHOD ) OR ( !empty( \IPS\OUTPUT_CACHE_METHOD ) AND \IPS\OUTPUT_CACHE_METHOD == 'None' ) )
@@ -660,18 +609,24 @@ class _Front extends \IPS\Dispatcher\Standard
}
}
/* Do things if we're actively using the easy theme editor */
\IPS\Theme::easyModePreOutput();
/* Meta tags */
\IPS\Output::i()->buildMetaTags();
/* Check MFA */
$this->checkMfa();
/* Check Alerts */
$this->checkAlerts();
/* Finish */
parent::finish();
}
/**
* Check MFA to see if we need to supply a code
* Check MFA to see if we need to supply a code. If the member elected to cancel, cancel (and redirect) here
*
* @param boolean $return Return any HTML (true) or add to Output (false)
*
@@ -685,6 +640,37 @@ class _Front extends \IPS\Dispatcher\Standard
$device = \IPS\Member\Device::loadOrCreate( $member, FALSE );
if ( $output = \IPS\MFA\MFAHandler::accessToArea( 'core', $device->known ? 'AuthenticateFrontKnown' : 'AuthenticateFront', \IPS\Request::i()->url(), $member ) )
{
/* Did we just cancel? */
if ( \IPS\Request::i()->_mfaCancel and ( ! \IPS\Member::loggedIn()->member_id or ( \IPS\Member::loggedIn()->member_id === $member->member_id ) ) )
{
/* We don't need this until we re-enter the MFA flow again */
unset( $_SESSION['processing2FA'] );
/* Is MFA required for this member? */
$mfaRequired = \IPS\Settings::i()->mfa_required_groups === '*' or $member->inGroup( explode( ',', \IPS\Settings::i()->mfa_required_groups ) );
/* Does this member require MFA upon login? */
$logout = \IPS\Settings::i()->mfa_required_prompt === 'immediate' and $mfaRequired;
/* Can they see this page without MFA? */
if ( !$mfaRequired OR ( $logout and $this->application->allowGuestAccess( $this->module, $this->controller, \IPS\Request::i()->do ) ) )
{
$redirectUrl = \IPS\Request::i()->url()->stripQueryString([ '_mfaCancel', '_mfaLogin', '_fromLogin', 'csrfKey' ]);
}
else
{
$redirectUrl = \IPS\Http\Url::internal( '' );
}
if ( $logout )
{
\IPS\Login::logout( $redirectUrl );
$redirectUrl = $redirectUrl->setQueryString( '_fromLogout', 1 );
}
\IPS\Output::i()->redirect( $redirectUrl );
}
if ( $return )
{
return $output;
@@ -699,6 +685,103 @@ class _Front extends \IPS\Dispatcher\Standard
}
}
/**
* Show a robots.txt file if configured to do so
*
* @param string $pathFromBaseUrl
*/
public function customResponse( string $pathFromBaseUrl )
{
if ( $pathFromBaseUrl === 'robots.txt' )
{
$this->robotsTxt();
}
else if ( \IPS\core\IndexNow::i()->isEnabled() AND $pathFromBaseUrl === \IPS\core\IndexNow::i()->getKeyFileName() )
{
$this->indexNow();
}
}
/**
* Return the IndexNow key.
*
* @return mixed
*/
protected function indexNow()
{
\IPS\Output::i()->sendOutput( \IPS\core\IndexNow::i()->getKeyfileContent(), 200, 'text/plain' );
}
/**
* Return the robots.txt files
*
* @return mixed
*/
protected function robotsTxt()
{
if ( \IPS\Settings::i()->robots_txt == 'default' )
{
\IPS\Output::i()->sendOutput( static::robotsTxtRules(), 200, 'text/plain' );
}
else if ( \IPS\Settings::i()->robots_txt != 'off' )
{
\IPS\Output::i()->sendOutput( \IPS\Settings::i()->robots_txt, 200, 'text/plain' );
}
throw new \OutOfRangeException;
}
/**
* Return the text for the robots.txt file
*
* @return string
*/
public static function robotsTxtRules(): string
{
$path = str_replace( '//', '/', '/' . trim( str_replace( 'robots.txt', '', \IPS\Http\Url::createFromString( \IPS\Http\Url::baseUrl() )->data[ \IPS\Http\Url::COMPONENT_PATH ] ), '/' ) . '/' );
$content = <<<FILE
# Rules for Invision Community (https://invisioncommunity.com)
User-Agent: *
# Block pages with no unique content
Disallow: {$path}startTopic/
Disallow: {$path}discover/unread/
Disallow: {$path}markallread/
Disallow: {$path}staff/
Disallow: {$path}online/
Disallow: {$path}discover/
Disallow: {$path}leaderboard/
Disallow: {$path}search/
Disallow: {$path}*?advancedSearchForm=
Disallow: {$path}register/
Disallow: {$path}lostpassword/
Disallow: {$path}login/
# Block faceted pages and 301 redirect pages
Disallow: {$path}*?sortby=
Disallow: {$path}*?filter=
Disallow: {$path}*?tab=
Disallow: {$path}*?do=
Disallow: {$path}*ref=
Disallow: {$path}*?forumId*
# Block profile pages as these have little unique value, consume a lot of crawl time and contain hundreds of 301 links
Disallow: {$path}profile/
FILE;
if ( \IPS\Settings::i()->sitemap_url )
{
$siteMapUrl = ( \IPS\Settings::i()->sitemap_url ) ? rtrim( \IPS\Settings::i()->sitemap_url, '/' ) : rtrim( \IPS\Settings::i()->base_url, '/' ) . '/sitemap.php';
$content .= <<<FILE
# Sitemap URL
Sitemap: {$siteMapUrl}
FILE;
}
return $content;
}
/**
* Output the basic javascript files every page needs
*
@@ -712,6 +795,10 @@ class _Front extends \IPS\Dispatcher\Standard
if ( !\IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->globalControllers[] = 'core.front.core.app';
if ( \IPS\Settings::i()->core_datalayer_enabled )
{
\IPS\Output::i()->globalControllers[] = 'core.front.core.dataLayer';
}
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front.js' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_core.js', 'core', 'front' ) );
@@ -777,4 +864,48 @@ class _Front extends \IPS\Dispatcher\Standard
}
}
}
/**
* Do Member Check
*
* @return \IPS\Http\Url|NULL
*/
protected static function doMemberCheck(): ?\IPS\Http\Url
{
foreach( \IPS\Application::applications() AS $app )
{
if ( $url = $app->doMemberCheck() )
{
return $url;
}
}
return NULL;
}
/**
*
*
* @return void
*/
public function checkAlerts()
{
/* Don't get in the way of validating members */
if ( \IPS\Member::loggedIn()->members_bitoptions['validating'] )
{
return;
}
/* Don't get in the way of the ModCP, registering, logging in, etc */
$ignoreControllers = [ 'modcp', 'register', 'login', 'redirect' ];
if( !\IPS\Request::i()->isAjax() and !\in_array( $this->controller, $ignoreControllers ) AND $alert = \IPS\core\Alerts\Alert::getNextAlertForMember( \IPS\Member::loggedIn() ) )
{
$alert->viewed( \IPS\Member::loggedIn() );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/alerts.css', 'core', 'front' ) );
\IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'alerts', 'core', 'front' )->alertModal( $alert, $url = base64_encode( \IPS\Request::i()->url() ) );
}
return;
}
}