Version 5.0.13

This commit is contained in:
Neo committed 2025-12-19 19:04:57 -08:00
1 parent b89858b2ff
commit 5efe744c5f
1407 files changed
+69690 -30177

No files matched your search

+129 -5
View File
@@ -51,10 +51,15 @@ use function is_dir;
use function is_file;
use function is_string;
use function json_encode;
use function mb_strtolower;
use function md5;
use function mkdir;
use function preg_match;
use function rtrim;
use function str_ends_with;
use function substr;
use function unlink;
use function urlencode;
use const IPS\CACHEBUST_KEY;
use const IPS\IPS_FILE_PERMISSION;
use const IPS\IPS_FOLDER_PERMISSION;
@@ -891,10 +896,38 @@ class Javascript extends ActiveRecord
'javascript_app, javascript_location, javascript_position'
)->setKeyField('javascript_id') );
/* Web Components are not loaded along with the rest of dev/js/framework */
$locationRows = [];
$componentRows = [];
foreach ( $rows as $id => $row )
{
if ( $fileName === 'framework.js' and $row['javascript_type'] === 'component' )
{
$componentRows[$id] = $row;
}
else
{
$locationRows[$id] = $row;
}
}
/* Write it */
$obj = static::_writeJavascriptFromResultset( $rows, $fileName, $app, 'root' );
$obj = static::_writeJavascriptFromResultset( $locationRows, $fileName, $app, 'root' );
$map[ $app ][ md5( $app .'-' . 'root' . '-' . $fileName ) ] = $obj ? (string) $obj : null;
/* Web Components should have their own file */
foreach ( $componentRows as $id => $row )
{
$componentName = explode( '.', $row['javascript_name'] )[0];
if ( !preg_match( "/^[a-zA-Z][a-zA-Z0-9]*$/", $componentName ) )
{
continue;
}
$row['javascript_content'] = static::wrapWebComponentContents( $row['javascript_content'], $componentName );
$componentObj = static::_writeJavascriptFromResultset( [ $id => $row ], "component_" . $row['javascript_name'], $app, 'root' );
$map[ $app ][ md5( $app . '-root-' . "component_" . $row['javascript_name'] ) ] = $componentObj ? (string) $componentObj : null;
}
}
}
}
@@ -1170,7 +1203,7 @@ class Javascript extends ActiveRecord
*
* @param string $file Filename
* @param string|null $app Application
* @param string|null $location Location (e.g. 'admin', 'front')
* @param string|null $location Location (e.g. 'admin', 'front', 'components')
* @return array URL to JS files
*/
public static function inDevJs( string $file, string $app=NULL, string $location=NULL ): array
@@ -1189,6 +1222,16 @@ class Javascript extends ActiveRecord
$location = '/';
}
}
// Loading a web component? we then need to load from the file directly
if ( $location === "components" )
{
if ( str_ends_with( $file, '.js' ) )
{
$file = mb_substr( $file, 0, -3 );
}
return array( Url::baseUrl() . "/applications/core/interface/js/webcomponents.php?component=" . urlencode( $file ) );
}
$app = $app ?: ( Dispatcher::i()->application ? Dispatcher::i()->application->directory : NULL );
$location = $location ?: Dispatcher::i()->controllerLocation;
@@ -1223,7 +1266,7 @@ class Javascript extends ActiveRecord
{
return static::_directoryJs( ROOT_PATH . "/dev/js/" . mb_substr( $file, 0, -3 ) );
}
$path = ROOT_PATH . "/dev/js";
}
else
@@ -1407,8 +1450,9 @@ class Javascript extends ActiveRecord
{
continue;
}
if ( $file->isDir() )
// Skip the /framework/common/components directory
if ( $file->isDir() && !( str_ends_with( $path, 'dev/js/framework/common' ) and rtrim( $file->getFilename(), '/' ) == 'components' ) )
{
$return = static::_directoryJs( $path . '/' . $file->getFileName(), $return );
}
@@ -1508,6 +1552,10 @@ class Javascript extends ActiveRecord
{
$type = 'mixins';
}
else if ( mb_strstr( $path, '/components/' ) )
{
$type = 'component';
}
else if ( mb_strstr( $path, '/ui/' ) )
{
$type = 'ui';
@@ -1572,4 +1620,80 @@ class Javascript extends ActiveRecord
return $map;
}
/**
* Wrap contents with the web components
*
* @param string $contents
* @param string $component The component's file name
* @return string
*/
protected static function wrapWebComponentContents( string $contents, string $component ) : string
{
/* Start with some light validation to make sure the component is a valid string */
if ( !preg_match( '/^[a-zA-Z][a-zA-Z0-9]*$/', $component ) )
{
throw new InvalidArgumentException( "The name {$component} is invalid. Expected an alphanumeric string (camel case)" );
}
$encodedComponent = json_encode( $component );
$errorStatement = <<<JS
Debug.log(`The source file for the component \${{$encodedComponent}} does not contain a class that extends HTMLElement. If the component extends a class other than HTMLElement, ips.ui.registerWebComponent() will need to be invoked directly.`);
JS;
if ( preg_match( "/class\s+([a-zA-Z][a-zA-Z0-9]*)\s+extends\s+HTMLElement/", $contents, $matches ) )
{
$classStatement = <<<JS
ips.ui.registerWebComponent( {$encodedComponent}, {$matches[1]} );
Debug.log(`Submitted the web component constructor, {$matches[1]}, for \${{$encodedComponent}}`);
JS;
}
else
{
$classStatement = $errorStatement;
}
return <<<JS
;(function() {
"use strict";
{$contents}
{$classStatement}
})();
JS;
}
/**
* Generate the JS that defines a Web Component
*
* @param string $component
*
* @return string
*/
public static function generateWebComponent( string $component ) : string
{
/* Start with some light validation to make sure the component is a valid string */
if ( !preg_match( '/^[a-zA-Z][a-zA-Z0-9]*$/', $component ) )
{
throw new InvalidArgumentException( "The name {$component} is invalid. Expected an alphanumeric string (camel case)" );
}
if ( \IPS\IN_DEV )
{
$path = Application::getRootPath() . '/dev/js/framework/common/components/' . $component . '.js';
if ( file_exists( $path ) and $contents = file_get_contents( $path ) )
{
return static::wrapWebComponentContents( $contents, $component );
}
}
else
{
$path = Application::getRootPath() . "static/js/global/root_component_" . $component . 'js';
if ( file_exists( $path ) and $contents = file_get_contents( $path ) )
{
return $contents;
}
}
throw new OutOfRangeException( "Cannot find file $path" );
}
}
+72 -21
View File
@@ -258,6 +258,11 @@ class Output
* @brief Additional CSS classes to add to body tag
*/
public array $bodyClasses = array();
/**
* @brief Additional data attributes to add to body tag
*/
public array $bodyAttributes = array();
/**
* @brief Elements that can be hidden from view
@@ -279,11 +284,6 @@ class Output
*/
public ?string $base = NULL;
/**
* @brief Allow default widgets with this output
*/
public bool $allowDefaultWidgets = TRUE;
/**
* @brief Allow page caching. This can be set at any point during controller execution to override defaults
*/
@@ -313,6 +313,9 @@ class Output
{
if ( Dispatcher::hasInstance() and 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 ( Settings::i()->clickjackprevention == 'csp' )
{
$this->httpHeaders['Content-Security-Policy'] = Settings::i()->csp_header;
@@ -380,6 +383,7 @@ class Output
* @li admin.js or front.js (these are controllers, templates and models which are used everywhere for that location)
* @li app.js (this is all models for a single application)
* @li {location}_{section}.js (this is all controllers and templates for this section called ad-hoc when needed)
* @li {component}.js - Load a Web Component from the Web Component files; Must set $location to "components" when using this option
*
* @param string $file Filename
* @param string|null $app Application
@@ -391,7 +395,7 @@ class Output
$file = trim( $file, '/' );
/* Legacy code support. The following directories were moved to /static */
if ( ( $app === "core" or $app === null) and $location === "interface" and preg_match( "/^(?:codemirror|fontawesome|themeeditor)/", $file ) )
if ( ( $app === "core" or $app === null) and $location === "interface" and preg_match( "/^(?:codemirror|fontawesome)/", $file ) )
{
$file = "static/" . $file;
}
@@ -412,7 +416,12 @@ class Output
}
$fileObj = null;
if ( $app === null OR $app === 'global' )
if ( $location === 'components' )
{
$fileName = "component_" . $file;
$fileObj = static::_getJavascriptFileObject( 'global', 'root', $fileName );
}
else if ( $app === null OR $app === 'global' )
{
if ( in_array( $file, static::$globalJavascript ) )
{
@@ -894,15 +903,6 @@ class Output
$faulty = Member::loggedIn()->language()->addToStack( 'faulty_app', FALSE, array( 'sprintf' => array( $app->_title, Url::internal( 'app=core&module=applications&controller=applications', 'admin' ) ) ) );
}
/* Send default Cache Headers if this is a front-end page. */
if ( !isset( $httpHeaders['Cache-Control'] ) AND ( $httpStatusCode == 404 OR $httpStatusCode == 403 ) )
{
if ( CACHE_PAGE_TIMEOUT AND $this->pageCaching AND Dispatcher::hasInstance() AND Dispatcher::i()->controllerLocation === 'front' AND !Member::loggedIn()->member_id )
{
$httpHeaders += static::getCacheHeaders( time(), CACHE_PAGE_TIMEOUT);
}
}
/* Send output */
if ( Application::appIsEnabled( 'cms' ) and isset( Settings::i()->cms_error_page ) and Settings::i()->cms_error_page )
{
@@ -910,6 +910,7 @@ class Output
}
else
{
Output::i()->setBodyAttributes();
Output::i()->sidebar['enabled'] = FALSE;
$this->sendOutput(Theme::i()->getTemplate('global', 'core')->globalTemplate($title, Theme::i()->getTemplate('global', 'core')->error($title, $message, $code, $extra, $member, $faulty, $httpStatusCode), array('app' => Dispatcher::i()->application ? Dispatcher::i()->application->directory : NULL, 'module' => Dispatcher::i()->module ? Dispatcher::i()->module->key : NULL, 'controller' => Dispatcher::i()->controller)), $httpStatusCode, 'text/html', $httpHeaders, FALSE );
}
@@ -1422,7 +1423,13 @@ JS;
*/
public function redirect( Url|string $url, ?string $message='', int $httpStatusCode=301, bool $forceScreen=FALSE ) : void
{
if( Request::i()->isAjax() )
/* We have to cache here instead of waiting for sendOutput because this method can close the session */
if ( DataLayer::hasInstance() and !$this->bypassDataLayer and Dispatcher::hasInstance() and Dispatcher::i()->controllerLocation == 'front' )
{
DataLayer::i()->cache();
}
if( Request::i()->isAjax() and !Request::i()->bypassAjaxRedirect() )
{
if( !empty( $message ) )
{
@@ -1519,6 +1526,7 @@ JS;
$this->title = Settings::i()->board_name;
Output::i()->sidebar['enabled'] = FALSE;
Output::i()->setBodyAttributes();
DispatcherFront::i()->checkMfa();
@@ -1569,6 +1577,7 @@ JS;
$this->title = Settings::i()->board_name;
Output::i()->sidebar['enabled'] = FALSE;
Output::i()->setBodyAttributes();
/* Unset page token */
unset( Output::i()->jsVars['page_token'] );
@@ -1618,13 +1627,12 @@ JS;
return true;
}
if( isset( $this->sidebar['widgets']['sidebar'] ) and count( $this->sidebar['widgets']['sidebar'] ) )
if( isset( $this->sidebar['widgetareas']['sidebar'] ) and !empty( $this->sidebar['widgetareas']['sidebar'] ) )
{
return true;
return (bool) $this->sidebar['widgetareas']['sidebar']->totalVisibleWidgets();
}
if( isset( $this->sidebar['widgetareas']['sidebar'] ) and !empty( $this->sidebar['widgetareas']['sidebar'] ) )
if( isset( $this->sidebar['widgets']['sidebar'] ) and count( $this->sidebar['widgets']['sidebar'] ) )
{
return true;
}
@@ -1945,6 +1953,49 @@ JS;
$this->jsonLd = array_merge( $this->jsonLd, $jsonLd );
}
/**
* Define the data attributes that will be applied to the body tag
*
* @return void
*/
public function setBodyAttributes() : void
{
if( !empty( $this->globalControllers ) )
{
$this->bodyAttributes['controller'] = implode( ",", $this->globalControllers );
}
if( $this->inlineMessage )
{
$this->bodyAttributes['message'] = $this->inlineMessage;
}
$this->bodyAttributes['pageApp'] = Dispatcher::i()->application->directory;
$this->bodyAttributes['pageLocation'] = 'front';
/* The module may not necessarily be set, in the event of an error (for example) */
if( isset( Dispatcher::i()->module ) )
{
$this->bodyAttributes['pageModule'] = Dispatcher::i()->module->key;
}
$this->bodyAttributes['pageController'] = Dispatcher::i()->controller;
if( isset( Request::i()->id ) )
{
$this->bodyAttributes['id'] = (int) Request::i()->id;
}
if( isset( Dispatcher::i()->dispatcherController ) and !Dispatcher::i()->dispatcherController->isContentPage )
{
$this->bodyAttributes['nocontent'] = '';
}
if( $this->pageName )
{
$this->bodyAttributes['pageName'] = $this->pageName;
}
}
/**
* @brief Global search menu options
*/
+5 -4
View File
@@ -12,11 +12,8 @@ namespace IPS\Output\Plugin;
/* To prevent PHP errors (extending class does not exist) revealing path */
use Exception;
use IPS\Application;
use IPS\Data\Cache;
use IPS\IPS;
use IPS\Member;
use IPS\Theme;
use function array_pop;
use function count;
@@ -69,7 +66,11 @@ class Resource
return '"../../applications/' . $app . '/interface/' . $data . '"';
}
return '"../resources/' . $app . '_' . $hash . '_' . $name . '"';
/* If we're building from install or upgrade, we need to use the relative paths otherwise the theme will use the building site's URL in the static files */
if ( $location !== 'admin' )
{
return '"../resources/' . $app . '_' . $hash . '_' . $name . '"';
}
}
return "\\IPS\\Theme::i()->resource( \"{$data}\", \"{$app}\", '{$location}', {$noProtocol} )";