3434
3535#include < ui_InstallDialog.h>
3636
37+ #include " Utilities.h"
38+
39+ #if defined(Q_OS_WIN32)
40+ #include < Windows.h>
41+ #endif
42+
43+ #if defined(Q_OS_WIN32)
44+ static BOOL GetFolderRights (LPCTSTR folderName, DWORD genericAccessRights, DWORD *grantedRights)
45+ {
46+ DWORD length = 0 ;
47+
48+ constexpr SECURITY_INFORMATION requestedInformation =
49+ OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION;
50+
51+ /* Get the needed length */
52+ GetFileSecurity (folderName, requestedInformation, nullptr , NULL , &length);
53+
54+ if (ERROR_INSUFFICIENT_BUFFER != GetLastError ())
55+ {
56+ return FALSE ;
57+ }
58+
59+ PSECURITY_DESCRIPTOR security = LocalAlloc (LPTR, length);
60+ if (GetFileSecurity (folderName, requestedInformation, security, length, &length) == FALSE )
61+ {
62+ LocalFree (security);
63+ return FALSE ;
64+ }
65+
66+ DWORD desiredAccess = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ;
67+ HANDLE hToken = nullptr ;
68+ if (OpenProcessToken (GetCurrentProcess (), desiredAccess, &hToken) == FALSE )
69+ {
70+ LocalFree (security);
71+ return FALSE ;
72+ }
73+
74+ HANDLE hImpersonatedToken = nullptr ;
75+ if (DuplicateToken (hToken, SecurityImpersonation, &hImpersonatedToken) == FALSE )
76+ {
77+ CloseHandle (hToken);
78+ LocalFree (security);
79+ return FALSE ;
80+ }
81+
82+ GENERIC_MAPPING mapping = {0xFFFFFFFF };
83+ PRIVILEGE_SET privileges = {0 };
84+ DWORD grantedAccess = 0 , privilegesLength = sizeof (privileges);
85+ BOOL result = FALSE ;
86+
87+ mapping.GenericRead = FILE_GENERIC_READ;
88+ mapping.GenericWrite = FILE_GENERIC_WRITE;
89+ mapping.GenericExecute = FILE_GENERIC_EXECUTE;
90+ mapping.GenericAll = FILE_ALL_ACCESS;
91+
92+ MapGenericMask (&genericAccessRights, &mapping);
93+ if (AccessCheck (security,
94+ hImpersonatedToken,
95+ genericAccessRights,
96+ &mapping,
97+ &privileges,
98+ &privilegesLength,
99+ &grantedAccess,
100+ &result) == FALSE )
101+ {
102+ CloseHandle (hImpersonatedToken);
103+ CloseHandle (hToken);
104+ LocalFree (security);
105+ return FALSE ;
106+ }
107+
108+ *grantedRights = grantedAccess;
109+
110+ CloseHandle (hImpersonatedToken);
111+ CloseHandle (hToken);
112+ LocalFree (security);
113+
114+ return TRUE ;
115+ }
116+
117+ static bool HasReadWriteAccessToFolder (const QString &folderPath)
118+ {
119+ constexpr DWORD access_mask = MAXIMUM_ALLOWED;
120+ DWORD grant = 0 ;
121+ #if defined(UNICODE)
122+ const std::wstring str = folderPath.toStdWString ();
123+ BOOL ret = GetFolderRights (str.c_str (), access_mask, &grant);
124+ #else
125+ const std::string str = folderPath.toStdString ();
126+ BOOL ret = GetFolderRights (str.c_str (), access_mask, &grant);
127+ #endif
128+
129+ if (ret == FALSE )
130+ {
131+ plgWarning () << " Failed to get access rights for path '" << folderPath << ' \n ' ;
132+ return false ;
133+ }
134+
135+ bool hasRead = false ;
136+ if (((grant & GENERIC_READ) == GENERIC_READ) ||
137+ ((grant & FILE_GENERIC_READ) == FILE_GENERIC_READ))
138+ {
139+ hasRead = true ;
140+ }
141+
142+ bool hasWrite = false ;
143+ if (((grant & GENERIC_WRITE) == GENERIC_WRITE) ||
144+ ((grant & FILE_GENERIC_WRITE) == FILE_GENERIC_WRITE))
145+ {
146+ hasWrite = true ;
147+ }
148+
149+ bool hasExecute = false ;
150+ if (((grant & GENERIC_EXECUTE) == GENERIC_EXECUTE) ||
151+ ((grant & FILE_GENERIC_EXECUTE) == FILE_GENERIC_EXECUTE))
152+ {
153+ hasExecute = true ;
154+ }
155+ return hasRead && hasWrite && hasExecute;
156+ }
157+ #endif // defined(Q_OS_WIN32)
158+
37159class InstallDialog : public QDialog
38160{
39161 Q_OBJECT
@@ -75,12 +197,12 @@ class CommandOutputDialog : public QDialog
75197 resize (600 , 300 );
76198 }
77199
78- void appendPlainText (const QString &text)
200+ void appendPlainText (const QString &text) const
79201 {
80202 m_display->appendPlainText (text);
81203 }
82204
83- void clear ()
205+ void clear () const
84206 {
85207 m_display->clear ();
86208 }
@@ -93,7 +215,8 @@ PackageManager::PackageManager(const PythonConfig &config, QWidget *parent)
93215 : QWidget(parent),
94216 m_ui(new Ui_PackageManager),
95217 m_pythonProcess(new QProcess),
96- m_outputDialog(new CommandOutputDialog(this ))
218+ m_outputDialog(new CommandOutputDialog(this )),
219+ m_shouldUseUserOption(false )
97220{
98221 m_ui->setupUi (this );
99222 connect (m_pythonProcess, &QProcess::started, [this ]() { setBusy (true ); });
@@ -120,6 +243,45 @@ PackageManager::PackageManager(const PythonConfig &config, QWidget *parent)
120243 connect (m_ui->searchBar , &QLineEdit::returnPressed, this , &PackageManager::handleSearch);
121244 config.preparePythonProcess (*m_pythonProcess);
122245 refreshInstalledPackagesList ();
246+
247+ m_ui->installBtn ->setEnabled (true );
248+ m_ui->messageFrame ->hide ();
249+
250+ switch (config.type ())
251+ {
252+ // The main intent of checking access rights of venv is for the Windows bundled
253+ // env, but checking for all venv won't hurt.
254+ // on Windowsn the bundled env is very likely to be installed in
255+ // "C:\Programs\Cloud Compare\plugins\Python" and that requires admin rights to add/modify.
256+ // It is better to notify user and prevent them from trying something that will faill.
257+ case PythonConfig::Type::Venv:
258+ case PythonConfig::Type::Conda:
259+ case PythonConfig::Type::Unknown:
260+ {
261+ #if defined Q_OS_WIN32
262+ // On Windows we use a custom function because isWritable from Qt was not correct
263+ // for our use case (its mentionned in their doc)
264+ const bool hasEnoughRights = HasReadWriteAccessToFolder (config.pythonHome ());
265+ #else
266+ const QFileInfo dirInfo (config.pythonHome ());
267+ const bool hasEnoughRights = dirInfo.isWritable ();
268+ #endif
269+ if (!hasEnoughRights)
270+ {
271+ m_ui->installBtn ->setEnabled (false );
272+ m_ui->messageIconLabel ->setPixmap (QApplication::style ()
273+ ->standardIcon (QStyle::SP_MessageBoxCritical)
274+ .pixmap ({64 , 64 }));
275+ m_ui->messageTextLabel ->setText (" Admin rights are required to be able to install "
276+ " packages in the current environment" );
277+ m_ui->messageFrame ->show ();
278+ }
279+ }
280+ break ;
281+ case PythonConfig::Type::System:
282+ m_shouldUseUserOption = true ;
283+ break ;
284+ }
123285}
124286
125287void PackageManager::refreshInstalledPackagesList ()
@@ -225,6 +387,10 @@ void PackageManager::handleInstallPackage()
225387 {
226388 arguments.push_back (" --upgrade" );
227389 }
390+ if (m_shouldUseUserOption)
391+ {
392+ arguments.push_back (" --user" );
393+ }
228394 executeCommand (arguments);
229395
230396 if (m_pythonProcess->exitCode () != 0 )
0 commit comments