1/*
2 * Copyright (C) 2011 Nokia Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "PluginProcessProxy.h"
28
29#if ENABLE(PLUGIN_PROCESS)
30
31#include "ProcessExecutablePath.h"
32#include <QByteArray>
33#include <QCoreApplication>
34#include <QDateTime>
35#include <QDir>
36#include <QEventLoop>
37#include <QFile>
38#include <QJsonArray>
39#include <QJsonDocument>
40#include <QJsonObject>
41#include <QMap>
42#include <QProcess>
43#include <QStandardPaths>
44#include <QStringBuilder>
45#include <QVariant>
46#include <WebCore/FileSystem.h>
47#include <wtf/OwnPtr.h>
48#include <wtf/PassOwnPtr.h>
49
50namespace WebKit {
51
52class PluginProcessCreationParameters;
53
54void PluginProcessProxy::platformGetLaunchOptions(ProcessLauncher::LaunchOptions& launchOptions, const PluginProcessAttributes& pluginProcessAttributes)
55{
56    launchOptions.extraInitializationData.add("plugin-path", pluginProcessAttributes.moduleInfo.path);
57}
58
59void PluginProcessProxy::platformInitializePluginProcess(PluginProcessCreationParameters&)
60{
61}
62
63static PassOwnPtr<QFile> cacheFile()
64{
65    QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
66    if (cachePath.isEmpty())
67        return PassOwnPtr<QFile>();
68
69    // This should match the path set through WKContextSetDiskCacheDirectory.
70    cachePath.append(QDir::separator()).append(QStringLiteral(".QtWebKit")).append(QDir::separator());
71    QString cacheFilePath = cachePath % QStringLiteral("plugin_meta_data.json");
72
73    QDir::root().mkpath(cachePath);
74    return adoptPtr(new QFile(cacheFilePath));
75}
76
77static void removeCacheFile()
78{
79    if (OwnPtr<QFile> file = cacheFile())
80        file->remove();
81}
82
83struct ReadResult {
84    enum Tag {
85        Empty,
86        Error,
87        Success
88    };
89};
90
91static ReadResult::Tag readMetaDataFromCacheFile(QJsonDocument& result)
92{
93    OwnPtr<QFile> file = cacheFile();
94    if (!file || !file->open(QFile::ReadOnly))
95        return ReadResult::Empty;
96    QByteArray data = file->readAll();
97    if (data.isEmpty())
98        return ReadResult::Empty;
99
100    QJsonParseError error;
101    result = QJsonDocument::fromJson(data, &error);
102    if (error.error != QJsonParseError::NoError || !result.isArray()) {
103        // Corrupted file.
104        file->remove();
105        return ReadResult::Error;
106    }
107
108    return ReadResult::Success;
109}
110
111static void writeToCacheFile(const QJsonArray& array)
112{
113    OwnPtr<QFile> file = cacheFile();
114    if (file && file->open(QFile::WriteOnly | QFile::Truncate))
115        // Don't care about write error here. We will detect it later.
116        file->write(QJsonDocument(array).toJson());
117}
118
119static void appendToCacheFile(const QJsonObject& object)
120{
121    QJsonDocument jsonDocument;
122    ReadResult::Tag result = readMetaDataFromCacheFile(jsonDocument);
123    if (result == ReadResult::Error)
124        return;
125    if (result == ReadResult::Empty)
126        jsonDocument.setArray(QJsonArray());
127
128    QJsonArray array = jsonDocument.array();
129    array.append(object);
130    writeToCacheFile(array);
131}
132
133struct MetaDataResult {
134    enum Tag {
135        NotAvailable,
136        Unloadable,
137        Available
138    };
139};
140
141static MetaDataResult::Tag tryReadPluginMetaDataFromCacheFile(const QString& canonicalPluginPath, RawPluginMetaData& result)
142{
143    QJsonDocument jsonDocument;
144    if (readMetaDataFromCacheFile(jsonDocument) != ReadResult::Success)
145        return MetaDataResult::NotAvailable;
146
147    QJsonArray array = jsonDocument.array();
148    QDateTime pluginLastModified = QFileInfo(canonicalPluginPath).lastModified();
149    for (QJsonArray::const_iterator i = array.constBegin(); i != array.constEnd(); ++i) {
150        QJsonValue item = *i;
151        if (!item.isObject()) {
152            removeCacheFile();
153            return MetaDataResult::NotAvailable;
154        }
155
156        QJsonObject object = item.toObject();
157        if (object.value(QStringLiteral("path")).toString() == canonicalPluginPath) {
158            QString timestampString = object.value(QStringLiteral("timestamp")).toString();
159            if (timestampString.isEmpty()) {
160                removeCacheFile();
161                return MetaDataResult::NotAvailable;
162            }
163            QDateTime timestamp = QDateTime::fromString(timestampString);
164            if (timestamp < pluginLastModified) {
165                // Out of date data for this plugin => remove it from the file.
166                array.removeAt(i.i);
167                writeToCacheFile(array);
168                return MetaDataResult::NotAvailable;
169            }
170
171            if (object.contains(QLatin1String("unloadable")))
172                return MetaDataResult::Unloadable;
173
174            // Match.
175            result.name = object.value(QStringLiteral("name")).toString();
176            result.description = object.value(QStringLiteral("description")).toString();
177            result.mimeDescription = object.value(QStringLiteral("mimeDescription")).toString();
178            if (result.mimeDescription.isEmpty()) {
179                // Only the mime description is mandatory.
180                // Don't trust in the cache file if it is empty.
181                removeCacheFile();
182                return MetaDataResult::NotAvailable;
183            }
184
185            return MetaDataResult::Available;
186        }
187    }
188
189    return MetaDataResult::NotAvailable;
190}
191
192bool PluginProcessProxy::scanPlugin(const String& pluginPath, RawPluginMetaData& result)
193{
194    QFileInfo pluginFileInfo(pluginPath);
195    if (!pluginFileInfo.exists())
196        return false;
197
198    MetaDataResult::Tag metaDataResult = tryReadPluginMetaDataFromCacheFile(pluginFileInfo.canonicalFilePath(), result);
199    if (metaDataResult == MetaDataResult::Available)
200        return true;
201    if (metaDataResult == MetaDataResult::Unloadable)
202        return false;
203
204    // Scan the plugin via the plugin process.
205    QString commandLine = QString(executablePathOfPluginProcess()) % QLatin1Char(' ')
206                          % QStringLiteral("-scanPlugin") % QLatin1Char(' ') % pluginFileInfo.canonicalFilePath();
207    QProcess process;
208    process.setReadChannel(QProcess::StandardOutput);
209    process.start(commandLine);
210
211    bool ranSuccessfully = process.waitForFinished()
212                           && process.exitStatus() == QProcess::NormalExit
213                           && process.exitCode() == EXIT_SUCCESS;
214    if (ranSuccessfully) {
215        QByteArray outputBytes = process.readAll();
216        ASSERT(!(outputBytes.size() % sizeof(UChar)));
217
218        String output(reinterpret_cast<const UChar*>(outputBytes.constData()), outputBytes.size() / sizeof(UChar));
219        Vector<String> lines;
220        output.split(UChar('\n'), true, lines);
221        ASSERT(lines.size() == 4 && lines.last().isEmpty());
222
223        result.name.swap(lines[0]);
224        result.description.swap(lines[1]);
225        result.mimeDescription.swap(lines[2]);
226    } else
227        process.kill();
228
229    QVariantMap map;
230    map[QStringLiteral("path")] = QString(pluginFileInfo.canonicalFilePath());
231    map[QStringLiteral("timestamp")] = QDateTime::currentDateTime().toString();
232
233    if (!ranSuccessfully || result.mimeDescription.isEmpty()) {
234        // We failed getting the meta data in some way. Cache this information, so we don't
235        // need to rescan such plugins every time. We will retry it once the plugin is updated.
236
237        map[QStringLiteral("unloadable")] = QStringLiteral("true");
238        appendToCacheFile(QJsonObject::fromVariantMap(map));
239        return false;
240    }
241
242    map[QStringLiteral("name")] = QString(result.name);
243    map[QStringLiteral("description")] = QString(result.description);
244    map[QStringLiteral("mimeDescription")] = QString(result.mimeDescription);
245    appendToCacheFile(QJsonObject::fromVariantMap(map));
246    return true;
247}
248
249} // namespace WebKit
250
251#endif // ENABLE(PLUGIN_PROCESS)
252