1/*
2 * Copyright 2009, The Android Open Source Project
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 *  * Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 *  * 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 THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "GeolocationPermissions.h"
28
29#include "DOMWindow.h"
30#include "Frame.h"
31#include "Geolocation.h"
32#include "Navigator.h"
33#include "SQLiteDatabase.h"
34#include "SQLiteFileSystem.h"
35#include "SQLiteStatement.h"
36#include "SQLiteTransaction.h"
37#include "WebViewCore.h"
38
39#include <text/CString.h>
40
41using namespace WebCore;
42
43namespace android {
44
45GeolocationPermissions::PermissionsMap GeolocationPermissions::s_permanentPermissions;
46GeolocationPermissions::GeolocationPermissionsVector GeolocationPermissions::s_instances;
47bool GeolocationPermissions::s_alwaysDeny = false;
48bool GeolocationPermissions::s_permanentPermissionsLoaded = false;
49bool GeolocationPermissions::s_permanentPermissionsModified = false;
50String GeolocationPermissions::s_databasePath;
51
52static const char* databaseName = "GeolocationPermissions.db";
53
54GeolocationPermissions::GeolocationPermissions(WebViewCore* webViewCore, Frame* mainFrame)
55    : m_webViewCore(webViewCore)
56    , m_mainFrame(mainFrame)
57    , m_timer(this, &GeolocationPermissions::timerFired)
58
59{
60    ASSERT(m_webViewCore);
61    maybeLoadPermanentPermissions();
62    s_instances.append(this);
63}
64
65GeolocationPermissions::~GeolocationPermissions()
66{
67    size_t index = s_instances.find(this);
68    s_instances.remove(index);
69}
70
71void GeolocationPermissions::queryPermissionState(Frame* frame)
72{
73    ASSERT(s_permanentPermissionsLoaded);
74
75    // We use SecurityOrigin::toString to key the map. Note that testing
76    // the SecurityOrigin pointer for equality is insufficient.
77    String originString = frame->document()->securityOrigin()->toString();
78
79    // If we've been told to always deny requests, do so.
80    if (s_alwaysDeny) {
81        makeAsynchronousCallbackToGeolocation(originString, false);
82        return;
83    }
84
85    // See if we have a record for this origin in the permanent permissions.
86    // These take precedence over temporary permissions so that changes made
87    // from the browser settings work as intended.
88    PermissionsMap::const_iterator iter = s_permanentPermissions.find(originString);
89    PermissionsMap::const_iterator end = s_permanentPermissions.end();
90    if (iter != end) {
91        bool allow = iter->second;
92        makeAsynchronousCallbackToGeolocation(originString, allow);
93        return;
94    }
95
96    // Check the temporary permisions.
97    iter = m_temporaryPermissions.find(originString);
98    end = m_temporaryPermissions.end();
99    if (iter != end) {
100        bool allow = iter->second;
101        makeAsynchronousCallbackToGeolocation(originString, allow);
102        return;
103    }
104
105    // If there's no pending request, prompt the user.
106    if (nextOriginInQueue().isEmpty()) {
107        // Although multiple tabs may request permissions for the same origin
108        // simultaneously, the routing in WebViewCore/CallbackProxy ensures that
109        // the result of the request will make it back to this object, so
110        // there's no need for a globally unique ID for the request.
111        m_webViewCore->geolocationPermissionsShowPrompt(originString);
112    }
113
114    // Add this request to the queue so we can track which frames requested it.
115    if (m_queuedOrigins.find(originString) == WTF::notFound) {
116        m_queuedOrigins.append(originString);
117        FrameSet frameSet;
118        frameSet.add(frame);
119        m_queuedOriginsToFramesMap.add(originString, frameSet);
120    } else {
121        ASSERT(m_queuedOriginsToFramesMap.contains(originString));
122        m_queuedOriginsToFramesMap.find(originString)->second.add(frame);
123    }
124}
125
126void GeolocationPermissions::cancelPermissionStateQuery(WebCore::Frame* frame)
127{
128    // We cancel any queued request for the given frame. There can be at most
129    // one of these, since each frame maps to a single origin. We only cancel
130    // the request if this frame is the only one reqesting permission for this
131    // origin.
132    //
133    // We can use the origin string to avoid searching the map.
134    String originString = frame->document()->securityOrigin()->toString();
135    size_t index = m_queuedOrigins.find(originString);
136    if (index == WTF::notFound)
137        return;
138
139    ASSERT(m_queuedOriginsToFramesMap.contains(originString));
140    OriginToFramesMap::iterator iter = m_queuedOriginsToFramesMap.find(originString);
141    ASSERT(iter->second.contains(frame));
142    iter->second.remove(frame);
143    if (!iter->second.isEmpty())
144        return;
145
146    m_queuedOrigins.remove(index);
147    m_queuedOriginsToFramesMap.remove(iter);
148
149    // If this is the origin currently being shown, cancel the prompt
150    // and show the next in the queue, if present.
151    if (index == 0) {
152        m_webViewCore->geolocationPermissionsHidePrompt();
153        if (!nextOriginInQueue().isEmpty())
154            m_webViewCore->geolocationPermissionsShowPrompt(nextOriginInQueue());
155    }
156}
157
158void GeolocationPermissions::makeAsynchronousCallbackToGeolocation(String origin, bool allow)
159{
160    m_callbackData.origin = origin;
161    m_callbackData.allow = allow;
162    m_timer.startOneShot(0);
163}
164
165void GeolocationPermissions::providePermissionState(String origin, bool allow, bool remember)
166{
167    ASSERT(s_permanentPermissionsLoaded);
168
169    // It's possible that this method is called with an origin that doesn't
170    // match m_originInProgress. This can occur if this object is reset
171    // while a permission result is in the process of being marshalled back to
172    // the WebCore thread from the browser. In this case, we simply ignore the
173    // call.
174    if (origin != nextOriginInQueue())
175        return;
176
177    maybeCallbackFrames(origin, allow);
178    recordPermissionState(origin, allow, remember);
179
180    // If the permissions are set to be remembered, cancel any queued requests
181    // for this domain in other tabs.
182    if (remember)
183        cancelPendingRequestsInOtherTabs(origin);
184
185    // Clear the origin from the queue.
186    ASSERT(!m_queuedOrigins.isEmpty());
187    m_queuedOrigins.remove(0);
188    ASSERT(m_queuedOriginsToFramesMap.contains(origin));
189    m_queuedOriginsToFramesMap.remove(origin);
190
191    // If there are other requests queued, start the next one.
192    if (!nextOriginInQueue().isEmpty())
193        m_webViewCore->geolocationPermissionsShowPrompt(nextOriginInQueue());
194}
195
196void GeolocationPermissions::recordPermissionState(String origin, bool allow, bool remember)
197{
198    if (remember) {
199        s_permanentPermissions.set(origin, allow);
200        s_permanentPermissionsModified = true;
201    } else {
202        // It's possible that another tab recorded a permanent permission for
203        // this origin while our request was in progress, but we record it
204        // anyway.
205        m_temporaryPermissions.set(origin, allow);
206    }
207}
208
209void GeolocationPermissions::cancelPendingRequestsInOtherTabs(String origin)
210{
211    for (GeolocationPermissionsVector::const_iterator iter = s_instances.begin();
212         iter != s_instances.end();
213         ++iter)
214        (*iter)->cancelPendingRequests(origin);
215}
216
217void GeolocationPermissions::cancelPendingRequests(String origin)
218{
219    size_t index = m_queuedOrigins.find(origin);
220
221    // Don't cancel the request if it's currently being shown, in which case
222    // it's at index 0.
223    if (index == WTF::notFound || !index)
224        return;
225
226    // Get the permission from the permanent list.
227    ASSERT(s_permanentPermissions.contains(origin));
228    PermissionsMap::const_iterator iter = s_permanentPermissions.find(origin);
229    bool allow = iter->second;
230
231    maybeCallbackFrames(origin, allow);
232
233    m_queuedOrigins.remove(index);
234    ASSERT(m_queuedOriginsToFramesMap.contains(origin));
235    m_queuedOriginsToFramesMap.remove(origin);
236}
237
238void GeolocationPermissions::timerFired(Timer<GeolocationPermissions>* timer)
239{
240    ASSERT_UNUSED(timer, timer == &m_timer);
241    maybeCallbackFrames(m_callbackData.origin, m_callbackData.allow);
242}
243
244void GeolocationPermissions::resetTemporaryPermissionStates()
245{
246    ASSERT(s_permanentPermissionsLoaded);
247    m_queuedOrigins.clear();
248    m_queuedOriginsToFramesMap.clear();
249    m_temporaryPermissions.clear();
250    // If any permission results are being marshalled back to this thread, this
251    // will render them inefective.
252    m_timer.stop();
253
254    m_webViewCore->geolocationPermissionsHidePrompt();
255}
256
257const WTF::String& GeolocationPermissions::nextOriginInQueue()
258{
259    static const String emptyString = "";
260    return m_queuedOrigins.isEmpty() ? emptyString : m_queuedOrigins[0];
261}
262
263void GeolocationPermissions::maybeCallbackFrames(String origin, bool allow)
264{
265    // We can't track which frame issued the request, as frames can be deleted
266    // or have their contents replaced. Even uniqueChildName is not unique when
267    // frames are dynamically deleted and created. Instead, we simply call back
268    // to the Geolocation object in all frames from the correct origin.
269    for (Frame* frame = m_mainFrame; frame; frame = frame->tree()->traverseNext()) {
270        if (origin == frame->document()->securityOrigin()->toString()) {
271            // If the page has changed, it may no longer have a Geolocation
272            // object.
273            Geolocation* geolocation = frame->domWindow()->navigator()->optionalGeolocation();
274            if (geolocation)
275                geolocation->setIsAllowed(allow);
276        }
277    }
278}
279
280GeolocationPermissions::OriginSet GeolocationPermissions::getOrigins()
281{
282    maybeLoadPermanentPermissions();
283    OriginSet origins;
284    PermissionsMap::const_iterator end = s_permanentPermissions.end();
285    for (PermissionsMap::const_iterator iter = s_permanentPermissions.begin(); iter != end; ++iter)
286        origins.add(iter->first);
287    return origins;
288}
289
290bool GeolocationPermissions::getAllowed(String origin)
291{
292    maybeLoadPermanentPermissions();
293    bool allowed = false;
294    PermissionsMap::const_iterator iter = s_permanentPermissions.find(origin);
295    PermissionsMap::const_iterator end = s_permanentPermissions.end();
296    if (iter != end)
297        allowed = iter->second;
298    return allowed;
299}
300
301void GeolocationPermissions::clear(String origin)
302{
303    maybeLoadPermanentPermissions();
304    PermissionsMap::iterator iter = s_permanentPermissions.find(origin);
305    if (iter != s_permanentPermissions.end()) {
306        s_permanentPermissions.remove(iter);
307        s_permanentPermissionsModified = true;
308    }
309}
310
311void GeolocationPermissions::allow(String origin)
312{
313    maybeLoadPermanentPermissions();
314    // We replace any existing permanent permission.
315    s_permanentPermissions.set(origin, true);
316    s_permanentPermissionsModified = true;
317}
318
319void GeolocationPermissions::clearAll()
320{
321    maybeLoadPermanentPermissions();
322    s_permanentPermissions.clear();
323    s_permanentPermissionsModified = true;
324}
325
326void GeolocationPermissions::maybeLoadPermanentPermissions()
327{
328    if (s_permanentPermissionsLoaded)
329        return;
330    s_permanentPermissionsLoaded = true;
331
332    SQLiteDatabase database;
333    if (!openDatabase(&database))
334        return;
335
336    // Create the table here, such that even if we've just created the DB, the
337    // commands below should succeed.
338    if (!database.executeCommand("CREATE TABLE IF NOT EXISTS Permissions (origin TEXT UNIQUE NOT NULL, allow INTEGER NOT NULL)")) {
339        database.close();
340        return;
341    }
342
343    SQLiteStatement statement(database, "SELECT * FROM Permissions");
344    if (statement.prepare() != SQLResultOk) {
345        database.close();
346        return;
347    }
348
349    ASSERT(s_permanentPermissions.size() == 0);
350    while (statement.step() == SQLResultRow)
351        s_permanentPermissions.set(statement.getColumnText(0), statement.getColumnInt64(1));
352
353    database.close();
354}
355
356void GeolocationPermissions::maybeStorePermanentPermissions()
357{
358    // If the permanent permissions haven't been modified, there's no need to
359    // save them to the DB. (If we haven't even loaded them, writing them now
360    // would overwrite the stored permissions with the empty set.)
361    if (!s_permanentPermissionsModified)
362        return;
363
364    SQLiteDatabase database;
365    if (!openDatabase(&database))
366        return;
367
368    SQLiteTransaction transaction(database);
369
370    // The number of entries should be small enough that it's not worth trying
371    // to perform a diff. Simply clear the table and repopulate it.
372    if (!database.executeCommand("DELETE FROM Permissions")) {
373        database.close();
374        return;
375    }
376
377    PermissionsMap::const_iterator end = s_permanentPermissions.end();
378    for (PermissionsMap::const_iterator iter = s_permanentPermissions.begin(); iter != end; ++iter) {
379         SQLiteStatement statement(database, "INSERT INTO Permissions (origin, allow) VALUES (?, ?)");
380         if (statement.prepare() != SQLResultOk)
381             continue;
382         statement.bindText(1, iter->first);
383         statement.bindInt64(2, iter->second);
384         statement.executeCommand();
385    }
386
387    transaction.commit();
388    database.close();
389
390    s_permanentPermissionsModified = false;
391}
392
393void GeolocationPermissions::setDatabasePath(String path)
394{
395    // Take the first non-empty value.
396    if (s_databasePath.length() > 0)
397        return;
398    s_databasePath = path;
399}
400
401bool GeolocationPermissions::openDatabase(SQLiteDatabase* database)
402{
403    ASSERT(database);
404    String filename = SQLiteFileSystem::appendDatabaseFileNameToPath(s_databasePath, databaseName);
405    if (!database->open(filename))
406        return false;
407    if (chmod(filename.utf8().data(), S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)) {
408        database->close();
409        return false;
410    }
411    return true;
412}
413
414void GeolocationPermissions::setAlwaysDeny(bool deny)
415{
416    s_alwaysDeny = deny;
417}
418
419}  // namespace android
420