diff --git a/runtime/BrowserWindow.cpp b/runtime/BrowserWindow.cpp index e875b284b..6b962bd7a 100644 --- a/runtime/BrowserWindow.cpp +++ b/runtime/BrowserWindow.cpp @@ -23,7 +23,6 @@ #include #include #endif - // App headers #include "BrowserWindow.h" #include "ConfigWindow.h" @@ -42,6 +41,15 @@ BrowserWindow::BrowserWindow(QString url) m_widget = NULL; m_toolBtnBack = NULL; m_toolBtnForward = NULL; + m_downloadStarted = 0; + m_downloadCancelled = 0; + m_file = NULL; + m_downloadFilename = ""; + m_defaultFilename = ""; + m_progressDialog = NULL; + m_last_open_folder_path = QDir::currentPath(); + m_dir = ""; + m_reply = NULL; m_appServerUrl = url; @@ -83,6 +91,11 @@ BrowserWindow::BrowserWindow(QString url) // Register the slot on tab index change connect(m_tabWidget,SIGNAL(currentChanged(int )),this,SLOT(tabIndexChanged(int ))); + // Listen for download file request from the web page + m_mainWebView->page()->setForwardUnsupportedContent(true); + connect(m_mainWebView->page(), SIGNAL(downloadRequested(const QNetworkRequest &)), this, SLOT(download(const QNetworkRequest &))); + connect(m_mainWebView->page(), SIGNAL(unsupportedContent(QNetworkReply*)), this, SLOT(unsupportedContent(QNetworkReply*))); + m_mainWebView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); // Restore the geometry @@ -199,6 +212,297 @@ int BrowserWindow::findURLTab(const QUrl &name) return 0; } +// Below slot will be called when user right click on download link and select "Save Link..." option from context menu +void BrowserWindow::download(const QNetworkRequest &request) +{ + // Check that request contains data for download at client side + QUrl name; + if (checkClientDownload(name, request)) + return; + + if (m_downloadStarted) + { + // Inform user that a download is already started + QMessageBox::information(this, tr("Download warning"), tr("File download already in progress: %1").arg(m_defaultFilename)); + return; + } + + m_defaultFilename = QFileInfo(request.url().toString()).fileName(); + + // Open the dialog to save file + QFileDialog save_dialog(this); + save_dialog.setAcceptMode(QFileDialog::AcceptSave); + save_dialog.setWindowTitle(tr("Save file")); + save_dialog.setDirectory(m_last_open_folder_path); + save_dialog.selectFile(m_defaultFilename); + + // Register the slot for directory travesing when file dialog is opened and save the last open directory + QObject::connect(&save_dialog, SIGNAL(directoryEntered(const QString &)), this, SLOT(current_dir_path(const QString &))); + m_dir = m_last_open_folder_path; + QString fileName = ""; + QString f_name = ""; + + if (save_dialog.exec() == QDialog::Accepted) { + fileName = save_dialog.selectedFiles().first(); + f_name = fileName.replace(m_dir, ""); + // Remove the first character(/) from fiename + f_name.remove(0,1); + m_defaultFilename = f_name; + } + else + return; + + fileName = m_dir + fileName; + // Clear the last open directory path + m_dir.clear(); + +#ifdef __APPLE__ + // Check that user has given valid file name or not - forward slash is not allowed in file name + // In Mac OSX, forward slash is converted to colon(:) by Qt so we need to check for colon. + if (f_name.indexOf(":") != -1) + { + QMessageBox::information(this, tr("File name error"), tr("Invalid file name")); + return; + } +#else + // Check that user has given valid file name or not - forward slash is not allowed in file name + if (f_name.indexOf("/") != -1) + { + QMessageBox::information(this, tr("File name error"), tr("Invalid file name")); + return; + } +#endif + + if (fileName.isEmpty()) + return; + else + { + m_downloadFilename = fileName; + + QNetworkRequest newRequest = request; + newRequest.setAttribute(QNetworkRequest::User, fileName); + + QObject *obj_web_page = QObject::sender(); + if (obj_web_page != NULL) + { + QWebPage *sender_web_page = dynamic_cast(obj_web_page); + if (sender_web_page != NULL) + { + QNetworkAccessManager *networkManager = sender_web_page->networkAccessManager(); + QNetworkReply *reply = networkManager->get(newRequest); + if (reply != NULL) + { + m_downloadStarted = 1; + m_downloadCancelled = 0; + // Connect the signal for downloadProgress and downloadFinished + connect( reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadFileProgress(qint64, qint64)) ); + connect( reply, SIGNAL(finished()), this, SLOT(downloadFinished())); + } + } + } + } +} + +// Below slot will be called when file download is in progress +void BrowserWindow::downloadFileProgress(qint64 readData, qint64 totalData) +{ + QNetworkReply *reply = ((QNetworkReply*)sender()); + QNetworkRequest request = reply->request(); + QVariant v = request.attribute(QNetworkRequest::User); + + // When download is canceled by user then no need to write data to file + if (m_downloadCancelled) + return; + + if(reply != NULL && reply->error() != QNetworkReply::NoError) + { + qDebug() << "Network error occurred whilst downloading: " << m_defaultFilename; + return; + } + + // Download is started so open the file + if (!m_file) + { + m_file = new QFile(m_downloadFilename); + if (!m_file->open(QIODevice::WriteOnly)) + { + qDebug() << "Error opening file: " << m_downloadFilename; + m_downloadFilename.clear(); + m_defaultFilename.clear(); + m_downloadStarted = 0; + return; + } + + // Create progress bar dialog + m_progressDialog = new QProgressDialog (tr("Downloading file: %1 ").arg(m_defaultFilename), "Cancel", readData, totalData, this); + m_progressDialog->setWindowModality(Qt::WindowModal); + m_progressDialog->setWindowTitle(tr("Download progress")); + m_progressDialog->setMinimumWidth(450); + m_progressDialog->setMinimumHeight(80); + m_progressDialog->setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowMinimizeButtonHint | Qt::WindowCloseButtonHint); + // Register slot for file download cancel request + QObject::connect(m_progressDialog, SIGNAL(canceled()), this, SLOT(progressCanceled())); + m_reply = reply; + // Show downloading progress bar + m_progressDialog->show(); + } + + if (m_file) + { + // Write data to file + m_file->write(reply->read(readData)); + m_progressDialog->setValue(readData); + + // As read data and totalData difference is zero means downloading is finished + if ((totalData - readData) == 0) + { + // As downloading is finished so remove progress bar dialog + if (m_progressDialog) + { + delete m_progressDialog; + m_progressDialog = NULL; + } + + m_downloadStarted = 0; + m_downloadFilename.clear(); + m_defaultFilename.clear(); + m_downloadCancelled = 0; + if (m_file) + { + m_file->close(); + delete m_file; + m_file = NULL; + } + + if (m_reply) + m_reply = NULL; + } + } +} + +// Below slot will be called when user cancel the downloading file which is in progress. +void BrowserWindow::progressCanceled() +{ + m_downloadCancelled = 1; + + if (m_progressDialog) + { + delete m_progressDialog; + m_progressDialog = NULL; + } + + if (m_file) + { + m_file->close(); + // Remove the file from file system as downloading is canceled by user + m_file->remove(); + delete m_file; + m_file = NULL; + } + + if (m_reply) + { + m_reply->abort(); + m_reply = NULL; + } + + m_downloadFilename.clear(); + m_defaultFilename.clear(); + m_downloadStarted = 0; +} + +// Below slot will called when file download is finished +void BrowserWindow::downloadFinished() +{ + if (m_progressDialog) + { + delete m_progressDialog; + m_progressDialog = NULL; + } + + m_downloadFilename.clear(); + m_defaultFilename.clear(); + m_downloadStarted = 0; + m_downloadCancelled = 0; + if (m_file) + { + m_file->close(); + delete m_file; + m_file = NULL; + } + + if (m_reply) + m_reply = NULL; +} + +// Below slot will be called when user directly click on any download link +void BrowserWindow::unsupportedContent(QNetworkReply * reply) +{ + if (m_downloadStarted) + { + // Inform user that download is already started + QMessageBox::information(this, tr("Download warning"), tr("File download already in progress: %1").arg(m_defaultFilename)); + return; + } + + m_defaultFilename = QFileInfo(reply->url().toString()).fileName(); + QFileDialog save_dialog(this); + save_dialog.setAcceptMode(QFileDialog::AcceptSave); + save_dialog.setWindowTitle(tr("Save file")); + save_dialog.setDirectory(m_last_open_folder_path); + save_dialog.selectFile(m_defaultFilename); + + QObject::connect(&save_dialog, SIGNAL(directoryEntered(const QString &)), this, SLOT(current_dir_path(const QString &))); + m_dir = m_last_open_folder_path; + QString fileName = ""; + QString f_name = ""; + + if (save_dialog.exec() == QDialog::Accepted) { + fileName = save_dialog.selectedFiles().first(); + f_name = fileName.replace(m_dir, ""); + // Remove the first character(/) from fiename + f_name.remove(0,1); + m_defaultFilename = f_name; + } + else + return; + + fileName = m_dir + fileName; + // Clear last open folder path + m_dir.clear(); + +#ifdef __APPLE__ + // Check that user has given valid file name or not - forward slash is not allowed in file name + // In Mac OSX, forward slash is converted to colon(:) by Qt so we need to check for colon. + if (f_name.indexOf(":") != -1) + { + QMessageBox::information(this, tr("File name error"), tr("Invalid file name")); + return; + } +#else + // Check that user has given valid file name or not - forward slash is not allowed in file name + if (f_name.indexOf("/") != -1) + { + QMessageBox::information(this, tr("File name error"), tr("Invalid file name")); + return; + } +#endif + + if (fileName.isEmpty()) + return; + else + { + m_downloadFilename = fileName; + if (reply != NULL) + { + m_downloadStarted = 1; + m_downloadCancelled = 0; + connect( reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadFileProgress(qint64, qint64))); + connect( reply, SIGNAL(finished()), this, SLOT(downloadFinished())); + } + } +} + // Slot: When the tab index change, hide/show the toolbutton displayed on tab void BrowserWindow::tabIndexChanged(int index) { @@ -340,9 +644,143 @@ void BrowserWindow::tabTitleChanged(const QString &str) } } +// Below function will be used to download the data set in encoded URL so data will be downloaded at client side. +bool BrowserWindow::checkClientDownload(const QUrl &name, const QNetworkRequest &request) +{ + QString mime_type = ""; + QString file_name = ""; + QString write_data = ""; + QString csv_data = ""; + bool return_val = false; + + /* + In Qt version 5.5, "download" signal is emitted when 'download' attribute is set on 'a' tag. + In "download" signal emission, name will be empty and data will be in request object. + Earlier version ( < 5.5 ), "urlLinkClicked" signal is emitted so name will contain the object data. + */ + if (name.isEmpty()) + csv_data = QFileInfo(request.url().toString()).fileName(); + else + csv_data = QString::fromUtf8(name.toEncoded()); + + // Extract the filename and value(data) from encoded URL + QUrlQuery downloadData(csv_data); + QStringList keyValueData = csv_data.split("&"); + file_name = downloadData.queryItemValue("filename"); + write_data = downloadData.queryItemValue("value"); + + int key_value_length = keyValueData.size(); + int i_count = 0; + + while (i_count < key_value_length) + { + // Extract the extension after "data:" word found from encoded url. + QString start_match_string = "data:"; + int s_offset = keyValueData.at(i_count).indexOf(start_match_string); + if (s_offset != -1) + { + int format_offset = keyValueData.at(i_count).indexOf("/"); + mime_type = keyValueData.at(i_count).mid((format_offset+1)); + break; + } + + int split_offset = keyValueData.at(i_count).indexOf("="); + if (split_offset == -1) + { + mime_type = keyValueData.at(i_count); + break; + } + + i_count += 1; + } + + // Write data to file + if (!write_data.isEmpty()) + { + QString filename = ""; + QString f_name = ""; + QFileDialog saveAsdialog(this); + saveAsdialog.setAcceptMode(QFileDialog::AcceptSave); + saveAsdialog.selectNameFilter(tr("Files (*.%1)").arg(mime_type)); + saveAsdialog.setWindowTitle(tr("Save %1 file").arg(mime_type)); + saveAsdialog.setDirectory(m_last_open_folder_path); + saveAsdialog.selectFile(file_name); + saveAsdialog.setDefaultSuffix(mime_type); + + QObject::connect(&saveAsdialog, SIGNAL(directoryEntered(const QString &)), this, SLOT(current_dir_path(const QString &))); + m_dir = m_last_open_folder_path; + + if (saveAsdialog.exec() == QDialog::Accepted) { + filename = saveAsdialog.selectedFiles().at(0); + QString filename = saveAsdialog.selectedFiles().first(); + f_name = filename.replace(m_dir, ""); + // Remove first character from fiename + f_name.remove(0,1); + } + + // clear last open folder path + m_dir.clear(); + + return_val = true; + +#ifdef __APPLE__ + // Check that user has given valid file name or not - forward slash is not allowed in file name + // In Mac OSX, forward slash is converted to colon(:) by Qt so we need to check for colon. + if (f_name.indexOf(":") != -1) + { + QMessageBox::information(this, tr("File name error"), tr("Invalid file name")); + return return_val; + } +#else + // Check that user has given valid file name or not - forward slash is not allowed in file name + if (f_name.indexOf("/") != -1) + { + QMessageBox::information(this, tr("File name error"), tr("Invalid file name")); + return return_val; + } +#endif + if(!filename.isEmpty()) + { + // Save last open folder path + m_last_open_folder_path = QFileInfo(filename).path(); + // Decode the encoded uri data + QString csvData = QUrl::fromPercentEncoding(write_data.toUtf8()); + + QFile csvfile(filename); + if (!csvfile.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QMessageBox::information(this, tr("Save csv file"), tr("Error while opening file %1").arg(filename)); + return return_val; + } + // Write csv data to file + qint64 data_return = csvfile.write(csvData.toUtf8().constData()); + if (data_return == -1) + { + QMessageBox::information(this, tr("Save csv file"), tr("Error while writing data to file %1").arg(filename)); + csvfile.close(); + return return_val; + } + csvfile.close(); + } + } + + return return_val; +} + +void BrowserWindow::current_dir_path(const QString &dir) +{ + m_dir = dir; + m_last_open_folder_path = dir; +} + // Slot: Link is open from pgAdmin mainwindow void BrowserWindow::urlLinkClicked(const QUrl &name) { + // Check that request contains the data download at client side + QNetworkRequest request; + if (checkClientDownload(name, request)) + return; + // First check is there any tab opened with same URL then open it again. int tabFound = findURLTab(name); @@ -353,6 +791,11 @@ void BrowserWindow::urlLinkClicked(const QUrl &name) m_addNewGridLayout->setContentsMargins(0, 0, 0, 0); m_addNewWebView = new WebViewWindow(m_addNewTab); + // Listen for the download request from the web page + m_addNewWebView->page()->setForwardUnsupportedContent(true); + connect(m_addNewWebView->page(), SIGNAL(downloadRequested(const QNetworkRequest &)), this, SLOT(download(const QNetworkRequest &))); + connect(m_addNewWebView->page(), SIGNAL(unsupportedContent(QNetworkReply*)), this, SLOT(unsupportedContent(QNetworkReply*))); + m_widget = new QWidget(m_addNewTab); m_toolBtnBack = new QToolButton(m_widget); m_toolBtnBack->setFixedHeight(PGA_BTN_SIZE); diff --git a/runtime/BrowserWindow.h b/runtime/BrowserWindow.h index 43f90fef3..7200ff324 100644 --- a/runtime/BrowserWindow.h +++ b/runtime/BrowserWindow.h @@ -54,6 +54,12 @@ public slots: void tabIndexChanged(int index); void goBackPage(); void goForwardPage(); + void download(const QNetworkRequest &request); + void unsupportedContent(QNetworkReply * reply); + void downloadFinished(); + void downloadFileProgress(qint64 , qint64 ); + void progressCanceled(); + void current_dir_path(const QString &dir); private: QString m_appServerUrl; @@ -79,10 +85,20 @@ private: bool m_initialLoad; int m_loadAttempt; + QString m_downloadFilename; + int m_downloadStarted; + int m_downloadCancelled; + QFile *m_file; + QProgressDialog *m_progressDialog; + QString m_defaultFilename; + QString m_last_open_folder_path; + QString m_dir; + QNetworkReply *m_reply; void createActions(); void pause(int seconds = 1); int findURLTab(const QUrl &name); + bool checkClientDownload(const QUrl &name, const QNetworkRequest &request); }; #endif // BROWSERWINDOW_H diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js index 965d56e67..944c0be2f 100644 --- a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js @@ -2601,7 +2601,7 @@ define( keys = _.pluck(self.columns, 'name'); // Fetch the items from fullCollection and convert it as csv format - var csv = labels.join(',') + '\n'; + var csv = keys.join(',') + '\n'; csv += coll.map(function(item) { return _.map(keys, function(key) { var cell = csv_col [key].cell, @@ -2614,7 +2614,7 @@ define( }).join('\n'); // Download the file. - var encodedUri = encodeURI('data:text/csv;charset=utf-8,' + csv), + var encodedUri = encodeURI('data:text/csv&charset=utf-8&filename=download.csv&value=' + csv), link = document.createElement('a'); link.setAttribute('href', encodedUri);