feat(sync): GitHub en plus de Gitea, et refus d'un depot public
GitHub parle la meme API des contenus (base64, sha, corps du PUT) : meme chemin, reconnu a api.github.com/repos/. User-Agent pose partout (GitHub refuse sans). Un depot public est refuse avant toute lecture ou ecriture, Gitea comme GitHub : l'historique garde tous les instantanes chiffres. Telephone : github.com/O/R se convertit en URL d'API et inversement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,7 @@ type
|
||||
FBaseUrl: string;
|
||||
FToken: string;
|
||||
FTimeoutMs: Integer;
|
||||
FPrivacyChecked: Boolean;
|
||||
public
|
||||
constructor Create(const ABaseUrl, AToken: string);
|
||||
|
||||
@@ -73,6 +74,12 @@ type
|
||||
/// n'y ressemble pas est de toute facon inutilisable.
|
||||
function IsGitea: Boolean;
|
||||
|
||||
/// UN DEPOT PUBLIC EST REFUSE (Gitea comme GitHub) : chaque synchro est un
|
||||
/// commit, et l'historique garde tous les instantanes, attaquables hors
|
||||
/// ligne par quiconque peut les lire. Message a afficher, ou '' si le depot
|
||||
/// est prive ou si on n'a pas pu le savoir. Verifie une fois par instance.
|
||||
function PublicRepoWarning: string;
|
||||
|
||||
/// POURQUOI UN 404 OU UN 401, en clair pour l'utilisateur. Gitea repond 404
|
||||
/// aussi bien pour un fichier absent que pour un depot ou un proprietaire
|
||||
/// inexistant : deux requetes de plus tranchent - le depot, puis
|
||||
@@ -102,7 +109,55 @@ uses
|
||||
|
||||
function TSyncClient.IsGitea: Boolean;
|
||||
begin
|
||||
Result := FBaseUrl.ToLower.Contains('/api/v1/repos/');
|
||||
// GitHub parle la meme API des contenus : meme chemin pour les deux.
|
||||
Result := FBaseUrl.ToLower.Contains('/api/v1/repos/') or
|
||||
FBaseUrl.ToLower.Contains('api.github.com/repos/');
|
||||
end;
|
||||
|
||||
/// URL de l'API du depot (sans /contents/...), identique pour Gitea
|
||||
/// (https://hote/api/v1/repos/O/R) et GitHub (https://api.github.com/repos/O/R).
|
||||
function RepoApiUrl(const AContentsUrl: string): string;
|
||||
var
|
||||
LP: Integer;
|
||||
begin
|
||||
LP := Pos('/contents/', AContentsUrl);
|
||||
if LP > 0 then Result := Copy(AContentsUrl, 1, LP - 1) else Result := '';
|
||||
end;
|
||||
|
||||
function TSyncClient.PublicRepoWarning: string;
|
||||
var
|
||||
LHttp: THTTPClient;
|
||||
LResp: IHTTPResponse;
|
||||
LDoc: TJSONObject;
|
||||
LPriv: Boolean;
|
||||
begin
|
||||
Result := '';
|
||||
if FPrivacyChecked or not IsGitea or (RepoApiUrl(FBaseUrl) = '') then Exit;
|
||||
FPrivacyChecked := True;
|
||||
LHttp := THTTPClient.Create;
|
||||
try
|
||||
LHttp.ConnectionTimeout := FTimeoutMs;
|
||||
LHttp.ResponseTimeout := FTimeoutMs;
|
||||
LHttp.HandleRedirects := False;
|
||||
LHttp.UserAgent := 'PMServer';
|
||||
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
|
||||
try
|
||||
LResp := LHttp.Get(RepoApiUrl(FBaseUrl));
|
||||
except
|
||||
Exit; // doute reseau : pas de verdict, la vraie requete le dira
|
||||
end;
|
||||
if LResp.StatusCode <> 200 then Exit;
|
||||
LDoc := TJSONObject.ParseJSONValue(LResp.ContentAsString(TEncoding.UTF8)) as TJSONObject;
|
||||
if LDoc = nil then Exit;
|
||||
try
|
||||
if LDoc.TryGetValue<Boolean>('private', LPriv) and not LPriv then
|
||||
Result := 'This repository is PUBLIC - make it private before syncing: anyone could download your encrypted vault and all its history.';
|
||||
finally
|
||||
LDoc.Free;
|
||||
end;
|
||||
finally
|
||||
LHttp.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TSyncClient.Diagnose(AHttpCode: Integer): string;
|
||||
@@ -126,10 +181,12 @@ begin
|
||||
if AHttpCode = 401 then
|
||||
Exit('Token refused - expired, revoked or mistyped');
|
||||
if (AHttpCode <> 404) or not IsGitea then Exit;
|
||||
// https://hote/api/v1/repos/OWNER/REPO/contents/FICHIER
|
||||
LP := Pos('/api/v1/repos/', LowerCase(FBaseUrl));
|
||||
LApi := Copy(FBaseUrl, 1, LP + Length('/api/v1/') - 1);
|
||||
LParts := Copy(FBaseUrl, LP + Length('/api/v1/repos/'), MaxInt).Split(['/']);
|
||||
// https://hote/api/v1/repos/O/R/contents/F ou https://api.github.com/repos/O/R/...
|
||||
// Tout ce qui precede "repos/" est la racine de l'API, pour les deux.
|
||||
LP := Pos('/repos/', LowerCase(FBaseUrl));
|
||||
if LP = 0 then Exit;
|
||||
LApi := Copy(FBaseUrl, 1, LP);
|
||||
LParts := Copy(FBaseUrl, LP + Length('/repos/'), MaxInt).Split(['/']);
|
||||
if Length(LParts) < 2 then Exit;
|
||||
LOwner := LParts[0];
|
||||
LRepo := LParts[1];
|
||||
@@ -138,6 +195,7 @@ begin
|
||||
LHttp.ConnectionTimeout := FTimeoutMs;
|
||||
LHttp.ResponseTimeout := FTimeoutMs;
|
||||
LHttp.HandleRedirects := False;
|
||||
LHttp.UserAgent := 'PMServer';
|
||||
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
|
||||
case CodeOf(LApi + 'repos/' + LOwner + '/' + LRepo) of
|
||||
200: Result := 'No vault in this repository yet - sync from the PC first';
|
||||
@@ -237,7 +295,7 @@ begin
|
||||
if not IsGitea then
|
||||
begin
|
||||
Result.Status := ssHttpError;
|
||||
Result.Error := 'Ecriture disponible seulement vers un depot Gitea';
|
||||
Result.Error := 'Ecriture disponible seulement vers un depot Gitea ou GitHub';
|
||||
Exit;
|
||||
end;
|
||||
if (FBaseUrl = '') or (FToken = '') then
|
||||
@@ -245,6 +303,12 @@ begin
|
||||
Result.Error := 'URL ou jeton non configure';
|
||||
Exit;
|
||||
end;
|
||||
Result.Error := PublicRepoWarning;
|
||||
if Result.Error <> '' then
|
||||
begin
|
||||
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
|
||||
Exit;
|
||||
end;
|
||||
|
||||
LEnv := TJSONObject.Create;
|
||||
LB64 := TBase64Encoding.Create(0); // pas de coupure : voir PMA.Crypto
|
||||
@@ -266,6 +330,7 @@ begin
|
||||
LHttp.ConnectionTimeout := FTimeoutMs;
|
||||
LHttp.ResponseTimeout := FTimeoutMs;
|
||||
LHttp.HandleRedirects := False;
|
||||
LHttp.UserAgent := 'PMServer'; // GitHub refuse toute requete sans
|
||||
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
|
||||
LHttp.CustomHeaders['Content-Type'] := 'application/json';
|
||||
try
|
||||
@@ -346,11 +411,18 @@ begin
|
||||
Result.Error := 'URL ou jeton non configure';
|
||||
Exit;
|
||||
end;
|
||||
Result.Error := PublicRepoWarning;
|
||||
if Result.Error <> '' then
|
||||
begin
|
||||
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
|
||||
Exit;
|
||||
end;
|
||||
|
||||
LHttp := THTTPClient.Create;
|
||||
try
|
||||
LHttp.ConnectionTimeout := FTimeoutMs;
|
||||
LHttp.ResponseTimeout := FTimeoutMs;
|
||||
LHttp.UserAgent := 'PMServer'; // GitHub refuse toute requete sans
|
||||
// Pas de redirection suivie : une redirection vers un autre hote nous
|
||||
// ferait envoyer le jeton ailleurs.
|
||||
LHttp.HandleRedirects := False;
|
||||
|
||||
@@ -574,7 +574,7 @@
|
||||
entier. Un input d'une ligne ne defilait pas sous le doigt dans la
|
||||
WebView de l'app, et la fin d'une URL d'API restait inatteignable. -->
|
||||
<textarea class="field-in multi" id="sUrl" rows="1"
|
||||
placeholder="repository URL, e.g. https://git.example.com/owner/repo"
|
||||
placeholder="repository URL - Gitea or github.com/owner/repo (private)"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="none"
|
||||
spellcheck="false" inputmode="url"></textarea>
|
||||
<div class="in-x" data-clear="sUrl">×</div>
|
||||
@@ -1696,6 +1696,9 @@ document.addEventListener('input', function (ev) {
|
||||
|
||||
// L'inverse, pour l'affichage : l'URL d'API se relit comme l'adresse du depot.
|
||||
function shortRepoUrl(u) {
|
||||
var g = /^https:\/\/api\.github\.com\/repos\/([^\/]+)\/([^\/]+)\/contents\/(.+)$/i.exec(u || '');
|
||||
if (g) return 'https://github.com/' + g[1] + '/' + g[2] +
|
||||
(g[3] === 'vault-sync.json' ? '' : '/blob/main/' + g[3]);
|
||||
var m = /^(https?:\/\/[^\/]+)\/api\/v1\/repos\/([^\/]+)\/([^\/]+)\/contents\/(.+)$/i.exec(u || '');
|
||||
if (!m) return u;
|
||||
return m[1] + '/' + m[2] + '/' + m[3] +
|
||||
@@ -1703,7 +1706,12 @@ function shortRepoUrl(u) {
|
||||
}
|
||||
function normalizeRepoUrl(u) {
|
||||
u = u.replace(/\s+/g, '');
|
||||
if (!u || /\/api\/v1\/repos\//i.test(u) || /\.php(\?|$)/i.test(u)) return u;
|
||||
if (!u || /\/api\/v1\/repos\//i.test(u) || /api\.github\.com\/repos\//i.test(u) ||
|
||||
/\.php(\?|$)/i.test(u)) return u;
|
||||
// GITHUB : l'adresse du depot est sur github.com, l'API sur api.github.com.
|
||||
var g = u.match(/^https?:\/\/(www\.)?github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?(?:\/blob\/[^\/]+\/(.+?))?\/?$/i);
|
||||
if (g) return 'https://api.github.com/repos/' + g[2] + '/' + g[3] +
|
||||
'/contents/' + (g[4] || 'vault-sync.json');
|
||||
var m = u.match(/^(https?:\/\/[^\/]+)\/([^\/]+)\/([^\/]+?)(?:\.git)?(?:\/src\/branch\/[^\/]+\/(.+?))?\/?$/i);
|
||||
if (!m) return u;
|
||||
return m[1] + '/api/v1/repos/' + m[2] + '/' + m[3] + '/contents/' + (m[4] || 'vault-sync.json');
|
||||
|
||||
Reference in New Issue
Block a user