diff --git a/atom/browser/api/atom_api_session.cc b/atom/browser/api/atom_api_session.cc index 7bc1a2153e3f..3c92266be33c 100644 --- a/atom/browser/api/atom_api_session.cc +++ b/atom/browser/api/atom_api_session.cc @@ -11,6 +11,7 @@ #include "atom/browser/api/atom_api_download_item.h" #include "atom/browser/atom_browser_context.h" #include "atom/browser/api/atom_api_web_contents.h" +#include "atom/browser/api/save_page_handler.h" #include "atom/common/native_mate_converters/callback.h" #include "atom/common/native_mate_converters/gurl_converter.h" #include "atom/common/native_mate_converters/file_path_converter.h" @@ -237,6 +238,8 @@ Session::~Session() { void Session::OnDownloadCreated(content::DownloadManager* manager, content::DownloadItem* item) { auto web_contents = item->GetWebContents(); + if (SavePageHandler::IsSavePageTypes(item->GetMimeType())) + return; bool prevent_default = Emit( "will-download", DownloadItem::Create(isolate(), item), diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index 0b8a9882cd1b..5b3ef2f4aa7b 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -162,6 +162,26 @@ struct Converter { } }; +template<> +struct Converter { + static bool FromV8(v8::Isolate* isolate, v8::Local val, + content::SavePageType* out) { + std::string save_type; + if (!ConvertFromV8(isolate, val, &save_type)) + return false; + if (save_type == "HTMLOnly") { + *out = content::SAVE_PAGE_TYPE_AS_ONLY_HTML; + } else if (save_type == "HTMLComplete") { + *out = content::SAVE_PAGE_TYPE_AS_COMPLETE_HTML; + } else if (save_type == "MHTML") { + *out = content::SAVE_PAGE_TYPE_AS_MHTML; + } else { + return false; + } + return true; + } +}; + } // namespace mate @@ -665,6 +685,13 @@ void WebContents::InsertCSS(const std::string& css) { web_contents()->InsertCSS(css); } +bool WebContents::SavePage(const base::FilePath& full_file_path, + const content::SavePageType& save_type, + const SavePageHandler::SavePageCallback& callback) { + auto handler = new SavePageHandler(web_contents(), callback); + return handler->Handle(full_file_path, save_type); +} + void WebContents::ExecuteJavaScript(const base::string16& code, bool has_user_gesture) { Send(new AtomViewMsg_ExecuteJavaScript(routing_id(), code, has_user_gesture)); @@ -976,6 +1003,7 @@ mate::ObjectTemplateBuilder WebContents::GetObjectTemplateBuilder( .SetMethod("setUserAgent", &WebContents::SetUserAgent) .SetMethod("getUserAgent", &WebContents::GetUserAgent) .SetMethod("insertCSS", &WebContents::InsertCSS) + .SetMethod("savePage", &WebContents::SavePage) .SetMethod("_executeJavaScript", &WebContents::ExecuteJavaScript) .SetMethod("openDevTools", &WebContents::OpenDevTools) .SetMethod("closeDevTools", &WebContents::CloseDevTools) diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index 01075c450a53..7fa8951106b2 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -9,6 +9,7 @@ #include #include "atom/browser/api/frame_subscriber.h" +#include "atom/browser/api/save_page_handler.h" #include "atom/browser/api/trackable_object.h" #include "atom/browser/common_web_contents_delegate.h" #include "content/public/browser/web_contents_observer.h" @@ -73,6 +74,9 @@ class WebContents : public mate::TrackableObject, void SetUserAgent(const std::string& user_agent); std::string GetUserAgent(); void InsertCSS(const std::string& css); + bool SavePage(const base::FilePath& full_file_path, + const content::SavePageType& save_type, + const SavePageHandler::SavePageCallback& callback); void ExecuteJavaScript(const base::string16& code, bool has_user_gesture); void OpenDevTools(mate::Arguments* args); diff --git a/atom/browser/api/save_page_handler.cc b/atom/browser/api/save_page_handler.cc new file mode 100644 index 000000000000..42b93befddf6 --- /dev/null +++ b/atom/browser/api/save_page_handler.cc @@ -0,0 +1,78 @@ +// Copyright (c) 2015 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/api/save_page_handler.h" + +#include + +#include "atom/browser/atom_browser_context.h" +#include "base/callback.h" +#include "base/files/file_path.h" +#include "content/public/browser/web_contents.h" + +namespace atom { + +namespace api { + +SavePageHandler::SavePageHandler(content::WebContents* web_contents, + const SavePageCallback& callback) + : web_contents_(web_contents), + callback_(callback) { +} + +SavePageHandler::~SavePageHandler() { +} + +void SavePageHandler::OnDownloadCreated(content::DownloadManager* manager, + content::DownloadItem* item) { + // OnDownloadCreated is invoked during WebContents::SavePage, so the |item| + // here is the one stated by WebContents::SavePage. + item->AddObserver(this); +} + +bool SavePageHandler::Handle(const base::FilePath& full_path, + const content::SavePageType& save_type) { + auto download_manager = content::BrowserContext::GetDownloadManager( + web_contents_->GetBrowserContext()); + download_manager->AddObserver(this); + bool result = web_contents_->SavePage(full_path, + full_path.DirName(), + save_type); + download_manager->RemoveObserver(this); + // If initialization fails which means fail to create |DownloadItem|, we need + // to delete the |SavePageHandler| instance to avoid memory-leak. + if (!result) + delete this; + return result; +} + +void SavePageHandler::OnDownloadUpdated(content::DownloadItem* item) { + if (item->IsDone()) { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + v8::Locker locker(isolate); + v8::HandleScope handle_scope(isolate); + if (item->GetState() == content::DownloadItem::COMPLETE) { + callback_.Run(v8::Null(isolate)); + } else { + v8::Local error_message = v8::String::NewFromUtf8( + isolate, "Fail to save page"); + callback_.Run(v8::Exception::Error(error_message)); + } + Destroy(item); + } +} + +void SavePageHandler::Destroy(content::DownloadItem* item) { + item->RemoveObserver(this); + delete this; +} + +// static +bool SavePageHandler::IsSavePageTypes(const std::string& type) { + return type == "multipart/related" || type == "text/html"; +} + +} // namespace api + +} // namespace atom diff --git a/atom/browser/api/save_page_handler.h b/atom/browser/api/save_page_handler.h new file mode 100644 index 000000000000..dd1692a8badc --- /dev/null +++ b/atom/browser/api/save_page_handler.h @@ -0,0 +1,60 @@ +// Copyright (c) 2015 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_API_SAVE_PAGE_HANDLER_H_ +#define ATOM_BROWSER_API_SAVE_PAGE_HANDLER_H_ + +#include + +#include "content/public/browser/download_item.h" +#include "content/public/browser/download_manager.h" +#include "content/public/browser/save_page_type.h" +#include "v8/include/v8.h" + +namespace base { +class FilePath; +} + +namespace content { +class WebContents; +} + +namespace atom { + +namespace api { + +// A self-destroyed class for handling save page request. +class SavePageHandler : public content::DownloadManager::Observer, + public content::DownloadItem::Observer { + public: + using SavePageCallback = base::Callback)>; + + SavePageHandler(content::WebContents* web_contents, + const SavePageCallback& callback); + ~SavePageHandler(); + + bool Handle(const base::FilePath& full_path, + const content::SavePageType& save_type); + + static bool IsSavePageTypes(const std::string& type); + + private: + void Destroy(content::DownloadItem* item); + + // content::DownloadManager::Observer: + void OnDownloadCreated(content::DownloadManager* manager, + content::DownloadItem* item) override; + + // content::DownloadItem::Observer: + void OnDownloadUpdated(content::DownloadItem* item) override; + + content::WebContents* web_contents_; // weak + SavePageCallback callback_; +}; + +} // namespace api + +} // namespace atom + +#endif // ATOM_BROWSER_API_SAVE_PAGE_HANDLER_H_ diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 08670bad9df7..006d7e3934b9 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -631,3 +631,26 @@ Get the `WebContents` of DevTools for this `WebContents`. **Note:** Users should never store this object because it may become `null` when the DevTools has been closed. + +### `webContents.savePage(fullPath, saveType, callback)` + +* `fullPath` String - The full file path. +* `saveType` String - Specify the save type. + * `HTMLOnly` - Save only the HTML of the page. + * `HTMLComplete` - Save complete-html page. + * `MHTML` - Save complete-html page as MHTML. +* `callback` Function - `function(error) {}`. + * `error` Error + +Returns true if the process of saving page has been initiated successfully. + +```javascript +win.loadUrl('https://github.com'); + +win.webContents.on('did-finish-load', function() { + win.webContents.savePage('/tmp/test.html', 'HTMLComplete', function(error) { + if (!error) + console.log("Save page successfully"); + }); +}); +``` diff --git a/filenames.gypi b/filenames.gypi index 6418ba287214..aadc1a092f6b 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -108,6 +108,8 @@ 'atom/browser/api/trackable_object.h', 'atom/browser/api/frame_subscriber.cc', 'atom/browser/api/frame_subscriber.h', + 'atom/browser/api/save_page_handler.cc', + 'atom/browser/api/save_page_handler.h', 'atom/browser/auto_updater.cc', 'atom/browser/auto_updater.h', 'atom/browser/auto_updater_delegate.h', diff --git a/spec/api-browser-window-spec.coffee b/spec/api-browser-window-spec.coffee index 2e4a66b92b9a..0f9b376784fc 100644 --- a/spec/api-browser-window-spec.coffee +++ b/spec/api-browser-window-spec.coffee @@ -301,3 +301,15 @@ describe 'browser-window module', -> assert.notEqual data.length, 0 w.webContents.endFrameSubscription() done() + + describe 'save page', -> + savePagePath = path.join fixtures, 'save_page.html' + it 'should save page', (done) -> + w.webContents.on 'did-finish-load', -> + w.webContents.savePage savePagePath, 'HTMLComplete', (error) -> + assert.equal error, null + assert fs.existsSync savePagePath + fs.unlinkSync savePagePath + done() + + w.loadUrl "file://#{fixtures}/api/blank.html"