Version 5.0.0 release

This commit is contained in:
Neo committed 2025-12-19 18:37:58 -08:00
1 parent 90e8c7e903
commit a3a4a9ccbd
651 files changed
+7275 -9481

No files matched your search

@@ -781,6 +781,9 @@ class ajax extends Controller
/* Don't cache the CSRF key */
Output::setCacheTime( false );
/* Restrict endpoint to our origin JS */
Output::i()->httpHeaders['Access-Control-Allow-Origin'] = Url::internal('')->data[ Url::COMPONENT_SCHEME ] . '://' . Url::internal('')->data[ Url::COMPONENT_HOST ];
if ( isset( Request::i()->path ) )
{
$baseUrlData = parse_url( Url::baseUrl() );
@@ -811,7 +814,8 @@ class ajax extends Controller
}
}
Output::i()->json( [ 'key' => Session::i()->csrfKey ] );
Output::i()->json( [ 'key' => Session::i()->csrfKey, 'expiry' => time() + 500 ] );
}
/**
@@ -19,10 +19,13 @@ use IPS\Member;
use IPS\Output;
use IPS\Settings;
use IPS\Theme;
use IPS\Xml\DOMDocument;
use function count;
use function defined;
use const IPS\DEBUG_JS;
use const IPS\DEV_DEBUG_JS;
use const IPS\IN_DEV;
use const IPS\ROOT_PATH;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -43,7 +46,6 @@ class serviceworker extends Controller
protected function manage() : void
{
$cachedUrls = array();
$cachedUrls[] = (string) Url::internal("app=core&module=system&controller=offline", "front", "user_offline");
$notificationIcon = NULL;
@@ -63,31 +65,57 @@ class serviceworker extends Controller
}
/* VARIABLES TO PASS THROUGH TO JS */
$DEBUG = ( ( \IPS\IN_DEV and DEV_DEBUG_JS ) or DEBUG_JS ) ? 'true' : 'false'; // Weird casting is intentional.
$BASE_URL = Settings::i()->base_url;
$CACHED_ASSETS = json_encode( $cachedUrls, JSON_UNESCAPED_SLASHES );
$OFFLINE_URL = (string) Url::internal("app=core&module=system&controller=offline", "front", "user_offline");
$CACHE_VERSION = Theme::i()->cssCacheBustKey();
$NOTIFICATION_ICON = $notificationIcon ? "\"{$notificationIcon}\"" : 'null'; // Weird casting is intentional. 'null' will become literal null in JS file.
$DEFAULT_NOTIFICATION_TITLE = Member::loggedIn()->language()->addToStack('default_notification_title');
$DEFAULT_NOTIFICATION_BODY = Member::loggedIn()->language()->addToStack('default_notification_body');
$variables = [
"DEBUG" => boolval( ( IN_DEV and DEV_DEBUG_JS ) or DEBUG_JS ),
"BASE_URL" => Settings::i()->base_url,
"CACHED_ASSETS" => $cachedUrls,
"CACHE_NAME" => "invision-community-{$CACHE_VERSION}",
"CACHE_VERSION" => $CACHE_VERSION,
"NOTIFICATION_ICON" => $notificationIcon ?: null,
"DEFAULT_NOTIFICATION_TITLE" => Member::loggedIn()->language()->addToStack( 'default_notification_title'),
"DEFAULT_NOTIFICATION_BODY" => Member::loggedIn()->language()->addToStack( 'default_notification_body'),
"OFFLINE_PAGE" => $this->buildCollapsedOfflinePage(),
];
$output = <<<JAVASCRIPT
"use strict";
const DEBUG = {$DEBUG};
const BASE_URL = "{$BASE_URL}";
const CACHED_ASSETS = {$CACHED_ASSETS};
const CACHE_NAME = 'invision-community-{$CACHE_VERSION}';
const OFFLINE_URL = "{$OFFLINE_URL}";
const NOTIFICATION_ICON = {$NOTIFICATION_ICON};
const DEFAULT_NOTIFICATION_TITLE = "{$DEFAULT_NOTIFICATION_TITLE}";
const DEFAULT_NOTIFICATION_BODY = "{$DEFAULT_NOTIFICATION_BODY}";
$variables["OFFLINE_PAGE_SIZE"] = \strlen( $variables["OFFLINE_PAGE"] );
$output = "\"use strict;\"\n\n";
foreach ( $variables as $var => $value )
{
$toEncode = json_encode( $value, JSON_UNESCAPED_SLASHES );
$output .= <<<JAVASCRIPT
const {$var} = {$toEncode};
JAVASCRIPT;
}
Member::loggedIn()->language()->parseOutputForDisplay( $output );
$output .= file_get_contents( \IPS\ROOT_PATH . '/applications/core/interface/js/serviceWorker.js' );
$cacheHeaders = !\IPS\IN_DEV ? Output::getCacheHeaders(time(), 86400) : array();
$output .= file_get_contents( ROOT_PATH . '/applications/core/interface/js/serviceWorker.js' );
$cacheHeaders = !IN_DEV ? Output::getCacheHeaders(time(), 86400) : array();
Output::i()->sendOutput($output, 200, 'text/javascript', $cacheHeaders);
}
/**
* Return template for offline page
*
* @return string
*/
protected function buildCollapsedOfflinePage() : string
{
$html = Theme::i()->getTemplate( 'global', 'core', 'global' )->offline();
Member::loggedIn()->language()->parseOutputForDisplay( $html );
$doc = new DOMDocument( "2.0", "utf-8" );
$doc->preserveWhiteSpace = false;
$doc->loadHTML( $html, LIBXML_NOBLANKS );
$doc->formatOutput = true;
$compact = $doc->saveHTML();
// We don't need these segments of whitespace. HTML entities can be used if it's absolutely necessary
return preg_replace( "/\s+/", " ", $compact );
}
}
@@ -132,6 +132,53 @@ class themeeditor extends Controller
}
}
/* Handle image settings (except logos) */
foreach( Db::i()->select( 'setting_key', 'core_theme_editor_settings', [
[ 'setting_type=?', Theme\Editor\Setting::SETTING_IMAGE ],
//[ Db::i()->in( 'setting_key', [ 'set__logo-light', 'set__logo-dark', 'set__mobile-logo-light', 'set__mobile-logo-dark' ], true ) ]
] ) as $key )
{
/* Did we delete the file? */
if( isset( Request::i()->{ 'delete__' . $key } ) and empty( Request::i()->$key ) )
{
$newValue = '';
}
elseif ( isset( $_FILES[ $key ] ) and $_FILES[ $key ]['tmp_name'] )
{
$imageData = $this->_handleImageUpload( $key, false );
/* If we specifically set the value to null, then we will use the default value */
$newValue = isset( $imageData['fullUrl'] ) ? (string) $imageData['fullUrl'] : null;
}
else
{
$newValue = $changed[ $key ] ?? null;
}
$cssVariables[ $key ] = $newValue;
/* Only delete the file if it was not the default! */
if( empty( $newValue ) and !empty( $changed[ $key ] ) )
{
File::get( 'core_Theme', $changed[$key] )->delete();
}
if ( $newValue !== null and $newValue !== $rootCssVariables[ $key ] )
{
$changed[ $key ] = $newValue;
}
else
{
/* If we previously changed the value and we changed it back, clear it */
if( array_key_exists( $key, $changed ) )
{
unset( $changed[ $key ] );
}
$unchanged[ $key ] = $rootCssVariables[ $key ];
}
}
/* Save the css */
Theme::i()->setCssVariables( $changed );
@@ -173,64 +220,9 @@ class themeeditor extends Controller
File::get( 'core_Theme', Theme::i()->logo[ $logoKey ]['url'] )->delete();
$data['logo'][$logoKey] = [];
}
else
elseif ( isset( $_FILES[ 'set__' . $type ] ) and $_FILES[ 'set__' . $type ]['tmp_name'] )
{
if ( isset( $_FILES['set__' . $type] ) and $_FILES['set__' . $type]['tmp_name'] )
{
$file = $_FILES['set__' . $type];
$fileExt = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
$imageAttributes = getimagesize( $file['tmp_name'] );
$logoHeightSetting = 'set__i-logo--he';
if ( $fileExt === 'svg' or in_array( $imageAttributes[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP, IMAGETYPE_AVIF] ) )
{
/* So we can use the File::create() obfuscation, but it will not re-add the file extension back on for SVG, so... */
$baseName = pathinfo( $file['name'], PATHINFO_FILENAME );
$extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
$randomKey = substr(bin2hex( random_bytes(3) ), 0, 6); // Produces a 6-character long key
$uniqueFilename = "{$baseName}_{$randomKey}.{$extension}";
try
{
if ( $fileExt !== 'svg' )
{
$image = Image::create( file_get_contents( $file['tmp_name'] ), false );
$width = $image->width;
$height = $image->height;
}
else
{
$image = file_get_contents( $file['tmp_name'] );
$width = null;
$height = null;
try
{
/* I tried many fancy methods including XML parsing but they all failed, but regex will always work becaue it is the best thing in the world */
preg_match( '#<image(?:.+?)width="(\d+)"\s+height="(\d+)"#i', $image, $matches );
if ( isset( $matches[1] ) and isset( $matches[2] ) )
{
$width = $matches[1];
$height = $matches[2];
}
}
catch ( Exception $e ) { }
}
$file = File::create( 'core_Theme', $uniqueFilename, $image, null, true, null, false );
$data['logo'][$logoKey] = [
'url' => (string)$file,
'setting_height' => isset( Request::i()->$logoHeightSetting ) ? Request::i()->$logoHeightSetting : 100,
'img_width' => $width,
'img_height' => $height,
];
}
catch ( RuntimeException $e )
{
$data['logo'][$logoKey] = [];
}
}
}
$data['logo'][ $logoKey ] = $this->_handleImageUpload( 'set__' . $type );
}
}
@@ -281,6 +273,73 @@ class themeeditor extends Controller
Output::i()->redirect( Url::internal( "" ), ( \IPS\IN_DEV and ! Settings::i()->theme_designer_mode ) ? 'theme_editor_saved_indev' : 'theme_editor_saved' );
}
/**
* Handle uploads in image settings
*
* @param string $key
* @param bool $isLogo
* @return array
*/
protected function _handleImageUpload( string $key, bool $isLogo=true ) : array
{
if ( isset( $_FILES[ $key ] ) and $_FILES[ $key ]['tmp_name'] )
{
$file = $_FILES[ $key ];
$fileExt = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
$imageAttributes = getimagesize( $file['tmp_name'] );
$logoHeightSetting = 'set__i-logo--he';
if ( $fileExt === 'svg' or in_array( $imageAttributes[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP, IMAGETYPE_AVIF] ) )
{
/* So we can use the File::create() obfuscation, but it will not re-add the file extension back on for SVG, so... */
$baseName = pathinfo( $file['name'], PATHINFO_FILENAME );
$extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
$randomKey = substr(bin2hex( random_bytes(3) ), 0, 6); // Produces a 6-character long key
$uniqueFilename = "{$baseName}_{$randomKey}.{$extension}";
try
{
if ( $fileExt !== 'svg' )
{
$image = Image::create( file_get_contents( $file['tmp_name'] ), false );
$width = $image->width;
$height = $image->height;
}
else
{
$image = file_get_contents( $file['tmp_name'] );
$width = null;
$height = null;
try
{
/* I tried many fancy methods including XML parsing, but they all failed, but regex will always work becaue it is the best thing in the world */
preg_match( '#<image(?:.+?)width="(\d+)"\s+height="(\d+)"#i', $image, $matches );
if ( isset( $matches[1] ) and isset( $matches[2] ) )
{
$width = $matches[1];
$height = $matches[2];
}
}
catch ( Exception $e ) { }
}
$file = File::create( 'core_Theme', $uniqueFilename, $image, null, true, null, false );
return [
'url' => (string) $file,
'fullUrl' => (string) $file->url,
'setting_height' => ( isset( Request::i()->$logoHeightSetting ) and $isLogo ) ? Request::i()->$logoHeightSetting : 100,
'img_width' => $width,
'img_height' => $height,
];
}
catch ( RuntimeException $e ){}
}
}
return [];
}
/**
* Close the theme editor
*
@@ -296,4 +355,23 @@ class themeeditor extends Controller
Output::i()->redirect( $redirect );
}
/**
* Parse Custom CSS so that we can properly handle any
* resource tags, etc
*
* @return void
*/
protected function customCss() : void
{
$functionName = "css_" . uniqid();
Theme::makeProcessFunction( Theme::fixResourceTags( (string) Request::i()->content, 'front' ), $functionName, '', FALSE, TRUE );
$fqFunc = 'IPS\\Theme\\'. $functionName;
$content = Theme::minifyCss( $fqFunc() );
/* Replace any <fileStore.xxx> tags in the CSS */
Output::i()->parseFileObjectUrls( $content );
Output::i()->json( [ 'content' => $content ] );
}
}