1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
4 SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
5 SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "chmodjob.h"
11#include "../utils_p.h"
12
13#include <KLocalizedString>
14#include <KUser>
15#include <QDebug>
16
17#include "askuseractioninterface.h"
18#include "job_p.h"
19#include "jobuidelegatefactory.h"
20#include "kioglobal_p.h"
21#include "listjob.h"
22
23#include <stack>
24
25namespace KIO
26{
27struct ChmodInfo {
28 QUrl url;
29 int permissions;
30};
31
32enum ChmodJobState {
33 CHMODJOB_STATE_LISTING,
34 CHMODJOB_STATE_CHMODING,
35};
36
37class ChmodJobPrivate : public KIO::JobPrivate
38{
39public:
40 ChmodJobPrivate(const KFileItemList &lstItems, int permissions, int mask, KUserId newOwner, KGroupId newGroup, bool recursive)
41 : state(CHMODJOB_STATE_LISTING)
42 , m_permissions(permissions)
43 , m_mask(mask)
44 , m_newOwner(newOwner)
45 , m_newGroup(newGroup)
46 , m_recursive(recursive)
47 , m_bAutoSkipFiles(false)
48 , m_lstItems(lstItems)
49 {
50 }
51
52 ChmodJobState state;
53 int m_permissions;
54 int m_mask;
55 KUserId m_newOwner;
56 KGroupId m_newGroup;
57 bool m_recursive;
58 bool m_bAutoSkipFiles;
59 KFileItemList m_lstItems;
60 std::stack<ChmodInfo> m_infos;
61
62 void chmodNextFile();
63 void slotEntries(KIO::Job *, const KIO::UDSEntryList &);
64 void processList();
65
66 Q_DECLARE_PUBLIC(ChmodJob)
67
68 static inline ChmodJob *
69 newJob(const KFileItemList &lstItems, int permissions, int mask, KUserId newOwner, KGroupId newGroup, bool recursive, JobFlags flags)
70 {
71 ChmodJob *job = new ChmodJob(*new ChmodJobPrivate(lstItems, permissions, mask, newOwner, newGroup, recursive));
72 job->setUiDelegate(KIO::createDefaultJobUiDelegate());
73 if (!(flags & HideProgressInfo)) {
74 KIO::getJobTracker()->registerJob(job);
75 }
76 if (!(flags & NoPrivilegeExecution)) {
77 job->d_func()->m_privilegeExecutionEnabled = true;
78 job->d_func()->m_operationType = ChangeAttr;
79 }
80 return job;
81 }
82};
83
84} // namespace KIO
85
86using namespace KIO;
87
88ChmodJob::ChmodJob(ChmodJobPrivate &dd)
89 : KIO::Job(dd)
90{
91 Q_D(ChmodJob);
92 auto processFunc = [d]() {
93 d->processList();
94 };
95 QMetaObject::invokeMethod(object: this, function&: processFunc, type: Qt::QueuedConnection);
96}
97
98ChmodJob::~ChmodJob()
99{
100}
101
102void ChmodJobPrivate::processList()
103{
104 Q_Q(ChmodJob);
105 while (!m_lstItems.isEmpty()) {
106 const KFileItem item = m_lstItems.first();
107 if (!item.isLink()) { // don't do anything with symlinks
108 // File or directory -> remember to chmod
109 ChmodInfo info;
110 info.url = item.url();
111 // This is a toplevel file, we apply changes directly (no +X emulation here)
112 const mode_t permissions = item.permissions() & 0777; // get rid of "set gid" and other special flags
113 info.permissions = (m_permissions & m_mask) | (permissions & ~m_mask);
114 /*//qDebug() << "toplevel url:" << info.url << "\n current permissions=" << QString::number(permissions,8)
115 << "\n wanted permission=" << QString::number(m_permissions,8)
116 << "\n with mask=" << QString::number(m_mask,8)
117 << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~m_mask,8)
118 << "\n bits we keep =" << QString::number(permissions & ~m_mask,8)
119 << "\n new permissions = " << QString::number(info.permissions,8);*/
120 m_infos.push(x: std::move(info));
121 // qDebug() << "processList : Adding info for " << info.url;
122 // Directory and recursive -> list
123 if (item.isDir() && m_recursive) {
124 // qDebug() << "ChmodJob::processList dir -> listing";
125 KIO::ListJob *listJob = KIO::listRecursive(url: item.url(), flags: KIO::HideProgressInfo);
126 q->connect(sender: listJob, signal: &KIO::ListJob::entries, context: q, slot: [this](KIO::Job *job, const KIO::UDSEntryList &entries) {
127 slotEntries(job, entries);
128 });
129 q->addSubjob(job: listJob);
130 return; // we'll come back later, when this one's finished
131 }
132 }
133 m_lstItems.removeFirst();
134 }
135 // qDebug() << "ChmodJob::processList -> going to STATE_CHMODING";
136 // We have finished, move on
137 state = CHMODJOB_STATE_CHMODING;
138 chmodNextFile();
139}
140
141void ChmodJobPrivate::slotEntries(KIO::Job *, const KIO::UDSEntryList &list)
142{
143 KIO::UDSEntryList::ConstIterator it = list.begin();
144 KIO::UDSEntryList::ConstIterator end = list.end();
145 for (; it != end; ++it) {
146 const KIO::UDSEntry &entry = *it;
147 const bool isLink = !entry.stringValue(field: KIO::UDSEntry::UDS_LINK_DEST).isEmpty();
148 const QString relativePath = entry.stringValue(field: KIO::UDSEntry::UDS_NAME);
149 if (!isLink && relativePath != QLatin1String("..")) {
150 const mode_t permissions = entry.numberValue(field: KIO::UDSEntry::UDS_ACCESS) & 0777; // get rid of "set gid" and other special flags
151
152 ChmodInfo info;
153 info.url = m_lstItems.first().url(); // base directory
154 info.url.setPath(path: Utils::concatPaths(path1: info.url.path(), path2: relativePath));
155 int mask = m_mask;
156 // Emulate -X: only give +x to files that had a +x bit already
157 // So the check is the opposite : if the file had no x bit, don't touch x bits
158 // For dirs this doesn't apply
159 if (!entry.isDir()) {
160 int newPerms = m_permissions & mask;
161 if ((newPerms & 0111) && !(permissions & 0111)) {
162 // don't interfere with mandatory file locking
163 if (newPerms & 02000) {
164 mask = mask & ~0101;
165 } else {
166 mask = mask & ~0111;
167 }
168 }
169 }
170 info.permissions = (m_permissions & mask) | (permissions & ~mask);
171 /*//qDebug() << info.url << "\n current permissions=" << QString::number(permissions,8)
172 << "\n wanted permission=" << QString::number(m_permissions,8)
173 << "\n with mask=" << QString::number(mask,8)
174 << "\n with ~mask (mask bits we keep) =" << QString::number((uint)~mask,8)
175 << "\n bits we keep =" << QString::number(permissions & ~mask,8)
176 << "\n new permissions = " << QString::number(info.permissions,8);*/
177 // Push this info on top of the stack so it's handled first.
178 // This way, the toplevel dirs are done last.
179 m_infos.push(x: std::move(info));
180 }
181 }
182}
183
184void ChmodJobPrivate::chmodNextFile()
185{
186 auto processNextFunc = [this]() {
187 chmodNextFile();
188 };
189
190 Q_Q(ChmodJob);
191 if (!m_infos.empty()) {
192 ChmodInfo info = m_infos.top();
193 m_infos.pop();
194 // First update group / owner (if local file)
195 // (permissions have to be set after, in case of suid and sgid)
196 if (info.url.isLocalFile() && (m_newOwner.isValid() || m_newGroup.isValid())) {
197 QString path = info.url.toLocalFile();
198 if (!KIOPrivate::changeOwnership(file: path, newOwner: m_newOwner, newGroup: m_newGroup)) {
199 auto *askUserActionInterface = KIO::delegateExtension<AskUserActionInterface *>(job: q);
200 if (!askUserActionInterface) {
201 Q_EMIT q->warning(job: q, i18n("Could not modify the ownership of file %1", path));
202 } else if (!m_bAutoSkipFiles) {
203 SkipDialog_Options options;
204 if (m_infos.size() > 1) {
205 options |= SkipDialog_MultipleItems;
206 }
207
208 auto skipSignal = &AskUserActionInterface::askUserSkipResult;
209 q->connect(sender: askUserActionInterface, signal: skipSignal, context: q, slot: [=, this](KIO::SkipDialog_Result result, KJob *parentJob) {
210 Q_ASSERT(q == parentJob);
211 q->disconnect(sender: askUserActionInterface, signal: skipSignal, receiver: q, zero: nullptr);
212
213 switch (result) {
214 case Result_AutoSkip:
215 m_bAutoSkipFiles = true;
216 // fall through
217 Q_FALLTHROUGH();
218 case Result_Skip:
219 QMetaObject::invokeMethod(object: q, function: processNextFunc, type: Qt::QueuedConnection);
220 return;
221 case Result_Retry:
222 m_infos.push(x: std::move(info));
223 QMetaObject::invokeMethod(object: q, function: processNextFunc, type: Qt::QueuedConnection);
224 return;
225 case Result_Cancel:
226 default:
227 q->setError(ERR_USER_CANCELED);
228 q->emitResult();
229 return;
230 }
231 });
232
233 askUserActionInterface->askUserSkip(job: q,
234 options,
235 xi18n("Could not modify the ownership of file <filename>%1</filename>. You have "
236 "insufficient access to the file to perform the change.",
237 path));
238 return;
239 }
240 }
241 }
242
243 /*qDebug() << "chmod'ing" << info.url << "to" << QString::number(info.permissions,8);*/
244 KIO::SimpleJob *job = KIO::chmod(url: info.url, permissions: info.permissions);
245 job->setParentJob(q);
246 // copy the metadata for acl and default acl
247 const QString aclString = q->queryMetaData(QStringLiteral("ACL_STRING"));
248 const QString defaultAclString = q->queryMetaData(QStringLiteral("DEFAULT_ACL_STRING"));
249 if (!aclString.isEmpty()) {
250 job->addMetaData(QStringLiteral("ACL_STRING"), value: aclString);
251 }
252 if (!defaultAclString.isEmpty()) {
253 job->addMetaData(QStringLiteral("DEFAULT_ACL_STRING"), value: defaultAclString);
254 }
255 q->addSubjob(job);
256 } else { // We have finished
257 q->emitResult();
258 }
259}
260
261void ChmodJob::slotResult(KJob *job)
262{
263 Q_D(ChmodJob);
264 removeSubjob(job);
265 if (job->error()) {
266 setError(job->error());
267 setErrorText(job->errorText());
268 emitResult();
269 return;
270 }
271 // qDebug() << "d->m_lstItems:" << d->m_lstItems.count();
272 switch (d->state) {
273 case CHMODJOB_STATE_LISTING:
274 d->m_lstItems.removeFirst();
275 // qDebug() << "-> processList";
276 d->processList();
277 return;
278 case CHMODJOB_STATE_CHMODING:
279 // qDebug() << "-> chmodNextFile";
280 d->chmodNextFile();
281 return;
282 default:
283 Q_ASSERT(false);
284 return;
285 }
286}
287
288ChmodJob *KIO::chmod(const KFileItemList &lstItems, int permissions, int mask, const QString &owner, const QString &group, bool recursive, JobFlags flags)
289{
290 KUserId uid = KUserId::fromName(name: owner);
291 KGroupId gid = KGroupId::fromName(name: group);
292 return ChmodJobPrivate::newJob(lstItems, permissions, mask, newOwner: uid, newGroup: gid, recursive, flags);
293}
294
295#include "moc_chmodjob.cpp"
296

source code of kio/src/core/chmodjob.cpp