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 | |
25 | namespace KIO |
26 | { |
27 | struct ChmodInfo { |
28 | QUrl url; |
29 | int permissions; |
30 | }; |
31 | |
32 | enum ChmodJobState { |
33 | CHMODJOB_STATE_LISTING, |
34 | CHMODJOB_STATE_CHMODING, |
35 | }; |
36 | |
37 | class ChmodJobPrivate : public KIO::JobPrivate |
38 | { |
39 | public: |
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 | |
86 | using namespace KIO; |
87 | |
88 | ChmodJob::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 | |
98 | ChmodJob::~ChmodJob() |
99 | { |
100 | } |
101 | |
102 | void 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 | |
141 | void 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 | |
184 | void 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 | |
261 | void 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 | |
288 | ChmodJob *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 | |