Skip to content

Conversation

@Wohlstand
Copy link

@Wohlstand Wohlstand commented Nov 18, 2025

This is the similar fix to #7868 done for OneDrive, but it's fully suitable for the Nextcloud too, allowing Unicode paths used normally. URL field at the UI expected to support unicode paths, and they should be URL-encoded and Punycoded at the backend during the work. Without making unexpected situations like described in the #13254 and other similar.

Image Image

I have read the CLA Document and I hereby sign the CLA


recheck

@github-actions
Copy link
Contributor

github-actions bot commented Nov 18, 2025

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@Wohlstand
Copy link
Author

recheck

@Wohlstand
Copy link
Author

@laurent22, your bot seems blind...

@Wohlstand
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

github-actions bot added a commit that referenced this pull request Nov 18, 2025
@mrjo118
Copy link
Contributor

mrjo118 commented Nov 18, 2025

@Wohlstand Did you commit using --no-verify? The default commit hooks run a linter and it looks like the bot has posted comments about linting issues on the PR which need fixing.

On another note, do you think this change would be compatible if added for the WebDAV sync target as well? That would be helpful so users can use whatever sub directory they like for a WebDAV server, not just for NextCloud

@Wohlstand
Copy link
Author

Wohlstand commented Nov 18, 2025

On another note, do you think this change would be compatible if added for the WebDAV sync target as well? That would be helpful so users can use whatever sub directory they like on the server, which gets appended to the url

I think ye, it would be best to apply such change just everywhere, where are HTTP queries formed from user's input data that supposed to be properly encoded.

@Wohlstand Did you commit using --no-verify? The default commit hooks run a linter and it looks like the bot has posted comments about linting issues on the PR which need fixing.

Nope, I commited just with my local defaults.

@Wohlstand
Copy link
Author

By the way, I attempted to run this locally, but it just fails to install dependencies via npm install, once it said they are incompatible one, then with --force, it just got broken with lines like npm ERR! src/Parser.ts(4,22): error TS2583: Cannot find name 'Set'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later. At me NodeJS 18.19.1. I wanted to test the thing locally, but right now I am unable to do that.

@Wohlstand
Copy link
Author

@Wohlstand Did you commit using --no-verify? The default commit hooks run a linter and it looks like the bot has posted comments about linting issues on the PR which need fixing.

On another note, do you think this change would be compatible if added for the WebDAV sync target as well? That would be helpful so users can use whatever sub directory they like for a WebDAV server, not just for NextCloud

Huh! Just now I paid attention on the code of commit, and I see. Interesting thing. Let me try to fix these warnings...

@laurent22
Copy link
Owner

By the way, I attempted to run this locally, but it just fails to install dependencies via npm install, once it said they are incompatible one, then with --force, it just got broken with lines like

So you didn't test this change then?

@Wohlstand
Copy link
Author

Wohlstand commented Nov 18, 2025

So you didn't test this change then?

I wanted to test it, but local build is faulty (I said that npm install fails). So, question, does your Github Actions produce artifact? Let me check some if I can obtain a build for a test...

EDIT: I see, it doesn't. just builds, verifies and cleans, heck.

@personalizedrefrigerator
Copy link
Collaborator

I attempted to run this locally, but it just fails to install dependencies via npm install,

Be aware that Joplin uses yarn (rather than npm).

@Wohlstand
Copy link
Author

Okay now, I managed to read this manual, then I built the thing, tested with https://xxxxxxx/remote.php/dav/files/xxxx/Documents/Мои заметки URL, and it works just fine now, as I expected. So, @laurent22, since this moment I can answer: Yes, I tested it!

@mrjo118
Copy link
Contributor

mrjo118 commented Nov 19, 2025

Okay now, I managed to read this manual, then I built the thing, tested with https://xxxxxxx/remote.php/dav/files/xxxx/Documents/Мои заметки URL, and it works just fine now, as I expected. So, @laurent22, since this moment I can answer: Yes, I tested it!

Any chance of adding for SyncTargetWebDAV as well? To avoid duplication, I'd suggest adding a method to BaseSyncTarget.ts and then reference that for both WebDAV and NextCloud

@Wohlstand
Copy link
Author

Wohlstand commented Nov 19, 2025

Any chance of adding for SyncTargetWebDAV as well? To avoid duplication, I'd suggest adding a method to BaseSyncTarget.ts and then reference that for both WebDAV and NextCloud

This would be an off-topic for THIS PR. But okay, I will need to adjust everything here then (redo the thing, and rename the PR). I'll try tomorrow as now I should go sleep, it's too late time at me now (UTC+3 time zone).

@Wohlstand
Copy link
Author

Okay, I made some check, and it seems to fix both WebDAW and NextCloud it's fully enough to apply fix on WebDAV side since NextCloud just explicitly forwards init to WebDAV, and the same call is performed.

@Wohlstand Wohlstand changed the title All: Resolves #13254: Always fully encode URL for the Nextcloud API All: Resolves #13254: Always fully encode URL for the Nextcloud and WebDAV API Nov 19, 2025
@Wohlstand
Copy link
Author

I just moved the fix to WebDAV side, and this thing fixes both Nextcloud and plain WebDAV. It's should be enough.

@Wohlstand Wohlstand changed the title All: Resolves #13254: Always fully encode URL for the Nextcloud and WebDAV API All: Resolves #13254: Always fully encode URL for the Nextcloud and WebDAV Nov 19, 2025
Comment on lines 40 to 47
// the syncPath might contain non-ASCII characters
// /[^\u0021-\u00ff]/ is used in Node.js to detect the unescaped characters.
// See https://github.com/nodejs/node/blob/bbbf97b6dae63697371082475dc8651a6a220336/lib/_http_client.js#L176
const charsReg = /[^\u0021-\u00ff]/;
const pathUrl = charsReg.exec(options.path()) !== null ? encodeURI(options.path()) : options.path();
const apiOptions = {
baseUrl: () => options.path(),
baseUrl: () => pathUrl,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried that may break backward compatibility in unexpected ways or lead to double-encoding.

I think a safer way would be something like:

  • Decode the URL
  • Check if the decoded URL is different from actual URL
  • If it's not - don't do anything. Otherwise encode it.

This would at least prevent double-encoding.

I think the error href ${href} not in baseUrl ${baseUrl} nor relativeBaseUrl ${relativeBaseUrl} might also be triggered depending on the WebDAV implementation but at least that shouldn't relate to backward compatibility

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me check across the code WHO actually uses WebDAV's initializer to make the full sure...

Copy link
Author

@Wohlstand Wohlstand Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, the double-encoding is impossible, since charsReg check is a condition that is only valid when input path contains inapropriate characters. If string is already encoded, so it will never be valid for this condition, and it will never double-encoding happen.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For your sure, I checked via grep:
изображение
And WebDAV is only used by Nextcloud, so, I am fully sure it will never been double-encoding in this case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other APIs like OneDrive perform encoding on their own and they don't use WebDAV at all.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually! It's might be possible that URL can be PARTIALLY encoded, and therefore it should be fully encoded. Feels like it's more effective to write a custom encoder function that will skip parts that are already encoded (valid %XX bytes).

Copy link
Author

@Wohlstand Wohlstand Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I made the custom function that should SAFELY encode and don't encode already encoded characters twice!

function finishEncodeURI(path)
{
    let out = "";
    let begin = 0;
    let i;

    if(path == undefined || path === null || path.length === 0)
        return path;

    // Collecting ranges that should be encoded
    for(i = 0; i < path.length; )
    {
        // If valid percent encoding is detected, don't encode it again!
        if(path[i] === '%' && i + 3 < path.length && /[a-fA-F0-9]{2}/.test(path.substring(i + 1, i + 3)))
        {
            if(begin !== i)
                out += encodeURI(path.substring(begin, i));

            out += path.substring(i, i + 3);
            begin = i + 3;
            i += 3;
            
        }
        else
            ++i;
    }

    // If it's a piece at end of string that can be encoded too
    if(begin < i)
        out += encodeURI(path.substring(begin, i));

    return out;
}

And my test case that confirms the code works:

function printTest(orig)
{
    let enc = finishEncodeURI(orig);
    console.log("Orig: " + orig)
    console.log("Enc1: " + encodeURI(orig))
    console.log("Enc2: " + enc)
    console.log("Dec:  " + decodeURI(enc) + "\n")
}

finishEncodeURI(); // damn

finishEncodeURI(""); // damn

printTest("f");
printTest("ж");

printTest("Мыши!");

printTest("#5hj35h53hj5335j53j53j53j53j");

printTest("https://server.uk/Pre%20Encoded%20Path%20and%20%D0%9C%D1%8B%D1%88%D0%B8!/Что-то пошло не%20так/");

printTest("ЕРц335р35ро3о35о35о№%НР325н235нг23г25г2г55555");
printTest("ЕРц335р35ро3о35о35о№%НР325н235нг23г25г2г55555жжжжжж");

printTest("Just%20A Freaking Test%20Чтобы проверить, что всё щик%20У%20а не%20одни пр%ППоценты!вввffff%20");

So, it encodes parts that needs to be encoded, but it does NOT encodes parts that are already encoded! And it does this with a sniper accuracy!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the output:
Orig - Input string as-is
Enc1 - Encoded by plain encodeURI()
Enc2 - Encoded by my custom function
Dec - Decoded result of my custom function

Orig: f
Enc1: f
Enc2: f
Dec:  f

Orig: ж
Enc1: %D0%B6
Enc2: %D0%B6
Dec:  ж

Orig: Мыши!
Enc1: %D0%9C%D1%8B%D1%88%D0%B8!
Enc2: %D0%9C%D1%8B%D1%88%D0%B8!
Dec:  Мыши!

Orig: #5hj35h53hj5335j53j53j53j53j
Enc1: #5hj35h53hj5335j53j53j53j53j
Enc2: #5hj35h53hj5335j53j53j53j53j
Dec:  #5hj35h53hj5335j53j53j53j53j

Orig: https://server.uk/Pre%20Encoded%20Path%20and%20%D0%9C%D1%8B%D1%88%D0%B8!/Что-то пошло не%20так/
Enc1: https://server.uk/Pre%2520Encoded%2520Path%2520and%2520%25D0%259C%25D1%258B%25D1%2588%25D0%25B8!/%D0%A7%D1%82%D0%BE-%D1%82%D0%BE%20%D0%BF%D0%BE%D1%88%D0%BB%D0%BE%20%D0%BD%D0%B5%2520%D1%82%D0%B0%D0%BA/
Enc2: https://server.uk/Pre%20Encoded%20Path%20and%20%D0%9C%D1%8B%D1%88%D0%B8!/%D0%A7%D1%82%D0%BE-%D1%82%D0%BE%20%D0%BF%D0%BE%D1%88%D0%BB%D0%BE%20%D0%BD%D0%B5%20%D1%82%D0%B0%D0%BA/
Dec:  https://server.uk/Pre Encoded Path and Мыши!/Что-то пошло не так/

Orig: ЕРц335р35ро3о35о35о№%НР325н235нг23г25г2г55555
Enc1: %D0%95%D0%A0%D1%86335%D1%8035%D1%80%D0%BE3%D0%BE35%D0%BE35%D0%BE%E2%84%96%25%D0%9D%D0%A0325%D0%BD235%D0%BD%D0%B323%D0%B325%D0%B32%D0%B355555
Enc2: %D0%95%D0%A0%D1%86335%D1%8035%D1%80%D0%BE3%D0%BE35%D0%BE35%D0%BE%E2%84%96%25%D0%9D%D0%A0325%D0%BD235%D0%BD%D0%B323%D0%B325%D0%B32%D0%B355555
Dec:  ЕРц335р35ро3о35о35о№%НР325н235нг23г25г2г55555

Orig: ЕРц335р35ро3о35о35о№%НР325н235нг23г25г2г55555жжжжжж
Enc1: %D0%95%D0%A0%D1%86335%D1%8035%D1%80%D0%BE3%D0%BE35%D0%BE35%D0%BE%E2%84%96%25%D0%9D%D0%A0325%D0%BD235%D0%BD%D0%B323%D0%B325%D0%B32%D0%B355555%D0%B6%D0%B6%D0%B6%D0%B6%D0%B6%D0%B6
Enc2: %D0%95%D0%A0%D1%86335%D1%8035%D1%80%D0%BE3%D0%BE35%D0%BE35%D0%BE%E2%84%96%25%D0%9D%D0%A0325%D0%BD235%D0%BD%D0%B323%D0%B325%D0%B32%D0%B355555%D0%B6%D0%B6%D0%B6%D0%B6%D0%B6%D0%B6
Dec:  ЕРц335р35ро3о35о35о№%НР325н235нг23г25г2г55555жжжжжж

Orig: Just%20A Freaking Test%20Чтобы проверить, что всё щик%20У%20а не%20одни пр%ППоценты!вввffff%20
Enc1: Just%2520A%20Freaking%20Test%2520%D0%A7%D1%82%D0%BE%D0%B1%D1%8B%20%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C,%20%D1%87%D1%82%D0%BE%20%D0%B2%D1%81%D1%91%20%D1%89%D0%B8%D0%BA%2520%D0%A3%2520%D0%B0%20%D0%BD%D0%B5%2520%D0%BE%D0%B4%D0%BD%D0%B8%20%D0%BF%D1%80%25%D0%9F%D0%9F%D0%BE%D1%86%D0%B5%D0%BD%D1%82%D1%8B!%D0%B2%D0%B2%D0%B2ffff%2520
Enc2: Just%20A%20Freaking%20Test%20%D0%A7%D1%82%D0%BE%D0%B1%D1%8B%20%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C,%20%D1%87%D1%82%D0%BE%20%D0%B2%D1%81%D1%91%20%D1%89%D0%B8%D0%BA%20%D0%A3%20%D0%B0%20%D0%BD%D0%B5%20%D0%BE%D0%B4%D0%BD%D0%B8%20%D0%BF%D1%80%25%D0%9F%D0%9F%D0%BE%D1%86%D0%B5%D0%BD%D1%82%D1%8B!%D0%B2%D0%B2%D0%B2ffff%2520
Dec:  Just A Freaking Test Чтобы проверить, что всё щик У а не одни пр%ППоценты!вввffff%20

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question, @laurent22 , which file I should place this function? It feels like can be used at other places too since it's a general purpose thing rather than WebDAV specific.

Copy link
Author

@Wohlstand Wohlstand Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping?
Just a conclusion, that your original idea will fail if URL is encoded partially, not fully (it will lead that unencoded parts will pass and will cause the same failure as I reported). It will work if URL is already encoded fully. However, I developed the alternative solution that scans entire string and encodes only parts are absolutely not encoded bytes (any valid percent-encoded bytes). So, I still have a question: WHICH place is good to store the new-made function since it can be useful at other places too?

@Wohlstand
Copy link
Author

Wohlstand commented Dec 21, 2025

Okay, I did the next:

  • Rebased everything to the top.
  • Implemented module "utils/urlUtils.js" with the safe-encoding function to avoid double-encoding.
  • Applied use of that function.
  • Tested thing locally, it works.

I had to temporarily rename ".husky" directory, otherwise it prevents me to commit because of missing "corepack". I am unable to install "corepack" since it's incompatible to my system-wide installed nodejs 18 (it requires too high version).

@Wohlstand
Copy link
Author

... and I fixed all the code style warnings, so it should be fine now. Is everything okay now, @laurent22 ?

@Wohlstand
Copy link
Author

@Wohlstand Did you commit using --no-verify? The default commit hooks run a linter and it looks like the bot has posted comments about linting issues on the PR which need fixing.

@mrjo118, Lately replying with a new info: The hook started to work since some unexpected moment, BUT, it always fails because it fails to find corepack. I attempted to install it via npm, but it fails, just won't install, my local nodejs is 18 (installed from apt packages), but corepack requires way newer nodejs and that's a bummer. So, I made a fake "corepack" bash script that always returns 0 since it makes no sense for me here. CI actually helped me to figure the rest of linter errors that I fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants