/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Communicator client code. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1998 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Chris Waterson * Pierre Phaneuf * Joe Hewitt * Blake Ross * Chris Sears * Michael Lowe * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ /* A global browser history implementation that also supports the RDF datasource interface. TODO 1) Hook up Assert() etc. so that we can delete stuff. */ #include "nsNetUtil.h" #include "nsGlobalHistory.h" #include "nsCRT.h" #include "nsIEnumerator.h" #include "nsIServiceManager.h" #include "nsEnumeratorUtils.h" #include "nsRDFCID.h" #include "nsIDirectoryService.h" #include "nsAppDirectoryServiceDefs.h" #include "nsString.h" #include "nsReadableUtils.h" #include "nsUnicharUtils.h" #include "nsXPIDLString.h" #include "plhash.h" #include "plstr.h" #include "prprf.h" #include "prtime.h" #include "rdf.h" #include "nsQuickSort.h" #include "nsIIOService.h" #include "nsILocalFile.h" #include "nsIURL.h" #include "nsNetCID.h" #include "nsInt64.h" #include "nsMorkCID.h" #include "nsIMdbFactoryFactory.h" #include "nsIPrefService.h" #include "nsIPrefBranch2.h" #include "nsIObserverService.h" #include "nsITextToSubURI.h" PRInt32 nsGlobalHistory::gRefCnt; nsIRDFService* nsGlobalHistory::gRDFService; nsIRDFResource* nsGlobalHistory::kNC_Page; nsIRDFResource* nsGlobalHistory::kNC_Date; nsIRDFResource* nsGlobalHistory::kNC_FirstVisitDate; nsIRDFResource* nsGlobalHistory::kNC_VisitCount; nsIRDFResource* nsGlobalHistory::kNC_AgeInDays; nsIRDFResource* nsGlobalHistory::kNC_Name; nsIRDFResource* nsGlobalHistory::kNC_NameSort; nsIRDFResource* nsGlobalHistory::kNC_Hostname; nsIRDFResource* nsGlobalHistory::kNC_Referrer; nsIRDFResource* nsGlobalHistory::kNC_child; nsIRDFResource* nsGlobalHistory::kNC_URL; nsIRDFResource* nsGlobalHistory::kNC_HistoryRoot; nsIRDFResource* nsGlobalHistory::kNC_HistoryByDate; nsIMdbFactory* nsGlobalHistory::gMdbFactory = nsnull; nsIPrefBranch* nsGlobalHistory::gPrefBranch = nsnull; #define PREF_BRANCH_BASE "browser." #define PREF_BROWSER_HISTORY_EXPIRE_DAYS "history_expire_days" #define PREF_AUTOCOMPLETE_ONLY_TYPED "urlbar.matchOnlyTyped" #define PREF_AUTOCOMPLETE_ENABLED "urlbar.autocomplete.enabled" #define FIND_BY_AGEINDAYS_PREFIX "find:datasource=history&match=AgeInDays&method=" // see bug #319004 -- clamp title and URL to generously-large but not too large // length #define HISTORY_URI_LENGTH_MAX 65536 #define HISTORY_TITLE_LENGTH_MAX 4096 // sync history every 10 seconds #define HISTORY_SYNC_TIMEOUT (10 * PR_MSEC_PER_SEC) //#define HISTORY_SYNC_TIMEOUT 3000 // every 3 seconds - testing only! // the value of mLastNow expires every 3 seconds #define HISTORY_EXPIRE_NOW_TIMEOUT (3 * PR_MSEC_PER_SEC) static const PRInt64 MSECS_PER_DAY = LL_INIT(20, 500654080); // (1000000LL * 60 * 60 * 24) //---------------------------------------------------------------------- // // CIDs static NS_DEFINE_CID(kRDFServiceCID, NS_RDFSERVICE_CID); // closure structures for RemoveMatchingRows struct matchExpiration_t { PRTime *expirationDate; nsGlobalHistory *history; }; struct matchHost_t { const char *host; PRBool entireDomain; // should we delete the entire domain? nsGlobalHistory *history; }; struct matchSearchTerm_t { nsIMdbEnv *env; nsIMdbStore *store; searchTerm *term; PRBool haveClosure; // are the rest of the fields valid? PRInt32 intValue; nsGlobalHistory* globalHist; }; struct matchQuery_t { searchQuery* query; nsGlobalHistory* history; }; // simple token/value struct class tokenPair { public: tokenPair(const char *aName, PRUint32 aNameLen, const char *aValue, PRUint32 aValueLen) : tokenName(aName), tokenNameLength(aNameLen), tokenValue(aValue), tokenValueLength(aValueLen) { MOZ_COUNT_CTOR(tokenPair); } ~tokenPair() { MOZ_COUNT_DTOR(tokenPair); } const char* tokenName; PRUint32 tokenNameLength; const char* tokenValue; PRUint32 tokenValueLength; }; // individual search term, pulled from token/value structs class searchTerm { public: searchTerm(const char* aDatasource, PRUint32 aDatasourceLen, const char *aProperty, PRUint32 aPropertyLen, const char* aMethod, PRUint32 aMethodLen, const char* aText, PRUint32 aTextLen): datasource(aDatasource, aDatasource+aDatasourceLen), property(aProperty, aProperty+aPropertyLen), method(aMethod, aMethod+aMethodLen) { MOZ_COUNT_CTOR(searchTerm); nsresult rv; nsCOMPtr textToSubURI = do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); if (NS_SUCCEEDED(rv)) textToSubURI->UnEscapeAndConvert("UTF-8", PromiseFlatCString(Substring(aText, aText + aTextLen)).get(), getter_Copies(text)); } ~searchTerm() { MOZ_COUNT_DTOR(searchTerm); } nsDependentCSubstring datasource; // should always be "history" ? nsDependentCSubstring property; // AgeInDays, Hostname, etc nsDependentCSubstring method; // is, isgreater, isless nsXPIDLString text; // text to match rowMatchCallback match; // matching callback if needed }; // list of terms, plus an optional groupby column struct searchQuery { nsVoidArray terms; // array of searchTerms mdb_column groupBy; // column to group by }; static PRBool HasCell(nsIMdbEnv *aEnv, nsIMdbRow* aRow, mdb_column aCol) { mdbYarn yarn; mdb_err err = aRow->AliasCellYarn(aEnv, aCol, &yarn); // no cell if (err != 0) return PR_FALSE; // if we have the cell, make sure it has a value?? return (yarn.mYarn_Fill != 0); } PRTime nsGlobalHistory::NormalizeTime(PRTime aTime) { // we can optimize this by converting the time to local time, rounding // down to the previous day boundary, and then converting back to UTC. // This avoids two costly calls to localtime() // we calculate (gmtTime - (gmtTime % MSECS_PER_DAY)) - mCachedGMTOffset PRTime gmtTime; LL_ADD(gmtTime, aTime, mCachedGMTOffset); PRInt64 curDayUSec; LL_MOD(curDayUSec, gmtTime, MSECS_PER_DAY); PRTime gmtMidnight; LL_SUB(gmtMidnight, gmtTime, curDayUSec); PRTime localMidnight; LL_SUB(localMidnight, gmtMidnight, mCachedGMTOffset); return localMidnight; } PRInt32 nsGlobalHistory::GetAgeInDays(PRTime aDate) { PRTime timeNow = GetNow(); PRTime dateMidnight = NormalizeTime(aDate); PRTime diff; LL_SUB(diff, timeNow, dateMidnight); PRInt64 ageInDays; LL_DIV(ageInDays, diff, MSECS_PER_DAY); PRInt32 retval; LL_L2I(retval, ageInDays); return retval; } PRBool nsGlobalHistory::MatchExpiration(nsIMdbRow *row, PRTime* expirationDate) { nsresult rv; // hidden and typed urls always match because they're invalid, // so we want to expire them asap. (if they were valid, they'd // have been unhidden -- see AddExistingPageToDatabase) if (HasCell(mEnv, row, kToken_HiddenColumn) && HasCell(mEnv, row, kToken_TypedColumn)) return PR_TRUE; PRTime lastVisitedTime; rv = GetRowValue(row, kToken_LastVisitDateColumn, &lastVisitedTime); if (NS_FAILED(rv)) return PR_FALSE; return LL_CMP(lastVisitedTime, <, *expirationDate); } static PRBool matchAgeInDaysCallback(nsIMdbRow *row, void *aClosure) { matchSearchTerm_t *matchSearchTerm = (matchSearchTerm_t*)aClosure; const searchTerm *term = matchSearchTerm->term; nsIMdbEnv *env = matchSearchTerm->env; nsIMdbStore *store = matchSearchTerm->store; // fill in the rest of the closure if it's not filled in yet // this saves us from recalculating this stuff on every row if (!matchSearchTerm->haveClosure) { PRInt32 err; // Need to create an nsAutoString to use ToInteger matchSearchTerm->intValue = nsAutoString(term->text).ToInteger(&err); if (err != 0) return PR_FALSE; matchSearchTerm->haveClosure = PR_TRUE; } // XXX convert the property to a column, get the column value mdb_column column; mdb_err err = store->StringToToken(env, "LastVisitDate", &column); if (err != 0) return PR_FALSE; mdbYarn yarn; err = row->AliasCellYarn(env, column, &yarn); if (err != 0) return PR_FALSE; PRTime rowDate; PR_sscanf((const char*)yarn.mYarn_Buf, "%lld", &rowDate); PRInt32 days = matchSearchTerm->globalHist->GetAgeInDays(rowDate); if (term->method.Equals("is")) return (days == matchSearchTerm->intValue); else if (term->method.Equals("isgreater")) return (days > matchSearchTerm->intValue); else if (term->method.Equals("isless")) return (days < matchSearchTerm->intValue); return PR_FALSE; } static PRBool matchExpirationCallback(nsIMdbRow *row, void *aClosure) { matchExpiration_t *expires = (matchExpiration_t*)aClosure; return expires->history->MatchExpiration(row, expires->expirationDate); } static PRBool matchAllCallback(nsIMdbRow *row, void *aClosure) { return PR_TRUE; } static PRBool matchHostCallback(nsIMdbRow *row, void *aClosure) { matchHost_t *hostInfo = (matchHost_t*)aClosure; return hostInfo->history->MatchHost(row, hostInfo); } static PRBool matchQueryCallback(nsIMdbRow *row, void *aClosure) { matchQuery_t *query = (matchQuery_t*)aClosure; return query->history->RowMatches(row, query->query); } //---------------------------------------------------------------------- nsMdbTableEnumerator::nsMdbTableEnumerator() : mEnv(nsnull), mTable(nsnull), mCursor(nsnull), mCurrent(nsnull) { } nsresult nsMdbTableEnumerator::Init(nsIMdbEnv* aEnv, nsIMdbTable* aTable) { NS_PRECONDITION(aEnv != nsnull, "null ptr"); if (! aEnv) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aTable != nsnull, "null ptr"); if (! aTable) return NS_ERROR_NULL_POINTER; mEnv = aEnv; NS_ADDREF(mEnv); mTable = aTable; NS_ADDREF(mTable); mdb_err err; err = mTable->GetTableRowCursor(mEnv, -1, &mCursor); if (err != 0) return NS_ERROR_FAILURE; return NS_OK; } nsMdbTableEnumerator::~nsMdbTableEnumerator() { NS_IF_RELEASE(mCurrent); NS_IF_RELEASE(mCursor); NS_IF_RELEASE(mTable); NS_IF_RELEASE(mEnv); } NS_IMPL_ISUPPORTS1(nsMdbTableEnumerator, nsISimpleEnumerator) NS_IMETHODIMP nsMdbTableEnumerator::HasMoreElements(PRBool* _result) { if (! mCurrent) { mdb_err err; while (1) { mdb_pos pos; err = mCursor->NextRow(mEnv, &mCurrent, &pos); if (err != 0) return NS_ERROR_FAILURE; // If there are no more rows, then bail. if (! mCurrent) break; // If this is a result, the stop. if (IsResult(mCurrent)) break; // Otherwise, drop the ref to the row we retrieved, and continue // on to the next one. NS_RELEASE(mCurrent); mCurrent = nsnull; } } *_result = (mCurrent != nsnull); return NS_OK; } NS_IMETHODIMP nsMdbTableEnumerator::GetNext(nsISupports** _result) { nsresult rv; PRBool hasMore; rv = HasMoreElements(&hasMore); if (NS_FAILED(rv)) return rv; if (! hasMore) return NS_ERROR_UNEXPECTED; rv = ConvertToISupports(mCurrent, _result); NS_RELEASE(mCurrent); mCurrent = nsnull; return rv; } //---------------------------------------------------------------------- // // nsGlobalHistory // // ctor dtor etc. // nsGlobalHistory::nsGlobalHistory() : mExpireDays(9), // make default be nine days mAutocompleteOnlyTyped(PR_FALSE), mBatchesInProgress(0), mNowValid(PR_FALSE), mDirty(PR_FALSE), mEnv(nsnull), mStore(nsnull), mTable(nsnull) { LL_I2L(mFileSizeOnDisk, 0); // commonly used prefixes that should be chopped off all // history and input urls before comparison mIgnoreSchemes.AppendString(NS_LITERAL_STRING("http://")); mIgnoreSchemes.AppendString(NS_LITERAL_STRING("https://")); mIgnoreSchemes.AppendString(NS_LITERAL_STRING("ftp://")); mIgnoreHostnames.AppendString(NS_LITERAL_STRING("www.")); mIgnoreHostnames.AppendString(NS_LITERAL_STRING("ftp.")); mTypedHiddenURIs.Init(3); } nsGlobalHistory::~nsGlobalHistory() { gRDFService->UnregisterDataSource(this); nsresult rv; rv = CloseDB(); NS_IF_RELEASE(mTable); NS_IF_RELEASE(mStore); if (--gRefCnt == 0) { NS_IF_RELEASE(gRDFService); NS_IF_RELEASE(kNC_Page); NS_IF_RELEASE(kNC_Date); NS_IF_RELEASE(kNC_FirstVisitDate); NS_IF_RELEASE(kNC_VisitCount); NS_IF_RELEASE(kNC_AgeInDays); NS_IF_RELEASE(kNC_Name); NS_IF_RELEASE(kNC_NameSort); NS_IF_RELEASE(kNC_Hostname); NS_IF_RELEASE(kNC_Referrer); NS_IF_RELEASE(kNC_child); NS_IF_RELEASE(kNC_URL); NS_IF_RELEASE(kNC_HistoryRoot); NS_IF_RELEASE(kNC_HistoryByDate); NS_IF_RELEASE(gMdbFactory); NS_IF_RELEASE(gPrefBranch); } NS_IF_RELEASE(mEnv); if (mSyncTimer) mSyncTimer->Cancel(); if (mExpireNowTimer) mExpireNowTimer->Cancel(); } //---------------------------------------------------------------------- // // nsGlobalHistory // // nsISupports methods NS_IMPL_ADDREF(nsGlobalHistory) NS_IMPL_RELEASE(nsGlobalHistory) NS_INTERFACE_MAP_BEGIN(nsGlobalHistory) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIGlobalHistory2, nsIGlobalHistory3) NS_INTERFACE_MAP_ENTRY(nsIGlobalHistory3) NS_INTERFACE_MAP_ENTRY(nsIBrowserHistory) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY(nsIRDFDataSource) NS_INTERFACE_MAP_ENTRY(nsIRDFRemoteDataSource) NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteSearch) NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteResult) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIBrowserHistory) NS_INTERFACE_MAP_END //---------------------------------------------------------------------- // // nsGlobalHistory // // nsIGlobalHistory2 methods // NS_IMETHODIMP nsGlobalHistory::AddURI(nsIURI *aURI, PRBool aRedirect, PRBool aTopLevel, nsIURI *aReferrer) { nsresult rv; NS_ENSURE_ARG_POINTER(aURI); // If history is set to expire after 0 days, // then it's technically disabled. Don't even // bother adding the page if (mExpireDays == 0) return NS_OK; // filter out unwanted URIs such as chrome: mailbox: etc // The model is really if we don't know differently then add which basically // means we are suppose to try all the things we know not to allow in and // then if we don't bail go on and allow it in. But here lets compare // against the most common case we know to allow in and go on and say yes // to it. PRBool isHTTP = PR_FALSE; PRBool isHTTPS = PR_FALSE; NS_ENSURE_SUCCESS(rv = aURI->SchemeIs("http", &isHTTP), rv); NS_ENSURE_SUCCESS(rv = aURI->SchemeIs("https", &isHTTPS), rv); if (!isHTTP && !isHTTPS) { PRBool isAbout, isImap, isNews, isMailbox, isViewSource, isChrome, isData; rv = aURI->SchemeIs("about", &isAbout); rv |= aURI->SchemeIs("imap", &isImap); rv |= aURI->SchemeIs("news", &isNews); rv |= aURI->SchemeIs("mailbox", &isMailbox); rv |= aURI->SchemeIs("view-source", &isViewSource); rv |= aURI->SchemeIs("chrome", &isChrome); rv |= aURI->SchemeIs("data", &isData); NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); if (isAbout || isImap || isNews || isMailbox || isViewSource || isChrome || isData) { return NS_OK; } } rv = OpenDB(); NS_ENSURE_SUCCESS(rv, rv); nsCAutoString URISpec; rv = aURI->GetSpec(URISpec); NS_ENSURE_SUCCESS(rv, rv); if (URISpec.Length() > HISTORY_URI_LENGTH_MAX) return NS_OK; nsCAutoString referrerSpec; if (aReferrer) { rv = aReferrer->GetSpec(referrerSpec); NS_ENSURE_SUCCESS(rv, rv); } PRTime now = GetNow(); // For notifying observers, later... nsCOMPtr url; rv = gRDFService->GetResource(URISpec, getter_AddRefs(url)); if (NS_FAILED(rv)) return rv; nsCOMPtr date; rv = gRDFService->GetDateLiteral(now, getter_AddRefs(date)); if (NS_FAILED(rv)) return rv; PRBool isJavascript; rv = aURI->SchemeIs("javascript", &isJavascript); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr row; rv = FindRow(kToken_URLColumn, URISpec.get(), getter_AddRefs(row)); if (NS_SUCCEEDED(rv)) { // If this is not a JS url, not a redirected URI and not in a frame, // unhide it since URIs are added hidden if they are redirected, in a // frame or typed. PRBool wasTyped = HasCell(mEnv, row, kToken_TypedColumn); if (wasTyped) { mTypedHiddenURIs.Remove(URISpec); } if ((!isJavascript && !aRedirect && aTopLevel) || wasTyped) { row->CutColumn(mEnv, kToken_HiddenColumn); } // update the database, and get the old info back PRTime oldDate; PRInt32 oldCount; rv = AddExistingPageToDatabase(row, now, referrerSpec.get(), &oldDate, &oldCount); NS_ASSERTION(NS_SUCCEEDED(rv), "AddExistingPageToDatabase failed; see bug 88961"); if (NS_FAILED(rv)) return rv; // Notify observers // visit date nsCOMPtr oldDateLiteral; rv = gRDFService->GetDateLiteral(oldDate, getter_AddRefs(oldDateLiteral)); if (NS_FAILED(rv)) return rv; rv = NotifyChange(url, kNC_Date, oldDateLiteral, date); if (NS_FAILED(rv)) return rv; // visit count nsCOMPtr oldCountLiteral; rv = gRDFService->GetIntLiteral(oldCount, getter_AddRefs(oldCountLiteral)); if (NS_FAILED(rv)) return rv; nsCOMPtr newCountLiteral; rv = gRDFService->GetIntLiteral(oldCount+1, getter_AddRefs(newCountLiteral)); if (NS_FAILED(rv)) return rv; rv = NotifyChange(url, kNC_VisitCount, oldCountLiteral, newCountLiteral); if (NS_FAILED(rv)) return rv; } else { rv = AddNewPageToDatabase(URISpec.get(), now, referrerSpec.get(), getter_AddRefs(row)); NS_ASSERTION(NS_SUCCEEDED(rv), "AddNewPageToDatabase failed; see bug 88961"); if (NS_FAILED(rv)) return rv; if (isJavascript || aRedirect || !aTopLevel) { // if this is a JS url, or a redirected URI or in a frame, hide it in // global history so that it doesn't show up in the autocomplete // dropdown. We'll unhide non-JS urls later if we visit the URI not as // part of a redirect and not in a frame. See bug 197127, bug 161531 and // bug 322106 for details. rv = SetRowValue(row, kToken_HiddenColumn, 1); NS_ENSURE_SUCCESS(rv, rv); } else { // Notify observers rv = NotifyAssert(url, kNC_Date, date); if (NS_FAILED(rv)) return rv; rv = NotifyAssert(kNC_HistoryRoot, kNC_child, url); if (NS_FAILED(rv)) return rv; NotifyFindAssertions(url, row); } } // Store last visited page if we have the pref set accordingly if (aTopLevel) { PRInt32 choice = 0; if (NS_SUCCEEDED(gPrefBranch->GetIntPref("startup.page", &choice))) { if (choice != 2) { if (NS_SUCCEEDED(gPrefBranch->GetIntPref("windows.loadOnNewWindow", &choice))) { if (choice != 2) { gPrefBranch->GetIntPref("tabs.loadOnNewTab", &choice); } } } } if (choice == 2) { NS_ENSURE_STATE(mMetaRow); SetRowValue(mMetaRow, kToken_LastPageVisited, URISpec.get()); } } SetDirty(); return NS_OK; } nsresult nsGlobalHistory::AddExistingPageToDatabase(nsIMdbRow *row, PRTime aDate, const char *aReferrer, PRTime *aOldDate, PRInt32 *aOldCount) { nsresult rv; nsCAutoString oldReferrer; // Update last visit date. // First get the old date so we can update observers... rv = GetRowValue(row, kToken_LastVisitDateColumn, aOldDate); if (NS_FAILED(rv)) return rv; // get the old count, so we can update it rv = GetRowValue(row, kToken_VisitCountColumn, aOldCount); if (NS_FAILED(rv) || *aOldCount < 1) *aOldCount = 1; // assume we've visited at least once // ...now set the new date. SetRowValue(row, kToken_LastVisitDateColumn, aDate); SetRowValue(row, kToken_VisitCountColumn, (*aOldCount) + 1); if (aReferrer && *aReferrer) { rv = GetRowValue(row, kToken_ReferrerColumn, oldReferrer); // No referrer? Now there is! if (NS_FAILED(rv) || oldReferrer.IsEmpty()) SetRowValue(row, kToken_ReferrerColumn, aReferrer); } return NS_OK; } nsresult nsGlobalHistory::AddNewPageToDatabase(const char *aURL, PRTime aDate, const char *aReferrer, nsIMdbRow **aResult) { mdb_err err; // Create a new row mdbOid rowId; rowId.mOid_Scope = kToken_HistoryRowScope; rowId.mOid_Id = mdb_id(-1); NS_PRECONDITION(mTable != nsnull, "not initialized"); if (! mTable) return NS_ERROR_NOT_INITIALIZED; nsCOMPtr row; err = mTable->NewRow(mEnv, &rowId, getter_AddRefs(row)); if (err != 0) return NS_ERROR_FAILURE; // Set the URL SetRowValue(row, kToken_URLColumn, aURL); // Set the date. SetRowValue(row, kToken_LastVisitDateColumn, aDate); SetRowValue(row, kToken_FirstVisitDateColumn, aDate); // Set the referrer if there is one. if (aReferrer && *aReferrer) SetRowValue(row, kToken_ReferrerColumn, aReferrer); nsCOMPtr uri; NS_NewURI(getter_AddRefs(uri), nsDependentCString(aURL), nsnull, nsnull); nsCAutoString hostname; if (uri) uri->GetHost(hostname); SetRowValue(row, kToken_HostnameColumn, hostname.get()); *aResult = row; NS_ADDREF(*aResult); return NS_OK; } nsresult nsGlobalHistory::RemovePageInternal(const nsCString& aSpec) { if (!mTable) return NS_ERROR_NOT_INITIALIZED; // find the old row, ignore it if we don't have it nsCOMPtr row; nsresult rv = FindRow(kToken_URLColumn, aSpec.get(), getter_AddRefs(row)); if (NS_FAILED(rv)) return NS_OK; // remove the row return RemovePageInternal(aSpec, row); } nsresult nsGlobalHistory::RemovePageInternal(const nsCString& aSpec, nsIMdbRow *aRow) { mdb_err err = mTable->CutRow(mEnv, aRow); NS_ENSURE_TRUE(err == 0, NS_ERROR_FAILURE); // if there are batches in progress, we don't want to notify // observers that we're deleting items. the caller promises // to handle whatever UI updating is necessary when we're finished. if (!mBatchesInProgress) { // get the resource so we can do the notification nsCOMPtr oldRowResource; gRDFService->GetResource(aSpec, getter_AddRefs(oldRowResource)); NotifyFindUnassertions(oldRowResource, aRow); } // not a fatal error if we can't cut all column err = aRow->CutAllColumns(mEnv); NS_ASSERTION(err == 0, "couldn't cut all columns"); return Commit(kCompressCommit); } nsresult nsGlobalHistory::SetRowValue(nsIMdbRow *aRow, mdb_column aCol, const PRTime& aValue) { mdb_err err; nsCAutoString val; val.AppendInt(aValue); mdbYarn yarn = { (void *)val.get(), val.Length(), val.Length(), 0, 0, nsnull }; err = aRow->AddColumn(mEnv, aCol, &yarn); if ( err != 0 ) return NS_ERROR_FAILURE; return NS_OK; } nsresult nsGlobalHistory::SetRowValue(nsIMdbRow *aRow, mdb_column aCol, const PRUnichar* aValue) { mdb_err err; PRInt32 len = (nsCRT::strlen(aValue) * sizeof(PRUnichar)); PRUnichar *swapval = nsnull; // eventually turn this on when we're confident in mork's ability // to handle yarn forms properly #if 0 NS_ConvertUTF16toUTF8 utf8Value(aValue); printf("Storing utf8 value %s\n", utf8Value.get()); mdbYarn yarn = { (void *)utf8Value.get(), utf8Value.Length(), utf8Value.Length(), 0, 1, nsnull }; #else if (mReverseByteOrder) { // The file is other-endian. Byte-swap the value. swapval = (PRUnichar *)malloc(len); if (!swapval) return NS_ERROR_OUT_OF_MEMORY; SwapBytes(aValue, swapval, len / sizeof(PRUnichar)); aValue = swapval; } mdbYarn yarn = { (void *)aValue, len, len, 0, 0, nsnull }; #endif err = aRow->AddColumn(mEnv, aCol, &yarn); if (swapval) free(swapval); if (err != 0) return NS_ERROR_FAILURE; return NS_OK; } nsresult nsGlobalHistory::SetRowValue(nsIMdbRow *aRow, mdb_column aCol, const char* aValue) { mdb_err err; PRInt32 len = PL_strlen(aValue); mdbYarn yarn = { (void*) aValue, len, len, 0, 0, nsnull }; err = aRow->AddColumn(mEnv, aCol, &yarn); if (err != 0) return NS_ERROR_FAILURE; return NS_OK; } nsresult nsGlobalHistory::SetRowValue(nsIMdbRow *aRow, mdb_column aCol, const PRInt32 aValue) { mdb_err err; nsCAutoString buf; buf.AppendInt(aValue); mdbYarn yarn = { (void *)buf.get(), buf.Length(), buf.Length(), 0, 0, nsnull }; err = aRow->AddColumn(mEnv, aCol, &yarn); if (err != 0) return NS_ERROR_FAILURE; return NS_OK; } nsresult nsGlobalHistory::GetRowValue(nsIMdbRow *aRow, mdb_column aCol, nsAString& aResult) { mdb_err err; mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, aCol, &yarn); if (err != 0) return NS_ERROR_FAILURE; aResult.Truncate(0); if (!yarn.mYarn_Fill) return NS_OK; if (aCol != kToken_NameColumn) { const char* start = (const char*)yarn.mYarn_Buf; if (start) AppendUTF8toUTF16(Substring(start, start + yarn.mYarn_Fill), aResult); return NS_OK; } switch (yarn.mYarn_Form) { case 0: // unicode if (mReverseByteOrder) { // The file is other-endian; we must byte-swap the result. PRUnichar *swapval; int len = yarn.mYarn_Fill / sizeof(PRUnichar); swapval = (PRUnichar *)malloc(yarn.mYarn_Fill); if (!swapval) return NS_ERROR_OUT_OF_MEMORY; SwapBytes((const PRUnichar *)yarn.mYarn_Buf, swapval, len); aResult.Assign(swapval, len); free(swapval); } else aResult.Assign((const PRUnichar *)yarn.mYarn_Buf, yarn.mYarn_Fill/sizeof(PRUnichar)); break; // eventually we'll be supporting this in SetRowValue() case 1: // UTF8 CopyUTF8toUTF16(Substring((const char*)yarn.mYarn_Buf, (const char*)yarn.mYarn_Buf + yarn.mYarn_Fill), aResult); break; default: return NS_ERROR_UNEXPECTED; } return NS_OK; } // Copy an array of 16-bit values, reversing the byte order. void nsGlobalHistory::SwapBytes(const PRUnichar *source, PRUnichar *dest, PRInt32 aLen) { PRUint16 c; const PRUnichar *inp; PRUnichar *outp; PRInt32 i; inp = source; outp = dest; for (i = 0; i < aLen; i++) { c = *inp++; *outp++ = (((c >> 8) & 0xff) | (c << 8)); } return; } nsresult nsGlobalHistory::GetRowValue(nsIMdbRow *aRow, mdb_column aCol, PRTime *aResult) { mdb_err err; mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, aCol, &yarn); if (err != 0) return NS_ERROR_FAILURE; *aResult = LL_ZERO; if (!yarn.mYarn_Fill || !yarn.mYarn_Buf) return NS_OK; PR_sscanf((const char*)yarn.mYarn_Buf, "%lld", aResult); return NS_OK; } nsresult nsGlobalHistory::GetRowValue(nsIMdbRow *aRow, mdb_column aCol, PRInt32 *aResult) { mdb_err err; mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, aCol, &yarn); if (err != 0) return NS_ERROR_FAILURE; if (yarn.mYarn_Buf) *aResult = atoi((char *)yarn.mYarn_Buf); else *aResult = 0; return NS_OK; } nsresult nsGlobalHistory::GetRowValue(nsIMdbRow *aRow, mdb_column aCol, nsACString& aResult) { mdb_err err; mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, aCol, &yarn); if (err != 0) return NS_ERROR_FAILURE; const char* startPtr = (const char*)yarn.mYarn_Buf; if (startPtr) aResult.Assign(Substring(startPtr, startPtr + yarn.mYarn_Fill)); else aResult.Truncate(); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::GetCount(PRUint32* aCount) { NS_ENSURE_ARG_POINTER(aCount); NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); if (!mTable) return NS_ERROR_FAILURE; mdb_err err = mTable->GetCount(mEnv, aCount); return (err == 0) ? NS_OK : NS_ERROR_FAILURE; } NS_IMETHODIMP nsGlobalHistory::SetPageTitle(nsIURI *aURI, const nsAString& aTitle) { nsresult rv; NS_ENSURE_ARG_POINTER(aURI); nsAutoString titleString(StringHead(aTitle, HISTORY_TITLE_LENGTH_MAX)); if (titleString.Length() < aTitle.Length() && NS_IS_HIGH_SURROGATE(titleString.Last())) titleString.Truncate(titleString.Length()-1); // skip about: URIs to avoid reading in the db (about:blank, especially) PRBool isAbout; rv = aURI->SchemeIs("about", &isAbout); NS_ENSURE_SUCCESS(rv, rv); if (isAbout) return NS_OK; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsCAutoString URISpec; rv = aURI->GetSpec(URISpec); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr row; rv = FindRow(kToken_URLColumn, URISpec.get(), getter_AddRefs(row)); // if the row doesn't exist, we silently succeed if (rv == NS_ERROR_NOT_AVAILABLE) return NS_OK; NS_ENSURE_SUCCESS(rv, rv); // Get the old title so we can notify observers nsAutoString oldtitle; rv = GetRowValue(row, kToken_NameColumn, oldtitle); if (NS_FAILED(rv)) return rv; nsCOMPtr oldname; if (!oldtitle.IsEmpty()) { rv = gRDFService->GetLiteral(oldtitle.get(), getter_AddRefs(oldname)); if (NS_FAILED(rv)) return rv; } SetRowValue(row, kToken_NameColumn, titleString.get()); // ...and update observers nsCOMPtr url; rv = gRDFService->GetResource(URISpec, getter_AddRefs(url)); if (NS_FAILED(rv)) return rv; nsCOMPtr name; rv = gRDFService->GetLiteral(titleString.get(), getter_AddRefs(name)); if (NS_FAILED(rv)) return rv; if (oldname) { rv = NotifyChange(url, kNC_Name, oldname, name); } else { rv = NotifyAssert(url, kNC_Name, name); } return rv; } NS_IMETHODIMP nsGlobalHistory::RemovePage(nsIURI *aURI) { nsCAutoString spec; nsresult rv = aURI->GetSpec(spec); if (NS_SUCCEEDED(rv)) rv = RemovePageInternal(spec); return rv; } NS_IMETHODIMP nsGlobalHistory::RemovePagesFromHost(const nsACString &aHost, PRBool aEntireDomain) { const nsCString &host = PromiseFlatCString(aHost); matchHost_t hostInfo; hostInfo.history = this; hostInfo.entireDomain = aEntireDomain; hostInfo.host = host.get(); nsresult rv = RemoveMatchingRows(matchHostCallback, (void *)&hostInfo, PR_TRUE); if (NS_FAILED(rv)) return rv; return Commit(kCompressCommit); } PRBool nsGlobalHistory::MatchHost(nsIMdbRow *aRow, matchHost_t *hostInfo) { mdb_err err; nsresult rv; mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, kToken_URLColumn, &yarn); if (err != 0) return PR_FALSE; nsCOMPtr uri; // do smart zero-termination const char* startPtr = (const char *)yarn.mYarn_Buf; rv = NS_NewURI(getter_AddRefs(uri), Substring(startPtr, startPtr + yarn.mYarn_Fill)); if (NS_FAILED(rv)) return PR_FALSE; nsCAutoString urlHost; rv = uri->GetHost(urlHost); if (NS_FAILED(rv)) return PR_FALSE; if (PL_strcmp(urlHost.get(), hostInfo->host) == 0) return PR_TRUE; // now try for a domain match, if necessary if (hostInfo->entireDomain) { // do a reverse-search to match the end of the string const char *domain = PL_strrstr(urlHost.get(), hostInfo->host); // now verify that we're matching EXACTLY the domain, and // not some random string inside the hostname if (domain && (PL_strcmp(domain, hostInfo->host) == 0)) return PR_TRUE; } return PR_FALSE; } NS_IMETHODIMP nsGlobalHistory::RemoveAllPages() { nsresult rv; rv = RemoveMatchingRows(matchAllCallback, nsnull, PR_TRUE); if (NS_FAILED(rv)) return rv; // Reset the file byte order. rv = InitByteOrder(PR_TRUE); if (NS_FAILED(rv)) return rv; return Commit(kCompressCommit); } nsresult nsGlobalHistory::RemoveMatchingRows(rowMatchCallback aMatchFunc, void *aClosure, PRBool notify) { NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsresult rv; if (!mTable) return NS_OK; mdb_err err; mdb_count count; err = mTable->GetCount(mEnv, &count); if (err != 0) return NS_ERROR_FAILURE; BeginUpdateBatch(); // Begin the batch. int marker; err = mTable->StartBatchChangeHint(mEnv, &marker); NS_ASSERTION(err == 0, "unable to start batch"); if (err != 0) return NS_ERROR_FAILURE; nsCOMPtr resource; // XXX from here until end batch, no early returns! for (mdb_pos pos = count - 1; pos >= 0; --pos) { nsCOMPtr row; err = mTable->PosToRow(mEnv, pos, getter_AddRefs(row)); NS_ASSERTION(err == 0, "unable to get row"); if (err != 0) break; NS_ASSERTION(row != nsnull, "no row"); if (! row) continue; // now we actually do the match. If this row doesn't match, loop again if (!(aMatchFunc)(row, aClosure)) continue; if (notify) { // What's the URL? We need to know to properly notify our RDF // observers. mdbYarn yarn; err = row->AliasCellYarn(mEnv, kToken_URLColumn, &yarn); if (err != 0) continue; nsCAutoString uri((const char*)yarn.mYarn_Buf, yarn.mYarn_Fill); rv = gRDFService->GetResource(uri, getter_AddRefs(resource)); NS_ASSERTION(NS_SUCCEEDED(rv), "unable to get resource"); if (NS_FAILED(rv)) continue; } // Officially cut the row *now*, before notifying any observers: // that way, any re-entrant calls won't find the row. err = mTable->CutRow(mEnv, row); NS_ASSERTION(err == 0, "couldn't cut row"); if (err != 0) continue; // possibly avoid leakage err = row->CutAllColumns(mEnv); NS_ASSERTION(err == 0, "couldn't cut all columns"); // we'll notify regardless of whether we could successfully // CutAllColumns or not. } // Finish the batch. err = mTable->EndBatchChangeHint(mEnv, &marker); NS_ASSERTION(err == 0, "error ending batch"); EndUpdateBatch(); return ( err == 0) ? NS_OK : NS_ERROR_FAILURE; } NS_IMETHODIMP nsGlobalHistory::IsVisited(nsIURI* aURI, PRBool *_retval) { NS_ENSURE_ARG_POINTER(aURI); nsresult rv; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_NOT_INITIALIZED); nsCAutoString URISpec; rv = aURI->GetSpec(URISpec); NS_ENSURE_SUCCESS(rv, rv); rv = FindRow(kToken_URLColumn, URISpec.get(), nsnull); *_retval = NS_SUCCEEDED(rv); // Hidden, typed URIs haven't really been visited yet. They've only // been typed in and the actual load hasn't happened yet. We maintain // the list of hidden+typed URIs in memory in mTypedHiddenURIs because // the list will usually be small and checking the actual Mork row // would require several dynamic memory allocations. if (*_retval && mTypedHiddenURIs.Contains(URISpec)) { *_retval = PR_FALSE; } return NS_OK; } NS_IMETHODIMP nsGlobalHistory::GetLastPageVisited(nsACString& _retval) { NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); NS_ENSURE_STATE(mMetaRow); return GetRowValue(mMetaRow, kToken_LastPageVisited, _retval); } // Set the byte order in the history file. The given string value should // be either "BE" (big-endian) or "LE" (little-endian). nsresult nsGlobalHistory::SaveByteOrder(const char *aByteOrder) { if (PL_strcmp(aByteOrder, "BE") != 0 && PL_strcmp(aByteOrder, "LE") != 0) { NS_WARNING("Invalid byte order argument."); return NS_ERROR_INVALID_ARG; } NS_ENSURE_STATE(mMetaRow); mdb_err err = SetRowValue(mMetaRow, kToken_ByteOrder, aByteOrder); NS_ENSURE_TRUE(err == 0, NS_ERROR_FAILURE); return NS_OK; } // Get the file byte order. nsresult nsGlobalHistory::GetByteOrder(char **_retval) { NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); NS_ENSURE_ARG_POINTER(_retval); NS_ENSURE_STATE(mMetaRow); nsCAutoString byteOrder; nsresult rv = GetRowValue(mMetaRow, kToken_ByteOrder, byteOrder); NS_ENSURE_SUCCESS(rv, rv); *_retval = ToNewCString(byteOrder); NS_ENSURE_TRUE(*_retval, NS_ERROR_OUT_OF_MEMORY); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::HidePage(nsIURI *aURI) { nsresult rv; NS_ENSURE_ARG_POINTER(aURI); nsCAutoString URISpec; rv = aURI->GetSpec(URISpec); NS_ENSURE_SUCCESS(rv, rv); if (URISpec.Length() > HISTORY_URI_LENGTH_MAX) return NS_OK; nsCOMPtr row; rv = FindRow(kToken_URLColumn, URISpec.get(), getter_AddRefs(row)); if (NS_FAILED(rv)) { // it hasn't been visited yet, but if one ever comes in, we need // to hide it when it is visited rv = AddURI(aURI, PR_FALSE, PR_FALSE, nsnull); if (NS_FAILED(rv)) return rv; rv = FindRow(kToken_URLColumn, URISpec.get(), getter_AddRefs(row)); if (NS_FAILED(rv)) return rv; } rv = SetRowValue(row, kToken_HiddenColumn, 1); if (NS_FAILED(rv)) return rv; // now pretend as if this row was deleted // HasAssertion() correctly checks the Hidden column to show that // the row is hidden nsCOMPtr urlResource; rv = gRDFService->GetResource(URISpec, getter_AddRefs(urlResource)); if (NS_FAILED(rv)) return rv; return NotifyFindUnassertions(urlResource, row); } NS_IMETHODIMP nsGlobalHistory::MarkPageAsTyped(nsIURI *aURI) { nsCAutoString spec; nsresult rv = aURI->GetSpec(spec); if (NS_FAILED(rv)) return rv; if (spec.Length() > HISTORY_URI_LENGTH_MAX) return NS_OK; nsCOMPtr row; rv = FindRow(kToken_URLColumn, spec.get(), getter_AddRefs(row)); if (NS_FAILED(rv)) { rv = AddNewPageToDatabase(spec.get(), GetNow(), nsnull, getter_AddRefs(row)); NS_ENSURE_SUCCESS(rv, rv); // We don't know if this is a valid URI yet. Hide it until it finishes // loading. SetRowValue(row, kToken_HiddenColumn, 1); mTypedHiddenURIs.Put(spec); } return SetRowValue(row, kToken_TypedColumn, 1); } NS_IMETHODIMP nsGlobalHistory::AddDocumentRedirect(nsIChannel *aOldChannel, nsIChannel *aNewChannel, PRInt32 aFlags, PRBool aTopLevel) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsGlobalHistory::SetURIGeckoFlags(nsIURI *aURI, PRUint32 aFlags) { nsCAutoString spec; nsresult rv = aURI->GetSpec(spec); if (NS_FAILED(rv)) return rv; nsCOMPtr row; rv = FindRow(kToken_URLColumn, spec.get(), getter_AddRefs(row)); if (NS_FAILED(rv)) { return rv; } return SetRowValue(row, kToken_GeckoFlagsColumn, (PRInt32)aFlags); } NS_IMETHODIMP nsGlobalHistory::GetURIGeckoFlags(nsIURI *aURI, PRUint32* aFlags) { nsCAutoString spec; nsresult rv = aURI->GetSpec(spec); if (NS_FAILED(rv)) return rv; nsCOMPtr row; rv = FindRow(kToken_URLColumn, spec.get(), getter_AddRefs(row)); if (NS_FAILED(rv)) { return rv; } if (!HasCell(mEnv, row, kToken_GeckoFlagsColumn)) return NS_ERROR_FAILURE; return GetRowValue(row, kToken_GeckoFlagsColumn, (PRInt32*)aFlags); } //---------------------------------------------------------------------- // // nsGlobalHistory // // nsIRDFDataSource methods NS_IMETHODIMP nsGlobalHistory::GetURI(char* *aURI) { NS_PRECONDITION(aURI != nsnull, "null ptr"); if (! aURI) return NS_ERROR_NULL_POINTER; *aURI = nsCRT::strdup("rdf:history"); if (! *aURI) return NS_ERROR_OUT_OF_MEMORY; return NS_OK; } NS_IMETHODIMP nsGlobalHistory::GetSource(nsIRDFResource* aProperty, nsIRDFNode* aTarget, PRBool aTruthValue, nsIRDFResource** aSource) { NS_PRECONDITION(aProperty != nsnull, "null ptr"); if (! aProperty) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aTarget != nsnull, "null ptr"); if (! aTarget) return NS_ERROR_NULL_POINTER; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsresult rv; *aSource = nsnull; if (aProperty == kNC_URL) { // See if we have the row... // XXX We could be more forgiving here, and check for literal // values as well. nsCOMPtr target = do_QueryInterface(aTarget); if (target && IsURLInHistory(target)) return CallQueryInterface(aTarget, aSource); } else if ((aProperty == kNC_Date) || (aProperty == kNC_FirstVisitDate) || (aProperty == kNC_VisitCount) || (aProperty == kNC_Name) || (aProperty == kNC_Hostname) || (aProperty == kNC_Referrer)) { // Call GetSources() and return the first one we find. nsCOMPtr sources; rv = GetSources(aProperty, aTarget, aTruthValue, getter_AddRefs(sources)); if (NS_FAILED(rv)) return rv; PRBool hasMore; rv = sources->HasMoreElements(&hasMore); if (NS_FAILED(rv)) return rv; if (hasMore) { nsCOMPtr isupports; rv = sources->GetNext(getter_AddRefs(isupports)); if (NS_FAILED(rv)) return rv; return CallQueryInterface(isupports, aSource); } } return NS_RDF_NO_VALUE; } NS_IMETHODIMP nsGlobalHistory::GetSources(nsIRDFResource* aProperty, nsIRDFNode* aTarget, PRBool aTruthValue, nsISimpleEnumerator** aSources) { // XXX TODO: make sure each URL in history is connected back to // NC:HistoryRoot. NS_PRECONDITION(aProperty != nsnull, "null ptr"); if (! aProperty) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aTarget != nsnull, "null ptr"); if (! aTarget) return NS_ERROR_NULL_POINTER; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsresult rv; if (aProperty == kNC_URL) { // Call GetSource() and return a singleton enumerator for the URL. nsCOMPtr source; rv = GetSource(aProperty, aTarget, aTruthValue, getter_AddRefs(source)); if (NS_FAILED(rv)) return rv; return NS_NewSingletonEnumerator(aSources, source); } else { // See if aProperty is something we understand, and create an // URLEnumerator to select URLs with the appropriate value. mdb_column col = 0; // == "not a property that I grok" void* value = nsnull; PRInt32 len = 0; // PRInt64/date properties if (aProperty == kNC_Date || aProperty == kNC_FirstVisitDate) { nsCOMPtr date = do_QueryInterface(aTarget); if (date) { PRInt64 n; rv = date->GetValue(&n); if (NS_FAILED(rv)) return rv; nsCAutoString valueStr; valueStr.AppendInt(n); value = (void *)ToNewCString(valueStr); if (aProperty == kNC_Date) col = kToken_LastVisitDateColumn; else col = kToken_FirstVisitDateColumn; } } // PRInt32 properties else if (aProperty == kNC_VisitCount) { nsCOMPtr countLiteral = do_QueryInterface(aTarget); if (countLiteral) { PRInt32 intValue; rv = countLiteral->GetValue(&intValue); if (NS_FAILED(rv)) return rv; nsAutoString valueStr; valueStr.AppendInt(intValue); value = ToNewUnicode(valueStr); len = valueStr.Length() * sizeof(PRUnichar); col = kToken_VisitCountColumn; } } // PRUnichar* properties else if (aProperty == kNC_Name) { nsCOMPtr name = do_QueryInterface(aTarget); if (name) { PRUnichar* p; rv = name->GetValue(&p); if (NS_FAILED(rv)) return rv; len = nsCRT::strlen(p) * sizeof(PRUnichar); value = p; col = kToken_NameColumn; } } // char* properties else if (aProperty == kNC_Hostname || aProperty == kNC_Referrer) { col = kToken_ReferrerColumn; nsCOMPtr referrer = do_QueryInterface(aTarget); if (referrer) { char* p; rv = referrer->GetValue(&p); if (NS_FAILED(rv)) return rv; len = PL_strlen(p); value = p; if (aProperty == kNC_Hostname) col = kToken_HostnameColumn; else if (aProperty == kNC_Referrer) col = kToken_ReferrerColumn; } } if (col) { // The URLEnumerator takes ownership of the bytes allocated in |value|. URLEnumerator* result = new URLEnumerator(kToken_URLColumn, col, kToken_HiddenColumn, value, len); if (! result) return NS_ERROR_OUT_OF_MEMORY; rv = result->Init(mEnv, mTable); if (NS_FAILED(rv)) return rv; *aSources = result; NS_ADDREF(*aSources); return NS_OK; } } return NS_NewEmptyEnumerator(aSources); } NS_IMETHODIMP nsGlobalHistory::GetTarget(nsIRDFResource* aSource, nsIRDFResource* aProperty, PRBool aTruthValue, nsIRDFNode** aTarget) { NS_PRECONDITION(aSource != nsnull, "null ptr"); if (! aSource) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aProperty != nsnull, "null ptr"); if (! aProperty) return NS_ERROR_NULL_POINTER; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsresult rv; // Initialize return value. *aTarget = nsnull; // Only "positive" assertions here. if (! aTruthValue) return NS_RDF_NO_VALUE; // XXX eventually use IsFindResource to simply return the first // matching row? if (aProperty == kNC_child && (aSource == kNC_HistoryRoot || aSource == kNC_HistoryByDate || IsFindResource(aSource))) { // If they're asking for all the children of the HistoryRoot, call // through to GetTargets() and return the first one. nsCOMPtr targets; rv = GetTargets(aSource, aProperty, aTruthValue, getter_AddRefs(targets)); if (NS_FAILED(rv)) return rv; PRBool hasMore; rv = targets->HasMoreElements(&hasMore); if (NS_FAILED(rv)) return rv; if (! hasMore) return NS_RDF_NO_VALUE; nsCOMPtr isupports; rv = targets->GetNext(getter_AddRefs(isupports)); if (NS_FAILED(rv)) return rv; return CallQueryInterface(isupports, aTarget); } else if ((aProperty == kNC_Date) || (aProperty == kNC_FirstVisitDate) || (aProperty == kNC_VisitCount) || (aProperty == kNC_AgeInDays) || (aProperty == kNC_Name) || (aProperty == kNC_NameSort) || (aProperty == kNC_Hostname) || (aProperty == kNC_Referrer) || (aProperty == kNC_URL)) { const char* uri; rv = aSource->GetValueConst(&uri); if (NS_FAILED(rv)) return rv; // url is self-referential, so we'll always just return itself // however, don't return the URLs of find resources if (aProperty == kNC_URL && !IsFindResource(aSource)) { nsCOMPtr uriLiteral; rv = gRDFService->GetLiteral(NS_ConvertUTF8toUTF16(uri).get(), getter_AddRefs(uriLiteral)); if (NS_FAILED(rv)) return(rv); *aTarget = uriLiteral; NS_ADDREF(*aTarget); return NS_OK; } // find URIs are special if (((aProperty == kNC_Name) || (aProperty == kNC_NameSort)) && IsFindResource(aSource)) { // for sorting, we sort by uri, so just return the URI as a literal if (aProperty == kNC_NameSort) { nsCOMPtr uriLiteral; rv = gRDFService->GetLiteral(NS_ConvertUTF8toUTF16(uri).get(), getter_AddRefs(uriLiteral)); if (NS_FAILED(rv)) return(rv); *aTarget = uriLiteral; NS_ADDREF(*aTarget); return NS_OK; } else return GetFindUriName(uri, aTarget); } // ok, we got this far, so we have to retrieve something from // the row in the database nsCOMPtr row; rv = FindRow(kToken_URLColumn, uri, getter_AddRefs(row)); if (NS_FAILED(rv)) return NS_RDF_NO_VALUE; mdb_err err; // ...and then depending on the property they want, we'll pull the // cell they want out of it. if (aProperty == kNC_Date || aProperty == kNC_FirstVisitDate) { // Last visit date PRTime i; if (aProperty == kNC_Date) rv = GetRowValue(row, kToken_LastVisitDateColumn, &i); else rv = GetRowValue(row, kToken_FirstVisitDateColumn, &i); if (NS_FAILED(rv)) return rv; nsCOMPtr date; rv = gRDFService->GetDateLiteral(i, getter_AddRefs(date)); if (NS_FAILED(rv)) return rv; return CallQueryInterface(date, aTarget); } else if (aProperty == kNC_VisitCount) { mdbYarn yarn; err = row->AliasCellYarn(mEnv, kToken_VisitCountColumn, &yarn); if (err != 0) return NS_ERROR_FAILURE; PRInt32 visitCount = 0; rv = GetRowValue(row, kToken_VisitCountColumn, &visitCount); if (NS_FAILED(rv) || visitCount <1) visitCount = 1; // assume we've visited at least once nsCOMPtr visitCountLiteral; rv = gRDFService->GetIntLiteral(visitCount, getter_AddRefs(visitCountLiteral)); if (NS_FAILED(rv)) return rv; return CallQueryInterface(visitCountLiteral, aTarget); } else if (aProperty == kNC_AgeInDays) { PRTime lastVisitDate; rv = GetRowValue(row, kToken_LastVisitDateColumn, &lastVisitDate); if (NS_FAILED(rv)) return rv; PRInt32 days = GetAgeInDays(lastVisitDate); nsCOMPtr ageLiteral; rv = gRDFService->GetIntLiteral(days, getter_AddRefs(ageLiteral)); if (NS_FAILED(rv)) return rv; *aTarget = ageLiteral; NS_ADDREF(*aTarget); return NS_OK; } else if (aProperty == kNC_Name || aProperty == kNC_NameSort) { // Site name (i.e., page title) nsAutoString title; rv = GetRowValue(row, kToken_NameColumn, title); if (NS_FAILED(rv) || title.IsEmpty()) { // yank out the filename from the url, use that nsCOMPtr aUri; rv = NS_NewURI(getter_AddRefs(aUri), uri); if (NS_FAILED(rv)) return rv; nsCOMPtr urlObj(do_QueryInterface(aUri)); if (!urlObj) return NS_ERROR_FAILURE; nsCAutoString filename; rv = urlObj->GetFileName(filename); if (NS_FAILED(rv) || filename.IsEmpty()) { // ok fine. we'll use the file path. then we give up! rv = urlObj->GetPath(filename); if (strcmp(filename.get(), "/") == 0) { // if the top of a site does not have a title // (common for redirections) then return the hostname rv = GetRowValue(row, kToken_HostnameColumn, filename); } } if (NS_FAILED(rv)) return rv; // assume the url is in UTF8 AppendUTF8toUTF16(filename, title); } if (NS_FAILED(rv)) return rv; nsCOMPtr name; rv = gRDFService->GetLiteral(title.get(), getter_AddRefs(name)); if (NS_FAILED(rv)) return rv; return CallQueryInterface(name, aTarget); } else if (aProperty == kNC_Hostname || aProperty == kNC_Referrer) { nsCAutoString str; if (aProperty == kNC_Hostname) rv = GetRowValue(row, kToken_HostnameColumn, str); else if (aProperty == kNC_Referrer) rv = GetRowValue(row, kToken_ReferrerColumn, str); if (NS_FAILED(rv)) return rv; // Avoid trying to create a resource from an empty string, which // will raise an exception if (str.IsEmpty()) return NS_RDF_NO_VALUE; nsCOMPtr resource; rv = gRDFService->GetResource(str, getter_AddRefs(resource)); if (NS_FAILED(rv)) return rv; return CallQueryInterface(resource, aTarget); } else { NS_NOTREACHED("huh, how'd I get here?"); } } return NS_RDF_NO_VALUE; } void nsGlobalHistory::Sync() { if (mDirty) Flush(); mDirty = PR_FALSE; mSyncTimer = nsnull; } void nsGlobalHistory::ExpireNow() { mNowValid = PR_FALSE; mExpireNowTimer = nsnull; } // when we're dirty, we want to make sure we sync again soon, // but make sure that we don't keep syncing if someone is surfing // a lot, so cancel the existing timer if any is still waiting to fire nsresult nsGlobalHistory::SetDirty() { nsresult rv; if (mSyncTimer) mSyncTimer->Cancel(); if (!mSyncTimer) { mSyncTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); if (NS_FAILED(rv)) return rv; } mDirty = PR_TRUE; mSyncTimer->InitWithFuncCallback(fireSyncTimer, this, HISTORY_SYNC_TIMEOUT, nsITimer::TYPE_ONE_SHOT); return NS_OK; } // hack to avoid calling PR_Now() too often, as is the case when // we're asked the ageindays of many history entries in a row PRTime nsGlobalHistory::GetNow() { if (!mNowValid) { // not dirty, mLastNow is crufty mLastNow = PR_Now(); // we also cache our offset from GMT, to optimize NormalizeTime() // note that this cache is only valid if GetNow() is called before // NormalizeTime(), but that is always the case here. PRExplodedTime explodedNow; PR_ExplodeTime(mLastNow, PR_LocalTimeParameters, &explodedNow); mCachedGMTOffset = nsInt64(explodedNow.tm_params.tp_gmt_offset) * nsInt64((PRUint32)PR_USEC_PER_SEC) + nsInt64(explodedNow.tm_params.tp_dst_offset) * nsInt64((PRUint32)PR_USEC_PER_SEC); mNowValid = PR_TRUE; if (!mExpireNowTimer) mExpireNowTimer = do_CreateInstance("@mozilla.org/timer;1"); if (mExpireNowTimer) mExpireNowTimer->InitWithFuncCallback(expireNowTimer, this, HISTORY_EXPIRE_NOW_TIMEOUT, nsITimer::TYPE_ONE_SHOT); } return mLastNow; } NS_IMETHODIMP nsGlobalHistory::GetTargets(nsIRDFResource* aSource, nsIRDFResource* aProperty, PRBool aTruthValue, nsISimpleEnumerator** aTargets) { NS_PRECONDITION(aSource != nsnull, "null ptr"); if (! aSource) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aProperty != nsnull, "null ptr"); if (! aProperty) return NS_ERROR_NULL_POINTER; if (!aTruthValue) return NS_NewEmptyEnumerator(aTargets); NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); // list all URLs off the root if ((aSource == kNC_HistoryRoot) && (aProperty == kNC_child)) { URLEnumerator* result = new URLEnumerator(kToken_URLColumn, kToken_HiddenColumn); if (! result) return NS_ERROR_OUT_OF_MEMORY; nsresult rv; rv = result->Init(mEnv, mTable); if (NS_FAILED(rv)) return rv; *aTargets = result; NS_ADDREF(*aTargets); return NS_OK; } else if ((aSource == kNC_HistoryByDate) && (aProperty == kNC_child)) { return GetRootDayQueries(aTargets); } else if (aProperty == kNC_child && IsFindResource(aSource)) { return CreateFindEnumerator(aSource, aTargets); } else if ((aProperty == kNC_Date) || (aProperty == kNC_FirstVisitDate) || (aProperty == kNC_VisitCount) || (aProperty == kNC_AgeInDays) || (aProperty == kNC_Name) || (aProperty == kNC_Hostname) || (aProperty == kNC_Referrer)) { nsresult rv; nsCOMPtr target; rv = GetTarget(aSource, aProperty, aTruthValue, getter_AddRefs(target)); if (NS_FAILED(rv)) return rv; if (rv == NS_OK) { return NS_NewSingletonEnumerator(aTargets, target); } } // we've already answered the queries from the root, so we must be // looking for real entries return NS_NewEmptyEnumerator(aTargets); } NS_IMETHODIMP nsGlobalHistory::Assert(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aTarget, PRBool aTruthValue) { // History cannot be modified return NS_RDF_ASSERTION_REJECTED; } NS_IMETHODIMP nsGlobalHistory::Unassert(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aTarget) { NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); // translate into an appropriate removehistory call nsresult rv; if ((aSource == kNC_HistoryRoot || aSource == kNC_HistoryByDate || IsFindResource(aSource)) && aProperty == kNC_child) { nsCOMPtr resource = do_QueryInterface(aTarget, &rv); if (NS_FAILED(rv)) return NS_RDF_ASSERTION_REJECTED; const char* targetUrl; rv = resource->GetValueConst(&targetUrl); if (NS_FAILED(rv)) return NS_RDF_ASSERTION_REJECTED; if (IsFindResource(resource)) { // convert uri to a query searchQuery query; rv = FindUrlToSearchQuery(targetUrl, query); if (NS_FAILED(rv)) return NS_RDF_ASSERTION_REJECTED; matchQuery_t matchQuery; matchQuery.history = this; matchQuery.query = &query; rv = RemoveMatchingRows(matchQueryCallback, (void*)&matchQuery, PR_TRUE); FreeSearchQuery(query); if (NS_FAILED(rv)) return NS_RDF_ASSERTION_REJECTED; // if there are batches in progress, we don't want to notify // observers that we're deleting items. the caller promises // to handle whatever UI updating is necessary when we're finished. if (!mBatchesInProgress) NotifyUnassert(aSource, aProperty, aTarget); return NS_OK; } // ignore any error rv = RemovePageInternal(nsDependentCString(targetUrl)); if (NS_FAILED(rv)) return NS_RDF_ASSERTION_REJECTED; return NS_OK; } return NS_RDF_ASSERTION_REJECTED; } NS_IMETHODIMP nsGlobalHistory::Change(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aOldTarget, nsIRDFNode* aNewTarget) { return NS_RDF_ASSERTION_REJECTED; } NS_IMETHODIMP nsGlobalHistory::Move(nsIRDFResource* aOldSource, nsIRDFResource* aNewSource, nsIRDFResource* aProperty, nsIRDFNode* aTarget) { return NS_RDF_ASSERTION_REJECTED; } NS_IMETHODIMP nsGlobalHistory::HasAssertion(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aTarget, PRBool aTruthValue, PRBool* aHasAssertion) { NS_PRECONDITION(aSource != nsnull, "null ptr"); if (! aSource) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aProperty != nsnull, "null ptr"); if (! aProperty) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aTarget != nsnull, "null ptr"); if (! aTarget) return NS_ERROR_NULL_POINTER; // Only "positive" assertions here. if (!aTruthValue) { *aHasAssertion = PR_FALSE; return NS_OK; } NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsresult rv; // answer if a specific row matches a find URI // // at some point, we should probably match groupby= findURIs with // findURIs that match all their criteria // nsCOMPtr target = do_QueryInterface(aTarget); if (target && aProperty == kNC_child && IsFindResource(aSource) && !IsFindResource(target)) { const char *uri; rv = target->GetValueConst(&uri); if (NS_FAILED(rv)) return rv; searchQuery query; FindUrlToSearchQuery(uri, query); nsCOMPtr row; rv = FindRow(kToken_URLColumn, uri, getter_AddRefs(row)); // not even in history. don't bother trying if (NS_FAILED(rv) || HasCell(mEnv, row, kToken_HiddenColumn)) { *aHasAssertion = PR_FALSE; return NS_OK; } *aHasAssertion = RowMatches(row, &query); FreeSearchQuery(query); return NS_OK; } // Do |GetTargets()| and grovel through the results to see if we // have the assertion. // // XXX *AHEM*, this could be implemented much more efficiently... nsCOMPtr targets; rv = GetTargets(aSource, aProperty, aTruthValue, getter_AddRefs(targets)); if (NS_FAILED(rv)) return rv; while (1) { PRBool hasMore; rv = targets->HasMoreElements(&hasMore); if (NS_FAILED(rv)) return rv; if (! hasMore) break; nsCOMPtr isupports; rv = targets->GetNext(getter_AddRefs(isupports)); if (NS_FAILED(rv)) return rv; nsCOMPtr node = do_QueryInterface(isupports); if (node.get() == aTarget) { *aHasAssertion = PR_TRUE; return NS_OK; } } *aHasAssertion = PR_FALSE; return NS_OK; } NS_IMETHODIMP nsGlobalHistory::AddObserver(nsIRDFObserver* aObserver) { NS_PRECONDITION(aObserver != nsnull, "null ptr"); if (! aObserver) return NS_ERROR_NULL_POINTER; if (! mObservers) { nsresult rv; rv = NS_NewISupportsArray(getter_AddRefs(mObservers)); if (NS_FAILED(rv)) return rv; } mObservers->AppendElement(aObserver); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::RemoveObserver(nsIRDFObserver* aObserver) { NS_PRECONDITION(aObserver != nsnull, "null ptr"); if (! aObserver) return NS_ERROR_NULL_POINTER; if (! mObservers) return NS_OK; mObservers->RemoveElement(aObserver); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::HasArcIn(nsIRDFNode *aNode, nsIRDFResource *aArc, PRBool *result) { NS_PRECONDITION(aNode != nsnull, "null ptr"); if (! aNode) return NS_ERROR_NULL_POINTER; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsCOMPtr resource = do_QueryInterface(aNode); if (resource && IsURLInHistory(resource)) { *result = (aArc == kNC_child); } else { *result = PR_FALSE; } return NS_OK; } NS_IMETHODIMP nsGlobalHistory::HasArcOut(nsIRDFResource *aSource, nsIRDFResource *aArc, PRBool *result) { NS_PRECONDITION(aSource != nsnull, "null ptr"); if (! aSource) return NS_ERROR_NULL_POINTER; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); if ((aSource == kNC_HistoryRoot) || (aSource == kNC_HistoryByDate)) { *result = (aArc == kNC_child); } else if (IsFindResource(aSource)) { // we handle children of find urls *result = (aArc == kNC_child || aArc == kNC_Name || aArc == kNC_NameSort); } else if (IsURLInHistory(aSource)) { // If the URL is in the history, then it'll have all the // appropriate attributes. *result = (aArc == kNC_Date || aArc == kNC_FirstVisitDate || aArc == kNC_VisitCount || aArc == kNC_Name || aArc == kNC_Hostname || aArc == kNC_Referrer); } else { *result = PR_FALSE; } return NS_OK; } NS_IMETHODIMP nsGlobalHistory::ArcLabelsIn(nsIRDFNode* aNode, nsISimpleEnumerator** aLabels) { NS_PRECONDITION(aNode != nsnull, "null ptr"); if (! aNode) return NS_ERROR_NULL_POINTER; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsCOMPtr resource = do_QueryInterface(aNode); if (resource && IsURLInHistory(resource)) { return NS_NewSingletonEnumerator(aLabels, kNC_child); } else { return NS_NewEmptyEnumerator(aLabels); } } NS_IMETHODIMP nsGlobalHistory::ArcLabelsOut(nsIRDFResource* aSource, nsISimpleEnumerator** aLabels) { NS_PRECONDITION(aSource != nsnull, "null ptr"); if (! aSource) return NS_ERROR_NULL_POINTER; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsresult rv; if ((aSource == kNC_HistoryRoot) || (aSource == kNC_HistoryByDate)) { return NS_NewSingletonEnumerator(aLabels, kNC_child); } else if (IsURLInHistory(aSource)) { // If the URL is in the history, then it'll have all the // appropriate attributes. nsCOMPtr array; rv = NS_NewISupportsArray(getter_AddRefs(array)); if (NS_FAILED(rv)) return rv; array->AppendElement(kNC_Date); array->AppendElement(kNC_FirstVisitDate); array->AppendElement(kNC_VisitCount); array->AppendElement(kNC_Name); array->AppendElement(kNC_Hostname); array->AppendElement(kNC_Referrer); return NS_NewArrayEnumerator(aLabels, array); } else if (IsFindResource(aSource)) { nsCOMPtr array; rv = NS_NewISupportsArray(getter_AddRefs(array)); if (NS_FAILED(rv)) return rv; array->AppendElement(kNC_child); array->AppendElement(kNC_Name); array->AppendElement(kNC_NameSort); return NS_NewArrayEnumerator(aLabels, array); } else { return NS_NewEmptyEnumerator(aLabels); } } NS_IMETHODIMP nsGlobalHistory::GetAllCmds(nsIRDFResource* aSource, nsISimpleEnumerator/**/** aCommands) { return NS_NewEmptyEnumerator(aCommands); } NS_IMETHODIMP nsGlobalHistory::IsCommandEnabled(nsISupportsArray/**/* aSources, nsIRDFResource* aCommand, nsISupportsArray/**/* aArguments, PRBool* aResult) { NS_NOTYETIMPLEMENTED("sorry"); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsGlobalHistory::DoCommand(nsISupportsArray/**/* aSources, nsIRDFResource* aCommand, nsISupportsArray/**/* aArguments) { NS_NOTYETIMPLEMENTED("sorry"); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsGlobalHistory::GetAllResources(nsISimpleEnumerator** aResult) { NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); URLEnumerator* result = new URLEnumerator(kToken_URLColumn, kToken_HiddenColumn); if (! result) return NS_ERROR_OUT_OF_MEMORY; nsresult rv; rv = result->Init(mEnv, mTable); if (NS_FAILED(rv)) return rv; *aResult = result; NS_ADDREF(*aResult); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::BeginUpdateBatch() { nsresult rv = NS_OK; ++mBatchesInProgress; // we could call mObservers->EnumerateForwards() here // to save the addref/release on each observer, but // it's unlikely that anyone but the tree builder // is observing us if (mObservers) { PRUint32 count; rv = mObservers->Count(&count); if (NS_FAILED(rv)) return rv; for (PRInt32 i = 0; i < PRInt32(count); ++i) { nsIRDFObserver* observer = static_cast(mObservers->ElementAt(i)); NS_ASSERTION(observer != nsnull, "null ptr"); if (! observer) continue; rv = observer->OnBeginUpdateBatch(this); NS_RELEASE(observer); } } return rv; } NS_IMETHODIMP nsGlobalHistory::EndUpdateBatch() { nsresult rv = NS_OK; --mBatchesInProgress; // we could call mObservers->EnumerateForwards() here // to save the addref/release on each observer, but // it's unlikely that anyone but the tree builder // is observing us if (mObservers) { PRUint32 count; rv = mObservers->Count(&count); if (NS_FAILED(rv)) return rv; for (PRInt32 i = 0; i < PRInt32(count); ++i) { nsIRDFObserver* observer = static_cast(mObservers->ElementAt(i)); NS_ASSERTION(observer != nsnull, "null ptr"); if (! observer) continue; rv = observer->OnEndUpdateBatch(this); NS_RELEASE(observer); } } return rv; } //////////////////////////////////////////////////////////////////////// // nsIRDFRemoteDataSource NS_IMETHODIMP nsGlobalHistory::GetLoaded(PRBool* _result) { *_result = PR_TRUE; return NS_OK; } NS_IMETHODIMP nsGlobalHistory::Init(const char* aURI) { return(NS_OK); } NS_IMETHODIMP nsGlobalHistory::Refresh(PRBool aBlocking) { return(NS_OK); } NS_IMETHODIMP nsGlobalHistory::Flush() { return Commit(kLargeCommit); } NS_IMETHODIMP nsGlobalHistory::FlushTo(const char *aURI) { // Do not ever implement this (security) return(NS_ERROR_NOT_IMPLEMENTED); } //---------------------------------------------------------------------- // // nsGlobalHistory // // Miscellaneous implementation methods // nsresult nsGlobalHistory::Init() { nsresult rv; // we'd like to get this pref when we need it, but at that point, // we can't get the pref service. register a pref observer so we update // if the pref changes if (!gPrefBranch) { nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = prefs->GetBranch(PREF_BRANCH_BASE, &gPrefBranch); NS_ENSURE_SUCCESS(rv, rv); } gPrefBranch->GetIntPref(PREF_BROWSER_HISTORY_EXPIRE_DAYS, &mExpireDays); gPrefBranch->GetBoolPref(PREF_AUTOCOMPLETE_ONLY_TYPED, &mAutocompleteOnlyTyped); nsCOMPtr pbi = do_QueryInterface(gPrefBranch); if (pbi) { pbi->AddObserver(PREF_AUTOCOMPLETE_ONLY_TYPED, this, PR_FALSE); pbi->AddObserver(PREF_BROWSER_HISTORY_EXPIRE_DAYS, this, PR_FALSE); } if (gRefCnt++ == 0) { rv = CallGetService(kRDFServiceCID, &gRDFService); NS_ASSERTION(NS_SUCCEEDED(rv), "unable to get RDF service"); if (NS_FAILED(rv)) return rv; gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "Page"), &kNC_Page); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "Date"), &kNC_Date); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "FirstVisitDate"), &kNC_FirstVisitDate); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "VisitCount"), &kNC_VisitCount); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "AgeInDays"), &kNC_AgeInDays); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "Name"), &kNC_Name); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "Name?sort=true"), &kNC_NameSort); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "Hostname"), &kNC_Hostname); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "Referrer"), &kNC_Referrer); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "child"), &kNC_child); gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "URL"), &kNC_URL); gRDFService->GetResource(NS_LITERAL_CSTRING("NC:HistoryRoot"), &kNC_HistoryRoot); gRDFService->GetResource(NS_LITERAL_CSTRING("NC:HistoryByDate"), &kNC_HistoryByDate); } // register this as a named data source with the RDF service rv = gRDFService->RegisterDataSource(this, PR_FALSE); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr bundleService = do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); if (NS_SUCCEEDED(rv)) { rv = bundleService->CreateBundle("chrome://communicator/locale/history/history.properties", getter_AddRefs(mBundle)); } // register to observe profile changes nsCOMPtr observerService = do_GetService("@mozilla.org/observer-service;1", &rv); NS_ASSERTION(observerService, "failed to get observer service"); if (observerService) { observerService->AddObserver(this, "profile-before-change", PR_TRUE); observerService->AddObserver(this, "profile-do-change", PR_TRUE); } return NS_OK; } nsresult nsGlobalHistory::OpenDB() { nsresult rv; if (mStore) return NS_OK; nsCOMPtr historyFile; rv = NS_GetSpecialDirectory(NS_APP_HISTORY_50_FILE, getter_AddRefs(historyFile)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr factoryfactory = do_GetService(NS_MORK_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = factoryfactory->GetMdbFactory(&gMdbFactory); NS_ENSURE_SUCCESS(rv, rv); mdb_err err; err = gMdbFactory->MakeEnv(nsnull, &mEnv); mEnv->SetAutoClear(PR_TRUE); NS_ASSERTION((err == 0), "unable to create mdb env"); if (err != 0) return NS_ERROR_FAILURE; // MDB requires native file paths nsCAutoString filePath; rv = historyFile->GetNativePath(filePath); NS_ENSURE_SUCCESS(rv, rv); PRBool exists = PR_TRUE; historyFile->Exists(&exists); if (!exists || NS_FAILED(rv = OpenExistingFile(gMdbFactory, filePath.get()))) { // we couldn't open the file, so it's either corrupt or doesn't exist. // attempt to delete the file, but ignore the error historyFile->Remove(PR_FALSE); rv = OpenNewFile(gMdbFactory, filePath.get()); } NS_ENSURE_SUCCESS(rv, rv); // get the initial filesize. Used in Commit() to determine type of commit. rv = historyFile->GetFileSize(&mFileSizeOnDisk); if (NS_FAILED(rv)) { LL_I2L(mFileSizeOnDisk, 0); } // See if we need to byte-swap. InitByteOrder(PR_FALSE); return NS_OK; } nsresult nsGlobalHistory::OpenExistingFile(nsIMdbFactory *factory, const char *filePath) { mdb_err err; nsresult rv; mdb_bool canopen = 0; mdbYarn outfmt = { nsnull, 0, 0, 0, 0, nsnull }; nsIMdbHeap* dbHeap = 0; mdb_bool dbFrozen = mdbBool_kFalse; // not readonly, we want modifiable nsCOMPtr oldFile; // ensures file is released err = factory->OpenOldFile(mEnv, dbHeap, filePath, dbFrozen, getter_AddRefs(oldFile)); // don't assert, the file might just not be there if ((err !=0) || !oldFile) return NS_ERROR_FAILURE; err = factory->CanOpenFilePort(mEnv, oldFile, // the file to investigate &canopen, &outfmt); // XXX possible that format out of date, in which case we should // just re-write the file. if ((err !=0) || !canopen) return NS_ERROR_FAILURE; nsIMdbThumb* thumb = nsnull; mdbOpenPolicy policy = { { 0, 0 }, 0, 0 }; err = factory->OpenFileStore(mEnv, dbHeap, oldFile, &policy, &thumb); if ((err !=0) || !thumb) return NS_ERROR_FAILURE; mdb_count total; mdb_count current; mdb_bool done; mdb_bool broken; do { err = thumb->DoMore(mEnv, &total, ¤t, &done, &broken); } while ((err == 0) && !broken && !done); if ((err == 0) && done) { err = factory->ThumbToOpenStore(mEnv, thumb, &mStore); } NS_IF_RELEASE(thumb); if (err != 0) return NS_ERROR_FAILURE; rv = CreateTokens(); NS_ENSURE_SUCCESS(rv, rv); mdbOid oid = { kToken_HistoryRowScope, 1 }; err = mStore->GetTable(mEnv, &oid, &mTable); NS_ENSURE_TRUE(err == 0, NS_ERROR_FAILURE); if (!mTable) { NS_WARNING("Your history file is somehow corrupt.. deleting it."); return NS_ERROR_FAILURE; } err = mTable->GetMetaRow(mEnv, &oid, nsnull, getter_AddRefs(mMetaRow)); if (err != 0) NS_WARNING("Could not get meta row\n"); CheckHostnameEntries(); if (err != 0) return NS_ERROR_FAILURE; return NS_OK; } nsresult nsGlobalHistory::OpenNewFile(nsIMdbFactory *factory, const char *filePath) { nsresult rv; mdb_err err; nsIMdbHeap* dbHeap = 0; nsCOMPtr newFile; // ensures file is released err = factory->CreateNewFile(mEnv, dbHeap, filePath, getter_AddRefs(newFile)); if ((err != 0) || !newFile) return NS_ERROR_FAILURE; mdbOpenPolicy policy = { { 0, 0 }, 0, 0 }; err = factory->CreateNewFileStore(mEnv, dbHeap, newFile, &policy, &mStore); if (err != 0) return NS_ERROR_FAILURE; rv = CreateTokens(); NS_ENSURE_SUCCESS(rv, rv); // Create the one and only table in the history db err = mStore->NewTable(mEnv, kToken_HistoryRowScope, kToken_HistoryKind, PR_TRUE, nsnull, &mTable); if (err != 0) return NS_ERROR_FAILURE; if (!mTable) return NS_ERROR_FAILURE; // Create the meta row. mdbOid oid = { kToken_HistoryRowScope, 1 }; err = mTable->GetMetaRow(mEnv, &oid, nsnull, getter_AddRefs(mMetaRow)); if (err != 0) NS_WARNING("Could not get meta row\n"); // Force a commit now to get it written out. nsCOMPtr thumb; err = mStore->LargeCommit(mEnv, getter_AddRefs(thumb)); if (err != 0) return NS_ERROR_FAILURE; mdb_count total; mdb_count current; mdb_bool done; mdb_bool broken; do { err = thumb->DoMore(mEnv, &total, ¤t, &done, &broken); } while ((err == 0) && !broken && !done); if ((err != 0) || !done) return NS_ERROR_FAILURE; return NS_OK; } // Set the history file byte order if necessary, and determine if // we need to byte-swap Unicode values. // If the force argument is true, the file byte order will be set // to that of this machine. nsresult nsGlobalHistory::InitByteOrder(PRBool aForce) { #ifdef IS_LITTLE_ENDIAN NS_NAMED_LITERAL_CSTRING(machine_byte_order, "LE"); #endif #ifdef IS_BIG_ENDIAN NS_NAMED_LITERAL_CSTRING(machine_byte_order, "BE"); #endif nsXPIDLCString file_byte_order; nsresult rv = NS_OK; if (!aForce) rv = GetByteOrder(getter_Copies(file_byte_order)); if (aForce || NS_FAILED(rv) || !(file_byte_order.EqualsLiteral("BE") || file_byte_order.EqualsLiteral("LE"))) { // Byte order is not yet set, or needs to be reset; initialize it. mReverseByteOrder = PR_FALSE; rv = SaveByteOrder(machine_byte_order.get()); if (NS_FAILED(rv)) return rv; } else mReverseByteOrder = !file_byte_order.Equals(machine_byte_order); return NS_OK; } // break the uri down into a search query, and pass off to // SearchEnumerator nsresult nsGlobalHistory::CreateFindEnumerator(nsIRDFResource *aSource, nsISimpleEnumerator **aResult) { nsresult rv; // make sure this was a find query if (!IsFindResource(aSource)) return NS_ERROR_FAILURE; const char* uri; rv = aSource->GetValueConst(&uri); if (NS_FAILED(rv)) return rv; // convert uri to a query searchQuery* query = new searchQuery; if (!query) return NS_ERROR_OUT_OF_MEMORY; FindUrlToSearchQuery(uri, *query); // the enumerator will take ownership of the query SearchEnumerator *result = new SearchEnumerator(query, this); if (!result) return NS_ERROR_OUT_OF_MEMORY; rv = result->Init(mEnv, mTable); if (NS_FAILED(rv)) return rv; // return the value *aResult = result; NS_ADDREF(*aResult); return NS_OK; } // for each row, we need to parse out the hostname from the url // then store it in a column nsresult nsGlobalHistory::CheckHostnameEntries() { nsresult rv = NS_OK; mdb_err err; nsCOMPtr cursor; nsCOMPtr row; err = mTable->GetTableRowCursor(mEnv, -1, getter_AddRefs(cursor)); if (err != 0) return NS_ERROR_FAILURE; int marker; err = mTable->StartBatchChangeHint(mEnv, &marker); NS_ASSERTION(err == 0, "unable to start batch"); if (err != 0) return NS_ERROR_FAILURE; mdb_pos pos; err = cursor->NextRow(mEnv, getter_AddRefs(row), &pos); if (err != 0) return NS_ERROR_FAILURE; // comment out this code to rebuild the hostlist at startup #if 1 // bail early if the first row has a hostname if (row) { nsCAutoString hostname; rv = GetRowValue(row, kToken_HostnameColumn, hostname); if (NS_SUCCEEDED(rv) && !hostname.IsEmpty()) return NS_OK; } #endif // cached variables used in the loop nsCAutoString url; nsXPIDLCString hostname; nsCOMPtr ioService = do_GetService(NS_IOSERVICE_CONTRACTID); if (!ioService) return NS_ERROR_FAILURE; while (row) { #if 0 rv = GetRowValue(row, kToken_URLColumn, url); if (NS_FAILED(rv)) break; ioService->ExtractUrlPart(url, nsIIOService::url_Host, 0, 0, getter_Copies(hostname)); SetRowValue(row, kToken_HostnameColumn, hostname); #endif // to be turned on when we're confident in mork's ability // to handle yarn forms properly #if 0 nsAutoString title; rv = GetRowValue(row, kToken_NameColumn, title); // reencode back into UTF8 if (NS_SUCCEEDED(rv)) SetRowValue(row, kToken_NameColumn, title.get()); #endif cursor->NextRow(mEnv, getter_AddRefs(row), &pos); } // Finish the batch. err = mTable->EndBatchChangeHint(mEnv, &marker); NS_ASSERTION(err == 0, "error ending batch"); return rv; } nsresult nsGlobalHistory::CreateTokens() { mdb_err err; NS_PRECONDITION(mStore != nsnull, "not initialized"); if (! mStore) return NS_ERROR_NOT_INITIALIZED; err = mStore->StringToToken(mEnv, "ns:history:db:row:scope:history:all", &kToken_HistoryRowScope); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "ns:history:db:table:kind:history", &kToken_HistoryKind); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "URL", &kToken_URLColumn); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "Referrer", &kToken_ReferrerColumn); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "LastVisitDate", &kToken_LastVisitDateColumn); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "FirstVisitDate", &kToken_FirstVisitDateColumn); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "VisitCount", &kToken_VisitCountColumn); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "Name", &kToken_NameColumn); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "Hostname", &kToken_HostnameColumn); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "Hidden", &kToken_HiddenColumn); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "Typed", &kToken_TypedColumn); if (err != 0) return NS_ERROR_FAILURE; err = mStore->StringToToken(mEnv, "GeckoFlags", &kToken_GeckoFlagsColumn); if (err != 0) return NS_ERROR_FAILURE; // meta-data tokens err = mStore->StringToToken(mEnv, "LastPageVisited", &kToken_LastPageVisited); err = mStore->StringToToken(mEnv, "ByteOrder", &kToken_ByteOrder); return NS_OK; } nsresult nsGlobalHistory::Commit(eCommitType commitType) { if (!mStore || !mTable) return NS_OK; nsresult err = NS_OK; nsCOMPtr thumb; if (commitType == kLargeCommit || commitType == kSessionCommit) { mdb_percent outActualWaste = 0; mdb_bool outShould; if (mStore) { // check how much space would be saved by doing a compress commit. // If it's more than 30%, go for it. // N.B. - I'm not sure this calls works in Mork for all cases. err = mStore->ShouldCompress(mEnv, 30, &outActualWaste, &outShould); if (NS_SUCCEEDED(err) && outShould) { commitType = kCompressCommit; } else { mdb_count count; err = mTable->GetCount(mEnv, &count); // Since Mork's shouldCompress doesn't work, we need to look // at the file size and the number of rows, and make a stab // at guessing if we've got a lot of deleted rows. The file // size is the size when we opened the db, and we really want // it to be the size after we've written out the file, // but I think this is a good enough approximation. if (count > 0) { PRInt64 numRows; PRInt64 bytesPerRow; PRInt64 desiredAvgRowSize; LL_UI2L(numRows, count); LL_DIV(bytesPerRow, mFileSizeOnDisk, numRows); LL_I2L(desiredAvgRowSize, 400); if (LL_CMP(bytesPerRow, >, desiredAvgRowSize)) commitType = kCompressCommit; } } } } switch (commitType) { case kLargeCommit: err = mStore->LargeCommit(mEnv, getter_AddRefs(thumb)); break; case kSessionCommit: err = mStore->SessionCommit(mEnv, getter_AddRefs(thumb)); break; case kCompressCommit: err = mStore->CompressCommit(mEnv, getter_AddRefs(thumb)); break; } if (err == 0) { mdb_count total; mdb_count current; mdb_bool done; mdb_bool broken; do { err = thumb->DoMore(mEnv, &total, ¤t, &done, &broken); } while ((err == 0) && !broken && !done); } if (err != 0) // mork doesn't return NS error codes. Yet. return NS_ERROR_FAILURE; else return NS_OK; } // if notify is true, we'll notify rdf of deleted rows. // If we're shutting down history, then (maybe?) we don't // need or want to notify rdf. nsresult nsGlobalHistory::ExpireEntries(PRBool notify) { PRTime expirationDate; PRInt64 microSecondsPerSecond, secondsInDays, microSecondsInExpireDays; LL_I2L(microSecondsPerSecond, PR_USEC_PER_SEC); LL_UI2L(secondsInDays, 60 * 60 * 24 * mExpireDays); LL_MUL(microSecondsInExpireDays, secondsInDays, microSecondsPerSecond); LL_SUB(expirationDate, GetNow(), microSecondsInExpireDays); matchExpiration_t expiration; expiration.history = this; expiration.expirationDate = &expirationDate; return RemoveMatchingRows(matchExpirationCallback, (void *)&expiration, notify); } nsresult nsGlobalHistory::CloseDB() { if (!mStore) return NS_OK; mResults.Clear(); mSearchString.Truncate(); mdb_err err; ExpireEntries(PR_FALSE /* don't notify */); err = Commit(kSessionCommit); // order is important here - logically smallest objects first mMetaRow = nsnull; if (mTable) mTable->Release(); mStore->Release(); if (mEnv) mEnv->Release(); mTable = nsnull; mEnv = nsnull; mStore = nsnull; return NS_OK; } nsresult nsGlobalHistory::FindRow(mdb_column aCol, const char *aValue, nsIMdbRow **aResult) { if (! mStore) return NS_ERROR_NOT_INITIALIZED; mdb_err err; PRInt32 len = PL_strlen(aValue); mdbYarn yarn = { (void*) aValue, len, len, 0, 0, nsnull }; mdbOid rowId; nsCOMPtr row; if (aResult) { err = mStore->FindRow(mEnv, kToken_HistoryRowScope, aCol, &yarn, &rowId, getter_AddRefs(row)); if (!row) return NS_ERROR_NOT_AVAILABLE; } else { err = mStore->FindRow(mEnv, kToken_HistoryRowScope, aCol, &yarn, &rowId, nsnull); } // make sure it's actually stored in the main table mdb_bool hasRow; mTable->HasOid(mEnv, &rowId, &hasRow); if (!hasRow) return NS_ERROR_NOT_AVAILABLE; if (aResult) { *aResult = row; (*aResult)->AddRef(); } return NS_OK; } PRBool nsGlobalHistory::IsURLInHistory(nsIRDFResource* aResource) { nsresult rv; const char* url; rv = aResource->GetValueConst(&url); if (NS_FAILED(rv)) return PR_FALSE; rv = FindRow(kToken_URLColumn, url, nsnull); return (NS_SUCCEEDED(rv)) ? PR_TRUE : PR_FALSE; } nsresult nsGlobalHistory::NotifyAssert(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aValue) { nsresult rv; if (mObservers) { PRUint32 count; rv = mObservers->Count(&count); if (NS_FAILED(rv)) return rv; for (PRInt32 i = 0; i < PRInt32(count); ++i) { nsIRDFObserver* observer = static_cast(mObservers->ElementAt(i)); NS_ASSERTION(observer != nsnull, "null ptr"); if (! observer) continue; rv = observer->OnAssert(this, aSource, aProperty, aValue); NS_RELEASE(observer); } } return NS_OK; } nsresult nsGlobalHistory::NotifyUnassert(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aValue) { nsresult rv; if (mObservers) { PRUint32 count; rv = mObservers->Count(&count); if (NS_FAILED(rv)) return rv; for (PRInt32 i = 0; i < PRInt32(count); ++i) { nsIRDFObserver* observer = static_cast(mObservers->ElementAt(i)); NS_ASSERTION(observer != nsnull, "null ptr"); if (! observer) continue; rv = observer->OnUnassert(this, aSource, aProperty, aValue); NS_RELEASE(observer); } } return NS_OK; } nsresult nsGlobalHistory::NotifyChange(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aOldValue, nsIRDFNode* aNewValue) { nsresult rv; if (mObservers) { PRUint32 count; rv = mObservers->Count(&count); if (NS_FAILED(rv)) return rv; for (PRInt32 i = 0; i < PRInt32(count); ++i) { nsIRDFObserver* observer = static_cast(mObservers->ElementAt(i)); NS_ASSERTION(observer != nsnull, "null ptr"); if (! observer) continue; rv = observer->OnChange(this, aSource, aProperty, aOldValue, aNewValue); NS_RELEASE(observer); } } return NS_OK; } // // this just generates a static list of find-style queries // only returns queries that currently have matches in global history // nsresult nsGlobalHistory::GetRootDayQueries(nsISimpleEnumerator **aResult) { nsresult rv; nsCOMPtr dayArray; NS_NewISupportsArray(getter_AddRefs(dayArray)); PRInt32 i; nsCOMPtr finduri; nsDependentCString prefix(FIND_BY_AGEINDAYS_PREFIX "is" "&text="); nsCAutoString uri; nsCOMPtr findEnumerator; PRBool hasMore = PR_FALSE; for (i=0; i<7; i++) { uri = prefix; uri.AppendInt(i); uri.Append("&groupby=Hostname"); rv = gRDFService->GetResource(uri, getter_AddRefs(finduri)); if (NS_FAILED(rv)) continue; rv = CreateFindEnumerator(finduri, getter_AddRefs(findEnumerator)); if (NS_FAILED(rv)) continue; rv = findEnumerator->HasMoreElements(&hasMore); if (NS_SUCCEEDED(rv) && hasMore) dayArray->AppendElement(finduri); } uri = FIND_BY_AGEINDAYS_PREFIX "isgreater" "&text="; uri.AppendInt(i-1); uri.Append("&groupby=Hostname"); rv = gRDFService->GetResource(uri, getter_AddRefs(finduri)); if (NS_SUCCEEDED(rv)) { rv = CreateFindEnumerator(finduri, getter_AddRefs(findEnumerator)); if (NS_SUCCEEDED(rv)) { rv = findEnumerator->HasMoreElements(&hasMore); if (NS_SUCCEEDED(rv) && hasMore) dayArray->AppendElement(finduri); } } return NS_NewArrayEnumerator(aResult, dayArray); } // // convert the name/value pairs stored in a string into an array of // these pairs // find:a=b&c=d&e=f&g=h // becomes an array containing // {"a" = "b", "c" = "d", "e" = "f", "g" = "h" } // nsresult nsGlobalHistory::FindUrlToTokenList(const char *aURL, nsVoidArray& aResult) { if (PL_strncmp(aURL, "find:", 5) != 0) return NS_ERROR_UNEXPECTED; const char *curpos = aURL + 5; const char *tokenstart = curpos; // this is where we will store the current name and value const char *tokenName = nsnull; const char *tokenValue = nsnull; PRUint32 tokenNameLength=0; PRUint32 tokenValueLength=0; PRBool haveValue = PR_FALSE; // needed because some values are 0-length while (PR_TRUE) { while (*curpos && (*curpos != '&') && (*curpos != '=')) curpos++; if (*curpos == '=') { // just found a token name tokenName = tokenstart; tokenNameLength = (curpos - tokenstart); } else if ((!*curpos || *curpos == '&') && (tokenNameLength>0)) { // found a value, and we have a // name tokenValue = tokenstart; tokenValueLength = (curpos - tokenstart); haveValue = PR_TRUE; } // once we have a name/value pair, store it away // note we're looking at lengths, so that // "find:&a=b" doesn't connect with a="" if (tokenNameLength>0 && haveValue) { tokenPair *tokenStruct = new tokenPair(tokenName, tokenNameLength, tokenValue, tokenValueLength); aResult.AppendElement((void *)tokenStruct); // reset our state tokenName = tokenValue = nsnull; tokenNameLength = tokenValueLength = 0; haveValue = PR_FALSE; } // the test has to be here to catch empty values if (!*curpos) break; curpos++; tokenstart = curpos; } return NS_OK; } void nsGlobalHistory::FreeTokenList(nsVoidArray& tokens) { PRUint32 length = tokens.Count(); PRUint32 i; for (i=0; iGetValueConst(&value); if (NS_FAILED(rv)) return PR_FALSE; return (PL_strncmp(value, "find:", 5)==0); } // // convert a list of name/value pairs into a search query with 0 or // more terms and an optional groupby // // a term consists of the values of the 4 name/value pairs // {datasource, match, method, text} // groupby is stored as a column # // nsresult nsGlobalHistory::TokenListToSearchQuery(const nsVoidArray& aTokens, searchQuery& aResult) { PRInt32 i; PRInt32 length = aTokens.Count(); aResult.groupBy = 0; const char *datasource=nsnull, *property=nsnull, *method=nsnull, *text=nsnull; PRUint32 datasourceLen=0, propertyLen=0, methodLen=0, textLen=0; rowMatchCallback matchCallback=nsnull; // matching callback if needed for (i=0; itokenName, token->tokenName + token->tokenNameLength); if (tokenName.EqualsLiteral("datasource")) { datasource = token->tokenValue; datasourceLen = token->tokenValueLength; } else if (tokenName.EqualsLiteral("match")) { if (Substring(token->tokenValue, token->tokenValue+token->tokenValueLength).Equals("AgeInDays")) matchCallback = matchAgeInDaysCallback; property = token->tokenValue; propertyLen = token->tokenValueLength; } else if (tokenName.EqualsLiteral("method")) { method = token->tokenValue; methodLen = token->tokenValueLength; } else if (tokenName.EqualsLiteral("text")) { text = token->tokenValue; textLen = token->tokenValueLength; } // really, we should be storing the group-by as a column number or // rdf resource else if (tokenName.EqualsLiteral("groupby")) { mdb_err err; err = mStore->QueryToken(mEnv, nsCAutoString(token->tokenValue).get(), &aResult.groupBy); if (err != 0) aResult.groupBy = 0; } // once we complete a term, we move on to the next one if (datasource && property && method && text) { searchTerm *currentTerm = new searchTerm(datasource, datasourceLen, property, propertyLen, method, methodLen, text, textLen); currentTerm->match = matchCallback; // append the old one, then create a new one aResult.terms.AppendElement((void *)currentTerm); // reset our state matchCallback=nsnull; currentTerm = nsnull; datasource = property = method = text = 0; } } return NS_OK; } nsresult nsGlobalHistory::FindUrlToSearchQuery(const char *aUrl, searchQuery& aResult) { nsresult rv; // convert uri to list of tokens nsVoidArray tokenPairs; rv = FindUrlToTokenList(aUrl, tokenPairs); if (NS_FAILED(rv)) return rv; // now convert the tokens to a query rv = TokenListToSearchQuery(tokenPairs, aResult); FreeTokenList(tokenPairs); return rv; } // preemptively construct some common find-queries so that we show up // asychronously when a search is open // we have to do the following assertions: // (a=AgeInDays, h=hostname; g=groupby, -> = #child) // 1) NC:HistoryRoot -> uri // // 2) NC:HistoryByDate -> a&g=h // 3) a&g=h -> a&h // 4) a&h -> uri // // 5) g=h -> h // 6) h->uri nsresult nsGlobalHistory::NotifyFindAssertions(nsIRDFResource *aSource, nsIMdbRow *aRow) { // we'll construct a bunch of sample queries, and then do // appropriate assertions // first pull out the appropriate values PRTime lastVisited; GetRowValue(aRow, kToken_LastVisitDateColumn, &lastVisited); PRInt32 ageInDays = GetAgeInDays(lastVisited); nsCAutoString ageString; ageString.AppendInt(ageInDays); nsCAutoString hostname; GetRowValue(aRow, kToken_HostnameColumn, hostname); // construct some terms that we'll use later // Hostname= searchTerm hostterm("history", sizeof("history")-1, "Hostname", sizeof("Hostname")-1, "is", sizeof("is")-1, hostname.get(), hostname.Length()); // AgeInDays= searchTerm ageterm("history", sizeof("history") -1, "AgeInDays", sizeof("AgeInDays")-1, "is", sizeof("is")-1, ageString.get(), ageString.Length()); searchQuery query; nsCAutoString findUri; nsCOMPtr childFindResource; nsCOMPtr parentFindResource; // 2) NC:HistoryByDate -> AgeInDays=&groupby=Hostname query.groupBy = kToken_HostnameColumn; query.terms.AppendElement((void *)&ageterm); GetFindUriPrefix(query, PR_TRUE, findUri); gRDFService->GetResource(findUri, getter_AddRefs(childFindResource)); NotifyAssert(kNC_HistoryByDate, kNC_child, childFindResource); query.terms.Clear(); // 3) AgeInDays=&groupby=Hostname -> // AgeInDays=&Hostname= parentFindResource=childFindResource; // AgeInDays=&groupby=Hostname query.groupBy = 0; // create AgeInDays=&Hostname= query.terms.AppendElement((void *)&ageterm); query.terms.AppendElement((void *)&hostterm); GetFindUriPrefix(query, PR_FALSE, findUri); gRDFService->GetResource(findUri, getter_AddRefs(childFindResource)); NotifyAssert(parentFindResource, kNC_child, childFindResource); query.terms.Clear(); // 4) AgeInDays=&Hostname= -> uri parentFindResource = childFindResource; // AgeInDays=&hostname= NotifyAssert(childFindResource, kNC_child, aSource); // 5) groupby=Hostname -> Hostname= query.groupBy = kToken_HostnameColumn; // create groupby=Hostname GetFindUriPrefix(query, PR_TRUE, findUri); gRDFService->GetResource(findUri, getter_AddRefs(parentFindResource)); query.groupBy = 0; // create Hostname= query.terms.AppendElement((void *)&hostterm); GetFindUriPrefix(query, PR_FALSE, findUri); findUri.Append(hostname); // append gRDFService->GetResource(findUri, getter_AddRefs(childFindResource)); NotifyAssert(parentFindResource, kNC_child, childFindResource); // 6) Hostname= -> uri parentFindResource = childFindResource; // Hostname= NotifyAssert(parentFindResource, kNC_child, aSource); return NS_OK; } // simpler than NotifyFindAssertions - basically just notifies // unassertions from // 1) NC:HistoryRoot -> uri // 2) a&h -> uri // 3) h -> uri nsresult nsGlobalHistory::NotifyFindUnassertions(nsIRDFResource *aSource, nsIMdbRow* aRow) { // 1) NC:HistoryRoot NotifyUnassert(kNC_HistoryRoot, kNC_child, aSource); // first get age in days PRTime lastVisited; GetRowValue(aRow, kToken_LastVisitDateColumn, &lastVisited); PRInt32 ageInDays = GetAgeInDays(lastVisited); nsCAutoString ageString; ageString.AppendInt(ageInDays); // now get hostname nsCAutoString hostname; GetRowValue(aRow, kToken_HostnameColumn, hostname); // construct some terms // Hostname= searchTerm hostterm("history", sizeof("history")-1, "Hostname", sizeof("Hostname")-1, "is", sizeof("is")-1, hostname.get(), hostname.Length()); // AgeInDays= searchTerm ageterm("history", sizeof("history") -1, "AgeInDays", sizeof("AgeInDays")-1, "is", sizeof("is")-1, ageString.get(), ageString.Length()); searchQuery query; query.groupBy = 0; nsCAutoString findUri; nsCOMPtr findResource; // 2) AgeInDays=&Hostname= query.terms.AppendElement((void *)&ageterm); query.terms.AppendElement((void *)&hostterm); GetFindUriPrefix(query, PR_FALSE, findUri); gRDFService->GetResource(findUri, getter_AddRefs(findResource)); NotifyUnassert(findResource, kNC_child, aSource); // 3) Hostname= query.terms.Clear(); query.terms.AppendElement((void *)&hostterm); GetFindUriPrefix(query, PR_FALSE, findUri); gRDFService->GetResource(findUri, getter_AddRefs(findResource)); NotifyUnassert(findResource, kNC_child, aSource); return NS_OK; } // // get the user-visible "name" of a find resource // we basically parse the string, and use the data stored in the last // term to generate an appropriate string // nsresult nsGlobalHistory::GetFindUriName(const char *aURL, nsIRDFNode **aResult) { nsresult rv; searchQuery query; rv = FindUrlToSearchQuery(aURL, query); // can't exactly get a name if there's nothing to search for if (query.terms.Count() < 1) return NS_OK; // now build up a string from the query (using only the last term) searchTerm *term = (searchTerm*)query.terms[query.terms.Count()-1]; // automatically build up string in the form // findurl--[-] // such as "finduri-AgeInDays-is" or "find-uri-AgeInDays-is-0" nsAutoString stringName(NS_LITERAL_STRING("finduri-")); // property AppendASCIItoUTF16(term->property, stringName); stringName.Append(PRUnichar('-')); // and now the method, such as "is" or "isgreater" AppendASCIItoUTF16(term->method, stringName); // try adding - to see if there's a match // for example, to match // finduri-LastVisitDate-is-0=Today PRInt32 preTextLength = stringName.Length(); stringName.Append(PRUnichar('-')); stringName.Append(term->text); // try to find a localizable string const PRUnichar *strings[] = { term->text.get() }; nsXPIDLString value; // first with the search text rv = mBundle->FormatStringFromName(stringName.get(), strings, 1, getter_Copies(value)); // ok, try it without the -, to match // finduri-LastVisitDate-is=%S days ago if (NS_FAILED(rv)) { stringName.Truncate(preTextLength); rv = mBundle->FormatStringFromName(stringName.get(), strings, 1, getter_Copies(value)); } nsCOMPtr literal; if (NS_SUCCEEDED(rv)) { rv = gRDFService->GetLiteral(value, getter_AddRefs(literal)); } else { // ok, no such string, so just put the match text itself there rv = gRDFService->GetLiteral(term->text.get(), getter_AddRefs(literal)); } FreeSearchQuery(query); if (NS_FAILED(rv)) return rv; *aResult = literal; NS_ADDREF(*aResult); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::Observe(nsISupports *aSubject, const char *aTopic, const PRUnichar *aSomeData) { nsresult rv; // pref changing - update member vars if (!nsCRT::strcmp(aTopic, "nsPref:changed")) { NS_ENSURE_STATE(gPrefBranch); // expiration date if (!nsCRT::strcmp(aSomeData, NS_LITERAL_STRING(PREF_BROWSER_HISTORY_EXPIRE_DAYS).get())) { gPrefBranch->GetIntPref(PREF_BROWSER_HISTORY_EXPIRE_DAYS, &mExpireDays); } else if (!nsCRT::strcmp(aSomeData, NS_LITERAL_STRING(PREF_AUTOCOMPLETE_ONLY_TYPED).get())) { gPrefBranch->GetBoolPref(PREF_AUTOCOMPLETE_ONLY_TYPED, &mAutocompleteOnlyTyped); } } else if (!nsCRT::strcmp(aTopic, "profile-before-change")) { rv = CloseDB(); if (!nsCRT::strcmp(aSomeData, NS_LITERAL_STRING("shutdown-cleanse").get())) { nsCOMPtr historyFile; rv = NS_GetSpecialDirectory(NS_APP_HISTORY_50_FILE, getter_AddRefs(historyFile)); if (NS_SUCCEEDED(rv)) rv = historyFile->Remove(PR_FALSE); } } else if (!nsCRT::strcmp(aTopic, "profile-do-change")) rv = OpenDB(); return NS_OK; } //---------------------------------------------------------------------- // // nsGlobalHistory::URLEnumerator // // Implementation nsGlobalHistory::URLEnumerator::~URLEnumerator() { nsMemory::Free(mSelectValue); } PRBool nsGlobalHistory::URLEnumerator::IsResult(nsIMdbRow* aRow) { if (HasCell(mEnv, aRow, mHiddenColumn)) return PR_FALSE; if (mSelectColumn) { mdb_err err; mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, mURLColumn, &yarn); if (err != 0) return PR_FALSE; // Do bitwise comparison PRInt32 count = PRInt32(yarn.mYarn_Fill); if (count != mSelectValueLen) return PR_FALSE; const char* p = (const char*) yarn.mYarn_Buf; const char* q = (const char*) mSelectValue; while (--count >= 0) { if (*p++ != *q++) return PR_FALSE; } } return PR_TRUE; } nsresult nsGlobalHistory::URLEnumerator::ConvertToISupports(nsIMdbRow* aRow, nsISupports** aResult) { mdb_err err; mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, mURLColumn, &yarn); if (err != 0) return NS_ERROR_FAILURE; // Since the URLEnumerator always returns the value of the URL // column, we create an RDF resource. nsresult rv; nsCOMPtr resource; const char* startPtr = (const char*) yarn.mYarn_Buf; rv = gRDFService->GetResource( Substring(startPtr, startPtr+yarn.mYarn_Fill), getter_AddRefs(resource)); if (NS_FAILED(rv)) return rv; *aResult = resource; NS_ADDREF(*aResult); return NS_OK; } //---------------------------------------------------------------------- // nsGlobalHistory::SearchEnumerator // // Implementation nsGlobalHistory::SearchEnumerator::~SearchEnumerator() { nsGlobalHistory::FreeSearchQuery(*mQuery); delete mQuery; } // convert the query in mQuery into a find URI // if there is a groupby= in the query, then convert that // into the start of another search term // for example, in the following query with one term: // // term[0] = { history, AgeInDays, is, 0 } // groupby = Hostname // // we generate the following uri: // // find:datasource=history&match=AgeInDays&method=is&text=0&datasource=history // &match=Hostname&method=is&text= // // and then the caller will append some text after after the "text=" // void nsGlobalHistory::GetFindUriPrefix(const searchQuery& aQuery, const PRBool aDoGroupBy, nsACString& aResult) { mdb_err err; aResult.Assign("find:"); PRUint32 length = aQuery.terms.Count(); PRUint32 i; for (i=0; i" if (aDoGroupBy) { aResult.Append("&groupby="); if (err == 0) aResult.Append((const char*)yarn.mYarn_Buf, yarn.mYarn_Fill); } // put &datasource=history&match=&method=is&text= else { // if the query has a groupby= then we want to append that // field as the last field to match.. caller has to be sure to // append that! aResult.Append("&datasource=history"); aResult.Append("&match="); if (err == 0) aResult.Append((const char*)yarn.mYarn_Buf, yarn.mYarn_Fill); // herep aResult.Append("&method=is"); aResult.Append("&text="); } } // // determines if the given row matches all terms // // if there is a "groupBy" column, then we have to remember that we've // seen a row with the given value in that column, and then make sure // all future rows with that value in that column DON'T match, no // matter if they match the terms or not. PRBool nsGlobalHistory::SearchEnumerator::IsResult(nsIMdbRow *aRow) { if (HasCell(mEnv, aRow, mHistory->kToken_HiddenColumn)) return PR_FALSE; mdb_err err; mdbYarn groupColumnValue = { nsnull, 0, 0, 0, 0, nsnull}; if (mQuery->groupBy!=0) { // if we have a 'groupby', then we use the hashtable to make sure // we only match the FIRST row with the column value that we're // grouping by err = aRow->AliasCellYarn(mEnv, mQuery->groupBy, &groupColumnValue); if (err!=0) return PR_FALSE; if (!groupColumnValue.mYarn_Buf) return PR_FALSE; const char* startPtr = (const char*)groupColumnValue.mYarn_Buf; nsCStringKey key(Substring(startPtr, startPtr + groupColumnValue.mYarn_Fill)); void *otherRow = mUniqueRows.Get(&key); // Hey! we've seen this row before, so ignore it if (otherRow) return PR_FALSE; } // now do the actual match if (!mHistory->RowMatches(aRow, mQuery)) return PR_FALSE; if (mQuery->groupBy != 0) { // we got this far, so we must have matched. // add ourselves to the hashtable so we don't match rows like this // in the future const char* startPtr = (const char*)groupColumnValue.mYarn_Buf; nsCStringKey key(Substring(startPtr, startPtr + groupColumnValue.mYarn_Fill)); // note - weak ref, don't worry about releasing mUniqueRows.Put(&key, (void *)aRow); } return PR_TRUE; } // // determines if the row matches the given terms, used above // PRBool nsGlobalHistory::RowMatches(nsIMdbRow *aRow, searchQuery *aQuery) { PRUint32 length = aQuery->terms.Count(); PRUint32 i; for (i=0; iterms[i]; if (!term->datasource.Equals("history")) continue; // we only match against history queries // use callback if it exists if (term->match) { // queue up some values just in case callback needs it // (how would we do this dynamically?) matchSearchTerm_t matchSearchTerm = { mEnv, mStore, term, PR_FALSE, 0, this }; if (!term->match(aRow, (void *)&matchSearchTerm)) return PR_FALSE; } else { mdb_err err; mdb_column property_column; err = mStore->QueryToken(mEnv, PromiseFlatCString(term->property).get(), &property_column); if (err != 0) { NS_WARNING("Unrecognized column!"); continue; // assume we match??? } nsAutoString rowVal; nsresult rv = GetRowValue(aRow, property_column, rowVal); if (NS_FAILED(rv)) return rv; const nsXPIDLString& searchText = term->text; if (term->method.Equals("is")) { if (!searchText.Equals(rowVal, nsCaseInsensitiveStringComparator())) return PR_FALSE; } else if (term->method.Equals("isnot")) { if (searchText.Equals(rowVal, nsCaseInsensitiveStringComparator())) return PR_FALSE; } else if (term->method.Equals("contains")) { if (!FindInReadable(searchText, rowVal, nsCaseInsensitiveStringComparator())) return PR_FALSE; } else if (term->method.Equals("doesntcontain")) { if (FindInReadable(searchText, rowVal, nsCaseInsensitiveStringComparator())) return PR_FALSE; } else if (term->method.Equals("startswith")) { if (!StringBeginsWith(rowVal, searchText, nsCaseInsensitiveStringComparator())) return PR_FALSE; } else if (term->method.Equals("endswith")) { if (!StringEndsWith(rowVal, searchText, nsCaseInsensitiveStringComparator())) return PR_FALSE; } else { NS_WARNING("Unrecognized search method in SearchEnumerator::RowMatches"); // don't handle other match types like isgreater/etc yet, // so assume the match failed and bail return PR_FALSE; } } } // we've gone through each term and didn't bail, so they must have // all matched! return PR_TRUE; } // // return either the row, or another find resource. // if we're doing grouping, then we don't want to return a real row, // instead we want to expand the current query into a deeper query // where we match up the groupby attribute. // if we're not doing grouping, then we just return the URL for the // current row nsresult nsGlobalHistory::SearchEnumerator::ConvertToISupports(nsIMdbRow* aRow, nsISupports** aResult) { mdb_err err; nsresult rv; nsCOMPtr resource; if (mQuery->groupBy == 0) { // no column to group by // just create a resource based on the URL of the current row mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, mHistory->kToken_URLColumn, &yarn); if (err != 0) return NS_ERROR_FAILURE; const char* startPtr = (const char*)yarn.mYarn_Buf; rv = gRDFService->GetResource( Substring(startPtr, startPtr+yarn.mYarn_Fill), getter_AddRefs(resource)); if (NS_FAILED(rv)) return rv; *aResult = resource; NS_ADDREF(*aResult); return NS_OK; } // we have a group by, so now we recreate the find url, but add a // query for the row asked for by groupby mdbYarn groupByValue; err = aRow->AliasCellYarn(mEnv, mQuery->groupBy, &groupByValue); if (err != 0) return NS_ERROR_FAILURE; if (mFindUriPrefix.IsEmpty()) mHistory->GetFindUriPrefix(*mQuery, PR_FALSE, mFindUriPrefix); nsCAutoString findUri(mFindUriPrefix); findUri.Append((const char*)groupByValue.mYarn_Buf, groupByValue.mYarn_Fill); rv = gRDFService->GetResource(findUri, getter_AddRefs(resource)); if (NS_FAILED(rv)) return rv; *aResult = resource; NS_ADDREF(*aResult); return NS_OK; } //---------------------------------------------------------------------- // // nsIAutoCompleteResult implementation // NS_IMETHODIMP nsGlobalHistory::GetSearchString(nsAString& aSearchString) { aSearchString = mSearchString; return NS_OK; } NS_IMETHODIMP nsGlobalHistory::GetSearchResult(PRUint16 *aSearchResult) { NS_ENSURE_ARG_POINTER(aSearchResult); *aSearchResult = mResults.Count() ? (PRUint16)RESULT_SUCCESS : (PRUint16)RESULT_NOMATCH; return NS_OK; } NS_IMETHODIMP nsGlobalHistory::GetDefaultIndex(PRInt32 *aDefaultIndex) { NS_ENSURE_ARG_POINTER(aDefaultIndex); *aDefaultIndex = mResults.Count() ? 0 : -1; return NS_OK; } NS_IMETHODIMP nsGlobalHistory::GetErrorDescription(nsAString& aErrorDescription) { aErrorDescription.Truncate(); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::GetMatchCount(PRUint32* aMatchCount) { NS_ENSURE_ARG_POINTER(aMatchCount); *aMatchCount = mResults.Count(); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::GetValueAt(PRInt32 aIndex, nsAString& aValue) { NS_ENSURE_ARG_RANGE(aIndex, 0, mResults.Count() - 1); return GetRowValue(mResults[aIndex], kToken_URLColumn, aValue); } NS_IMETHODIMP nsGlobalHistory::GetCommentAt(PRInt32 aIndex, nsAString& aValue) { NS_ENSURE_ARG(aIndex >= 0 && aIndex < mResults.Count()); return GetRowValue(mResults[aIndex], kToken_NameColumn, aValue); } NS_IMETHODIMP nsGlobalHistory::GetStyleAt(PRInt32 aIndex, nsAString& aValue) { NS_ENSURE_ARG(aIndex >= 0 && aIndex < mResults.Count()); aValue.Truncate(); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::GetImageAt(PRInt32 aIndex, nsAString& aValue) { NS_ENSURE_ARG(aIndex >= 0 && aIndex < mResults.Count()); aValue.Truncate(); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::RemoveValueAt(PRInt32 aIndex, PRBool aRemoveFromDb) { NS_ENSURE_ARG(aIndex >= 0 && aIndex < mResults.Count()); if (aRemoveFromDb) { nsCAutoString url; nsCOMPtr row = mResults[aIndex]; nsresult rv = GetRowValue(row, kToken_URLColumn, url); if (NS_FAILED(rv)) return rv; rv = RemovePageInternal(url, row); if (NS_FAILED(rv)) return rv; } mResults.RemoveObjectAt(aIndex); return NS_OK; } //---------------------------------------------------------------------- // // nsIAutoCompleteSearch implementation // NS_IMETHODIMP nsGlobalHistory::StartSearch(const nsAString& aSearchString, const nsAString& aSearchParam, nsIAutoCompleteResult* aPreviousResult, nsIAutoCompleteObserver* aListener) { NS_ENSURE_ARG_POINTER(aListener); NS_ENSURE_STATE(gPrefBranch); NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); PRBool enabled = PR_FALSE; gPrefBranch->GetBoolPref(PREF_AUTOCOMPLETE_ENABLED, &enabled); nsAutoString cut(aSearchString); AutoCompleteCutPrefix(cut, nsnull); if (cut.IsEmpty() || !enabled) { mResults.Clear(); mSearchString = aSearchString; aListener->OnSearchResult(this, this); return NS_OK; } nsString filtered = AutoCompletePrefilter(aSearchString); AutocompleteExclude exclude; AutoCompleteGetExcludeInfo(filtered, &exclude); if (aPreviousResult == this && StringBeginsWith(filtered, AutoCompletePrefilter(mSearchString))) { nsAutoString value; PRUint32 count = mResults.Count(); while (count--) { GetRowValue(mResults[count], kToken_URLColumn, value); if (!AutoCompleteCompare(value, filtered, &exclude)) mResults.RemoveObjectAt(count); } } else { mResults.Clear(); mdb_pos pos; nsCOMPtr current; nsCOMPtr cursor; NS_ENSURE_TRUE(mTable->GetTableRowCursor(mEnv, -1, getter_AddRefs(cursor)) == 0, NS_ERROR_FAILURE); while (cursor->NextRow(mEnv, getter_AddRefs(current), &pos) == 0 && current) { if (!HasCell(mEnv, current, kToken_TypedColumn)) { if (mAutocompleteOnlyTyped || HasCell(mEnv, current, kToken_HiddenColumn)) continue; } nsAutoString url; GetRowValue(current, kToken_URLColumn, url); if (AutoCompleteCompare(url, filtered, &exclude)) mResults.AppendObject(current); } // Setup the structure we pass into the sort function, // including a set of url prefixes to ignore. These prefixes // must match with the logic in nsGlobalHistory::nsGlobalHistory(). NS_NAMED_LITERAL_STRING(prefixHWStr, "http://www."); NS_NAMED_LITERAL_STRING(prefixHStr, "http://"); NS_NAMED_LITERAL_STRING(prefixHSWStr, "https://www."); NS_NAMED_LITERAL_STRING(prefixHSStr, "https://"); NS_NAMED_LITERAL_STRING(prefixFFStr, "ftp://ftp."); NS_NAMED_LITERAL_STRING(prefixFStr, "ftp://"); // note: the number of prefixes stored in the closure below // must match with the constant AUTOCOMPLETE_PREFIX_LIST_COUNT AutoCompleteSortClosure closure; closure.history = this; closure.prefixCount = AUTOCOMPLETE_PREFIX_LIST_COUNT; closure.prefixes[0] = &prefixHWStr; closure.prefixes[1] = &prefixHStr; closure.prefixes[2] = &prefixHSWStr; closure.prefixes[3] = &prefixHSStr; closure.prefixes[4] = &prefixFFStr; closure.prefixes[5] = &prefixFStr; mResults.Sort(AutoCompleteSortComparison, (void*)&closure); } mSearchString = aSearchString; aListener->OnSearchResult(this, this); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::StopSearch() { return NS_OK; } //---------------------------------------------------------------------- // // AutoComplete stuff // // If aURL begins with a protocol or domain prefix from our lists, // then mark their index in an AutocompleteExclude struct. void nsGlobalHistory::AutoCompleteGetExcludeInfo(const nsAString& aURL, AutocompleteExclude* aExclude) { aExclude->schemePrefix = -1; aExclude->hostnamePrefix = -1; PRInt32 index = 0; PRInt32 i; for (i = 0; i < mIgnoreSchemes.Count(); ++i) { nsString* string = mIgnoreSchemes.StringAt(i); if (StringBeginsWith(aURL, *string)) { aExclude->schemePrefix = i; index = string->Length(); break; } } for (i = 0; i < mIgnoreHostnames.Count(); ++i) { nsString* string = mIgnoreHostnames.StringAt(i); if (Substring(aURL, index, string->Length()).Equals(*string)) { aExclude->hostnamePrefix = i; break; } } } // Cut any protocol and domain prefixes from aURL, except for those which // are specified in aExclude void nsGlobalHistory::AutoCompleteCutPrefix(nsAString& aURL, AutocompleteExclude* aExclude) { // This comparison is case-sensitive. Therefore, it assumes that aUserURL is a // potential URL whose host name is in all lower case. PRInt32 idx = 0; PRInt32 i; for (i = 0; i < mIgnoreSchemes.Count(); ++i) { if (aExclude && i == aExclude->schemePrefix) continue; nsString* string = mIgnoreSchemes.StringAt(i); if (StringBeginsWith(aURL, *string)) { idx = string->Length(); break; } } if (idx > 0) aURL.Cut(0, idx); idx = 0; for (i = 0; i < mIgnoreHostnames.Count(); ++i) { if (aExclude && i == aExclude->hostnamePrefix) continue; nsString* string = mIgnoreHostnames.StringAt(i); if (StringBeginsWith(aURL, *string)) { idx = string->Length(); break; } } if (idx > 0) aURL.Cut(0, idx); } nsString nsGlobalHistory::AutoCompletePrefilter(const nsAString& aSearchString) { nsAutoString url(aSearchString); PRInt32 slash = url.FindChar('/', 0); if (slash >= 0) { // if user is typing a url but has already typed past the host, // then convert the host to lowercase nsAutoString host; url.Left(host, slash); ToLowerCase(host); url.Assign(host + Substring(url, slash, url.Length()-slash)); } else { // otherwise, assume the user could still be typing the host, and // convert everything to lowercase ToLowerCase(url); } return nsString(url); } PRBool nsGlobalHistory::AutoCompleteCompare(nsAString& aHistoryURL, const nsAString& aUserURL, AutocompleteExclude* aExclude) { AutoCompleteCutPrefix(aHistoryURL, aExclude); return StringBeginsWith(aHistoryURL, aUserURL); } int PR_CALLBACK nsGlobalHistory::AutoCompleteSortComparison(nsIMdbRow* row1, nsIMdbRow* row2, void* closureVoid) { AutoCompleteSortClosure* closure = static_cast(closureVoid); nsGlobalHistory* history = closure->history; // get visit counts - we're ignoring all errors from GetRowValue(), // and relying on default values PRInt32 item1visits = 0, item2visits = 0; history->GetRowValue(row1, history->kToken_VisitCountColumn, &item1visits); history->GetRowValue(row2, history->kToken_VisitCountColumn, &item2visits); // get URLs nsAutoString url1, url2; history->GetRowValue(row1, history->kToken_URLColumn, url1); history->GetRowValue(row2, history->kToken_URLColumn, url2); // Favour websites and webpaths more than webpages by boosting // their visit counts. This assumes that URLs have been normalized, // appending a trailing '/'. // // We use addition to boost the visit count rather than multiplication // since we want URLs with large visit counts to remain pretty much // in raw visit count order - we assume the user has visited these urls // often for a reason and there shouldn't be a problem with putting them // high in the autocomplete list regardless of whether they are sites/ // paths or pages. However for URLs visited only a few times, sites // & paths should be presented before pages since they are generally // more likely to be visited again. // PRBool isPath1 = PR_FALSE, isPath2 = PR_FALSE; if (!url1.IsEmpty()) { // url is a site/path if it has a trailing slash isPath1 = (url1.Last() == PRUnichar('/')); if (isPath1) item1visits += AUTOCOMPLETE_NONPAGE_VISIT_COUNT_BOOST; } if (!url2.IsEmpty()) { // url is a site/path if it has a trailing slash isPath2 = (url2.Last() == PRUnichar('/')); if (isPath2) item2visits += AUTOCOMPLETE_NONPAGE_VISIT_COUNT_BOOST; } // primary sort by visit count if (item1visits != item2visits) { // return visit count comparison return item2visits - item1visits; } else { // Favour websites and webpaths more than webpages if (isPath1 && !isPath2) return -1; // url1 is a website/path, url2 isn't if (!isPath1 && isPath2) return 1; // url1 isn't a website/path, url2 is // We have two websites/paths.. ignore "http[s]://[www.]" & "ftp://[ftp.]" // prefixes. Find a starting position in the string, just past any of the // above prefixes. Only check for the prefix once, in the far left of the // string - it is assumed there is no whitespace. PRInt32 postPrefix1 = 0, postPrefix2 = 0; size_t i; // iterate through our prefixes looking for a match for (i=0; iprefixCount; i++) { // Check if string is prefixed. Note: the parameters of the Find() // method specify the url is searched at the 0th character and if there // is no match the rest of the url is not searched. if (url1.Find((*closure->prefixes[i]), 0, 1) == 0) { // found a match - record post prefix position postPrefix1 = closure->prefixes[i]->Length(); // bail out of the for loop break; } } // iterate through our prefixes looking for a match for (i=0; iprefixCount; i++) { // Check if string is prefixed. Note: the parameters of the Find() // method specify the url is searched at the 0th character and if there // is no match the rest of the url is not searched. if (url2.Find((*closure->prefixes[i]), 0, 1) == 0) { // found a match - record post prefix position postPrefix2 = closure->prefixes[i]->Length(); // bail out of the for loop break; } } // compare non-prefixed urls PRInt32 ret = Compare( Substring(url1, postPrefix1, url1.Length()), Substring(url2, postPrefix2, url2.Length())); if (ret != 0) return ret; // sort http://xyz.com before http://www.xyz.com return postPrefix1 - postPrefix2; } }