From 499408ea6f475b560b91408a78426e7a7a5421a0 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Thu, 18 Jul 2024 18:33:03 +0530 Subject: [PATCH 01/73] fix: add restart policy for rails/sidekiq containers (#9797) - Add `restart:always` policy for rails and sidekiq containers in the production compose file Fixes #9501 Fixes https://linear.app/chatwoot/issue/PR-1099/missing-restart-always-at-docker-compose-file --- docker-compose.production.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.production.yaml b/docker-compose.production.yaml index d2f5b435c..c1406b02b 100644 --- a/docker-compose.production.yaml +++ b/docker-compose.production.yaml @@ -20,6 +20,7 @@ services: - INSTALLATION_ENV=docker entrypoint: docker/entrypoints/rails.sh command: ['bundle', 'exec', 'rails', 's', '-p', '3000', '-b', '0.0.0.0'] + restart: always sidekiq: <<: *base @@ -31,6 +32,7 @@ services: - RAILS_ENV=production - INSTALLATION_ENV=docker command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml'] + restart: always postgres: image: postgres:12 From ae8619142fd3c0043dc5906949cbf2a921eae6a9 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 18 Jul 2024 20:08:26 -0700 Subject: [PATCH 02/73] chore: Update dependencies to fix security issues (#9801) - update dependencies to fix security issues --- Gemfile.lock | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 91d3b3d8b..32f54c64d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,8 +103,8 @@ GEM tzinfo (~> 2.0) acts-as-taggable-on (9.0.1) activerecord (>= 6.0, < 7.1) - addressable (2.8.4) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) administrate (0.20.1) actionpack (>= 6.0, < 8.0) actionview (>= 6.0, < 8.0) @@ -171,7 +171,8 @@ GEM commonmarker (0.23.10) concurrent-ruby (1.3.3) connection_pool (2.4.1) - crack (0.4.5) + crack (1.0.0) + bigdecimal rexml crass (1.0.6) csv (3.3.0) @@ -358,7 +359,7 @@ GEM ruby2ruby (~> 2.4) ruby_parser (~> 3.10) hana (1.3.7) - hashdiff (1.0.1) + hashdiff (1.1.0) hashie (5.0.0) http (5.1.1) addressable (~> 2.8) @@ -550,7 +551,7 @@ GEM method_source (~> 1.0) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (5.0.1) + public_suffix (6.0.0) puma (6.4.2) nio4r (~> 2.0) pundit (2.3.0) @@ -633,8 +634,8 @@ GEM retriable (3.1.2) reverse_markdown (2.1.1) nokogiri - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.3.2) + strscan rspec-core (3.13.0) rspec-support (~> 3.13.0) rspec-expectations (3.13.1) @@ -809,7 +810,7 @@ GEM web-push (3.0.1) jwt (~> 2.0) openssl (~> 3.0) - webmock (3.18.1) + webmock (3.23.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) From 23e30fcb1a0d8492fb93b53716b4a4c48ad00903 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:14:56 +0530 Subject: [PATCH 03/73] feat: Delete `bulkActionsMixin` (#9800) # Pull Request Template ## Description This PR will remove the `bulkActionsMixin` usage. Seems like it is not used anywhere. Fixes https://linear.app/chatwoot/issue/CW-3453/delete-bulkactionsmixin ## Type of change - [x] New feature (non-breaking change which adds functionality) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on 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 - [ ] Any dependent changes have been merged and published in downstream modules Co-authored-by: Sojan Jose --- .../dashboard/mixins/bulkActionsMixin.js | 15 --------------- .../mixins/specs/bulkActions.spec.js | 19 ------------------- 2 files changed, 34 deletions(-) delete mode 100644 app/javascript/dashboard/mixins/bulkActionsMixin.js delete mode 100644 app/javascript/dashboard/mixins/specs/bulkActions.spec.js diff --git a/app/javascript/dashboard/mixins/bulkActionsMixin.js b/app/javascript/dashboard/mixins/bulkActionsMixin.js deleted file mode 100644 index 129fc7913..000000000 --- a/app/javascript/dashboard/mixins/bulkActionsMixin.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - props: { - trianglePosition: { - type: String, - default: '0', - }, - }, - computed: { - cssVars() { - return { - '--triangle-position': this.trianglePosition + 'rem', - }; - }, - }, -}; diff --git a/app/javascript/dashboard/mixins/specs/bulkActions.spec.js b/app/javascript/dashboard/mixins/specs/bulkActions.spec.js deleted file mode 100644 index c6f096dae..000000000 --- a/app/javascript/dashboard/mixins/specs/bulkActions.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import { mount } from '@vue/test-utils'; -import bulkActionsMixin from '../bulkActionsMixin'; -describe('bulkActionsMixin', () => { - it('returns the prop and computed values for triangle position:', async () => { - const Component = { - render() {}, - title: 'MyComponent', - mixins: [bulkActionsMixin], - }; - const wrapper = mount(Component); - await wrapper.setProps({ - trianglePosition: '10', - }); - expect(wrapper.props().trianglePosition).toEqual('10'); - expect(wrapper.vm.cssVars).toEqual({ - '--triangle-position': '10rem', - }); - }); -}); From cb0642564ced2cf83692905d674cc6b2b81fea12 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 22 Jul 2024 11:32:05 +0530 Subject: [PATCH 04/73] feat: add promise based loader for FB script (#9780) ![CleanShot 2024-07-16 at 11 10 40@2x](https://github.com/user-attachments/assets/8b938968-5f80-4a19-95fb-e00e1dbd7526) --------- Co-authored-by: Muhsin Keloth --- app/javascript/dashboard/helper/DOMHelpers.js | 124 ++++++++++++++++++ .../dashboard/helper/specs/DOMHelpers.spec.js | 95 ++++++++++++++ .../dashboard/i18n/locale/en/inboxMgmt.json | 1 + .../settings/inbox/channels/Facebook.vue | 89 ++++++------- .../settings/inbox/facebook/Reauthorize.vue | 65 +++++---- 5 files changed, 296 insertions(+), 78 deletions(-) create mode 100644 app/javascript/dashboard/helper/DOMHelpers.js create mode 100644 app/javascript/dashboard/helper/specs/DOMHelpers.spec.js diff --git a/app/javascript/dashboard/helper/DOMHelpers.js b/app/javascript/dashboard/helper/DOMHelpers.js new file mode 100644 index 000000000..54fe6cd92 --- /dev/null +++ b/app/javascript/dashboard/helper/DOMHelpers.js @@ -0,0 +1,124 @@ +const SCRIPT_TYPE = 'text/javascript'; +const DATA_LOADED_ATTR = 'data-loaded'; +const SCRIPT_PROPERTIES = [ + 'defer', + 'crossOrigin', + 'noModule', + 'referrerPolicy', + 'id', +]; + +/** + * Custom error class for script loading failures. + * @extends Error + */ +class ScriptLoaderError extends Error { + /** + * Creates a new ScriptLoaderError. + * @param {string} src - The source URL of the script that failed to load. + * @param {string} message - The error message. + */ + constructor(src, message = 'Failed to load script') { + super(message); + this.name = 'ScriptLoaderError'; + this.src = src; + } + + /** + * Gets detailed error information. + * @returns {string} A string containing the error details. + */ + getErrorDetails() { + return `Failed to load script from source: ${this.src}`; + } +} + +/** + * Creates a new script element with the specified attributes. + * @param {string} src - The source URL of the script. + * @param {Object} options - Options for configuring the script element. + * @param {string} [options.type='text/javascript'] - The type of the script. + * @param {boolean} [options.async=true] - Whether the script should load asynchronously. + * @param {boolean} [options.defer] - Whether the script execution should be deferred. + * @param {string} [options.crossOrigin] - The CORS setting for the script. + * @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module. + * @param {string} [options.referrerPolicy] - The referrer policy for the script. + * @param {string} [options.id] - The id attribute for the script element. + * @param {Object} [options.attrs] - Additional attributes to set on the script element. + * @returns {HTMLScriptElement} The created script element. + */ +const createScriptElement = (src, options) => { + const el = document.createElement('script'); + el.type = options.type || SCRIPT_TYPE; + el.async = options.async !== false; + el.src = src; + + SCRIPT_PROPERTIES.forEach(property => { + if (property in options) { + el[property] = options[property]; + } + }); + + Object.entries(options.attrs || {}).forEach(([name, value]) => + el.setAttribute(name, value) + ); + + return el; +}; + +/** + * Finds an existing script element with the specified source URL. + * @param {string} src - The source URL to search for. + * @returns {HTMLScriptElement|null} The found script element, or null if not found. + */ +const findExistingScript = src => { + return document.querySelector(`script[src="${src}"]`); +}; + +/** + * Loads a script asynchronously and returns a promise. + * @param {string} src - The source URL of the script to load. + * @param {Object} options - Options for configuring the script element. + * @param {string} [options.type='text/javascript'] - The type of the script. + * @param {boolean} [options.async=true] - Whether the script should load asynchronously. + * @param {boolean} [options.defer] - Whether the script execution should be deferred. + * @param {string} [options.crossOrigin] - The CORS setting for the script. + * @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module. + * @param {string} [options.referrerPolicy] - The referrer policy for the script. + * @param {string} [options.id] - The id attribute for the script element. + * @param {Object} [options.attrs] - Additional attributes to set on the script element. + * @returns {Promise} A promise that resolves with the loaded script element, + * or false if the script couldn't be loaded. + * @throws {ScriptLoaderError} If the script fails to load. + */ +export async function loadScript(src, options) { + if (typeof window === 'undefined' || !window.document) { + return Promise.resolve(false); + } + + return new Promise((resolve, reject) => { + if (typeof src !== 'string' || src.trim() === '') { + reject(new Error('Invalid source URL provided')); + return; + } + + let el = findExistingScript(src); + + if (!el) { + el = createScriptElement(src, options); + document.head.appendChild(el); + } else if (el.hasAttribute(DATA_LOADED_ATTR)) { + resolve(el); + return; + } + + const handleError = () => reject(new ScriptLoaderError(src)); + + el.addEventListener('error', handleError); + el.addEventListener('abort', handleError); + el.addEventListener('load', () => { + el.setAttribute(DATA_LOADED_ATTR, 'true'); + resolve(el); + }); + }); +} diff --git a/app/javascript/dashboard/helper/specs/DOMHelpers.spec.js b/app/javascript/dashboard/helper/specs/DOMHelpers.spec.js new file mode 100644 index 000000000..fb3f01c45 --- /dev/null +++ b/app/javascript/dashboard/helper/specs/DOMHelpers.spec.js @@ -0,0 +1,95 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { loadScript } from '../DOMHelpers'; +import { JSDOM } from 'jsdom'; + +describe('loadScript', () => { + let dom; + let window; + let document; + + beforeEach(() => { + dom = new JSDOM('', { + url: 'http://localhost', + }); + window = dom.window; + document = window.document; + global.document = document; + }); + + afterEach(() => { + vi.restoreAllMocks(); + delete global.document; + }); + + it('should load a script successfully', async () => { + const src = 'https://example.com/script.js'; + const loadPromise = loadScript(src, {}); + + // Simulate successful script load + setTimeout(() => { + const script = document.querySelector(`script[src="${src}"]`); + if (script) { + script.dispatchEvent(new window.Event('load')); + } + }, 0); + + const script = await loadPromise; + + expect(script).toBeTruthy(); + expect(script.getAttribute('src')).toBe(src); + expect(script.getAttribute('data-loaded')).toBe('true'); + }); + + it('should not load a script if document is not available', async () => { + delete global.document; + const result = await loadScript('https://example.com/script.js', {}); + expect(result).toBe(false); + }); + + it('should use an existing script if already present', async () => { + const src = 'https://example.com/existing-script.js'; + const existingScript = document.createElement('script'); + existingScript.src = src; + existingScript.setAttribute('data-loaded', 'true'); + document.head.appendChild(existingScript); + + const script = await loadScript(src, {}); + + expect(script).toBe(existingScript); + }); + + it('should set custom attributes on the script element', async () => { + const src = 'https://example.com/custom-script.js'; + const options = { + type: 'module', + async: false, + defer: true, + crossOrigin: 'anonymous', + noModule: true, + referrerPolicy: 'origin', + id: 'custom-script', + attrs: { 'data-custom': 'value' }, + }; + + const loadPromise = loadScript(src, options); + + // Simulate successful script load + setTimeout(() => { + const script = document.querySelector(`script[src="${src}"]`); + if (script) { + script.dispatchEvent(new window.Event('load')); + } + }, 0); + + const script = await loadPromise; + + expect(script.type).toBe('module'); + expect(script.async).toBe(false); + expect(script.defer).toBe(true); + expect(script.crossOrigin).toBe('anonymous'); + expect(script.noModule).toBe(true); + expect(script.referrerPolicy).toBe('origin'); + expect(script.id).toBe('custom-script'); + expect(script.getAttribute('data-custom')).toBe('value'); + }); +}); diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 82e6e7c2b..eec8ccde6 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -379,6 +379,7 @@ }, "DETAILS": { "LOADING_FB": "Authenticating you with Facebook...", + "ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.", "ERROR_FB_AUTH": "Something went wrong, Please refresh page...", "ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ", "ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles here.", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue index 374a88854..b52af77a6 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue @@ -2,14 +2,15 @@
-