From 6f69f1935c7d0d4c4e8ffee4b2e20c1ffa81d77a Mon Sep 17 00:00:00 2001 From: Nilansh Bansal Date: Thu, 4 May 2023 10:49:45 +0530 Subject: [PATCH 1/5] fix: display name validation (server) (#22927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description > This PR adds a validation regex for display name, it allows for Accented characters such as (ä ö ü è ß) etc, and alphanumeric with some special characters dot (.), apostrophe ('), hyphen (-) and spaces. > It also allows for chinese characters, eg. 황현미 Fixes #22578 ## Type of change - Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - JUnit ### Test Plan > Add Testsmith test cases links that relate to this PR ### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) ## Checklist: ### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --------- Co-authored-by: Anand Srinivasan (cherry picked from commit 3915334dd9ef185f95581165415af55212aff617) --- app/client/src/ce/constants/messages.ts | 3 ++ app/client/src/ce/sagas/userSagas.tsx | 12 +++++- .../server/services/ce/UserServiceCEImpl.java | 17 +++++++- .../server/services/UserServiceTest.java | 39 ++++++++++++++++--- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 4272db8b48..fdbabc79fa 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -171,6 +171,9 @@ export const USERS_HAVE_ACCESS_TO_ONLY_THIS_APP = () => "Users will only have access to this application"; export const NO_USERS_INVITED = () => "You haven't invited any users yet"; +export const UPDATE_USER_DETAILS_FAILED = () => + "Unable to update user details."; + export const CREATE_PASSWORD_RESET_SUCCESS = () => `Your password has been set`; export const CREATE_PASSWORD_RESET_SUCCESS_LOGIN_LINK = () => `Login`; diff --git a/app/client/src/ce/sagas/userSagas.tsx b/app/client/src/ce/sagas/userSagas.tsx index b0dfe22a86..70f2ce04a3 100644 --- a/app/client/src/ce/sagas/userSagas.tsx +++ b/app/client/src/ce/sagas/userSagas.tsx @@ -21,6 +21,7 @@ import UserApi from "@appsmith/api/UserApi"; import { AUTH_LOGIN_URL, SETUP } from "constants/routes"; import history from "utils/history"; import type { ApiResponse } from "api/ApiResponses"; +import type { ErrorActionPayload } from "sagas/ErrorSagas"; import { validateResponse, getResponseErrorMessage, @@ -72,6 +73,8 @@ import type { SegmentState } from "reducers/uiReducers/analyticsReducer"; import type FeatureFlags from "entities/FeatureFlags"; import UsagePulse from "usagePulse"; import { isAirgapped } from "@appsmith/utils/airgapHelpers"; +import { UPDATE_USER_DETAILS_FAILED } from "ce/constants/messages"; +import { createMessage } from "design-system-old/build/constants/messages"; export function* createUserSaga( action: ReduxActionWithPromise, @@ -382,9 +385,16 @@ export function* updateUserDetailsSaga(action: ReduxAction) { }); } } catch (error) { + const payload: ErrorActionPayload = { + show: true, + error: { + message: + (error as Error).message ?? createMessage(UPDATE_USER_DETAILS_FAILED), + }, + }; yield put({ type: ReduxActionErrorTypes.UPDATE_USER_DETAILS_ERROR, - payload: (error as Error).message, + payload, }); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java index fb2c5bf0a3..d7d5b79192 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java @@ -69,6 +69,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.regex.Pattern; import static com.appsmith.server.acl.AclPermission.MANAGE_USERS; import static com.appsmith.server.helpers.ValidationUtils.LOGIN_PASSWORD_MAX_LENGTH; @@ -100,6 +101,7 @@ public class UserServiceCEImpl extends BaseService private static final String FORGOT_PASSWORD_CLIENT_URL_FORMAT = "%s/user/resetPassword?token=%s"; private static final String INVITE_USER_CLIENT_URL_FORMAT = "%s/user/signup?email=%s"; public static final String INVITE_USER_EMAIL_TEMPLATE = "email/inviteUserTemplate.html"; + private static final Pattern ALLOWED_ACCENTED_CHARACTERS_PATTERN = Pattern.compile("^[\\p{L} 0-9 .\'\\-]+$"); @Autowired public UserServiceCEImpl(Scheduler scheduler, @@ -659,6 +661,14 @@ public class UserServiceCEImpl extends BaseService return Flux.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); } + private boolean validateName(String name){ + /* + Regex allows for Accented characters and alphanumeric with some special characters dot (.), apostrophe ('), + hyphen (-) and spaces + */ + return ALLOWED_ACCENTED_CHARACTERS_PATTERN.matcher(name).matches(); + } + @Override public Mono updateCurrentUser(final UserUpdateDTO allUpdates, ServerWebExchange exchange) { List> monos = new ArrayList<>(); @@ -668,7 +678,12 @@ public class UserServiceCEImpl extends BaseService if (allUpdates.hasUserUpdates()) { final User updates = new User(); - updates.setName(allUpdates.getName()); + String inputName = allUpdates.getName(); + boolean isValidName = validateName(inputName); + if (!isValidName){ + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.NAME)); + } + updates.setName(inputName); updatedUserMono = sessionUserService.getCurrentUser() .flatMap(user -> update(user.getEmail(), updates, fieldName(QUser.user.email)) diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserServiceTest.java index bd3bbcb86c..821a6ed979 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserServiceTest.java @@ -415,12 +415,39 @@ public class UserServiceTest { @WithUserDetails(value = "api_user") public void updateNameOfUser() { UserUpdateDTO updateUser = new UserUpdateDTO(); - updateUser.setName("New name of api_user"); + updateUser.setName("New name of api user"); StepVerifier.create(userService.updateCurrentUser(updateUser, null)) .assertNext(user -> { assertNotNull(user); - assertThat(user.getEmail()).isEqualTo("api_user"); - assertThat(user.getName()).isEqualTo("New name of api_user"); + assertEquals("api_user", user.getEmail()); + assertEquals("New name of api user", user.getName()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void updateNameOfUser_WithNotAllowedSpecialCharacter_InvalidName() { + UserUpdateDTO updateUser = new UserUpdateDTO(); + updateUser.setName("invalid name@symbol"); + StepVerifier.create(userService.updateCurrentUser(updateUser, null)) + .expectErrorMatches(throwable -> + throwable instanceof AppsmithException + && + throwable.getMessage().contains(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.NAME)) + ) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void updateNameOfUser_WithAccentedCharacters_IsValid() { + UserUpdateDTO updateUser = new UserUpdateDTO(); + updateUser.setName("ä ö ü è ß Test . '- ðƒ 你好 123'"); + StepVerifier.create(userService.updateCurrentUser(updateUser, null)) + .assertNext(user -> { + assertNotNull(user); + assertEquals("ä ö ü è ß Test . '- ðƒ 你好 123'", user.getName()); }) .verifyComplete(); } @@ -491,9 +518,9 @@ public class UserServiceTest { final UserData userData = tuple.getT2(); assertNotNull(user); assertNotNull(userData); - assertThat(user.getName()).isEqualTo("New name of user here"); - assertThat(userData.getRole()).isEqualTo("New role of user"); - assertThat(userData.getUseCase()).isEqualTo("New use case"); + assertEquals("New name of user here", user.getName()); + assertEquals("New role of user", userData.getRole()); + assertEquals("New use case", userData.getUseCase()); }) .verifyComplete(); } From 60bcb1521d17ac51b3556a7b2050d1d6ccadabf2 Mon Sep 17 00:00:00 2001 From: Anand Srinivasan <66776129+eco-monk@users.noreply.github.com> Date: Thu, 4 May 2023 18:29:57 +0530 Subject: [PATCH 2/5] fix: display name validation (client) (#22938) (cherry picked from commit 6e39a3a5ec134ae2cf001670974f96f53b62148b) --- app/client/package.json | 1 + app/client/src/ce/constants/messages.ts | 8 ++ app/client/src/constants/Regex.ts | 5 ++ app/client/src/pages/UserProfile/General.tsx | 81 +++++++++++++++----- app/client/typings/js-regex-pl/index.d.ts | 1 + app/client/yarn.lock | 10 +++ 6 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 app/client/src/constants/Regex.ts create mode 100644 app/client/typings/js-regex-pl/index.d.ts diff --git a/app/client/package.json b/app/client/package.json index d0883890f4..c752e88749 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -98,6 +98,7 @@ "interweave-autolink": "^4.4.2", "jest-preview": "^0.3.1", "js-beautify": "^1.14.0", + "js-regex-pl": "^1.0.1", "js-sha256": "^0.9.0", "jshint": "^2.13.4", "klona": "^2.0.5", diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index fdbabc79fa..9104c8f2f2 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -173,6 +173,14 @@ export const NO_USERS_INVITED = () => "You haven't invited any users yet"; export const UPDATE_USER_DETAILS_FAILED = () => "Unable to update user details."; +export const USER_DISPLAY_PICTURE_FILE_INVALID = () => + "File content doesn't seem to be an image. Please verify."; +export const USER_DISPLAY_NAME_CHAR_CHECK_FAILED = () => + "No special characters allowed except .'-"; +export const USER_DISPLAY_NAME_PLACEHOLDER = () => "Display name"; +export const USER_DISPLAY_PICTURE_PLACEHOLDER = () => "Display picture"; +export const USER_EMAIL_PLACEHOLDER = () => "Email"; +export const USER_RESET_PASSWORD = () => "Reset Password"; export const CREATE_PASSWORD_RESET_SUCCESS = () => `Your password has been set`; export const CREATE_PASSWORD_RESET_SUCCESS_LOGIN_LINK = () => `Login`; diff --git a/app/client/src/constants/Regex.ts b/app/client/src/constants/Regex.ts new file mode 100644 index 0000000000..cf76772395 --- /dev/null +++ b/app/client/src/constants/Regex.ts @@ -0,0 +1,5 @@ +import pL from "js-regex-pl"; + +/* ref: https://github.com/yury-dymov/js-regex-pl/blob/master/src/index.js + includes support for other languages (e.g. latin, chinese, japanese etc..) */ +export const ALL_LANGUAGE_CHARACTERS_REGEX = `${pL}`; diff --git a/app/client/src/pages/UserProfile/General.tsx b/app/client/src/pages/UserProfile/General.tsx index ff418ad5ad..3b6e66c9ae 100644 --- a/app/client/src/pages/UserProfile/General.tsx +++ b/app/client/src/pages/UserProfile/General.tsx @@ -1,6 +1,5 @@ -import React from "react"; +import React, { useState } from "react"; import styled from "styled-components"; -import { debounce } from "lodash"; import { notEmptyValidator, Text, @@ -14,13 +13,20 @@ import { getCurrentUser } from "selectors/usersSelectors"; import { forgotPasswordSubmitHandler } from "pages/UserAuth/helpers"; import { FORGOT_PASSWORD_SUCCESS_TEXT, - createMessage, + USER_DISPLAY_NAME_CHAR_CHECK_FAILED, + USER_DISPLAY_NAME_PLACEHOLDER, + USER_DISPLAY_PICTURE_PLACEHOLDER, + USER_EMAIL_PLACEHOLDER, + USER_RESET_PASSWORD, } from "@appsmith/constants/messages"; import { logoutUser, updateUserDetails } from "actions/userActions"; import UserProfileImagePicker from "./UserProfileImagePicker"; import { Wrapper, FieldWrapper, LabelWrapper } from "./StyledComponents"; import { getAppsmithConfigs } from "@appsmith/configs"; import { ANONYMOUS_USERNAME } from "constants/userConstants"; +import { ALL_LANGUAGE_CHARACTERS_REGEX } from "constants/Regex"; +import { createMessage } from "design-system-old/build/constants/messages"; + const { disableLoginForm } = getAppsmithConfigs(); const ForgotPassword = styled.a` @@ -33,8 +39,31 @@ const ForgotPassword = styled.a` display: inline-block; `; +const nameValidator = ( + value: string, +): { + isValid: boolean; + message: string; +} => { + const notEmpty = notEmptyValidator(value); + if (!notEmpty.isValid) { + return notEmpty; + } + if (!new RegExp(`^[${ALL_LANGUAGE_CHARACTERS_REGEX} 0-9.'-]+$`).test(value)) { + return { + isValid: false, + message: createMessage(USER_DISPLAY_NAME_CHAR_CHECK_FAILED), + }; + } + return { + isValid: true, + message: "", + }; +}; + function General() { const user = useSelector(getCurrentUser); + const [name, setName] = useState(user?.name); const dispatch = useDispatch(); const forgotPassword = async () => { try { @@ -51,15 +80,15 @@ function General() { }); } }; - - const timeout = 1000; - const onNameChange = debounce((newName: string) => { - dispatch( - updateUserDetails({ - name: newName, - }), - ); - }, timeout); + const saveName = () => { + name && + nameValidator(name).isValid && + dispatch( + updateUserDetails({ + name, + }), + ); + }; if (user?.email === ANONYMOUS_USERNAME) return null; @@ -67,37 +96,49 @@ function General() { - Display Picture + + {createMessage(USER_DISPLAY_PICTURE_PLACEHOLDER)} + - Display name + + {createMessage(USER_DISPLAY_NAME_PLACEHOLDER)} + {
{ + if (ev.key === "Enter") { + saveName(); + } + }} + placeholder={createMessage(USER_DISPLAY_NAME_PLACEHOLDER)} + validator={nameValidator} />
}
- Email + + {createMessage(USER_EMAIL_PLACEHOLDER)} +
{{user?.email}} {!disableLoginForm && ( - Reset Password + {createMessage(USER_RESET_PASSWORD)} )}
diff --git a/app/client/typings/js-regex-pl/index.d.ts b/app/client/typings/js-regex-pl/index.d.ts new file mode 100644 index 0000000000..649576009c --- /dev/null +++ b/app/client/typings/js-regex-pl/index.d.ts @@ -0,0 +1 @@ +declare module "js-regex-pl"; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 401d6a8282..eb0db9892c 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -8692,6 +8692,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classnames@*: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + classnames@2.x, classnames@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz" @@ -14298,6 +14303,11 @@ js-levenshtein@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz" +js-regex-pl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/js-regex-pl/-/js-regex-pl-1.0.1.tgz#a0e4d76c7c2b4a503b17bc6b32ce8c5b0e39bca1" + integrity sha512-Cj291//fFojnJrYxh46mhK1jINe4F4WXk+/FNGaPy+2q61pKRX0n9LyY3rtZNlH06s41DtoNVh9UR7bTeuAhGg== + js-sdsl@^4.1.4: version "4.3.0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" From 37843c131c421feb2317e253ac0ee3afee82cc20 Mon Sep 17 00:00:00 2001 From: Anand Srinivasan Date: Wed, 10 May 2023 15:25:09 +0530 Subject: [PATCH 3/5] fix: display picture - prevent invalid files (#22900) When selecting a display picture, show alert on selecting invalid file type. e.g. when you rename a pdf file to png and try to upload it Design system changes: https://github.com/appsmithorg/design-system/pull/466 Fixes #22592 Media https://www.loom.com/share/f8971769aaad422fb86c6b02306102ce - Bug fix (non-breaking change which fixes an issue) - Cypress - Manual > Add Testsmith test cases links that relate to this PR > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --- .../cypress/fixtures/Files/invalid-image.png | 198 ++++++++++++++++++ .../cypress/fixtures/Files/valid-image.jpeg | Bin 0 -> 13947 bytes .../UserProfile/UpdateUserPicture_spec.js | 56 +++++ .../UpdateUsersName_spec.js | 0 .../cypress/support/Objects/CommonLocators.ts | 6 + app/client/cypress/support/Pages/HomePage.ts | 15 ++ app/client/package.json | 2 +- .../src/ce/constants/ReduxActionConstants.tsx | 1 + .../UserProfile/UserProfileImagePicker.tsx | 17 ++ app/client/yarn.lock | 8 +- 10 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 app/client/cypress/fixtures/Files/invalid-image.png create mode 100644 app/client/cypress/fixtures/Files/valid-image.jpeg create mode 100644 app/client/cypress/integration/Regression_TestSuite/ClientSideTests/UserProfile/UpdateUserPicture_spec.js rename app/client/cypress/integration/Regression_TestSuite/ClientSideTests/{ExplorerTests => UserProfile}/UpdateUsersName_spec.js (100%) diff --git a/app/client/cypress/fixtures/Files/invalid-image.png b/app/client/cypress/fixtures/Files/invalid-image.png new file mode 100644 index 0000000000..dbf091df9a --- /dev/null +++ b/app/client/cypress/fixtures/Files/invalid-image.png @@ -0,0 +1,198 @@ +%PDF-1.3 +%âãÏÓ + +1 0 obj +<< +/Type /Catalog +/Outlines 2 0 R +/Pages 3 0 R +>> +endobj + +2 0 obj +<< +/Type /Outlines +/Count 0 +>> +endobj + +3 0 obj +<< +/Type /Pages +/Count 2 +/Kids [ 4 0 R 6 0 R ] +>> +endobj + +4 0 obj +<< +/Type /Page +/Parent 3 0 R +/Resources << +/Font << +/F1 9 0 R +>> +/ProcSet 8 0 R +>> +/MediaBox [0 0 612.0000 792.0000] +/Contents 5 0 R +>> +endobj + +5 0 obj +<< /Length 1074 >> +stream +2 J +BT +0 0 0 rg +/F1 0027 Tf +57.3750 722.2800 Td +( A Simple PDF File ) Tj +ET +BT +/F1 0010 Tf +69.2500 688.6080 Td +( This is a small demonstration .pdf file - ) Tj +ET +BT +/F1 0010 Tf +69.2500 664.7040 Td +( just for use in the Virtual Mechanics tutorials. More text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 652.7520 Td +( text. And more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 628.8480 Td +( And more text. And more text. And more text. And more text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 616.8960 Td +( text. And more text. Boring, zzzzz. And more text. And more text. And ) Tj +ET +BT +/F1 0010 Tf +69.2500 604.9440 Td +( more text. And more text. And more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 592.9920 Td +( And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 569.0880 Td +( And more text. And more text. And more text. And more text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 557.1360 Td +( text. And more text. And more text. Even more. Continued on page 2 ...) Tj +ET +endstream +endobj + +6 0 obj +<< +/Type /Page +/Parent 3 0 R +/Resources << +/Font << +/F1 9 0 R +>> +/ProcSet 8 0 R +>> +/MediaBox [0 0 612.0000 792.0000] +/Contents 7 0 R +>> +endobj + +7 0 obj +<< /Length 676 >> +stream +2 J +BT +0 0 0 rg +/F1 0027 Tf +57.3750 722.2800 Td +( Simple PDF File 2 ) Tj +ET +BT +/F1 0010 Tf +69.2500 688.6080 Td +( ...continued from page 1. Yet more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 676.6560 Td +( And more text. And more text. And more text. And more text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 664.7040 Td +( text. Oh, how boring typing this stuff. But not as boring as watching ) Tj +ET +BT +/F1 0010 Tf +69.2500 652.7520 Td +( paint dry. And more text. And more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 640.8000 Td +( Boring. More, a little more text. The end, and just as well. ) Tj +ET +endstream +endobj + +8 0 obj +[/PDF /Text] +endobj + +9 0 obj +<< +/Type /Font +/Subtype /Type1 +/Name /F1 +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +>> +endobj + +10 0 obj +<< +/Creator (Rave \(http://www.nevrona.com/rave\)) +/Producer (Nevrona Designs) +/CreationDate (D:20060301072826) +>> +endobj + +xref +0 11 +0000000000 65535 f +0000000019 00000 n +0000000093 00000 n +0000000147 00000 n +0000000222 00000 n +0000000390 00000 n +0000001522 00000 n +0000001690 00000 n +0000002423 00000 n +0000002456 00000 n +0000002574 00000 n + +trailer +<< +/Size 11 +/Root 1 0 R +/Info 10 0 R +>> + +startxref +2714 +%%EOF diff --git a/app/client/cypress/fixtures/Files/valid-image.jpeg b/app/client/cypress/fixtures/Files/valid-image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..54665a85ea11eb03172dc6f8acd9d7f3c9713777 GIT binary patch literal 13947 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 { + _.homePage.GotoEditProfile(); + + _.agHelper.GetText(_.locators._ds_imageSelector_label).then((text) => { + text === "Remove" && + _.agHelper.GetNClick(_.locators._ds_imageSelector_label); + }); + + // API is finished even before wait begins + // cy.intercept("GET", "/api/v1/users/photo", { + // body: { responseMeta: { status: 200, success: true }, data: {} }, + // }).as("savePhoto"); + }); + + it("1. Update a user's picture with valid file", function () { + _.agHelper.GetNClick(_.locators._ds_imageSelector); + _.agHelper.GetElement(_.locators._ds_uppy_fileInput).as("fileInput"); + + cy.fixture("Files/valid-image.jpeg").then((fileContent) => { + _.agHelper.GetElement("@fileInput").attachFile({ + fileContent: fileContent.toString(), + fileName: "valid-image.jpeg", + mimeType: "image/jpeg", + encoding: "base64", + }); + + _.agHelper.GetNClick(_.locators._ds_uppy_crop_confirm); + _.agHelper.GetNClick(_.locators._ds_uppy_upload_btn); + // API is finished even before wait begins + // cy.wait("@savePhoto"); + _.agHelper.AssertElementExist(".image-view img"); + }); + }); + + it("2. Invalid file throws error", function () { + _.agHelper.GetNClick(_.locators._ds_imageSelector); + _.agHelper.GetElement(_.locators._ds_uppy_fileInput).as("fileInput"); + + cy.fixture("Files/invalid-image.png").then((fileContent) => { + _.agHelper.GetElement("@fileInput").attachFile({ + fileContent: fileContent.toString(), + fileName: "invalid-image.png", + mimeType: "image/png", + encoding: "base64", + }); + + _.agHelper.ValidateToastMessage( + "File content doesn't seem to be an image. Please verify.", + ); + }); + }); +}); diff --git a/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/ExplorerTests/UpdateUsersName_spec.js b/app/client/cypress/integration/Regression_TestSuite/ClientSideTests/UserProfile/UpdateUsersName_spec.js similarity index 100% rename from app/client/cypress/integration/Regression_TestSuite/ClientSideTests/ExplorerTests/UpdateUsersName_spec.js rename to app/client/cypress/integration/Regression_TestSuite/ClientSideTests/UserProfile/UpdateUsersName_spec.js diff --git a/app/client/cypress/support/Objects/CommonLocators.ts b/app/client/cypress/support/Objects/CommonLocators.ts index 0d8ff0db99..95fc95ffad 100644 --- a/app/client/cypress/support/Objects/CommonLocators.ts +++ b/app/client/cypress/support/Objects/CommonLocators.ts @@ -180,4 +180,10 @@ export class CommonLocators { _canvas = "[data-testid=widgets-editor]"; _enterPreviewMode = "[data-cy='edit-mode']"; _exitPreviewMode = "[data-cy='preview-mode']"; + + _ds_imageSelector = ".ads-dialog-trigger"; + _ds_imageSelector_label = ".ads-dialog-trigger .label"; + _ds_uppy_fileInput = ".uppy-Dashboard-input"; + _ds_uppy_crop_confirm = ".uppy-ImageCropper-controls .uppy-c-btn"; + _ds_uppy_upload_btn = ".uppy-StatusBar-actionBtn--upload"; } diff --git a/app/client/cypress/support/Pages/HomePage.ts b/app/client/cypress/support/Pages/HomePage.ts index c61cf31b4d..718531d600 100644 --- a/app/client/cypress/support/Pages/HomePage.ts +++ b/app/client/cypress/support/Pages/HomePage.ts @@ -38,6 +38,7 @@ export class HomePage { role + "']"; private _profileMenu = ".t--profile-menu"; + private _editProfileMenu = ".t--edit-profile"; private _signout = ".t--logout-icon"; _searchUsersInput = ".search-input"; @@ -272,6 +273,20 @@ export class HomePage { this.agHelper.Sleep(); //for logout to complete! } + public GotoProfileMenu() { + this.agHelper.GetNClick(this._profileMenu); + } + + public GotoEditProfile() { + cy.location().then((loc) => { + if (loc.pathname !== "/profile") { + this.NavigateToHome(); + this.GotoProfileMenu(); + this.agHelper.GetNClick(this._editProfileMenu); + } + }); + } + public LogintoApp( uname: string, pswd: string, diff --git a/app/client/package.json b/app/client/package.json index c752e88749..31d369523c 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -79,7 +79,7 @@ "cypress-log-to-output": "^1.1.2", "dayjs": "^1.10.6", "deep-diff": "^1.0.2", - "design-system-old": "npm:@appsmithorg/design-system-old@1.1.5", + "design-system-old": "npm:@appsmithorg/design-system-old@1.1.6", "downloadjs": "^1.4.7", "exceljs": "^4.3.0", "fast-deep-equal": "^3.1.3", diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index c0ffff9aa0..fa8b6c26c9 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -984,6 +984,7 @@ export const ReduxActionErrorTypes = { INSTALL_LIBRARY_FAILED: "INSTALL_LIBRARY_FAILED", UNINSTALL_LIBRARY_FAILED: "UNINSTALL_LIBRARY_FAILED", FETCH_JS_LIBRARIES_FAILED: "FETCH_JS_LIBRARIES_FAILED", + USER_IMAGE_INVALID_FILE_CONTENT: "USER_IMAGE_INVALID_FILE_CONTENT", }; export const ReduxFormActionTypes = { diff --git a/app/client/src/pages/UserProfile/UserProfileImagePicker.tsx b/app/client/src/pages/UserProfile/UserProfileImagePicker.tsx index 09ba40575f..01cacb3065 100644 --- a/app/client/src/pages/UserProfile/UserProfileImagePicker.tsx +++ b/app/client/src/pages/UserProfile/UserProfileImagePicker.tsx @@ -7,6 +7,9 @@ import { USER_PHOTO_ASSET_URL } from "constants/userConstants"; import { DisplayImageUpload } from "design-system-old"; import type Uppy from "@uppy/core"; +import { ReduxActionErrorTypes } from "ce/constants/ReduxActionConstants"; +import type { ErrorActionPayload } from "sagas/ErrorSagas"; +import { USER_DISPLAY_PICTURE_FILE_INVALID } from "ce/constants/messages"; function FormDisplayImage() { const [file, setFile] = useState(); @@ -51,9 +54,23 @@ function FormDisplayImage() { ); }; + const onFileTypeInvalid = () => { + const payload: ErrorActionPayload = { + show: true, + error: { + message: USER_DISPLAY_PICTURE_FILE_INVALID(), + }, + }; + dispatch({ + type: ReduxActionErrorTypes.USER_IMAGE_INVALID_FILE_CONTENT, + payload, + }); + }; + return ( Date: Thu, 4 May 2023 15:39:10 +0530 Subject: [PATCH 4/5] fix: advanced filetype validation (#22808) ## Description > This PR adds a second layer validation to the basic mime type validation that's in place which checks the file extension. > If the `ImageIO.read()` library returns null for .png/.jpeg type files, it indicates a corrupted file upload, hence it returns VALIDATION_FAILURE. Fixes #22592 ## Type of change - Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - JUnit ## Checklist: ### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --------- Co-authored-by: Anand Srinivasan --- .../src/ce/constants/ReduxActionConstants.tsx | 1 + app/client/src/ce/constants/messages.ts | 2 + app/client/src/ce/sagas/userSagas.tsx | 17 ++ .../services/ce/AssetServiceCEImpl.java | 28 ++- .../server/services/UserDataServiceTest.java | 18 ++ .../WorkspaceServiceTest/json_file_to_png.png | 166 ++++++++++++++++++ 6 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 app/server/appsmith-server/src/test/resources/test_assets/WorkspaceServiceTest/json_file_to_png.png diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index fa8b6c26c9..ed34c58443 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -984,6 +984,7 @@ export const ReduxActionErrorTypes = { INSTALL_LIBRARY_FAILED: "INSTALL_LIBRARY_FAILED", UNINSTALL_LIBRARY_FAILED: "UNINSTALL_LIBRARY_FAILED", FETCH_JS_LIBRARIES_FAILED: "FETCH_JS_LIBRARIES_FAILED", + USER_PROFILE_PICTURE_UPLOAD_FAILED: "USER_PROFILE_PICTURE_UPLOAD_FAILED", USER_IMAGE_INVALID_FILE_CONTENT: "USER_IMAGE_INVALID_FILE_CONTENT", }; diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 9104c8f2f2..6cbebdf981 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -171,6 +171,8 @@ export const USERS_HAVE_ACCESS_TO_ONLY_THIS_APP = () => "Users will only have access to this application"; export const NO_USERS_INVITED = () => "You haven't invited any users yet"; +export const USER_PROFILE_PICTURE_UPLOAD_FAILED = () => + "Unable to upload display picture."; export const UPDATE_USER_DETAILS_FAILED = () => "Unable to update user details."; export const USER_DISPLAY_PICTURE_FILE_INVALID = () => diff --git a/app/client/src/ce/sagas/userSagas.tsx b/app/client/src/ce/sagas/userSagas.tsx index 70f2ce04a3..d779835bd5 100644 --- a/app/client/src/ce/sagas/userSagas.tsx +++ b/app/client/src/ce/sagas/userSagas.tsx @@ -73,6 +73,7 @@ import type { SegmentState } from "reducers/uiReducers/analyticsReducer"; import type FeatureFlags from "entities/FeatureFlags"; import UsagePulse from "usagePulse"; import { isAirgapped } from "@appsmith/utils/airgapHelpers"; +import { USER_PROFILE_PICTURE_UPLOAD_FAILED } from "ce/constants/messages"; import { UPDATE_USER_DETAILS_FAILED } from "ce/constants/messages"; import { createMessage } from "design-system-old/build/constants/messages"; @@ -486,11 +487,27 @@ export function* updatePhoto( const response: ApiResponse = yield call(UserApi.uploadPhoto, { file: action.payload.file, }); + if (!response.responseMeta.success) { + throw response.responseMeta.error; + } //@ts-expect-error: response is of type unknown const photoId = response.data?.profilePhotoAssetId; //get updated photo id of iploaded image if (action.payload.callback) action.payload.callback(photoId); } catch (error) { log.error(error); + + const payload: ErrorActionPayload = { + show: true, + error: { + message: + (error as any).message ?? + createMessage(USER_PROFILE_PICTURE_UPLOAD_FAILED), + }, + }; + yield put({ + type: ReduxActionErrorTypes.USER_PROFILE_PICTURE_UPLOAD_FAILED, + payload, + }); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AssetServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AssetServiceCEImpl.java index a75a412200..409cc08f19 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AssetServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AssetServiceCEImpl.java @@ -46,11 +46,32 @@ public class AssetServiceCEImpl implements AssetServiceCE { MediaType.valueOf("image/vnd.microsoft.icon") ); + private static final Set ALLOWED_CONTENT_TYPES_STR = Set.of( + MediaType.IMAGE_JPEG_VALUE, + MediaType.IMAGE_PNG_VALUE + ); + @Override public Mono getById(String id) { return repository.findById(id); } + private Boolean checkImageTypeValidation(DataBuffer dataBuffer, MediaType contentType) throws IOException { + BufferedImage bufferedImage = ImageIO.read(dataBuffer.asInputStream()); + if (bufferedImage == null) { + /* + This is true for SVG and ICO images. + If ImageIO.read returns bufferedImage as null and the contentType file extension is .png or .jpeg which + means the file is not an image type file rather any other corrupted file but the extension has been + changed to .png or .jpeg to upload the flawed file. This is a security vulnerability hence reject + */ + if (ALLOWED_CONTENT_TYPES_STR.contains(contentType.toString())){ + return false; + } + } + return true; + } + @Override public Mono upload(List fileParts, int maxFileSizeKB, boolean isThumbnail) { fileParts = fileParts.stream().filter(Objects::nonNull).collect(Collectors.toList()); @@ -111,8 +132,13 @@ public class AssetServiceCEImpl implements AssetServiceCE { } private Asset createAsset(DataBuffer dataBuffer, MediaType srcContentType, boolean createThumbnail) throws IOException { - byte[] imageData = null; MediaType contentType = srcContentType; + Boolean isValidImage = checkImageTypeValidation(dataBuffer, contentType); + if (isValidImage != true) { + throw new AppsmithException(AppsmithError.VALIDATION_FAILURE, "Please upload a valid image. Only JPEG, PNG, SVG and ICO are allowed."); + } + + byte[] imageData = null; if (createThumbnail) { try { diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserDataServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserDataServiceTest.java index d1a93a9497..50ca8c8244 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserDataServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserDataServiceTest.java @@ -176,6 +176,24 @@ public class UserDataServiceTest { .expectErrorMatches(error -> error instanceof AppsmithException) .verify(); } + /* + This test uploads an invalid image (json file for which extension has been changed to .png) and validates the upload failure + */ + @Test + @WithUserDetails(value = "api_user") + public void testUploadProfilePhoto_invalidImageContent() { + FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); + Flux dataBufferFlux = DataBufferUtils + .read(new ClassPathResource("test_assets/WorkspaceServiceTest/json_file_to_png.png"), new DefaultDataBufferFactory(), 4096).cache(); + Mockito.when(filepart.content()).thenReturn(dataBufferFlux); + Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.IMAGE_PNG); + + final Mono saveMono = userDataService.saveProfilePhoto(filepart).cache(); + + StepVerifier.create(saveMono) + .expectErrorMatches(error -> error instanceof AppsmithException) + .verify(); + } @Test @WithUserDetails(value = "api_user") diff --git a/app/server/appsmith-server/src/test/resources/test_assets/WorkspaceServiceTest/json_file_to_png.png b/app/server/appsmith-server/src/test/resources/test_assets/WorkspaceServiceTest/json_file_to_png.png new file mode 100644 index 0000000000..96d09a9515 --- /dev/null +++ b/app/server/appsmith-server/src/test/resources/test_assets/WorkspaceServiceTest/json_file_to_png.png @@ -0,0 +1,166 @@ +{ + "newPage": { + "id": "testPageId", + "applicationId": "testApplicationId", + "defaultResources": { + "pageId" : "testPageId" + }, + "unpublishedPage": { + "name": "TestPage", + "layouts": [ + { + "id": "testLayoutId", + "viewMode": false, + "dsl": { + "widgetName": "MainContainer", + "children": [] + }, + "layoutOnLoadActions": [], + "widgetNames": [ + "MainContainer" + ], + "allOnPageLoadActionNames": [], + "actionsUsedInDynamicBindings": [], + "deleted": false, + "policies": [] + } + ] + }, + "publishedPage": { + "name": "TestPage", + "layouts": [ + { + "id": "testLayoutId", + "viewMode": false, + "dsl": { + "widgetName": "MainContainer", + "children": [] + }, + "widgetNames": [ + "MainContainer" + ], + "deleted": false, + "policies": [] + } + ] + } + }, + "actionCollectionWithAction": { + "id": "testCollectionId", + "applicationId": "testApplicationId", + "workspaceId": "testWorkspaceId", + "unpublishedCollection": { + "name": "testCollection", + "pageId": "testPageId", + "pluginId": "testPluginId", + "defaultToBranchedActionIdsMap": [ + { + "defaultTestActionId1": "testActionId1" + }, + { + "defaultTestActionId2": "testActionId2" + } + ], + "actions": [ + { + "id": "testActionId1" + }, + { + "id": "testActionId2" + } + ] + }, + "publishedCollection": { + "name": "testCollection", + "pageId": "testPageId", + "pluginId": "testPluginId", + "defaultToBranchedActionIdsMap": [ + { + "defaultTestActionId1": "testActionId1" + } + ] + } + }, + "actionCollectionDTOWithModifiedActions": { + "id": "testCollectionId", + "applicationId": "testApplicationId", + "workspaceId": "testWorkspaceId", + "name": "testCollection", + "pageId": "testPageId", + "pluginId": "testPluginId", + "defaultToBranchedActionIdsMap": [ + { + "defaultTestActionId1": "testActionId1" + }, + { + "defaultTestActionId3": "testActionId3" + } + ], + "defaultToBranchedArchivedActionIdsMap": [ + { + "defaultTestActionId2": "testActionId2" + } + ], + "actions": [ + { + "id": "testActionId1" + }, + { + "id": "testActionId3" + } + ], + "archivedActions": [ + { + "id": "testActionId2" + } + ] + }, + "actionCollectionAfterModifiedActions": { + "id": "testCollectionId", + "applicationId": "testApplicationId", + "workspaceId": "testWorkspaceId", + "unpublishedCollection": { + "id": "testCollectionId", + "workspaceId": "testWorkspaceId", + "name": "testCollection", + "pageId": "testPageId", + "pluginId": "testPluginId", + "defaultToBranchedActionIdsMap": [ + { + "defaultTestActionId1": "testActionId1" + }, + { + "defaultTestActionId3": "testActionId3" + } + ], + "defaultToBranchedArchivedActionIdsMap": [ + { + "defaultTestActionId2": "testActionId2" + } + ], + "actions": [ + { + "id": "testActionId1" + }, + { + "id": "testActionId3" + } + ], + "archivedActions": [ + { + "id": "testActionId2" + } + ] + }, + "publishedCollection": { + "name": "testCollection", + "pageId": "testPageId", + "pluginId": "testPluginId", + "defaultToBranchedActionIdsMap": [ + { + "defaultTestActionId1": "testActionId1" + } + ] + } + } +} \ No newline at end of file From c54e1ba8d1f87878fd5f0973b31bb1e3d2b476ee Mon Sep 17 00:00:00 2001 From: Nilansh Bansal Date: Mon, 8 May 2023 17:28:55 +0530 Subject: [PATCH 5/5] fix: upload image logo fix (#23056) ## Description > This PR fixes the logo upload failure for branding and app navigation. > The cursor moves ahead when we read the file, it had to be resetted to position 0 back again. Fixes #23055 #### Type of change - Bug fix (non-breaking change which fixes an issue) ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [ ] Manual - [ ] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Test-plan-implementation#speedbreaker-features-to-consider-for-every-change) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans/_edit#areas-of-interest) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed Co-authored-by: Aishwarya UR --- .../com/appsmith/server/services/ce/AssetServiceCEImpl.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AssetServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AssetServiceCEImpl.java index 409cc08f19..d138bbcb72 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AssetServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AssetServiceCEImpl.java @@ -58,6 +58,8 @@ public class AssetServiceCEImpl implements AssetServiceCE { private Boolean checkImageTypeValidation(DataBuffer dataBuffer, MediaType contentType) throws IOException { BufferedImage bufferedImage = ImageIO.read(dataBuffer.asInputStream()); + // Resetting the position of the cursor + dataBuffer.readPosition(0); if (bufferedImage == null) { /* This is true for SVG and ICO images.