opennx/MacUninstallApp.cpp
2025-08-08 20:34:09 +02:00

688 lines
24 KiB
C++

/////////////////////////////////////////////////////////////////////////////
// Name: macuninstallapp.cpp
// Purpose:
// Author: Fritz Elfert
// Modified by:
// Created: Fri 11 Sep 2009 01:17:42 PM CEST
// RCS-ID:
// Copyright: (C) 2009 by Fritz Elfert
// Licence:
/////////////////////////////////////////////////////////////////////////////
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
// For compilers that support precompilation, includes "wx/wx.h".
#include "wx/wxprec.h"
#ifdef __BORLANDC__
#pragma hdrstop
#endif
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif
#include <wx/stdpaths.h>
#include <wx/apptrait.h>
#include <wx/filename.h>
#include <wx/cmdline.h>
#include <wx/xml/xml.h>
#include <wx/arrstr.h>
#include <wx/dir.h>
#include <wx/mstream.h>
#include <wx/wfstream.h>
#include <Security/Authorization.h>
#include <Security/AuthorizationTags.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fstream>
#include "macuninstallapp.h"
static unsigned long failed_files;
static unsigned long failed_dirs;
class RmRfTraverser : public wxDirTraverser
{
public:
RmRfTraverser(bool testMode)
: m_bTestMode(testMode) { }
~RmRfTraverser()
{
int n = m_aFiles.GetCount() - 1;
wxString fn;
while (n >= 0) {
fn = m_aFiles[n--];
if (m_bTestMode) {
wxLogMessage(_("TEST: Would delete file: %s"), fn.c_str());
} else {
if (::wxRemoveFile(fn))
wxLogMessage(_("Deleted file: %s"), fn.c_str());
else {
failed_files++;
wxLogWarning(_("Could not delete file %s"), fn.c_str());
}
}
}
n = m_aDirs.GetCount() - 1;
while (n >= 0) {
fn = m_aDirs[n--];
if (m_bTestMode) {
wxLogMessage(_("TEST: Would delete diretory %s"), fn.c_str());
} else {
if (::wxRmdir(fn))
wxLogMessage(_("Deleted diretory %s"), fn.c_str());
else {
failed_dirs++;
wxLogWarning(_("Could not delete directory %s"), fn.c_str());
}
}
}
}
virtual wxDirTraverseResult OnFile(const wxString& filename)
{
m_aFiles.Add(filename);
return wxDIR_CONTINUE;
}
virtual wxDirTraverseResult OnDir(const wxString& dirpath)
{
m_aDirs.Add(dirpath);
return wxDIR_CONTINUE;
}
private:
bool m_bTestMode;
wxArrayString m_aDirs;
wxArrayString m_aFiles;
};
IMPLEMENT_APP(MacUninstallApp)
IMPLEMENT_CLASS(MacUninstallApp, wxApp)
BEGIN_EVENT_TABLE(MacUninstallApp, wxApp)
END_EVENT_TABLE()
MacUninstallApp::MacUninstallApp()
{
failed_files = failed_dirs = 0;
m_bBatchMode = false;
m_bTestMode = false;
m_bCancelled = false;
m_sSelfPath = wxFileName(
GetTraits()->GetStandardPaths().GetExecutablePath()).GetFullPath();
m_nodelete.insert(wxT("."));
m_nodelete.insert(wxT("./Applications"));
m_nodelete.insert(wxT("./Library"));
m_nodelete.insert(wxT("./usr/share/man/man1"));
m_nodelete.insert(wxT("./usr/share/man"));
m_nodelete.insert(wxT("./usr/share"));
m_nodelete.insert(wxT("./usr"));
}
void MacUninstallApp::OnInitCmdLine(wxCmdLineParser& parser)
{
// Init standard options (--help, --verbose);
wxApp::OnInitCmdLine(parser);
parser.AddSwitch(wxEmptyString, wxT("test"),
_("Test mode. Just create log file reporting what would happen."));
parser.AddSwitch(wxEmptyString, wxT("batch"),
_("Uninstall without asking the user (needs admin rights)."));
}
bool MacUninstallApp::OnCmdLineParsed(wxCmdLineParser& parser)
{
m_bBatchMode = parser.Found(wxT("batch"));
m_bTestMode = parser.Found(wxT("test"));
if (m_bTestMode)
m_bBatchMode = true;
return true;
}
/*
* Initialisation for MacUninstallApp
*/
bool MacUninstallApp::OnInit()
{
wxFileName fn(m_sSelfPath);
fn.RemoveLastDir();
fn.AppendDir(wxT("share"));
fn.AppendDir(wxT("locale"));
m_cLocale.AddCatalogLookupPathPrefix(fn.GetPath());
m_cLocale.Init();
m_cLocale.AddCatalog(wxT("opennx"));
wxString targetPkg = wxT("OpenNX");
wxString targetPkgId = wxT("org.opennx.OpenNX");
// Call to base class needed for initializing command line processing
if (!wxApp::OnInit())
return false;
wxInitAllImageHandlers();
wxBitmap::InitStandardHandlers();
m_sLogName << wxT("/tmp/uninstall-") << targetPkgId << wxT(".log");
if (m_bBatchMode) {
if ((!m_bTestMode) && (0 != geteuid())) {
::wxMessageBox(_("Batch uninstall needs to be started as root."),
wxString::Format(_("Uninstall %s"), targetPkg.c_str()),
wxOK|wxICON_ERROR);
while (Pending())
Dispatch();
return false;
}
bool ok = DoUninstall(targetPkg, targetPkgId);
wxLogMessage(_("Uninstall finished at %s"), wxDateTime::Now().Format().c_str());
wxLogMessage(_("Status: %s, failed files: %lu, failed dirs: %lu"),
(ok ? _("OK") : _("FAILED")), failed_files, failed_dirs);
// Print result to stdout for parent (elevation wrapper)
wxString ptmp;
if (::wxGetEnv(wxT("MACUNINST_ELEVATION_PID"), &ptmp)) {
long epid;
if (ptmp.ToLong(&epid)) {
if (getppid() == epid) {
wxLogMessage(_("Reporting result to elevation wrapper %s"), ptmp.c_str());
printf("%d %lu %lu\n", ok ? 0 : 1, failed_files, failed_dirs);
}
}
}
} else {
int r = ::wxMessageBox(
wxString::Format(
_("This operation can not be undone!\nDo you really want to uninstall %s?"),
targetPkg.c_str()),
wxString::Format(_("Uninstall %s"), targetPkg.c_str()),
wxYES_NO|wxNO_DEFAULT|wxICON_EXCLAMATION);
if (wxYES == r) {
if (!TestReceipt(targetPkg, targetPkgId)) {
while (Pending())
Dispatch();
return false;
}
if (ElevatedUninstall(targetPkg, targetPkgId)) {
if (!m_bCancelled) {
if (0 == (failed_files + failed_dirs)) {
::wxMessageBox(
wxString::Format(_("%s has been removed successfully."),
targetPkg.c_str()),
_("Uninstallation complete"), wxOK|wxICON_INFORMATION);
} else {
::wxMessageBox(
wxString::Format(
_("%s could not be removed completely.\nSome files or directories could not be deleted.\nPlease investigate the log file\n%s\n for more information."),
targetPkg.c_str(), m_sLogName.c_str()),
_("Uninstallation incomplete"), wxOK|wxICON_EXCLAMATION);
}
}
} else
::wxMessageBox(
wxString::Format(
_("Uninstallation has failed.\nThe reason should have been logged in the file\n%s"),
m_sLogName.c_str()),
_("Uninstallation failed"), wxOK|wxICON_ERROR);
}
}
while (Pending())
Dispatch();
return false;
}
int MacUninstallApp::OnExit()
{
return wxApp::OnExit();
}
wxVariant MacUninstallApp::GetPlistValue(
const wxXmlDocument &doc, const wxString &docname, const wxString &keyname)
{
wxVariant ret;
ret.Clear();
if (doc.GetRoot()->GetName() != wxT("plist")) {
wxLogError(_("Not an XML plist: %s"), docname.c_str());
return ret;
}
wxXmlNode *child = doc.GetRoot()->GetChildren();
if (child->GetName() != wxT("dict")) {
wxLogError(
_("Invalid plist (missing toplevel <dict> in %s"),
docname.c_str());
return ret;
}
child = child->GetChildren();
bool needkey = true;
bool found = false;
while (child) {
if (needkey) {
if (child->GetName() != wxT("key")) {
wxLogError(
_("Invalid plist (expected a key) in %s"),
docname.c_str());
return ret;
}
if (child->GetNodeContent().IsSameAs(keyname))
found = true;
} else {
if (found) {
if (child->GetName().IsSameAs(wxT("array"))) {
wxLogError(
_("Unsupported type array in %s"), docname.c_str());
return ret;
}
if (child->GetName().IsSameAs(wxT("real"))) {
double val;
if (!child->GetNodeContent().ToDouble(&val)) {
wxLogError(
_("Invalid plist (invalid real value %s) in %s"),
child->GetNodeContent().c_str(), docname.c_str());
} else
ret = val;
return ret;
}
if (child->GetName().IsSameAs(wxT("integer"))) {
long val;
if (!child->GetNodeContent().ToLong(&val)) {
wxLogError(
_("Invalid plist (invalid integer value %s) in %s"),
child->GetNodeContent().c_str(), docname.c_str());
} else
ret = val;
return ret;
}
if (child->GetName().IsSameAs(wxT("string"))) {
ret = child->GetNodeContent();
return ret;
}
if (child->GetName().IsSameAs(wxT("true")) ||
child->GetName().IsSameAs(wxT("false"))) {
ret = child->GetName().IsSameAs(wxT("true"));
return ret;
}
if (child->GetName().IsSameAs(wxT("date"))) {
wxDateTime dt;
if (NULL == dt.ParseDateTime(child->GetNodeContent())) {
wxLogError(
_("Invalid plist (invalid date value %s) in %s"),
child->GetNodeContent().c_str(), docname.c_str());
} else
ret = dt;
return ret;
}
wxLogError(
_("Invalid plist (unexpected type %s) in %s"),
child->GetName().c_str(), docname.c_str());
return ret;
}
}
needkey = (!needkey);
child = child->GetNext();
}
return ret;
}
wxString MacUninstallApp::GetInstalledPath(const wxString &rcpt)
{
wxXmlDocument doc;
// Test for binary plist and convert it to xml if necessary.
wxFileInputStream fis(rcpt);
if (fis.IsOk()) {
char buf[8];
memset(buf, 0, 8);
fis.Read(&buf, 7);
if (0 == memcmp(buf, "bplist0", 7)) {
wxString cmd(wxT("plutil -convert xml1 -o - "));
cmd << rcpt;
wxArrayString lines;
if (0 == ::wxExecute(cmd, lines)) {
size_t i;
wxString fbuf;
for (i = 0; i < lines.GetCount(); ++i) {
// wxLogMessage(_("RLINE: %s"), lines[i].c_str());
fbuf.Append(lines[i]).Append(wxT("\n"));
}
wxMemoryInputStream mis(fbuf.mb_str(), fbuf.Length());
doc.Load(mis);
}
} else {
doc.Load(rcpt);
}
}
if (doc.IsOk()) {
wxVariant v = GetPlistValue(doc, rcpt, wxT("InstallPrefixPath"));
if (v.IsNull()) {
// Old variant
v = GetPlistValue(doc, rcpt, wxT("IFPkgFlagRelocatable"));
if (v.IsNull()) {
wxLogError(_("Could not find requested key 'IFPkgFlagRelocatable' in receipt %s"),
rcpt.c_str());
return wxEmptyString;
}
wxString t = v.GetType();
if (t != wxT("bool")) {
wxLogError(_("Unexpected type '%s' of 'IFPkgFlagRelocatable' in receipt %s"),
t.c_str(), rcpt.c_str());
return wxEmptyString;
}
wxString pkey = (v.GetBool() ?
wxT("IFPkgRelocatedPath") : wxT("IFPkgFlagDefaultLocation"));
v = GetPlistValue(doc, rcpt, pkey);
if (v.IsNull()) {
wxLogError(_("Could not find requested key '%s' in receipt %s"),
pkey.c_str(), rcpt.c_str());
return wxEmptyString;
}
t = v.GetType();
if (t != wxT("string")) {
wxLogError(_("Unexpected type '%s' of '%s' in receipt %s"),
t.c_str(), pkey.c_str(), rcpt.c_str());
return wxEmptyString;
}
return v.GetString();
} else {
// New variant
return v.GetString().Append(wxT("/"));
}
} else
wxLogError(_("Could not read package receipt %s"), rcpt.c_str());
return wxEmptyString;
}
bool MacUninstallApp::FetchBOM(const wxString &bom,
wxArrayString &dirs, wxArrayString &files)
{
if (!wxFileName::FileExists(bom)) {
wxLogError(
_("Missing BOM (Bill Of Materials) '%s'. Already unistalled?"), bom.c_str());
return false;
}
wxString cmd(wxT("lsbom -fbcl -p f "));
cmd << bom;
wxArrayString err;
if (0 != ::wxExecute(cmd, files, err)) {
wxLogError(
_("Could not list BOM (Bill Of Materials) '%s'. Already unistalled?"), bom.c_str());
return false;
}
if (0 != err.GetCount() != 0) {
wxLogError(
_("Invalid BOM (Bill Of Materials) '%s'. Already unistalled?"), bom.c_str());
return false;
}
cmd = wxT("lsbom -d -p f ");
cmd << bom;
err.Empty();
if (0 != ::wxExecute(cmd, dirs, err)) {
wxLogError(
_("Could not list BOM (Bill Of Materials) '%s'. Already unistalled?"), bom.c_str());
return false;
}
if (0 != err.GetCount() != 0) {
wxLogError(
_("Invalid BOM (Bill Of Materials) '%s'. Already unistalled?"), bom.c_str());
return false;
}
return true;
}
bool MacUninstallApp::TestReceipt(const wxString &pkg, const wxString &pkgid)
{
wxString rpath = wxT("/Library/Receipts/");
wxString proot;
wxString bompath;
rpath.Append(pkg).Append(wxT(".pkg"));
if (wxFileName::DirExists(rpath)) {
proot = GetInstalledPath(rpath + wxT("/Contents/Info.plist"));
bompath = rpath + wxT("/Contents/Archive.bom");
} else {
rpath = wxT("/var/db/receipts/");
bompath = rpath;
rpath.Append(pkgid).Append(wxT(".plist"));
if (wxFileName::FileExists(rpath)) {
proot = GetInstalledPath(rpath);
bompath.Append(pkgid).Append(wxT(".bom"));
} else {
wxLogWarning(
_("The package receipt does not exist. Already unistalled?"));
return false;
}
}
if (proot.IsEmpty())
return false;
if (!wxFileName::DirExists(proot)) {
wxLogWarning(
_("The package install path does not exist. Already unistalled?"));
return false;
}
wxArrayString d;
wxArrayString f;
if (!FetchBOM(bompath, d, f))
return false;
return true;
}
bool MacUninstallApp::DoUninstall(const wxString &pkg, const wxString &pkgid)
{
std::ofstream *log = new std::ofstream();
log->open(m_sLogName.mb_str());
delete wxLog::SetActiveTarget(new wxLogStream(log));
wxLogMessage(_("Uninstall started at %s"), wxDateTime::Now().Format().c_str());
wxString rpath = wxT("/Library/Receipts/");
wxString proot;
wxString bompath;
bool oldreceipt = true;
rpath.Append(pkg).Append(wxT(".pkg"));
if (wxFileName::DirExists(rpath)) {
proot = GetInstalledPath(rpath + wxT("/Contents/Info.plist"));
bompath = rpath + wxT("/Contents/Archive.bom");
} else {
rpath = wxT("/var/db/receipts/");
bompath = rpath;
rpath.Append(pkgid).Append(wxT(".plist"));
proot = GetInstalledPath(rpath);
bompath.Append(pkgid).Append(wxT(".bom"));
oldreceipt = false;
}
if (proot.IsEmpty())
return false;
wxArrayString d;
wxArrayString f;
if (!FetchBOM(bompath, d, f))
return false;
size_t i;
wxLogMessage(_("Deleting package content"));
for (i = 0; i < f.GetCount(); i++) {
if (m_nodelete.find(f[i]) != m_nodelete.end()) {
f.RemoveAt(i--);
continue;
}
wxFileName fn(proot + f[i]);
if (fn.Normalize(wxPATH_NORM_DOTS|wxPATH_NORM_ABSOLUTE)) {
wxString name = fn.GetFullPath();
if (m_bTestMode) {
f.RemoveAt(i--);
wxLogMessage(_("TEST: Would delete file: %s"), name.c_str());
} else {
if (::wxRemoveFile(name) || (!fn.FileExists())) {
f.RemoveAt(i--);
wxLogMessage(_("Deleted file: %s"), name.c_str());
} else {
failed_files++;
wxLogWarning(_("Could not delete file %s"), name.c_str());
}
}
}
}
size_t lcd;
do {
lcd = d.GetCount();
for (i = 0; i < d.GetCount(); i++) {
if (m_nodelete.find(d[i]) != m_nodelete.end()) {
d.RemoveAt(i--);
continue;
}
wxFileName fn(proot + d[i]);
if (fn.Normalize(wxPATH_NORM_DOTS|wxPATH_NORM_ABSOLUTE)) {
wxString name = fn.GetFullPath();
if (m_bTestMode) {
d.RemoveAt(i--);
wxLogMessage(_("TEST: Would delete directory: %s"), name.c_str());
} else {
if (::wxRmdir(name) || (!fn.DirExists())) {
d.RemoveAt(i--);
wxLogMessage(_("Deleted directory: %s"), name.c_str());
}
}
}
}
} while (lcd != d.GetCount());
if (0 < d.GetCount()) {
for (i = 0; i < d.GetCount(); i++) {
failed_dirs++;
wxLogWarning(_("Could not delete directory %s"), d[i].c_str());
}
}
if (0 == (d.GetCount() + f.GetCount())) {
// Finally delete the receipe itself
wxLogMessage(_("Deleting receipt"));
if (oldreceipt) {
{
wxDir d(rpath);
RmRfTraverser t(m_bTestMode);
d.Traverse(t);
}
if (m_bTestMode) {
wxLogMessage(_("TEST: Would delete directory: %s"), rpath.c_str());
} else {
if (::wxRmdir(rpath))
wxLogMessage(_("Deleted directory: %s"), rpath.c_str());
else {
failed_dirs++;
wxLogWarning(_("Could not delete directory %s"), rpath.c_str());
}
}
} else {
if (m_bTestMode) {
wxLogMessage(_("TEST: Would delete receipt: %s"), rpath.c_str());
wxLogMessage(_("TEST: Would delete BOM: %s"), bompath.c_str());
} else {
if (::wxRemoveFile(rpath)) {
wxLogMessage(_("Deleted receipt: %s"), rpath.c_str());
} else {
wxLogWarning(_("Could not delete receipt %s"), rpath.c_str());
}
if (::wxRemoveFile(bompath)) {
wxLogMessage(_("Deleted BOM: %s"), bompath.c_str());
} else {
wxLogWarning(_("Could not delete BOM %s"), bompath.c_str());
}
}
}
} else
wxLogMessage(_("Receipt NOT deleted, because package files have been left."));
return true;
}
/**
* Execute with administrative rights.
*/
bool MacUninstallApp::ElevatedUninstall(const wxString &pkg, const wxString &pkgid)
{
// If we are already root, do the uninstall directly
if (geteuid() == 0)
return DoUninstall(pkg, pkgid);
wxString msg = wxString::Format(
_("In order to uninstall %s, administrative rights are required.\n\n"),
pkg.c_str());
char *prompt = strdup(msg.utf8_str());
OSStatus st;
AuthorizationFlags aFlags = kAuthorizationFlagDefaults;
AuthorizationRef aRef;
AuthorizationItem promptItem = {
kAuthorizationEnvironmentPrompt, strlen(prompt), prompt, 0
};
AuthorizationEnvironment aEnv = { 1, &promptItem };
st = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment,
kAuthorizationFlagDefaults, &aRef);
if (errAuthorizationSuccess != st) {
wxLogError(_("Authorization could not be created: %s"), MacAuthError(st).c_str());
return true;
}
AuthorizationItem aItems = { kAuthorizationRightExecute, 0, NULL, 0 };
AuthorizationRights aRights = { 1, &aItems };
aFlags = kAuthorizationFlagDefaults |
kAuthorizationFlagInteractionAllowed |
kAuthorizationFlagPreAuthorize |
kAuthorizationFlagExtendRights;
st = AuthorizationCopyRights(aRef, &aRights, &aEnv, aFlags, NULL );
bool ret = true;
if (errAuthorizationSuccess == st) {
char *executable = strdup(m_sSelfPath.utf8_str());
char *args[] = { "--batch", NULL };
FILE *pout = NULL;
if (!::wxGetEnv(wxT("TMPDIR"), NULL))
::wxSetEnv(wxT("TMPDIR"), wxFileName::GetTempDir());
::wxSetEnv(wxT("MACUNINST_ELEVATION_PID"), wxString::Format(wxT("%d"), ::wxGetProcessId()));
st = AuthorizationExecuteWithPrivileges(aRef,
executable, kAuthorizationFlagDefaults, args, &pout);
if (errAuthorizationSuccess == st) {
int status;
fscanf(pout, "%d %lu %lu", &status, &failed_files, &failed_dirs);
ret = (0 == status);
} else
wxLogError(_("Could not execute with administrative rights:\n%s"), MacAuthError(st).c_str());
} else {
if (st) {
m_bCancelled = (errAuthorizationCanceled == st);
if (!m_bCancelled)
wxLogError(_("Authorization failed: %s"), MacAuthError(st).c_str());
}
}
AuthorizationFree(aRef, kAuthorizationFlagDefaults);
return ret;
}
wxString MacUninstallApp::MacAuthError(long code)
{
wxString ret;
switch (code) {
case errAuthorizationSuccess:
return wxT("The operation completed successfully.");
case errAuthorizationInvalidSet:
return wxT("The set parameter is invalid.");
case errAuthorizationInvalidRef:
return wxT("The authorization parameter is invalid.");
case errAuthorizationInvalidTag:
return wxT("The tag parameter is invalid.");
case errAuthorizationInvalidPointer:
return wxT("The authorizedRights parameter is invalid.");
case errAuthorizationDenied:
return wxT("The Security Server denied authorization for one or more requested rights.");
case errAuthorizationCanceled:
return wxT("The user canceled the operation.");
case errAuthorizationInteractionNotAllowed:
return wxT("The Security Server denied authorization because no user interaction is allowed.");
case errAuthorizationInternal:
return wxT("An unrecognized internal error occurred.");
case errAuthorizationExternalizeNotAllowed:
return wxT("The Security Server denied externalization of the authorization reference.");
case errAuthorizationInternalizeNotAllowed:
return wxT("The Security Server denied internalization of the authorization reference.");
case errAuthorizationInvalidFlags:
return wxT("The flags parameter is invalid.");
case errAuthorizationToolExecuteFailure:
return wxT("The tool failed to execute.");
case errAuthorizationToolEnvironmentError:
return wxT("The attempt to execute the tool failed to return a success or an error code.");
}
return ret;
}