Skip to content

Commit 1e8c003

Browse files
committed
Added clean image url method
1 parent b0077a7 commit 1e8c003

11 files changed

Lines changed: 205 additions & 54 deletions

File tree

lib/api/api_interceptor.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import 'dart:io';
33

44
import 'package:dio/dio.dart';
55
import 'package:flutter/material.dart';
6-
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
76
import 'package:piwigo_ng/app.dart';
87
import 'package:piwigo_ng/components/snackbars.dart';
98
import 'package:piwigo_ng/services/preferences_service.dart';
@@ -17,10 +16,8 @@ class ApiInterceptor extends Interceptor {
1716
RequestInterceptorHandler handler,
1817
) async {
1918
debugPrint("[${options.method}] ${options.queryParameters['method']}");
20-
FlutterSecureStorage secureStorage = const FlutterSecureStorage();
2119
SharedPreferences prefs = await SharedPreferences.getInstance();
22-
options.baseUrl =
23-
(await secureStorage.read(key: Preferences.serverUrlKey))!;
20+
options.baseUrl = (prefs.getString(Preferences.serverUrlKey))!;
2421
if (Preferences.getEnableBasicAuth) {
2522
String? username = prefs.getString(Preferences.basicUsernameKey) ?? '';
2623
String? password = prefs.getString(Preferences.basicPasswordKey) ?? '';

lib/api/authentication.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import 'dart:convert';
22

33
import 'package:dio/dio.dart';
44
import 'package:flutter/material.dart';
5-
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
65
import 'package:piwigo_ng/api/api_error.dart';
76
import 'package:piwigo_ng/api/upload.dart';
87
import 'package:piwigo_ng/models/info_model.dart';
98
import 'package:piwigo_ng/models/status_model.dart';
109
import 'package:piwigo_ng/services/preferences_service.dart';
10+
import 'package:shared_preferences/shared_preferences.dart';
1111

1212
import 'api_client.dart';
1313

@@ -44,8 +44,8 @@ Future<ApiResult<bool>> loginUser(
4444
}
4545

4646
ApiClient.cookieJar.deleteAll();
47-
FlutterSecureStorage secureStorage = const FlutterSecureStorage();
48-
await secureStorage.write(key: Preferences.serverUrlKey, value: url);
47+
SharedPreferences prefs = await SharedPreferences.getInstance();
48+
await prefs.setString(Preferences.serverUrlKey, url);
4949

5050
if (username.isEmpty && password.isEmpty) {
5151
ApiResult<StatusModel> status = await sessionStatus();

lib/api/images.dart

Lines changed: 178 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import 'dart:convert';
22
import 'dart:io';
33

44
import 'package:dio/dio.dart';
5+
import 'package:extended_text/extended_text.dart';
56
import 'package:file_picker/file_picker.dart';
6-
import 'package:flutter/material.dart';
7+
import 'package:flutter/foundation.dart';
78
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
89
import 'package:image_picker/image_picker.dart';
910
import 'package:path/path.dart' as path;
@@ -21,7 +22,9 @@ import 'package:share_plus/share_plus.dart';
2122
import 'albums.dart';
2223
import 'api_client.dart';
2324

24-
Future<ApiResult<ImageModel>> getImage(int imageId) async {
25+
Future<ApiResult<ImageModel>> getImage(
26+
int imageId,
27+
) async {
2528
Map<String, dynamic> queries = {
2629
'format': 'json',
2730
'method': 'pwg.images.getInfo',
@@ -48,8 +51,10 @@ Future<ApiResult<ImageModel>> getImage(int imageId) async {
4851
return ApiResult(error: ApiErrors.error);
4952
}
5053

51-
Future<ApiResult<List<ImageModel>>> fetchImages(int albumID,
52-
[int page = 0]) async {
54+
Future<ApiResult<List<ImageModel>>> fetchImages(
55+
int albumID, [
56+
int page = 0,
57+
]) async {
5358
Map<String, dynamic> queries = {
5459
'format': 'json',
5560
'method': 'pwg.categories.getImages',
@@ -78,7 +83,10 @@ Future<ApiResult<List<ImageModel>>> fetchImages(int albumID,
7883
return ApiResult(error: ApiErrors.fetchImagesError);
7984
}
8085

81-
Future<ApiResult<Map>> searchImages(String searchQuery, [int page = 0]) async {
86+
Future<ApiResult<Map>> searchImages(
87+
String searchQuery, [
88+
int page = 0,
89+
]) async {
8290
Map<String, dynamic> query = {
8391
'format': 'json',
8492
'method': 'pwg.images.search',
@@ -116,7 +124,9 @@ Future<ApiResult<Map>> searchImages(String searchQuery, [int page = 0]) async {
116124
return ApiResult(error: ApiErrors.searchImagesError);
117125
}
118126

119-
Future<ApiResult<Map>> fetchFavorites([int page = 0]) async {
127+
Future<ApiResult<Map>> fetchFavorites([
128+
int page = 0,
129+
]) async {
120130
Map<String, dynamic> query = {
121131
'format': 'json',
122132
'method': 'pwg.users.favorites.getList',
@@ -160,8 +170,10 @@ Future<String?> pickDirectoryPath() async {
160170
return await FilePicker.platform.getDirectoryPath();
161171
}
162172

163-
Future<void> _showDownloadNotification(
164-
{bool success = true, String? payload}) async {
173+
Future<void> _showDownloadNotification({
174+
bool success = true,
175+
String? payload,
176+
}) async {
165177
if (!Preferences.getDownloadNotification) return;
166178
final android = AndroidNotificationDetails(
167179
'piwigo-ng-download',
@@ -183,7 +195,9 @@ Future<void> _showDownloadNotification(
183195
);
184196
}
185197

186-
Future<bool> share(List<ImageModel> images) async {
198+
Future<bool> share(
199+
List<ImageModel> images,
200+
) async {
187201
List<XFile>? filesPath = await downloadImages(
188202
images,
189203
showNotification: false,
@@ -233,7 +247,10 @@ Future<List<XFile>?> downloadImages(
233247
return files;
234248
}
235249

236-
Future<XFile?> downloadImage(String dirPath, ImageModel image) async {
250+
Future<XFile?> downloadImage(
251+
String dirPath,
252+
ImageModel image,
253+
) async {
237254
String localPath = path.join(dirPath, image.file);
238255
try {
239256
await ApiClient.download(
@@ -276,7 +293,9 @@ Future<int> deleteImages(
276293
return nbSuccess;
277294
}
278295

279-
Future<bool> deleteImage(ImageModel image) async {
296+
Future<bool> deleteImage(
297+
ImageModel image,
298+
) async {
280299
Map<String, String> queries = {
281300
'format': 'json',
282301
'method': 'pwg.images.delete',
@@ -302,7 +321,10 @@ Future<bool> deleteImage(ImageModel image) async {
302321
return false;
303322
}
304323

305-
Future<int> removeImages(List<ImageModel> images, int albumId) async {
324+
Future<int> removeImages(
325+
List<ImageModel> images,
326+
int albumId,
327+
) async {
306328
int nbSuccess = 0;
307329
for (ImageModel image in images) {
308330
bool response = await removeImage(image, albumId);
@@ -313,7 +335,10 @@ Future<int> removeImages(List<ImageModel> images, int albumId) async {
313335
return nbSuccess;
314336
}
315337

316-
Future<bool> removeImage(ImageModel image, int albumId) async {
338+
Future<bool> removeImage(
339+
ImageModel image,
340+
int albumId,
341+
) async {
317342
final List<int> albums =
318343
image.categories.map<int>((album) => album['id']).toList();
319344
albums.removeWhere((album) => album == albumId);
@@ -348,7 +373,10 @@ Future<bool> removeImage(ImageModel image, int albumId) async {
348373
}
349374

350375
Future<int> moveImages(
351-
List<ImageModel> images, int oldAlbumId, int newAlbumId) async {
376+
List<ImageModel> images,
377+
int oldAlbumId,
378+
int newAlbumId,
379+
) async {
352380
int nbMoved = 0;
353381
for (var image in images) {
354382
bool response = await moveImage(image, oldAlbumId, newAlbumId);
@@ -359,7 +387,11 @@ Future<int> moveImages(
359387
return nbMoved;
360388
}
361389

362-
Future<bool> moveImage(ImageModel image, int oldAlbumId, int newAlbumId) async {
390+
Future<bool> moveImage(
391+
ImageModel image,
392+
int oldAlbumId,
393+
int newAlbumId,
394+
) async {
363395
final List<int> albums =
364396
image.categories.map<int>((album) => album['id']).toList();
365397
albums.removeWhere((id) => id == oldAlbumId);
@@ -390,7 +422,10 @@ Future<bool> moveImage(ImageModel image, int oldAlbumId, int newAlbumId) async {
390422
return false;
391423
}
392424

393-
Future<int> assignImages(List<ImageModel> images, int albumId) async {
425+
Future<int> assignImages(
426+
List<ImageModel> images,
427+
int albumId,
428+
) async {
394429
int nbAssigned = 0;
395430
for (ImageModel image in images) {
396431
final List<int> categories =
@@ -404,7 +439,10 @@ Future<int> assignImages(List<ImageModel> images, int albumId) async {
404439
return nbAssigned;
405440
}
406441

407-
Future<bool> assignImage(int imageId, List<int> categories) async {
442+
Future<bool> assignImage(
443+
int imageId,
444+
List<int> categories,
445+
) async {
408446
final Map<String, dynamic> queries = {
409447
'format': 'json',
410448
'method': 'pwg.images.setInfo',
@@ -491,8 +529,10 @@ Future<bool> editImage(
491529
}
492530

493531
/// Return a list of files that are not in the server
494-
Future<List<File>> checkImagesNotExist(List<File> files,
495-
{bool returnExistFiles = false}) async {
532+
Future<List<File>> checkImagesNotExist(
533+
List<File> files, {
534+
bool returnExistFiles = false,
535+
}) async {
496536
Map<String, File> md5sumList = {};
497537

498538
for (File file in files) {
@@ -528,3 +568,122 @@ Future<List<File>> checkImagesNotExist(List<File> files,
528568
}
529569
return [];
530570
}
571+
572+
String? removeUrlProtocol(String? url) {
573+
if (url == null) return null;
574+
url = url.replaceFirst('http://', '');
575+
url = url.replaceFirst('https://', '');
576+
return url;
577+
}
578+
579+
String? cleanImageUrl(String? originalUrl) {
580+
if (originalUrl == null) return null;
581+
final String okUrl = originalUrl;
582+
583+
// TEMPORARY PATCH for case where $conf['original_url_protection'] = 'images';
584+
/// See https://github.com/Piwigo/Piwigo-Mobile/issues/503
585+
String patchedUrl = okUrl.replaceAll('&amp;part=', '&part=');
586+
587+
// Servers may return incorrect URLs
588+
/// See https://tools.ietf.org/html/rfc3986#section-2
589+
Uri? serverUrl = Uri.tryParse(patchedUrl);
590+
591+
print("$patchedUrl");
592+
593+
if (serverUrl == null) {
594+
// URL not RFC compliant!
595+
String leftUrl = patchedUrl;
596+
597+
// Remove protocol header
598+
leftUrl = removeUrlProtocol(patchedUrl) ?? leftUrl;
599+
600+
// Retrieve authority
601+
int endAuthority = leftUrl.indexOf('/');
602+
// No path, incomplete URL —> return image.jpg but should never happen
603+
if (endAuthority == -1) return null;
604+
leftUrl = leftUrl.substring(endAuthority);
605+
606+
// The Piwigo server may not be in the root e.g. example.com/piwigo/…
607+
// So we remove the path to avoid a duplicate if necessary
608+
String? loginUrl = appPreferences.getString(Preferences.serverUrlKey);
609+
loginUrl = removeUrlProtocol(loginUrl);
610+
if (loginUrl != null &&
611+
loginUrl.isNotEmpty &&
612+
leftUrl.startsWith(loginUrl)) {
613+
leftUrl = leftUrl.substring(loginUrl.length);
614+
}
615+
616+
// Retrieve path
617+
int endQuery = leftUrl.indexOf('?');
618+
if (endQuery != -1) {
619+
String query = leftUrl.substring(0, endQuery);
620+
query.replaceAll('??', '?');
621+
String encodedQuery = Uri.encodeComponent(query);
622+
leftUrl.substring(0, query.length);
623+
String encodedPath = Uri.encodeComponent(leftUrl);
624+
serverUrl = Uri.tryParse("$loginUrl$encodedQuery$encodedPath");
625+
} else {
626+
// No query -> remaining string is a path
627+
String encodedPath = Uri.encodeComponent(leftUrl);
628+
serverUrl = Uri.tryParse("$loginUrl$encodedPath");
629+
}
630+
631+
// Last check
632+
if (serverUrl == null) {
633+
// Could not apply percent encoding —> return nil
634+
return null;
635+
}
636+
}
637+
638+
// Servers may return image URLs different from those used to login (e.g. wrong server settings)
639+
// We only keep the path+query because we only accept to download images from the same server.
640+
String cleanPath = serverUrl.path;
641+
// todo : parameterString
642+
String query = serverUrl.query;
643+
if (query.isNotEmpty) {
644+
cleanPath.joinChar("?$query");
645+
}
646+
String fragment = serverUrl.fragment;
647+
if (fragment.isNotEmpty) {
648+
cleanPath.joinChar("#$fragment");
649+
}
650+
651+
// The Piwigo server may not be in the root e.g. example.com/piwigo/…
652+
// and images may not be in the same path
653+
String? loginPath = appPreferences.getString(Preferences.serverUrlKey);
654+
loginPath = removeUrlProtocol(loginPath);
655+
if (loginPath != null && loginPath.isNotEmpty) {
656+
if (cleanPath.startsWith(loginPath)) {
657+
cleanPath = cleanPath.substring(loginPath.length);
658+
} else if (cleanPath.endsWith(loginPath)) {
659+
cleanPath = cleanPath.substring(0, cleanPath.length - loginPath.length);
660+
}
661+
}
662+
663+
// Remove the .php?, i? prefixes if any
664+
String prefix = '';
665+
int pos = cleanPath.indexOf('?');
666+
if (pos != -1) {
667+
prefix = cleanPath.substring(0, pos);
668+
}
669+
670+
// Path may not be encoded
671+
String decodedPath = Uri.decodeComponent(cleanPath);
672+
if (cleanPath == decodedPath) {
673+
String test = Uri.encodeComponent(cleanPath);
674+
cleanPath = test;
675+
}
676+
677+
// Compile final URL using the one provided at login
678+
String encodedImageUrl = "${serverUrl.scheme}://$loginPath$prefix$cleanPath";
679+
if (kDebugMode) {
680+
if (encodedImageUrl != originalUrl) {
681+
print("=> originalURL:$originalUrl");
682+
print(" encodedURL:$encodedImageUrl");
683+
print(
684+
" path=${serverUrl.path}, parameterString=${serverUrl.data}, query:${serverUrl.query}, fragment:${serverUrl.fragment}");
685+
}
686+
}
687+
688+
return encodedImageUrl;
689+
}

lib/api/upload.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,12 @@ Future<List<int>> uploadPhotos(
8181
// Initialize variables
8282
List<int> result = [];
8383
List<UploadItem> items = [];
84+
SharedPreferences prefs = await SharedPreferences.getInstance();
8485
FlutterSecureStorage storage = const FlutterSecureStorage();
85-
String? url = await storage.read(key: 'SERVER_URL');
86+
String? url = prefs.getString(Preferences.serverUrlKey);
8687
if (url == null) return [];
87-
String? username = await storage.read(key: 'SERVER_USERNAME');
88-
String? password = await storage.read(key: 'SERVER_PASSWORD');
88+
String? username = await storage.read(key: Preferences.usernameKey);
89+
String? password = await storage.read(key: Preferences.passwordKey);
8990
UploadNotifier uploadNotifier =
9091
App.appKey.currentContext!.read<UploadNotifier>();
9192
int nbError = 0;

lib/components/app_image_display.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import 'dart:io';
33

44
import 'package:cached_network_image/cached_network_image.dart';
55
import 'package:flutter/material.dart';
6-
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
76
import 'package:piwigo_ng/api/api_client.dart';
87
import 'package:piwigo_ng/services/preferences_service.dart';
98
import 'package:shared_preferences/shared_preferences.dart';
@@ -32,8 +31,7 @@ class _AppImageDisplayState extends State<AppImageDisplay> {
3231
}
3332

3433
Future<Map<String, String>> _getHeaders() async {
35-
FlutterSecureStorage secureStorage = const FlutterSecureStorage();
36-
String? serverUrl = await secureStorage.read(key: 'SERVER_URL');
34+
String? serverUrl = appPreferences.getString(Preferences.serverUrlKey);
3735

3836
if (serverUrl == null) return {};
3937

0 commit comments

Comments
 (0)