diff --git a/src/bookmarks.cpp b/src/bookmarks.cpp new file mode 100644 index 0000000..0aab081 --- /dev/null +++ b/src/bookmarks.cpp @@ -0,0 +1,415 @@ +#include "bookmarks.hpp" + +#include "globals.hpp" + +#include "include/cef_request.h" + +#include +#include + +namespace browservice { + +namespace { + +mutex bookmarkCacheMutex; +bool bookmarkCacheInitialized = false; +std::map bookmarkCache; + +// Caller needs to lock bookmarkCacheMutex +void populateBookmarkCache(const Bookmarks& bookmarks) { + bookmarkCache.clear(); + for(const auto& item : bookmarks.getData()) { + bookmarkCache[item.second.url] = item.first; + } + bookmarkCacheInitialized = true; +} + +bool tryCreateDotDir() { + if(mkdir(globals->dotDirPath.c_str(), 0700) == 0) { + return true; + } else { + if(errno == EEXIST) { + struct stat st; + if(stat(globals->dotDirPath.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) { + return true; + } + } + return false; + } +} + +void writeLE(ofstream& fp, uint64_t val) { + uint8_t bytes[8]; + for(int i = 0; i < 8; ++i) { + bytes[i] = (uint8_t)val; + val >>= 8; + } + fp.write((char*)bytes, 8); +} + +void writeStr(ofstream& fp, const string& val) { + writeLE(fp, val.size()); + fp.write(val.data(), val.size()); + + size_t padCount = (-val.size()) & (size_t)7; + if(padCount) { + char zeros[8] = {}; + fp.write(zeros, padCount); + } +} + +bool readLE(ifstream& fp, uint64_t& val) { + uint8_t bytes[8]; + fp.read((char*)bytes, 8); + if(!fp.good()) { + return false; + } + + val = 0; + for(int i = 7; i >= 0; --i) { + val <<= 8; + val |= (uint64_t)bytes[i]; + } + return true; +} + +bool readStr(ifstream& fp, string& val) { + uint64_t size; + if(!readLE(fp, size)) { + return false; + } + + size_t padCount = (-size) & (size_t)7; + size_t paddedSize = size + padCount; + + vector buf(paddedSize); + fp.read(buf.data(), paddedSize); + if(!fp.good()) { + return false; + } + + val.assign(buf.begin(), buf.begin() + size); + return true; +} + +} + +Bookmarks::Bookmarks(CKey) {} + +shared_ptr Bookmarks::load() { + if(!tryCreateDotDir()) { + ERROR_LOG( + "Loading bookmarks failed: " + "Directory '", globals->dotDirPath, "' does not exist and creating it failed" + ); + return {}; + } + + string bookmarkPath = globals->dotDirPath + "/bookmarks"; + + shared_ptr ret = Bookmarks::create(); + + struct stat st; + if(stat(bookmarkPath.c_str(), &st) == -1 && errno == ENOENT) { + INFO_LOG("Bookmark file '", bookmarkPath, "' does not exist, using empty set of bookmarks"); + return ret; + } + + auto readError = [&]() -> shared_ptr { + ERROR_LOG( + "Loading bookmarks failed: Reading file '", bookmarkPath, "' failed" + ); + return {}; + }; + auto formatError = [&]() -> shared_ptr { + ERROR_LOG( + "Loading bookmarks failed: File '", bookmarkPath, "' has invalid format" + ); + return {}; + }; + + ifstream fp; + fp.open(bookmarkPath); + if(!fp.good()) return readError(); + + uint64_t signature; + if(!readLE(fp, signature)) return readError(); + if(signature != 0xBA0F5EAF1CEB00C3) return formatError(); + + uint64_t version; + if(!readLE(fp, version)) return readError(); + if(version != (uint64_t)0) return formatError(); + + while(true) { + uint64_t hasNext; + if(!readLE(fp, hasNext)) return readError(); + if(hasNext != (uint64_t)0 && hasNext != (uint64_t)1) return formatError(); + + if(!hasNext) { + break; + } + + uint64_t id; + Bookmark bookmark; + if(!readLE(fp, id) || !readStr(fp, bookmark.url) || !readStr(fp, bookmark.title) || !readLE(fp, bookmark.time)) { + return readError(); + } + + if(!ret->data_.emplace(id, move(bookmark)).second) { + return formatError(); + } + } + + fp.close(); + + INFO_LOG("Bookmarks successfully read from '", bookmarkPath, "'"); + return ret; +} + +bool Bookmarks::save() { + if(!tryCreateDotDir()) { + ERROR_LOG( + "Saving bookmarks failed: " + "Directory '", globals->dotDirPath, "' does not exist and creating it failed" + ); + return false; + } + + string bookmarkPath = globals->dotDirPath + "/bookmarks"; + + string bookmarkTmpPath = globals->dotDirPath + "/.tmp.bookmarks."; + string charPalette = "abcdefghijklmnopqrstuvABCDEFGHIJKLMNOPQRSTUV0123456789"; + for(int i = 0; i < 16; ++i) { + char c = charPalette[uniform_int_distribution(0, charPalette.size() - 1)(rng)]; + bookmarkTmpPath.push_back(c); + } + + ofstream fp; + fp.open(bookmarkTmpPath); + + // File signature + writeLE(fp, 0xBA0F5EAF1CEB00C3); + + // Format version + writeLE(fp, 0); + + for(const auto& item : data_) { + uint64_t id = item.first; + const Bookmark& bookmark = item.second; + + // One more item + writeLE(fp, 1); + + writeLE(fp, id); + writeStr(fp, bookmark.url); + writeStr(fp, bookmark.title); + writeLE(fp, bookmark.time); + } + + // No more items + writeLE(fp, 0); + + fp.close(); + + if(!fp.good()) { + ERROR_LOG( + "Saving bookmarks failed: " + "Could not write temporary file '", bookmarkTmpPath, "'" + ); + return false; + } + + if(rename(bookmarkTmpPath.c_str(), bookmarkPath.c_str()) != 0) { + ERROR_LOG( + "Saving bookmarks failed: " + "Renaming temporary file '", bookmarkTmpPath, "' to '", bookmarkPath, "' failed" + ); + unlink(bookmarkTmpPath.c_str()); + return false; + } + + INFO_LOG("Bookmarks successfully written to '", bookmarkPath, "'"); + + { + lock_guard lock(bookmarkCacheMutex); + populateBookmarkCache(*this); + } + + return true; +} + +const map& Bookmarks::getData() const { + return data_; +} + +uint64_t Bookmarks::putBookmark(Bookmark bookmark) { + uint64_t id; + do { + id = uniform_int_distribution()(rng); + } while(data_.count(id)); + + data_.emplace(id, move(bookmark)); + return id; +} + +void Bookmarks::removeBookmark(uint64_t id) { + data_.erase(id); +} + +optional getCachedBookmarkIDByURL(string url) { + lock_guard lock(bookmarkCacheMutex); + if(!bookmarkCacheInitialized) { + shared_ptr bookmarks = Bookmarks::load(); + if(bookmarks) { + populateBookmarkCache(*bookmarks); + } else { + bookmarkCache.clear(); + bookmarkCacheInitialized = true; + } + } + auto it = bookmarkCache.find(url); + if(it == bookmarkCache.end()) { + optional empty; + return empty; + } else { + return it->second; + } +} + +namespace { + +string htmlEscapeString(string str) { + string ret; + for(int point : sanitizeUTF8StringToCodePoints(move(str))) { + if(point <= 0 || point > 0x10FFFF) continue; + if(point == 0xD) continue; + if( + (point <= 0x1F || (point >= 0x7F && point <= 0x9F)) && + point != 0x20 && point != 0x9 && point != 0xA && point != 0xC + ) continue; + if(point >= 0xFDD0 && point <= 0xFDEF) continue; + if((point & 0xFFFF) == 0xFFFE || (point & 0xFFFF) == 0xFFFF) continue; + ret += "&#" + toString(point) + ";"; + } + return ret; +} + +void parseSelectedBookmarks(string formData, std::set& selectedBookmarks) { + if(formData.empty()) { + return; + } + + size_t start = 0; + while(true) { + size_t end = formData.find('&', start); + bool isLast = (end == string::npos); + if(isLast) { + end = formData.size(); + } + + string elem = formData.substr(start, end - start); + const string prefix = "bookmark"; + const string suffix = "=on"; + if( + elem.size() > prefix.size() + suffix.size() && + elem.substr(0, prefix.size()) == prefix && + elem.substr(elem.size() - suffix.size()) == suffix + ) { + optional id = parseString( + elem.substr(prefix.size(), elem.size() - prefix.size() - suffix.size()) + ); + if(id.has_value()) { + selectedBookmarks.insert(id.value()); + } + } + + if(isLast) { + return; + } + start = end + 1; + } +} + +} + +string handleBookmarksRequest(CefRefPtr request) { + CEF_REQUIRE_IO_THREAD(); + + shared_ptr bookmarks = Bookmarks::load(); + + if(request->GetMethod() == "POST") { + CefRefPtr postData = request->GetPostData(); + std::set selectedBookmarks; + if(postData) { + std::vector> elements; + postData->GetElements(elements); + for(CefRefPtr elem : elements) { + if(elem->GetType() == PDE_TYPE_BYTES) { + size_t dataSize = elem->GetBytesCount(); + std::vector data(dataSize); + if(elem->GetBytes(dataSize, data.data()) == dataSize) { + parseSelectedBookmarks(string(data.begin(), data.end()), selectedBookmarks); + } + } + } + } + if(!selectedBookmarks.empty() && bookmarks) { + for(uint64_t id : selectedBookmarks) { + bookmarks->removeBookmark(id); + } + bookmarks->save(); + } + } + + string page = + "\n" + "Bookmarks\n" + "

Bookmarks

\n" + "
\n"; + + if(bookmarks) { + const map& bookmarkData = bookmarks->getData(); + vector*> items; + for(const auto& item : bookmarkData) { + items.push_back(&item); + } + sort( + items.begin(), items.end(), + []( + const pair* a, + const pair* b + ) { + return + make_tuple(a->second.time, a->second.title, a->second.url) < + make_tuple(b->second.time, b->second.title, b->second.url); + } + ); + for(const auto* item : items) { + uint64_t id = item->first; + const Bookmark& bookmark = item->second; + page += "

\n"; + page += + "\n"; + page += "\n"; + page += "

\n"; + } + if(items.empty()) { + page += "

You have no bookmarks

\n"; + } else { + page += "

\n"; + } + } else { + page += "

Loading bookmarks failed (see log)

\n"; + } + + page += "\n"; + page += "\n"; + return page; +} + +} diff --git a/src/bookmarks.hpp b/src/bookmarks.hpp new file mode 100644 index 0000000..860e06c --- /dev/null +++ b/src/bookmarks.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "common.hpp" + +class CefRequest; + +namespace browservice { + +struct Bookmark { + string url; + string title; + uint64_t time; +}; + +class Bookmarks { +SHARED_ONLY_CLASS(Bookmarks); +public: + Bookmarks(CKey); + + // Returns empty pointer and writes to log if loading failed + static shared_ptr load(); + + // Returns false and writes to log if saving failed + bool save(); + + const map& getData() const; + + uint64_t putBookmark(Bookmark bookmark); + void removeBookmark(uint64_t id); + +private: + map data_; +}; + +// Returns the bookmark ID if given url is bookmarked in the last state saved +// using Bookmarks::save, or if no bookmarks have yet been saved, the state +// loaded on the first call to this function. Safe to call from any thread. +optional getCachedBookmarkIDByURL(string url); + +string handleBookmarksRequest(CefRefPtr request); + +} diff --git a/src/common.cpp b/src/common.cpp index 66da3ab..d9dc838 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -6,15 +6,18 @@ namespace browservice { thread_local mt19937 rng(random_device{}()); -string sanitizeUTF8String(string str) { - string ret; +namespace { + +template +void sanitizeUTF8StringImpl(const string& str, A byteHandler, B pointHandler) { for(size_t i = 0; i < str.size(); ++i) { int ch = (int)(uint8_t)str[i]; if(ch == 0) { continue; } if(ch < 128) { - ret.push_back((char)ch); + byteHandler(str.data() + i, 1); + pointHandler(ch); continue; } @@ -55,10 +58,36 @@ string sanitizeUTF8String(string str) { )) || (length == (size_t)4 && point >= 0x10000 && point <= 0x10FFFF) ) { - ret.append(str.substr(i, length)); + byteHandler(str.data() + i, length); + pointHandler(point); i += length - 1; } } +} + +} + +string sanitizeUTF8String(string str) { + string ret; + sanitizeUTF8StringImpl( + str, + [&](const char* bytes, size_t count) { + str.insert(str.end(), bytes, bytes + count); + }, + [](int) {} + ); + return ret; +} + +vector sanitizeUTF8StringToCodePoints(string str) { + vector ret; + sanitizeUTF8StringImpl( + str, + [](const char*, size_t) {}, + [&](int point) { + ret.push_back(point); + } + ); return ret; } diff --git a/src/common.hpp b/src/common.hpp index 4656e1c..8ea92ba 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -125,6 +125,7 @@ string toString(const T& obj) { } string sanitizeUTF8String(string str); +vector sanitizeUTF8StringToCodePoints(string str); // Logging macros that log given message along with severity, source file and // line information to stderr. Message is formed by calling toString for each diff --git a/src/control_bar.cpp b/src/control_bar.cpp index 4a45776..09d6610 100644 --- a/src/control_bar.cpp +++ b/src/control_bar.cpp @@ -1,8 +1,11 @@ #include "control_bar.hpp" +#include "bookmarks.hpp" #include "text.hpp" #include "timeout.hpp" +#include + namespace browservice { namespace { @@ -146,6 +149,207 @@ MenuButtonIcon goIcon = { ) }; +vector openBookmarksIconPattern = { + "...................", + "...................", + "...ggggggggggggg...", + "...ghwwwwwwwwwwH...", + "...gchwwDDDDDDDwH..", + "...gKqssDrrrrr*sH..", + "...gKkkkDrrrrr*k*..", + "...gKkkkDrrrrr*k*..", + "...gKk--Drrrrr*k*..", + "...gKkkkDrr*rr*k*..", + "...gKkkkDr*kmr*k*..", + "...gKk--D*---m*k*..", + "...gKkkkDkkkkk*k*..", + "...gKkkkkkkkkkkk*..", + "...gKk---------k*..", + "...gKkkkkkkkkkkk*..", + "....Hkkkkkkkkkkk*..", + "....*************..", + "..................." +}; + +MenuButtonIcon openBookmarksIcon = { + ImageSlice::createImageFromStrings( + openBookmarksIconPattern, + { + {'.', {192, 192, 192}}, + {'*', {0, 0, 0}}, + {'-', {160, 96, 0}}, + {'w', {255, 255, 255}}, + {'r', {255, 128, 128}}, + {'D', {56, 30, 30}}, + {'m', {23, 15, 15}}, + {'k', {192, 128, 0}}, + {'s', {66, 46, 0}}, + {'q', {80, 48, 0}}, + {'K', {128, 64, 0}}, + {'c', {112, 56, 0}}, + {'g', {56, 28, 0}}, + {'h', {88, 46, 0}}, + {'H', {44, 22, 0}} + } + ), + ImageSlice::createImageFromStrings( + openBookmarksIconPattern, + { + {'.', {192, 192, 192}}, + {'*', {0, 0, 0}}, + {'-', {128, 128, 128}}, + {'w', {255, 255, 255}}, + {'r', {224, 224, 224}}, + {'D', {48, 48, 48}}, + {'m', {24, 24, 24}}, + {'k', {160, 160, 160}}, + {'s', {58, 58, 58}}, + {'q', {60, 60, 60}}, + {'K', {96, 96, 96}}, + {'c', {80, 80, 80}}, + {'g', {48, 48, 48}}, + {'h', {56, 56, 56}}, + {'H', {40, 40, 40}} + } + ) +}; + +vector bookmarkOffIconPattern = { + "...................", + "...................", + ".....xSSSSSSSSP....", + ".....Drrrrrrrr*....", + ".....Drrrrrrrr*....", + ".....Drrrrrrrr*....", + ".....Drrrrrrrr*....", + ".....Drrrrrrrr*....", + ".....Drrrrrrrr*....", + ".....Drrrrrrrr*....", + ".....Drrrrrrrr*....", + ".....Drrrrrrrr*....", + ".....Drrr**rrr*....", + ".....Drr*..mrr*....", + ".....Dr*....mr*....", + ".....D*......m*....", + ".....D........*....", + "...................", + "..................." +}; + +MenuButtonIcon bookmarkOffIcon = { + ImageSlice::createImageFromStrings( + bookmarkOffIconPattern, + { + {'.', {192, 192, 192}}, + {'*', {0, 0, 0}}, + {'r', {255, 128, 128}}, + {'S', {80, 80, 80}}, + {'P', {48, 48, 48}}, + {'D', {72, 48, 48}}, + {'m', {36, 24, 24}}, + {'x', {76, 64, 64}} + } + ), + ImageSlice::createImageFromStrings( + bookmarkOffIconPattern, + { + {'.', {192, 192, 192}}, + {'*', {0, 0, 0}}, + {'r', {212, 212, 212}}, + {'S', {96, 96, 96}}, + {'P', {64, 64, 64}}, + {'D', {64, 64, 64}}, + {'m', {32, 32, 32}}, + {'x', {80, 80, 80}} + } + ) +}; + +vector bookmarkOnIconPattern = { + "...................", + "...................", + "......xxxxxxxg.....", + ".....gwvvvvvvv*....", + "....gdg8wwwwwwC*...", + "....grg8rrrrrrD*...", + "....grg8rrrrrrE*...", + "....grg7AAAAAAF*...", + "....grg6BBBBBBG*...", + "....grg5CCCCCCH*...", + "....g*grDDDDDDI*...", + "......gAEEGEEEJ*...", + "......gAEG**EEJ*...", + "......gAG*..mEJ*...", + "......gA*....mJ*...", + "......g*......m*...", + "......g........*...", + "...................", + "..................." +}; + +MenuButtonIcon bookmarkOnIcon = { + ImageSlice::createImageFromStrings( + bookmarkOnIconPattern, + { + {'.', {192, 192, 192}}, + {'*', {0, 0, 0}}, + {'5', {255, 131, 131}}, + {'6', {255, 134, 134}}, + {'7', {255, 137, 137}}, + {'8', {255, 140, 140}}, + {'r', {255, 128, 128}}, + {'A', {253, 127, 127}}, + {'B', {250, 125, 125}}, + {'C', {247, 124, 124}}, + {'D', {244, 122, 122}}, + {'E', {241, 121, 121}}, + {'F', {238, 119, 119}}, + {'G', {235, 118, 118}}, + {'H', {232, 116, 116}}, + {'I', {229, 115, 115}}, + {'J', {226, 113, 113}}, + {'d', {216, 104, 104}}, + {'2', {228, 112, 112}}, + {'w', {255, 134, 134}}, + {'v', {255, 148, 148}}, + {'g', {96, 48, 48}}, + {'m', {48, 32, 32}}, + {'x', {96, 64, 64}}, + {'p', {48, 32, 32}} + } + ), + ImageSlice::createImageFromStrings( + bookmarkOnIconPattern, + { + {'.', {192, 192, 192}}, + {'*', {0, 0, 0}}, + {'5', {227, 227, 227}}, + {'6', {230, 230, 230}}, + {'7', {233, 233, 233}}, + {'8', {236, 236, 236}}, + {'r', {224, 224, 224}}, + {'A', {221, 221, 221}}, + {'B', {218, 218, 218}}, + {'C', {215, 215, 215}}, + {'D', {212, 212, 212}}, + {'E', {209, 209, 209}}, + {'F', {206, 206, 206}}, + {'G', {203, 203, 203}}, + {'H', {200, 200, 200}}, + {'I', {197, 197, 197}}, + {'J', {194, 194, 194}}, + {'d', {192, 192, 192}}, + {'2', {200, 200, 200}}, + {'w', {230, 230, 230}}, + {'v', {240, 240, 240}}, + {'g', {64, 64, 64}}, + {'m', {40, 40, 40}}, + {'x', {80, 80, 80}}, + {'p', {40, 40, 40}} + } + ) +}; + vector findIconPattern = { "...................", "...................", @@ -357,7 +561,10 @@ struct ControlBar::Layout { findButtonEnd = clipboardButtonStart; findButtonStart = findButtonEnd - BtnWidth; - int separator1End = findButtonStart; + openBookmarksButtonEnd = findButtonStart; + openBookmarksButtonStart = openBookmarksButtonEnd - BtnWidth; + + int separator1End = openBookmarksButtonStart; int separator1Start = separator1End - SeparatorWidth; separator1Pos = separator1Start + SeparatorWidth / 2; @@ -367,7 +574,10 @@ struct ControlBar::Layout { addrTextStart = addrStart; addrTextEnd = addrTextStart + AddressTextWidth; - goButtonEnd = addrEnd; + bookmarkToggleButtonEnd = addrEnd; + bookmarkToggleButtonStart = bookmarkToggleButtonEnd - BtnWidth; + + goButtonEnd = bookmarkToggleButtonStart; goButtonStart = goButtonEnd - BtnWidth; addrBoxStart = addrTextEnd; @@ -394,6 +604,9 @@ struct ControlBar::Layout { int goButtonStart; int goButtonEnd; + int bookmarkToggleButtonStart; + int bookmarkToggleButtonEnd; + int securityIconStart; int addrFieldStart; @@ -414,6 +627,9 @@ struct ControlBar::Layout { int downloadStart; int downloadEnd; + int openBookmarksButtonStart; + int openBookmarksButtonEnd; + int findButtonStart; int findButtonEnd; @@ -499,9 +715,16 @@ void ControlBar::setSecurityStatus(SecurityStatus value) { void ControlBar::setAddress(string addr) { REQUIRE_UI_THREAD(); + address_ = addr; + setBookmarkID_(getCachedBookmarkIDByURL(addr)); addrField_->setText(move(addr)); } +void ControlBar::setPageTitle(string pageTitle) { + REQUIRE_UI_THREAD(); + pageTitle_ = move(pageTitle); +} + void ControlBar::setLoading(bool loading) { REQUIRE_UI_THREAD(); @@ -588,6 +811,37 @@ void ControlBar::onMenuButtonPressed(weak_ptr button) { ); } + if(button.lock() == bookmarkToggleButton_ && !address_.empty()) { + shared_ptr bookmarks = Bookmarks::load(); + if(bookmarks) { + if(bookmarkID_.has_value()) { + bookmarks->removeBookmark(bookmarkID_.value()); + if(bookmarks->save()) { + setBookmarkID_({}); + } + } else if(address_ != "browservice:bookmarks") { + Bookmark bookmark; + bookmark.url = address_; + bookmark.title = pageTitle_; + if(bookmark.title.empty()) { + bookmark.title = bookmark.url; + } + bookmark.time = time(nullptr); + uint64_t id = bookmarks->putBookmark(move(bookmark)); + if(bookmarks->save()) { + setBookmarkID_(id); + } + } + } + } + + if(button.lock() == openBookmarksButton_) { + postTask( + eventHandler_, + &ControlBarEventHandler::onOpenBookmarksButtonPressed + ); + } + if(button.lock() == findButton_) { openFindBar(); } @@ -644,6 +898,8 @@ void ControlBar::afterConstruct_(shared_ptr self) { addrField_->setAllowEmptySubmit(false); goButton_ = MenuButton::create(goIcon, self, self); + bookmarkToggleButton_ = MenuButton::create(bookmarkOffIcon, self, self); + openBookmarksButton_ = MenuButton::create(openBookmarksIcon, self, self); findButton_ = MenuButton::create(findIcon, self, self); clipboardButton_ = MenuButton::create(clipboardIcon, self, self); downloadButton_ = Button::create(self, self); @@ -664,6 +920,11 @@ ControlBar::Layout ControlBar::layout_() { ); } +void ControlBar::setBookmarkID_(optional bookmarkID) { + bookmarkID_ = bookmarkID; + bookmarkToggleButton_->setIcon(bookmarkID_.has_value() ? bookmarkOnIcon : bookmarkOffIcon); +} + void ControlBar::widgetViewportUpdated_() { REQUIRE_UI_THREAD(); @@ -676,6 +937,12 @@ void ControlBar::widgetViewportUpdated_() { goButton_->setViewport(viewport.subRect( layout.goButtonStart, layout.goButtonEnd, 1, Height - 4 )); + bookmarkToggleButton_->setViewport(viewport.subRect( + layout.bookmarkToggleButtonStart, layout.bookmarkToggleButtonEnd, 1, Height - 4 + )); + openBookmarksButton_->setViewport(viewport.subRect( + layout.openBookmarksButtonStart, layout.openBookmarksButtonEnd, 1, Height - 4 + )); findButton_->setViewport(viewport.subRect( layout.findButtonStart, layout.findButtonEnd, 1, Height - 4 )); @@ -851,6 +1118,8 @@ vector> ControlBar::widgetListChildren_() { vector> children = { addrField_, goButton_, + bookmarkToggleButton_, + openBookmarksButton_, findButton_ }; if(qualitySelector_) { diff --git a/src/control_bar.hpp b/src/control_bar.hpp index b564bfa..e4e90c0 100644 --- a/src/control_bar.hpp +++ b/src/control_bar.hpp @@ -23,6 +23,7 @@ class ControlBarEventHandler { virtual void onFind(string text, bool forward, bool findNext) = 0; virtual void onStopFind(bool clearSelection) = 0; virtual void onClipboardButtonPressed() = 0; + virtual void onOpenBookmarksButtonPressed() = 0; }; class TextLayout; @@ -52,6 +53,7 @@ SHARED_ONLY_CLASS(ControlBar); void setSecurityStatus(SecurityStatus value); void setAddress(string addr); + void setPageTitle(string pageTitle); void setLoading(bool loading); void setPendingDownloadCount(int count); @@ -88,6 +90,8 @@ SHARED_ONLY_CLASS(ControlBar); class Layout; Layout layout_(); + void setBookmarkID_(optional bookmarkID); + // Widget: virtual void widgetViewportUpdated_() override; virtual void widgetRender_() override; @@ -107,6 +111,8 @@ SHARED_ONLY_CLASS(ControlBar); SecurityStatus securityStatus_; shared_ptr goButton_; + shared_ptr bookmarkToggleButton_; + shared_ptr openBookmarksButton_; shared_ptr findButton_; shared_ptr clipboardButton_; @@ -123,6 +129,10 @@ SHARED_ONLY_CLASS(ControlBar); bool loading_; optional loadingAnimationStartTime_; + + string address_; + string pageTitle_; + optional bookmarkID_; }; } diff --git a/src/globals.cpp b/src/globals.cpp index 70b78d0..b6a7edd 100644 --- a/src/globals.cpp +++ b/src/globals.cpp @@ -3,12 +3,54 @@ #include "text.hpp" #include "xwindow.hpp" +#include +#include +#include + namespace browservice { +namespace { + +string getHomeDirPath() { + const char* path = getenv("HOME"); + if(path != nullptr) { + return path; + } + + uid_t uid = getuid(); + + long bufSizeSuggestion = sysconf(_SC_GETPW_R_SIZE_MAX); + size_t bufSize = bufSizeSuggestion > 0 ? (size_t)bufSizeSuggestion : (size_t)1; + + while(true) { + struct passwd pwd; + char* buf = (char*)malloc(bufSize); + REQUIRE(buf != nullptr); + + int resultCode; + struct passwd* resultPtr; + resultCode = getpwuid_r(uid, &pwd, buf, bufSize, &resultPtr); + if(resultCode == 0 && resultPtr == &pwd) { + string path = pwd.pw_dir; + free(buf); + return path; + } + REQUIRE(resultCode == ERANGE && resultPtr == nullptr); + bufSize *= 2; + } +} + +string getDotDirPath() { + return getHomeDirPath() + "/.browservice"; +} + +} + Globals::Globals(CKey, shared_ptr config) : config(config), xWindow(XWindow::create()), - textRenderContext(TextRenderContext::create()) + textRenderContext(TextRenderContext::create()), + dotDirPath(getDotDirPath()) { REQUIRE(config); } diff --git a/src/globals.hpp b/src/globals.hpp index b81131a..edcf9b5 100644 --- a/src/globals.hpp +++ b/src/globals.hpp @@ -15,6 +15,7 @@ SHARED_ONLY_CLASS(Globals); const shared_ptr config; const shared_ptr xWindow; const shared_ptr textRenderContext; + const string dotDirPath; }; extern shared_ptr globals; diff --git a/src/main.cpp b/src/main.cpp index 3aa07d5..50a125d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include "globals.hpp" #include "server.hpp" +#include "scheme.hpp" #include "vice.hpp" #include "xvfb.hpp" @@ -31,7 +32,14 @@ class App : public CefBrowserProcessHandler { public: - App(shared_ptr viceCtx) { + App() { + initialized_ = false; + } + + void initialize(shared_ptr viceCtx) { + REQUIRE(!initialized_); + + initialized_ = true; serverEventHandler_ = AppServerEventHandler::create(); shutdown_ = false; viceCtx_ = viceCtx; @@ -39,6 +47,7 @@ class App : void shutdown() { REQUIRE_UI_THREAD(); + REQUIRE(initialized_); if(server_) { server_->shutdown(); @@ -47,7 +56,7 @@ class App : } } - // CefApp: + // CefApp (may be used with initialized_ = false in other processes): virtual CefRefPtr GetBrowserProcessHandler() override { return this; } @@ -55,6 +64,10 @@ class App : const CefString& processType, CefRefPtr commandLine ) override { + if(!initialized_) { + return; + } + commandLine->AppendSwitch("disable-smooth-scrolling"); commandLine->AppendSwitchWithValue("use-gl", "desktop"); @@ -66,12 +79,22 @@ class App : } } } + virtual void OnRegisterCustomSchemes(CefRawPtr registrar) { + registrar->AddCustomScheme("browservice", CEF_SCHEME_OPTION_LOCAL | CEF_SCHEME_OPTION_DISPLAY_ISOLATED); + } - // CefBrowserProcessHandler: + // CefBrowserProcessHandler (may be used with initialized_ = false in other processes): virtual void OnContextInitialized() override { + if(!initialized_) { + return; + } + REQUIRE_UI_THREAD(); REQUIRE(!server_); + CefRefPtr schemeHandlerFactory = new BrowserviceSchemeHandlerFactory(); + CefRegisterSchemeHandlerFactory("browservice", "", schemeHandlerFactory); + server_ = Server::create(serverEventHandler_, viceCtx_); viceCtx_.reset(); if(shutdown_) { @@ -80,6 +103,7 @@ class App : } private: + bool initialized_; shared_ptr server_; shared_ptr serverEventHandler_; bool shutdown_; @@ -110,7 +134,9 @@ int main(int argc, char* argv[]) { CefMainArgs mainArgs(argc, argv); - int exitCode = CefExecuteProcess(mainArgs, nullptr, nullptr); + app = new App(); + + int exitCode = CefExecuteProcess(mainArgs, app, nullptr); if(exitCode >= 0) { return exitCode; } @@ -152,7 +178,7 @@ int main(int argc, char* argv[]) { XSetErrorHandler([](Display*, XErrorEvent*) { return 0; }); XSetIOErrorHandler([](Display*) { return 0; }); - app = new App(viceCtx); + app->initialize(viceCtx); viceCtx.reset(); CefSettings settings; diff --git a/src/menu_button.cpp b/src/menu_button.cpp index 04b3ee6..f9b20e6 100644 --- a/src/menu_button.cpp +++ b/src/menu_button.cpp @@ -21,6 +21,13 @@ MenuButton::MenuButton(CKey, mouseDown_ = false; } +void MenuButton::setIcon(MenuButtonIcon icon) { + REQUIRE_UI_THREAD(); + + icon_ = icon; + signalViewDirty_(); +} + void MenuButton::mouseMove_(int x, int y) { ImageSlice viewport = getViewport(); diff --git a/src/menu_button.hpp b/src/menu_button.hpp index 28c6240..ec8f93a 100644 --- a/src/menu_button.hpp +++ b/src/menu_button.hpp @@ -30,6 +30,8 @@ SHARED_ONLY_CLASS(MenuButton); weak_ptr eventHandler ); + void setIcon(MenuButtonIcon icon); + private: void mouseMove_(int x, int y); diff --git a/src/scheme.cpp b/src/scheme.cpp new file mode 100644 index 0000000..9d1593e --- /dev/null +++ b/src/scheme.cpp @@ -0,0 +1,117 @@ +#include "scheme.hpp" + +#include "bookmarks.hpp" + +namespace browservice { + +namespace { + +class StaticResponseResourceHandler : public CefResourceHandler { +public: + StaticResponseResourceHandler(int status, string statusText, string response) { + status_ = status; + statusText_ = move(statusText); + response_ = move(response); + pos_ = 0; + } + + virtual bool Open( + CefRefPtr request, + bool& handleRequest, + CefRefPtr callback + ) override{ + handleRequest = true; + return true; + } + + virtual void GetResponseHeaders( + CefRefPtr response, + int64_t& responseLength, + CefString& redirectUrl + ) override{ + responseLength = (int64_t)response_.size(); + response->SetStatus(status_); + response->SetStatusText(statusText_); + response->SetMimeType("text/html"); + response->SetCharset("UTF-8"); + } + + virtual bool Skip( + int64_t bytesToSkip, + int64_t& bytesSkipped, + CefRefPtr callback + ) override { + int64_t maxSkip = (int64_t)(response_.size() - pos_); + int64_t skipCount = min(bytesToSkip, maxSkip); + REQUIRE(skipCount >= (int64_t)0); + + if(skipCount > (int64_t)0) { + bytesSkipped = skipCount; + pos_ += (size_t)skipCount; + return true; + } else { + bytesSkipped = -2; + return false; + } + } + + virtual bool Read( + void* dataOut, + int bytesToRead, + int& bytesRead, + CefRefPtr callback + ) override { + int64_t maxRead = (int64_t)(response_.size() - pos_); + int readCount = (int)min((int64_t)bytesToRead, maxRead); + REQUIRE(readCount >= 0); + + if(readCount > 0) { + bytesRead = readCount; + memcpy(dataOut, response_.data() + pos_, (size_t)readCount); + pos_ += (size_t)readCount; + return true; + } else { + bytesRead = 0; + return false; + } + } + + virtual void Cancel() override { + response_.clear(); + pos_ = 0; + } + +private: + int status_; + string statusText_; + string response_; + size_t pos_; + + IMPLEMENT_REFCOUNTING(StaticResponseResourceHandler); +}; + +} + +CefRefPtr BrowserviceSchemeHandlerFactory::Create( + CefRefPtr browser, + CefRefPtr frame, + const CefString& scheme_name, + CefRefPtr request +) { + CEF_REQUIRE_IO_THREAD(); + REQUIRE(request); + + int status = 404; + string statusText = "Not Found"; + string response = + "\n" + "404 Not Found

404 Not Found

\n"; + if(request->GetURL() == "browservice:bookmarks") { + status = 200; + statusText = "OK"; + response = handleBookmarksRequest(request); + } + return new StaticResponseResourceHandler(status, move(statusText), move(response)); +} + +} diff --git a/src/scheme.hpp b/src/scheme.hpp new file mode 100644 index 0000000..edc517f --- /dev/null +++ b/src/scheme.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "common.hpp" + +#include "include/cef_app.h" + +namespace browservice { + +class BrowserviceSchemeHandlerFactory : public CefSchemeHandlerFactory { +public: + virtual CefRefPtr Create( + CefRefPtr browser, + CefRefPtr frame, + const CefString& scheme_name, + CefRefPtr request + ) override; + +private: + IMPLEMENT_REFCOUNTING(BrowserviceSchemeHandlerFactory); +}; + +} diff --git a/src/server.cpp b/src/server.cpp index 4816a6b..9e547d3 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -405,8 +405,12 @@ void Server::onWindowCreatePopupRequest( ); shared_ptr newWindow = accept(newHandle); - REQUIRE(newWindow); - REQUIRE(openWindows_.emplace(newHandle, newWindow).second); + if(newWindow) { + REQUIRE(openWindows_.emplace(newHandle, newWindow).second); + } else { + WARNING_LOG("Creating popup window ", newHandle, " failed, closing it in vice plugin"); + viceCtx_->closeWindow(newHandle); + } } else { INFO_LOG( "Popup window ", newHandle, diff --git a/src/window.cpp b/src/window.cpp index e62aace..0c6c77b 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -285,6 +285,16 @@ class Window::Client : window_->updateSecurityStatus_(); } + virtual void OnTitleChange(CefRefPtr browser, const CefString& title) override { + BROWSER_EVENT_HANDLER_CHECKS(); + + if(window_->state_ != Open) { + return; + } + + window_->rootWidget_->controlBar()->setPageTitle(title); + } + virtual bool OnCursorChange( CefRefPtr browser, CefCursorHandle cursorHandle, @@ -781,6 +791,62 @@ void Window::onClipboardButtonPressed() { } } +void Window::onOpenBookmarksButtonPressed() { + REQUIRE_UI_THREAD(); + + if(state_ == Open) { + bool openPopup = true; + if(browser_) { + CefRefPtr frame = browser_->GetMainFrame(); + if(frame) { + string url = frame->GetURL(); + if(url == "about:blank" || url == "browservice:bookmarks") { + openPopup = false; + } + } + } + + if(openPopup) { + INFO_LOG("Bookmark button pressed in window ", handle_, ", opening bookmark popup"); + + shared_ptr allowAcceptCall(new bool); + bool acceptCalled = false; + bool popupDenied = true; + + auto accept = [&, allowAcceptCall]( + uint64_t newHandle + ) -> shared_ptr { + REQUIRE(*allowAcceptCall); + REQUIRE(!acceptCalled); + acceptCalled = true; + + REQUIRE(newHandle); + REQUIRE(newHandle != handle_); + + INFO_LOG("Creating bookmark popup window ", newHandle); + + shared_ptr newWindow = + Window::tryCreate(eventHandler_, newHandle, "browservice:bookmarks"); + + popupDenied = false; + return newWindow; + }; + + REQUIRE(eventHandler_); + *allowAcceptCall = true; + eventHandler_->onWindowCreatePopupRequest(handle_, accept); + *allowAcceptCall = false; + + if(popupDenied) { + WARNING_LOG("Creating bookmark popup window failed because request was denied"); + } + } else { + INFO_LOG("Bookmark button pressed in window ", handle_, ", navigating to bookmarks"); + navigateToURI("browservice:bookmarks"); + } + } +} + void Window::onBrowserAreaViewDirty() { REQUIRE_UI_THREAD(); diff --git a/src/window.hpp b/src/window.hpp index 30de9ba..4d5d7a1 100644 --- a/src/window.hpp +++ b/src/window.hpp @@ -34,7 +34,7 @@ class WindowEventHandler { // To accept popup creation, the implementation should call the accept // function once with the handle of the new window as argument before // returning. The accept function returns the new window that uses the same - // event handler as the original window. + // event handler as the original window, or empty if creation fails. virtual void onWindowCreatePopupRequest( uint64_t handle, function(uint64_t)> accept @@ -120,6 +120,7 @@ SHARED_ONLY_CLASS(Window); virtual void onFind(string text, bool forward, bool findNext) override; virtual void onStopFind(bool clearSelection) override; virtual void onClipboardButtonPressed() override; + virtual void onOpenBookmarksButtonPressed() override; // BrowserAreaEventHandler: virtual void onBrowserAreaViewDirty() override;