/* -*- 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 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 "nsCOMArray.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_HistoryByDateAndSite; nsIRDFResource* nsGlobalHistory::kNC_HistoryByDate; nsIRDFResource* nsGlobalHistory::kNC_DayFolderIndex; 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) #define MSECS_PER_DAY (PR_MSEC_PER_SEC * 60 * 60 * 24) //---------------------------------------------------------------------- // // CIDs static NS_DEFINE_CID(kRDFServiceCID, NS_RDFSERVICE_CID); static NS_DEFINE_CID(kStringBundleServiceCID, NS_STRINGBUNDLESERVICE_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? PRTime now; PRInt32 intValue; }; 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); } static PRTime NormalizeTime(PRTime aTime) { // normalize both now and date to midnight of the day they occur on PRExplodedTime explodedTime; PR_ExplodeTime(aTime, PR_LocalTimeParameters, &explodedTime); // set to midnight (0:00) explodedTime.tm_min = explodedTime.tm_hour = explodedTime.tm_sec = explodedTime.tm_usec = 0; return PR_ImplodeTime(&explodedTime); } // pass in a pre-normalized now and a date, and we'll find // the difference since midnight on each of the days.. static PRInt32 GetAgeInDays(PRTime aNormalizedNow, PRTime aDate) { PRTime dateMidnight = NormalizeTime(aDate); PRTime diff; LL_SUB(diff, aNormalizedNow, dateMidnight); // two-step process since I can't seem to load // MSECS_PER_DAY * PR_MSEC_PER_SEC into a PRInt64 at compile time PRInt64 msecPerSec; LL_I2L(msecPerSec, PR_MSEC_PER_SEC); PRInt64 ageInSeconds; LL_DIV(ageInSeconds, diff, msecPerSec); PRInt32 ageSec; LL_L2I(ageSec, ageInSeconds); PRInt64 msecPerDay; LL_I2L(msecPerDay, MSECS_PER_DAY); PRInt64 ageInDays; LL_DIV(ageInDays, ageInSeconds, msecPerDay); 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); matchSearchTerm->now = NormalizeTime(PR_Now()); 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 = GetAgeInDays(matchSearchTerm->now, 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, PR_TRUE); } //---------------------------------------------------------------------- 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_HistoryByDateAndSite); NS_IF_RELEASE(kNC_HistoryByDate); NS_IF_RELEASE(kNC_DayFolderIndex); 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_ISUPPORTS7(nsGlobalHistory, nsIGlobalHistory2, nsIBrowserHistory, nsIObserver, nsISupportsWeakReference, nsIRDFDataSource, nsIRDFRemoteDataSource, nsIAutoCompleteSearch) //---------------------------------------------------------------------- // // nsGlobalHistory // // nsIGlobalHistory2 methods // NS_IMETHODIMP nsGlobalHistory::AddURI(nsIURI *aURI, PRBool aRedirect, PRBool aTopLevel, nsIURI *aReferrer) { PRTime now = GetNow(); return AddPageToDatabase(aURI, aRedirect, aTopLevel, now, aReferrer); } nsresult nsGlobalHistory::AddPageToDatabase(nsIURI* aURI, PRBool aRedirect, PRBool aTopLevel, PRTime aLastVisitDate, 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) { NS_WARNING("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) { #ifdef DEBUG_bsmedberg printf("Filtering out unwanted scheme.\n"); #endif 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; #ifdef DEBUG_bsmedberg printf("AddURI: %s%s%s", URISpec.get(), aRedirect ? ", redirect" : "", aTopLevel ? ", toplevel" : ""); #endif nsCOMPtr row; rv = FindRow(kToken_URLColumn, URISpec.get(), getter_AddRefs(row)); if (NS_SUCCEEDED(rv)) { // update the database, and get the old info back PRTime oldDate; PRInt32 oldCount; rv = AddExistingPageToDatabase(row, aLastVisitDate, aReferrer, &oldDate, &oldCount); NS_ASSERTION(NS_SUCCEEDED(rv), "AddExistingPageToDatabase failed; see bug 88961"); if (NS_FAILED(rv)) return rv; #ifdef DEBUG_bsmedberg printf("Existing page succeeded.\n"); #endif } else { rv = AddNewPageToDatabase(aURI, aLastVisitDate, aRedirect, aTopLevel, aReferrer, getter_AddRefs(row)); NS_ASSERTION(NS_SUCCEEDED(rv), "AddNewPageToDatabase failed; see bug 88961"); if (NS_FAILED(rv)) return rv; #ifdef DEBUG_bsmedberg printf("New page succeeded.\n"); #endif } // 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, nsIURI* aReferrer, PRTime *aOldDate, PRInt32 *aOldCount) { nsresult rv; nsCAutoString oldReferrer; nsCAutoString URISpec; rv = GetRowValue(row, kToken_URLColumn, URISpec); NS_ENSURE_SUCCESS(rv, rv); nsCAutoString referrerSpec; if (aReferrer) { rv = aReferrer->GetSpec(referrerSpec); NS_ENSURE_SUCCESS(rv, rv); } // if the page was typed, unhide it now because it's // known to be valid if (HasCell(mEnv, row, kToken_TypedColumn)) { mTypedHiddenURIs.Remove(URISpec); row->CutColumn(mEnv, kToken_HiddenColumn); } // 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) { rv = GetRowValue(row, kToken_ReferrerColumn, oldReferrer); // No referrer? Now there is! if ((NS_FAILED(rv) || oldReferrer.IsEmpty())) SetRowValue(row, kToken_ReferrerColumn, referrerSpec.get()); } // Notify observers nsCOMPtr url; rv = gRDFService->GetResource(URISpec, getter_AddRefs(url)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr date; rv = gRDFService->GetDateLiteral(aDate, getter_AddRefs(date)); NS_ENSURE_SUCCESS(rv, rv); // visit date nsCOMPtr oldDateLiteral; rv = gRDFService->GetDateLiteral(*aOldDate, getter_AddRefs(oldDateLiteral)); NS_ENSURE_SUCCESS(rv, rv); rv = NotifyChange(url, kNC_Date, oldDateLiteral, date); NS_ENSURE_SUCCESS(rv, rv); // visit count nsCOMPtr oldCountLiteral; rv = gRDFService->GetIntLiteral(*aOldCount, getter_AddRefs(oldCountLiteral)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr newCountLiteral; rv = gRDFService->GetIntLiteral(*aOldCount+1, getter_AddRefs(newCountLiteral)); NS_ENSURE_SUCCESS(rv, rv); rv = NotifyChange(url, kNC_VisitCount, oldCountLiteral, newCountLiteral); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult nsGlobalHistory::AddNewPageToDatabase(nsIURI* aURI, PRTime aDate, PRBool aRedirect, PRBool aTopLevel, nsIURI* aReferrer, nsIMdbRow **aResult) { mdb_err err; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_NOT_INITIALIZED); nsCAutoString URISpec; nsresult rv = aURI->GetSpec(URISpec); NS_ENSURE_SUCCESS(rv, rv); nsCAutoString referrerSpec; if (aReferrer) { rv = aReferrer->GetSpec(referrerSpec); NS_ENSURE_SUCCESS(rv, rv); } // 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, URISpec.get()); // Set the date. SetRowValue(row, kToken_LastVisitDateColumn, aDate); SetRowValue(row, kToken_FirstVisitDateColumn, aDate); // Set the referrer if there is one. if (aReferrer) SetRowValue(row, kToken_ReferrerColumn, referrerSpec.get()); nsCOMPtr uri; NS_NewURI(getter_AddRefs(uri), URISpec, nsnull, nsnull); nsCAutoString hostname; if (uri) uri->GetHost(hostname); // Strip www. if (Substring(hostname, 0, 4).EqualsLiteral("www.")) hostname.Cut(0, 4); SetRowValue(row, kToken_HostnameColumn, hostname.get()); *aResult = row; NS_ADDREF(*aResult); PRBool isJavascript; rv = aURI->SchemeIs("javascript", &isJavascript); NS_ENSURE_SUCCESS(rv, 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. AddExistingPageToDatabase has logic to override this // behavior for URIs which were typed. See bug 197127 and bug 161531 // for details. rv = SetRowValue(row, kToken_HiddenColumn, 1); NS_ENSURE_SUCCESS(rv, rv); } else { // Notify observers nsCOMPtr url; rv = gRDFService->GetResource(URISpec, getter_AddRefs(url)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr date; rv = gRDFService->GetDateLiteral(aDate, getter_AddRefs(date)); NS_ENSURE_SUCCESS(rv, rv); 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); } return NS_OK; } nsresult nsGlobalHistory::RemovePageInternal(const char *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, getter_AddRefs(row)); if (NS_FAILED(rv)) return NS_OK; // remove the row mdb_err err = mTable->CutRow(mEnv, row); 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(nsDependentCString(aSpec), getter_AddRefs(oldRowResource)); NotifyFindUnassertions(oldRowResource, row); } // not a fatal error if we can't cut all column err = row->CutAllColumns(mEnv); NS_ASSERTION(err == 0, "couldn't cut all columns"); // Sigh. This is pretty bad - if the user selects a bunch of things then // hits delete we'll be re-writing history over and over. Still, we will // be whacking global history pretty hard after 1.0 so I don't feel too // bad. 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 abilitiy // to handle yarn forms properly #if 0 NS_ConvertUCS2toUTF8 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; 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 aResult.Assign(NS_ConvertUTF8toUCS2((const char*)yarn.mYarn_Buf, yarn.mYarn_Fill)); 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::AddPageWithDetails(nsIURI *aURI, const PRUnichar *aTitle, PRTime aLastVisitDate) { nsresult rv = AddPageToDatabase(aURI, PR_FALSE, PR_TRUE, aLastVisitDate, nsnull); if (NS_FAILED(rv)) return rv; return SetPageTitle(aURI, nsDependentString(aTitle)); } 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)); // 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.get()); 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; const char* startPtr = (const char*) yarn.mYarn_Buf; nsCAutoString uri(Substring(startPtr, startPtr+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); mdb_err err = GetRowValue(mMetaRow, kToken_LastPageVisited, _retval); NS_ENSURE_TRUE(err == 0, NS_ERROR_FAILURE); return NS_OK; } // 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; mdb_err err = GetRowValue(mMetaRow, kToken_ByteOrder, byteOrder); NS_ENSURE_TRUE(err == 0, NS_ERROR_FAILURE); *_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; #ifdef DEBUG_bsmedberg printf("nsGlobalHistory::HidePage: %s\n", URISpec.get()); #endif 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) { NS_ENSURE_ARG_POINTER(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(aURI, GetNow(), PR_FALSE, PR_TRUE, 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); } //---------------------------------------------------------------------- // // 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; 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 (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; 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; 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_HistoryByDateAndSite || 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) || (aProperty == kNC_DayFolderIndex)) { 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_ConvertUTF8toUCS2(uri).get(), getter_AddRefs(uriLiteral)); if (NS_FAILED(rv)) return(rv); *aTarget = uriLiteral; NS_ADDREF(*aTarget); return NS_OK; } // find URIs are special if (IsFindResource(aSource)) { if (aProperty == kNC_Name) return GetFindUriName(uri, aTarget); if (aProperty == kNC_NameSort) { // parse out the 'text' token nsVoidArray tokenList; FindUrlToTokenList(uri, tokenList); nsCOMPtr literal; for (PRInt32 i = 0; i < tokenList.Count(); ++i) { tokenPair* token = NS_STATIC_CAST(tokenPair*, tokenList[i]); if (!strncmp(token->tokenName, "text", token->tokenNameLength)) { rv = gRDFService->GetLiteral(NS_ConvertUTF8toUCS2(Substring(token->tokenValue, token->tokenValue + token->tokenValueLength)).get(), getter_AddRefs(literal)); // We don't break out of the loop here because there could be other text tokens in the string. // The last one is the most specific so wait and see if we've got one... } } FreeTokenList(tokenList); if (literal && NS_SUCCEEDED(rv)) { *aTarget = literal; NS_ADDREF(*aTarget); return NS_OK; } *aTarget = nsnull; return rv; } } // 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(NormalizeTime(GetNow()), 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 title = NS_ConvertUTF8toUCS2(filename); } 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(); 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_HistoryByDateAndSite) && (aProperty == kNC_child)) { return GetRootDayQueries(aTargets, PR_TRUE); } else if ((aSource == kNC_HistoryByDate) && (aProperty == kNC_child)) { return GetRootDayQueries(aTargets, PR_FALSE); } 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) || (aProperty == kNC_DayFolderIndex)) { 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) { // translate into an appropriate removehistory call nsresult rv; if ((aSource == kNC_HistoryRoot || aSource == kNC_HistoryByDateAndSite || 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(targetUrl); if (NS_FAILED(rv)) return NS_RDF_ASSERTION_REJECTED; if (!mBatchesInProgress && IsFindResource(aSource)) { // 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. NotifyUnassert(aSource, aProperty, aTarget); } 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; } 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, PR_TRUE); 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; 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; if ((aSource == kNC_HistoryRoot) || (aSource == kNC_HistoryByDateAndSite) || (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 || aArc == kNC_DayFolderIndex); } 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; 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; nsresult rv; if ((aSource == kNC_HistoryRoot) || (aSource == kNC_HistoryByDateAndSite) || (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); array->AppendElement(kNC_DayFolderIndex); 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) { 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 = NS_STATIC_CAST(nsIRDFObserver*, 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 = NS_STATIC_CAST(nsIRDFObserver*, 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_NAMESPACE_URI "DayFolderIndex"), &kNC_DayFolderIndex); gRDFService->GetResource(NS_LITERAL_CSTRING("NC:HistoryRoot"), &kNC_HistoryRoot); gRDFService->GetResource(NS_LITERAL_CSTRING("NC:HistoryByDateAndSite"), &kNC_HistoryByDateAndSite); 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(kStringBundleServiceCID, &rv); if (NS_SUCCEEDED(rv)) { rv = bundleService->CreateBundle("chrome://global/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); observerService->AddObserver(this, "quit-application", 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); static NS_DEFINE_CID(kMorkCID, NS_MORK_CID); nsCOMPtr factoryfactory = do_CreateInstance(kMorkCID, &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.Equals(NS_LITERAL_CSTRING("BE")) || file_byte_order.Equals(NS_LITERAL_CSTRING("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, kToken_HiddenColumn, 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; // 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() { 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(); if (mStore) 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 = NS_STATIC_CAST(nsIRDFObserver*, 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 = NS_STATIC_CAST(nsIRDFObserver*, 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 = NS_STATIC_CAST(nsIRDFObserver*, 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, PRBool aBySite) { 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); if (aBySite) 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); if (aBySite) 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); if (tokenStruct) 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(NormalizeTime(GetNow()), 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_HistoryByDateAndSite, kNC_child, childFindResource); parentFindResource = childFindResource; query.terms.Clear(); query.groupBy = 0; 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(); query.groupBy = 0; query.terms.AppendElement((void *)&ageterm); GetFindUriPrefix(query, PR_TRUE, findUri); gRDFService->GetResource(findUri, getter_AddRefs(childFindResource)); NotifyAssert(childFindResource, kNC_child, aSource); query.terms.Clear(); // 3) AgeInDays=&groupby=Hostname -> // AgeInDays=&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(NormalizeTime(GetNow()), 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); query.terms.Clear(); 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 stringName.Append(NS_ConvertASCIItoUCS2(term->property)); stringName.Append(PRUnichar('-')); // and now the method, such as "is" or "isgreater" stringName.Append(NS_ConvertASCIItoUCS2(term->method)); // 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); stringName.Append(PRUnichar(0)); // 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(); else if (!nsCRT::strcmp(aTopic, "quit-application")) rv = Flush(); 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, mHiddenColumn)) 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, PR_FALSE)) 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, PRBool caseSensitive) { 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}; if (!term->match(aRow, (void *)&matchSearchTerm)) return PR_FALSE; } else { mdb_err err; mdb_column property_column; nsCAutoString property_name(term->property); property_name.Append(char(0)); err = mStore->QueryToken(mEnv, property_name.get(), &property_column); if (err != 0) { NS_WARNING("Unrecognized column!"); continue; // assume we match??? } // match the term directly against the column? mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, property_column, &yarn); if (err != 0 || !yarn.mYarn_Buf) return PR_FALSE; nsAutoString rowVal; PRInt32 yarnLength = yarn.mYarn_Fill;; if (property_column == kToken_NameColumn) { // The name column (page title) is stored as UTF-16. rowVal.Assign((const PRUnichar*)yarn.mYarn_Buf, yarnLength / 2); } else { // Other columns are stored as UTF-8 and can be null. if (yarn.mYarn_Buf) rowVal = NS_ConvertUTF8toUTF16((const char*)yarn.mYarn_Buf, yarnLength); } // set up some iterators nsString::const_iterator start, end; rowVal.BeginReading(start); rowVal.EndReading(end); const nsXPIDLString& searchText = term->text; if (term->method.Equals("is")) { if (caseSensitive) { if (!searchText.Equals(rowVal, nsDefaultStringComparator())) return PR_FALSE; } else { if (!searchText.Equals(rowVal, nsCaseInsensitiveStringComparator())) return PR_FALSE; } } else if (term->method.Equals("isnot")) { if (caseSensitive) { if (searchText.Equals(rowVal, nsDefaultStringComparator())) return PR_FALSE; } else { if (searchText.Equals(rowVal, nsCaseInsensitiveStringComparator())) return PR_FALSE; } } else if (term->method.Equals("contains")) { if (caseSensitive) { if (!FindInReadable(searchText, start, end, nsDefaultStringComparator())) return PR_FALSE; } else { if (!FindInReadable(searchText, start, end, nsCaseInsensitiveStringComparator())) return PR_FALSE; } } else if (term->method.Equals("doesntcontain")) { if (caseSensitive) { if (FindInReadable(searchText, start, end, nsDefaultStringComparator())) return PR_FALSE; } else { if (FindInReadable(searchText, start, end, nsCaseInsensitiveStringComparator())) return PR_FALSE; } } else if (term->method.Equals("startswith")) { // need to make sure that the found string is // at the beginning of the string nsAString::const_iterator real_start = start; if (caseSensitive) { if (!(FindInReadable(searchText, start, end, nsDefaultStringComparator()) && real_start == start)) return PR_FALSE; } else { if (!(FindInReadable(searchText, start, end, nsCaseInsensitiveStringComparator()) && real_start == start)) return PR_FALSE; } } else if (term->method.Equals("endswith")) { // need to make sure that the found string ends // at the end of the string nsAString::const_iterator real_end = end; if (caseSensitive) { if (!(RFindInReadable(searchText, start, end, nsDefaultStringComparator()) && real_end == end)) return PR_FALSE; } else { if (!(RFindInReadable(searchText, start, end, nsCaseInsensitiveStringComparator()) && real_end == end)) 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); const char* startPtr = (const char *)groupByValue.mYarn_Buf; findUri.Append(Substring(startPtr, startPtr+groupByValue.mYarn_Fill)); findUri.Append('\0'); rv = gRDFService->GetResource(findUri, getter_AddRefs(resource)); if (NS_FAILED(rv)) return rv; *aResult = resource; NS_ADDREF(*aResult); return NS_OK; } //---------------------------------------------------------------------- // // nsIAutoCompleteSession 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); nsCOMPtr result; if (aSearchString.IsEmpty()) { AutoCompleteTypedSearch(getter_AddRefs(result)); } else { // if the search string is empty after it has had prefixes removed, then // we need to ignore the previous result set nsAutoString cut(aSearchString); AutoCompleteCutPrefix(cut, nsnull); if (cut.Length() == 0) aPreviousResult = nsnull; // pass string through filter and then determine which prefixes to exclude // when chopping prefixes off of history urls during comparison nsString filtered = AutoCompletePrefilter(aSearchString); AutocompleteExclude exclude; AutoCompleteGetExcludeInfo(filtered, &exclude); // perform the actual search here nsresult rv = AutoCompleteSearch(filtered, &exclude, NS_STATIC_CAST(nsIAutoCompleteMdbResult2 *, aPreviousResult), getter_AddRefs(result)); NS_ENSURE_SUCCESS(rv, rv); } aListener->OnSearchResult(this, result); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::StopSearch() { return NS_OK; } //---------------------------------------------------------------------- // // AutoComplete stuff // nsresult nsGlobalHistory::AutoCompleteTypedSearch(nsIAutoCompleteMdbResult2 **aResult) { mdb_count count; mdb_err err = mTable->GetCount(mEnv, &count); // Get a cursor to iterate through all rows in the database nsCOMPtr rowCursor; err = mTable->GetTableRowCursor(mEnv, count, getter_AddRefs(rowCursor)); NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE); nsresult rv; nsCOMPtr result = do_CreateInstance("@mozilla.org/autocomplete/mdb-result;1", &rv); NS_ENSURE_SUCCESS(rv, rv); result->Init(mEnv, mTable); result->SetTokens(kToken_URLColumn, nsIAutoCompleteMdbResult2::kCharType, kToken_NameColumn, nsIAutoCompleteMdbResult2::kUnicharType); result->SetReverseByteOrder(mReverseByteOrder); nsCOMPtr row; mdb_pos pos; do { rowCursor->PrevRow(mEnv, getter_AddRefs(row), &pos); if (!row) break; if (HasCell(mEnv, row, kToken_TypedColumn)) { result->AddRow(row); } } while (row); // Determine the result of the search PRUint32 matchCount; rv = result->GetMatchCount(&matchCount); if (matchCount > 0) { result->SetSearchResult(nsIAutoCompleteResult::RESULT_SUCCESS); result->SetDefaultIndex(0); } else { result->SetSearchResult(nsIAutoCompleteResult::RESULT_NOMATCH); result->SetDefaultIndex(-1); } *aResult = result; NS_ADDREF(*aResult); return NS_OK; } nsresult nsGlobalHistory::AutoCompleteSearch(const nsAString &aSearchString, AutocompleteExclude *aExclude, nsIAutoCompleteMdbResult2 *aPrevResult, nsIAutoCompleteMdbResult2 **aResult) { // determine if we can skip searching the whole history and only search // through the previous search results PRBool searchPrevious = PR_FALSE; if (aPrevResult) { nsAutoString prevURLStr; aPrevResult->GetSearchString(prevURLStr); // if search string begins with the previous search string, it's a go searchPrevious = Substring(aSearchString, 0, prevURLStr.Length()).Equals(prevURLStr); } if (searchPrevious) { // Search through the previous result PRUint32 matchCount; aPrevResult->GetMatchCount(&matchCount); for (PRInt32 i = matchCount-1; i >= 0; --i) { // Make a copy of the value because AutoCompleteCompare is destructive nsAutoString url; aPrevResult->GetValueAt(i, url); if (!AutoCompleteCompare(url, aSearchString, aExclude)) aPrevResult->RemoveValueAt(i, PR_FALSE); } NS_ADDREF(*aResult = aPrevResult); } else { // Search through the entire history // Create and initialize a new result object nsresult rv = NS_OK; nsCOMPtr result = do_CreateInstance("@mozilla.org/autocomplete/mdb-result;1", &rv); NS_ENSURE_SUCCESS(rv, rv); result->Init(mEnv, mTable); result->SetTokens(kToken_URLColumn, nsIAutoCompleteMdbResult2::kCharType, kToken_NameColumn, nsIAutoCompleteMdbResult2::kUnicharType); result->SetReverseByteOrder(mReverseByteOrder); result->SetSearchString(aSearchString); // Get a cursor to iterate through all rows in the database nsCOMPtr rowCursor; mdb_err err = mTable->GetTableRowCursor(mEnv, -1, getter_AddRefs(rowCursor)); NS_ENSURE_TRUE(!err, NS_ERROR_FAILURE); // Store hits in an nsIArray initially nsCOMArray resultArray; nsCOMPtr row; mdb_pos pos; do { rowCursor->NextRow(mEnv, getter_AddRefs(row), &pos); if (!row) break; if (!HasCell(mEnv, row, kToken_TypedColumn)) if (mAutocompleteOnlyTyped || HasCell(mEnv, row, kToken_HiddenColumn)) continue; nsCAutoString url; GetRowValue(row, kToken_URLColumn, url); NS_ConvertUTF8toUCS2 utf8Url(url); if (AutoCompleteCompare(utf8Url, aSearchString, aExclude)) resultArray.AppendObject(row); } while (row); // 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; // sort it resultArray.Sort(AutoCompleteSortComparison, NS_STATIC_CAST(void*, &closure)); // place the sorted array into the autocomplete results PRUint32 count = resultArray.Count(); PRUint32 i; for (i = 0; i < count; ++i) { result->AddRow(resultArray[i]); } // Determine the result of the search PRUint32 matchCount; rv = result->GetMatchCount(&matchCount); if (matchCount > 0) { result->SetSearchResult(nsIAutoCompleteResult::RESULT_SUCCESS); result->SetDefaultIndex(0); } else { result->SetSearchResult(nsIAutoCompleteResult::RESULT_NOMATCH); result->SetDefaultIndex(-1); } *aResult = result; NS_ADDREF(*aResult); } return NS_OK; } // 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 (Substring(aURL, 0, string->Length()).Equals(*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 (Substring(aURL, 0, string->Length()).Equals(*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 (Substring(aURL, 0, string->Length()).Equals(*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 Substring(aHistoryURL, 0, aUserURL.Length()).Equals(aUserURL); } int PR_CALLBACK nsGlobalHistory::AutoCompleteSortComparison(nsIMdbRow *row1, nsIMdbRow *row2, void *closureVoid) { // // NOTE: The design and reasoning behind the following autocomplete // sort implementation is documented in bug 78270. // // cast our function parameters back into their real form AutoCompleteSortClosure* closure = NS_STATIC_CAST(AutoCompleteSortClosure*, closureVoid); // get visit counts - we're ignoring all errors from GetRowValue(), // and relying on default values PRInt32 item1visits = 0, item2visits = 0; closure->history->GetRowValue(row1, closure->history->kToken_VisitCountColumn, &item1visits); closure->history->GetRowValue(row2, closure->history->kToken_VisitCountColumn, &item2visits); // get URLs nsAutoString url1, url2; closure->history->GetRowValue(row1, closure->history->kToken_URLColumn, url1); closure->history->GetRowValue(row2, closure->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; } if (HasCell(closure->history->mEnv, row1, closure->history->kToken_TypedColumn)) item1visits += AUTOCOMPLETE_NONPAGE_VISIT_COUNT_BOOST; if (HasCell(closure->history->mEnv, row2, closure->history->kToken_TypedColumn)) 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; } return 0; }