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:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.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_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_editor/appflowy_editor.dart' hide Log;
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_test/flutter_test.dart';
@ -132,8 +133,7 @@ class EditorOperations {
of: find.byType(EmbedImageUrlWidget),
matching: find.byType(TextField),
);
final textField = tester.widget<TextField>(imageUrlTextField);
textField.controller?.text = imageUrl;
await tester.enterText(imageUrlTextField, imageUrl);
await tester.pumpAndSettle();
await tester.tapButton(
find.descendant(

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.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/widgets.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
@ -23,9 +25,7 @@ class FontPickerScreen extends StatelessWidget {
}
class LanguagePickerPage extends StatefulWidget {
const LanguagePickerPage({
super.key,
});
const LanguagePickerPage({super.key});
@override
State<LanguagePickerPage> createState() => _LanguagePickerPageState();
@ -52,6 +52,7 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
body: SafeArea(
child: Scrollbar(
child: ListView.builder(
itemCount: availableFonts.length + 1, // with search bar
itemBuilder: (context, index) {
if (index == 0) {
// search bar
@ -65,7 +66,8 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
setState(() {
availableFonts = _availableFonts
.where(
(element) => parseFontFamilyName(element)
(font) => font
.parseFontFamilyName()
.toLowerCase()
.contains(keyword.toLowerCase()),
)
@ -75,8 +77,9 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
),
);
}
final fontFamilyName = availableFonts[index - 1];
final displayName = parseFontFamilyName(fontFamilyName);
final displayName = fontFamilyName.parseFontFamilyName();
return FlowyOptionTile.checkbox(
text: displayName,
isSelected: selectedFontFamilyName == fontFamilyName,
@ -86,17 +89,9 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
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_type_item.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/field_entities.pbenum.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) {
final regex = RegExp(r'^(\d+(?:\.\d*?[1-9](?=0|\b))?)\.?0*$');
if (regex.hasMatch(value)) {
final match = regex.firstMatch(value)!;
if (trailingZerosRegex.hasMatch(value)) {
final match = trailingZerosRegex.firstMatch(value)!;
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/shared/patterns/common_patterns.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 {
Future<void> pastePlainText(String plainText) async {
if (await pasteHtmlIfAvailable(plainText)) {
@ -23,7 +20,7 @@ extension PasteFromPlainText on EditorState {
.map((e) {
// parse the url content
final Attributes attributes = {};
if (_hrefRegex.hasMatch(e)) {
if (hrefRegex.hasMatch(e)) {
attributes[AppFlowyRichTextKeys.href] = e;
}
return Delta()..insert(e, attributes: attributes);
@ -45,7 +42,7 @@ extension PasteFromPlainText on EditorState {
if (selection == null ||
!selection.isSingle ||
selection.isCollapsed ||
!_hrefRegex.hasMatch(plainText)) {
!hrefRegex.hasMatch(plainText)) {
return false;
}

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmbedImageUrlWidget extends StatefulWidget {
const EmbedImageUrlWidget({
@ -16,6 +18,7 @@ class EmbedImageUrlWidget extends StatefulWidget {
}
class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
bool isUrlValid = true;
String inputText = '';
@override
@ -25,8 +28,15 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
FlowyTextField(
hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(),
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),
SizedBox(
width: 160,
@ -37,10 +47,20 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
LocaleKeys.document_imageBlock_embedLink_label.tr(),
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 'package:flutter/material.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/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.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 {
String parseFontFamilyName() {
final camelCase = RegExp('(?<=[a-z])[A-Z]');
return replaceAll('_regular', '')
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
String parseFontFamilyName() => replaceAll('_regular', '')
.replaceAllMapped(camelCaseRegex, (m) => ' ${m.group(0)}');
}

View File

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

View File

@ -1,10 +1,12 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:appflowy/core/config/kv.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_backend/log.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import '../../../startup/tasks/prelude.dart';
@ -26,7 +28,7 @@ class ApplicationDataStorage {
if (Platform.isMacOS) {
// remove the prefix `/Volumes/*`
path = path.replaceFirst(RegExp('^/Volumes/[^/]+'), '');
path = path.replaceFirst(macOSVolumesRegex, '');
} else if (Platform.isWindows) {
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(
popoverKey: ThemeFontFamilySetting.popoverKey,
popoverController: widget.popoverController,
currentValue: parseFontFamilyName(widget.currentFontFamily),
currentValue: widget.currentFontFamily.parseFontFamilyName(),
onClose: () {
query.value = '';
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(
BuildContext context,
TextStyle style,
) {
final buttonFontFamily = parseFontFamilyName(style.fontFamily!);
final buttonFontFamily = style.fontFamily!.parseFontFamilyName();
return Tooltip(
message: buttonFontFamily,
@ -184,21 +177,19 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
child: FlowyButton(
onHover: (_) => FocusScope.of(context).unfocus(),
text: FlowyText.medium(
parseFontFamilyName(style.fontFamily!),
buttonFontFamily,
fontFamily: style.fontFamily!,
),
rightIcon:
buttonFontFamily == parseFontFamilyName(widget.currentFontFamily)
? const FlowySvg(
FlowySvgs.check_s,
)
buttonFontFamily == widget.currentFontFamily.parseFontFamilyName()
? const FlowySvg(FlowySvgs.check_s)
: null,
onTap: () {
if (widget.onFontFamilyChanged != null) {
widget.onFontFamilyChanged!(style.fontFamily!);
} else {
final fontFamily = style.fontFamily!.parseFontFamilyName();
if (parseFontFamilyName(widget.currentFontFamily) !=
if (widget.currentFontFamily.parseFontFamilyName() !=
buttonFontFamily) {
context
.read<AppearanceSettingsCubit>()