2626#include < QMimeDatabase>
2727#include < QPainter>
2828#include < QPainterPath>
29+ #include < QStandardPaths>
2930#include < qgis.h>
3031#include < qgsexiftools.h>
3132#include < qgsfileutils.h>
33+ #include < qgsproject.h>
3234#include < qgsrendercontext.h>
3335#include < qgstextformat.h>
3436#include < qgstextrenderer.h>
3537
38+
3639FileUtils::FileUtils ( QObject *parent )
3740 : QObject( parent )
3841{
@@ -318,3 +321,233 @@ void FileUtils::addImageStamp( const QString &imagePath, const QString &text )
318321 }
319322 }
320323}
324+
325+ bool FileUtils::isWithinProjectDirectory ( const QString &filePath )
326+ {
327+ // Get the project instance
328+ QgsProject *project = QgsProject::instance ();
329+ if ( !project || project->fileName ().isEmpty () )
330+ return false ;
331+
332+ QFileInfo projectFileInfo ( project->fileName () );
333+ if ( !projectFileInfo.exists () )
334+ return false ;
335+
336+ // Get the canonical path for the project directory
337+ QString projectDirCanonical = QFileInfo ( projectFileInfo.dir ().absolutePath () ).canonicalFilePath ();
338+ if ( projectDirCanonical.isEmpty () )
339+ {
340+ // Fallback to absolutePath() if canonicalFilePath() is empty
341+ projectDirCanonical = QFileInfo ( projectFileInfo.dir ().absolutePath () ).absoluteFilePath ();
342+ if ( projectDirCanonical.isEmpty () )
343+ return false ;
344+ }
345+
346+ // Get target file info and its canonical path
347+ QFileInfo targetInfo ( filePath );
348+ QString targetCanonical;
349+
350+ if ( targetInfo.exists () || targetInfo.isSymLink () )
351+ {
352+ targetCanonical = targetInfo.canonicalFilePath ();
353+ if ( targetCanonical.isEmpty () )
354+ {
355+ targetCanonical = targetInfo.absoluteFilePath ();
356+ }
357+ }
358+ else
359+ {
360+ // Walk up the directory tree until we find an existing parent
361+ QDir dir = targetInfo.dir ();
362+ QStringList pendingSegments;
363+
364+ while ( !dir.exists () && dir.cdUp () )
365+ {
366+ pendingSegments.prepend ( QFileInfo ( dir.path () ).fileName () );
367+ }
368+
369+ QString existingCanonical = QFileInfo ( dir.path () ).canonicalFilePath ();
370+ if ( existingCanonical.isEmpty () )
371+ {
372+ existingCanonical = QFileInfo ( dir.path () ).absoluteFilePath ();
373+ if ( existingCanonical.isEmpty () )
374+ return false ;
375+ }
376+
377+ // Rebuild the target path from existing directories
378+ QDir rebuiltDir ( existingCanonical );
379+ for ( const QString &segment : pendingSegments )
380+ {
381+ rebuiltDir.cd ( segment );
382+ }
383+
384+ targetCanonical = rebuiltDir.absoluteFilePath ( targetInfo.fileName () );
385+ }
386+
387+ // Normalize paths for Windows (case-insensitive check)
388+ #ifdef Q_OS_WIN
389+ projectDirCanonical = projectDirCanonical.toLower ();
390+ targetCanonical = targetCanonical.toLower ();
391+ #endif
392+
393+ // Check if the target path is equal to or within the project directory
394+ return targetCanonical == projectDirCanonical || targetCanonical.startsWith ( projectDirCanonical + QDir::separator () );
395+ }
396+
397+ QByteArray FileUtils::readFileContent ( const QString &filePath )
398+ {
399+ QByteArray content;
400+
401+ if ( !isWithinProjectDirectory ( filePath ) )
402+ {
403+ qWarning () << QStringLiteral ( " Security warning: Attempted to read file outside project directory: %1" ).arg ( filePath );
404+ return QByteArray ();
405+ }
406+
407+ QFile file ( filePath );
408+
409+ if ( file.exists () )
410+ {
411+ if ( file.open ( QIODevice::ReadOnly ) )
412+ {
413+ content = file.readAll ();
414+ file.close ();
415+ }
416+ else
417+ {
418+ qDebug () << QStringLiteral ( " Failed to read file content: %1" ).arg ( file.errorString () );
419+ }
420+ }
421+ else
422+ {
423+ qDebug () << QStringLiteral ( " File does not exist: %1" ).arg ( filePath );
424+ }
425+
426+ return content;
427+ }
428+
429+ bool FileUtils::writeFileContent ( const QString &filePath, const QByteArray &content )
430+ {
431+ if ( !isWithinProjectDirectory ( filePath ) )
432+ {
433+ qWarning () << QStringLiteral ( " Security warning: Attempted to write file outside project directory: %1" ).arg ( filePath );
434+ return false ;
435+ }
436+
437+ QFile file ( filePath );
438+ QFileInfo fileInfo ( filePath );
439+ QDir directory = fileInfo.dir ();
440+
441+ bool isLikelyWritable = false ;
442+
443+ #ifdef Q_OS_ANDROID
444+ QString appStorage = QStandardPaths::writableLocation ( QStandardPaths::AppDataLocation );
445+ isLikelyWritable = filePath.startsWith ( appStorage );
446+ #elif defined( Q_OS_IOS )
447+ QString appStorage = QStandardPaths::writableLocation ( QStandardPaths::AppDataLocation );
448+ isLikelyWritable = filePath.startsWith ( appStorage );
449+ #else
450+ QFileInfo dirInfo ( directory.absolutePath () );
451+ isLikelyWritable = dirInfo.isWritable ();
452+ #endif
453+
454+ if ( !isLikelyWritable )
455+ {
456+ qWarning () << QStringLiteral ( " Writing to %1 may fail due to platform restrictions. Use PlatformUtilities.applicationDirectory() for a safe location." ).arg ( filePath );
457+ }
458+
459+ if ( !directory.exists () )
460+ {
461+ if ( !directory.mkpath ( " ." ) )
462+ {
463+ qDebug () << QStringLiteral ( " Failed to create directory for file: %1. This may be due to permission restrictions." ).arg ( filePath );
464+ return false ;
465+ }
466+ }
467+
468+ if ( file.open ( QIODevice::WriteOnly ) )
469+ {
470+ qint64 bytesWritten = file.write ( content );
471+ file.close ();
472+
473+ if ( bytesWritten != content.size () )
474+ {
475+ qDebug () << QStringLiteral ( " Failed to write all data to file: %1" ).arg ( filePath );
476+ return false ;
477+ }
478+
479+ return true ;
480+ }
481+ else
482+ {
483+ QString errorMsg = file.errorString ();
484+ if ( file.error () == QFile::PermissionsError )
485+ {
486+ errorMsg += QStringLiteral ( " - This may be due to platform security restrictions." );
487+ }
488+ qDebug () << QStringLiteral ( " Failed to open file for writing: %1" ).arg ( errorMsg );
489+ return false ;
490+ }
491+ }
492+
493+ QVariantMap FileUtils::getFileInfo ( const QString &filePath )
494+ {
495+ QVariantMap info;
496+
497+ if ( !isWithinProjectDirectory ( filePath ) )
498+ {
499+ qWarning () << QStringLiteral ( " Security warning: Attempted to access file info outside project directory: %1" ).arg ( filePath );
500+ info[" exists" ] = false ;
501+ info[" error" ] = QStringLiteral ( " Access denied: File is outside the current project directory" );
502+ return info;
503+ }
504+
505+ QFile file ( filePath );
506+ QFileInfo fileInfo ( filePath );
507+
508+ if ( fileInfo.exists () )
509+ {
510+ info[" exists" ] = true ;
511+ info[" fileName" ] = fileInfo.fileName ();
512+ info[" filePath" ] = fileInfo.absoluteFilePath ();
513+ info[" fileSize" ] = fileInfo.size ();
514+ info[" lastModified" ] = fileInfo.lastModified ();
515+ info[" suffix" ] = fileInfo.suffix ();
516+
517+ QMimeDatabase db;
518+ info[" mimeType" ] = db.mimeTypeForFile ( filePath ).name ();
519+
520+ QByteArray md5Hash = fileChecksum ( filePath, QCryptographicHash::Md5 );
521+ if ( !md5Hash.isEmpty () )
522+ {
523+ info[" md5" ] = md5Hash.toHex ();
524+ }
525+ else
526+ {
527+ info[" md5Error" ] = QStringLiteral ( " Could not calculate MD5 hash - possibly due to permission restrictions" );
528+ }
529+
530+ if ( file.open ( QIODevice::ReadOnly ) )
531+ {
532+ info[" content" ] = file.readAll ();
533+ info[" readable" ] = true ;
534+ file.close ();
535+ }
536+ else
537+ {
538+ info[" readable" ] = false ;
539+ info[" readError" ] = file.errorString ();
540+
541+ if ( file.error () == QFile::PermissionsError )
542+ {
543+ info[" readError" ] = info[" readError" ].toString () + QStringLiteral ( " - This may be due to platform security restrictions" );
544+ }
545+ }
546+ }
547+ else
548+ {
549+ info[" exists" ] = false ;
550+ }
551+
552+ return info;
553+ }
0 commit comments