1
0
mirror of synced 2024-12-23 21:14:51 +01:00
ImHex/plugins/builtin/source/content/views/view_store.cpp

381 lines
16 KiB
C++
Raw Normal View History

2021-12-07 22:47:41 +01:00
#include "content/views/view_store.hpp"
2023-04-19 21:56:34 +02:00
#include <hex/api/theme_manager.hpp>
#include <hex/api/achievement_manager.hpp>
2023-04-19 21:56:34 +02:00
#include <hex/api_urls.hpp>
#include <hex/api/content_registry.hpp>
#include <popups/popup_notification.hpp>
#include <toasts/toast_notification.hpp>
2023-04-08 12:30:38 +02:00
#include <imgui.h>
#include <hex/helpers/utils.hpp>
#include <hex/helpers/crypto.hpp>
#include <hex/helpers/logger.hpp>
2021-09-08 17:22:54 +02:00
#include <hex/helpers/magic.hpp>
#include <hex/helpers/fs.hpp>
#include <hex/helpers/tar.hpp>
#include <filesystem>
2021-09-08 17:22:54 +02:00
#include <functional>
#include <nlohmann/json.hpp>
#include <wolv/io/file.hpp>
2021-12-07 22:47:41 +01:00
namespace hex::plugin::builtin {
using namespace std::literals::string_literals;
using namespace std::literals::chrono_literals;
ViewStore::ViewStore() : View::Floating("hex.builtin.view.store.name") {
2024-01-08 21:51:48 +01:00
ContentRegistry::Interface::addMenuItem({ "hex.builtin.menu.extras", "hex.builtin.view.store.name" }, ICON_VS_GLOBE, 1000, Shortcut::None, [&, this] {
2023-12-19 13:10:25 +01:00
if (m_requestStatus == RequestStatus::NotAttempted)
this->refresh();
this->getWindowOpenState() = true;
});
2023-12-19 13:10:25 +01:00
m_httpRequest.setTimeout(30'0000);
addCategory("hex.builtin.view.store.tab.patterns", "patterns", &paths::Patterns);
addCategory("hex.builtin.view.store.tab.includes", "includes", &paths::PatternsInclude);
addCategory("hex.builtin.view.store.tab.magic", "magic", &paths::Magic, []{
magic::compile();
});
addCategory("hex.builtin.view.store.tab.nodes", "nodes", &paths::Nodes);
addCategory("hex.builtin.view.store.tab.encodings", "encodings", &paths::Encodings);
addCategory("hex.builtin.view.store.tab.constants", "constants", &paths::Constants);
addCategory("hex.builtin.view.store.tab.themes", "themes", &paths::Themes, [this]{
2023-12-19 13:10:25 +01:00
auto themeFile = wolv::io::File(m_downloadPath, wolv::io::File::Mode::Read);
ThemeManager::addTheme(themeFile.readString());
});
addCategory("hex.builtin.view.store.tab.yara", "yara", &paths::Yara);
}
void updateEntryMetadata(StoreEntry &storeEntry, const StoreCategory &category) {
// Check if file is installed already or has an update available
for (const auto &folder : category.path->write()) {
auto path = folder / std::fs::path(storeEntry.fileName);
if (wolv::io::fs::exists(path)) {
storeEntry.installed = true;
wolv::io::File file(path, wolv::io::File::Mode::Read);
auto bytes = file.readVector();
auto fileHash = crypt::sha256(bytes);
// Compare installed file hash with hash of repo file
if (std::vector(fileHash.begin(), fileHash.end()) != crypt::decode16(storeEntry.hash))
storeEntry.hasUpdate = true;
storeEntry.system = !fs::isPathWritable(folder);
return;
}
}
storeEntry.installed = false;
storeEntry.hasUpdate = false;
storeEntry.system = false;
}
void ViewStore::drawTab(hex::plugin::builtin::StoreCategory &category) {
if (ImGui::BeginTabItem(Lang(category.unlocalizedName))) {
if (ImGui::BeginTable("##pattern_language", 4, ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("hex.builtin.view.store.row.name"_lang, ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("hex.builtin.view.store.row.description"_lang, ImGuiTableColumnFlags_None);
ImGui::TableSetupColumn("hex.builtin.view.store.row.authors"_lang, ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableHeadersRow();
u32 id = 1;
for (auto &entry : category.entries) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::TextUnformatted(entry.name.c_str());
ImGui::TableNextColumn();
ImGui::TextUnformatted(entry.description.c_str());
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(500);
ImGui::TextUnformatted(entry.description.c_str());
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
ImGui::TableNextColumn();
// The space makes a padding in the UI
ImGuiExt::TextFormatted("{} ", wolv::util::combineStrings(entry.authors, ", "));
ImGui::TableNextColumn();
const auto buttonSize = ImVec2(100_scaled, ImGui::GetTextLineHeightWithSpacing());
ImGui::PushID(id);
2023-12-19 13:10:25 +01:00
ImGui::BeginDisabled(m_updateAllTask.isRunning() || (m_download.valid() && m_download.wait_for(0s) != std::future_status::ready));
{
if (entry.downloading) {
2023-12-19 13:10:25 +01:00
ImGui::ProgressBar(m_httpRequest.getProgress(), buttonSize, "");
2023-12-19 13:10:25 +01:00
if (m_download.valid() && m_download.wait_for(0s) == std::future_status::ready) {
this->handleDownloadFinished(category, entry);
}
} else {
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
if (entry.hasUpdate) {
if (ImGui::Button("hex.builtin.view.store.update"_lang, buttonSize)) {
2023-12-28 21:51:31 +01:00
entry.downloading = this->download(category.path, entry.fileName, entry.link);
}
} else if (entry.system) {
ImGui::BeginDisabled();
ImGui::Button("hex.builtin.view.store.system"_lang, buttonSize);
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::BeginTooltip();
ImGui::TextUnformatted("hex.builtin.view.store.system.explanation"_lang);
ImGui::EndTooltip();
}
} else if (!entry.installed) {
if (ImGui::Button("hex.builtin.view.store.download"_lang, buttonSize)) {
2023-12-28 21:51:31 +01:00
entry.downloading = this->download(category.path, entry.fileName, entry.link);
AchievementManager::unlockAchievement("hex.builtin.achievement.misc", "hex.builtin.achievement.misc.download_from_store.name");
}
} else {
if (ImGui::Button("hex.builtin.view.store.remove"_lang, buttonSize)) {
entry.installed = !this->remove(category.path, entry.fileName);
// remove() will not update the entry to mark it as a system entry, so we do it manually
updateEntryMetadata(entry, category);
}
}
ImGui::PopStyleVar();
}
}
ImGui::EndDisabled();
ImGui::PopID();
id++;
}
ImGui::EndTable();
}
ImGui::EndTabItem();
}
}
void ViewStore::drawStore() {
ImGuiExt::Header("hex.builtin.view.store.desc"_lang, true);
bool reloading = false;
2023-12-19 13:10:25 +01:00
if (m_apiRequest.valid()) {
if (m_apiRequest.wait_for(0s) != std::future_status::ready)
reloading = true;
else {
try {
this->parseResponse();
} catch (nlohmann::json::exception &e) {
log::error("Failed to parse store response: {}", e.what());
}
}
}
ImGui::BeginDisabled(reloading);
2021-12-07 22:47:41 +01:00
if (ImGui::Button("hex.builtin.view.store.reload"_lang)) {
this->refresh();
}
ImGui::EndDisabled();
if (reloading) {
ImGui::SameLine();
ImGuiExt::TextSpinner("hex.builtin.view.store.loading"_lang);
}
// Align the button to the right
ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - 25_scaled);
2023-12-19 13:10:25 +01:00
ImGui::BeginDisabled(m_updateAllTask.isRunning() || m_updateCount == 0);
if (ImGuiExt::IconButton(ICON_VS_CLOUD_DOWNLOAD, ImGui::GetStyleColorVec4(ImGuiCol_Text))) {
m_updateAllTask = TaskManager::createTask("hex.builtin.task.updating_store"_lang, m_updateCount, [this](auto &task) {
2023-12-19 13:10:25 +01:00
for (auto &category : m_categories) {
for (auto &entry : category.entries) {
if (entry.hasUpdate) {
2023-12-28 21:51:31 +01:00
entry.downloading = this->download(category.path, entry.fileName, entry.link);
if (!m_download.valid())
continue;
2023-12-19 13:10:25 +01:00
m_download.wait();
while (m_download.valid() && m_download.wait_for(100ms) != std::future_status::ready) {
task.update();
}
entry.hasUpdate = false;
entry.downloading = false;
task.increment();
}
}
}
});
}
2023-12-19 13:10:25 +01:00
ImGuiExt::InfoTooltip(hex::format("hex.builtin.view.store.update_count"_lang, m_updateCount.load()).c_str());
ImGui::EndDisabled();
if (ImGui::BeginTabBar("storeTabs")) {
2023-12-19 13:10:25 +01:00
for (auto &category : m_categories) {
this->drawTab(category);
}
ImGui::EndTabBar();
}
}
void ViewStore::refresh() {
// Do not refresh if a refresh is already in progress
2023-12-19 13:10:25 +01:00
if (m_requestStatus == RequestStatus::InProgress)
return;
2023-12-19 13:10:25 +01:00
m_requestStatus = RequestStatus::InProgress;
2023-12-19 13:10:25 +01:00
for (auto &category : m_categories) {
category.entries.clear();
}
2023-12-19 13:10:25 +01:00
m_httpRequest.setUrl(ImHexApiURL + "/store"s);
m_apiRequest = m_httpRequest.execute();
}
void ViewStore::parseResponse() {
2023-12-19 13:10:25 +01:00
auto response = m_apiRequest.get();
m_requestStatus = response.isSuccess() ? RequestStatus::Succeeded : RequestStatus::Failed;
if (m_requestStatus == RequestStatus::Succeeded) {
auto json = nlohmann::json::parse(response.getData());
auto parseStoreEntries = [](auto storeJson, StoreCategory &category) {
// Check if the response handles the type of files
if (storeJson.contains(category.requestName)) {
for (auto &entry : storeJson[category.requestName]) {
// Check if entry is valid
if (entry.contains("name") && entry.contains("desc") && entry.contains("authors") && entry.contains("file") && entry.contains("url") && entry.contains("hash") && entry.contains("folder")) {
// Parse entry
StoreEntry storeEntry = { entry["name"], entry["desc"], entry["authors"], entry["file"], entry["url"], entry["hash"], entry["folder"], false, false, false, false };
updateEntryMetadata(storeEntry, category);
category.entries.push_back(storeEntry);
}
}
}
std::sort(category.entries.begin(), category.entries.end(), [](const auto &lhs, const auto &rhs) {
return lhs.name < rhs.name;
});
};
2023-12-19 13:10:25 +01:00
for (auto &category : m_categories) {
parseStoreEntries(json, category);
}
2023-12-19 13:10:25 +01:00
m_updateCount = 0;
for (auto &category : m_categories) {
for (auto &entry : category.entries) {
if (entry.hasUpdate)
2023-12-19 13:10:25 +01:00
m_updateCount += 1;
}
}
}
2023-12-19 13:10:25 +01:00
m_apiRequest = {};
}
void ViewStore::drawContent() {
2023-12-19 13:10:25 +01:00
if (m_requestStatus == RequestStatus::Failed)
ImGuiExt::TextFormattedColored(ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_ToolbarRed), "hex.builtin.view.store.netfailed"_lang);
this->drawStore();
}
bool ViewStore::download(const paths::impl::DefaultPath *pathType, const std::string &fileName, const std::string &url) {
bool downloading = false;
for (const auto &folderPath : pathType->write()) {
if (!fs::isPathWritable(folderPath))
continue;
// Verify that we write the file to the right folder
// this is to prevent the filename from having elements like ../
auto fullPath = std::fs::weakly_canonical(folderPath / std::fs::path(fileName));
auto [folderIter, pathIter] = std::mismatch(folderPath.begin(), folderPath.end(), fullPath.begin());
2023-11-10 20:47:08 +01:00
if (folderIter != folderPath.end()) {
log::warn("The destination file name '{}' is invalid", fileName);
return false;
}
downloading = true;
m_downloadPath = fullPath;
m_httpRequest.setUrl(url);
m_download = m_httpRequest.downloadFile(fullPath);
break;
}
if (!downloading) {
ui::ToastError::open("hex.builtin.view.store.download_error"_lang);
return false;
}
return downloading;
}
bool ViewStore::remove(const paths::impl::DefaultPath *pathType, const std::string &fileName) {
bool removed = true;
for (const auto &path : pathType->write()) {
const auto filePath = path / fileName;
const auto folderPath = (path / std::fs::path(fileName).stem());
wolv::io::fs::remove(filePath);
wolv::io::fs::removeAll(folderPath);
removed = removed && !wolv::io::fs::exists(filePath) && !wolv::io::fs::exists(folderPath);
EventStoreContentRemoved::post(filePath);
}
return removed;
}
void ViewStore::addCategory(const UnlocalizedString &unlocalizedName, const std::string &requestName, const paths::impl::DefaultPath *path, std::function<void()> downloadCallback) {
2023-12-19 13:10:25 +01:00
m_categories.push_back({ unlocalizedName, requestName, path, { }, std::move(downloadCallback) });
}
void ViewStore::handleDownloadFinished(const StoreCategory &category, StoreEntry &entry) {
entry.downloading = false;
2023-12-19 13:10:25 +01:00
auto response = m_download.get();
if (response.isSuccess()) {
if (entry.hasUpdate)
2023-12-19 13:10:25 +01:00
m_updateCount -= 1;
entry.installed = true;
entry.hasUpdate = false;
entry.system = false;
if (entry.isFolder) {
2023-12-19 13:10:25 +01:00
Tar tar(m_downloadPath, Tar::Mode::Read);
tar.extractAll(m_downloadPath.parent_path() / m_downloadPath.stem());
EventStoreContentDownloaded::post(m_downloadPath.parent_path() / m_downloadPath.stem());
} else {
2023-12-19 13:10:25 +01:00
EventStoreContentDownloaded::post(m_downloadPath);
}
category.downloadCallback();
2023-12-27 16:33:49 +01:00
} else {
log::error("Download failed! HTTP Code {}", response.getStatusCode());
2023-12-27 16:33:49 +01:00
}
2023-12-19 13:10:25 +01:00
m_download = {};
}
}