Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include <QDateTime>
24 : #include <QFileInfo>
25 : #include <QQmlEngine>
26 : #include <QQmlContext>
27 :
28 : #include "project/inotationproject.h"
29 :
30 : #include "util.h"
31 :
32 : #include "log.h"
33 :
34 : using namespace muse;
35 :
36 : namespace mu::engraving::apiv1 {
37 : //---------------------------------------------------------
38 : // FileIO
39 : //---------------------------------------------------------
40 :
41 0 : FileIO::FileIO(QObject* parent)
42 0 : : QObject(parent), muse::Contextable(muse::iocCtxForQmlObject(this))
43 : {
44 0 : }
45 :
46 4 : FileIO::FileIO(const muse::modularity::ContextPtr& ctx, QObject* parent)
47 4 : : QObject(parent), muse::Contextable(ctx)
48 : {
49 4 : }
50 :
51 : // User's MuseScore documents directory (default location for scores, plugins, etc.)
52 8 : QString FileIO::userDataPath()
53 : {
54 8 : return globalConfiguration() ? globalConfiguration()->userDataPath().toQString() : QString();
55 : }
56 :
57 : // User-configured Plugins directory (Preferences → Folders → Plugins)
58 8 : QString FileIO::pluginsUserPath()
59 : {
60 8 : return extensionsConfiguration() ? extensionsConfiguration()->pluginsUserPath().toQString() : QString();
61 : }
62 :
63 : // User-configured Scores directory (Preferences → Folders → Scores)
64 8 : QString FileIO::userProjectsPath()
65 : {
66 8 : return projectConfiguration() ? projectConfiguration()->userProjectsPath().toQString() : QString();
67 : }
68 :
69 : // User-configured Templates directory (Preferences → Folders → Templates)
70 8 : QString FileIO::userTemplatesPath()
71 : {
72 8 : return projectConfiguration() ? projectConfiguration()->userTemplatesPath().toQString() : QString();
73 : }
74 :
75 : // User-configured Styles directory (Preferences → Folders → Styles)
76 8 : QString FileIO::userStylesPath()
77 : {
78 8 : return notationConfiguration() ? notationConfiguration()->userStylesPath().toQString() : QString();
79 : }
80 :
81 : // User-configured SoundFonts directories (Preferences → Folders → SoundFonts)
82 8 : QStringList FileIO::userSoundFontDirectories()
83 : {
84 8 : QStringList paths;
85 8 : if (!audioConfiguration()) {
86 8 : return paths;
87 : }
88 :
89 0 : for (const auto& path : audioConfiguration()->userSoundFontDirectories()) {
90 0 : if (!path.empty()) {
91 0 : paths << path.toQString();
92 : }
93 0 : }
94 :
95 0 : return paths;
96 0 : }
97 :
98 : // The running plugin's directory
99 0 : QString FileIO::pluginDirectoryPath()
100 : {
101 0 : QQmlContext* context = QQmlEngine::contextForObject(this);
102 :
103 0 : if (!context) {
104 0 : return QString();
105 : }
106 :
107 0 : QUrl url = context->baseUrl();
108 :
109 0 : if (!url.isLocalFile()) {
110 0 : return QString();
111 : }
112 :
113 0 : return QFileInfo(url.toLocalFile()).absolutePath();
114 0 : }
115 :
116 : // Path of project file (ex: .../Desktop/project.mscz)
117 8 : QString FileIO::projectPath()
118 : {
119 8 : if (!projectSession()) {
120 8 : return QString();
121 : }
122 :
123 0 : project::INotationProjectPtr project = projectSession()->project();
124 0 : if (!project) {
125 0 : return QString();
126 : }
127 :
128 0 : muse::io::path_t projectPath = project->path();
129 0 : if (projectPath.empty()) {
130 0 : return QString();
131 : }
132 :
133 0 : return projectPath.toQString();
134 0 : }
135 :
136 : // Is the project a folder with a .mscx file
137 8 : bool FileIO::isProjectDirectory()
138 : {
139 8 : QString projectPath = FileIO::projectPath();
140 8 : if (projectPath.isEmpty()) {
141 8 : return false;
142 : }
143 :
144 0 : std::string suffix = muse::io::suffix(projectPath);
145 :
146 0 : return mscIoModeBySuffix(suffix) == MscIoMode::Dir;
147 8 : }
148 :
149 : // Path of project's containing folder
150 0 : QString FileIO::projectDirectoryPath()
151 : {
152 0 : QString projectPath = FileIO::projectPath();
153 0 : if (projectPath.isEmpty()) {
154 0 : return QString();
155 : }
156 :
157 0 : QString directoryPath = io::dirpath(projectPath).toQString();
158 0 : return directoryPath;
159 0 : }
160 :
161 : //---------------------------------------------------------
162 : // isPathWriteable
163 : // Check if the file path is within allowed directories
164 : //---------------------------------------------------------
165 8 : bool FileIO::isPathWriteable(const QString& filePath)
166 : {
167 : // Get the canonical (absolute, symlinks resolved) path
168 8 : QFileInfo fileInfo(filePath);
169 8 : QString canonicalPath = fileInfo.canonicalFilePath();
170 :
171 : // If file doesn't exist yet, we need to clean the path manually
172 : // because absoluteFilePath() does NOT resolve .. and . elements
173 8 : if (canonicalPath.isEmpty()) {
174 : // Use QDir::cleanPath to resolve .. and . elements
175 8 : canonicalPath = QDir::cleanPath(fileInfo.absoluteFilePath());
176 : }
177 :
178 : // Build list of allowed base paths from all user-configurable directories
179 8 : QStringList allowedPaths;
180 :
181 : // Note: userAppDataPath() is NOT included because it contains sensitive data:
182 : // - User credentials (musescorecom_cred.dat)
183 : // - System configuration (shortcuts.xml, midi_mappings.xml)
184 : // - Application logs
185 : // Plugins should not write to this directory
186 :
187 : // 1. System temp directory (for temporary files)
188 8 : QDir tempDir;
189 8 : allowedPaths << tempDir.tempPath();
190 :
191 : // 2. User's MuseScore documents directory (default location for Scores, Plugins, SoundFonts, Styles, Templates)
192 8 : if (QString path = FileIO::userDataPath(); !path.isEmpty()) {
193 8 : allowedPaths << path;
194 8 : }
195 :
196 : // 3. User-configured Plugins directory (Preferences → Folders → Plugins)
197 8 : if (QString path = FileIO::pluginsUserPath(); !path.isEmpty()) {
198 0 : allowedPaths << path;
199 8 : }
200 :
201 : // 4. User-configured Scores directory (Preferences → Folders → Scores)
202 8 : if (QString path = FileIO::userProjectsPath(); !path.isEmpty()) {
203 0 : allowedPaths << path;
204 8 : }
205 :
206 : // 5. User-configured Templates directory (Preferences → Folders → Templates)
207 8 : if (QString path = FileIO::userTemplatesPath(); !path.isEmpty()) {
208 0 : allowedPaths << path;
209 8 : }
210 :
211 : // 6. User-configured Styles directory (Preferences → Folders → Styles)
212 8 : if (QString path = FileIO::userStylesPath(); !path.isEmpty()) {
213 0 : allowedPaths << path;
214 8 : }
215 :
216 : // 7. User-configured SoundFonts directories (Preferences → Folders → SoundFonts)
217 8 : allowedPaths << FileIO::userSoundFontDirectories();
218 :
219 : // 8. Project path if it's a folder (with a .mscx file) instead of a .mscz file
220 8 : if (FileIO::isProjectDirectory()) {
221 0 : allowedPaths << FileIO::projectDirectoryPath();
222 : }
223 :
224 : // Check if the canonical path starts with any allowed base path
225 8 : bool allowed = false;
226 20 : for (const QString& basePath : allowedPaths) {
227 14 : if (canonicalPath.startsWith(basePath)) {
228 2 : allowed = true;
229 2 : break;
230 : }
231 : }
232 :
233 8 : if (!allowed) {
234 6 : LOGW() << "File write blocked: path '" << canonicalPath << "' is outside allowed directories";
235 6 : LOGW() << "Allowed directories: " << allowedPaths.join(", ");
236 : }
237 :
238 8 : return allowed;
239 8 : }
240 :
241 0 : QString FileIO::read()
242 : {
243 0 : if (m_source.isEmpty()) {
244 0 : emit error("source is empty");
245 0 : return QString();
246 : }
247 0 : QUrl url(m_source);
248 0 : QString source(m_source);
249 0 : if (url.isValid() && url.isLocalFile()) {
250 0 : source = url.toLocalFile();
251 : }
252 0 : QFile file(source);
253 0 : QString fileContent;
254 0 : if (file.open(QIODevice::ReadOnly)) {
255 0 : QString line;
256 0 : QTextStream t(&file);
257 : do {
258 0 : line = t.readLine();
259 0 : fileContent += line + "\n";
260 0 : } while (!line.isNull());
261 0 : file.close();
262 0 : } else {
263 0 : emit error("Unable to open the file");
264 0 : return QString();
265 : }
266 0 : return fileContent;
267 0 : }
268 :
269 8 : bool FileIO::write(const QString& data)
270 : {
271 8 : if (m_source.isEmpty()) {
272 0 : return false;
273 : }
274 :
275 8 : QUrl url(m_source);
276 :
277 8 : QString source = (url.isValid() && url.isLocalFile()) ? url.toLocalFile() : m_source;
278 :
279 : // Security: Check if path is within allowed directories
280 8 : if (!FileIO::isPathWriteable(source)) {
281 6 : emit error("File write blocked: path is outside allowed directories");
282 6 : return false;
283 : }
284 :
285 2 : QFile file(source);
286 2 : if (!file.open(QFile::WriteOnly | QFile::Truncate)) {
287 0 : return false;
288 : }
289 :
290 2 : QTextStream out(&file);
291 2 : out << data;
292 2 : file.close();
293 2 : return true;
294 8 : }
295 :
296 0 : bool FileIO::writeBinary(const QString& data)
297 : {
298 0 : if (m_source.isEmpty()) {
299 0 : return false;
300 : }
301 :
302 0 : QUrl url(m_source);
303 :
304 0 : QString source = (url.isValid() && url.isLocalFile()) ? url.toLocalFile() : m_source;
305 :
306 : // Security: Check if path is within allowed directories
307 0 : if (!FileIO::isPathWriteable(source)) {
308 0 : emit error("File write blocked: path is outside allowed directories");
309 0 : return false;
310 : }
311 :
312 0 : QFile file(source);
313 0 : if (!file.open(QFile::WriteOnly | QFile::Truncate)) {
314 0 : return false;
315 : }
316 :
317 : // Write binary data without text encoding
318 : // Each character in the QString represents a byte (0-255)
319 0 : QByteArray bytes = data.toLatin1();
320 0 : qint64 written = file.write(bytes);
321 0 : file.close();
322 :
323 0 : return written == bytes.size();
324 0 : }
325 :
326 : //---------------------------------------------------------
327 : // remove
328 : //---------------------------------------------------------
329 :
330 0 : bool FileIO::remove()
331 : {
332 0 : if (m_source.isEmpty()) {
333 0 : return false;
334 : }
335 :
336 0 : QFile file(m_source);
337 0 : return file.remove();
338 0 : }
339 :
340 0 : bool FileIO::exists()
341 : {
342 0 : QFile file(m_source);
343 0 : return file.exists();
344 0 : }
345 :
346 0 : int FileIO::modifiedTime()
347 : {
348 0 : if (m_source.isEmpty()) {
349 0 : emit error("source is empty");
350 0 : return 0;
351 : }
352 0 : QUrl url(m_source);
353 0 : QString source(m_source);
354 0 : if (url.isValid() && url.isLocalFile()) {
355 0 : source = url.toLocalFile();
356 : }
357 0 : QFileInfo fileInfo(source);
358 0 : return fileInfo.lastModified().toSecsSinceEpoch();
359 0 : }
360 :
361 0 : void MsProcess::start(const QString& command)
362 : {
363 : QT_WARNING_PUSH;
364 : QT_WARNING_DISABLE_DEPRECATED;
365 0 : DEPRECATED_USE("startWithArgs(program, [arg1, arg2, ...])");
366 0 : QProcess::start(command);
367 : QT_WARNING_POP;
368 0 : }
369 :
370 0 : void MsProcess::startWithArgs(const QString& program, const QStringList& args)
371 : {
372 0 : QProcess::start(program, args, ReadWrite);
373 0 : }
374 : }
|