Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve robustness of our handling of file path to source URLs #5528

Merged
merged 4 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- name: 🔨 Prepare build env
run: |
sudo apt-get update
sudo apt-get install -y gperf autopoint '^libxcb.*-dev' libx11-xcb-dev libegl1-mesa libegl1-mesa-dev libgl1-mesa-dev libglu1-mesa-dev mesa-common-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libxrandr-dev libxxf86vm-dev autoconf-archive libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libfuse2 libpulse-dev libcups2-dev nasm
sudo apt-get install -y gperf autopoint '^libxcb.*-dev' libx11-xcb-dev libegl1-mesa libegl1-mesa-dev libgl1-mesa-dev libglu1-mesa-dev mesa-common-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libxrandr-dev libxxf86vm-dev autoconf-archive libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libfuse2 libpulse-dev libcups2-dev nasm python3-tk
# Required to run unit tests on linux
- name: Install linuxdeploy
Expand Down
3 changes: 2 additions & 1 deletion src/core/platforms/platformutilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "qgsmessagelog.h"
#include "resourcesource.h"
#include "stringutils.h"
#include "urlutils.h"

#include <QApplication>
#include <QClipboard>
Expand Down Expand Up @@ -324,7 +325,7 @@ ResourceSource *PlatformUtilities::getFile( const QString &prefix, const QString

ViewStatus *PlatformUtilities::open( const QString &uri, bool, QObject * )
{
QDesktopServices::openUrl( QStringLiteral( "file://%1" ).arg( uri ) );
QDesktopServices::openUrl( UrlUtils::fromString( uri ) );
return nullptr;
}

Expand Down
14 changes: 12 additions & 2 deletions src/core/utils/urlutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

#include "urlutils.h"

#include <QFileInfo>
#include <QUrl>


UrlUtils::UrlUtils( QObject *parent )
: QObject( parent )
{
Expand All @@ -28,8 +28,18 @@ UrlUtils::UrlUtils( QObject *parent )

bool UrlUtils::isRelativeOrFileUrl( const QString &url )
{
if ( url.startsWith( QStringLiteral( "file:///" ) ) )
if ( url.startsWith( QStringLiteral( "file://" ) ) )
return true;

return QUrl( url ).isRelative();
}

QUrl UrlUtils::fromString( const QString &string )
{
if ( QFileInfo::exists( string ) )
{
return QUrl::fromLocalFile( string );
}

return QUrl( string );
}
6 changes: 4 additions & 2 deletions src/core/utils/urlutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ class QFIELD_CORE_EXPORT UrlUtils : public QObject
public:
explicit UrlUtils( QObject *parent = nullptr );


/**
* Checks whether the provided string is a relative url (has no protocol or starts with `file://`).
* Checks whether the provided string is a relative \a url (has no protocol or starts with `file://`).
*/
static Q_INVOKABLE bool isRelativeOrFileUrl( const QString &url );

//! Returns a URL from a \a string with logic to handle local paths
static Q_INVOKABLE QUrl fromString( const QString &string );
nirvn marked this conversation as resolved.
Show resolved Hide resolved
};

#endif // URLUTILS_H
2 changes: 1 addition & 1 deletion src/qml/About.qml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Item {
label = dataDirs.length > 1 ? qsTr('QField app directories') : qsTr('QField app directory');
for (let dataDir of dataDirs) {
if (isDesktopPlatform) {
label += '<br><a href="file://' + dataDir + '">' + dataDir + '</a>';
label += '<br><a href="' + UrlUtils.fromString(dataDir) + '">' + dataDir + '</a>';
} else {
label += '<br>' + dataDir;
}
Expand Down
2 changes: 1 addition & 1 deletion src/qml/FeatureListForm.qml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Rectangle {
width: {
if ( props.isVisible || featureForm.canvasOperationRequested )
{
if (fullScreenView || parent.width < parent.height || parent.width < 300)
if (fullScreenView || parent.width <= parent.height || parent.width < 300)
{
parent.width
}
Expand Down
2 changes: 1 addition & 1 deletion src/qml/PluginManagerSettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Popup {
Layout.preferredWidth: 24
Layout.preferredHeight: 24

source: Icon !== '' ? 'file://' + Icon : ''
source: Icon !== '' ? UrlUtils.fromString(Icon) : ''
fillMode: Image.PreserveAspectFit
mipmap: true
}
Expand Down
6 changes: 1 addition & 5 deletions src/qml/QFieldAudioRecorder.qml
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,7 @@ Popup {
running: false

onTriggered: {
var path = recorder.actualLocation.toString()
// On Android, the file protocol prefix is present while on Linux it isn't
var filePos = path.indexOf('file://')
path = filePos == -1 ? 'file://' + path : path
player.source = path
player.source = UrlUtils.fromString(recorder.actualLocation.toString())
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/qml/QFieldCamera.qml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Popup {

onImageSaved: (requestId, path) => {
currentPath = path
photoPreview.source = 'file://'+path
photoPreview.source = UrlUtils.fromString(path)
cameraItem.state = "PhotoPreview"
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/qml/QFieldSketcher.qml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ Popup {
anchors.margins: 5
visible: templatePath !== ''
fillMode: Image.PreserveAspectFit
source: templatePath !== '' ? 'file://' + templatePath : ''
source: templatePath !== '' ? UrlUtils.fromString(templatePath) : ''
}

Rectangle {
Expand Down
13 changes: 9 additions & 4 deletions src/qml/editorwidgets/ExternalResource.qml
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,16 @@ EditorWidgetBase {
image.hasImage = true
image.opacity = 1
image.anchors.topMargin = 0
image.source = (!isHttp ? 'file://' : '') + fullValue
image.source = UrlUtils.fromString(fullValue)
geoTagBadge.hasGeoTag = ExifTools.hasGeoTag(fullValue)
} else if (isAudio || isVideo) {
mediaFrame.height = 48

image.visible = false
image.opacity = 0.5
image.source = ''
player.firstFrameDrawn = false
player.sourceUrl = (!isHttp ? 'file://' : '') + fullValue
player.sourceUrl = UrlUtils.fromString(fullValue)
}
} else {
image.source = ''
Expand Down Expand Up @@ -262,7 +267,7 @@ EditorWidgetBase {
id: player
active: isAudio || isVideo

property string sourceUrl: ''
property url sourceUrl: ''
property bool firstFrameDrawn: false

anchors.left: parent.left
Expand Down Expand Up @@ -324,7 +329,7 @@ EditorWidgetBase {
id: sketchButton
anchors.top: image.top
anchors.right: image.right
visible: image.status === Image.Ready && isEnabled
visible: image.source != '' && image.status === Image.Ready && isEnabled

round: true
iconSource: Theme.getThemeVectorIcon( "ic_freehand_white_24dp" )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ EditorWidgetBase {
Image {
id: featureImage
source: ImagePath
? ('file://' + ImagePath)
? UrlUtils.fromString(ImagePath)
: Theme.getThemeIcon("ic_photo_notavailable_black_24dp")
width: parent.height
height: parent.height
Expand Down
2 changes: 2 additions & 0 deletions test/spix/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ py
pytest
pytest-html
pytest-image-diff
pyautogui
tk
53 changes: 50 additions & 3 deletions test/spix/smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import platform
from pathlib import Path
from PIL import Image
import pyautogui


@pytest.fixture
Expand Down Expand Up @@ -124,7 +125,7 @@ def test_wms_layer(app, screenshot_path, screenshot_check, extra, process_alive)
messagesCount = 0
for i in range(0, 10):
message = app.getStringProperty(
"mainWindow/messageLog/messageItem_{}/messageText".format(i), "text"
f"mainWindow/messageLog/messageItem_{i}/messageText", "text"
)
if message == "":
break
Expand Down Expand Up @@ -156,7 +157,7 @@ def test_projection(app, screenshot_path, screenshot_check, extra, process_alive
messagesCount = 0
for i in range(0, 10):
message = app.getStringProperty(
"mainWindow/messageLog/messageItem_{}/messageText".format(i), "text"
f"mainWindow/messageLog/messageItem_{i}/messageText", "text"
)
if message == "":
break
Expand All @@ -166,6 +167,52 @@ def test_projection(app, screenshot_path, screenshot_check, extra, process_alive
assert messagesCount == 0


@pytest.mark.project_file("test_image_attachment.qgz")
def test_projection(app, screenshot_path, screenshot_check, extra, process_alive):
"""
Starts a test app and check for proper reprojection support (including rendering check and message logs).
This also tests that QField is able to reach proj's crucial proj.db
"""
assert app.existsAndVisible("mainWindow")

# Arbitrary wait period to insure project fully loaded and rendered
time.sleep(4)
nirvn marked this conversation as resolved.
Show resolved Hide resolved

messagesCount = 0
for i in range(0, 10):
message = app.getStringProperty(
f"mainWindow/messageLog/messageItem_{i}/messageText", "text"
)
if message == "":
break
extra.append(extras.html("Message logs content: {}".format(message)))
messagesCount = messagesCount + 1
extra.append(extras.html("Message logs count: {}".format(messagesCount)))
assert messagesCount == 0

bounds = app.getBoundingBox("mainWindow/mapCanvas")
move_x = bounds[0] + bounds[2] / 2
move_y = bounds[1] + bounds[3] / 3

pyautogui.moveTo(move_x, move_y, duration=0.5)
pyautogui.click(interval=0.5)

bounds = app.getBoundingBox("mainWindow/featureForm")
move_x = bounds[0] + bounds[2] / 2
move_y = bounds[1] + 80

pyautogui.moveTo(move_x, move_y, duration=0.5)
pyautogui.click(interval=0.5)

app.takeScreenshot(
"mainWindow", os.path.join(screenshot_path, "test_image_attachment.png")
)
assert process_alive()
extra.append(extras.html('<img src="images/test_image_attachment.png"/>'))

assert screenshot_check("test_image_attachment", "test_image_attachment")


@pytest.mark.project_file("test_svg.qgz")
def test_svg(app, screenshot_path, screenshot_check, extra, process_alive):
"""
Expand Down Expand Up @@ -198,7 +245,7 @@ def test_postgis_ssl(app, screenshot_path, screenshot_check, extra, process_aliv
messagesCount = 0
for i in range(0, 10):
message = app.getStringProperty(
"mainWindow/messageLog/messageItem_{}/messageText".format(i), "text"
f"mainWindow/messageLog/messageItem_{i}/messageText", "text"
)
if message == "":
break
Expand Down
36 changes: 27 additions & 9 deletions test/test_urlutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,35 @@

#include <QDebug>
#include <QFileInfo>
#include <QTemporaryFile>
#include <QUrl>


TEST_CASE( "UrlUtils" )
{
// should be considered relative
REQUIRE( UrlUtils::isRelativeOrFileUrl( QStringLiteral( "path/to/file" ) ) );
REQUIRE( UrlUtils::isRelativeOrFileUrl( QStringLiteral( "/path/to/file" ) ) );
REQUIRE( UrlUtils::isRelativeOrFileUrl( QStringLiteral( "file:///path/to/file" ) ) );

// should NOT be considered relative
REQUIRE( !UrlUtils::isRelativeOrFileUrl( QStringLiteral( "http://osm.org" ) ) );
REQUIRE( !UrlUtils::isRelativeOrFileUrl( QStringLiteral( "http://osm.org/test?query=1" ) ) );
REQUIRE( !UrlUtils::isRelativeOrFileUrl( QStringLiteral( "https://osm.org/test?query=1" ) ) );
SECTION( "isRelativeOrFileUrl" )
{
// should be considered relative
REQUIRE( UrlUtils::isRelativeOrFileUrl( QStringLiteral( "path/to/file" ) ) );
REQUIRE( UrlUtils::isRelativeOrFileUrl( QStringLiteral( "/path/to/file" ) ) );
REQUIRE( UrlUtils::isRelativeOrFileUrl( QStringLiteral( "file:///path/to/file" ) ) );

// should NOT be considered relative
REQUIRE( !UrlUtils::isRelativeOrFileUrl( QStringLiteral( "http://osm.org" ) ) );
REQUIRE( !UrlUtils::isRelativeOrFileUrl( QStringLiteral( "http://osm.org/test?query=1" ) ) );
REQUIRE( !UrlUtils::isRelativeOrFileUrl( QStringLiteral( "https://osm.org/test?query=1" ) ) );
}

SECTION( "fromString" )
{
// a file that exists will be transformed into a file:// URL
QTemporaryFile tmpFile( QStringLiteral( "test.jpg" ) );
REQUIRE( UrlUtils::fromString( tmpFile.fileName() ).toString() == QUrl::fromLocalFile( tmpFile.fileName() ).toString() );

// a string that doesn't link to an existing file will not transform into a file:// URL
REQUIRE( UrlUtils::fromString( QStringLiteral( "/my/missing/file.txt" ) ).toString() == QStringLiteral( "/my/missing/file.txt" ) );

// a URL string (e.g. http(s)) will be handled as such
REQUIRE( UrlUtils::fromString( QStringLiteral( "https://www.opengis.ch/" ) ).toString() == QStringLiteral( "https://www.opengis.ch/" ) );
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/testdata/polygons.gpkg
Binary file not shown.
Binary file modified test/testdata/projection_dataset.gpkg
Binary file not shown.
Binary file added test/testdata/reserve.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/testdata/test_image_attachment.qgz
Binary file not shown.
Loading