diff --git a/src/Peachpie.Library.MySql/MySqlConnectionManager.cs b/src/Peachpie.Library.MySql/MySqlConnectionManager.cs index db19ee14f3..3e5b56df43 100644 --- a/src/Peachpie.Library.MySql/MySqlConnectionManager.cs +++ b/src/Peachpie.Library.MySql/MySqlConnectionManager.cs @@ -29,5 +29,11 @@ internal MySqlConnectionResource CreateConnection(IDbConnection dbconnection) return connection; } + + public virtual void ReportException(Exception exception, string exceptionMessage) + { + // MySql outputs the error to php error handler + PhpException.Throw(PhpError.Warning, exceptionMessage); + } } } diff --git a/src/Peachpie.Library.MySql/MySqlConnectionResource.cs b/src/Peachpie.Library.MySql/MySqlConnectionResource.cs index 429890d7d6..8eb03d9359 100644 --- a/src/Peachpie.Library.MySql/MySqlConnectionResource.cs +++ b/src/Peachpie.Library.MySql/MySqlConnectionResource.cs @@ -168,5 +168,26 @@ internal long LastInsertedId return command != null ? MySqlExtensions.LastInsertedId(command) : -1; } } + + public override int GetLastErrorNumber() + { + if (LastException == null) + { + return (int)MySqlErrorCode.None; // success + } + else if (LastException is MySqlException me) + { + return (int)me.ErrorCode; + } + else + { + return (int)MySqlErrorCode.UnknownError; // unk erro number + } + } + + protected override void ReportException(Exception exception, string exceptionMessage) + { + _manager.ReportException(exception, exceptionMessage); + } } } diff --git a/src/Peachpie.Library.MySql/MySqli/Constants.cs b/src/Peachpie.Library.MySql/MySqli/Constants.cs index c93e64e7bf..1f1295dcad 100644 --- a/src/Peachpie.Library.MySql/MySqli/Constants.cs +++ b/src/Peachpie.Library.MySql/MySqli/Constants.cs @@ -248,20 +248,30 @@ public static class Constants /// public const int MYSQLI_SET_CHARSET_NAME = 7; - //MYSQLI_REPORT_INDEX - //Report if no index or bad index was used in a query. + /// + /// Turns reporting off. + /// + public const int MYSQLI_REPORT_OFF = 0; - //MYSQLI_REPORT_ERROR - //Report errors from mysqli function calls. + /// + /// Report errors from mysqli function calls. + /// + public const int MYSQLI_REPORT_ERROR = 1; - //MYSQLI_REPORT_STRICT - //Throw a mysqli_sql_exception for errors instead of warnings. + /// + /// Throw a for errors instead of warnings. + /// + public const int MYSQLI_REPORT_STRICT = 2; - //MYSQLI_REPORT_ALL - //Set all options on (report all). + /// + /// Report if no index or bad index was used in a query. + /// + public const int MYSQLI_REPORT_INDEX = 4; - //MYSQLI_REPORT_OFF - //Turns reporting off. + /// + /// Set all options on (report all). + /// + public const int MYSQLI_REPORT_ALL = 0xff; //MYSQLI_DEBUG_TRACE_ENABLED //Is set to 1 if mysqli_debug() functionality is enabled. diff --git a/src/Peachpie.Library.MySql/MySqli/Functions.cs b/src/Peachpie.Library.MySql/MySqli/Functions.cs index 9c5527430f..06d0de6b5e 100644 --- a/src/Peachpie.Library.MySql/MySqli/Functions.cs +++ b/src/Peachpie.Library.MySql/MySqli/Functions.cs @@ -12,16 +12,6 @@ namespace Peachpie.Library.MySql.MySqli [PhpExtension(Constants.ExtensionName)] public static class Functions { - /// - /// Internal object used to store per-request (context) data. - /// - internal class MySqliContextData - { - public static MySqliContextData/*!*/GetContextData(Context ctx) => ctx.GetStatic(); - - public string LastConnectionError { get; set; } - } - /// /// Initializes MySQLi and returns a resource for use with mysqli_real_connect(). /// @@ -96,7 +86,7 @@ public static mysqli mysqli_connect(Context ctx, IDbConnection dbconnection/*, b /// The connection error message. Otherwise null. /// public static string mysqli_connect_error(Context ctx, mysqli link = null) - => (link != null) ? link.connect_error : MySqliContextData.GetContextData(ctx).LastConnectionError; + => (link != null) ? link.connect_error : MySqliConnectionManager.GetInstance(ctx).LastConnectionError; /// /// Returns the error code from last connect call. @@ -104,7 +94,7 @@ public static string mysqli_connect_error(Context ctx, mysqli link = null) public static int mysqli_connect_errno(Context ctx, mysqli link = null) => (link != null) ? link.connect_errno - : string.IsNullOrEmpty(MySqliContextData.GetContextData(ctx).LastConnectionError) ? 0 : -1; + : string.IsNullOrEmpty(MySqliConnectionManager.GetInstance(ctx).LastConnectionError) ? 0 : -1; /// /// Returns the error code for the most recent function call. @@ -369,10 +359,13 @@ public static bool mysqli_ssl_set(mysqli link, string key = null, string cert = /// /// Sets mysqli error reporting mode. /// - public static bool mysqli_report(int flags) + public static bool mysqli_report(Context ctx, ReportMode flags) { - PhpException.FunctionNotSupported(nameof(mysqli_report)); - return false; + var manager = MySqliConnectionManager.GetInstance(ctx); + + manager.ReportMode = flags; + + return true; } } } diff --git a/src/Peachpie.Library.MySql/MySqli/MySqliConnectionManager.cs b/src/Peachpie.Library.MySql/MySqli/MySqliConnectionManager.cs new file mode 100644 index 0000000000..d71f9b332b --- /dev/null +++ b/src/Peachpie.Library.MySql/MySqli/MySqliConnectionManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MySqlConnector; +using Pchp.Core; + +namespace Peachpie.Library.MySql.MySqli +{ + class MySqliConnectionManager : MySqlConnectionManager + { + /// + /// Gets the manager singleton within runtime context. + /// + public static new MySqliConnectionManager GetInstance(Context ctx) => ctx.GetStatic(); + + public string LastConnectionError { get; set; } + + /// + /// The error handling mode. + /// + public ReportMode ReportMode { get; set; } = ReportMode.Default; + + public override void ReportException(Exception exception, string exceptionMessage) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + + if ((ReportMode & ReportMode.Error) != 0) + { + if ((ReportMode & ReportMode.Strict) != 0) + { + var errcode = exception is MySqlException me ? me.ErrorCode : MySqlErrorCode.UnknownError; + + throw new mysqli_sql_exception(exceptionMessage, (int)errcode); + } + else + { + // outputs the error to php error handler + PhpException.Throw(PhpError.Warning, exceptionMessage); + } + } + } + } + + /// + /// MYSQLI_REPORT_*** constants. + /// + [PhpHidden, Flags] + public enum ReportMode + { + /// + Off = Constants.MYSQLI_REPORT_OFF, + /// + Error = Constants.MYSQLI_REPORT_ERROR, + /// + Strict = Constants.MYSQLI_REPORT_STRICT, + /// + Index = Constants.MYSQLI_REPORT_INDEX, + /// + All = Constants.MYSQLI_REPORT_ALL, + + /// Default error handling flags. + Default = Error | Strict, // as it is in PHP 8.1 + } +} diff --git a/src/Peachpie.Library.MySql/MySqli/mysqli.cs b/src/Peachpie.Library.MySql/MySqli/mysqli.cs index a8ac5eed00..4adad1640b 100644 --- a/src/Peachpie.Library.MySql/MySqli/mysqli.cs +++ b/src/Peachpie.Library.MySql/MySqli/mysqli.cs @@ -52,7 +52,7 @@ internal mysqli(Context ctx, IDbConnection dbconnection) { // create connection resource and // register it in the list of active connections - this.Connection = MySqlConnectionManager + this.Connection = MySqliConnectionManager .GetInstance(ctx) .CreateConnection(dbconnection); } @@ -306,6 +306,7 @@ public PhpValue query(PhpString query, int resultmode = Constants.MYSQLI_STORE_R public bool real_connect(Context ctx, string host = null, string username = null, string passwd = null, string dbname = "", int port = -1, string socket = null, int flags = 0) { var config = ctx.Configuration.Get(); + var manager = MySqliConnectionManager.GetInstance(ctx); // string $host = ini_get("mysqli.default_host") // string $username = ini_get("mysqli.default_user") @@ -346,12 +347,12 @@ public bool real_connect(Context ctx, string host = null, string username = null } } - Connection = MySqlConnectionManager.GetInstance(ctx) - .CreateConnection(connection_string.ToString(), true, -1, out bool success); + Connection = manager.CreateConnection(connection_string.ToString(), true, -1, out bool success); if (success) { Connection.Server = host; + manager.LastConnectionError = null; if (!string.IsNullOrEmpty(dbname)) { @@ -360,9 +361,7 @@ public bool real_connect(Context ctx, string host = null, string username = null } else { - MySqliContextData.GetContextData(ctx).LastConnectionError - = connect_error - = Connection.GetLastErrorMessage(); + manager.LastConnectionError = connect_error = Connection.GetLastErrorMessage(); } // diff --git a/src/Peachpie.Library/Database/ConnectionManager.cs b/src/Peachpie.Library/Database/ConnectionManager.cs index a7b6e3ae4f..7dceae8a58 100644 --- a/src/Peachpie.Library/Database/ConnectionManager.cs +++ b/src/Peachpie.Library/Database/ConnectionManager.cs @@ -16,8 +16,7 @@ public abstract class ConnectionManager : IStaticInit where TConnec /// /// Associated runtime context. /// - public Context Context => _ctx; - Context _ctx; + public Context Context { get; private set; } /// /// List of connections established by the manager. @@ -69,7 +68,7 @@ public TConnection CreateConnection(string connectionString, bool newConnection, if ((success = connection.Connect()) == true) { AddConnection(connection); - _ctx.RegisterDisposable(connection); + this.Context.RegisterDisposable(connection); } return connection; @@ -108,7 +107,7 @@ protected void AddConnection(TConnection connection) public bool RemoveConnection(TConnection connection) { // - _ctx.UnregisterDisposable(connection); + this.Context.UnregisterDisposable(connection); // if (_connections.Count != 0) @@ -132,7 +131,7 @@ public TConnection GetLastConnection() void IStaticInit.Init(Context ctx) { - _ctx = ctx; + this.Context = ctx; } } } diff --git a/src/Peachpie.Library/Database/ConnectionResource.cs b/src/Peachpie.Library/Database/ConnectionResource.cs index 5a17c9a365..cbbcc055f3 100644 --- a/src/Peachpie.Library/Database/ConnectionResource.cs +++ b/src/Peachpie.Library/Database/ConnectionResource.cs @@ -29,15 +29,38 @@ public abstract class ConnectionResource : PhpResource /// /// Last result resource. /// - public ResultResource LastResult => _lastResult; - private ResultResource _lastResult; + public ResultResource LastResult { get; private set; } + + /// + /// Handle exception thrown by the last DB operation. + /// + /// Thrown exception + /// Optional formatted message to be reported. + private void OnException(Exception exception, string exceptionMessage) + { + LastException = exception; + + ReportException(exception, exceptionMessage ?? exception.Message); + } + + protected virtual void ReportException(Exception exception, string exceptionMessage) + { + PhpException.Throw(PhpError.Warning, exceptionMessage); + } + + /// + /// Handle success by the last DB operation. + /// + protected virtual void OnSuccess() + { + LastException = null; + } /// /// Gets an exception thrown by last performed operation or a null reference /// if that operation succeeded. /// - public Exception LastException => _lastException; - protected Exception _lastException; + public Exception LastException { get; protected set; } /// /// Gets the number of rows affected by the last query executed on this connection. @@ -46,10 +69,10 @@ public int LastAffectedRows { get { - if (_lastResult == null) return -1; + if (LastResult == null) return -1; // SELECT gives -1, UPDATE/INSERT gives the number: - return (_lastResult.RecordsAffected >= 0) ? _lastResult.RecordsAffected : _lastResult.RowCount; + return (LastResult.RecordsAffected >= 0) ? LastResult.RecordsAffected : LastResult.RowCount; } } @@ -85,12 +108,12 @@ public virtual bool Connect() try { ActiveConnection.Open(); // TODO: Async - _lastException = null; + + OnSuccess(); } catch (Exception e) { - _lastException = e; - PhpException.Throw(PhpError.Warning, LibResources.cannot_open_connection, GetExceptionMessage(e)); + OnException(e, string.Format(LibResources.cannot_open_connection, GetExceptionMessage(e))); return false; } @@ -112,12 +135,11 @@ protected override void FreeManaged() connection.Close(); } - _lastException = null; + OnSuccess(); } catch (Exception e) { - _lastException = e; - PhpException.Throw(PhpError.Warning, LibResources.error_closing_connection, GetExceptionMessage(e)); + OnException(e, string.Format(LibResources.error_closing_connection, GetExceptionMessage(e))); } } @@ -241,7 +263,7 @@ protected virtual ResultResource ExecuteCommandProtected(IDbCommand command, boo { command.Parameters.Clear(); - for (int iparam = 0; iparam < parameters.Count; iparam ++) + for (int iparam = 0; iparam < parameters.Count; iparam++) { command.Parameters.Add(parameters[iparam]); } @@ -261,21 +283,20 @@ protected virtual ResultResource ExecuteCommandProtected(IDbCommand command, boo } else { - _lastResult = null; + LastResult = null; // read all data into PhpDbResult: result = GetResult(reader, convertTypes); result.command = command; - _lastResult = result; + LastResult = result; } - _lastException = null; + OnSuccess(); } catch (Exception e) { - _lastException = e; - PhpException.Throw(PhpError.Warning, LibResources.command_execution_failed, GetExceptionMessage(e)); + OnException(e, string.Format(LibResources.command_execution_failed, GetExceptionMessage(e))); } // @@ -295,11 +316,12 @@ internal void ReexecuteSchemaQuery(ResultResource/*!*/ result) try { result.Reader = _pendingReader = result.Command.ExecuteReader(CommandBehavior.KeyInfo | CommandBehavior.SchemaOnly); + + OnSuccess(); } catch (Exception e) { - _lastException = e; - PhpException.Throw(PhpError.Warning, LibResources.command_execution_failed, GetExceptionMessage(e)); + OnException(e, string.Format(LibResources.command_execution_failed, GetExceptionMessage(e))); } } @@ -318,14 +340,13 @@ public bool SelectDb(string databaseName) if (connection.State == ConnectionState.Open) { connection.ChangeDatabase(databaseName); - _lastException = null; + OnSuccess(); return true; } } catch (Exception e) { - _lastException = e; - PhpException.Throw(PhpError.Warning, LibResources.database_selection_failed, GetExceptionMessage(e)); + OnException(e, string.Format(LibResources.database_selection_failed, GetExceptionMessage(e))); } return false; @@ -338,9 +359,11 @@ public bool SelectDb(string databaseName) /// Exception. /// The message. /// is a null reference. - public virtual string GetExceptionMessage(System.Exception/*!*/ e) + public virtual string GetExceptionMessage(Exception/*!*/ e) { - if (e == null) throw new ArgumentNullException(nameof(e)); + if (e == null) + throw new ArgumentNullException(nameof(e)); + return PhpException.ToErrorMessage(e.Message); } diff --git a/src/Peachpie.Runtime/Errors.cs b/src/Peachpie.Runtime/Errors.cs index ce4c3e6eb1..acb90095f6 100644 --- a/src/Peachpie.Runtime/Errors.cs +++ b/src/Peachpie.Runtime/Errors.cs @@ -494,8 +494,10 @@ public static void ObjectConvertError(string classname, string targettype) /// is a null reference. public static string ToErrorMessage(string exceptionMessage) { - if (exceptionMessage == null) throw new ArgumentNullException("exceptionMessage"); - return exceptionMessage.TrimEnd(new char[] { '.' }); + if (exceptionMessage == null) + throw new ArgumentNullException(nameof(exceptionMessage)); + + return exceptionMessage.AsSpan().TrimEnd('.').ToString(); } internal static void ThrowSelfOutOfClass()