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