Version 4.7.23

This commit is contained in:
Neo committed 2025-12-19 16:21:27 -08:00
1 parent 7124a02564
commit 25ddeb65d6
1791 files changed
+76990 -44452

No files matched your search

+169 -50
View File
@@ -11,6 +11,10 @@
namespace IPS;
/* To prevent PHP errors (extending class does not exist) revealing path */
use function class_exists;
use function time;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
@@ -235,6 +239,11 @@ class _Output
* @brief Data which were loaded via the GraphQL framework, but which have to be immediately available, rather then via later AJAX requests
*/
public $graphData = [];
/**
* @brief A custom cache DateTime object set to UTC
*/
protected static bool|null|DateTime $cacheDate = NULL;
/**
* Constructor
@@ -245,6 +254,9 @@ class _Output
{
if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation !== 'setup' )
{
/* Additional security: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Opener-Policy */
$this->httpHeaders['Cross-Origin-Opener-Policy'] = "same-origin";
if ( \IPS\Settings::i()->clickjackprevention == 'csp' )
{
$this->httpHeaders['Content-Security-Policy'] = \IPS\Settings::i()->csp_header;
@@ -371,7 +383,7 @@ class _Output
/* {location}_{section}.js */
else if ( mb_strstr( $file, '_') AND mb_substr( $file, -3 ) === '.js' )
{
list( $location, $key ) = explode( '_', mb_substr( $file, 0, -3 ) );
[ $location, $key ] = explode( '_', mb_substr( $file, 0, -3 ) );
if ( ( $location == 'front' OR $location == 'admin' OR $location == 'global' ) AND ! empty( $key ) )
{
@@ -472,6 +484,17 @@ class _Output
return $title;
}
/**
* Store any custom DateTime for cache headers
*
* @param DateTime|bool $date
* @return void
*/
public static function setCacheTime( \IPS\DateTime|bool $date=false ): void
{
static::$cacheDate = $date;
}
/**
* Retrieve cache headers
*
@@ -481,10 +504,43 @@ class _Output
*/
public static function getCacheHeaders( int $lastModified, int $cacheSeconds ): array
{
/* The default is null, so if it is to false from setCacheTime, we should not cache */
if ( static::$cacheDate === false )
{
return static::getNoCacheHeaders();
}
$expires = \IPS\DateTime::ts( ( time() + $cacheSeconds ), TRUE );
if ( static::$cacheDate instanceof \DateTime )
{
$expires = static::$cacheDate;
$cacheSeconds = $expires->getTimestamp() - time();
}
else
{
/* Set the cache date to the expiration date in case anything intercepts the output process and checks the cacheDate */
static::$cacheDate = $expires;
}
if ( \IPS\CIC or \IPS\IN_DEV )
{
return array(
'Date' => \IPS\DateTime::ts( time(), TRUE )->rfc1123(),
'Last-Modified' => \IPS\DateTime::ts( $lastModified, TRUE )->rfc1123(),
'Expires' => $expires->rfc1123(),
'Cache-Control' => implode( ', ', [
'max-age=0', // No cache for the browser [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives]
'public', // Public cache
's-maxage=' . $cacheSeconds, // Cache for the CDN [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives]
'stale-if-error' // Allow the CDN to serve stale content if there is an error
] )
);
}
return array(
'Date' => \IPS\DateTime::ts( time(), TRUE )->rfc1123(),
'Last-Modified' => \IPS\DateTime::ts( $lastModified, TRUE )->rfc1123(),
'Expires' => \IPS\DateTime::ts( ( time() + $cacheSeconds ), TRUE )->rfc1123(),
'Expires' => $expires->rfc1123(),
'Cache-Control' => 'no-cache="Set-Cookie", max-age=' . $cacheSeconds . ", public, s-maxage=" . $cacheSeconds . ", stale-while-revalidate, stale-if-error",
);
}
@@ -713,7 +769,7 @@ class _Output
/* Log */
$level = \intval( \substr( $code, 0, 1 ) );
if( !\IPS\Session::i()->userAgent->spider )
if( !\IPS\Session::i()->userAgent->bot )
{
if( $code and \IPS\Settings::i()->error_log_level and $level >= \IPS\Settings::i()->error_log_level )
{
@@ -755,16 +811,6 @@ class _Output
}
}
/* Send default Cache Headers if this is a front-end page. */
$allowGuestCache = FALSE;
if ( !isset( $httpHeaders['Cache-Control'] ) AND ( $httpStatusCode == 404 OR $httpStatusCode == 403 ) )
{
if ( \IPS\CACHE_PAGE_TIMEOUT AND $this->pageCaching AND \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'front' AND !\IPS\Member::loggedIn()->member_id )
{
$httpHeaders += static::getCacheHeaders( time(), \IPS\CACHE_PAGE_TIMEOUT );
}
}
/* Send output */
\IPS\Output::i()->sidebar['enabled'] = FALSE;
$this->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( $title, \IPS\Theme::i()->getTemplate( 'global', 'core' )->error( $title, $message, $code, $extra, $member, $faulty, $httpStatusCode ), array( 'app' => \IPS\Dispatcher::i()->application ? \IPS\Dispatcher::i()->application->directory : NULL, 'module' => \IPS\Dispatcher::i()->module ? \IPS\Dispatcher::i()->module->key : NULL, 'controller' => \IPS\Dispatcher::i()->controller ) ), $httpStatusCode, 'text/html', $httpHeaders, FALSE, FALSE );
@@ -806,6 +852,79 @@ class _Output
}
}
/**
* Can this output be cached by CDN?
*
* @param string $contentType
* @param bool $checkCachePageTimeout
* @return bool
*/
public function isCacheable( string $contentType='text/html', bool $checkCachePageTimeout = true ): bool
{
/* Have we got a value for the default cache timeout? */
if ( $checkCachePageTimeout and ! \IPS\CACHE_PAGE_TIMEOUT )
{
return false;
}
/* Have we set a per-page cache timeout of false, which means we don't want to cache? */
if ( static::$cacheDate === false )
{
return false;
}
else if ( static::$cacheDate instanceof \IPS\DateTime )
{
/* Or have we set a specific cache date and it's in the past? */
$cacheSeconds = static::$cacheDate->getTimestamp() - time();
if ( $cacheSeconds <= 0 )
{
return false;
}
}
/* Are we logged in? */
if ( \IPS\Member::loggedIn()->member_id )
{
return false;
}
/* Are we using the legacy pageCaching flag? */
if ( ! $this->pageCaching )
{
return false;
}
/* Do we have a noCache cookie set? */
if ( isset( \IPS\Request::i()->cookie['noCache'] ) )
{
return false;
}
/* Do we have a CSRF key? */
if ( isset( \IPS\Request::i()->csrfKey ) )
{
return false;
}
/* Check the request method */
if ( ! in_array( mb_strtolower( $_SERVER['REQUEST_METHOD'] ), [ 'get', 'head' ] ) )
{
return false;
}
if ( ! in_array( $contentType, ['text/html', 'text/xml', 'application/manifest+json' ] ) )
{
return false;
}
if ( \IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) and \IPS\Dispatcher::i()->controllerLocation !== 'front' )
{
return false;
}
return true;
}
/**
* @brief Flag to bypass CSRF IN_DEV check
*/
@@ -862,36 +981,18 @@ class _Output
$output = $this->replaceEmojiWithImages( $output );
}
/* Combine some common checks for re-use */
$canCache = function( $contentType, $obj ) {
if( $obj->pageCaching and
( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' OR mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'head' ) and // Is a HTTP GET/HEAD request (don't cache output for POSTs)
( $contentType == 'text/html' or $contentType == 'text/xml' or $contentType == 'application/manifest+json' ) and // Output is HTML, XML or [PWA] application manifest (don't cache JSON output, etc)
\IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) and \IPS\Dispatcher::i()->controllerLocation === 'front' // Is a normal, front-end page (necessary to know if user is logged in)
)
{
return TRUE;
}
return FALSE;
};
if (
\IPS\CACHE_PAGE_TIMEOUT and // Page caching is enabled
$cacheThisPage and // Some pages can specify not to be cached (for example, when displaying a cached page, you don't want it recached)
!isset( \IPS\Request::i()->cookie['noCache'] ) and // A noCache cookie might get set to not cache a particular guest (for example, if they have added items to their cart in the store)
$canCache( $contentType, $this ) and // Common checks, see above
!isset( \IPS\Request::i()->csrfKey ) and // CSRF key isn't present (which would be like a POST request)
!\IPS\Member::loggedIn()->member_id // User is not logged in
) {
/* Can we cache this page to a CDN? */
if( $cacheThisPage and $this->isCacheable( $contentType ) )
{
/* Add caching headers */
if( !isset( $httpHeaders['Cache-Control'] ) )
{
$httpHeaders += \IPS\Output::getCacheHeaders( time(), \IPS\CACHE_PAGE_TIMEOUT );
}
}
/* Send no-cache headers if we got to this point without any cache-control headers being set, or page caching is forced off */
elseif( !isset( $httpHeaders['Cache-Control'] ) OR !$this->pageCaching )
elseif( !isset( $httpHeaders['Cache-Control'] ) )
{
/* Send no-cache headers if we got to this point without any cache-control headers being set, or page caching is forced off */
$httpHeaders += \IPS\Output::getNoCacheHeaders();
}
@@ -1292,7 +1393,22 @@ class _Output
}
return $output;
}
/**
* Should we reduce the number of links in the HTML for SEO purposes?
*
* @return bool
*/
public function reduceLinks(): bool
{
if ( ! \IPS\Member::loggedIn()->member_id and \IPS\Settings::i()->seo_reduce_links )
{
return true;
}
return false;
}
/**
* Show Offline
*
@@ -1462,8 +1578,9 @@ class _Output
}
else
{
if( trim( $row['meta_url'], '/' ) == trim( $this->metaTagsUrl, '/' ) and ( ( $row['meta_url'] == '/' and \IPS\Request::i()->url()->data['path'] == $rootPath ) or $row['meta_url'] !== '/' ) )
if( trim( $row['meta_url'], '/' ) == trim( $this->metaTagsUrl, '/' ) and ( ( $row['meta_url'] == '/' and rtrim( \IPS\Request::i()->url()->data['path'], '/' ) . '/' == rtrim( $rootPath, '/' ) . '/' ) or $row['meta_url'] !== '/' ) )
{
$_tags = json_decode( $row['meta_tags'], TRUE );
if ( \is_array( $_tags ) )
@@ -1607,28 +1724,30 @@ class _Output
foreach( $this->breadcrumb as $breadcrumb )
{
$crumb = [
'@type' => "ListItem",
'position' => $position,
'item' => [
'name' => $breadcrumb[1],
]
];
if( $breadcrumb[0] )
{
$elements[] = array(
'@type' => "ListItem",
'position' => $position,
'item' => array(
'@id' => (string) $breadcrumb[0],
'name' => $breadcrumb[1],
)
);
$position++;
$crumb['item']['@id'] = (string) $breadcrumb[0];
}
$elements[] = $crumb;
$position++;
}
if( \count( $elements ) )
{
$jsonLd['breadcrumbs'] = array(
$jsonLd['breadcrumbs'] = [
'@context' => "http://schema.org",
'@type' => "BreadcrumbList",
'itemListElement' => $elements,
);
];
}
}