/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: NPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Netscape 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/NPL/ * * 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 * * 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 NPL, 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 NPL, 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 "nsIFileSpec.h" #include "nsCRT.h" #include "nsFileStream.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 "nsXPIDLString.h" #include "plhash.h" #include "plstr.h" #include "prprf.h" #include "prtime.h" #include "rdf.h" #include "nsQuickSort.h" #include "nsIIOService.h" #include "nsIURL.h" #include "nsNetCID.h" #include "nsInt64.h" #include "nsMorkCID.h" #include "nsIMdbFactoryFactory.h" #include "nsIPref.h" #include "nsIObserverService.h" PRInt32 nsGlobalHistory::gRefCnt; nsIRDFService* nsGlobalHistory::gRDFService; nsIRDFResource* nsGlobalHistory::kNC_Page; nsIRDFResource* nsGlobalHistory::kNC_Date; nsIRDFResource* nsGlobalHistory::kNC_FirstVisitDate; nsIRDFResource* nsGlobalHistory::kNC_VisitCount; nsIRDFResource* nsGlobalHistory::kNC_AgeInDays; nsIRDFResource* nsGlobalHistory::kNC_Name; nsIRDFResource* nsGlobalHistory::kNC_NameSort; nsIRDFResource* nsGlobalHistory::kNC_Hostname; nsIRDFResource* nsGlobalHistory::kNC_Referrer; nsIRDFResource* nsGlobalHistory::kNC_child; nsIRDFResource* nsGlobalHistory::kNC_URL; nsIRDFResource* nsGlobalHistory::kNC_HistoryRoot; nsIRDFResource* nsGlobalHistory::kNC_HistoryByDate; #define PREF_BROWSER_HISTORY_LAST_PAGE_VISITED "browser.history.last_page_visited" #define PREF_BROWSER_HISTORY_EXPIRE_DAYS "browser.history_expire_days" #define PREF_AUTOCOMPLETE_ENABLED "browser.urlbar.autocomplete.enabled" #define FIND_BY_AGEINDAYS_PREFIX "find:datasource=history&match=AgeInDays&method=" // 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(kPrefCID, NS_PREF_CID); static NS_DEFINE_CID(kStringBundleServiceCID, NS_STRINGBUNDLESERVICE_CID); // closure structures for RemoveMatchingRows struct matchExpiration_t { PRInt64 *expirationDate; nsGlobalHistory *history; }; struct matchHost_t { const char *host; PRBool entireDomain; // should we delete the entire domain? nsGlobalHistory *history; nsIURI* cachedUrl; }; struct matchSearchTerm_t { nsIMdbEnv *env; nsIMdbStore *store; searchTerm *term; PRBool haveClosure; // are the rest of the fields valid? PRInt64 now; PRInt32 intValue; }; // 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) {} 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) { // need to do UTF8-conversion/unescaping here, using // nsITextToSubURI text.AssignWithConversion(aText, aTextLen); } nsDependentSingleFragmentCSubstring datasource; // should always be "history" ? nsDependentSingleFragmentCSubstring property; // AgeInDays, Hostname, etc nsDependentSingleFragmentCSubstring method; // is, isgreater, isless nsAutoString 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 nsresult PRInt64ToChars(const PRInt64& aValue, nsAWritableCString& aResult) { // Convert an unsigned 64-bit value to a string of up to aSize // decimal digits, placed in aBuf. nsInt64 value(aValue); aResult.Truncate(0); if (value == nsInt64(0)) { aResult.Append('0'); } while (value != nsInt64(0)) { PRInt32 ones = PRInt32(value % nsInt64(10)); value /= nsInt64(10); if (ones <=9) aResult.Insert(char('0' + ones), 0); } return NS_OK; } 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 nsresult CharsToPRInt64(const char* aBuf, PRUint32 aCount, PRInt64* aResult) { // Convert aBuf of exactly aCount decimal characters to a 64-bit // unsigned integer value. nsInt64 result(0); while (aCount-- > 0) { PRInt32 digit = (*aBuf++) - '0'; result *= nsInt64(10); result += nsInt64(digit); } *aResult = result; return NS_OK; } static PRTime NormalizeTime(PRInt64 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(PRInt64 aNormalizedNow, PRInt64 aDate) { PRInt64 dateMidnight = NormalizeTime(aDate); PRInt64 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, PRInt64* expirationDate) { nsresult rv; PRInt64 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; matchSearchTerm->intValue = 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 PRInt64 rowDate; 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; CharsToPRInt64((const char*)yarn.mYarn_Buf, yarn.mYarn_Fill, &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); } //---------------------------------------------------------------------- nsMdbTableEnumerator::nsMdbTableEnumerator() : mEnv(nsnull), mTable(nsnull), mCursor(nsnull), mCurrent(nsnull) { NS_INIT_REFCNT(); } 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; mEnv->AddStrongRef(mEnv); mTable = aTable; mTable->AddStrongRef(mEnv); mdb_err err; err = mTable->GetTableRowCursor(mEnv, -1, &mCursor); if (err != 0) return NS_ERROR_FAILURE; return NS_OK; } nsMdbTableEnumerator::~nsMdbTableEnumerator() { if (mCurrent) mCurrent->CutStrongRef(mEnv); if (mCursor) mCursor->CutStrongRef(mEnv); if (mTable) mTable->CutStrongRef(mEnv); if (mEnv) mEnv->CutStrongRef(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. mCurrent->CutStrongRef(mEnv); 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); mCurrent->CutStrongRef(mEnv); mCurrent = nsnull; return rv; } //---------------------------------------------------------------------- // // nsGlobalHistory // // ctor dtor etc. // nsGlobalHistory::nsGlobalHistory() : mExpireDays(9), // make default be nine days mNowValid(PR_FALSE), mDirty(PR_FALSE), mEnv(nsnull), mStore(nsnull), mTable(nsnull) { NS_INIT_REFCNT(); 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.")); } nsGlobalHistory::~nsGlobalHistory() { gRDFService->UnregisterDataSource(this); nsresult rv; rv = CloseDB(); if (--gRefCnt == 0) { if (gRDFService) { nsServiceManager::ReleaseService(kRDFServiceCID, gRDFService); gRDFService = nsnull; } NS_IF_RELEASE(kNC_Page); NS_IF_RELEASE(kNC_Date); NS_IF_RELEASE(kNC_FirstVisitDate); NS_IF_RELEASE(kNC_VisitCount); NS_IF_RELEASE(kNC_AgeInDays); NS_IF_RELEASE(kNC_Name); NS_IF_RELEASE(kNC_NameSort); NS_IF_RELEASE(kNC_Hostname); NS_IF_RELEASE(kNC_Referrer); NS_IF_RELEASE(kNC_child); NS_IF_RELEASE(kNC_URL); NS_IF_RELEASE(kNC_HistoryRoot); NS_IF_RELEASE(kNC_HistoryByDate); } if (mSyncTimer) mSyncTimer->Cancel(); if (mExpireNowTimer) mExpireNowTimer->Cancel(); } //---------------------------------------------------------------------- // // nsGlobalHistory // // nsISupports methods NS_IMPL_ISUPPORTS7(nsGlobalHistory, nsIGlobalHistory, nsIBrowserHistory, nsIObserver, nsISupportsWeakReference, nsIRDFDataSource, nsIRDFRemoteDataSource, nsIAutoCompleteSession) //---------------------------------------------------------------------- // // nsGlobalHistory // // nsIGlobalHistory methods // NS_IMETHODIMP nsGlobalHistory::AddPage(const char *aURL) { NS_ENSURE_ARG_POINTER(aURL); NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsresult rv; rv = SaveLastPageVisited(aURL); if (NS_FAILED(rv)) return rv; rv = AddPageToDatabase(aURL, GetNow()); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult nsGlobalHistory::AddPageToDatabase(const char *aURL, PRInt64 aDate) { nsresult rv; // Sanity check the URL PRInt32 len = PL_strlen(aURL); NS_ASSERTION(len != 0, "no URL"); if (! len) return NS_ERROR_INVALID_ARG; // For notifying observers, later... nsCOMPtr url; rv = gRDFService->GetResource(aURL, getter_AddRefs(url)); if (NS_FAILED(rv)) return rv; nsCOMPtr date; rv = gRDFService->GetDateLiteral(aDate, getter_AddRefs(date)); if (NS_FAILED(rv)) return rv; nsMdbPtr row(mEnv); rv = FindRow(kToken_URLColumn, aURL, getter_Acquires(row)); if (NS_SUCCEEDED(rv)) { // update the database, and get the old info back PRInt64 oldDate; PRInt32 oldCount; rv = AddExistingPageToDatabase(row, aDate, &oldDate, &oldCount); NS_ASSERTION(NS_SUCCEEDED(rv), "AddExistingPageToDatabase failed; see bug 88961"); if (NS_FAILED(rv)) return rv; // Notify observers // visit date nsCOMPtr oldDateLiteral; rv = gRDFService->GetDateLiteral(oldDate, getter_AddRefs(oldDateLiteral)); if (NS_FAILED(rv)) return rv; rv = NotifyChange(url, kNC_Date, oldDateLiteral, date); if (NS_FAILED(rv)) return rv; // visit count nsCOMPtr oldCountLiteral; rv = gRDFService->GetIntLiteral(oldCount, getter_AddRefs(oldCountLiteral)); if (NS_FAILED(rv)) return rv; nsCOMPtr newCountLiteral; rv = gRDFService->GetIntLiteral(oldCount+1, getter_AddRefs(newCountLiteral)); if (NS_FAILED(rv)) return rv; rv = NotifyChange(url, kNC_VisitCount, oldCountLiteral, newCountLiteral); if (NS_FAILED(rv)) return rv; } else { rv = AddNewPageToDatabase(aURL, aDate, getter_Acquires(row)); NS_ASSERTION(NS_SUCCEEDED(rv), "AddNewPageToDatabase failed; see bug 88961"); if (NS_FAILED(rv)) return rv; // Notify observers rv = NotifyAssert(url, kNC_Date, date); if (NS_FAILED(rv)) return rv; rv = NotifyAssert(kNC_HistoryRoot, kNC_child, url); if (NS_FAILED(rv)) return rv; } NotifyFindAssertions(url, row); SetDirty(); return rv; } nsresult nsGlobalHistory::AddExistingPageToDatabase(nsIMdbRow *row, PRInt64 aDate, PRInt64 *aOldDate, PRInt32 *aOldCount) { nsresult rv; // 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); return NS_OK; } nsresult nsGlobalHistory::AddNewPageToDatabase(const char *aURL, PRInt64 aDate, nsIMdbRow **aResult) { mdb_err err; // Create a new row mdbOid rowId; rowId.mOid_Scope = kToken_HistoryRowScope; rowId.mOid_Id = mdb_id(-1); NS_PRECONDITION(mTable != nsnull, "not initialized"); if (! mTable) return NS_ERROR_NOT_INITIALIZED; nsMdbPtr row(mEnv); err = mTable->NewRow(mEnv, &rowId, getter_Acquires(row)); if (err != 0) return NS_ERROR_FAILURE; // Set the URL SetRowValue(row, kToken_URLColumn, aURL); // Set the date. SetRowValue(row, kToken_LastVisitDateColumn, aDate); SetRowValue(row, kToken_FirstVisitDateColumn, aDate); nsXPIDLCString hostname; nsCOMPtr ioService = do_GetService(NS_IOSERVICE_CONTRACTID); if (!ioService) return NS_ERROR_FAILURE; ioService->ExtractUrlPart(aURL, nsIIOService::url_Host, 0, 0, getter_Copies(hostname)); SetRowValue(row, kToken_HostnameColumn, hostname); *aResult = row; (*aResult)->AddStrongRef(mEnv); return NS_OK; } nsresult nsGlobalHistory::SetRowValue(nsIMdbRow *aRow, mdb_column aCol, const PRInt64& aValue) { mdb_err err; nsCAutoString val; PRInt64ToChars(aValue, val); 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)); // 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 mdbYarn yarn = { (void *)aValue, len, len, 0, 0, nsnull }; #endif 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 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, nsAWritableString& 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 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; } nsresult nsGlobalHistory::GetRowValue(nsIMdbRow *aRow, mdb_column aCol, PRInt64 *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; return CharsToPRInt64((char *)yarn.mYarn_Buf, yarn.mYarn_Fill, aResult); } 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, nsAWritableCString& 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; aResult.Assign(Substring(startPtr, startPtr + yarn.mYarn_Fill)); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::SetPageTitle(const char *aURL, const PRUnichar *aTitle) { NS_PRECONDITION(aURL != nsnull, "null ptr"); if (! aURL) return NS_ERROR_NULL_POINTER; // avoid this one well-known url since we can avoid // reading in the db if (PL_strcmp(aURL, "about:blank")==0) return NS_OK; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); nsresult rv; // Be defensive if somebody sends us a null title. static PRUnichar kEmptyString[] = { 0 }; if (! aTitle) aTitle = kEmptyString; nsMdbPtr row(mEnv); rv = FindRow(kToken_URLColumn, aURL, getter_Acquires(row)); if (NS_FAILED(rv)) return 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, aTitle); // ...and update observers nsCOMPtr url; rv = gRDFService->GetResource(aURL, getter_AddRefs(url)); if (NS_FAILED(rv)) return rv; nsCOMPtr name; rv = gRDFService->GetLiteral(aTitle, 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(const char *aURL) { mdb_err err; nsresult rv; if (!mTable) return NS_ERROR_NOT_INITIALIZED; // find the old row, ignore it if we don't have it nsMdbPtr row(mEnv); rv = FindRow(kToken_URLColumn, aURL, getter_Acquires(row)); if (NS_FAILED(rv)) return NS_OK; // get the resource so we can do the notification nsCOMPtr oldRowResource; gRDFService->GetResource(aURL, getter_AddRefs(oldRowResource)); // remove the row err = mTable->CutRow(mEnv, row); NS_ENSURE_TRUE(err == 0, NS_ERROR_FAILURE); 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"); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::RemovePagesFromHost(const char *aHost, PRBool aEntireDomain) { matchHost_t hostInfo; hostInfo.history = this; hostInfo.entireDomain = aEntireDomain; hostInfo.host = aHost; hostInfo.cachedUrl = nsnull; // todo: leak? return RemoveMatchingRows(matchHostCallback, (void *)&hostInfo, PR_TRUE); } 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; // do smart zero-termination const char* startPtr = (const char *)yarn.mYarn_Buf; rv = NS_NewURI(&hostInfo->cachedUrl, nsCAutoString(Substring(startPtr, startPtr + yarn.mYarn_Fill)).get()); if (NS_FAILED(rv)) return PR_FALSE; nsXPIDLCString urlHost; rv = hostInfo->cachedUrl->GetHost(getter_Copies(urlHost)); if (NS_FAILED(rv)) return PR_FALSE; if (PL_strcmp(urlHost, 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 char *domain = PL_strrstr(urlHost, 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; return Commit(kCompressCommit); } nsresult nsGlobalHistory::RemoveMatchingRows(rowMatchCallback aMatchFunc, void *aClosure, PRBool notify) { 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; // XXX tell RDF observers that we're about to do a batch update // 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) { nsMdbPtr row(mEnv); err = mTable->PosToRow(mEnv, pos, getter_Acquires(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.get(), 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; // Notify observers that the row is, er, history. if (notify) NotifyFindUnassertions(resource, row); // 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"); // XXX tell RDF observers that we're done with the batch return ( err == 0) ? NS_OK : NS_ERROR_FAILURE; } NS_IMETHODIMP nsGlobalHistory::IsVisited(const char *aURL, PRBool *_retval) { NS_PRECONDITION(aURL != nsnull, "null ptr"); if (! aURL) return NS_ERROR_NULL_POINTER; nsresult rv; NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_NOT_INITIALIZED); nsMdbPtr row(mEnv); rv = FindRow(kToken_URLColumn, aURL, getter_Acquires(row)); if (NS_FAILED(rv)) *_retval = PR_FALSE; else *_retval = PR_TRUE; return NS_OK; } nsresult nsGlobalHistory::SaveLastPageVisited(const char *aURL) { nsresult rv; if (!aURL) return NS_ERROR_FAILURE; nsCOMPtr prefs(do_GetService(kPrefCID, &rv)); if (NS_FAILED(rv)) return rv; rv = prefs->SetCharPref(PREF_BROWSER_HISTORY_LAST_PAGE_VISITED, aURL); return rv; } NS_IMETHODIMP nsGlobalHistory::GetLastPageVisited(char **_retval) { nsresult rv; if (!_retval) return NS_ERROR_NULL_POINTER; nsCOMPtr prefs(do_GetService(kPrefCID, &rv)); if (NS_FAILED(rv)) return rv; nsXPIDLCString lastPageVisited; rv = prefs->CopyCharPref(PREF_BROWSER_HISTORY_LAST_PAGE_VISITED, getter_Copies(lastPageVisited)); if (NS_FAILED(rv)) return rv; *_retval = nsCRT::strdup((const char *)lastPageVisited); return NS_OK; } NS_IMETHODIMP nsGlobalHistory::HidePage(const char *aURL) { nsresult rv; nsMdbPtr row(mEnv); rv = FindRow(kToken_URLColumn, aURL, getter_Acquires(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 = AddPage(aURL); if (NS_FAILED(rv)) return rv; rv = FindRow(kToken_URLColumn, aURL, getter_Acquires(row)); if (NS_FAILED(rv)) return rv; } return SetRowValue(row, kToken_HiddenColumn, 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; rv = PRInt64ToChars(n, valueStr); if (NS_FAILED(rv)) return rv; 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_HistoryByDate || IsFindResource(aSource))) { // If they're asking for all the children of the HistoryRoot, call // through to GetTargets() and return the first one. nsCOMPtr targets; rv = GetTargets(aSource, aProperty, aTruthValue, getter_AddRefs(targets)); if (NS_FAILED(rv)) return rv; PRBool hasMore; rv = targets->HasMoreElements(&hasMore); if (NS_FAILED(rv)) return rv; if (! hasMore) return NS_RDF_NO_VALUE; nsCOMPtr isupports; rv = targets->GetNext(getter_AddRefs(isupports)); if (NS_FAILED(rv)) return rv; return CallQueryInterface(isupports, aTarget); } else if ((aProperty == kNC_Date) || (aProperty == kNC_FirstVisitDate) || (aProperty == kNC_VisitCount) || (aProperty == kNC_AgeInDays) || (aProperty == kNC_Name) || (aProperty == kNC_NameSort) || (aProperty == kNC_Hostname) || (aProperty == kNC_Referrer) || (aProperty == kNC_URL)) { const char* uri; rv = aSource->GetValueConst(&uri); if (NS_FAILED(rv)) return rv; // url is self-referential, so we'll always just return itself // however, don't return the URLs of find resources if (aProperty == kNC_URL && !IsFindResource(aSource)) { nsCOMPtr uriLiteral; rv = gRDFService->GetLiteral(NS_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 (((aProperty == kNC_Name) || (aProperty == kNC_NameSort)) && IsFindResource(aSource)) { // for sorting, we sort by uri, so just return the URI as a literal if (aProperty == kNC_NameSort) { nsCOMPtr uriLiteral; rv = gRDFService->GetLiteral(NS_ConvertUTF8toUCS2(uri).get(), getter_AddRefs(uriLiteral)); if (NS_FAILED(rv)) return(rv); *aTarget = uriLiteral; NS_ADDREF(*aTarget); return NS_OK; } else return GetFindUriName(uri, aTarget); } // ok, we got this far, so we have to retrieve something from // the row in the database nsMdbPtr row(mEnv); rv = FindRow(kToken_URLColumn, uri, getter_Acquires(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 PRInt64 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) { PRInt64 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; nsXPIDLCString filename; rv = urlObj->GetFileName(getter_Copies(filename)); if (NS_FAILED(rv) || !(const char*)filename) { // ok fine. we'll use the file path. then we give up! rv = urlObj->GetPath(getter_Copies(filename)); if (PL_strcmp(filename, "/") == 0) { // if the top of a site does not have a title // (common for redirections) then return the hostname return GetTarget(aSource, kNC_Hostname, aTruthValue, aTarget); } } 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; nsCOMPtr resource; rv = gRDFService->GetResource(str.get(), 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; } void nsGlobalHistory::ExpireNow() { mNowValid = PR_FALSE; } // 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->Init(fireSyncTimer, this, HISTORY_SYNC_TIMEOUT, NS_PRIORITY_LOWEST, NS_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 PRInt64 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->Init(expireNowTimer, this, HISTORY_EXPIRE_NOW_TIMEOUT, NS_PRIORITY_LOWEST, NS_TYPE_ONE_SHOT); } return mLastNow; } NS_IMETHODIMP nsGlobalHistory::GetTargets(nsIRDFResource* aSource, nsIRDFResource* aProperty, PRBool aTruthValue, nsISimpleEnumerator** aTargets) { NS_PRECONDITION(aSource != nsnull, "null ptr"); if (! aSource) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aProperty != nsnull, "null ptr"); if (! aProperty) return NS_ERROR_NULL_POINTER; if (!aTruthValue) return NS_NewEmptyEnumerator(aTargets); NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); // list all URLs off the root if ((aSource == kNC_HistoryRoot) && (aProperty == kNC_child)) { URLEnumerator* result = new URLEnumerator(kToken_URLColumn, kToken_HiddenColumn); if (! result) return NS_ERROR_OUT_OF_MEMORY; nsresult rv; rv = result->Init(mEnv, mTable); if (NS_FAILED(rv)) return rv; *aTargets = result; NS_ADDREF(*aTargets); return NS_OK; } else if ((aSource == kNC_HistoryByDate) && (aProperty == kNC_child)) { return GetRootDayQueries(aTargets); } else if (aProperty == kNC_child && IsFindResource(aSource)) { return CreateFindEnumerator(aSource, aTargets); } else if ((aProperty == kNC_Date) || (aProperty == kNC_FirstVisitDate) || (aProperty == kNC_VisitCount) || (aProperty == kNC_AgeInDays) || (aProperty == kNC_Name) || (aProperty == kNC_Hostname) || (aProperty == kNC_Referrer)) { nsresult rv; nsCOMPtr target; rv = GetTarget(aSource, aProperty, aTruthValue, getter_AddRefs(target)); if (NS_FAILED(rv)) return rv; if (rv == NS_OK) { return NS_NewSingletonEnumerator(aTargets, target); } } // we've already answered the queries from the root, so we must be // looking for real entries return NS_NewEmptyEnumerator(aTargets); } NS_IMETHODIMP nsGlobalHistory::Assert(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aTarget, PRBool aTruthValue) { // History cannot be modified return NS_RDF_ASSERTION_REJECTED; } NS_IMETHODIMP nsGlobalHistory::Unassert(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aTarget) { // translate into an appropriate removehistory call nsresult rv; if ((aSource == kNC_HistoryRoot || aSource == kNC_HistoryByDate || IsFindResource(aSource)) && aProperty == kNC_child) { nsCOMPtr resource = do_QueryInterface(aTarget, &rv); if (NS_FAILED(rv)) return NS_RDF_ASSERTION_REJECTED; const char* targetUrl; rv = resource->GetValueConst(&targetUrl); if (NS_FAILED(rv)) return NS_RDF_ASSERTION_REJECTED; // ignore any error rv = RemovePage(targetUrl); if (NS_FAILED(rv)) return NS_RDF_ASSERTION_REJECTED; return NS_OK; } return NS_RDF_ASSERTION_REJECTED; } NS_IMETHODIMP nsGlobalHistory::Change(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aOldTarget, nsIRDFNode* aNewTarget) { return NS_RDF_ASSERTION_REJECTED; } NS_IMETHODIMP nsGlobalHistory::Move(nsIRDFResource* aOldSource, nsIRDFResource* aNewSource, nsIRDFResource* aProperty, nsIRDFNode* aTarget) { return NS_RDF_ASSERTION_REJECTED; } NS_IMETHODIMP nsGlobalHistory::HasAssertion(nsIRDFResource* aSource, nsIRDFResource* aProperty, nsIRDFNode* aTarget, PRBool aTruthValue, PRBool* aHasAssertion) { NS_PRECONDITION(aSource != nsnull, "null ptr"); if (! aSource) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aProperty != nsnull, "null ptr"); if (! aProperty) return NS_ERROR_NULL_POINTER; NS_PRECONDITION(aTarget != nsnull, "null ptr"); if (! aTarget) return NS_ERROR_NULL_POINTER; // Only "positive" assertions here. if (!aTruthValue) { *aHasAssertion = PR_FALSE; return NS_OK; } 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); nsMdbPtr row(mEnv); rv = FindRow(kToken_URLColumn, uri, getter_Acquires(row)); // not even in history. don't bother trying if (NS_FAILED(rv)) { *aHasAssertion = PR_FALSE; return NS_OK; } *aHasAssertion = RowMatches(row, &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_HistoryByDate)) { *result = (aArc == kNC_child); } else if (IsFindResource(aSource)) { // we handle children of find urls *result = (aArc == kNC_child || aArc == kNC_Name || aArc == kNC_NameSort); } else if (IsURLInHistory(aSource)) { // If the URL is in the history, then it'll have all the // appropriate attributes. *result = (aArc == kNC_Date || aArc == kNC_FirstVisitDate || aArc == kNC_VisitCount || aArc == kNC_Name || aArc == kNC_Hostname || aArc == kNC_Referrer); } else { *result = PR_FALSE; } return NS_OK; } NS_IMETHODIMP nsGlobalHistory::ArcLabelsIn(nsIRDFNode* aNode, nsISimpleEnumerator** aLabels) { NS_PRECONDITION(aNode != nsnull, "null ptr"); if (! aNode) return NS_ERROR_NULL_POINTER; 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_HistoryByDate)) { return NS_NewSingletonEnumerator(aLabels, kNC_child); } else if (IsURLInHistory(aSource)) { // If the URL is in the history, then it'll have all the // appropriate attributes. nsCOMPtr array; rv = NS_NewISupportsArray(getter_AddRefs(array)); if (NS_FAILED(rv)) return rv; array->AppendElement(kNC_Date); array->AppendElement(kNC_FirstVisitDate); array->AppendElement(kNC_VisitCount); array->AppendElement(kNC_Name); array->AppendElement(kNC_Hostname); array->AppendElement(kNC_Referrer); return NS_NewArrayEnumerator(aLabels, array); } else if (IsFindResource(aSource)) { nsCOMPtr array; rv = NS_NewISupportsArray(getter_AddRefs(array)); if (NS_FAILED(rv)) return rv; array->AppendElement(kNC_child); array->AppendElement(kNC_Name); array->AppendElement(kNC_NameSort); return NS_NewArrayEnumerator(aLabels, array); } else { return NS_NewEmptyEnumerator(aLabels); } } NS_IMETHODIMP nsGlobalHistory::GetAllCommands(nsIRDFResource* aSource, nsIEnumerator/**/** aCommands) { NS_NOTYETIMPLEMENTED("sorry"); return NS_ERROR_NOT_IMPLEMENTED; } 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; } //////////////////////////////////////////////////////////////////////// // 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); } //---------------------------------------------------------------------- // // 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. This means if the user changes // this pref, we won't notice until the next time we run. nsCOMPtr prefs(do_GetService(kPrefCID, &rv)); if (NS_SUCCEEDED(rv)) rv = prefs->GetIntPref(PREF_BROWSER_HISTORY_EXPIRE_DAYS, &mExpireDays); if (gRefCnt++ == 0) { rv = nsServiceManager::GetService(kRDFServiceCID, NS_GET_IID(nsIRDFService), (nsISupports**) &gRDFService); NS_ASSERTION(NS_SUCCEEDED(rv), "unable to get RDF service"); if (NS_FAILED(rv)) return rv; gRDFService->GetResource(NC_NAMESPACE_URI "Page", &kNC_Page); gRDFService->GetResource(NC_NAMESPACE_URI "Date", &kNC_Date); gRDFService->GetResource(NC_NAMESPACE_URI "FirstVisitDate", &kNC_FirstVisitDate); gRDFService->GetResource(NC_NAMESPACE_URI "VisitCount", &kNC_VisitCount); gRDFService->GetResource(NC_NAMESPACE_URI "AgeInDays", &kNC_AgeInDays); gRDFService->GetResource(NC_NAMESPACE_URI "Name", &kNC_Name); gRDFService->GetResource(NC_NAMESPACE_URI "Name?sort=true", &kNC_NameSort); gRDFService->GetResource(NC_NAMESPACE_URI "Hostname", &kNC_Hostname); gRDFService->GetResource(NC_NAMESPACE_URI "Referrer", &kNC_Referrer); gRDFService->GetResource(NC_NAMESPACE_URI "child", &kNC_child); gRDFService->GetResource(NC_NAMESPACE_URI "URL", &kNC_URL); gRDFService->GetResource("NC:HistoryRoot", &kNC_HistoryRoot); gRDFService->GetResource("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://communicator/locale/history/history.properties", getter_AddRefs(mBundle)); } // register to observe profile changes nsCOMPtr observerService = do_GetService("@mozilla.org/observer-service;1", &rv); NS_ASSERTION(observerService, "failed to get observer service"); if (observerService) { observerService->AddObserver(this, "profile-before-change", PR_TRUE); observerService->AddObserver(this, "profile-do-change", PR_TRUE); } return NS_OK; } nsresult nsGlobalHistory::OpenDB() { nsresult rv; if (mStore) return NS_OK; nsCOMPtr historyFile; rv = NS_GetSpecialDirectory(NS_APP_HISTORY_50_FILE, getter_AddRefs(historyFile)); NS_ENSURE_SUCCESS(rv, rv); static NS_DEFINE_CID(kMorkCID, NS_MORK_CID); nsCOMPtr factoryfactory; rv = nsComponentManager::CreateInstance(kMorkCID, nsnull, NS_GET_IID(nsIMdbFactoryFactory), getter_AddRefs(factoryfactory)); NS_ENSURE_SUCCESS(rv, rv); // Leaving XPCOM, entering MDB. They may look like XPCOM interfaces, // but they're not. The 'factory' is an interface; however, it isn't // reference counted. So no, this isn't a leak. nsIMdbFactory* factory; rv = factoryfactory->GetMdbFactory(&factory); NS_ENSURE_SUCCESS(rv, rv); mdb_err err; err = factory->MakeEnv(nsnull, &mEnv); mEnv->SetAutoClear(PR_TRUE); NS_ASSERTION((err == 0), "unable to create mdb env"); if (err != 0) return NS_ERROR_FAILURE; nsXPIDLCString filePath; rv = historyFile->GetPath(getter_Copies(filePath)); NS_ENSURE_SUCCESS(rv, rv); PRBool exists = PR_TRUE; historyFile->Exists(&exists); if (!exists || NS_FAILED(rv = OpenExistingFile(factory, filePath))) { // 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(factory, filePath); } NS_ENSURE_SUCCESS(rv, rv); 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 nsMdbPtr oldFile(mEnv); // ensures file is released err = factory->OpenOldFile(mEnv, dbHeap, filePath, dbFrozen, getter_Acquires(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); } thumb->CutStrongRef(mEnv); thumb = nsnull; 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; } 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; nsMdbPtr newFile(mEnv); // ensures file is released err = factory->CreateNewFile(mEnv, dbHeap, filePath, getter_Acquires(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; // Force a commit now to get it written out. nsMdbPtr thumb(mEnv); err = mStore->LargeCommit(mEnv, getter_Acquires(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; } // 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; mdb_err err; nsMdbPtr cursor(mEnv); nsMdbPtr row(mEnv); err = mTable->GetTableRowCursor(mEnv, -1, getter_Acquires(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_Acquires(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_Acquires(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; return NS_OK; } nsresult nsGlobalHistory::Commit(eCommitType commitType) { if (!mStore || !mTable) return NS_OK; nsresult err = NS_OK; nsMdbPtr thumb(mEnv); 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_Acquires(thumb)); break; case kSessionCommit: err = mStore->SessionCommit(mEnv, getter_Acquires(thumb)); break; case kCompressCommit: err = mStore->CompressCommit(mEnv, getter_Acquires(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); if (mTable) mTable->CutStrongRef(mEnv); if (mStore) mStore->CutStrongRef(mEnv); if (mEnv) mEnv->CloseMdbObject(mEnv /* XXX */); mTable = nsnull; mEnv = nsnull; mStore = nsnull; return NS_OK; } nsresult nsGlobalHistory::FindRow(mdb_column aCol, const char *aValue, nsIMdbRow **aResult) { mdb_err err; PRInt32 len = PL_strlen(aValue); mdbYarn yarn = { (void*) aValue, len, len, 0, 0, nsnull }; mdbOid rowId; nsMdbPtr row(mEnv); err = mStore->FindRow(mEnv, kToken_HistoryRowScope, aCol, &yarn, &rowId, getter_Acquires(row)); if (!row) return NS_ERROR_NOT_AVAILABLE; // make sure it's actually stored in the main table mdb_bool hasRow; mTable->HasRow(mEnv, row, &hasRow); if (!hasRow) return NS_ERROR_NOT_AVAILABLE; *aResult = row; (*aResult)->AddStrongRef(mEnv); return NS_OK; } PRBool nsGlobalHistory::IsURLInHistory(nsIRDFResource* aResource) { nsresult rv; const char* url; rv = aResource->GetValueConst(&url); if (NS_FAILED(rv)) return PR_FALSE; nsMdbPtr row(mEnv); rv = FindRow(kToken_URLColumn, url, getter_Acquires(row)); 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 is just generates static list of find-style queries // nsresult nsGlobalHistory::GetRootDayQueries(nsISimpleEnumerator **aResult) { nsresult rv; nsCOMPtr dayArray; NS_NewISupportsArray(getter_AddRefs(dayArray)); PRInt32 i; nsCOMPtr finduri; nsDependentCString prefix(FIND_BY_AGEINDAYS_PREFIX "is" "&text="); nsCAutoString uri; for (i=0; i<7; i++) { uri = prefix; uri.AppendInt(i); uri.Append("&groupby=Hostname"); rv = gRDFService->GetResource(uri.get(), getter_AddRefs(finduri)); if (NS_SUCCEEDED(rv)) dayArray->AppendElement(finduri); } uri = FIND_BY_AGEINDAYS_PREFIX "isgreater" "&text="; uri.AppendInt(i-1); uri.Append("&groupby=Hostname"); rv = gRDFService->GetResource(uri.get(), getter_AddRefs(finduri)); if (NS_SUCCEEDED(rv)) rv = dayArray->AppendElement(finduri); PRUint32 arraylength; dayArray->Count(&arraylength); return NS_NewArrayEnumerator(aResult, dayArray); } // // convert the name/value pairs stored in a string into an array of // these pairs // find:a=b&c=d&e=f&g=h // becomes an array containing // {"a" = "b", "c" = "d", "e" = "f", "g" = "h" } // nsresult nsGlobalHistory::FindUrlToTokenList(const char *aURL, nsVoidArray& aResult) { if (PL_strncmp(aURL, "find:", 5) != 0) return NS_ERROR_UNEXPECTED; const char *curpos = aURL + 5; const char *tokenstart = curpos; // this is where we will store the current name and value const char *tokenName = nsnull; const char *tokenValue = nsnull; PRUint32 tokenNameLength=0; PRUint32 tokenValueLength=0; PRBool haveValue = PR_FALSE; // needed because some values are 0-length while (PR_TRUE) { while (*curpos && (*curpos != '&') && (*curpos != '=')) curpos++; if (*curpos == '=') { // just found a token name tokenName = tokenstart; tokenNameLength = (curpos - tokenstart); } else if ((!*curpos || *curpos == '&') && (tokenNameLength>0)) { // found a value, and we have a // name tokenValue = tokenstart; tokenValueLength = (curpos - tokenstart); haveValue = PR_TRUE; } // once we have a name/value pair, store it away // note we're looking at lengths, so that // "find:&a=b" doesn't connect with a="" if (tokenNameLength>0 && haveValue) { tokenPair *tokenStruct = new tokenPair(tokenName, tokenNameLength, tokenValue, tokenValueLength); aResult.AppendElement((void *)tokenStruct); // reset our state tokenName = tokenValue = nsnull; tokenNameLength = tokenValueLength = 0; haveValue = PR_FALSE; } // the test has to be here to catch empty values if (!*curpos) break; curpos++; tokenstart = curpos; } return NS_OK; } void nsGlobalHistory::FreeTokenList(nsVoidArray& tokens) { PRUint32 length = tokens.Count(); PRUint32 i; for (i=0; iGetValueConst(&value); if (NS_FAILED(rv)) return PR_FALSE; return (PL_strncmp(value, "find:", 5)==0); } // // convert a list of name/value pairs into a search query with 0 or // more terms and an optional groupby // // a term consists of the values of the 4 name/value pairs // {datasource, match, method, text} // groupby is stored as a column # // nsresult nsGlobalHistory::TokenListToSearchQuery(const nsVoidArray& aTokens, searchQuery& aResult) { PRInt32 i; PRInt32 length = aTokens.Count(); aResult.groupBy = 0; const char *datasource=nsnull, *property=nsnull, *method=nsnull, *text=nsnull; PRUint32 datasourceLen=0, propertyLen=0, methodLen=0, textLen=0; rowMatchCallback matchCallback=nsnull; // matching callback if needed for (i=0; itokenName, token->tokenName + token->tokenNameLength); if (tokenName.Equals(NS_LITERAL_CSTRING("datasource"))) { datasource = token->tokenValue; datasourceLen = token->tokenValueLength; } else if (tokenName.Equals(NS_LITERAL_CSTRING("match"))) { if (Substring(token->tokenValue, token->tokenValue+token->tokenValueLength).Equals("AgeInDays")) matchCallback = matchAgeInDaysCallback; property = token->tokenValue; propertyLen = token->tokenValueLength; } else if (tokenName.Equals(NS_LITERAL_CSTRING("method"))) { method = token->tokenValue; methodLen = token->tokenValueLength; } else if (tokenName.Equals(NS_LITERAL_CSTRING("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.Equals(NS_LITERAL_CSTRING("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 PRInt64 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.get(), getter_AddRefs(childFindResource)); NotifyAssert(kNC_HistoryByDate, kNC_child, childFindResource); query.terms.Clear(); // 3) AgeInDays=&groupby=Hostname -> // AgeInDays=&Hostname= parentFindResource=childFindResource; // AgeInDays=&groupby=Hostname query.groupBy = 0; // create AgeInDays=&Hostname= query.terms.AppendElement((void *)&ageterm); query.terms.AppendElement((void *)&hostterm); GetFindUriPrefix(query, PR_FALSE, findUri); gRDFService->GetResource(findUri.get(), 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.get(), 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.get(), 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 PRInt64 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); // XXX |sourceStr| unused ... why are we doing this? const char* sourceStr; aSource->GetValueConst(&sourceStr); gRDFService->GetResource(findUri.get(), 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.get(), getter_AddRefs(findResource)); NotifyUnassert(findResource, kNC_child, aSource); return NS_OK; } // // get the user-visible "name" of a find resource // we basically parse the string, and use the data stored in the last // term to generate an appropriate string // nsresult nsGlobalHistory::GetFindUriName(const char *aURL, nsIRDFNode **aResult) { nsresult rv; searchQuery query; rv = FindUrlToSearchQuery(aURL, query); // can't exactly get a name if there's nothing to search for if (query.terms.Count() < 1) return NS_OK; // now build up a string from the query 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)); } 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")) { // expiration date if (!nsCRT::strcmp(aSomeData, NS_LITERAL_STRING(PREF_BROWSER_HISTORY_EXPIRE_DAYS).get())) { nsCOMPtr prefs = do_GetService(kPrefCID, &rv); if (NS_SUCCEEDED(rv)) prefs->GetIntPref(PREF_BROWSER_HISTORY_EXPIRE_DAYS, &mExpireDays); } } else if (!nsCRT::strcmp(aTopic, "profile-before-change")) { rv = CloseDB(); if (!nsCRT::strcmp(aSomeData, NS_LITERAL_STRING("shutdown-cleanse").get())) { nsCOMPtr historyFile; rv = NS_GetSpecialDirectory(NS_APP_HISTORY_50_FILE, getter_AddRefs(historyFile)); if (NS_SUCCEEDED(rv)) rv = historyFile->Remove(PR_FALSE); } } else if (!nsCRT::strcmp(aTopic, "profile-do-change")) rv = OpenDB(); return NS_OK; } //---------------------------------------------------------------------- // // nsGlobalHistory::URLEnumerator // // Implementation nsGlobalHistory::URLEnumerator::~URLEnumerator() { nsMemory::Free(mSelectValue); } PRBool nsGlobalHistory::URLEnumerator::IsResult(nsIMdbRow* aRow) { if (HasCell(mEnv, aRow, mHiddenColumn)) return PR_FALSE; if (mSelectColumn) { mdb_err err; mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, mURLColumn, &yarn); if (err != 0) return PR_FALSE; // Do bitwise comparison PRInt32 count = PRInt32(yarn.mYarn_Fill); if (count != mSelectValueLen) return PR_FALSE; const char* p = (const char*) yarn.mYarn_Buf; const char* q = (const char*) mSelectValue; while (--count >= 0) { if (*p++ != *q++) return PR_FALSE; } } return PR_TRUE; } nsresult nsGlobalHistory::URLEnumerator::ConvertToISupports(nsIMdbRow* aRow, nsISupports** aResult) { mdb_err err; mdbYarn yarn; err = aRow->AliasCellYarn(mEnv, mURLColumn, &yarn); if (err != 0) return NS_ERROR_FAILURE; // Since the URLEnumerator always returns the value of the URL // column, we create an RDF resource. nsresult rv; nsCOMPtr resource; const char* startPtr = (const char*) yarn.mYarn_Buf; rv = gRDFService->GetResource( nsCAutoString(Substring(startPtr, startPtr+yarn.mYarn_Fill)).get(), getter_AddRefs(resource)); if (NS_FAILED(rv)) return rv; *aResult = resource; NS_ADDREF(*aResult); return NS_OK; } //---------------------------------------------------------------------- // nsGlobalHistory::SearchEnumerator // // Implementation nsGlobalHistory::SearchEnumerator::~SearchEnumerator() { // free up the query } // 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, nsAWritableCString& 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; nsCStringKey key(nsCAutoString((const char*)groupColumnValue.mYarn_Buf, groupColumnValue.mYarn_Fill)); void *otherRow = mUniqueRows.Get(&key); // Hey! we've seen this row before, so ignore it if (otherRow) return PR_FALSE; } // now do the actual match if (!mHistory->RowMatches(aRow, mQuery)) return PR_FALSE; if (mQuery->groupBy != 0) { // we got this far, so we must have matched. // add ourselves to the hashtable so we don't match rows like this // in the future nsCStringKey key(nsCAutoString((const char*)groupColumnValue.mYarn_Buf, groupColumnValue.mYarn_Fill)); // note - weak ref, don't worry about releasing mUniqueRows.Put(&key, (void *)aRow); } return PR_TRUE; } // // determines if the row matches the given terms, used above // PRBool nsGlobalHistory::RowMatches(nsIMdbRow *aRow, searchQuery *aQuery) { PRUint32 length = aQuery->terms.Count(); PRUint32 i; for (i=0; iterms[i]; if (!term->datasource.Equals("history")) continue; // we only match against history queries // use callback if it exists if (term->match) { // queue up some values just in case callback needs it // (how would we do this dynamically?) matchSearchTerm_t matchSearchTerm = { mEnv, mStore, term , PR_FALSE}; 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; aRow->AliasCellYarn(mEnv, property_column, &yarn); const char* startPtr = (const char *)yarn.mYarn_Buf; const nsASingleFragmentCString& rowVal = Substring(startPtr, startPtr + yarn.mYarn_Fill); // set up some iterators nsASingleFragmentCString::const_iterator start, end; rowVal.BeginReading(start); rowVal.EndReading(end); // is the cell in unicode or not? Hmm...let's assume so? NS_ConvertUCS2toUTF8 utf8Value(term->text); if (term->method.Equals("is")) { if (utf8Value != rowVal) return PR_FALSE; } else if (term->method.Equals("isnot")) { if (utf8Value == rowVal) return PR_FALSE; } else if (term->method.Equals("contains")) { if (!FindInReadable(utf8Value, start, end)) return PR_FALSE; } else if (term->method.Equals("doesntcontain")) { if (FindInReadable(utf8Value, start, end)) return PR_FALSE; } else if (term->method.Equals("startswith")) { // need to make sure that the found string is // at the beginning of the string nsACString::const_iterator real_start = start; if (!(FindInReadable(utf8Value, start, end) && 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 nsACString::const_iterator real_end = end; if (!(RFindInReadable(utf8Value, start, end) && 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( nsCAutoString(Substring(startPtr, startPtr+yarn.mYarn_Fill)).get(), 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.get(), getter_AddRefs(resource)); if (NS_FAILED(rv)) return rv; *aResult = resource; NS_ADDREF(*aResult); return NS_OK; } //---------------------------------------------------------------------- // // nsGlobalHistory::AutoCompleteEnumerator // // Implementation nsGlobalHistory::AutoCompleteEnumerator::~AutoCompleteEnumerator() { } PRBool nsGlobalHistory::AutoCompleteEnumerator::IsResult(nsIMdbRow* aRow) { if (HasCell(mEnv, aRow, mHiddenColumn)) return PR_FALSE; nsCAutoString url; mHistory->GetRowValue(aRow, mURLColumn, url); nsAutoString url2; url2.AssignWithConversion(url.get()); PRBool result = mHistory->AutoCompleteCompare(url2, mSelectValue, mExclude); return result; } nsresult nsGlobalHistory::AutoCompleteEnumerator::ConvertToISupports(nsIMdbRow* aRow, nsISupports** aResult) { nsCAutoString url; mHistory->GetRowValue(aRow, mURLColumn, url); nsAutoString comments; mHistory->GetRowValue(aRow, mCommentColumn, comments); nsCOMPtr newItem(do_CreateInstance(NS_AUTOCOMPLETEITEM_CONTRACTID)); NS_ENSURE_TRUE(newItem, NS_ERROR_FAILURE); newItem->SetValue(NS_ConvertASCIItoUCS2(url.get())); newItem->SetComment(comments.get()); *aResult = newItem; NS_ADDREF(*aResult); return NS_OK; } //---------------------------------------------------------------------- // // nsIAutoCompleteSession implementation // NS_IMETHODIMP nsGlobalHistory::OnStartLookup(const PRUnichar *searchString, nsIAutoCompleteResults *previousSearchResult, nsIAutoCompleteListener *listener) { NS_ASSERTION(searchString, "searchString can't be null, fix your caller"); NS_ENSURE_SUCCESS(OpenDB(), NS_ERROR_FAILURE); if (!listener) return NS_ERROR_NULL_POINTER; nsresult rv = NS_OK; nsCOMPtr prefs(do_GetService(kPrefCID, &rv)); if (NS_FAILED(rv)) return rv; PRBool enabled = PR_FALSE; prefs->GetBoolPref(PREF_AUTOCOMPLETE_ENABLED, &enabled); if (!enabled || searchString[0] == 0) { listener->OnAutoComplete(nsnull, nsIAutoCompleteStatus::ignored); return NS_OK; } nsCOMPtr results; results = do_CreateInstance(NS_AUTOCOMPLETERESULTS_CONTRACTID, &rv); if (NS_FAILED(rv)) return rv; AutoCompleteStatus status = nsIAutoCompleteStatus::failed; // if the search string is empty after it has had prefixes removed, then // there is no need to proceed with the search nsAutoString cut(searchString); AutoCompleteCutPrefix(cut, nsnull); if (cut.Length() == 0) { listener->OnAutoComplete(results, status); return NS_OK; } // pass string through filter and then determine which prefixes to exclude // when chopping prefixes off of history urls during comparison nsSharableString filtered = AutoCompletePrefilter(nsDependentString(searchString)); AutocompleteExclude exclude; AutoCompleteGetExcludeInfo(filtered, &exclude); // perform the actual search here rv = AutoCompleteSearch(filtered, &exclude, previousSearchResult, results); // describe the search results if (NS_SUCCEEDED(rv)) { results->SetSearchString(searchString); results->SetDefaultItemIndex(0); // determine if we have found any matches or not nsCOMPtr array; rv = results->GetItems(getter_AddRefs(array)); if (NS_SUCCEEDED(rv)) { PRUint32 nbrOfItems; rv = array->Count(&nbrOfItems); if (NS_SUCCEEDED(rv)) { if (nbrOfItems >= 1) { status = nsIAutoCompleteStatus::matchFound; } else { status = nsIAutoCompleteStatus::noMatch; } } } // notify the listener listener->OnAutoComplete(results, status); } return NS_OK; } NS_IMETHODIMP nsGlobalHistory::OnStopLookup() { return NS_OK; } NS_IMETHODIMP nsGlobalHistory::OnAutoComplete(const PRUnichar *searchString, nsIAutoCompleteResults *previousSearchResult, nsIAutoCompleteListener *listener) { return NS_OK; } //---------------------------------------------------------------------- // // AutoComplete stuff // nsresult nsGlobalHistory::AutoCompleteSearch(const nsAReadableString& aSearchString, AutocompleteExclude* aExclude, nsIAutoCompleteResults* aPrevResults, nsIAutoCompleteResults* aResults) { // determine if we can skip searching the whole history and only search // through the previous search results PRBool searchPrevious = PR_FALSE; if (aPrevResults) { nsXPIDLString prevURL; aPrevResults->GetSearchString(getter_Copies(prevURL)); nsDependentString prevURLStr(prevURL); // if search string begins with the previous search string, it's a go searchPrevious = Substring(aSearchString, 0, prevURLStr.Length()).Equals(prevURLStr); } nsCOMPtr resultItems; nsresult rv = aResults->GetItems(getter_AddRefs(resultItems)); if (searchPrevious) { // searching through the previous results... nsCOMPtr prevResultItems; aPrevResults->GetItems(getter_AddRefs(prevResultItems)); PRUint32 count; prevResultItems->Count(&count); for (PRUint32 i = 0; i < count; ++i) { nsCOMPtr item; prevResultItems->GetElementAt(i, getter_AddRefs(item)); // make a copy of the value because AutoCompleteCompare // is destructive nsAutoString url; item->GetValue(url); if (AutoCompleteCompare(url, aSearchString, aExclude)) resultItems->AppendElement(item); } } else { // searching through the entire history... // prepare the search enumerator AutoCompleteEnumerator* enumerator; enumerator = new AutoCompleteEnumerator(this, kToken_URLColumn, kToken_NameColumn, kToken_HiddenColumn, aSearchString, aExclude); rv = enumerator->Init(mEnv, mTable); if (NS_FAILED(rv)) return rv; // store hits in an auto array initially nsAutoVoidArray array; // not using nsCOMPtr here to avoid time spent // refcounting while passing these around between the 3 arrays nsISupports* entry; // step through the enumerator to get the items into 'array' // because we don't know how many items there will be PRBool hasMore; while (PR_TRUE) { enumerator->HasMoreElements(&hasMore); if (!hasMore) break; // addref's each entry as it enters 'array' enumerator->GetNext(&entry); array.AppendElement(entry); } // turn auto array into flat array for quick sort, now that we // know how many items there are PRUint32 count = array.Count(); nsIAutoCompleteItem** items = new nsIAutoCompleteItem*[count]; PRUint32 i; for (i = 0; i < count; ++i) items[i] = (nsIAutoCompleteItem*)array.ElementAt(i); // sort it NS_QuickSort(items, count, sizeof(nsIAutoCompleteItem*), AutoCompleteSortComparison, nsnull); // place the sorted array into the autocomplete results for (i = 0; i < count; ++i) { nsISupports* item = (nsISupports*)items[i]; resultItems->AppendElement(item); NS_IF_RELEASE(item); // release manually since we didn't use nsCOMPtr above } delete[] items; } 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(nsAReadableString& 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(nsAWritableString& 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); } nsSharableString nsGlobalHistory::AutoCompletePrefilter(const nsAReadableString& 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); host.ToLowerCase(); 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 url.ToLowerCase(); } return nsSharableString(url); } PRBool nsGlobalHistory::AutoCompleteCompare(nsAString& aHistoryURL, const nsAReadableString& aUserURL, AutocompleteExclude* aExclude) { AutoCompleteCutPrefix(aHistoryURL, aExclude); return Substring(aHistoryURL, 0, aUserURL.Length()).Equals(aUserURL); } int PR_CALLBACK AutoCompleteSortComparison(const void *v1, const void *v2, void *unused) { nsIAutoCompleteItem *item1 = *(nsIAutoCompleteItem**) v1; nsIAutoCompleteItem *item2 = *(nsIAutoCompleteItem**) v2; nsAutoString s1; item1->GetValue(s1); nsAutoString s2; item2->GetValue(s2); return nsCRT::strcmp(s1.get(), s2.get()); }