1/*
2 * Copyright (C) 2007, 2008 Apple 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 *
8 * 1.  Redistributions of source code must retain the above copyright
9 *     notice, this list of conditions and the following disclaimer.
10 * 2.  Redistributions in binary form must reproduce the above copyright
11 *     notice, this list of conditions and the following disclaimer in the
12 *     documentation and/or other materials provided with the distribution.
13 * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
14 *     its contributors may be used to endorse or promote products derived
15 *     from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#import "WebDatabaseManagerPrivate.h"
30
31#if ENABLE(SQL_DATABASE)
32
33#import "WebDatabaseManagerClient.h"
34#import "WebPlatformStrategies.h"
35#import "WebSecurityOriginInternal.h"
36
37#import <WebCore/DatabaseManager.h>
38#import <WebCore/SecurityOrigin.h>
39
40#if PLATFORM(IOS)
41#import "WebDatabaseManagerInternal.h"
42#import <WebCore/DatabaseTracker.h>
43#import <WebCore/WebCoreThread.h>
44#endif
45
46using namespace WebCore;
47
48NSString *WebDatabaseDirectoryDefaultsKey = @"WebDatabaseDirectory";
49
50NSString *WebDatabaseDisplayNameKey = @"WebDatabaseDisplayNameKey";
51NSString *WebDatabaseExpectedSizeKey = @"WebDatabaseExpectedSizeKey";
52NSString *WebDatabaseUsageKey = @"WebDatabaseUsageKey";
53
54NSString *WebDatabaseDidModifyOriginNotification = @"WebDatabaseDidModifyOriginNotification";
55NSString *WebDatabaseDidModifyDatabaseNotification = @"WebDatabaseDidModifyDatabaseNotification";
56NSString *WebDatabaseIdentifierKey = @"WebDatabaseIdentifierKey";
57
58#if PLATFORM(IOS)
59CFStringRef WebDatabaseOriginsDidChangeNotification = CFSTR("WebDatabaseOriginsDidChangeNotification");
60#endif
61
62static NSString *databasesDirectoryPath();
63
64@implementation WebDatabaseManager
65
66+ (WebDatabaseManager *) sharedWebDatabaseManager
67{
68    static WebDatabaseManager *sharedManager = [[WebDatabaseManager alloc] init];
69    return sharedManager;
70}
71
72- (id)init
73{
74    if (!(self = [super init]))
75        return nil;
76
77    WebPlatformStrategies::initializeIfNecessary();
78
79    DatabaseManager& dbManager = DatabaseManager::manager();
80
81    // Set the database root path in WebCore
82    dbManager.initialize(databasesDirectoryPath());
83
84    // Set the DatabaseManagerClient
85    dbManager.setClient(WebDatabaseManagerClient::sharedWebDatabaseManagerClient());
86
87    return self;
88}
89
90- (NSArray *)origins
91{
92    Vector<RefPtr<SecurityOrigin>> coreOrigins;
93    DatabaseManager::manager().origins(coreOrigins);
94    NSMutableArray *webOrigins = [[NSMutableArray alloc] initWithCapacity:coreOrigins.size()];
95
96    for (unsigned i = 0; i < coreOrigins.size(); ++i) {
97        WebSecurityOrigin *webOrigin = [[WebSecurityOrigin alloc] _initWithWebCoreSecurityOrigin:coreOrigins[i].get()];
98        [webOrigins addObject:webOrigin];
99        [webOrigin release];
100    }
101
102    return [webOrigins autorelease];
103}
104
105- (NSArray *)databasesWithOrigin:(WebSecurityOrigin *)origin
106{
107    Vector<String> nameVector;
108    if (!DatabaseManager::manager().databaseNamesForOrigin([origin _core], nameVector))
109        return nil;
110
111    NSMutableArray *names = [[NSMutableArray alloc] initWithCapacity:nameVector.size()];
112
113    for (unsigned i = 0; i < nameVector.size(); ++i)
114        [names addObject:(NSString *)nameVector[i]];
115
116    return [names autorelease];
117}
118
119- (NSDictionary *)detailsForDatabase:(NSString *)databaseIdentifier withOrigin:(WebSecurityOrigin *)origin
120{
121    static id keys[3] = {WebDatabaseDisplayNameKey, WebDatabaseExpectedSizeKey, WebDatabaseUsageKey};
122
123    DatabaseDetails details = DatabaseManager::manager().detailsForNameAndOrigin(databaseIdentifier, [origin _core]);
124    if (details.name().isNull())
125        return nil;
126
127    id objects[3];
128    objects[0] = details.displayName().isEmpty() ? databaseIdentifier : (NSString *)details.displayName();
129    objects[1] = [NSNumber numberWithUnsignedLongLong:details.expectedUsage()];
130    objects[2] = [NSNumber numberWithUnsignedLongLong:details.currentUsage()];
131
132    return [[[NSDictionary alloc] initWithObjects:objects forKeys:keys count:3] autorelease];
133}
134
135- (void)deleteAllDatabases
136{
137    DatabaseManager::manager().deleteAllDatabases();
138#if PLATFORM(IOS)
139    // FIXME: This needs to be removed once DatabaseTrackers in multiple processes
140    // are in sync: <rdar://problem/9567500> Remove Website Data pane is not kept in sync with Safari
141    [[NSFileManager defaultManager] removeItemAtPath:databasesDirectoryPath() error:NULL];
142#endif
143}
144
145- (BOOL)deleteOrigin:(WebSecurityOrigin *)origin
146{
147    return DatabaseManager::manager().deleteOrigin([origin _core]);
148}
149
150- (BOOL)deleteDatabase:(NSString *)databaseIdentifier withOrigin:(WebSecurityOrigin *)origin
151{
152    return DatabaseManager::manager().deleteDatabase([origin _core], databaseIdentifier);
153}
154
155#if PLATFORM(IOS)
156static bool isFileHidden(NSString *file)
157{
158    ASSERT([file length]);
159    return [file characterAtIndex:0] == '.';
160}
161
162+ (void)removeEmptyDatabaseFiles
163{
164    NSString *databasesDirectory = databasesDirectoryPath();
165    NSFileManager *fileManager = [NSFileManager defaultManager];
166    NSArray *array = [fileManager contentsOfDirectoryAtPath:databasesDirectory error:0];
167    if (!array)
168        return;
169
170    NSUInteger count = [array count];
171    for (NSUInteger i = 0; i < count; ++i) {
172        NSString *fileName = [array objectAtIndex:i];
173        // Skip hidden files.
174        if (![fileName length] || isFileHidden(fileName))
175            continue;
176
177        NSString *path = [databasesDirectory stringByAppendingPathComponent:fileName];
178        // Look for directories that contain database files belonging to the same origins.
179        BOOL isDirectory;
180        if (![fileManager fileExistsAtPath:path isDirectory:&isDirectory] || !isDirectory)
181            continue;
182
183        // Make sure the directory is not a symbolic link that points to something else.
184        NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:0];
185        if ([attributes fileType] == NSFileTypeSymbolicLink)
186            continue;
187
188        NSArray *databaseFilesInOrigin = [fileManager contentsOfDirectoryAtPath:path error:0];
189        NSUInteger databaseFileCount = [databaseFilesInOrigin count];
190        NSUInteger deletedDatabaseFileCount = 0;
191        for (NSUInteger j = 0; j < databaseFileCount; ++j) {
192            NSString *dbFileName = [databaseFilesInOrigin objectAtIndex:j];
193            // Skip hidden files.
194            if (![dbFileName length] || isFileHidden(dbFileName))
195                continue;
196
197            NSString *dbFilePath = [path stringByAppendingPathComponent:dbFileName];
198
199            // There shouldn't be any directories in this folder - but check for it anyway.
200            if (![fileManager fileExistsAtPath:dbFilePath isDirectory:&isDirectory] || isDirectory)
201                continue;
202
203            if (DatabaseTracker::deleteDatabaseFileIfEmpty(dbFilePath))
204                ++deletedDatabaseFileCount;
205        }
206
207        // If we have removed every database file for this origin, delete the folder for this origin.
208        if (databaseFileCount == deletedDatabaseFileCount) {
209            // Use rmdir - we don't want the deletion to happen if the folder is not empty.
210            rmdir([path fileSystemRepresentation]);
211        }
212    }
213}
214
215+ (void)scheduleEmptyDatabaseRemoval
216{
217    DatabaseTracker::emptyDatabaseFilesRemovalTaskWillBeScheduled();
218
219    dispatch_async(dispatch_get_global_queue(0, 0), ^{
220        [WebDatabaseManager removeEmptyDatabaseFiles];
221        DatabaseTracker::emptyDatabaseFilesRemovalTaskDidFinish();
222    });
223}
224#endif // PLATFORM(IOS)
225
226@end
227
228#if PLATFORM(IOS)
229@implementation WebDatabaseManager (WebDatabaseManagerInternal)
230
231static Mutex& transactionBackgroundTaskIdentifierLock()
232{
233    DEPRECATED_DEFINE_STATIC_LOCAL(Mutex, mutex, ());
234    return mutex;
235}
236
237static WebBackgroundTaskIdentifier transactionBackgroundTaskIdentifier;
238
239static void setTransactionBackgroundTaskIdentifier(WebBackgroundTaskIdentifier identifier)
240{
241    transactionBackgroundTaskIdentifier = identifier;
242}
243
244static WebBackgroundTaskIdentifier getTransactionBackgroundTaskIdentifier()
245{
246    static dispatch_once_t pred;
247    dispatch_once(&pred, ^{
248        setTransactionBackgroundTaskIdentifier(invalidWebBackgroundTaskIdentifier());
249    });
250
251    return transactionBackgroundTaskIdentifier;
252}
253
254+ (void)willBeginFirstTransaction
255{
256    [self startBackgroundTask];
257}
258
259+ (void)didFinishLastTransaction
260{
261    [self endBackgroundTask];
262}
263
264+ (void)startBackgroundTask
265{
266    MutexLocker lock(transactionBackgroundTaskIdentifierLock());
267
268    // If there's already an existing background task going on, there's no need to start a new one.
269    if (getTransactionBackgroundTaskIdentifier() != invalidWebBackgroundTaskIdentifier())
270        return;
271
272    setTransactionBackgroundTaskIdentifier(startBackgroundTask(^ { [WebDatabaseManager endBackgroundTask]; }));
273}
274
275+ (void)endBackgroundTask
276{
277    MutexLocker lock(transactionBackgroundTaskIdentifierLock());
278
279    // It is possible that we were unable to start the background task when the first transaction began.
280    // Don't try to end the task in that case.
281    // It is also possible we finally finish the last transaction right when the background task expires
282    // and this will end up being called twice for the same background task.  transactionBackgroundTaskIdentifier
283    // will be invalid for the second caller.
284    if (getTransactionBackgroundTaskIdentifier() == invalidWebBackgroundTaskIdentifier())
285        return;
286
287    endBackgroundTask(getTransactionBackgroundTaskIdentifier());
288    setTransactionBackgroundTaskIdentifier(invalidWebBackgroundTaskIdentifier());
289}
290
291@end
292
293void WebKitSetWebDatabasePaused(bool paused)
294{
295    DatabaseTracker::tracker().setDatabasesPaused(paused);
296}
297#endif // PLATFORM(IOS)
298
299static NSString *databasesDirectoryPath()
300{
301    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
302    NSString *databasesDirectory = [defaults objectForKey:WebDatabaseDirectoryDefaultsKey];
303    if (!databasesDirectory || ![databasesDirectory isKindOfClass:[NSString class]])
304        databasesDirectory = @"~/Library/WebKit/Databases";
305
306    return [databasesDirectory stringByStandardizingPath];
307}
308
309#endif
310