#include "updater.h"
#include <fmt/core.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <wx/evtloop.h>
#include <wx/progdlg.h>
#include <wx/webrequest.h>
#include <wx/wfstream.h>
#include <wx/zipstrm.h>
#include <yaml-cpp/yaml.h>
#include <cstdio>
#include <deque>
#include <filesystem>
#include <fstream>
#include "global.h"
#include "logger.h"
#include "version.h"
constexpr const char* kVersionFileUrl =
"https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml";
constexpr const char* kChangelogUrl =
"https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md";
namespace {
std::string CalculateStringSha256(const wxString& data) {
unsigned char hash[SHA256_DIGEST_LENGTH];
EVP_MD_CTX* sha256 = EVP_MD_CTX_new();
EVP_DigestInit(sha256, EVP_sha256());
EVP_DigestUpdate(sha256, data.c_str(), data.length());
EVP_DigestFinal_ex(sha256, hash, nullptr);
EVP_MD_CTX_free(sha256);
char output[65] = {0};
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
sprintf(output + (i * 2), "%02x", hash[i]);
}
return std::string(output);
}
} // namespace
Updater::Updater(wxFrame* parent) : parent_(parent) {
Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this);
}
void Updater::Cleanup() {
std::filesystem::path oldDir = GetExecutableDirectory() / "old";
if (std::filesystem::is_directory(oldDir)) {
std::filesystem::remove_all(oldDir);
}
}
void Updater::CheckForUpdates(bool invisible) {
wxWebRequest versionRequest =
wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl);
if (invisible) {
update_state_ = UpdateState::GetVersionInvisible;
versionRequest.Start();
} else {
update_state_ = UpdateState::GetVersionManual;
if (DownloadWithProgress(versionRequest)) {
if (versionRequest.GetState() == wxWebRequest::State_Failed) {
wxMessageBox("Could not check for updates.", "Error",
wxOK | wxICON_ERROR);
} else if (versionRequest.GetState() == wxWebRequest::State_Completed) {
ProcessVersionFile(
versionRequest.GetResponse().AsString().utf8_string());
}
}
}
}
void Updater::OnWebRequestState(wxWebRequestEvent& evt) {
if (update_state_ == UpdateState::GetVersionInvisible) {
if (evt.GetState() == wxWebRequest::State_Completed) {
ProcessVersionFile(evt.GetResponse().AsString().utf8_string());
} else if (evt.GetState() == wxWebRequest::State_Failed) {
parent_->SetStatusText("Could not check for updates.");
}
}
}
void Updater::ProcessVersionFile(std::string data) {
try {
YAML::Node versionInfo = YAML::Load(data);
Version latestVersion(versionInfo["version"].as<std::string>());
if (kTrackerVersion < latestVersion) {
if (versionInfo["packages"]) {
std::string platformIdentifier;
if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) {
platformIdentifier = "win64";
}
if (!platformIdentifier.empty() &&
versionInfo["packages"][platformIdentifier]) {
wxMessageDialog dialog(
nullptr,
fmt::format("There is a newer version of Lingo AP Tracker "
"available. You have {}, and the latest version is "
"{}. Would you like to update?",
kTrackerVersion.ToString(), latestVersion.ToString()),
"Update available", wxYES_NO | wxCANCEL);
dialog.SetYesNoLabels("Install update", "Open changelog");
int dlgResult = dialog.ShowModal();
if (dlgResult == wxID_YES) {
const YAML::Node& packageInfo =
versionInfo["packages"][platformIdentifier];
std::string packageUrl = packageInfo["url"].as<std::string>();
std::string packageChecksum =
packageInfo["checksum"].as<std::string>();
std::vector<std::filesystem::path> packageFiles;
if (packageInfo["files"]) {
for (const YAML::Node& filename : packageInfo["files"]) {
packageFiles.push_back(filename.as<std::string>());
}
}
std::vector<std::filesystem::path> deletedFiles;
if (packageInfo["deleted_files"]) {
for (const YAML::Node& filename : packageInfo["deleted_files"]) {
deletedFiles.push_back(filename.as<std::string>());
}
}
InstallUpdate(packageUrl, packageChecksum, packageFiles,
deletedFiles);
} else if (dlgResult == wxID_NO) {
wxLaunchDefaultBrowser(kChangelogUrl);
}
return;
}
}
if (wxMessageBox(
fmt::format("There is a newer version of Lingo AP Tracker "
"available. You have {}, and the latest version is "
"{}. Would you like to update?",
kTrackerVersion.ToString(), latestVersion.ToString()),
"Update available", wxYES_NO) == wxYES) {
wxLaunchDefaultBrowser(kChangelogUrl);
}
} else if (update_state_ == UpdateState::GetVersionManual) {
wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK);
}
} catch (const std::exception& ex) {
wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR);
}
}
void Updater::InstallUpdate(std::string url, std::string checksum,
std::vector<std::filesystem::path> files,
std::vector<std::filesystem::path> deletedFiles) {
update_state_ = UpdateState::GetPackage;
wxWebRequest packageRequest =
wxWebSession::GetDefault().CreateRequest(this, url);
if (!DownloadWithProgress(packageRequest)) {
return;
}
package_path_.clear();
package_path_.resize(L_tmpnam + 1);
tmpnam_s(package_path_.data(), L_tmpnam);
{
wxFileOutputStream writeOut(package_path_);
wxString fileData = packageRequest.GetResponse().AsString();
writeOut.WriteAll(fileData.c_str(), fileData.length());
std::string downloadedChecksum = CalculateStringSha256(fileData);
if (downloadedChecksum != checksum) {
if (wxMessageBox("There was an issue downloading the update. Would you "
"like to manually download it instead?",
"Error", wxYES_NO | wxICON_ERROR) == wxID_YES) {
wxLaunchDefaultBrowser(kChangelogUrl);
}
return;
}
}
std::filesystem::path newArea = GetExecutableDirectory();
std::filesystem::path oldArea = newArea / "old";
std::set<std::filesystem::path> folders;
std::set<std::filesystem::path> filesToMove;
for (const std::filesystem::path& existingFile : files) {
std::filesystem::path movedPath = oldArea / existingFile;
std::filesystem::path movedDir = movedPath;
movedDir.remove_filename();
folders.insert(movedDir);
filesToMove.insert(existingFile);
}
for (const std::filesystem::path& existingFile : deletedFiles) {
std::filesystem::path movedPath = oldArea / existingFile;
std::filesystem::path movedDir = movedPath;
movedDir.remove_filename();
folders.insert(movedDir);
}
for (const std::filesystem::path& newFolder : folders) {
TrackerLog(fmt::format("Creating directory {}", newFolder.string()));
std::filesystem::create_directories(newFolder);
}
for (const std::filesystem::path& existingFile : files) {
std::filesystem::path existingPath = newArea / existingFile;
if (std::filesystem::is_regular_file(existingPath)) {
std::filesystem::path movedPath = oldArea / existingFile;
TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
movedPath.string()));
std::filesystem::rename(existingPath, movedPath);
}
}
for (const std::filesystem::path& existingFile : deletedFiles) {
std::filesystem::path existingPath = newArea / existingFile;
if (std::filesystem::is_regular_file(existingPath)) {
std::filesystem::path movedPath = oldArea / existingFile;
TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
movedPath.string()));
std::filesystem::rename(existingPath, movedPath);
}
}
wxFileInputStream fileInputStream(package_path_);
wxZipInputStream zipStream(fileInputStream);
std::unique_ptr<wxZipEntry> zipEntry;
while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) !=
nullptr) {
if (zipEntry->IsDir()) {
continue;
}
std::filesystem::path archivePath = zipEntry->GetName().utf8_string();
TrackerLog(fmt::format("Found {} in archive", archivePath.string()));
// Cut off the root folder name
std::filesystem::path subPath;
for (auto it = std::next(archivePath.begin()); it != archivePath.end();
it++) {
subPath /= *it;
}
std::filesystem::path pastePath = newArea / subPath;
wxFileOutputStream fileOutput(pastePath.string());
zipStream.Read(fileOutput);
}
if (wxMessageBox(
"Update installed! The tracker must be restarted for the changes to take "
"effect. Do you want to close the tracker?",
"Update installed", wxYES_NO) == wxYES) {
wxExit();
}
}
bool Updater::DownloadWithProgress(wxWebRequest& request) {
request.Start();
wxProgressDialog dialog("Checking for updates...", "Checking for updates...",
100, nullptr,
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT |
wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME);
while (request.GetState() != wxWebRequest::State_Completed &&
request.GetState() != wxWebRequest::State_Failed) {
if (request.GetBytesExpectedToReceive() == -1) {
if (!dialog.Pulse()) {
request.Cancel();
return false;
}
} else {
dialog.SetRange(request.GetBytesExpectedToReceive());
if (!dialog.Update(request.GetBytesReceived())) {
request.Cancel();
return false;
}
}
}
return true;
}