Files
sbox-public/engine/Sandbox.Engine/Utility/WebSurface.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

514 lines
12 KiB
C#

using Sandbox.Utility;
using Steamworks;
using Steamworks.Data;
namespace Sandbox;
/// <summary>
/// Enables rendering and interacting with a webpage
/// </summary>
public sealed class WebSurface : IDisposable
{
static ISteamHTMLSurface steamHTMLSurface;
static Dictionary<uint, WeakReference<WebSurface>> All;
Task<HTML_BrowserReady_t> initTask;
Action _initQueue;
uint BrowserId;
public bool IsLimited { get; private set; }
static WebSurface()
{
if ( Application.IsUnitTest || Application.IsHeadless )
return;
All = new Dictionary<uint, WeakReference<WebSurface>>();
steamHTMLSurface = NativeEngine.Steam.SteamHTMLSurface();
if ( !steamHTMLSurface.IsValid )
{
Log.Warning( "WebSurface unavailable - SteamHTMLSurface is null" );
return;
}
steamHTMLSurface.Init();
Dispatch.Install<HTML_NeedsPaint_t>( x => GetBrowser( x.UnBrowserHandle )?.OnNeedsRepaint( x ) );
Dispatch.Install<HTML_StartRequest_t>( x => GetBrowser( x.UnBrowserHandle )?.OnStartRequest( x ) );
Dispatch.Install<HTML_URLChanged_t>( x => GetBrowser( x.UnBrowserHandle )?.OnURLChanged( x ) );
Dispatch.Install<HTML_ChangedTitle_t>( x => GetBrowser( x.UnBrowserHandle )?.OnChangedTitle( x ) );
Dispatch.Install<HTML_FinishedRequest_t>( x => GetBrowser( x.UnBrowserHandle )?.OnFinishedRequest( x ) );
Dispatch.Install<HTML_SetCursor_t>( x => GetBrowser( x.UnBrowserHandle )?.OnSetCursor( x ) );
}
static WebSurface GetBrowser( uint handle )
{
lock ( All )
{
if ( All.TryGetValue( handle, out var v ) && v.TryGetTarget( out var target ) )
{
return target;
}
return null;
}
}
public delegate void TextureChangedDelegate( ReadOnlySpan<byte> span, Vector2 size );
/// <summary>
/// Called when the texture has changed and should be updated
/// </summary>
public TextureChangedDelegate OnTexture { get; set; }
internal WebSurface( bool isLimited )
{
IsLimited = isLimited;
_ = CreateAsync();
}
async Task<HTML_BrowserReady_t> CreateBrowser()
{
if ( !steamHTMLSurface.IsValid )
return default;
var callback = steamHTMLSurface.CreateBrowser( "Mozilla/5.0 (Linux; U; X11; en-US; Facepunch Sandbox; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36", null );
var t = await new CallResult<HTML_BrowserReady_t>( callback, false );
return t ?? default;
}
async Task CreateAsync()
{
initTask = CreateBrowser();
var s = await initTask;
if ( initTask == null )
return;
BrowserId = s.UnBrowserHandle;
initTask = null;
lock ( All )
{
All[BrowserId] = new WeakReference<WebSurface>( this );
}
_initQueue?.Invoke();
_initQueue = null;
}
~WebSurface()
{
Dispose();
}
public void Dispose()
{
if ( BrowserId == 0 )
return;
lock ( All )
{
All.Remove( BrowserId );
steamHTMLSurface.RemoveBrowser( BrowserId );
BrowserId = 0;
foreach ( var expired in All.Where( x => x.Value.TryGetTarget( out var target ) == false ).Select( x => x.Key ).ToArray() )
{
All.Remove( expired );
}
}
}
string currentUrl;
public string PageTitle { get; private set; }
/// <summary>
/// The current Url
/// </summary>
public string Url
{
get => currentUrl;
set
{
if ( BrowserId == 0 )
{
_initQueue += () => Url = value;
return;
}
steamHTMLSurface.LoadURL( BrowserId, value ?? "", null );
}
}
/*
* None of this works, in my experience
*
internal void SetCookie( string hostname, string key, string value, string path = "/", bool insecure = false )
{
var expires = (uint)DateTime.UtcNow.AddDays( 7 ).GetEpoch();
if ( insecure )
{
steamHTMLSurface.SetCookie( hostname, key, value, path, expires, false, true );
}
else
{
steamHTMLSurface.SetCookie( hostname, key, value, path, expires, true, true );
}
}
*/
static string[] blacklistHostname = new[]
{
"*steampowered.com",
"*steamchina.com",
"*steamcommunity.com",
"*steamgames.com",
"*steamusercontent.com",
"*steamcontent.com",
"*steamstatic.com",
};
/// <summary>
/// Is this URL allowed
/// </summary>
internal static void CheckUrlIsAllowed( Uri uri )
{
if ( uri.Scheme != "http" && uri.Scheme != "https" )
throw new InvalidOperationException( $"The url scheme '{uri.Scheme}' is not allowed" );
if ( uri.IsUnc )
throw new InvalidOperationException( $"UNC urls are not allowed" );
if ( uri.IsFile )
throw new InvalidOperationException( $"File urls are not allowed" );
if ( uri.HostNameType == UriHostNameType.IPv4 || uri.HostNameType == UriHostNameType.IPv6 )
throw new InvalidOperationException( $"IP urls are not allowed" );
var host = uri.Host;
if ( blacklistHostname.Any( x => host.WildcardMatch( x ) ) )
throw new InvalidOperationException( $"You cannot access this hostname" );
// Users can opt for local http in editor, don't allow in non-editor please.
// Opt in is so they know they're doing something that won't work normally
if ( Application.IsEditor && CommandLine.HasSwitch( "-allowlocalhttp" ) )
return;
if ( uri.IsLoopback )
throw new InvalidOperationException( $"Loopback urls are not allowed" );
try
{
if ( uri.IsPrivate() )
throw new InvalidOperationException( $"Urls that resolve to private IPs are not allowed" );
}
catch ( System.Net.Sockets.SocketException e )
{
throw new InvalidOperationException( $"Socket error: {e.Message}" );
}
}
Vector2 _size;
/// <summary>
/// The size of the browser
/// </summary>
public Vector2 Size
{
get => _size;
set
{
if ( BrowserId == 0 )
{
_initQueue += () => Size = value;
return;
}
_size = value;
steamHTMLSurface.SetSize( BrowserId, (uint)value.x, (uint)value.y );
}
}
/// <summary>
/// Invoked when the browser needs to be repainted
/// </summary>
private unsafe void OnNeedsRepaint( HTML_NeedsPaint_t x )
{
ReadOnlySpan<byte> s = new ReadOnlySpan<byte>( (void*)x.PBGRA, (int)(x.UnWide * x.UnTall * 4) );
var size = new Vector2( x.UnWide, x.UnTall );
_size = size;
OnTexture?.Invoke( s, size );
}
private void OnChangedTitle( HTML_ChangedTitle_t x )
{
PageTitle = x.PchTitle;
}
/// <summary>
/// A navigation has happened, allow or deny it
/// </summary>
/// <param name="r"></param>
private unsafe void OnStartRequest( HTML_StartRequest_t r )
{
var url = r.PchURL;
try
{
if ( IsLimited )
CheckUrlIsAllowed( new Uri( url ) );
}
catch ( InvalidOperationException e )
{
Log.Warning( $"{e.Message}" );
steamHTMLSurface.AllowStartRequest( BrowserId, false );
return;
}
steamHTMLSurface.AllowStartRequest( BrowserId, true );
currentUrl = url;
}
/// <summary>
/// Invoked when the browser is navigating to a new url
/// </summary>
/// <param name="x"></param>
private unsafe void OnURLChanged( HTML_URLChanged_t x )
{
currentUrl = x.PchURL;
}
private void OnFinishedRequest( HTML_FinishedRequest_t x )
{
currentUrl = x.PchURL;
PageTitle = x.PchPageTitle;
}
public string Cursor { get; private set; }
enum EMouseCursor
{
dc_user = 0,
dc_none,
dc_arrow,
dc_ibeam,
dc_hourglass,
dc_waitarrow,
dc_crosshair,
dc_up,
dc_sizenw,
dc_sizese,
dc_sizene,
dc_sizesw,
dc_sizew,
dc_sizee,
dc_sizen,
dc_sizes,
dc_sizewe,
dc_sizens,
dc_sizeall,
dc_no,
dc_hand,
dc_blank, // don't show any custom cursor, just use your default
dc_middle_pan,
dc_north_pan,
dc_north_east_pan,
dc_east_pan,
dc_south_east_pan,
dc_south_pan,
dc_south_west_pan,
dc_west_pan,
dc_north_west_pan,
dc_alias,
dc_cell,
dc_colresize,
dc_copycur,
dc_verticaltext,
dc_rowresize,
dc_zoomin,
dc_zoomout,
dc_help,
dc_custom,
dc_last, // custom cursors start from this value and up
}
private void OnSetCursor( HTML_SetCursor_t x )
{
Cursor = "default";
if ( x.EMouseCursor == (int)EMouseCursor.dc_hand )
Cursor = "pointer";
if ( x.EMouseCursor == (int)EMouseCursor.dc_ibeam )
Cursor = "text";
}
/// <summary>
/// Tell the browser the mouse has moved
/// </summary>
public void TellMouseMove( Vector2 position )
{
steamHTMLSurface.MouseMove( BrowserId, (int)position.x, (int)position.y );
}
/// <summary>
/// Tell the browser the mouse wheel has moved
/// </summary>
/// <param name="delta"></param>
public void TellMouseWheel( int delta )
{
steamHTMLSurface.MouseWheel( BrowserId, delta );
}
/// <summary>
/// Tell the browser a mouse button has been pressed
/// </summary>
public void TellMouseButton( MouseButtons button, bool state )
{
if ( (button & MouseButtons.Left) != 0 )
{
if ( state ) steamHTMLSurface.MouseDown( BrowserId, 0 );
else steamHTMLSurface.MouseUp( BrowserId, 0 );
}
if ( (button & MouseButtons.Right) != 0 )
{
if ( state ) steamHTMLSurface.MouseDown( BrowserId, 1 );
else steamHTMLSurface.MouseUp( BrowserId, 1 );
}
if ( (button & MouseButtons.Middle) != 0 )
{
if ( state ) steamHTMLSurface.MouseDown( BrowserId, 2 );
else steamHTMLSurface.MouseUp( BrowserId, 2 );
}
}
/// <summary>
/// Tell the browser a unicode key has been pressed
/// </summary>
public void TellChar( uint unicodeKey, KeyboardModifiers modifiers )
{
// don't allow paste
if ( IsLimited && modifiers.Contains( KeyboardModifiers.Ctrl ) )
{
if ( unicodeKey == 'x' ) return;
if ( unicodeKey == 'c' ) return;
if ( unicodeKey == 'v' ) return;
}
steamHTMLSurface.KeyChar( BrowserId, unicodeKey, (int)modifiers );
}
/// <summary>
/// Tell the browser a key has been pressed or released
/// </summary>
public void TellKey( uint virtualKeyCode, KeyboardModifiers modifiers, bool state )
{
//
// Don't allow paste. Since the potential is there to open a webpage and simulate
// key presses to past the contents of the clipboard into a hidden textarea and
// record that shit on a server somewhere. They could do this without the game having
// focus - so anything happening in the background could get captured.
//
// There is a strategy to allow this, by having Copy, Paste, Cut functions that take some
// special token class that only we can make, that come as part of the UI panel input systems.
// but we'll leave that implementation until someone notices and cares.
//
if ( IsLimited && modifiers.Contains( KeyboardModifiers.Ctrl ) )
{
if ( virtualKeyCode == 86 ) return;
if ( virtualKeyCode == 67 ) return;
if ( virtualKeyCode == 88 ) return;
}
if ( state ) steamHTMLSurface.KeyDown( BrowserId, virtualKeyCode, (int)modifiers, false );
else steamHTMLSurface.KeyUp( BrowserId, virtualKeyCode, (int)modifiers );
}
bool _keyFocus;
/// <summary>
/// Tell the html control if it has key focus currently, controls showing the I-beam cursor in text controls amongst other things
/// </summary>
public bool HasKeyFocus
{
get => _keyFocus;
set
{
if ( BrowserId == 0 )
{
_initQueue += () => HasKeyFocus = value;
return;
}
_keyFocus = value;
steamHTMLSurface.SetKeyFocus( BrowserId, value );
}
}
float _scaleFactor = 1.0f;
/// <summary>
/// DPI Scaling factor
/// </summary>
public float ScaleFactor
{
get => _scaleFactor;
set
{
if ( BrowserId == 0 )
{
_initQueue += () => ScaleFactor = value;
return;
}
_scaleFactor = value;
steamHTMLSurface.SetDPIScalingFactor( BrowserId, value );
}
}
bool _backgroudMode;
/// <summary>
/// Enable/disable low-resource background mode, where javascript and repaint timers are throttled, resources are
/// more aggressively purged from memory, and audio/video elements are paused. When background mode is enabled,
/// all HTML5 video and audio objects will execute ".pause()" and gain the property "._steam_background_paused = 1".
/// When background mode is disabled, any video or audio objects with that property will resume with ".play()".
/// </summary>
public bool InBackgroundMode
{
get => _backgroudMode;
set
{
if ( BrowserId == 0 )
{
_initQueue += () => InBackgroundMode = value;
return;
}
_backgroudMode = value;
steamHTMLSurface.SetBackgroundMode( BrowserId, value );
}
}
}