SearchPopupMenuCocoa.mm   [plain text]


/*
 * Copyright (C) 2015 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#import "config.h"
#import "SearchPopupMenuCocoa.h"

using namespace std::chrono;

namespace WebCore {

static NSString * const dateKey = @"date";
static NSString * const itemsKey = @"items";
static NSString * const searchesKey = @"searches";
static NSString * const searchStringKey = @"searchString";

static NSString *searchFieldRecentSearchesStorageDirectory()
{
    NSString *appName = [[NSBundle mainBundle] bundleIdentifier];
    if (!appName)
        appName = [[NSProcessInfo processInfo] processName];
    
    return [[NSHomeDirectory() stringByAppendingPathComponent:@"Library/WebKit"] stringByAppendingPathComponent:appName];
}

static NSString *searchFieldRecentSearchesPlistPath()
{
    return [searchFieldRecentSearchesStorageDirectory() stringByAppendingPathComponent:@"RecentSearches.plist"];
}

static RetainPtr<NSMutableDictionary> readSearchFieldRecentSearchesPlist()
{
    return adoptNS([[NSMutableDictionary alloc] initWithContentsOfFile:searchFieldRecentSearchesPlistPath()]);
}

static system_clock::time_point toSystemClockTime(NSDate *date)
{
    ASSERT(date);

    return system_clock::time_point(duration_cast<system_clock::duration>(duration<double>(date.timeIntervalSince1970)));
}

static NSDate *toNSDateFromSystemClock(system_clock::time_point time)
{
    return [NSDate dateWithTimeIntervalSince1970:duration_cast<duration<double>>(time.time_since_epoch()).count()];
}

static NSMutableArray *typeCheckedRecentSearchesArray(NSMutableDictionary *itemsDictionary, NSString *name)
{
    NSMutableDictionary *nameDictionary = [itemsDictionary objectForKey:name];
    if (![nameDictionary isKindOfClass:[NSDictionary class]])
        return nil;

    NSMutableArray *recentSearches = [nameDictionary objectForKey:searchesKey];
    if (![recentSearches isKindOfClass:[NSArray class]])
        return nil;

    return recentSearches;
}

static NSDate *typeCheckedDateInRecentSearch(NSDictionary *recentSearch)
{
    if (![recentSearch isKindOfClass:[NSDictionary class]])
        return nil;

    NSDate *date = [recentSearch objectForKey:dateKey];
    if (![date isKindOfClass:[NSDate class]])
        return nil;

    return date;
}

static RetainPtr<NSDictionary> typeCheckedRecentSearchesRemovingRecentSearchesAddedAfterDate(NSDate *date)
{
    if ([date isEqualToDate:[NSDate distantPast]])
        return nil;

    RetainPtr<NSMutableDictionary> recentSearchesPlist = readSearchFieldRecentSearchesPlist();
    NSMutableDictionary *itemsDictionary = [recentSearchesPlist objectForKey:itemsKey];
    if (![itemsDictionary isKindOfClass:[NSDictionary class]])
        return nil;

    RetainPtr<NSMutableArray> keysToRemove = adoptNS([[NSMutableArray alloc] init]);
    for (NSString *key in itemsDictionary) {
        if (![key isKindOfClass:[NSString class]])
            return nil;

        NSMutableArray *recentSearches = typeCheckedRecentSearchesArray(itemsDictionary, key);
        if (!recentSearches)
            return nil;

        RetainPtr<NSMutableArray> entriesToRemove = adoptNS([[NSMutableArray alloc] init]);
        for (NSDictionary *recentSearch in recentSearches) {
            NSDate *dateAdded = typeCheckedDateInRecentSearch(recentSearch);
            if (!dateAdded)
                return nil;

            if ([dateAdded compare:date] == NSOrderedDescending)
                [entriesToRemove addObject:recentSearch];
        }

        [recentSearches removeObjectsInArray:entriesToRemove.get()];
        if (!recentSearches.count)
            [keysToRemove addObject:key];
    }

    [itemsDictionary removeObjectsForKeys:keysToRemove.get()];
    if (!itemsDictionary.count)
        return nil;

    return recentSearchesPlist;
}

static void writeEmptyRecentSearchesPlist()
{
    auto emptyItemsDictionary = adoptNS([[NSDictionary alloc] init]);
    auto emptyRecentSearchesDictionary = adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:emptyItemsDictionary.get(), itemsKey, nil]);
    [emptyRecentSearchesDictionary writeToFile:searchFieldRecentSearchesPlistPath() atomically:YES];
}

void saveRecentSearches(const String& name, const Vector<RecentSearch>& searchItems)
{
    if (name.isEmpty())
        return;

    RetainPtr<NSDictionary> recentSearchesPlist = readSearchFieldRecentSearchesPlist();
    RetainPtr<NSMutableDictionary> itemsDictionary = [recentSearchesPlist objectForKey:itemsKey];
    // The NSMutableDictionary method we use to read the property list guarantees we get only
    // mutable containers, but it does not guarantee the file has a dictionary as expected.
    if (![itemsDictionary isKindOfClass:[NSDictionary class]]) {
        itemsDictionary = adoptNS([[NSMutableDictionary alloc] init]);
        recentSearchesPlist = adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:itemsDictionary.get(), itemsKey, nil]);
    }

    if (searchItems.isEmpty())
        [itemsDictionary removeObjectForKey:name];
    else {
        RetainPtr<NSMutableArray> items = adoptNS([[NSMutableArray alloc] initWithCapacity:searchItems.size()]);
        for (auto& searchItem : searchItems)
            [items addObject:adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:searchItem.string, searchStringKey, toNSDateFromSystemClock(searchItem.time), dateKey, nil]).get()];

        [itemsDictionary setObject:adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:items.get(), searchesKey, nil]).get() forKey:name];
    }

    [recentSearchesPlist writeToFile:searchFieldRecentSearchesPlistPath() atomically:YES];
}

Vector<RecentSearch> loadRecentSearches(const String& name)
{
    Vector<RecentSearch> searchItems;

    if (name.isEmpty())
        return searchItems;

    RetainPtr<NSMutableDictionary> recentSearchesPlist = readSearchFieldRecentSearchesPlist();
    if (!recentSearchesPlist)
        return searchItems;

    NSMutableDictionary *items = [recentSearchesPlist objectForKey:itemsKey];
    if (![items isKindOfClass:[NSDictionary class]])
        return searchItems;

    NSArray *recentSearches = typeCheckedRecentSearchesArray(items, name);
    if (!recentSearches)
        return searchItems;
    
    for (NSDictionary *item in recentSearches) {
        NSDate *date = typeCheckedDateInRecentSearch(item);
        if (!date)
            continue;

        NSString *searchString = [item objectForKey:searchStringKey];
        if (![searchString isKindOfClass:[NSString class]])
            continue;
        
        searchItems.append({ String{ searchString }, toSystemClockTime(date) });
    }

    return searchItems;
}

void removeRecentlyModifiedRecentSearches(std::chrono::system_clock::time_point oldestTimeToRemove)
{
    NSDate *date = toNSDateFromSystemClock(oldestTimeToRemove);
    auto recentSearchesPlist = typeCheckedRecentSearchesRemovingRecentSearchesAddedAfterDate(date);
    if (recentSearchesPlist)
        [recentSearchesPlist writeToFile:searchFieldRecentSearchesPlistPath() atomically:YES];
    else
        writeEmptyRecentSearchesPlist();
}

}