fix: image url check for embed link (#4826)

* fix: image url check in for embed link

* chore: move all patterns to shared

* test: prefer enterText over manipulating widget
This commit is contained in:
Mathias Mogensen 2024-03-06 16:31:30 +01:00 committed by GitHub
parent 6e2caf3358
commit f5cb8b6d25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 104 additions and 75 deletions

View File

@ -1,6 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
@ -14,8 +17,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embe
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -132,8 +133,7 @@ class EditorOperations {
of: find.byType(EmbedImageUrlWidget), of: find.byType(EmbedImageUrlWidget),
matching: find.byType(TextField), matching: find.byType(TextField),
); );
final textField = tester.widget<TextField>(imageUrlTextField); await tester.enterText(imageUrlTextField, imageUrl);
textField.controller?.text = imageUrl;
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tapButton( await tester.tapButton(
find.descendant( find.descendant(

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart'; import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@ -23,9 +25,7 @@ class FontPickerScreen extends StatelessWidget {
} }
class LanguagePickerPage extends StatefulWidget { class LanguagePickerPage extends StatefulWidget {
const LanguagePickerPage({ const LanguagePickerPage({super.key});
super.key,
});
@override @override
State<LanguagePickerPage> createState() => _LanguagePickerPageState(); State<LanguagePickerPage> createState() => _LanguagePickerPageState();
@ -52,6 +52,7 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
body: SafeArea( body: SafeArea(
child: Scrollbar( child: Scrollbar(
child: ListView.builder( child: ListView.builder(
itemCount: availableFonts.length + 1, // with search bar
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
// search bar // search bar
@ -65,7 +66,8 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
setState(() { setState(() {
availableFonts = _availableFonts availableFonts = _availableFonts
.where( .where(
(element) => parseFontFamilyName(element) (font) => font
.parseFontFamilyName()
.toLowerCase() .toLowerCase()
.contains(keyword.toLowerCase()), .contains(keyword.toLowerCase()),
) )
@ -75,8 +77,9 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
), ),
); );
} }
final fontFamilyName = availableFonts[index - 1]; final fontFamilyName = availableFonts[index - 1];
final displayName = parseFontFamilyName(fontFamilyName); final displayName = fontFamilyName.parseFontFamilyName();
return FlowyOptionTile.checkbox( return FlowyOptionTile.checkbox(
text: displayName, text: displayName,
isSelected: selectedFontFamilyName == fontFamilyName, isSelected: selectedFontFamilyName == fontFamilyName,
@ -86,17 +89,9 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
); );
}, },
itemCount: availableFonts.length + 1, // with search bar
), ),
), ),
), ),
); );
} }
String parseFontFamilyName(String fontFamilyName) {
final camelCase = RegExp('(?<=[a-z])[A-Z]');
return fontFamilyName
.replaceAll('_regular', '')
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
} }

View File

@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database/grid/application/calculations/field_ty
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
@ -132,9 +133,8 @@ class _CalculateCellState extends State<CalculateCell> {
} }
String _withoutTrailingZeros(String value) { String _withoutTrailingZeros(String value) {
final regex = RegExp(r'^(\d+(?:\.\d*?[1-9](?=0|\b))?)\.?0*$'); if (trailingZerosRegex.hasMatch(value)) {
if (regex.hasMatch(value)) { final match = trailingZerosRegex.firstMatch(value)!;
final match = regex.firstMatch(value)!;
return match.group(1)!; return match.group(1)!;
} }

View File

@ -1,10 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
RegExp _hrefRegex = RegExp(
r'https?://(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(?:/[^\s]*)?',
);
extension PasteFromPlainText on EditorState { extension PasteFromPlainText on EditorState {
Future<void> pastePlainText(String plainText) async { Future<void> pastePlainText(String plainText) async {
if (await pasteHtmlIfAvailable(plainText)) { if (await pasteHtmlIfAvailable(plainText)) {
@ -23,7 +20,7 @@ extension PasteFromPlainText on EditorState {
.map((e) { .map((e) {
// parse the url content // parse the url content
final Attributes attributes = {}; final Attributes attributes = {};
if (_hrefRegex.hasMatch(e)) { if (hrefRegex.hasMatch(e)) {
attributes[AppFlowyRichTextKeys.href] = e; attributes[AppFlowyRichTextKeys.href] = e;
} }
return Delta()..insert(e, attributes: attributes); return Delta()..insert(e, attributes: attributes);
@ -45,7 +42,7 @@ extension PasteFromPlainText on EditorState {
if (selection == null || if (selection == null ||
!selection.isSingle || !selection.isSingle ||
selection.isCollapsed || selection.isCollapsed ||
!_hrefRegex.hasMatch(plainText)) { !hrefRegex.hasMatch(plainText)) {
return false; return false;
} }

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmbedImageUrlWidget extends StatefulWidget { class EmbedImageUrlWidget extends StatefulWidget {
const EmbedImageUrlWidget({ const EmbedImageUrlWidget({
@ -16,6 +18,7 @@ class EmbedImageUrlWidget extends StatefulWidget {
} }
class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> { class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
bool isUrlValid = true;
String inputText = ''; String inputText = '';
@override @override
@ -25,8 +28,15 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
FlowyTextField( FlowyTextField(
hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(), hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(),
onChanged: (value) => inputText = value, onChanged: (value) => inputText = value,
onEditingComplete: () => widget.onSubmit(inputText), onEditingComplete: submit,
), ),
if (!isUrlValid) ...[
const VSpace(8),
FlowyText(
LocaleKeys.document_plugins_cover_invalidImageUrl.tr(),
color: Theme.of(context).colorScheme.error,
),
],
const VSpace(8), const VSpace(8),
SizedBox( SizedBox(
width: 160, width: 160,
@ -37,10 +47,20 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
LocaleKeys.document_imageBlock_embedLink_label.tr(), LocaleKeys.document_imageBlock_embedLink_label.tr(),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
onTap: () => widget.onSubmit(inputText), onTap: submit,
), ),
), ),
], ],
); );
} }
void submit() {
if (checkUrlValidity(inputText)) {
return widget.onSubmit(inputText);
}
setState(() => isUrlValid = false);
}
bool checkUrlValidity(String url) => imgUrlRegex.hasMatch(url);
} }

View File

@ -1,12 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/util/google_font_family_extension.dart'; import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@ -0,0 +1,23 @@
const _trailingZerosPattern = r'^(\d+(?:\.\d*?[1-9](?=0|\b))?)\.?0*$';
final trailingZerosRegex = RegExp(_trailingZerosPattern);
const _hrefPattern =
r'https?://(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(?:/[^\s]*)?';
final hrefRegex = RegExp(_hrefPattern);
/// This pattern allows for both HTTP and HTTPS Scheme
/// It allows for query parameters
/// It only allows the following image extensions: .png, .jpg, .gif, .webm
///
const _imgUrlPattern =
r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.gif|.webm)(\?[^\s[",><]*)?';
final imgUrlRegex = RegExp(_imgUrlPattern);
const _appflowyCloudUrlPattern = r'^(https:\/\/)(.*)(\.appflowy\.cloud\/)(.*)';
final appflowyCloudUrlRegex = RegExp(_appflowyCloudUrlPattern);
const _camelCasePattern = '(?<=[a-z])[A-Z]';
final camelCaseRegex = RegExp(_camelCasePattern);
const _macOSVolumesPattern = '^/Volumes/[^/]+';
final macOSVolumesRegex = RegExp(_macOSVolumesPattern);

View File

@ -0,0 +1,19 @@
/// RegExp to match Twelve Hour formats
/// Source: https://stackoverflow.com/a/33906224
///
/// Matches eg: "05:05 PM", "5:50 Pm", "10:59 am", etc.
///
const _twelveHourTimePattern =
r'\b((1[0-2]|0?[1-9]):([0-5][0-9]) ([AaPp][Mm]))';
final twelveHourTimeRegex = RegExp(_twelveHourTimePattern);
bool isTwelveHourTime(String? time) => twelveHourTimeRegex.hasMatch(time ?? '');
/// RegExp to match Twenty Four Hour formats
/// Source: https://stackoverflow.com/a/7536768
///
/// Matches eg: "0:01", "04:59", "16:30", etc.
///
const _twentyFourHourtimePattern = r'^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$';
final tewentyFourHourTimeRegex = RegExp(_twentyFourHourtimePattern);
bool isTwentyFourHourTime(String? time) =>
tewentyFourHourTimeRegex.hasMatch(time ?? '');

View File

@ -1,7 +1,6 @@
import 'package:appflowy/shared/patterns/common_patterns.dart';
extension GoogleFontsParser on String { extension GoogleFontsParser on String {
String parseFontFamilyName() { String parseFontFamilyName() => replaceAll('_regular', '')
final camelCase = RegExp('(?<=[a-z])[A-Z]'); .replaceAllMapped(camelCaseRegex, (m) => ' ${m.group(0)}');
return replaceAll('_regular', '')
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
} }

View File

@ -2,6 +2,8 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
extension StringExtension on String { extension StringExtension on String {
static const _specialCharacters = r'\/:*?"<>| '; static const _specialCharacters = r'\/:*?"<>| ';
@ -31,8 +33,6 @@ extension StringExtension on String {
return null; return null;
} }
/// Returns if the string is a appflowy cloud url. /// Returns true if the string is a appflowy cloud url.
bool get isAppFlowyCloudUrl { bool get isAppFlowyCloudUrl => appflowyCloudUrlRegex.hasMatch(this);
return RegExp(r'^(https:\/\/)(.*)(\.appflowy\.cloud\/)(.*)').hasMatch(this);
}
} }

View File

@ -1,10 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import '../../../startup/tasks/prelude.dart'; import '../../../startup/tasks/prelude.dart';
@ -26,7 +28,7 @@ class ApplicationDataStorage {
if (Platform.isMacOS) { if (Platform.isMacOS) {
// remove the prefix `/Volumes/*` // remove the prefix `/Volumes/*`
path = path.replaceFirst(RegExp('^/Volumes/[^/]+'), ''); path = path.replaceFirst(macOSVolumesRegex, '');
} else if (Platform.isWindows) { } else if (Platform.isWindows) {
path = path.replaceAll('/', '\\'); path = path.replaceAll('/', '\\');
} }

View File

@ -1,18 +0,0 @@
/// RegExp to match Twelve Hour formats
/// Source: https://stackoverflow.com/a/33906224
///
/// Matches eg: "05:05 PM", "5:50 Pm", "10:59 am", etc.
///
final _twelveHourTimePattern =
RegExp(r'\b((1[0-2]|0?[1-9]):([0-5][0-9]) ([AaPp][Mm]))');
bool isTwelveHourTime(String? time) =>
_twelveHourTimePattern.hasMatch(time ?? '');
/// RegExp to match Twenty Four Hour formats
/// Source: https://stackoverflow.com/a/7536768
///
/// Matches eg: "0:01", "04:59", "16:30", etc.
///
final _twentyFourHourtimePattern = RegExp(r'^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$');
bool isTwentyFourHourTime(String? time) =>
_twentyFourHourtimePattern.hasMatch(time ?? '');

View File

@ -95,7 +95,7 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
return FlowySettingValueDropDown( return FlowySettingValueDropDown(
popoverKey: ThemeFontFamilySetting.popoverKey, popoverKey: ThemeFontFamilySetting.popoverKey,
popoverController: widget.popoverController, popoverController: widget.popoverController,
currentValue: parseFontFamilyName(widget.currentFontFamily), currentValue: widget.currentFontFamily.parseFontFamilyName(),
onClose: () { onClose: () {
query.value = ''; query.value = '';
widget.onClose?.call(); widget.onClose?.call();
@ -162,18 +162,11 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
); );
} }
String parseFontFamilyName(String fontFamilyName) {
final camelCase = RegExp('(?<=[a-z])[A-Z]');
return fontFamilyName
.replaceAll('_regular', '')
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
Widget _fontFamilyItemButton( Widget _fontFamilyItemButton(
BuildContext context, BuildContext context,
TextStyle style, TextStyle style,
) { ) {
final buttonFontFamily = parseFontFamilyName(style.fontFamily!); final buttonFontFamily = style.fontFamily!.parseFontFamilyName();
return Tooltip( return Tooltip(
message: buttonFontFamily, message: buttonFontFamily,
@ -184,21 +177,19 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
child: FlowyButton( child: FlowyButton(
onHover: (_) => FocusScope.of(context).unfocus(), onHover: (_) => FocusScope.of(context).unfocus(),
text: FlowyText.medium( text: FlowyText.medium(
parseFontFamilyName(style.fontFamily!), buttonFontFamily,
fontFamily: style.fontFamily!, fontFamily: style.fontFamily!,
), ),
rightIcon: rightIcon:
buttonFontFamily == parseFontFamilyName(widget.currentFontFamily) buttonFontFamily == widget.currentFontFamily.parseFontFamilyName()
? const FlowySvg( ? const FlowySvg(FlowySvgs.check_s)
FlowySvgs.check_s,
)
: null, : null,
onTap: () { onTap: () {
if (widget.onFontFamilyChanged != null) { if (widget.onFontFamilyChanged != null) {
widget.onFontFamilyChanged!(style.fontFamily!); widget.onFontFamilyChanged!(style.fontFamily!);
} else { } else {
final fontFamily = style.fontFamily!.parseFontFamilyName(); final fontFamily = style.fontFamily!.parseFontFamilyName();
if (parseFontFamilyName(widget.currentFontFamily) != if (widget.currentFontFamily.parseFontFamilyName() !=
buttonFontFamily) { buttonFontFamily) {
context context
.read<AppearanceSettingsCubit>() .read<AppearanceSettingsCubit>()