diff --git a/src/community.js b/src/community.js index db69fb2b9..8293434ed 100644 --- a/src/community.js +++ b/src/community.js @@ -43,37 +43,43 @@ function getVersionData(versionsRoot, version) { return { ...versionData, unstable }; // { manifest, publishState, creationDate, ts, unstable } } -function _toRawUrl(parsedUrl) { +// github: https://github.com/cloudron/test-app/blob/main/CloudronVersions.json +// raw: https://raw.githubusercontent.com/cloudron/test-app/main/CloudronVersions.json +// gitlab: https://gitlab.com/gitlab-org/gitlab/-/blob/master/CloudronVersions.json?ref_type=heads +// raw: https://gitlab.com/gitlab-org/gitlab/-/raw/master/CloudronVersions.json +// codeberg: https://codeberg.org/forgejo/forgejo/src/branch/forgejo/CloudronVersions.json +// raw: https://codeberg.org/forgejo/forgejo/raw/branch/forgejo/CloudronVersions.json +// gitea: https://git.selfhosted.in/girish/configs/src/branch/master/CloudronVersions.json +// raw: https://git.selfhosted.in/girish/configs/raw/branch/master/CloudronVersions.json +// surfer: https://lotsof-space.de/cloudron/webmail/CloudronVersions.json +function _getUrlCandidates(parsedUrl) { assert.strictEqual(typeof parsedUrl, 'object'); - if (parsedUrl.hostname === 'raw.githubusercontent.com') return null; // no rewrite + const candidates = []; + let url = parsedUrl.origin + parsedUrl.pathname; // strip query params - if (parsedUrl.hostname === 'github.com') { - const match = parsedUrl.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/(.+)$/); - if (match) { - const [, user, repo, rest] = match; - return `https://raw.githubusercontent.com/${user}/${repo}/${rest}`; - } + if (!url.endsWith(`/${CLOUDRON_VERSIONS_FILE}`)) url += url.endsWith('/') ? CLOUDRON_VERSIONS_FILE : `/${CLOUDRON_VERSIONS_FILE}`; + + candidates.push(url); + + if (url.startsWith('https://raw.githubusercontent.com/')) return candidates; + + if (url.startsWith('https://github.com/') && url.includes('/blob/')) { + const match = url.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/(.+)$/); + if (match) candidates.push(`https://raw.githubusercontent.com/${match[1]}/${match[2]}/${match[3]}`); + } else if (url.includes('/-/blob/')) { // gitlab (hosted and self-hosted) + candidates.push(url.replace('/-/blob/', '/-/raw/')); + } else if (/\/src\/(branch|tag|commit)\//.test(url)) { // gitea/codeberg/gogs/forgejo + candidates.push(url.replace('/src/', '/raw/')); + } else if (/\b(git|gitlab|gitea|forgejo|gogs|codeberg|code)\b/.test(parsedUrl.hostname)) { + const repoBase = url.slice(0, -CLOUDRON_VERSIONS_FILE.length - 1); + candidates.push(`${repoBase}/-/raw/master/${CLOUDRON_VERSIONS_FILE}`); + candidates.push(`${repoBase}/-/raw/main/${CLOUDRON_VERSIONS_FILE}`); + candidates.push(`${repoBase}/raw/branch/master/${CLOUDRON_VERSIONS_FILE}`); + candidates.push(`${repoBase}/raw/branch/main/${CLOUDRON_VERSIONS_FILE}`); } - // GitLab (gitlab.com or self-hosted with "gitlab" in hostname) - if (parsedUrl.hostname.includes('gitlab')) { - const match = parsedUrl.pathname.match(/^((?:\/[^/]+)+)\/-\/blob\/(.+)$/); - if (match) { - const [, repoBase, rest] = match; - return `https://${parsedUrl.hostname}${repoBase}/-/raw/${rest}`; - } - } - - // Gitea / Forgejo / Gogs - // Handles /user/repo/src/(branch|tag|commit)/ref/...path - const giteaMatch = parsedUrl.pathname.match(/^(\/[^/]+\/[^/]+)\/src\/(branch|tag|commit)\/(.+)$/); - if (giteaMatch) { - const [, repoBase, refType, rest] = giteaMatch; - return `https://${parsedUrl.hostname}${repoBase}/raw/${refType}/${rest}`; - } - - return null; + return candidates; } async function resolveVersionsUrl(url, version = 'latest') { @@ -84,10 +90,7 @@ async function resolveVersionsUrl(url, version = 'latest') { if (!parsedUrl) throw new BoxError(BoxError.BAD_FIELD, 'versionsUrl is not a valid URL'); if (parsedUrl.protocol !== 'https:') throw new BoxError(BoxError.BAD_FIELD, 'versionsUrl must use https'); - const rawUrl = _toRawUrl(parsedUrl); - const candidates = []; - if (parsedUrl.pathname.endsWith(CLOUDRON_VERSIONS_FILE)) candidates.push(url); - if (rawUrl && rawUrl !== url) candidates.push(rawUrl); + const candidates = _getUrlCandidates(parsedUrl); for (const candidate of candidates) { const [error, versionsRoot] = await safe(fetchVersionsRoot(candidate)); @@ -182,7 +185,7 @@ async function downloadIcon(manifest) { } export default { - _toRawUrl, + _getUrlCandidates, resolveVersionsUrl, getAppVersion, downloadManifest, diff --git a/src/test/community-test.js b/src/test/community-test.js index 6f3b3d356..6a9cb1e28 100644 --- a/src/test/community-test.js +++ b/src/test/community-test.js @@ -41,52 +41,137 @@ describe('Community', function () { nock.cleanAll(); }); - describe('_toRawUrl', function () { - it('rewrites github blob root and nested paths', async function () { - assert.equal( - community._toRawUrl(new URL('https://github.com/cloudron/test-app/blob/main/CloudronVersions.json')), - 'https://raw.githubusercontent.com/cloudron/test-app/main/CloudronVersions.json' + describe('_getUrlCandidates', function () { + it('returns github blob URL and raw URL', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://github.com/cloudron/test-app/blob/main/CloudronVersions.json')), + [ + 'https://github.com/cloudron/test-app/blob/main/CloudronVersions.json', + 'https://raw.githubusercontent.com/cloudron/test-app/main/CloudronVersions.json' + ] ); - assert.equal( - community._toRawUrl(new URL('https://github.com/cloudron/test-app/blob/main/packaging/CloudronVersions.json')), - 'https://raw.githubusercontent.com/cloudron/test-app/main/packaging/CloudronVersions.json' + assert.deepEqual( + community._getUrlCandidates(new URL('https://github.com/cloudron/test-app/blob/main/packaging/CloudronVersions.json')), + [ + 'https://github.com/cloudron/test-app/blob/main/packaging/CloudronVersions.json', + 'https://raw.githubusercontent.com/cloudron/test-app/main/packaging/CloudronVersions.json' + ] ); }); - it('rewrites gitlab blob root and nested paths', async function () { - assert.equal( - community._toRawUrl(new URL('https://gitlab.com/cloudron/test-app/-/blob/main/CloudronVersions.json')), - 'https://gitlab.com/cloudron/test-app/-/raw/main/CloudronVersions.json' + it('returns gitlab blob URL and raw URL', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://gitlab.com/cloudron/test-app/-/blob/main/CloudronVersions.json')), + [ + 'https://gitlab.com/cloudron/test-app/-/blob/main/CloudronVersions.json', + 'https://gitlab.com/cloudron/test-app/-/raw/main/CloudronVersions.json' + ] ); - assert.equal( - community._toRawUrl(new URL('https://my.gitlab.company/group/subgroup/test-app/-/blob/main/path/CloudronVersions.json')), - 'https://my.gitlab.company/group/subgroup/test-app/-/raw/main/path/CloudronVersions.json' + assert.deepEqual( + community._getUrlCandidates(new URL('https://my.gitlab.company/group/subgroup/test-app/-/blob/main/path/CloudronVersions.json')), + [ + 'https://my.gitlab.company/group/subgroup/test-app/-/blob/main/path/CloudronVersions.json', + 'https://my.gitlab.company/group/subgroup/test-app/-/raw/main/path/CloudronVersions.json' + ] ); }); - it('rewrites gitea/forgejo/gogs src paths', async function () { - assert.equal( - community._toRawUrl(new URL('https://gitea.example.com/user/repo/src/branch/main/CloudronVersions.json')), - 'https://gitea.example.com/user/repo/raw/branch/main/CloudronVersions.json' + it('returns gitea/forgejo/gogs src URL and raw URL', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://gitea.example.com/user/repo/src/branch/main/CloudronVersions.json')), + [ + 'https://gitea.example.com/user/repo/src/branch/main/CloudronVersions.json', + 'https://gitea.example.com/user/repo/raw/branch/main/CloudronVersions.json' + ] ); - assert.equal( - community._toRawUrl(new URL('https://codeberg.org/forgejo/forgejo/src/branch/forgejo/packaging/CloudronVersions.json')), - 'https://codeberg.org/forgejo/forgejo/raw/branch/forgejo/packaging/CloudronVersions.json' + assert.deepEqual( + community._getUrlCandidates(new URL('https://codeberg.org/forgejo/forgejo/src/branch/forgejo/packaging/CloudronVersions.json')), + [ + 'https://codeberg.org/forgejo/forgejo/src/branch/forgejo/packaging/CloudronVersions.json', + 'https://codeberg.org/forgejo/forgejo/raw/branch/forgejo/packaging/CloudronVersions.json' + ] ); - assert.equal( - community._toRawUrl(new URL('https://gogs.example.com/user/repo/src/tag/v1.0.0/CloudronVersions.json')), - 'https://gogs.example.com/user/repo/raw/tag/v1.0.0/CloudronVersions.json' + assert.deepEqual( + community._getUrlCandidates(new URL('https://gogs.example.com/user/repo/src/tag/v1.0.0/CloudronVersions.json')), + [ + 'https://gogs.example.com/user/repo/src/tag/v1.0.0/CloudronVersions.json', + 'https://gogs.example.com/user/repo/raw/tag/v1.0.0/CloudronVersions.json' + ] ); }); - it('returns null for non-rewrite URLs', async function () { - assert.equal( - community._toRawUrl(new URL('https://raw.githubusercontent.com/cloudron/test-app/main/CloudronVersions.json')), - null + it('returns single candidate for already-raw github URL', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://raw.githubusercontent.com/cloudron/test-app/main/CloudronVersions.json')), + ['https://raw.githubusercontent.com/cloudron/test-app/main/CloudronVersions.json'] ); - assert.equal( - community._toRawUrl(new URL('https://github.com/cloudron/test-app')), - null + }); + + it('appends CloudronVersions.json when missing', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://example.com/cloudron/webmail/')), + ['https://example.com/cloudron/webmail/CloudronVersions.json'] + ); + assert.deepEqual( + community._getUrlCandidates(new URL('https://github.com/cloudron/test-app/blob/main/packaging/')), + [ + 'https://github.com/cloudron/test-app/blob/main/packaging/CloudronVersions.json', + 'https://raw.githubusercontent.com/cloudron/test-app/main/packaging/CloudronVersions.json' + ] + ); + assert.deepEqual( + community._getUrlCandidates(new URL('https://github.com/cloudron/test-app')), + ['https://github.com/cloudron/test-app/CloudronVersions.json'] + ); + }); + + it('strips query params', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://gitlab.com/group/project/-/blob/main/CloudronVersions.json?ref_type=heads')), + [ + 'https://gitlab.com/group/project/-/blob/main/CloudronVersions.json', + 'https://gitlab.com/group/project/-/raw/main/CloudronVersions.json' + ] + ); + }); + + it('returns single candidate for plain surfer URL', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://minimal-space.de/cloudron/webmail/CloudronVersions.json')), + ['https://minimal-space.de/cloudron/webmail/CloudronVersions.json'] + ); + }); + + it('generates forge candidates for bare repo URL with git in subdomain', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://git.cloudron.io/platform/test-app')), + [ + 'https://git.cloudron.io/platform/test-app/CloudronVersions.json', + 'https://git.cloudron.io/platform/test-app/-/raw/master/CloudronVersions.json', + 'https://git.cloudron.io/platform/test-app/-/raw/main/CloudronVersions.json', + 'https://git.cloudron.io/platform/test-app/raw/branch/master/CloudronVersions.json', + 'https://git.cloudron.io/platform/test-app/raw/branch/main/CloudronVersions.json' + ] + ); + }); + + it('generates forge candidates for bare gitlab.com repo URL', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://gitlab.com/group/project')), + [ + 'https://gitlab.com/group/project/CloudronVersions.json', + 'https://gitlab.com/group/project/-/raw/master/CloudronVersions.json', + 'https://gitlab.com/group/project/-/raw/main/CloudronVersions.json', + 'https://gitlab.com/group/project/raw/branch/master/CloudronVersions.json', + 'https://gitlab.com/group/project/raw/branch/main/CloudronVersions.json' + ] + ); + }); + + it('does not generate forge candidates for non-git hostname', function () { + assert.deepEqual( + community._getUrlCandidates(new URL('https://example.com/some/path')), + ['https://example.com/some/path/CloudronVersions.json'] ); }); });