/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include "nsError.h" #include "nsMemory.h" #include "nsThreadUtils.h" #include "nsIClassInfoImpl.h" #include "Variant.h" #include "mozIStorageError.h" #include "mozStorageBindingParams.h" #include "mozStorageConnection.h" #include "mozStorageStatementJSHelper.h" #include "mozStoragePrivateHelpers.h" #include "mozStorageStatementParams.h" #include "mozStorageStatementRow.h" #include "mozStorageStatement.h" #include "GeckoProfiler.h" #include "mozilla/Logging.h" #include "mozilla/Printf.h" extern mozilla::LazyLogModule gStorageLog; namespace mozilla { namespace storage { //////////////////////////////////////////////////////////////////////////////// //// nsIClassInfo NS_IMPL_CI_INTERFACE_GETTER(Statement, mozIStorageStatement, mozIStorageBaseStatement, mozIStorageBindingParams, mozIStorageValueArray, mozilla::storage::StorageBaseStatementInternal) class StatementClassInfo : public nsIClassInfo { public: constexpr StatementClassInfo() {} NS_DECL_ISUPPORTS_INHERITED NS_IMETHOD GetInterfaces(nsTArray &_array) override { return NS_CI_INTERFACE_GETTER_NAME(Statement)(_array); } NS_IMETHOD GetScriptableHelper(nsIXPCScriptable **_helper) override { static StatementJSHelper sJSHelper; *_helper = &sJSHelper; return NS_OK; } NS_IMETHOD GetContractID(nsACString &aContractID) override { aContractID.SetIsVoid(true); return NS_OK; } NS_IMETHOD GetClassDescription(nsACString &aDesc) override { aDesc.SetIsVoid(true); return NS_OK; } NS_IMETHOD GetClassID(nsCID **_id) override { *_id = nullptr; return NS_OK; } NS_IMETHOD GetFlags(uint32_t *_flags) override { *_flags = 0; return NS_OK; } NS_IMETHOD GetClassIDNoAlloc(nsCID *_cid) override { return NS_ERROR_NOT_AVAILABLE; } }; NS_IMETHODIMP_(MozExternalRefCountType) StatementClassInfo::AddRef() { return 2; } NS_IMETHODIMP_(MozExternalRefCountType) StatementClassInfo::Release() { return 1; } NS_IMPL_QUERY_INTERFACE(StatementClassInfo, nsIClassInfo) static StatementClassInfo sStatementClassInfo; //////////////////////////////////////////////////////////////////////////////// //// Statement Statement::Statement() : StorageBaseStatementInternal(), mDBStatement(nullptr), mParamCount(0), mResultColumnCount(0), mColumnNames(), mExecuting(false) {} nsresult Statement::initialize(Connection *aDBConnection, sqlite3 *aNativeConnection, const nsACString &aSQLStatement) { MOZ_ASSERT(aDBConnection, "No database connection given!"); MOZ_ASSERT(aDBConnection->isConnectionReadyOnThisThread(), "Database connection should be valid"); MOZ_ASSERT(!mDBStatement, "Statement already initialized!"); MOZ_ASSERT(aNativeConnection, "No native connection given!"); int srv = aDBConnection->prepareStatement( aNativeConnection, PromiseFlatCString(aSQLStatement), &mDBStatement); if (srv != SQLITE_OK) { MOZ_LOG(gStorageLog, LogLevel::Error, ("Sqlite statement prepare error: %d '%s'", srv, ::sqlite3_errmsg(aNativeConnection))); MOZ_LOG(gStorageLog, LogLevel::Error, ("Statement was: '%s'", PromiseFlatCString(aSQLStatement).get())); return NS_ERROR_FAILURE; } MOZ_LOG(gStorageLog, LogLevel::Debug, ("Initialized statement '%s' (0x%p)", PromiseFlatCString(aSQLStatement).get(), mDBStatement)); mDBConnection = aDBConnection; mNativeConnection = aNativeConnection; mParamCount = ::sqlite3_bind_parameter_count(mDBStatement); mResultColumnCount = ::sqlite3_column_count(mDBStatement); mColumnNames.Clear(); nsCString *columnNames = mColumnNames.AppendElements(mResultColumnCount); for (uint32_t i = 0; i < mResultColumnCount; i++) { const char *name = ::sqlite3_column_name(mDBStatement, i); columnNames[i].Assign(name); } #ifdef DEBUG // We want to try and test for LIKE and that consumers are using // escapeStringForLIKE instead of just trusting user input. The idea to // check to see if they are binding a parameter after like instead of just // using a string. We only do this in debug builds because it's expensive! const nsCaseInsensitiveCStringComparator c; nsACString::const_iterator start, end, e; aSQLStatement.BeginReading(start); aSQLStatement.EndReading(end); e = end; while (::FindInReadable(NS_LITERAL_CSTRING(" LIKE"), start, e, c)) { // We have a LIKE in here, so we perform our tests // FindInReadable moves the iterator, so we have to get a new one for // each test we perform. nsACString::const_iterator s1, s2, s3; s1 = s2 = s3 = start; if (!(::FindInReadable(NS_LITERAL_CSTRING(" LIKE ?"), s1, end, c) || ::FindInReadable(NS_LITERAL_CSTRING(" LIKE :"), s2, end, c) || ::FindInReadable(NS_LITERAL_CSTRING(" LIKE @"), s3, end, c))) { // At this point, we didn't find a LIKE statement followed by ?, :, // or @, all of which are valid characters for binding a parameter. // We will warn the consumer that they may not be safely using LIKE. NS_WARNING( "Unsafe use of LIKE detected! Please ensure that you " "are using mozIStorageStatement::escapeStringForLIKE " "and that you are binding that result to the statement " "to prevent SQL injection attacks."); } // resetting start and e start = e; e = end; } #endif return NS_OK; } mozIStorageBindingParams *Statement::getParams() { nsresult rv; // If we do not have an array object yet, make it. if (!mParamsArray) { nsCOMPtr array; rv = NewBindingParamsArray(getter_AddRefs(array)); NS_ENSURE_SUCCESS(rv, nullptr); mParamsArray = static_cast(array.get()); } // If there isn't already any rows added, we'll have to add one to use. if (mParamsArray->length() == 0) { RefPtr params(new BindingParams(mParamsArray, this)); NS_ENSURE_TRUE(params, nullptr); rv = mParamsArray->AddParams(params); NS_ENSURE_SUCCESS(rv, nullptr); // We have to unlock our params because AddParams locks them. This is safe // because no reference to the params object was, or ever will be given out. params->unlock(this); // We also want to lock our array at this point - we don't want anything to // be added to it. Nothing has, or will ever get a reference to it, but we // will get additional safety checks via assertions by doing this. mParamsArray->lock(); } return *mParamsArray->begin(); } Statement::~Statement() { (void)internalFinalize(true); } //////////////////////////////////////////////////////////////////////////////// //// nsISupports NS_IMPL_ADDREF(Statement) NS_IMPL_RELEASE(Statement) NS_INTERFACE_MAP_BEGIN(Statement) NS_INTERFACE_MAP_ENTRY(mozIStorageStatement) NS_INTERFACE_MAP_ENTRY(mozIStorageBaseStatement) NS_INTERFACE_MAP_ENTRY(mozIStorageBindingParams) NS_INTERFACE_MAP_ENTRY(mozIStorageValueArray) NS_INTERFACE_MAP_ENTRY(mozilla::storage::StorageBaseStatementInternal) if (aIID.Equals(NS_GET_IID(nsIClassInfo))) { foundInterface = static_cast(&sStatementClassInfo); } else NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIStorageStatement) NS_INTERFACE_MAP_END //////////////////////////////////////////////////////////////////////////////// //// StorageBaseStatementInternal Connection *Statement::getOwner() { return mDBConnection; } int Statement::getAsyncStatement(sqlite3_stmt **_stmt) { // If we have no statement, we shouldn't be calling this method! NS_ASSERTION(mDBStatement != nullptr, "We have no statement to clone!"); // If we do not yet have a cached async statement, clone our statement now. if (!mAsyncStatement) { nsDependentCString sql(::sqlite3_sql(mDBStatement)); int rc = mDBConnection->prepareStatement(mNativeConnection, sql, &mAsyncStatement); if (rc != SQLITE_OK) { *_stmt = nullptr; return rc; } MOZ_LOG(gStorageLog, LogLevel::Debug, ("Cloned statement 0x%p to 0x%p", mDBStatement, mAsyncStatement)); } *_stmt = mAsyncStatement; return SQLITE_OK; } nsresult Statement::getAsynchronousStatementData(StatementData &_data) { if (!mDBStatement) return NS_ERROR_UNEXPECTED; sqlite3_stmt *stmt; int rc = getAsyncStatement(&stmt); if (rc != SQLITE_OK) return convertResultCode(rc); _data = StatementData(stmt, bindingParamsArray(), this); return NS_OK; } already_AddRefed Statement::newBindingParams( mozIStorageBindingParamsArray *aOwner) { nsCOMPtr params = new BindingParams(aOwner, this); return params.forget(); } //////////////////////////////////////////////////////////////////////////////// //// mozIStorageStatement // proxy to StorageBaseStatementInternal using its define helper. MIXIN_IMPL_STORAGEBASESTATEMENTINTERNAL(Statement, (void)0;) NS_IMETHODIMP Statement::Clone(mozIStorageStatement **_statement) { RefPtr statement(new Statement()); NS_ENSURE_TRUE(statement, NS_ERROR_OUT_OF_MEMORY); nsAutoCString sql(::sqlite3_sql(mDBStatement)); nsresult rv = statement->initialize(mDBConnection, mNativeConnection, sql); NS_ENSURE_SUCCESS(rv, rv); statement.forget(_statement); return NS_OK; } NS_IMETHODIMP Statement::Finalize() { return internalFinalize(false); } nsresult Statement::internalFinalize(bool aDestructing) { if (!mDBStatement) return NS_OK; int srv = SQLITE_OK; { // If the statement ends up being finalized twice, the second finalization // would apply to a dangling pointer and may cause unexpected consequences. // Thus we must be sure that the connection state won't change during this // operation, to avoid racing with finalizations made by the closing // connection. See Connection::internalClose(). MutexAutoLock lockedScope(mDBConnection->sharedAsyncExecutionMutex); if (!mDBConnection->isClosed(lockedScope)) { MOZ_LOG(gStorageLog, LogLevel::Debug, ("Finalizing statement '%s' during garbage-collection", ::sqlite3_sql(mDBStatement))); srv = ::sqlite3_finalize(mDBStatement); } #ifdef DEBUG else { // The database connection is closed. The sqlite // statement has either been finalized already by the connection // or is about to be finalized by the connection. // // Finalizing it here would be useless and segfaultish. // // Note that we can't display the statement itself, as the data structure // is not valid anymore. However, the address shown here should help // developers correlate with the more complete debug message triggered // by AsyncClose(). SmprintfPointer msg = ::mozilla::Smprintf( "SQL statement (%p) should have been finalized" " before garbage-collection. For more details on this statement, set" " NSPR_LOG_MESSAGES=mozStorage:5 .", mDBStatement); NS_WARNING(msg.get()); // Use %s so we aren't exposing random strings to printf interpolation. MOZ_LOG(gStorageLog, LogLevel::Warning, ("%s", msg.get())); } #endif // DEBUG } mDBStatement = nullptr; if (mAsyncStatement) { // If the destructor called us, there are no pending async statements (they // hold a reference to us) and we can/must just kill the statement directly. if (aDestructing) destructorAsyncFinalize(); else asyncFinalize(); } // Release the holders, so they can release the reference to us. mStatementParamsHolder = nullptr; mStatementRowHolder = nullptr; return convertResultCode(srv); } NS_IMETHODIMP Statement::GetParameterCount(uint32_t *_parameterCount) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; *_parameterCount = mParamCount; return NS_OK; } NS_IMETHODIMP Statement::GetParameterName(uint32_t aParamIndex, nsACString &_name) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; ENSURE_INDEX_VALUE(aParamIndex, mParamCount); const char *name = ::sqlite3_bind_parameter_name(mDBStatement, aParamIndex + 1); if (name == nullptr) { // this thing had no name, so fake one nsAutoCString fakeName(":"); fakeName.AppendInt(aParamIndex); _name.Assign(fakeName); } else { _name.Assign(nsDependentCString(name)); } return NS_OK; } NS_IMETHODIMP Statement::GetParameterIndex(const nsACString &aName, uint32_t *_index) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; // We do not accept any forms of names other than ":name", but we need to add // the colon for SQLite. nsAutoCString name(":"); name.Append(aName); int ind = ::sqlite3_bind_parameter_index(mDBStatement, name.get()); if (ind == 0) // Named parameter not found. return NS_ERROR_INVALID_ARG; *_index = ind - 1; // SQLite indexes are 1-based, we are 0-based. return NS_OK; } NS_IMETHODIMP Statement::GetColumnCount(uint32_t *_columnCount) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; *_columnCount = mResultColumnCount; return NS_OK; } NS_IMETHODIMP Statement::GetColumnName(uint32_t aColumnIndex, nsACString &_name) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; ENSURE_INDEX_VALUE(aColumnIndex, mResultColumnCount); const char *cname = ::sqlite3_column_name(mDBStatement, aColumnIndex); _name.Assign(nsDependentCString(cname)); return NS_OK; } NS_IMETHODIMP Statement::GetColumnIndex(const nsACString &aName, uint32_t *_index) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; // Surprisingly enough, SQLite doesn't provide an API for this. We have to // determine it ourselves sadly. for (uint32_t i = 0; i < mResultColumnCount; i++) { if (mColumnNames[i].Equals(aName)) { *_index = i; return NS_OK; } } return NS_ERROR_INVALID_ARG; } NS_IMETHODIMP Statement::Reset() { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; #ifdef DEBUG MOZ_LOG(gStorageLog, LogLevel::Debug, ("Resetting statement: '%s'", ::sqlite3_sql(mDBStatement))); checkAndLogStatementPerformance(mDBStatement); #endif mParamsArray = nullptr; (void)sqlite3_reset(mDBStatement); (void)sqlite3_clear_bindings(mDBStatement); mExecuting = false; return NS_OK; } NS_IMETHODIMP Statement::BindParameters(mozIStorageBindingParamsArray *aParameters) { NS_ENSURE_ARG_POINTER(aParameters); if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; BindingParamsArray *array = static_cast(aParameters); if (array->getOwner() != this) return NS_ERROR_UNEXPECTED; if (array->length() == 0) return NS_ERROR_UNEXPECTED; mParamsArray = array; mParamsArray->lock(); return NS_OK; } NS_IMETHODIMP Statement::Execute() { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; bool ret; nsresult rv = ExecuteStep(&ret); nsresult rv2 = Reset(); return NS_FAILED(rv) ? rv : rv2; } NS_IMETHODIMP Statement::ExecuteStep(bool *_moreResults) { AUTO_PROFILER_LABEL("Statement::ExecuteStep", OTHER); if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; // Bind any parameters first before executing. if (mParamsArray) { // If we have more than one row of parameters to bind, they shouldn't be // calling this method (and instead use executeAsync). if (mParamsArray->length() != 1) return NS_ERROR_UNEXPECTED; BindingParamsArray::iterator row = mParamsArray->begin(); nsCOMPtr bindingInternal = do_QueryInterface(*row); nsCOMPtr error = bindingInternal->bind(mDBStatement); if (error) { int32_t srv; (void)error->GetResult(&srv); return convertResultCode(srv); } // We have bound, so now we can clear our array. mParamsArray = nullptr; } int srv = mDBConnection->stepStatement(mNativeConnection, mDBStatement); if (srv != SQLITE_ROW && srv != SQLITE_DONE && MOZ_LOG_TEST(gStorageLog, LogLevel::Debug)) { nsAutoCString errStr; (void)mDBConnection->GetLastErrorString(errStr); MOZ_LOG(gStorageLog, LogLevel::Debug, ("Statement::ExecuteStep error: %s", errStr.get())); } // SQLITE_ROW and SQLITE_DONE are non-errors if (srv == SQLITE_ROW) { // we got a row back mExecuting = true; *_moreResults = true; return NS_OK; } else if (srv == SQLITE_DONE) { // statement is done (no row returned) mExecuting = false; *_moreResults = false; return NS_OK; } else if (srv == SQLITE_BUSY || srv == SQLITE_MISUSE) { mExecuting = false; } else if (mExecuting) { MOZ_LOG(gStorageLog, LogLevel::Error, ("SQLite error after mExecuting was true!")); mExecuting = false; } return convertResultCode(srv); } NS_IMETHODIMP Statement::GetState(int32_t *_state) { if (!mDBStatement) *_state = MOZ_STORAGE_STATEMENT_INVALID; else if (mExecuting) *_state = MOZ_STORAGE_STATEMENT_EXECUTING; else *_state = MOZ_STORAGE_STATEMENT_READY; return NS_OK; } //////////////////////////////////////////////////////////////////////////////// //// mozIStorageValueArray (now part of mozIStorageStatement too) NS_IMETHODIMP Statement::GetNumEntries(uint32_t *_length) { *_length = mResultColumnCount; return NS_OK; } NS_IMETHODIMP Statement::GetTypeOfIndex(uint32_t aIndex, int32_t *_type) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); if (!mExecuting) return NS_ERROR_UNEXPECTED; int t = ::sqlite3_column_type(mDBStatement, aIndex); switch (t) { case SQLITE_INTEGER: *_type = mozIStorageStatement::VALUE_TYPE_INTEGER; break; case SQLITE_FLOAT: *_type = mozIStorageStatement::VALUE_TYPE_FLOAT; break; case SQLITE_TEXT: *_type = mozIStorageStatement::VALUE_TYPE_TEXT; break; case SQLITE_BLOB: *_type = mozIStorageStatement::VALUE_TYPE_BLOB; break; case SQLITE_NULL: *_type = mozIStorageStatement::VALUE_TYPE_NULL; break; default: return NS_ERROR_FAILURE; } return NS_OK; } NS_IMETHODIMP Statement::GetInt32(uint32_t aIndex, int32_t *_value) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); if (!mExecuting) return NS_ERROR_UNEXPECTED; *_value = ::sqlite3_column_int(mDBStatement, aIndex); return NS_OK; } NS_IMETHODIMP Statement::GetInt64(uint32_t aIndex, int64_t *_value) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); if (!mExecuting) return NS_ERROR_UNEXPECTED; *_value = ::sqlite3_column_int64(mDBStatement, aIndex); return NS_OK; } NS_IMETHODIMP Statement::GetDouble(uint32_t aIndex, double *_value) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); if (!mExecuting) return NS_ERROR_UNEXPECTED; *_value = ::sqlite3_column_double(mDBStatement, aIndex); return NS_OK; } NS_IMETHODIMP Statement::GetUTF8String(uint32_t aIndex, nsACString &_value) { // Get type of Index will check aIndex for us, so we don't have to. int32_t type; nsresult rv = GetTypeOfIndex(aIndex, &type); NS_ENSURE_SUCCESS(rv, rv); if (type == mozIStorageStatement::VALUE_TYPE_NULL) { // NULL columns should have IsVoid set to distinguish them from the empty // string. _value.SetIsVoid(true); } else { const char *value = reinterpret_cast( ::sqlite3_column_text(mDBStatement, aIndex)); _value.Assign(value, ::sqlite3_column_bytes(mDBStatement, aIndex)); } return NS_OK; } NS_IMETHODIMP Statement::GetString(uint32_t aIndex, nsAString &_value) { // Get type of Index will check aIndex for us, so we don't have to. int32_t type; nsresult rv = GetTypeOfIndex(aIndex, &type); NS_ENSURE_SUCCESS(rv, rv); if (type == mozIStorageStatement::VALUE_TYPE_NULL) { // NULL columns should have IsVoid set to distinguish them from the empty // string. _value.SetIsVoid(true); } else { const char16_t *value = static_cast( ::sqlite3_column_text16(mDBStatement, aIndex)); _value.Assign(value, ::sqlite3_column_bytes16(mDBStatement, aIndex) / 2); } return NS_OK; } NS_IMETHODIMP Statement::GetBlob(uint32_t aIndex, uint32_t *_size, uint8_t **_blob) { if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); if (!mExecuting) return NS_ERROR_UNEXPECTED; int size = ::sqlite3_column_bytes(mDBStatement, aIndex); void *blob = nullptr; if (size) { blob = moz_xmemdup(::sqlite3_column_blob(mDBStatement, aIndex), size); } *_blob = static_cast(blob); *_size = size; return NS_OK; } NS_IMETHODIMP Statement::GetBlobAsString(uint32_t aIndex, nsAString &aValue) { return DoGetBlobAsString(this, aIndex, aValue); } NS_IMETHODIMP Statement::GetBlobAsUTF8String(uint32_t aIndex, nsACString &aValue) { return DoGetBlobAsString(this, aIndex, aValue); } NS_IMETHODIMP Statement::GetSharedUTF8String(uint32_t aIndex, uint32_t *_length, const char **_value) { if (_length) *_length = ::sqlite3_column_bytes(mDBStatement, aIndex); *_value = reinterpret_cast( ::sqlite3_column_text(mDBStatement, aIndex)); return NS_OK; } NS_IMETHODIMP Statement::GetSharedString(uint32_t aIndex, uint32_t *_length, const char16_t **_value) { if (_length) *_length = ::sqlite3_column_bytes16(mDBStatement, aIndex); *_value = static_cast( ::sqlite3_column_text16(mDBStatement, aIndex)); return NS_OK; } NS_IMETHODIMP Statement::GetSharedBlob(uint32_t aIndex, uint32_t *_size, const uint8_t **_blob) { *_size = ::sqlite3_column_bytes(mDBStatement, aIndex); *_blob = static_cast(::sqlite3_column_blob(mDBStatement, aIndex)); return NS_OK; } NS_IMETHODIMP Statement::GetIsNull(uint32_t aIndex, bool *_isNull) { // Get type of Index will check aIndex for us, so we don't have to. int32_t type; nsresult rv = GetTypeOfIndex(aIndex, &type); NS_ENSURE_SUCCESS(rv, rv); *_isNull = (type == mozIStorageStatement::VALUE_TYPE_NULL); return NS_OK; } //////////////////////////////////////////////////////////////////////////////// //// mozIStorageBindingParams BOILERPLATE_BIND_PROXIES(Statement, if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED;) } // namespace storage } // namespace mozilla