1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 #include "msgCore.h"
7 #include "nntpCore.h"
8 #include "netCore.h"
9 #include "nsIMsgNewsFolder.h"
10 #include "nsIStringBundle.h"
11 #include "nsNewsDownloader.h"
12 #include "nsINntpService.h"
13 #include "nsMsgNewsCID.h"
14 #include "nsIMsgSearchSession.h"
15 #include "nsIMsgSearchTerm.h"
16 #include "nsIMsgAccountManager.h"
17 #include "nsMsgFolderFlags.h"
18 #include "nsIMsgMailSession.h"
19 #include "nsMsgMessageFlags.h"
20 #include "nsServiceManagerUtils.h"
21 #include "nsComponentManagerUtils.h"
22 #include "nsMsgUtils.h"
23 #include "mozilla/Services.h"
24 
25 // This file contains the news article download state machine.
26 
27 // if pIds is not null, download the articles whose id's are passed in.
28 // Otherwise, which articles to download is determined by nsNewsDownloader
29 // object, or subclasses thereof. News can download marked objects, for example.
DownloadArticles(nsIMsgWindow * window,nsIMsgFolder * folder,nsTArray<nsMsgKey> * pIds)30 nsresult nsNewsDownloader::DownloadArticles(nsIMsgWindow* window,
31                                             nsIMsgFolder* folder,
32                                             nsTArray<nsMsgKey>* pIds) {
33   if (pIds != nullptr)
34     m_keysToDownload.InsertElementsAt(0, pIds->Elements(), pIds->Length());
35 
36   if (!m_keysToDownload.IsEmpty()) m_downloadFromKeys = true;
37 
38   m_folder = folder;
39   m_window = window;
40   m_numwrote = 0;
41 
42   bool headersToDownload = GetNextHdrToRetrieve();
43   // should we have a special error code for failure here?
44   return (headersToDownload) ? DownloadNext(true) : NS_ERROR_FAILURE;
45 }
46 
47 /* Saving news messages
48  */
49 
NS_IMPL_ISUPPORTS(nsNewsDownloader,nsIUrlListener,nsIMsgSearchNotify)50 NS_IMPL_ISUPPORTS(nsNewsDownloader, nsIUrlListener, nsIMsgSearchNotify)
51 
52 nsNewsDownloader::nsNewsDownloader(nsIMsgWindow* window, nsIMsgDatabase* msgDB,
53                                    nsIUrlListener* listener) {
54   m_numwrote = 0;
55   m_downloadFromKeys = false;
56   m_newsDB = msgDB;
57   m_abort = false;
58   m_listener = listener;
59   m_window = window;
60   m_lastPercent = -1;
61   m_lastProgressTime = 0;
62   // not the perfect place for this, but I think it will work.
63   if (m_window) m_window->SetStopped(false);
64 }
65 
~nsNewsDownloader()66 nsNewsDownloader::~nsNewsDownloader() {
67   if (m_listener)
68     m_listener->OnStopRunningUrl(/* don't have a url */ nullptr, m_status);
69   if (m_newsDB) {
70     m_newsDB->Commit(nsMsgDBCommitType::kLargeCommit);
71     m_newsDB = nullptr;
72   }
73 }
74 
OnStartRunningUrl(nsIURI * url)75 NS_IMETHODIMP nsNewsDownloader::OnStartRunningUrl(nsIURI* url) { return NS_OK; }
76 
OnStopRunningUrl(nsIURI * url,nsresult exitCode)77 NS_IMETHODIMP nsNewsDownloader::OnStopRunningUrl(nsIURI* url,
78                                                  nsresult exitCode) {
79   bool stopped = false;
80   if (m_window) m_window->GetStopped(&stopped);
81   if (stopped) exitCode = NS_BINDING_ABORTED;
82 
83   nsresult rv = exitCode;
84   if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND)
85     rv = DownloadNext(false);
86 
87   return rv;
88 }
89 
DownloadNext(bool firstTimeP)90 nsresult nsNewsDownloader::DownloadNext(bool firstTimeP) {
91   nsresult rv;
92   if (!firstTimeP) {
93     bool moreHeaders = GetNextHdrToRetrieve();
94     if (!moreHeaders) {
95       if (m_listener) m_listener->OnStopRunningUrl(nullptr, NS_OK);
96       return NS_OK;
97     }
98   }
99   StartDownload();
100   m_wroteAnyP = false;
101   nsCOMPtr<nsINntpService> nntpService =
102       do_GetService(NS_NNTPSERVICE_CONTRACTID, &rv);
103   NS_ENSURE_SUCCESS(rv, rv);
104 
105   return nntpService->FetchMessage(m_folder, m_keyToDownload, m_window, nullptr,
106                                    this, nullptr);
107 }
108 
GetNextHdrToRetrieve()109 bool DownloadNewsArticlesToOfflineStore::GetNextHdrToRetrieve() {
110   nsresult rv;
111 
112   if (m_downloadFromKeys) return nsNewsDownloader::GetNextHdrToRetrieve();
113 
114   if (m_headerEnumerator == nullptr)
115     rv = m_newsDB->EnumerateMessages(getter_AddRefs(m_headerEnumerator));
116 
117   bool hasMore = false;
118 
119   while (NS_SUCCEEDED(rv = m_headerEnumerator->HasMoreElements(&hasMore)) &&
120          hasMore) {
121     rv = m_headerEnumerator->GetNext(getter_AddRefs(m_newsHeader));
122     NS_ENSURE_SUCCESS(rv, false);
123     uint32_t hdrFlags;
124     m_newsHeader->GetFlags(&hdrFlags);
125     if (hdrFlags & nsMsgMessageFlags::Marked) {
126       m_newsHeader->GetMessageKey(&m_keyToDownload);
127       break;
128     } else {
129       m_newsHeader = nullptr;
130     }
131   }
132   return hasMore;
133 }
134 
Abort()135 void nsNewsDownloader::Abort() {}
Complete()136 void nsNewsDownloader::Complete() {}
137 
GetNextHdrToRetrieve()138 bool nsNewsDownloader::GetNextHdrToRetrieve() {
139   nsresult rv;
140   if (m_downloadFromKeys) {
141     if (m_numwrote >= (int32_t)m_keysToDownload.Length()) return false;
142 
143     m_keyToDownload = m_keysToDownload[m_numwrote++];
144     int32_t percent;
145     percent = (100 * m_numwrote) / (int32_t)m_keysToDownload.Length();
146 
147     int64_t nowMS = 0;
148     if (percent < 100)  // always need to do 100%
149     {
150       nowMS = PR_IntervalToMilliseconds(PR_IntervalNow());
151       if (nowMS - m_lastProgressTime < 750) return true;
152     }
153 
154     m_lastProgressTime = nowMS;
155     nsCOMPtr<nsIStringBundleService> bundleService =
156         mozilla::services::GetStringBundleService();
157     NS_ENSURE_TRUE(bundleService, false);
158     nsCOMPtr<nsIStringBundle> bundle;
159     rv = bundleService->CreateBundle(NEWS_MSGS_URL, getter_AddRefs(bundle));
160     NS_ENSURE_SUCCESS(rv, false);
161 
162     nsAutoString firstStr;
163     firstStr.AppendInt(m_numwrote);
164     nsAutoString totalStr;
165     totalStr.AppendInt(int(m_keysToDownload.Length()));
166     nsString prettyName;
167     nsString statusString;
168 
169     m_folder->GetPrettyName(prettyName);
170 
171     AutoTArray<nsString, 3> formatStrings = {firstStr, totalStr, prettyName};
172     rv = bundle->FormatStringFromName("downloadingArticlesForOffline",
173                                       formatStrings, statusString);
174     NS_ENSURE_SUCCESS(rv, false);
175     ShowProgress(statusString.get(), percent);
176     return true;
177   }
178   NS_ASSERTION(false, "shouldn't get here if we're not downloading from keys.");
179   return false;  // shouldn't get here if we're not downloading from keys.
180 }
181 
ShowProgress(const char16_t * progressString,int32_t percent)182 nsresult nsNewsDownloader::ShowProgress(const char16_t* progressString,
183                                         int32_t percent) {
184   if (!m_statusFeedback) {
185     if (m_window) m_window->GetStatusFeedback(getter_AddRefs(m_statusFeedback));
186   }
187   if (m_statusFeedback) {
188     m_statusFeedback->ShowStatusString(nsDependentString(progressString));
189     if (percent != m_lastPercent) {
190       m_statusFeedback->ShowProgress(percent);
191       m_lastPercent = percent;
192     }
193   }
194   return NS_OK;
195 }
196 
OnStartRunningUrl(nsIURI * url)197 NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStartRunningUrl(
198     nsIURI* url) {
199   return NS_OK;
200 }
201 
OnStopRunningUrl(nsIURI * url,nsresult exitCode)202 NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStopRunningUrl(
203     nsIURI* url, nsresult exitCode) {
204   m_status = exitCode;
205   if (m_newsHeader != nullptr) {
206 #ifdef DEBUG_bienvenu
207     //    XP_Trace("finished retrieving %ld\n", m_newsHeader->GetMessageKey());
208 #endif
209     if (m_newsDB) {
210       nsMsgKey msgKey;
211       m_newsHeader->GetMessageKey(&msgKey);
212       m_newsDB->MarkMarked(msgKey, false, nullptr);
213     }
214   }
215   m_newsHeader = nullptr;
216   return nsNewsDownloader::OnStopRunningUrl(url, exitCode);
217 }
218 
FinishDownload()219 int DownloadNewsArticlesToOfflineStore::FinishDownload() { return 0; }
220 
OnSearchHit(nsIMsgDBHdr * header,nsIMsgFolder * folder)221 NS_IMETHODIMP nsNewsDownloader::OnSearchHit(nsIMsgDBHdr* header,
222                                             nsIMsgFolder* folder) {
223   NS_ENSURE_ARG(header);
224 
225   uint32_t msgFlags;
226   header->GetFlags(&msgFlags);
227   // only need to download articles we don't already have...
228   if (!(msgFlags & nsMsgMessageFlags::Offline)) {
229     nsMsgKey key;
230     header->GetMessageKey(&key);
231     m_keysToDownload.AppendElement(key);
232   }
233   return NS_OK;
234 }
235 
OnSearchDone(nsresult status)236 NS_IMETHODIMP nsNewsDownloader::OnSearchDone(nsresult status) {
237   if (m_keysToDownload.IsEmpty()) {
238     if (m_listener) return m_listener->OnStopRunningUrl(nullptr, NS_OK);
239   }
240   nsresult rv = DownloadArticles(
241       m_window, m_folder,
242       /* we've already set m_keysToDownload, so don't pass it in */ nullptr);
243   if (NS_FAILED(rv))
244     if (m_listener) m_listener->OnStopRunningUrl(nullptr, rv);
245 
246   return rv;
247 }
OnNewSearch()248 NS_IMETHODIMP nsNewsDownloader::OnNewSearch() { return NS_OK; }
249 
StartDownload()250 int DownloadNewsArticlesToOfflineStore::StartDownload() {
251   m_newsDB->GetMsgHdrForKey(m_keyToDownload, getter_AddRefs(m_newsHeader));
252   return 0;
253 }
254 
DownloadNewsArticlesToOfflineStore(nsIMsgWindow * window,nsIMsgDatabase * db,nsIUrlListener * listener)255 DownloadNewsArticlesToOfflineStore::DownloadNewsArticlesToOfflineStore(
256     nsIMsgWindow* window, nsIMsgDatabase* db, nsIUrlListener* listener)
257     : nsNewsDownloader(window, db, listener) {
258   m_newsDB = db;
259 }
260 
~DownloadNewsArticlesToOfflineStore()261 DownloadNewsArticlesToOfflineStore::~DownloadNewsArticlesToOfflineStore() {}
262 
DownloadMatchingNewsArticlesToNewsDB(nsIMsgWindow * window,nsIMsgFolder * folder,nsIMsgDatabase * newsDB,nsIUrlListener * listener)263 DownloadMatchingNewsArticlesToNewsDB::DownloadMatchingNewsArticlesToNewsDB(
264     nsIMsgWindow* window, nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
265     nsIUrlListener* listener)
266     : DownloadNewsArticlesToOfflineStore(window, newsDB, listener) {
267   m_window = window;
268   m_folder = folder;
269   m_newsDB = newsDB;
270   m_downloadFromKeys = true;  // search term matching means downloadFromKeys.
271 }
272 
~DownloadMatchingNewsArticlesToNewsDB()273 DownloadMatchingNewsArticlesToNewsDB::~DownloadMatchingNewsArticlesToNewsDB() {}
274 
NS_IMPL_ISUPPORTS(nsMsgDownloadAllNewsgroups,nsIUrlListener)275 NS_IMPL_ISUPPORTS(nsMsgDownloadAllNewsgroups, nsIUrlListener)
276 
277 nsMsgDownloadAllNewsgroups::nsMsgDownloadAllNewsgroups(
278     nsIMsgWindow* window, nsIUrlListener* listener) {
279   m_window = window;
280   m_listener = listener;
281   m_downloaderForGroup =
282       new DownloadMatchingNewsArticlesToNewsDB(window, nullptr, nullptr, this);
283   m_downloadedHdrsForCurGroup = false;
284 }
285 
~nsMsgDownloadAllNewsgroups()286 nsMsgDownloadAllNewsgroups::~nsMsgDownloadAllNewsgroups() {}
287 
OnStartRunningUrl(nsIURI * url)288 NS_IMETHODIMP nsMsgDownloadAllNewsgroups::OnStartRunningUrl(nsIURI* url) {
289   return NS_OK;
290 }
291 
292 NS_IMETHODIMP
OnStopRunningUrl(nsIURI * url,nsresult exitCode)293 nsMsgDownloadAllNewsgroups::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
294   nsresult rv = exitCode;
295   if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND) {
296     if (m_downloadedHdrsForCurGroup) {
297       bool savingArticlesOffline = false;
298       nsCOMPtr<nsIMsgNewsFolder> newsFolder =
299           do_QueryInterface(m_currentFolder);
300       if (newsFolder) newsFolder->GetSaveArticleOffline(&savingArticlesOffline);
301 
302       m_downloadedHdrsForCurGroup = false;
303       if (savingArticlesOffline)  // skip this group - we're saving to it
304                                   // already
305         rv = ProcessNextGroup();
306       else
307         rv = DownloadMsgsForCurrentGroup();
308     } else {
309       rv = ProcessNextGroup();
310     }
311   } else if (m_listener)  // notify main observer.
312     m_listener->OnStopRunningUrl(url, exitCode);
313 
314   return rv;
315 }
316 
317 /**
318  * Leaves m_currentServer at the next nntp "server" that
319  * might have folders to download for offline use. If no more servers,
320  * m_currentServer will be left at nullptr and the function returns false.
321  * Also, sets up m_folderQueue to hold a (reversed) list of all the folders
322  * to consider for the current server.
323  * If no servers found, returns false.
324  */
AdvanceToNextServer()325 bool nsMsgDownloadAllNewsgroups::AdvanceToNextServer() {
326   nsresult rv;
327 
328   if (m_allServers.IsEmpty()) {
329     nsCOMPtr<nsIMsgAccountManager> accountManager =
330         do_GetService(NS_MSGACCOUNTMANAGER_CONTRACTID, &rv);
331     NS_ASSERTION(accountManager && NS_SUCCEEDED(rv),
332                  "couldn't get account mgr");
333     if (!accountManager || NS_FAILED(rv)) return false;
334 
335     rv = accountManager->GetAllServers(m_allServers);
336     NS_ENSURE_SUCCESS(rv, false);
337   }
338   size_t serverIndex = 0;
339   if (m_currentServer) {
340     serverIndex = m_allServers.IndexOf(m_currentServer);
341     if (serverIndex == m_allServers.NoIndex) {
342       serverIndex = 0;
343     } else {
344       ++serverIndex;
345     }
346   }
347   m_currentServer = nullptr;
348   uint32_t numServers = m_allServers.Length();
349   nsCOMPtr<nsIMsgFolder> rootFolder;
350 
351   while (serverIndex < numServers) {
352     nsCOMPtr<nsIMsgIncomingServer> server(m_allServers[serverIndex]);
353     serverIndex++;
354 
355     nsCOMPtr<nsINntpIncomingServer> newsServer = do_QueryInterface(server);
356     if (!newsServer)  // we're only looking for news servers
357       continue;
358 
359     if (server) {
360       m_currentServer = server;
361       server->GetRootFolder(getter_AddRefs(rootFolder));
362       if (rootFolder) {
363         rv = rootFolder->GetDescendants(m_folderQueue);
364         if (NS_SUCCEEDED(rv)) {
365           if (!m_folderQueue.IsEmpty()) {
366             // We'll be popping folders from the end of the queue as we go.
367             m_folderQueue.Reverse();
368             return true;
369           }
370         }
371       }
372     }
373   }
374   return false;
375 }
376 
377 /**
378  * Sets m_currentFolder to the next usable folder.
379  *
380  * @return  False if no more folders found, otherwise true.
381  */
AdvanceToNextGroup()382 bool nsMsgDownloadAllNewsgroups::AdvanceToNextGroup() {
383   nsresult rv = NS_OK;
384 
385   if (m_currentFolder) {
386     nsCOMPtr<nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
387     if (newsFolder) newsFolder->SetSaveArticleOffline(false);
388 
389     nsCOMPtr<nsIMsgMailSession> session =
390         do_GetService(NS_MSGMAILSESSION_CONTRACTID, &rv);
391     if (NS_SUCCEEDED(rv) && session) {
392       bool folderOpen;
393       uint32_t folderFlags;
394       m_currentFolder->GetFlags(&folderFlags);
395       session->IsFolderOpenInWindow(m_currentFolder, &folderOpen);
396       if (!folderOpen &&
397           !(folderFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox)))
398         m_currentFolder->SetMsgDatabase(nullptr);
399     }
400     m_currentFolder = nullptr;
401   }
402 
403   bool hasMore = false;
404   if (m_currentServer) {
405     hasMore = !m_folderQueue.IsEmpty();
406   }
407   if (!hasMore) {
408     hasMore = AdvanceToNextServer();
409   }
410 
411   if (hasMore) {
412     m_currentFolder = m_folderQueue.PopLastElement();
413   }
414   return m_currentFolder;
415 }
416 
RunSearch(nsIMsgFolder * folder,nsIMsgDatabase * newsDB,nsIMsgSearchSession * searchSession)417 nsresult DownloadMatchingNewsArticlesToNewsDB::RunSearch(
418     nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
419     nsIMsgSearchSession* searchSession) {
420   m_folder = folder;
421   m_newsDB = newsDB;
422   m_searchSession = searchSession;
423 
424   m_keysToDownload.Clear();
425 
426   NS_ENSURE_ARG(searchSession);
427   NS_ENSURE_ARG(folder);
428 
429   searchSession->RegisterListener(this, nsIMsgSearchSession::allNotifications);
430   nsresult rv =
431       searchSession->AddScopeTerm(nsMsgSearchScope::localNews, folder);
432   NS_ENSURE_SUCCESS(rv, rv);
433 
434   return searchSession->Search(m_window);
435 }
436 
ProcessNextGroup()437 nsresult nsMsgDownloadAllNewsgroups::ProcessNextGroup() {
438   bool done = false;
439 
440   while (!done) {
441     done = !AdvanceToNextGroup();
442     if (!done && m_currentFolder) {
443       uint32_t folderFlags;
444       m_currentFolder->GetFlags(&folderFlags);
445       if (folderFlags & nsMsgFolderFlags::Offline) break;
446     }
447   }
448   if (done) {
449     if (m_listener) return m_listener->OnStopRunningUrl(nullptr, NS_OK);
450   }
451   m_downloadedHdrsForCurGroup = true;
452   return m_currentFolder ? m_currentFolder->GetNewMessages(m_window, this)
453                          : NS_ERROR_NOT_INITIALIZED;
454 }
455 
DownloadMsgsForCurrentGroup()456 nsresult nsMsgDownloadAllNewsgroups::DownloadMsgsForCurrentGroup() {
457   NS_ENSURE_TRUE(m_downloaderForGroup, NS_ERROR_OUT_OF_MEMORY);
458   nsCOMPtr<nsIMsgDatabase> db;
459   nsCOMPtr<nsIMsgDownloadSettings> downloadSettings;
460   m_currentFolder->GetMsgDatabase(getter_AddRefs(db));
461   nsresult rv =
462       m_currentFolder->GetDownloadSettings(getter_AddRefs(downloadSettings));
463   NS_ENSURE_SUCCESS(rv, rv);
464 
465   nsCOMPtr<nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
466   if (newsFolder) newsFolder->SetSaveArticleOffline(true);
467 
468   nsCOMPtr<nsIMsgSearchSession> searchSession =
469       do_CreateInstance(NS_MSGSEARCHSESSION_CONTRACTID, &rv);
470   NS_ENSURE_SUCCESS(rv, rv);
471 
472   bool downloadByDate, downloadUnreadOnly;
473   uint32_t ageLimitOfMsgsToDownload;
474 
475   downloadSettings->GetDownloadByDate(&downloadByDate);
476   downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly);
477   downloadSettings->GetAgeLimitOfMsgsToDownload(&ageLimitOfMsgsToDownload);
478 
479   nsCOMPtr<nsIMsgSearchTerm> term;
480   nsCOMPtr<nsIMsgSearchValue> value;
481 
482   rv = searchSession->CreateTerm(getter_AddRefs(term));
483   NS_ENSURE_SUCCESS(rv, rv);
484   term->GetValue(getter_AddRefs(value));
485 
486   if (downloadUnreadOnly) {
487     value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
488     value->SetStatus(nsMsgMessageFlags::Read);
489     searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus,
490                                  nsMsgSearchOp::Isnt, value, true, nullptr);
491   }
492   if (downloadByDate) {
493     value->SetAttrib(nsMsgSearchAttrib::AgeInDays);
494     value->SetAge(ageLimitOfMsgsToDownload);
495     searchSession->AddSearchTerm(nsMsgSearchAttrib::AgeInDays,
496                                  nsMsgSearchOp::IsLessThan, value,
497                                  nsMsgSearchBooleanOp::BooleanAND, nullptr);
498   }
499   value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
500   value->SetStatus(nsMsgMessageFlags::Offline);
501   searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus,
502                                nsMsgSearchOp::Isnt, value,
503                                nsMsgSearchBooleanOp::BooleanAND, nullptr);
504 
505   m_downloaderForGroup->RunSearch(m_currentFolder, db, searchSession);
506   return rv;
507 }
508