1 /*
2     ark: A program for modifying archives via a GUI.
3 
4     SPDX-FileCopyrightText: 2004-2008 Henrique Pinto <henrique.pinto@kdemail.net>
5 
6     SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 #include "arkviewer.h"
10 #include "ark_debug.h"
11 
12 #include <KIO/JobUiDelegate>
13 #include <KIO/ApplicationLauncherJob>
14 #include <KLocalizedString>
15 #include <KMimeTypeTrader>
16 #include <KMessageBox>
17 #include <KParts/OpenUrlArguments>
18 #include <KXMLGUIFactory>
19 #include <KActionCollection>
20 #include <KAboutPluginDialog>
21 #include <KPluginMetaData>
22 
23 #include <QFile>
24 #include <QMimeDatabase>
25 #include <QProgressDialog>
26 #include <QPushButton>
27 #include <QStyle>
28 #include <QAction>
29 
30 #include <algorithm>
31 
ArkViewer()32 ArkViewer::ArkViewer()
33         : KParts::MainWindow()
34 {
35     setupUi(this);
36 
37     KStandardAction::close(this, &QMainWindow::close, actionCollection());
38 
39     QAction *closeAction = actionCollection()->addAction(QStringLiteral("close"), this, &ArkViewer::close);
40     closeAction->setShortcut(Qt::Key_Escape);
41 
42     setXMLFile(QStringLiteral("ark_viewer.rc"));
43     setupGUI(ToolBar);
44 }
45 
~ArkViewer()46 ArkViewer::~ArkViewer()
47 {
48     if (m_part) {
49         QProgressDialog progressDialog(this);
50         progressDialog.setWindowTitle(i18n("Closing preview"));
51         progressDialog.setLabelText(i18n("Please wait while the preview is being closed..."));
52 
53         progressDialog.setMinimumDuration(500);
54         progressDialog.setModal(true);
55         progressDialog.setCancelButton(nullptr);
56         progressDialog.setRange(0, 0);
57 
58         // #261785: this preview dialog is not modal, so we need to delete
59         //          the previewed file ourselves when the dialog is closed;
60 
61         m_part.data()->closeUrl();
62 
63         if (!m_fileName.isEmpty()) {
64             QFile::remove(m_fileName);
65         }
66     }
67 
68     guiFactory()->removeClient(m_part);
69     delete m_part;
70 }
71 
openExternalViewer(const KService::Ptr viewer,const QString & fileName)72 void ArkViewer::openExternalViewer(const KService::Ptr viewer, const QString& fileName)
73 {
74     qCDebug(ARK) << "Using external viewer";
75 
76     const QList<QUrl> fileUrlList = {QUrl::fromLocalFile(fileName)};
77     KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(viewer);
78     job->setUrls(fileUrlList);
79     job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
80     // The temporary file will be removed when the viewer application exits.
81     job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
82     job->start();
83 }
84 
openInternalViewer(const KService::Ptr viewer,const QString & fileName,const QMimeType & mimeType)85 void ArkViewer::openInternalViewer(const KService::Ptr viewer, const QString& fileName, const QMimeType& mimeType)
86 {
87     qCDebug(ARK) << "Opening internal viewer";
88 
89     ArkViewer *internalViewer = new ArkViewer();
90     internalViewer->show();
91     if (internalViewer->viewInInternalViewer(viewer, fileName, mimeType)) {
92         // The internal viewer is showing the file, and will
93         // remove the temporary file in its destructor.  So there
94         // is no more to do here.
95         return;
96     }
97     else {
98         KMessageBox::sorry(nullptr, i18n("The internal viewer cannot preview this file."));
99         delete internalViewer;
100 
101         qCDebug(ARK) << "Removing temporary file:" << fileName;
102         QFile::remove(fileName);
103     }
104 }
105 
askViewAsPlainText(const QMimeType & mimeType)106 bool ArkViewer::askViewAsPlainText(const QMimeType& mimeType)
107 {
108     int response;
109     if (!mimeType.isDefault()) {
110         // File has a defined MIME type, and not the default
111         // application/octet-stream.  So it could be viewable as
112         // plain text, ask the user.
113         response = KMessageBox::warningContinueCancel(nullptr,
114             xi18n("The internal viewer cannot preview this type of file<nl/>(%1).<nl/><nl/>Do you want to try to view it as plain text?", mimeType.name()),
115             i18nc("@title:window", "Cannot Preview File"),
116             KGuiItem(i18nc("@action:button", "Preview as Text"), QIcon::fromTheme(QStringLiteral("text-plain"))),
117             KStandardGuiItem::cancel(),
118             QStringLiteral("PreviewAsText_%1").arg(mimeType.name()));
119     }
120     else {
121         // No defined MIME type, or the default application/octet-stream.
122         // There is still a possibility that it could be viewable as plain
123         // text, so ask the user.  Not the same as the message/question
124         // above, because the wording and default are different.
125         response = KMessageBox::warningContinueCancel(nullptr,
126             xi18n("The internal viewer cannot preview this unknown type of file.<nl/><nl/>Do you want to try to view it as plain text?"),
127             i18nc("@title:window", "Cannot Preview File"),
128             KGuiItem(i18nc("@action:button", "Preview as Text"), QIcon::fromTheme(QStringLiteral("text-plain"))),
129             KStandardGuiItem::cancel(),
130             QString(),
131             KMessageBox::Dangerous);
132     }
133 
134     return response != KMessageBox::Cancel;
135 }
136 
view(const QString & fileName)137 void ArkViewer::view(const QString& fileName)
138 {
139     QMimeDatabase db;
140     QMimeType mimeType = db.mimeTypeForFile(fileName);
141     qCDebug(ARK) << "viewing" << fileName << "with mime type:" << mimeType.name();
142 
143     const KService::Ptr internalViewer = ArkViewer::getInternalViewer(mimeType.name());
144 
145     if (internalViewer) {
146         openInternalViewer(internalViewer, fileName, mimeType);
147         return;
148     }
149 
150     const KService::Ptr externalViewer = ArkViewer::getExternalViewer(mimeType.name());
151 
152     if (externalViewer) {
153         openExternalViewer(externalViewer, fileName);
154         return;
155     }
156 
157     // No internal or external viewer available for the file.  Ask the user if it
158     // should be previewed as text/plain.
159     if (askViewAsPlainText(mimeType)) {
160         const KService::Ptr textViewer = ArkViewer::getInternalViewer(QStringLiteral("text/plain"));
161         openInternalViewer(textViewer, fileName, db.mimeTypeForName(QStringLiteral("text/plain")));
162     } else {
163         qCDebug(ARK) << "Removing temporary file:" << fileName;
164         QFile::remove(fileName);
165     }
166 }
167 
viewInInternalViewer(const KService::Ptr viewer,const QString & fileName,const QMimeType & mimeType)168 bool ArkViewer::viewInInternalViewer(const KService::Ptr viewer, const QString& fileName, const QMimeType &mimeType)
169 {
170     setWindowFilePath(fileName);
171 
172     // Set icon and comment for the mimetype.
173     m_iconLabel->setPixmap(QIcon::fromTheme(mimeType.iconName()).pixmap(style()->pixelMetric(QStyle::PixelMetric::PM_SmallIconSize)));
174     m_commentLabel->setText(mimeType.comment());
175 
176     // Create the ReadOnlyPart instance.
177     m_part = viewer->createInstance<KParts::ReadOnlyPart>(this, this);
178 
179     if (!m_part.data()) {
180         return false;
181     }
182 
183     // Insert the KPart into its placeholder.
184     mainLayout->insertWidget(0, m_part->widget());
185 
186     QAction* action = actionCollection()->addAction(QStringLiteral("help_about_kpart"));
187     const KPluginMetaData partMetaData = m_part->metaData();
188     const QString iconName = partMetaData.iconName();
189     if (!iconName.isEmpty()) {
190         action->setIcon(QIcon::fromTheme(iconName));
191     }
192     action->setText(i18nc("@action", "About Viewer Component"));
193     connect(action, &QAction::triggered, this, &ArkViewer::aboutKPart);
194 
195     createGUI(m_part.data());
196     setAutoSaveSettings(QStringLiteral("Viewer"), true);
197 
198     m_part.data()->openUrl(QUrl::fromLocalFile(fileName));
199     m_part.data()->widget()->setFocus();
200     m_fileName = fileName;
201 
202     return true;
203 }
204 
getExternalViewer(const QString & mimeType)205 KService::Ptr ArkViewer::getExternalViewer(const QString &mimeType)
206 {
207     KService::List offers = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("Application"));
208 
209     if (!offers.isEmpty()) {
210         return offers.first();
211     } else {
212         return KService::Ptr();
213     }
214 }
215 
getInternalViewer(const QString & mimeType)216 KService::Ptr ArkViewer::getInternalViewer(const QString& mimeType)
217 {
218     // No point in even trying to find anything for application/octet-stream
219     if (mimeType == QLatin1String("application/octet-stream")) {
220         return KService::Ptr();
221     }
222 
223     // Try to get a read-only kpart for the internal viewer
224     KService::List offers = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("KParts/ReadOnlyPart"));
225 
226     auto arkPartIt = std::find_if(offers.begin(), offers.end(), [](KService::Ptr service) {
227         return service->storageId() == QLatin1String("ark_part.desktop");
228     });
229 
230     // Use the Ark part only when the mime type matches an archive type directly.
231     // Many file types (e.g. Open Document) are technically just archives
232     // but browsing their internals is typically not what the user wants.
233     if (arkPartIt != offers.end()) {
234         // Not using hasMimeType() as we're explicitly not interested in inheritance.
235         if (!(*arkPartIt)->mimeTypes().contains(mimeType)) {
236             offers.erase(arkPartIt);
237         }
238     }
239 
240     // Skip the KHTML part
241     auto khtmlPart = std::find_if(offers.begin(), offers.end(), [](KService::Ptr service) {
242         return service->desktopEntryName() == QLatin1String("khtml");
243     });
244 
245     if (khtmlPart != offers.end()) {
246         offers.erase(khtmlPart);
247     }
248 
249     if (!offers.isEmpty()) {
250         return offers.first();
251     } else {
252         return KService::Ptr();
253     }
254 }
255 
aboutKPart()256 void ArkViewer::aboutKPart()
257 {
258     if (!m_part) {
259         return;
260     }
261 
262     auto *dialog = new KAboutPluginDialog(m_part->metaData(), this);
263     dialog->setAttribute(Qt::WA_DeleteOnClose);
264     dialog->show();
265 }
266