From eb06f9cd8b19f1483706dbe0ae2d76f0f0478eb1 Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 01/68] feat: OS-14592 Create BrightSign CI workflows * workflows: Copy across BrightSign CI workflows from dev. * workflows: Temp change to debug new auth method * workflows: Another temp hack to debug * workflows: Another temp hack to debug * workflows: Change to use Github App authentication instead of using a PAT token. * workflows: Use version of ec2 runner with a fix for authentication in stop case. * workflows: Update ec2 runner version again. * workflows: Update to an AMI image that has pkg-config installed. * workflows: Use new leave_ec2_instance_running debug parameter from the ec2 runner action * workflows: Temp hack to stop EC2 instances being terminated after test run to debug failures. * workflows: Update to the ami that has pkg-config installed. * workflows: Update to a newer ami image that has dependencies installed properly. * workflows: Remove hack that set leave_ec2_instance_running to true for debugging purposes. * workflows: Switch to correct AWS account. OS-14873: Switch to using chromium export_tarball.py (#10) * workflows: Switch to using chromium export_tarball.py to create src tarball for release * workflows: Use ELECTRON_VERSION env var when calling export_tarball.py * workflows: Add missing $ when accessing env.SOURCE_RELEASE_FILE_NAME * workflows: Set src tarball upload to be after gclient sync again * workflows: Fix electron_package_name from .tgz to .gz * workflows: Remove commented out line Cherry picked from: 23ffb1ea714c6bd45401bd35df217866dcf78453: feat: OS-14592 Create BrightSign CI workflows 5027fb73b93926afb8e9ab438e2b24bfb75b69fb: OS-14873: Switch to using chromium export_tarball.py (#10) actions: Disable GPU when run unit tests on CI Cherry picked from: 63538665ec27b1dc46e0b2f7a263e91a1812d1b6: feat: OS-14592 Create BrightSign CI workflows 367fa4dd6a19fdb2e8a69d277a7405d7db817939: actions: Disable GPU when run unit tests on CI actions: Fixes to github actions for electron 36 actions: Update sccache to v0.10.0 Cherry picked from: 997b44e82fca53abd4d8d1bb5af74e62ab684e15: feat: OS-14592 Create BrightSign CI workflows 5e569531c0a0c07cd9669bae5c13d738750c86c4: actions: Fixes to github actions for electron 36 f35dc7da61202b8bdad5ee8b7505dba0e983e3bb: actions: Update sccache to v0.10.0 --- .../workflows/bs_electron_build_and_test.yml | 296 ++++++++++++++++++ .github/workflows/bs_electron_ci_ec2.yml | 108 +++++++ .../bs_electron_ci_manual_process.yml | 41 +++ .../bs_electron_ci_release_process.yml | 35 +++ .../workflows/bs_electron_ci_test_process.yml | 27 ++ 5 files changed, 507 insertions(+) create mode 100644 .github/workflows/bs_electron_build_and_test.yml create mode 100644 .github/workflows/bs_electron_ci_ec2.yml create mode 100644 .github/workflows/bs_electron_ci_manual_process.yml create mode 100644 .github/workflows/bs_electron_ci_release_process.yml create mode 100644 .github/workflows/bs_electron_ci_test_process.yml diff --git a/.github/workflows/bs_electron_build_and_test.yml b/.github/workflows/bs_electron_build_and_test.yml new file mode 100644 index 0000000000000..085fb82f66179 --- /dev/null +++ b/.github/workflows/bs_electron_build_and_test.yml @@ -0,0 +1,296 @@ +name: 'BrightSign Build and Test Electron: Build and test workflow' +on: + workflow_call: + inputs: + runner_name: + required: true + type: string + + github_hosted_runner: + required: true + type: boolean + + build_type: + required: true + type: string + # "test" or "release" + + aws_arn_role: + required: true + type: string + + aws_region: + required: true + type: string + +jobs: + build-and-test-electron: + name: Build and Test Electron + runs-on: ${{ inputs.runner_name }} + defaults: + run: + working-directory: ./ + + steps: + # Cleanups only needed for self hosted runners + # - name: Clean up work dir (temp instead of colpal/actions-clean@v1, until docker usage issue with sock file permissions sorted) + # run: rm -rf * + + # - name: Cleanup work dir + # uses: colpal/actions-clean@v1 + # if: ${{ always() }} # To ensure this step runs even when earlier steps fail + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-region: ${{ inputs.aws_region}} + role-to-assume: ${{ inputs.aws_arn_role }} + # Set role-duration-seconds to 10 hours + role-duration-seconds: 36000 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 21 + + - name: Download config file and set env vars from it + run: | + aws s3 cp s3://electron-ci-config-bucket/config.json . + aws s3 cp s3://electron-ci-config-bucket/set_github_env_vars.py . + python3 set_github_env_vars.py --file config.json + + - name: Check build type + if: inputs.build_type != 'test' && inputs.build_type != 'release' + run: | + echo "ERROR> build_type is set to \"${{ inputs.build_type }}\" and should be set to \"test\" or \"release\"" + exit 1 + + - name: Install dependencies if Github hosted runner + if: inputs.github_hosted_runner == true + run: | + # From https://www.electronjs.org/docs/latest/development/build-instructions-linux + sudo apt-get update + sudo apt-get install -y build-essential clang libdbus-1-dev libgtk-3-dev \ + libnotify-dev libasound2-dev libcap-dev \ + libcups2-dev libxtst-dev \ + libxss1 libnss3-dev gcc-multilib g++-multilib curl \ + gperf bison python3-dbusmock openjdk-8-jre + + # virtual framebuffer for running tests. + sudo apt install -y xvfb + + # A fix for test case (to remove fonts-liberation that seems to be installed by default in Github default ubuntu image)... + # 2022-12-23T10:56:48.4520914Z not ok 2079 font fallback should use Helvetica for sans-serif on Mac, and Arial on Windows and Linux + # 2022-12-23T10:56:48.4522432Z AssertionError: expected 'Liberation Sans' to equal 'DejaVu Sans' + # 2022-12-23T10:56:48.4523556Z at Context. (electron/spec/chromium-spec.ts:2222:38) + sudo apt-get remove -y fonts-liberation + + - name: Install and setup sccache + run: | + # export SCCACHE_ERROR_LOG="$PWD"/sccache.log + # export SCCACHE_LOG="info,sccache::cache=debug" + + curl -o sccache_package.tar.gz -L https://github.com/mozilla/sccache/releases/download/v0.10.0/sccache-v0.10.0-x86_64-unknown-linux-musl.tar.gz + echo "1fbb35e135660d04a2d5e42b59c7874d39b3deb17de56330b25b713ec59f849b sccache_package.tar.gz" | shasum -a 256 -c + + tar xzf sccache_package.tar.gz --strip-components=1 + sudo ln -s "$PWD"/sccache /usr/local/bin/sccache + + # echo '/home/ubuntu/.cargo/bin/sccache' >> $GITHUB_PATH + echo 'SCCACHE_REGION=${{ inputs.aws_region }}' >> $GITHUB_ENV + echo 'SCCACHE_CACHE_SIZE=100G' >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@v3 + with: + path: ./src/electron + fetch-depth: 0 + + - name: Get electron_version + run: echo "ELECTRON_VERSION=v$(python3 ./src/electron/script/get-git-version.py)" >> $GITHUB_ENV + + - name: Setup build type Testing + if: inputs.build_type == 'test' + run: | + echo "Setting up build type as testing" + echo 'OUTPUT_FOLDER=out/Testing' >> $GITHUB_ENV + echo 'GN_IMPORT=//electron/build/args/testing.gn' >> $GITHUB_ENV + echo 'SCCACHE_BUCKET=${{ env.SCCACHE_TEST_BUILD_BUCKET_NAME }}' >> $GITHUB_ENV + + - name: Setup build type Release + if: inputs.build_type == 'release' + run: | + echo "Setting up build type as release" + echo 'OUTPUT_FOLDER=out/Release' >> $GITHUB_ENV + echo 'GN_IMPORT=//electron/build/args/release.gn' >> $GITHUB_ENV + echo 'SOURCE_RELEASE_FILE_NAME=${{ env.ELECTRON_VERSION }}' >> $GITHUB_ENV + echo 'RELEASE_SRC_FOLDER=${{ env.ELECTRON_VERSION }}' >> $GITHUB_ENV + echo 'RELEASE_DST_FOLDER=${{ env.ELECTRON_VERSION }}' >> $GITHUB_ENV + echo 'SCCACHE_BUCKET=${{ env.SCCACHE_RELEASE_BUILD_BUCKET_NAME }}' >> $GITHUB_ENV + + - name: Display sccache config + run: | + # echo $SCCACHE_BUCKET $SCCACHE_REGION $SCCACHE_CACHE_SIZE + + sccache -s + + - name: Configure Git cache params + run: | + echo 'GIT_CACHE_FOLDER_NAME=git-cache' >> $GITHUB_ENV + echo 'GIT_CACHE_PATH='"$PWD"'/git-cache' >> $GITHUB_ENV + echo 'GIT_CACHE_FILENAME=git-cache-${{ hashFiles('src/electron/DEPS') }}.tar.gz' >> $GITHUB_ENV + + - name: Get depot tools + run: git clone --depth=1 https://chromium.googlesource.com/chromium/tools/depot_tools.git + + - name: Add depot tools to PATH + run: echo ''"$PWD"'/depot_tools' >> $GITHUB_PATH + + - name: Retrieve git-cache + run: | + aws s3api head-object --bucket ${{ env.GIT_CACHE_BUCKET_NAME }} --key $GIT_CACHE_FILENAME || not_exist=true + if [ $not_exist ]; then + echo "Git cache ${GIT_CACHE_FILENAME} not found in bucket ${{ env.GIT_CACHE_BUCKET_NAME }}" + echo "GIT_CACHE_RESTORED=false" >> $GITHUB_ENV + else + echo "Git cache ${GIT_CACHE_FILENAME} found in bucket ${{ env.GIT_CACHE_BUCKET_NAME }}" + echo "GIT_CACHE_RESTORED=true" >> $GITHUB_ENV + aws s3 cp s3://${{ env.GIT_CACHE_BUCKET_NAME }}/$GIT_CACHE_FILENAME $GIT_CACHE_FILENAME --quiet + echo "Downloaded ${GIT_CACHE_FILENAME}" + tar --use-compress-program=pigz -xf $GIT_CACHE_FILENAME + echo "Uncompressed ${GIT_CACHE_FILENAME}" + rm $GIT_CACHE_FILENAME + fi + + - name: GClient sync + run: | + gclient config --name "src/electron" --unmanaged git@github.com:${{ github.repository }}.git + gclient sync --with_branch_heads --with_tags + + # TEMP if you don't do gsync + # cd src/electron + # gclient sync -f + + - name: Save git-cache cache if needed + if: env.GIT_CACHE_RESTORED == 'false' + run: | + tar --use-compress-program=pigz -cf $GIT_CACHE_FILENAME ./$GIT_CACHE_FOLDER_NAME + aws s3 cp $GIT_CACHE_FILENAME s3://${{ env.GIT_CACHE_BUCKET_NAME }}/$GIT_CACHE_FILENAME --storage-class ONEZONE_IA --quiet + + - name: Create source tarball and copy to artifacts bucket + if: inputs.build_type == 'release' + run: | + mkdir -p ${{ env.RELEASE_SRC_FOLDER }} + + echo "Fetching chromium build tools to get export_tarball.py" + git clone https://chromium.googlesource.com/chromium/tools/build --depth 1 + + echo "Running export_tarball.py" + ./build/recipes/recipe_modules/chromium/resources/export_tarball.py $RELEASE_SRC_FOLDER/${{ env.SOURCE_RELEASE_FILE_NAME }} --basename src --src-dir=./src --version=${{ env.ELECTRON_VERSION }} --remove-nonessential-files + + echo "Uploading source tarball" + aws s3 cp ${{ env.RELEASE_SRC_FOLDER }}/${{ env.SOURCE_RELEASE_FILE_NAME }}.tar.xz s3://${{ env.ARTIFACT_BUCKET_NAME }}/${{ env.RELEASE_DST_FOLDER }}/ --acl public-read --quiet + + - name: Set CHROMIUM_BUILDTOOLS_PATH env var + run: echo 'CHROMIUM_BUILDTOOLS_PATH='"$PWD"'/src/buildtools' >> $GITHUB_ENV + + - name: Make sure electron git pack-refs file is present + # This is to stop ninja getting into an infinte loop of "[0/1] Regenerating ninja files" + # Found this file was the issue (as it was missing for some reason) by manually running ninja with -d explain + run: | + cd src/electron + git pack-refs --all + + - name: Run GN Gen + run: | + cd src + gn gen $OUTPUT_FOLDER --args="import(\"${GN_IMPORT}\") cc_wrapper=\"sccache\"" + + - name: Run Ninja Build + run: | + cd src + ninja -C $OUTPUT_FOLDER electron + + - name: Build other items for tests + if: inputs.build_type == 'test' + run: | + cd src + ninja -C $OUTPUT_FOLDER electron:node_headers + + - name: Display sccache stats + run: sccache -s + + - name: Run unit tests + if: inputs.build_type == 'test' + run: | + cd src/electron + xvfb-run node script/spec-runner --disable-gpu + + - name: Run Node.js Smoke Tests + if: inputs.build_type == 'test' + run: | + cd src/electron + xvfb-run node script/node-spec-runner.js --default + + - name: Prepare release dist and upload + if: inputs.build_type == 'release' + run: | + cd src + ninja -C $OUTPUT_FOLDER electron:electron_dist_zip electron:node_headers third_party/electron_node:overlapped-checker electron:hunspell_dictionaries_zip + if [ "`uname`" == "Darwin" ]; then + target_os=mac + target_cpu=x64 + if [ x"$MAS_BUILD" == x"true" ]; then + target_os=mac_mas + fi + if [ "$TARGET_ARCH" == "arm64" ]; then + target_cpu=arm64 + fi + elif [ "`uname`" == "Linux" ]; then + target_os=linux + if [ x"$TARGET_ARCH" == x ]; then + target_cpu=x64 + else + target_cpu="$TARGET_ARCH" + fi + else + echo "Unknown system: `uname`" + exit 1 + fi + echo "Checking dist_zip.${target_os}.${target_cpu}.manifest" + echo "TARGET_OS_CPU=${target_os}-${target_cpu}" >> $GITHUB_ENV + electron/script/zip_manifests/check-zip-manifest.py $OUTPUT_FOLDER/dist.zip electron/script/zip_manifests/dist_zip.$target_os.$target_cpu.manifest + + - name: Prepare release package and upload to artifacts bucket + if: inputs.build_type == 'release' + run: | + cd ${{ env.RELEASE_SRC_FOLDER }} + mkdir package + cd package + unzip -q ../../src/out/Release/dist.zip -d ./dist + cp ../../src/electron/npm/* . + cp ../../src/electron/electron.d.ts . + cp ../../src/LICENSE . + + # Normally when electron is installed it will run install.js which will download the binary, create path.txt + # and copy fields 'name', 'repository', 'description', 'license', 'author', 'keywords' from the root package.json. + + # We will manually do these steps and clear install.js + + echo -n "electron" > path.txt + echo "" > install.js + + echo "$(jq '. += {"name": "electron"}' package.json)" > package.json + echo "$(jq '. += {"repository": "https://github.com/brightsign/electron"}' package.json)" > package.json + echo "$(jq '. += {"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS"}' package.json)" > package.json + echo "$(jq '. += {"license": "MIT"}' package.json)" > package.json + echo "$(jq '. += {"author": "Electron Community"}' package.json)" > package.json + echo "$(jq '. += {"keywords": [ "electron" ] }' package.json)" > package.json + echo "$(jq '. += {"version": "${{ env.ELECTRON_VERSION }}"}' package.json)" > package.json + + electron_package_name="electron-package-${{ env.TARGET_OS_CPU }}-${{ env.SOURCE_RELEASE_FILE_NAME }}.tar.gz" + echo "Creating $electron_package_name" + tar --use-compress-program=pigz -cf "../$electron_package_name" . + cd .. + + echo "Uploading $electron_package_name to s3://${{ env.ARTIFACT_BUCKET_NAME }}/${{ env.RELEASE_DST_FOLDER }}/" + aws s3 cp $electron_package_name s3://${{ env.ARTIFACT_BUCKET_NAME }}/${{ env.RELEASE_DST_FOLDER }}/ --acl public-read --quiet diff --git a/.github/workflows/bs_electron_ci_ec2.yml b/.github/workflows/bs_electron_ci_ec2.yml new file mode 100644 index 0000000000000..4cd5baa57c628 --- /dev/null +++ b/.github/workflows/bs_electron_ci_ec2.yml @@ -0,0 +1,108 @@ +name: 'BrightSign Build and Test Electron: EC2 controller' +on: + workflow_call: + inputs: + build_type: + description: 'Build Type' + required: true + type: string + + instance_type: + description: 'EC2 instance type' + required: false + type: string + default: c6a.4xlarge + + leave_ec2_instance_running: + description: 'Leave EC2 instance running after use' + type: boolean + default: false + + instance_name_postfix: + description: 'Name to add as postfix to the EC2 machine' + type: string + default: auto-triggered + + aws_arn_role: + required: true + type: string + + aws_region: + required: true + type: string + +jobs: + start-runner: + name: Start self-hosted EC2 runner + runs-on: ubuntu-latest + outputs: + label: ${{ steps.start-ec2-runner.outputs.label }} + ec2-instance-id: ${{ steps.start-ec2-runner.outputs.ec2-instance-id }} + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-region: ${{ inputs.aws_region }} + role-to-assume: ${{ inputs.aws_arn_role }} + + - name: Download config file and set env vars from it + run: | + aws s3 cp s3://electron-ci-config-bucket/config.json . + aws s3 cp s3://electron-ci-config-bucket/set_github_env_vars.py . + python set_github_env_vars.py --file config.json + + - name: Start EC2 runner + id: start-ec2-runner + uses: brightsign/ec2-github-runner@0fa8b183dd4124fd191ccdbc48b68f0ea46a9634 + with: + mode: start + github-app-private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + github-app-id: 287690 + ec2-image-id: ami-07b4a16cd5294af98 + ec2-instance-type: ${{ inputs.instance_type }} + subnet-id: ${{ env.VPC_SUBNET_ID }} + security-group-id: ${{ env.VPC_SG_ID }} + run-as-service-with-user: ubuntu + runner-home-dir: /home/ubuntu + # iam-role-name: my-role-name # optional, requires additional permissions + aws-resource-tags: > # optional, requires additional permissions + [ + {"Key": "Name", "Value": "github-runner-${{ inputs.instance_name_postfix }}"}, + {"Key": "GitHubRepository", "Value": "${{ github.repository }}"} + ] + + build-and-test-electron: + name: Build and Test Electron + needs: start-runner # required to start the main job when the runner is ready + uses: ./.github/workflows/bs_electron_build_and_test.yml + secrets: inherit + with: + runner_name: ${{ needs.start-runner.outputs.label }} # run the job on the newly created runner + github_hosted_runner: false + build_type: ${{ inputs.build_type }} + aws_arn_role: ${{ inputs.aws_arn_role }} + aws_region: ${{ inputs.aws_region }} + + stop-runner: + name: Stop self-hosted EC2 runner + needs: + - start-runner # required to get output from the start-runner job + - build-and-test-electron # required to wait when the main job is done + runs-on: ubuntu-latest + if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + role-to-assume: ${{ inputs.aws_arn_role }} + aws-region: ${{ inputs.aws_region }} + + - name: Stop EC2 runner + uses: brightsign/ec2-github-runner@0fa8b183dd4124fd191ccdbc48b68f0ea46a9634 + with: + mode: stop + github-app-private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + github-app-id: 287690 + label: ${{ needs.start-runner.outputs.label }} + ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }} + leave-ec2-instance-running: ${{ inputs.leave_ec2_instance_running }} \ No newline at end of file diff --git a/.github/workflows/bs_electron_ci_manual_process.yml b/.github/workflows/bs_electron_ci_manual_process.yml new file mode 100644 index 0000000000000..7b5f7ff047d96 --- /dev/null +++ b/.github/workflows/bs_electron_ci_manual_process.yml @@ -0,0 +1,41 @@ +name: 'BrightSign Manual Build Process' +on: + workflow_dispatch: + inputs: + build_type: + description: 'Build Type' + type: choice + options: + - test + - release + + instance_type: + description: 'EC2 Instance Type' + type: choice + options: + - c6a.large + - c6a.xlarge + - c6a.2xlarge + - c6a.4xlarge + - c6a.8xlarge + + leave_ec2_instance_running: + description: 'Leave EC2 instance running after use' + type: boolean + default: false + +permissions: + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout + +jobs: + call-build-and-test: + uses: ./.github/workflows/bs_electron_ci_ec2.yml + secrets: inherit + with: + build_type: ${{ inputs.build_type }} + instance_type: ${{ inputs.instance_type }} + leave_ec2_instance_running: ${{ inputs.leave_ec2_instance_running }} + instance_name_postfix: manual--${{ github.actor }} + aws_arn_role: arn:aws:iam::195607249165:role/github-actions-electron-repo + aws_region: us-east-1 diff --git a/.github/workflows/bs_electron_ci_release_process.yml b/.github/workflows/bs_electron_ci_release_process.yml new file mode 100644 index 0000000000000..5ad725b0a3093 --- /dev/null +++ b/.github/workflows/bs_electron_ci_release_process.yml @@ -0,0 +1,35 @@ +name: 'BrightSign Release Processes' +on: + create: + delete: + tags: + +permissions: + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout + +jobs: + create-release: + if: github.event_name == 'create' && github.event.ref_type == 'tag' + uses: ./.github/workflows/bs_electron_ci_ec2.yml + secrets: inherit + with: + build_type: release + aws_arn_role: arn:aws:iam::195607249165:role/github-actions-electron-repo + aws_region: us-east-1 + + delete-release: + if: github.event_name == 'delete' && github.event.ref_type == 'tag' # startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Info + run: echo "TODO.. Delete release with Tag" ${{ github.event.ref }} + + # debug: + # runs-on: ubuntu-latest + # steps: + # - name: Dump GitHub context + # env: + # GITHUB_CONTEXT: ${{ toJson(github) }} + # run: | + # echo "$GITHUB_CONTEXT" diff --git a/.github/workflows/bs_electron_ci_test_process.yml b/.github/workflows/bs_electron_ci_test_process.yml new file mode 100644 index 0000000000000..d324bcf9a0eea --- /dev/null +++ b/.github/workflows/bs_electron_ci_test_process.yml @@ -0,0 +1,27 @@ +name: 'BrightSign Test Process' +on: + pull_request: + # branches: [ "main" ] + +permissions: + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout + +jobs: + test-pr: + if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize') + uses: ./.github/workflows/bs_electron_ci_ec2.yml + secrets: inherit + with: + build_type: test + aws_arn_role: arn:aws:iam::195607249165:role/github-actions-electron-repo + aws_region: us-east-1 + + # debug: + # runs-on: ubuntu-latest + # steps: + # - name: Dump GitHub context + # env: + # GITHUB_CONTEXT: ${{ toJson(github) }} + # run: | + # echo "$GITHUB_CONTEXT" From 7a8162f3a74074b8e7d99d07637373439f247cf5 Mon Sep 17 00:00:00 2001 From: Caner Altinbasak Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 02/68] update ec2-github-runner with latest github runner --- .github/workflows/bs_electron_ci_ec2.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bs_electron_ci_ec2.yml b/.github/workflows/bs_electron_ci_ec2.yml index 4cd5baa57c628..2c845be3b6e70 100644 --- a/.github/workflows/bs_electron_ci_ec2.yml +++ b/.github/workflows/bs_electron_ci_ec2.yml @@ -53,7 +53,7 @@ jobs: - name: Start EC2 runner id: start-ec2-runner - uses: brightsign/ec2-github-runner@0fa8b183dd4124fd191ccdbc48b68f0ea46a9634 + uses: brightsign/ec2-github-runner@3ef5e07bd58c60757b26c39b8cf25f234df7b974 with: mode: start github-app-private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} @@ -98,11 +98,11 @@ jobs: aws-region: ${{ inputs.aws_region }} - name: Stop EC2 runner - uses: brightsign/ec2-github-runner@0fa8b183dd4124fd191ccdbc48b68f0ea46a9634 + uses: brightsign/ec2-github-runner@3ef5e07bd58c60757b26c39b8cf25f234df7b974 with: mode: stop github-app-private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} github-app-id: 287690 label: ${{ needs.start-runner.outputs.label }} ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }} - leave-ec2-instance-running: ${{ inputs.leave_ec2_instance_running }} \ No newline at end of file + leave-ec2-instance-running: ${{ inputs.leave_ec2_instance_running }} From 64eede40ba2470c26ef14016a4c6148256b8ac1f Mon Sep 17 00:00:00 2001 From: Caner Altinbasak Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 03/68] ci: update EC2 AMI to 200GB disk image Increase disk image size from 150GB to 200GB to resolve out of space failures during CI builds. Update ec2-image-id to the new AMI (ami-0aa7bb41d6a04e736) built with the larger disk. --- .github/workflows/bs_electron_ci_ec2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bs_electron_ci_ec2.yml b/.github/workflows/bs_electron_ci_ec2.yml index 2c845be3b6e70..41051e387dc08 100644 --- a/.github/workflows/bs_electron_ci_ec2.yml +++ b/.github/workflows/bs_electron_ci_ec2.yml @@ -58,7 +58,7 @@ jobs: mode: start github-app-private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} github-app-id: 287690 - ec2-image-id: ami-07b4a16cd5294af98 + ec2-image-id: ami-0aa7bb41d6a04e736 ec2-instance-type: ${{ inputs.instance_type }} subnet-id: ${{ env.VPC_SUBNET_ID }} security-group-id: ${{ env.VPC_SG_ID }} From e020bd6a75d999b8b5978e8aa56f178d9dbcc5c7 Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 04/68] actions: Update to node 24 --- .github/workflows/bs_electron_build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bs_electron_build_and_test.yml b/.github/workflows/bs_electron_build_and_test.yml index 085fb82f66179..33efe273a960d 100644 --- a/.github/workflows/bs_electron_build_and_test.yml +++ b/.github/workflows/bs_electron_build_and_test.yml @@ -50,7 +50,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 21 + node-version: 24 - name: Download config file and set env vars from it run: | From 183410dded2c9608e4f712fab5b44e04d28a128e Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 05/68] actions: Remove target electron_node:overlapped-checker for release build This target has been removed --- .github/workflows/bs_electron_build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bs_electron_build_and_test.yml b/.github/workflows/bs_electron_build_and_test.yml index 33efe273a960d..1fd605397b444 100644 --- a/.github/workflows/bs_electron_build_and_test.yml +++ b/.github/workflows/bs_electron_build_and_test.yml @@ -235,7 +235,7 @@ jobs: if: inputs.build_type == 'release' run: | cd src - ninja -C $OUTPUT_FOLDER electron:electron_dist_zip electron:node_headers third_party/electron_node:overlapped-checker electron:hunspell_dictionaries_zip + ninja -C $OUTPUT_FOLDER electron:electron_dist_zip electron:node_headers electron:hunspell_dictionaries_zip if [ "`uname`" == "Darwin" ]; then target_os=mac target_cpu=x64 From b84d77558a36885e5d648ee7d9d939bd9ecd55e1 Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 06/68] actions: Add step to display the disk space free after compilation --- .github/workflows/bs_electron_build_and_test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/bs_electron_build_and_test.yml b/.github/workflows/bs_electron_build_and_test.yml index 1fd605397b444..9673976a863f1 100644 --- a/.github/workflows/bs_electron_build_and_test.yml +++ b/.github/workflows/bs_electron_build_and_test.yml @@ -219,6 +219,9 @@ jobs: - name: Display sccache stats run: sccache -s + - name: Check disk space + run: df -h + - name: Run unit tests if: inputs.build_type == 'test' run: | From 4c2ceb8f591079b411e13b651786c60d2f2bad64 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 07/68] feat: OS-14993 Make redirects from one file to another transparent (#12) * transparent redirect: Make redirects from one file to another transparent * webRequest: Add new test case to api-web-request-spec for transparent and normal redirects fix: Clear content-type and last modified from file fetches: OS-16593 (#39) Clear content-type and last modified from file fetches: OS-16593 Cherry picked from: 67a9eecbbfff84f8839feefa607af95b2aee5a7f: feat: OS-14993 Make redirects from one file to another transparent (#12) --- .../net/proxying_url_loader_factory.cc | 31 +++++++++++++ spec/api-web-request-spec.ts | 43 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/shell/browser/net/proxying_url_loader_factory.cc b/shell/browser/net/proxying_url_loader_factory.cc index 50644e54b7577..b73ecaef286e0 100644 --- a/shell/browser/net/proxying_url_loader_factory.cc +++ b/shell/browser/net/proxying_url_loader_factory.cc @@ -258,6 +258,26 @@ void ProxyingURLLoaderFactory::InProgressRequest::OnReceiveResponse( network::mojom::URLResponseHeadPtr head, mojo::ScopedDataPipeConsumerHandle body, std::optional cached_metadata) { + // Chromium sets the kContentType and kLastModified fields in the header + // always. + if (request_.url.SchemeIs(url::kFileScheme) && + !request_.url.ExtractFileName().empty() && head->headers) { + if (request_.url.ExtractFileName().find(".") == std::string::npos && + head->headers->HasHeaderValue(net::HttpRequestHeaders::kContentType, + "text/plain")) { + // Having content-type set causes an issue for css style sheets with no + // file extension (asset pool files). In this case the content type is set + // to text/plain as the mime sniffer doesn't support detecting a css file, + // which causes the style sheet to be ignored. To fix this remove the + // content type. + head->headers->RemoveHeader(net::HttpRequestHeaders::kContentType); + } + if (head->headers->HasHeader(net::HttpResponseHeaders::kLastModified)) { + // We don't want to cache files so remove the last modified header. + head->headers->RemoveHeader(net::HttpResponseHeaders::kLastModified); + } + } + current_body_ = std::move(body); current_cached_metadata_ = std::move(cached_metadata); if (current_request_uses_header_client_) { @@ -457,6 +477,17 @@ void ProxyingURLLoaderFactory::InProgressRequest::ContinueToBeforeSendHeaders( return; } + // At this point we know that onBeforeRequest (if overridden) will have + // updated redirect_url_. Note: The callback used by OnBeforeRequest (in + // electron_api_web_request.cc) is passed in RestartInternal. If we have a + // redirect url and both the original and redirect urls are file schema, then + // perform a 'transparent' redirect. + if (request_.url.SchemeIs(url::kFileScheme) && !redirect_url_.is_empty() && + redirect_url_.SchemeIs(url::kFileScheme)) { + request_.url = redirect_url_; + // Clear the redirect_url_ to prevent a normal redirect being triggered + redirect_url_ = GURL(); + } if (!current_request_uses_header_client_ && !redirect_url_.is_empty()) { if (for_cors_preflight_) { // CORS preflight doesn't support redirect. diff --git a/spec/api-web-request-spec.ts b/spec/api-web-request-spec.ts index 1d3ebec564159..d27351f329ec1 100644 --- a/spec/api-web-request-spec.ts +++ b/spec/api-web-request-spec.ts @@ -238,6 +238,49 @@ describe('webRequest module', () => { expect(data).to.equal('/redirect'); }); + it('check normal and transparent redirections', async () => { + let onBeforeRedirectCount = 0; + let onBeforeRequestCount = 0; + const redirectFileURL = url.format({ + pathname: path.join(fixturesPath, 'blank.html').replace(/\\/g, '/'), + protocol: 'file', + slashes: true + }); + + ses.webRequest.onBeforeRedirect(() => { + onBeforeRedirectCount++; + }); + ses.webRequest.onBeforeRequest((details, callback) => { + onBeforeRequestCount++; + if (details.url === 'file:///fileTofile') { + callback({ redirectURL: redirectFileURL }); + } else if (details.url === 'file:///fileTohttp' || details.url === defaultURL) { + callback({ redirectURL: `${defaultURL}redirect` }); + } else { + callback({}); + } + }); + + let fileUrl = 'file:///fileTofile'; + let result = await ajax(fileUrl); + expect(result.status).to.equal(200); + expect(onBeforeRequestCount).to.equal(1); + expect(onBeforeRedirectCount).to.equal(0); + onBeforeRequestCount = onBeforeRedirectCount = 0; + + fileUrl = 'file:///fileTohttp'; + result = await ajax(fileUrl); + expect(result.data).to.equal('/redirect'); + expect(onBeforeRequestCount).to.equal(2); + expect(onBeforeRedirectCount).to.equal(1); + onBeforeRequestCount = onBeforeRedirectCount = 0; + + result = await ajax(defaultURL); + expect(result.data).to.equal('/redirect'); + expect(onBeforeRequestCount).to.equal(2); + expect(onBeforeRedirectCount).to.equal(1); + }); + it('does not crash for redirects', async () => { ses.webRequest.onBeforeRequest((details, callback) => { callback({ cancel: false }); From ac6ef77d9bf0f8ed8cb44dfdb3e4f9497e3af50a Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 08/68] feat: OS-14967 Patch to force mime sniffing for file urls in Chromium (#11) file urls: Path to force mime sniffing for file urls in Chromium fix: Patch mime sniffer for html utf8 bom, svg and xml files OS-16547 (#37) * fix: mime sniffer handle html utf8 bom, svg files and xml comments OS-16547 * fix: stop binary sniffer overriding mime type for xml files Cherry picked from: 96b26cfe2a09599f5ad93e89256189e4acffaa8f: feat: OS-14967 Patch to force mime sniffing for file urls in Chromium (#11) 782601e92b437563ec0b9df016d982206612a0f5: fix: Patch mime sniffer for html utf8 bom, svg and xml files OS-16547 (#37) --- patches/chromium/.patches | 2 + ...at_os_14967_force_sniffing_file_urls.patch | 20 +++ ...r_for_html_utf8_bom_svg_xml_comments.patch | 123 ++++++++++++++++++ spec/api-web-contents-spec.ts | 80 ++++++++++++ .../assets/mime-sniffing/html-utf8-bom | 7 + .../mime-sniffing/html-utf8-bom-with-comments | 8 ++ .../assets/mime-sniffing/html-with-comments | 8 ++ spec/fixtures/assets/mime-sniffing/svg | 34 +++++ .../assets/mime-sniffing/svg-utf8-bom | 34 +++++ .../mime-sniffing/svg-utf8-bom-with-comments | 93 +++++++++++++ .../assets/mime-sniffing/svg-with-comments | 93 +++++++++++++ spec/fixtures/assets/mime-sniffing/xml | 5 + .../assets/mime-sniffing/xml-utf8-bom | 5 + .../mime-sniffing/xml-utf8-bom-with-comments | 6 + .../assets/mime-sniffing/xml-with-comments | 6 + 15 files changed, 524 insertions(+) create mode 100644 patches/chromium/feat_os_14967_force_sniffing_file_urls.patch create mode 100644 patches/chromium/os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch create mode 100644 spec/fixtures/assets/mime-sniffing/html-utf8-bom create mode 100644 spec/fixtures/assets/mime-sniffing/html-utf8-bom-with-comments create mode 100644 spec/fixtures/assets/mime-sniffing/html-with-comments create mode 100644 spec/fixtures/assets/mime-sniffing/svg create mode 100644 spec/fixtures/assets/mime-sniffing/svg-utf8-bom create mode 100644 spec/fixtures/assets/mime-sniffing/svg-utf8-bom-with-comments create mode 100644 spec/fixtures/assets/mime-sniffing/svg-with-comments create mode 100644 spec/fixtures/assets/mime-sniffing/xml create mode 100644 spec/fixtures/assets/mime-sniffing/xml-utf8-bom create mode 100644 spec/fixtures/assets/mime-sniffing/xml-utf8-bom-with-comments create mode 100644 spec/fixtures/assets/mime-sniffing/xml-with-comments diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 255dbf239d815..fde81a04ddc2e 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -151,3 +151,5 @@ fix_initialize_com_on_desktopmedialistcapturethread_on_windows.patch fix_use_fresh_lazynow_for_onendworkitemimpl_after_didruntask.patch fix_make_macos_text_replacement_work_on_contenteditable.patch fix_constrain_allowuniversalaccessfromfileurls_to_file_origins_in.patch +feat_os_14967_force_sniffing_file_urls.patch +os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch diff --git a/patches/chromium/feat_os_14967_force_sniffing_file_urls.patch b/patches/chromium/feat_os_14967_force_sniffing_file_urls.patch new file mode 100644 index 0000000000000..36a8352dac25e --- /dev/null +++ b/patches/chromium/feat_os_14967_force_sniffing_file_urls.patch @@ -0,0 +1,20 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir +Date: Wed, 15 Mar 2023 11:37:56 +0000 +Subject: feat:os_14967_force_sniffing_file_urls + +This patch forces Mime Sniffing for file urls. + +diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc +index d0b3e4bc348921df7e6446dbc1f14860b8a84d87..1f3f1b59ee869599ca8bd9e5348e2878887eb261 100644 +--- a/content/public/browser/content_browser_client.cc ++++ b/content/public/browser/content_browser_client.cc +@@ -447,7 +447,7 @@ bool ContentBrowserClient::IsFileAccessAllowed( + } + + bool ContentBrowserClient::ForceSniffingFileUrlsForHtml() { +- return false; ++ return true; + } + + std::string ContentBrowserClient::GetApplicationClientGUIDForQuarantineCheck() { diff --git a/patches/chromium/os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch b/patches/chromium/os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch new file mode 100644 index 0000000000000..ccfb0e404946a --- /dev/null +++ b/patches/chromium/os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch @@ -0,0 +1,123 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir +Date: Fri, 5 Jan 2024 15:16:40 +0000 +Subject: OS-16547: patch mime sniffer for html utf8 bom, svg and xml comments + +Change to the mime sniffer to recognise html files with a utf8 bom, svg files and +xml files with comments. +Cherry picked from QtWebEngine change IDs: +I11c9915a4c23155fc8b8b4e405b69ed48821db5a +I7ee3558529c24a08b22b1ad3a4a9d6df73174cdd +I6d9a40bb65b8ff4d44e4e5b6d850f1a728768feb + +Includes an additional change missing from QtWebEngine version to stop the binary sniffer changing the mime type of text/xml files to text/plain. + +diff --git a/net/base/mime_sniffer.cc b/net/base/mime_sniffer.cc +index e746623e6387b9b1ecf2df7785690a05c31a177e..00c96740d8dc4c284fdb64ecc90f5f977279f9e2 100644 +--- a/net/base/mime_sniffer.cc ++++ b/net/base/mime_sniffer.cc +@@ -370,8 +370,17 @@ static bool SniffForHTML(std::string_view content, + + // We adopt a strategy similar to that used by Mozilla to sniff HTML tags, + // but with some modifications to better match the HTML5 spec. +- std::string_view trimmed = +- base::TrimWhitespaceASCII(content, base::TRIM_LEADING); ++ std::string_view trimmed; ++ if (base::StartsWith(content, base::kUtf8ByteOrderMark)) ++ trimmed = base::TrimString(content, base::kUtf8ByteOrderMark, base::TrimPositions::TRIM_LEADING); ++ else if (base::StartsWith(content, "\xFE\xFF")) ++ trimmed = base::TrimString(content, "\xFE\xFF", base::TrimPositions::TRIM_LEADING); ++ else if (base::StartsWith(content, "\xFF\xFE")) ++ trimmed = base::TrimString(content, "\xFF\xFE", base::TrimPositions::TRIM_LEADING); ++ else ++ trimmed = content; ++ ++ trimmed = base::TrimWhitespaceASCII(trimmed, base::TRIM_LEADING); + + // |trimmed| now starts at first non-whitespace character (or is empty). + return CheckForMagicNumbers(trimmed, kSniffableTags, result); +@@ -503,6 +512,7 @@ static bool SniffForInvalidOfficeDocs(std::string_view content, + static const MagicNumber kMagicXML[] = { + MAGIC_STRING("application/atom+xml", ""); + if (base::StartsWith(current, kXmlPrefix, + base::CompareCase::INSENSITIVE_ASCII) || + base::StartsWith(current, kDocTypePrefix, + base::CompareCase::INSENSITIVE_ASCII)) { + ++pos; + continue; ++ } else if (base::StartsWith(current, kCommentPrefix, ++ base::CompareCase::INSENSITIVE_ASCII)) { ++ // Skip comment ++ size_t close_comment = current.find(kCommentSuffix); ++ if (close_comment != std::string_view::npos) { ++ pos += close_comment + kCommentSuffix.length(); ++ continue; ++ } else ++ return false; // Truncated or unterminated comment, can't find any magic + } +- + if (CheckForMagicNumbers(current, kMagicXML, result)) + return true; + +@@ -727,6 +747,9 @@ bool SniffMimeType(std::string_view content, + // Cache information about the type_hint + bool hint_is_unknown_mime_type = IsUnknownMimeType(type_hint); + ++ // Did SniffForHtml() detect XML? ++ bool sniffed_xml = false; ++ + // First check for HTML, unless it's a file URL and + // |allow_sniffing_files_urls_as_html| is false. + if (hint_is_unknown_mime_type && +@@ -735,8 +758,13 @@ bool SniffMimeType(std::string_view content, + // We're only willing to sniff HTML if the server has not supplied a mime + // type, or if the type it did supply indicates that it doesn't know what + // the type should be. +- if (SniffForHTML(content, &have_enough_content, result)) +- return true; // We succeeded in sniffing HTML. No more content needed. ++ if (SniffForHTML(content, &have_enough_content, result)) { ++ if (*result == "text/xml") ++ // if we found XML, continue in case it is SVG or another subtype ++ sniffed_xml = true; ++ else ++ return true; // We succeeded in sniffing HTML. No more content needed. ++ } + } + + // We're only willing to sniff for binary in 3 cases: +@@ -746,7 +774,7 @@ bool SniffMimeType(std::string_view content, + // 3. The type is "text/plain" which is the default on some web servers and + // could be indicative of a mis-configuration that we shield the user from. + const bool hint_is_text_plain = (type_hint == "text/plain"); +- if (hint_is_unknown_mime_type || hint_is_text_plain) { ++ if ((hint_is_unknown_mime_type || hint_is_text_plain) && !sniffed_xml) { + if (!SniffBinary(content, &have_enough_content, result)) { + // If the server said the content was text/plain and it doesn't appear + // to be binary, then we trust it. +@@ -757,12 +785,14 @@ bool SniffMimeType(std::string_view content, + } + + // If we have plain XML, sniff XML subtypes. +- if (type_hint == "text/xml" || type_hint == "application/xml") { ++ if (type_hint == "text/xml" || type_hint == "application/xml" || sniffed_xml) { + // We're not interested in sniffing these types for images and the like. + // Instead, we're looking explicitly for a feed. If we don't find one + // we're done and return early. + if (SniffXML(content, &have_enough_content, result)) + return true; ++ if (sniffed_xml) // The HTML sniffer detected XML, so report result ++ return true; + return have_enough_content; + } + diff --git a/spec/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts index 517268e4d4783..c145080649ba3 100644 --- a/spec/api-web-contents-spec.ts +++ b/spec/api-web-contents-spec.ts @@ -3679,4 +3679,84 @@ describe('webContents module', () => { expect(w.getBounds().height).to.equal(height); }); }); + + describe('mime sniffing', () => { + afterEach(closeAllWindows); + it('check text/html utf8 bom', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/html-utf8-bom").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('text/html'); + }); + + it('check text/html with comments at the start of the file', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/html-with-comments").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('text/html'); + }); + + it('check text/html utf8 bom with comments at the start of the file', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/html-utf8-bom-with-comments").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('text/html'); + }); + + it('check image/svg+xml', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/svg").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('image/svg+xml'); + }); + + it('check image/svg+xml utf8 bom', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/svg-utf8-bom").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('image/svg+xml'); + }); + + it('check image/svg+xml with comments', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/svg-with-comments").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('image/svg+xml'); + }); + + it('check image/svg+xml utf8 bom with comments', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/svg-utf8-bom-with-comments").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('image/svg+xml'); + }); + + it('check text/xml', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/xml").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('text/xml'); + }); + + it('check text/xml utf8 bom', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/xml-utf8-bom").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('text/xml'); + }); + + it('check text/xml with comments', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/xml-with-comments").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('text/xml'); + }); + + it('check text/xml utf8 bom with comments', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } }); + await w.loadURL(`file://${fixturesPath}/pages/blank.html`); + const mimeType = await w.webContents.executeJavaScript(`fetch("file://${fixturesPath}/assets/mime-sniffing/xml-utf8-bom-with-comments").then(r => r.blob().then(b => b.type))`); + expect(mimeType).to.equal('text/xml'); + }); + }); }); diff --git a/spec/fixtures/assets/mime-sniffing/html-utf8-bom b/spec/fixtures/assets/mime-sniffing/html-utf8-bom new file mode 100644 index 0000000000000..15187657baede --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/html-utf8-bom @@ -0,0 +1,7 @@ + + + HTML utf8 bom + + + + diff --git a/spec/fixtures/assets/mime-sniffing/html-utf8-bom-with-comments b/spec/fixtures/assets/mime-sniffing/html-utf8-bom-with-comments new file mode 100644 index 0000000000000..584225513bf19 --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/html-utf8-bom-with-comments @@ -0,0 +1,8 @@ + + + + HTML utf8 bom with comments + + + + diff --git a/spec/fixtures/assets/mime-sniffing/html-with-comments b/spec/fixtures/assets/mime-sniffing/html-with-comments new file mode 100644 index 0000000000000..4ce67672a85d4 --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/html-with-comments @@ -0,0 +1,8 @@ + + + + HTML with comments + + + + diff --git a/spec/fixtures/assets/mime-sniffing/svg b/spec/fixtures/assets/mime-sniffing/svg new file mode 100644 index 0000000000000..373cc9068a0f6 --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/svg @@ -0,0 +1,34 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/spec/fixtures/assets/mime-sniffing/svg-utf8-bom b/spec/fixtures/assets/mime-sniffing/svg-utf8-bom new file mode 100644 index 0000000000000..63e3ddc0fb209 --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/svg-utf8-bom @@ -0,0 +1,34 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/spec/fixtures/assets/mime-sniffing/svg-utf8-bom-with-comments b/spec/fixtures/assets/mime-sniffing/svg-utf8-bom-with-comments new file mode 100644 index 0000000000000..41f66e9a83fcd --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/svg-utf8-bom-with-comments @@ -0,0 +1,93 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/spec/fixtures/assets/mime-sniffing/svg-with-comments b/spec/fixtures/assets/mime-sniffing/svg-with-comments new file mode 100644 index 0000000000000..2bf199d483205 --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/svg-with-comments @@ -0,0 +1,93 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/spec/fixtures/assets/mime-sniffing/xml b/spec/fixtures/assets/mime-sniffing/xml new file mode 100644 index 0000000000000..8da9e78d97934 --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/xml @@ -0,0 +1,5 @@ + + + + text + diff --git a/spec/fixtures/assets/mime-sniffing/xml-utf8-bom b/spec/fixtures/assets/mime-sniffing/xml-utf8-bom new file mode 100644 index 0000000000000..7fc4cde4f734a --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/xml-utf8-bom @@ -0,0 +1,5 @@ + + + + text + diff --git a/spec/fixtures/assets/mime-sniffing/xml-utf8-bom-with-comments b/spec/fixtures/assets/mime-sniffing/xml-utf8-bom-with-comments new file mode 100644 index 0000000000000..f3175990547a8 --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/xml-utf8-bom-with-comments @@ -0,0 +1,6 @@ + + + + + text + diff --git a/spec/fixtures/assets/mime-sniffing/xml-with-comments b/spec/fixtures/assets/mime-sniffing/xml-with-comments new file mode 100644 index 0000000000000..48fcd4383f7c6 --- /dev/null +++ b/spec/fixtures/assets/mime-sniffing/xml-with-comments @@ -0,0 +1,6 @@ + + + + + text + From 9a8bbc950af1c1a58d0ce22b4bfadc1f39976b3c Mon Sep 17 00:00:00 2001 From: George Joseph Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 09/68] feat : backport, Allow partitions to have app set quotas OS-14197 (#14) Description of Change Allow the passing of a quota to the session.fromPartition() API. This patch allows partitions to have a quota size, which in turn persuades the storage manager to run a garbage collector(at specific times) to free reclaimable storage space. Checklist - [x] PR description included and stakeholders cc'd - [x] npm test passes - [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md) - [x] relevant documentation is changed or added - [x] [PR release notes](https://github.com/electron/clerk/blob/master/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/master/README.md#examples). Release Notes notes: Allow the passing of a quota to the session.fromPath() API. Cherry picked from: 571273c13b662ef04ea1c624bc4c88bfb71feda3: feat : backport, Allow partitions to have app set quotas OS-14197 (#14) --- docs/api/session.md | 7 +- patches/chromium/.patches | 1 + ...rage_quota_capability_for_partitions.patch | 232 ++++++++++++++++++ shell/browser/electron_browser_context.cc | 12 + shell/browser/electron_browser_context.h | 2 + spec/api-session-spec.ts | 69 ++++++ 6 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 patches/chromium/feat_backported_add_storage_quota_capability_for_partitions.patch diff --git a/docs/api/session.md b/docs/api/session.md index c81cb24dfa3da..8013a42d8f21e 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -49,6 +49,7 @@ of an existing `Session` object. * `options` Object (optional) * `cache` boolean - Whether to enable cache. Default is `true` unless the [`--disable-http-cache` switch](command-line-switches.md#--disable-http-cache) is used. + * `quota` number (optional) - Use a quota size for a partition (in bytes). Returns `Session` - A session instance from the absolute path as specified by the `path` string. When there is an existing `Session` with the same absolute path, it @@ -58,7 +59,11 @@ be thrown if an empty string is provided. To create a `Session` with `options`, you have to ensure the `Session` with the `path` has never been used before. There is no way to change the `options` -of an existing `Session` object. +of an existing `Session` object. The optional `quota` parameter can be used to +specify the a quota size that can be used by the session, which will be returned as the quota provided by navigator.storage.estimate(). If the `quota` is not +provided, the size of the volume on which path is located will be used. A smaller +quota size will allow the LRU algorithm to evict more data from the cache, though there +is no guarantee that the cache will not exceed the quota size. ## Properties diff --git a/patches/chromium/.patches b/patches/chromium/.patches index fde81a04ddc2e..b45c739f60665 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -153,3 +153,4 @@ fix_make_macos_text_replacement_work_on_contenteditable.patch fix_constrain_allowuniversalaccessfromfileurls_to_file_origins_in.patch feat_os_14967_force_sniffing_file_urls.patch os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch +feat_backported_add_storage_quota_capability_for_partitions.patch diff --git a/patches/chromium/feat_backported_add_storage_quota_capability_for_partitions.patch b/patches/chromium/feat_backported_add_storage_quota_capability_for_partitions.patch new file mode 100644 index 0000000000000..782d76732871e --- /dev/null +++ b/patches/chromium/feat_backported_add_storage_quota_capability_for_partitions.patch @@ -0,0 +1,232 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Fri, 19 Apr 2024 17:36:50 +0100 +Subject: feat:(backported) add storage quota capability for partitions + +This patch adds the capability for a storage quota +to be set on partitions.This capability will be +exercised only when physical storage based +partitions are used. + +diff --git a/content/browser/storage_partition_impl.cc b/content/browser/storage_partition_impl.cc +index ab16e6b15905b8812be29ca8cd099d6f325a43e7..cdd510a6f397a669a91df9f69d3e62fd30783952 100644 +--- a/content/browser/storage_partition_impl.cc ++++ b/content/browser/storage_partition_impl.cc +@@ -1333,6 +1333,8 @@ void StoragePartitionImpl::Initialize( + weak_factory_.GetWeakPtr()), + report_static_storage_quota); + quota_manager_ = quota_context_->quota_manager(); ++ if (config_.quota()) ++ quota_manager_->SetQuota(config_.quota().value()); + scoped_refptr quota_manager_proxy = + quota_manager_->proxy(); + +diff --git a/content/public/browser/storage_partition_config.cc b/content/public/browser/storage_partition_config.cc +index a2fae1b19d9489bbb5c42f29de7480a29133ea6e..ff56aedfc7361645be5968f7b017161d999c149f 100644 +--- a/content/public/browser/storage_partition_config.cc ++++ b/content/public/browser/storage_partition_config.cc +@@ -21,8 +21,10 @@ StoragePartitionConfig& StoragePartitionConfig::operator=( + + // static + StoragePartitionConfig StoragePartitionConfig::CreateDefault( +- BrowserContext* browser_context) { +- return StoragePartitionConfig("", "", browser_context->IsOffTheRecord()); ++ BrowserContext* browser_context, ++ std::optional quota_size) { ++ return StoragePartitionConfig("", "", browser_context->IsOffTheRecord(), ++ quota_size); + } + + // static +@@ -30,22 +32,26 @@ StoragePartitionConfig StoragePartitionConfig::Create( + BrowserContext* browser_context, + const std::string& partition_domain, + const std::string& partition_name, +- bool in_memory) { ++ bool in_memory, ++ std::optional quota_size) { + // If a caller tries to pass an empty partition_domain something is seriously + // wrong or the calling code is not explicitly signalling its desire to create + // a default partition by calling CreateDefault(). + CHECK(!partition_domain.empty()); + return StoragePartitionConfig(partition_domain, partition_name, +- in_memory || browser_context->IsOffTheRecord()); ++ in_memory || browser_context->IsOffTheRecord(), ++ quota_size); + } + + StoragePartitionConfig::StoragePartitionConfig( + const std::string& partition_domain, + const std::string& partition_name, +- bool in_memory) ++ bool in_memory, ++ std::optional quota_size) + : partition_domain_(partition_domain), + partition_name_(partition_name), +- in_memory_(in_memory) {} ++ in_memory_(in_memory), ++ quota_size_(quota_size) {} + + std::optional + StoragePartitionConfig::GetFallbackForBlobUrls() const { +@@ -55,7 +61,8 @@ StoragePartitionConfig::GetFallbackForBlobUrls() const { + return StoragePartitionConfig( + partition_domain_, "", + /*in_memory=*/fallback_to_partition_domain_for_blob_urls_ == +- FallbackMode::kFallbackPartitionInMemory); ++ FallbackMode::kFallbackPartitionInMemory, ++ quota_size_); + } + + std::ostream& operator<<(std::ostream& out, +diff --git a/content/public/browser/storage_partition_config.h b/content/public/browser/storage_partition_config.h +index 30da39f2c648b587d0902d7ffe7e16f982ad0b95..b1b708c1982a28f40ee961d363f6b2583619f8f0 100644 +--- a/content/public/browser/storage_partition_config.h ++++ b/content/public/browser/storage_partition_config.h +@@ -28,7 +28,8 @@ class CONTENT_EXPORT StoragePartitionConfig { + + // Creates a default config for |browser_context|. If |browser_context| is an + // off-the-record profile, then the config will have |in_memory_| set to true. +- static StoragePartitionConfig CreateDefault(BrowserContext* browser_context); ++ static StoragePartitionConfig CreateDefault(BrowserContext* browser_context, ++ std::optional quota_size = std::nullopt); + + // Creates a config tied to a specific domain. + // The |partition_domain| is [a-z]* UTF-8 string, specifying the domain in +@@ -43,11 +44,13 @@ class CONTENT_EXPORT StoragePartitionConfig { + static StoragePartitionConfig Create(BrowserContext* browser_context, + const std::string& partition_domain, + const std::string& partition_name, +- bool in_memory); ++ bool in_memory, ++ std::optional quota_size = std::nullopt); + + const std::string& partition_domain() const { return partition_domain_; } + const std::string& partition_name() const { return partition_name_; } + bool in_memory() const { return in_memory_; } ++ std::optional quota() const { return quota_size_; } + + // Returns true if this config was created by CreateDefault() or is + // a copy of a config created with that method. +@@ -95,11 +98,13 @@ class CONTENT_EXPORT StoragePartitionConfig { + + StoragePartitionConfig(const std::string& partition_domain, + const std::string& partition_name, +- bool in_memory); ++ bool in_memory, ++ std::optional quota_size = std::nullopt); + + std::string partition_domain_; + std::string partition_name_; + bool in_memory_ = false; ++ std::optional quota_size_ = std::nullopt; + FallbackMode fallback_to_partition_domain_for_blob_urls_ = + FallbackMode::kNone; + }; +diff --git a/storage/browser/quota/quota_manager_impl.cc b/storage/browser/quota/quota_manager_impl.cc +index beb6c5cde9ea013e068531f08fcc384412716081..2c23db6849f9f9d2290c73ecfcb723c36ff4fabe 100644 +--- a/storage/browser/quota/quota_manager_impl.cc ++++ b/storage/browser/quota/quota_manager_impl.cc +@@ -2729,7 +2729,7 @@ void QuotaManagerImpl::GetStorageCapacity(StorageCapacityCallback callback) { + db_runner_->PostTaskAndReplyWithResult( + FROM_HERE, + base::BindOnce(&QuotaManagerImpl::CallGetVolumeInfo, get_volume_info_fn_, +- profile_path_), ++ profile_path_,start_quota_), + base::BindOnce(&QuotaManagerImpl::DidGetStorageCapacity, + weak_factory_.GetWeakPtr())); + } +@@ -3018,16 +3018,25 @@ void QuotaManagerImpl::PostTaskAndReplyWithResultForDBThread( + std::move(reply)); + } + ++void QuotaManagerImpl::SetQuota(int start_quota) { ++ // Set the quota for a browser context */ ++ // If an electron::BrowserWindow uses a partition path ++ // with no existing browser context, then ++ // this quota takes effect ++ start_quota_ = start_quota; ++} ++ + // static + QuotaAvailability QuotaManagerImpl::CallGetVolumeInfo( + GetVolumeInfoFn get_volume_info_fn, +- const base::FilePath& path) { ++ const base::FilePath& path, ++ const std::optional& quota_size) { + if (!base::CreateDirectory(path)) { + LOG(WARNING) << "Create directory failed for path" << path.value(); + return QuotaAvailability(0, 0); + } + +- const QuotaAvailability quotaAvailability = get_volume_info_fn(path); ++ const QuotaAvailability quotaAvailability = get_volume_info_fn(path,quota_size); + const auto total = quotaAvailability.total; + const auto available = quotaAvailability.available; + +@@ -3049,10 +3058,16 @@ QuotaAvailability QuotaManagerImpl::CallGetVolumeInfo( + } + + // static +-QuotaAvailability QuotaManagerImpl::GetVolumeInfo(const base::FilePath& path) { +- return QuotaAvailability( +- base::SysInfo::AmountOfTotalDiskSpace(path).value_or(-1), +- base::SysInfo::AmountOfFreeDiskSpace(path).value_or(-1)); ++QuotaAvailability QuotaManagerImpl::GetVolumeInfo( ++ const base::FilePath& path, ++ const std::optional& quota_size) { ++ if (!quota_size) { ++ return QuotaAvailability(base::SysInfo::AmountOfTotalDiskSpace(path).value_or(-1), ++ base::SysInfo::AmountOfFreeDiskSpace(path).value_or(-1)); ++ } else { ++ return QuotaAvailability(base::SysInfo::AmountOfTotalDiskSpace(path).value_or(-1), ++ quota_size.value()); ++ } + } + + void QuotaManagerImpl::AddObserver( +diff --git a/storage/browser/quota/quota_manager_impl.h b/storage/browser/quota/quota_manager_impl.h +index eac74a9ae3dcec612fee8868556d501f40c07d16..ccc5d8911b5b33438afc9eec221ec48b706103a8 100644 +--- a/storage/browser/quota/quota_manager_impl.h ++++ b/storage/browser/quota/quota_manager_impl.h +@@ -156,7 +156,8 @@ class COMPONENT_EXPORT(STORAGE_BROWSER) QuotaManagerImpl + // Function pointer type used to store the function which returns + // information about the volume containing the given FilePath. + // The value returned is the QuotaAvailability struct. +- using GetVolumeInfoFn = QuotaAvailability (*)(const base::FilePath&); ++ using GetVolumeInfoFn = QuotaAvailability (*)(const base::FilePath&, ++ const std::optional&); + + static constexpr int64_t kGBytes = 1024 * 1024 * 1024; + static constexpr int64_t kNoLimit = INT64_MAX; +@@ -469,6 +470,8 @@ class COMPONENT_EXPORT(STORAGE_BROWSER) QuotaManagerImpl + eviction_disabled_ = disable; + } + ++ void SetQuota(const int start_quota); ++ + // Testing support for handling corruption in the underlying database. + // + // Runs `corrupter` on the same sequence used to do database I/O, +@@ -753,8 +756,10 @@ class COMPONENT_EXPORT(STORAGE_BROWSER) QuotaManagerImpl + CreateDeleteBucketSetCallback(StatusCallback done); + + static QuotaAvailability CallGetVolumeInfo(GetVolumeInfoFn get_volume_info_fn, +- const base::FilePath& path); +- static QuotaAvailability GetVolumeInfo(const base::FilePath& path); ++ const base::FilePath& path, ++ const std::optional& quota_size = std::nullopt); ++ static QuotaAvailability GetVolumeInfo(const base::FilePath& path, ++ const std::optional& quota_size = std::nullopt); + + const bool is_incognito_; + const base::FilePath profile_path_; +@@ -850,6 +855,8 @@ class COMPONENT_EXPORT(STORAGE_BROWSER) QuotaManagerImpl + // QuotaManagerImpl::GetVolumeInfo. + GetVolumeInfoFn get_volume_info_fn_; + ++ std::optional start_quota_ = std::nullopt; ++ + // Whether to report static values for storage quota estimate APIs. + const bool report_static_storage_quota_; + diff --git a/shell/browser/electron_browser_context.cc b/shell/browser/electron_browser_context.cc index 4a425dc3afd7a..10d8b4109f44d 100644 --- a/shell/browser/electron_browser_context.cc +++ b/shell/browser/electron_browser_context.cc @@ -386,6 +386,9 @@ ElectronBrowserContext::ElectronBrowserContext( &partition_location)) { const base::FilePath& partition_path = filepath_partition->get(); path_ = std::move(partition_path); + if (auto quota_size_opt = options.FindInt("quota")) { + user_set_quota_ = quota_size_opt.value(); + } } BrowserContextDependencyManager::GetInstance()->MarkBrowserContextLive(this); @@ -426,6 +429,15 @@ ElectronBrowserContext::~ElectronBrowserContext() { ShutdownStoragePartitions(); } +content::StoragePartition* +ElectronBrowserContext::GetDefaultStoragePartition() { + if (user_set_quota_) { + return GetStoragePartition( + content::StoragePartitionConfig::CreateDefault(this, user_set_quota_)); + } + return BrowserContext::GetDefaultStoragePartition(); +} + void ElectronBrowserContext::InitPrefs() { auto prefs_path = GetPath().Append(FILE_PATH_LITERAL("Preferences")); ScopedAllowBlockingForElectron allow_blocking; diff --git a/shell/browser/electron_browser_context.h b/shell/browser/electron_browser_context.h index a9c59761f12b2..2f9d9a7875e11 100644 --- a/shell/browser/electron_browser_context.h +++ b/shell/browser/electron_browser_context.h @@ -113,6 +113,7 @@ class ElectronBrowserContext : public content::BrowserContext { override; content::DownloadManagerDelegate* GetDownloadManagerDelegate() override; content::BrowserPluginGuestManager* GetGuestManager() override; + content::StoragePartition* GetDefaultStoragePartition(); content::PlatformNotificationService* GetPlatformNotificationService() override; content::PermissionControllerDelegate* GetPermissionControllerDelegate() @@ -215,6 +216,7 @@ class ElectronBrowserContext : public content::BrowserContext { base::FilePath path_; bool in_memory_ = false; bool use_cache_ = true; + std::optional user_set_quota_ = std::nullopt; int max_cache_size_ = 0; // Shared URLLoaderFactory. diff --git a/spec/api-session-spec.ts b/spec/api-session-spec.ts index d4a17bc1b5b26..7a38d7d00ed21 100644 --- a/spec/api-session-spec.ts +++ b/spec/api-session-spec.ts @@ -9,6 +9,7 @@ import { once } from 'node:events'; import * as fs from 'node:fs'; import * as http from 'node:http'; import * as https from 'node:https'; +import * as os from 'node:os'; import * as path from 'node:path'; import { setTimeout } from 'node:timers/promises'; @@ -39,6 +40,74 @@ describe('session module', () => { }); }); + describe('session.fromPath(path) with a quota.', () => { + const pathloc = 'testsession'; + const tmppath = require('electron').app.getPath('temp') + path.sep + pathloc; + + after(() => { + if (fs.existsSync(tmppath)) { fs.rmSync(tmppath, { recursive: true, force: true }); } + }); + it('Assert that a set quota value is returned when a quota value is specified for a session', async () => { + const quotasize = 256000; + if (fs.existsSync(tmppath)) { fs.rmSync(tmppath, { recursive: true, force: true }); } + fs.mkdirSync(tmppath); + const localsession = session.fromPath(tmppath, { quota: quotasize, cache: true }); + const w = new BrowserWindow({ + show: false, + webPreferences: { + session: localsession + } + }); + + const readQuotaSize: any = () => { + return w.webContents.executeJavaScript(` + navigator.storage.estimate().then(estimate => estimate.quota).catch(err => err.message); + `); + }; + + await w.loadFile(path.join(fixtures, 'api', 'localstorage.html')); + const size = await readQuotaSize(); + expect(size).to.equal(quotasize); + }); + }); + + /* Note : This test cannot be up-streamed */ + describe('session.fromPath(path) with no quota.', () => { + const pathloc = 'sessionnoquota'; + const tmppath = require('electron').app.getPath('temp') + path.sep + pathloc; + + after(() => { + if (fs.existsSync(tmppath)) { fs.rmSync(tmppath, { recursive: true, force: true }); } + }); + it('Assert that free space value is returned when a quota value is not specified for a session', async () => { + if (fs.existsSync(tmppath)) { fs.rmSync(tmppath, { recursive: true, force: true }); } + fs.mkdirSync(tmppath); + const localsession = session.fromPath(tmppath); + const w = new BrowserWindow({ + show: false, + webPreferences: { + session: localsession + } + }); + + const readQuotaSize: any = () => { + return w.webContents.executeJavaScript(` + navigator.storage.estimate().then(estimate => estimate.quota).catch(err => err.message); + `); + }; + + await w.loadFile(path.join(fixtures, 'api', 'localstorage.html')); + const size = await readQuotaSize(); + expect(size).to.be.greaterThan(0); + + if (os.platform() === 'linux') { + const output = ChildProcess.execSync(`df ${tmppath} --output=avail | tail -n1`); + const availablespace = parseInt(output.toString(), 10) * 1024; + expect(availablespace).to.be.greaterThan(0); + } + }); + }); + describe('ses.cookies', () => { const name = '0'; const value = '0'; From 95cbb07a57ca7d6db449b9174c9f97e835ebf5cd Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 10/68] feat: Add BrightSign custom set-z-index feature to Wayland (#17) * wayland: Add BrightSign custom set-z-index feature A custom feature has been added to BrightSign Weston server to enable a z-index to be set to top level xdg windows. This patch adds the functionality into third_party/wayland and chromium ui/ozone/platform/wayland. * wayland: Add fixup patch to add missing include * fixup: Fix for storage quota test case Cherry picked from: 0cf7a25d92c25076bb460fd23a7109d944917e93: feat: Add BrightSign custom set-z-index feature to Wayland (#17) --- patches/chromium/.patches | 1 + ...et_z_order_custom_feature_to_wayland.patch | 210 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 patches/chromium/OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index b45c739f60665..a0e2a51d72275 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -154,3 +154,4 @@ fix_constrain_allowuniversalaccessfromfileurls_to_file_origins_in.patch feat_os_14967_force_sniffing_file_urls.patch os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch feat_backported_add_storage_quota_capability_for_partitions.patch +OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch diff --git a/patches/chromium/OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch b/patches/chromium/OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch new file mode 100644 index 0000000000000..4d0c7249355dc --- /dev/null +++ b/patches/chromium/OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch @@ -0,0 +1,210 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Fri, 19 Apr 2024 19:01:06 +0100 +Subject: OS-15118: add bs set z order custom feature to wayland + +A custom feature has been added to BrightSign Weston server to enable +a z-index to be set to top level xdg windows. +This patch adds the functionality into third_party/wayland-protocols and +chromium ui/ozone/platform/wayland. + +diff --git a/third_party/wayland-protocols/BUILD.gn b/third_party/wayland-protocols/BUILD.gn +index 1ff4af39ff051be46708b2e166505a42a98d2d31..24258a6b21e5ea5104e587b7d16f5d9abc9cb935 100644 +--- a/third_party/wayland-protocols/BUILD.gn ++++ b/third_party/wayland-protocols/BUILD.gn +@@ -11,6 +11,10 @@ wayland_protocol("alpha_compositing_protocol") { + sources = [ "unstable/alpha-compositing/alpha-compositing-unstable-v1.xml" ] + } + ++wayland_protocol("bs_z_order_protocol") { ++ sources = [ "unstable/bs-z-order/bs-z-order-unstable-v1.xml" ] ++} ++ + wayland_protocol("color_management_protocol") { + sources = [ "src/staging/color-management/color-management-v1.xml" ] + } +diff --git a/third_party/wayland-protocols/unstable/bs-z-order/bs-z-order-unstable-v1.xml b/third_party/wayland-protocols/unstable/bs-z-order/bs-z-order-unstable-v1.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..da7ae7a6e20242d1593d5cfcc4bb41ba16f2e775 +--- /dev/null ++++ b/third_party/wayland-protocols/unstable/bs-z-order/bs-z-order-unstable-v1.xml +@@ -0,0 +1,14 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/ui/ozone/platform/wayland/BUILD.gn b/ui/ozone/platform/wayland/BUILD.gn +index 2fa1868b74183bc1eb79b3ff8c273858499fbda2..a4039cd2ed12ed2e95dc876582998e83a6e05ee4 100644 +--- a/ui/ozone/platform/wayland/BUILD.gn ++++ b/ui/ozone/platform/wayland/BUILD.gn +@@ -252,6 +252,7 @@ source_set("wayland") { + "//third_party/wayland:wayland_egl", + "//third_party/wayland:wayland_util", + "//third_party/wayland-protocols:alpha_compositing_protocol", ++ "//third_party/wayland-protocols:bs_z_order_protocol", + "//third_party/wayland-protocols:color_management_protocol", + "//third_party/wayland-protocols:content_type_protocol", + "//third_party/wayland-protocols:cursor_shape_protocol", +diff --git a/ui/ozone/platform/wayland/common/wayland_object.cc b/ui/ozone/platform/wayland/common/wayland_object.cc +index e29910a05b209a988a3bdd87ccfca621c68e4b21..d4c5b9c27377367526c51b4f9a3c5aff84ffd755 100644 +--- a/ui/ozone/platform/wayland/common/wayland_object.cc ++++ b/ui/ozone/platform/wayland/common/wayland_object.cc +@@ -149,6 +149,7 @@ void (*ObjectTraits::deleter)(void*) = &wl_proxy_wrapper_destroy; + IMPLEMENT_WAYLAND_OBJECT_TRAITS_WITH_DELETER(TYPE, TYPE##_destroy) + + // For convenience, keep aphabetical order in this list. ++IMPLEMENT_WAYLAND_OBJECT_TRAITS(bs_z_order_v1) + IMPLEMENT_WAYLAND_OBJECT_TRAITS(gtk_primary_selection_device) + IMPLEMENT_WAYLAND_OBJECT_TRAITS(gtk_primary_selection_device_manager) + IMPLEMENT_WAYLAND_OBJECT_TRAITS(gtk_primary_selection_offer) +diff --git a/ui/ozone/platform/wayland/common/wayland_object.h b/ui/ozone/platform/wayland/common/wayland_object.h +index dd46b63c0e63e847eba90dc355c5ee8641287682..1c8bcacf3176ca491a8c4682d9cb276eb3fa1f87 100644 +--- a/ui/ozone/platform/wayland/common/wayland_object.h ++++ b/ui/ozone/platform/wayland/common/wayland_object.h +@@ -6,6 +6,7 @@ + #define UI_OZONE_PLATFORM_WAYLAND_COMMON_WAYLAND_OBJECT_H_ + + #include ++#include + + #include "base/check.h" + #include "ui/ozone/platform/wayland/common/wayland.h" +@@ -103,6 +104,7 @@ bool CanBind(const std::string& interface, + } // namespace wl + + // For convenience, keep aphabetical order in this list. ++DECLARE_WAYLAND_OBJECT_TRAITS(bs_z_order_v1) + DECLARE_WAYLAND_OBJECT_TRAITS(gtk_primary_selection_device) + DECLARE_WAYLAND_OBJECT_TRAITS(gtk_primary_selection_device_manager) + DECLARE_WAYLAND_OBJECT_TRAITS(gtk_primary_selection_offer) +diff --git a/ui/ozone/platform/wayland/host/wayland_connection.cc b/ui/ozone/platform/wayland/host/wayland_connection.cc +index 224f6abfe06794d31fc4d876c8242dab79ba075d..33749fd43e86e005313acfb29c501d0662795c21 100644 +--- a/ui/ozone/platform/wayland/host/wayland_connection.cc ++++ b/ui/ozone/platform/wayland/host/wayland_connection.cc +@@ -8,6 +8,7 @@ + #include + #include + #include ++#include + + #include + +@@ -79,6 +80,7 @@ namespace { + // The maximum supported versions for a given interface. + // The version bound will be the minimum of the value and the version + // advertised by the server. ++constexpr uint32_t kMaxBsZOrderVersion = 1; + constexpr uint32_t kMaxCompositorVersion = 4; + constexpr uint32_t kMaxKeyboardExtensionVersion = 2; + constexpr uint32_t kMaxXdgShellVersion = 6; +@@ -567,7 +569,7 @@ void WaylandConnection::OnGlobal(void* data, + uint32_t version) { + auto* self = static_cast(data); + DCHECK(self); +- self->HandleGlobal(registry, name, interface, version); ++ self->HandleGlobal(data, registry, name, interface, version); + } + + // static +@@ -605,13 +607,24 @@ void WaylandConnection::OnClockId(void* data, + connection->presentation_clk_id_ = clk_id; + } + +-void WaylandConnection::HandleGlobal(wl_registry* registry, ++void WaylandConnection::HandleGlobal(void* data, ++ wl_registry* registry, + uint32_t name, + const char* interface, + uint32_t version) { ++ auto* connection = static_cast(data); ++ + auto factory_it = global_object_factories_.find(interface); + if (factory_it != global_object_factories_.end()) { + (*factory_it->second)(this, registry, name, interface, version); ++ } else if (!connection->bs_z_order_v1_ && ++ UNSAFE_TODO(strcmp(interface, "bs_z_order_v1")) == 0) { ++ connection->bs_z_order_v1_ = wl::Bind( ++ registry, name, std::min(version, kMaxBsZOrderVersion)); ++ if (!connection->bs_z_order_v1_) { ++ LOG(ERROR) << "Failed to bind to bs_z_order_v1 global"; ++ return; ++ } + } else if (!compositor_ && + UNSAFE_TODO(strcmp(interface, "wl_compositor")) == 0) { + compositor_ = wl::Bind( +diff --git a/ui/ozone/platform/wayland/host/wayland_connection.h b/ui/ozone/platform/wayland/host/wayland_connection.h +index 241a1e5ae9c5c176d8424b08d6f691f2e9fa94b3..88c319881565344bf5444b9723480d3525dcf5bc 100644 +--- a/ui/ozone/platform/wayland/host/wayland_connection.h ++++ b/ui/ozone/platform/wayland/host/wayland_connection.h +@@ -91,6 +91,8 @@ class WaylandConnection { + // error. Called by WaylandEventWatcher. + void SetShutdownCb(base::OnceCallback shutdown_cb); + ++ bs_z_order_v1* bs_z_order() const { return bs_z_order_v1_.get(); } ++ + wl_compositor* compositor() const { return compositor_.get(); } + // The server version of the compositor interface (might be higher than the + // version binded). +@@ -387,7 +389,8 @@ class WaylandConnection { + wp_presentation* presentation, + uint32_t clk_id); + +- void HandleGlobal(wl_registry* registry, ++ void HandleGlobal(void* data, ++ wl_registry* registry, + uint32_t name, + const char* interface, + uint32_t version); +@@ -395,6 +398,7 @@ class WaylandConnection { + base::flat_map global_object_factories_; + + uint32_t compositor_version_ = 0; ++ wl::Object bs_z_order_v1_; + wl::Object display_; + // `event_queue_` must be declared before `wrapped_display_`, so that the + // latter is destroyed first. This prevents libwayland warnings about the +diff --git a/ui/ozone/platform/wayland/host/xdg_toplevel.cc b/ui/ozone/platform/wayland/host/xdg_toplevel.cc +index c85b59a99437b0cb6d0ec033eddaeffbd50661a5..77e7334416ea8ad36f4feb4344b157d7307a6428 100644 +--- a/ui/ozone/platform/wayland/host/xdg_toplevel.cc ++++ b/ui/ozone/platform/wayland/host/xdg_toplevel.cc +@@ -395,4 +395,10 @@ void XdgToplevel::SetIcon(const gfx::ImageSkia& icon) { + xdg_toplevel_icon_v1_destroy(xdg_icon); + } + ++void XdgToplevel::SetZOrder(ZOrderLevel z_order) { ++ if (connection()->bs_z_order()) { ++ // The z-index we get if we are using xdg on a BS device is an absolute value. ++ bs_z_order_v1_set_z_index(connection()->bs_z_order(), window()->root_surface()->surface(), (int)z_order); ++ } ++} + } // namespace ui +diff --git a/ui/ozone/platform/wayland/host/xdg_toplevel.h b/ui/ozone/platform/wayland/host/xdg_toplevel.h +index 9dbbbd96f0a13d213c55aa112ea5d77e3910b312..1c6efcd01195f3c5e1efa3405226da87c8e1ea2a 100644 +--- a/ui/ozone/platform/wayland/host/xdg_toplevel.h ++++ b/ui/ozone/platform/wayland/host/xdg_toplevel.h +@@ -12,6 +12,7 @@ + #include + + #include "base/memory/raw_ptr.h" ++#include "ui/base/ui_base_types.h" + #include "ui/gfx/geometry/rect.h" + #include "ui/gfx/image/image_skia.h" + #include "ui/ozone/platform/wayland/common/wayland_object.h" +@@ -57,6 +58,7 @@ class XdgToplevel { + void SetDecoration(DecorationMode decoration); + void SetSystemModal(bool modal); + void SetIcon(const gfx::ImageSkia& icon); ++ void SetZOrder(ZOrderLevel z_order); + + struct xdg_surface* xdg_surface() const { return xdg_surface_->wl_object(); } + struct xdg_toplevel* wl_object() const { return xdg_toplevel_.get(); } From f315b34e4840aa8a65745566c6e26b8bab16dbe8 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 11/68] OS-14339: Support negative window offsets (#18) Patch to set the x & y offsets from the window bounds in the SetWindowGeometry call. Cherry picked from: d78a7110a301fa711388cb1ba368a5077427c01b: OS-14339: Support negative window offsets (#18) f1f1bc63d7c228d90e08a2829d228efa91a365d2: fix: Always check bounds are not 0 in SetWindowGeometry OS-17405 (#59) --- patches/chromium/.patches | 1 + ...pass_window_boundary_x_and_y_offsets.patch | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 patches/chromium/os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index a0e2a51d72275..c3fe8607ffda8 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -155,3 +155,4 @@ feat_os_14967_force_sniffing_file_urls.patch os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch feat_backported_add_storage_quota_capability_for_partitions.patch OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch +os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch diff --git a/patches/chromium/os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch b/patches/chromium/os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch new file mode 100644 index 0000000000000..3614822269eee --- /dev/null +++ b/patches/chromium/os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch @@ -0,0 +1,52 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir +Date: Thu, 11 May 2023 17:53:08 +0100 +Subject: OS-14339: BrightSign change to pass window boundary x and y offsets + to SetGeometry + +Patch to set the x & y offsets from the window bounds in the SetWindowGeometry call. +This ensures that the offsets are passed to the Wayland compositor through the +xdg_surface_set_window_geometry call. This is because we don't support aura shell +and hence cannot use RequestWindowBounds calls to set the offset. +Also, some BrightSign customers create windows with zero width and height bounds. +This change also ensures that the bounds are always at least 1 pixel in size. + +diff --git a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc +index 072d543d347103393b1eb23f23f13f90aa110125..33b7d01af0da3564fa6ddc4f5467d9727d4adf17 100644 +--- a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc ++++ b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc +@@ -652,17 +652,24 @@ void WaylandToplevelWindow::SetWindowGeometry( + auto insets_dip = delegate()->CalculateInsetsInDIP(state.window_state); + if (!insets_dip.IsEmpty()) { + geometry_dip.Inset(insets_dip); +- +- // Shrinking the bounds by the decoration insets might result in empty +- // bounds. For the reasons already explained in WaylandWindow::Initialize(), +- // we mustn't request an empty window geometry. +- if (geometry_dip.width() == 0) { +- geometry_dip.set_width(1); +- } +- if (geometry_dip.height() == 0) { +- geometry_dip.set_height(1); +- } + } ++ // Always check if the bounds are zero, this can happen for certain BrightSign ++ // customer scenarios where the window is created with zero width or height ++ // bounds. For the reasons already explained in WaylandWindow::Initialize(), ++ // we mustn't request an empty window geometry. ++ if (geometry_dip.width() == 0) { ++ geometry_dip.set_width(1); ++ } ++ if (geometry_dip.height() == 0) { ++ geometry_dip.set_height(1); ++ } ++ ++ // BrightSign modification to set the x & y offsets from the window bounds ++ // in the SetWindowGeometry call. This is because we don't support aura shell ++ // and hence cannot use RequestWindowBounds calls to set the offset. ++ geometry_dip.set_x(GetBoundsInDIP().x()); ++ geometry_dip.set_y(GetBoundsInDIP().y()); ++ + xdg_toplevel_->SetWindowGeometry(geometry_dip); + } + From e4f3087cef4ef1aef8f73ae987e5c31be36fdfcc Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 12/68] feat: Add hideScrollBars BrowserWindow constructor param (#19) * OS-15351: Add hideScrollBars BrowserWindow constructor param * tests: Add hideScrollBars test case to BrowserWindow spec * tests: Add vertical scrollbar check Cherry picked from: f59a20c6e4d620d34626659fc17718924ced44ea: feat: Add hideScrollBars BrowserWindow constructor param (#19) --- docs/api/structures/web-preferences.md | 2 ++ shell/browser/web_contents_preferences.cc | 3 ++ shell/browser/web_contents_preferences.h | 1 + shell/common/options_switches.h | 2 ++ spec/api-browser-window-spec.ts | 37 +++++++++++++++++++++++ spec/fixtures/pages/scrollbars.html | 18 +++++++++++ 6 files changed, 63 insertions(+) create mode 100644 spec/fixtures/pages/scrollbars.html diff --git a/docs/api/structures/web-preferences.md b/docs/api/structures/web-preferences.md index e8393585fe6e3..3f458729e29ef 100644 --- a/docs/api/structures/web-preferences.md +++ b/docs/api/structures/web-preferences.md @@ -159,6 +159,8 @@ * `enableDeprecatedPaste` boolean (optional) _Deprecated_ - Whether to enable the `paste` [execCommand](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand). Default is `false`. * `focusOnNavigation` boolean (optional) - Whether to focus the WebContents when navigating. Default is `true`. +* `hideScrollBars` boolean (optional) - Sets the Chromium WebPreferences + hide_scrollbars field. Default is `false`. [chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment [runtime-enabled-features]: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/runtime_enabled_features.json5 diff --git a/shell/browser/web_contents_preferences.cc b/shell/browser/web_contents_preferences.cc index da2fe80b8aa5b..de84a712d0084 100644 --- a/shell/browser/web_contents_preferences.cc +++ b/shell/browser/web_contents_preferences.cc @@ -123,6 +123,7 @@ void WebContentsPreferences::Clear() { text_areas_are_resizable_ = true; webgl_ = true; enable_preferred_size_mode_ = false; + hide_scroll_bars_ = false; web_security_ = true; allow_running_insecure_content_ = false; offscreen_ = false; @@ -183,6 +184,7 @@ void WebContentsPreferences::SetFromDictionary( web_preferences.Get(options::kWebGL, &webgl_); web_preferences.Get(options::kEnablePreferredSizeMode, &enable_preferred_size_mode_); + web_preferences.Get(options::kHideScrollBars, &hide_scroll_bars_); web_preferences.Get(options::kWebSecurity, &web_security_); if (!web_preferences.Get(options::kAllowRunningInsecureContent, &allow_running_insecure_content_) && @@ -471,6 +473,7 @@ void WebContentsPreferences::OverrideWebkitPrefs( prefs->v8_cache_options = v8_cache_options_; prefs->dom_paste_enabled = deprecated_paste_enabled_; + prefs->hide_scrollbars = hide_scroll_bars_; } WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPreferences); diff --git a/shell/browser/web_contents_preferences.h b/shell/browser/web_contents_preferences.h index 2ae8bac18bf24..d8566bbdf5684 100644 --- a/shell/browser/web_contents_preferences.h +++ b/shell/browser/web_contents_preferences.h @@ -110,6 +110,7 @@ class WebContentsPreferences bool text_areas_are_resizable_; bool webgl_; bool enable_preferred_size_mode_; + bool hide_scroll_bars_; bool web_security_; bool allow_running_insecure_content_; bool offscreen_; diff --git a/shell/common/options_switches.h b/shell/common/options_switches.h index d362b26c4f4d0..9700619c1cb08 100644 --- a/shell/common/options_switches.h +++ b/shell/common/options_switches.h @@ -176,6 +176,8 @@ inline constexpr std::string_view kSandbox = "sandbox"; inline constexpr std::string_view kWebSecurity = "webSecurity"; +inline constexpr std::string_view kHideScrollBars = "hideScrollBars"; + inline constexpr std::string_view kAllowRunningInsecureContent = "allowRunningInsecureContent"; diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts index a9a7c4e944f4b..f7aa163affdaa 100755 --- a/spec/api-browser-window-spec.ts +++ b/spec/api-browser-window-spec.ts @@ -7493,4 +7493,41 @@ describe('BrowserWindow module', () => { await screenCapture.expectColorAtCenterMatches(HexColors.BLUE); }); }); + + describe('hideScrollBars webpreference', () => { + const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures'); + let w: BrowserWindow; + + afterEach(closeAllWindows); + + it('shows scrollbar by default', async () => { + w = new BrowserWindow({ width: 400, height: 400 }); + const url = `file://${fixturesPath}/pages/scrollbars.html`; + await w.loadURL(url); + const { scrollWidth } = await w.webContents.executeJavaScript('({scrollWidth: window.innerWidth - document.documentElement.clientWidth})'); + expect(scrollWidth).to.be.greaterThan(0); + const { scrollHeight } = await w.webContents.executeJavaScript('({scrollHeight: window.innerHeight - document.documentElement.clientHeight})'); + expect(scrollHeight).to.be.greaterThan(0); + }); + + it('shows scrollbar when set to false', async () => { + w = new BrowserWindow({ width: 400, height: 400, webPreferences: { hideScrollBars: false } }); + const url = `file://${fixturesPath}/pages/scrollbars.html`; + await w.loadURL(url); + const { scrollWidth } = await w.webContents.executeJavaScript('({scrollWidth: window.innerWidth - document.documentElement.clientWidth})'); + expect(scrollWidth).to.be.greaterThan(0); + const { scrollHeight } = await w.webContents.executeJavaScript('({scrollHeight: window.innerHeight - document.documentElement.clientHeight})'); + expect(scrollHeight).to.be.greaterThan(0); + }); + + it('hides scrollbar when set to true', async () => { + w = new BrowserWindow({ width: 400, height: 400, webPreferences: { hideScrollBars: true } }); + const url = `file://${fixturesPath}/pages/scrollbars.html`; + await w.loadURL(url); + const { scrollWidth } = await w.webContents.executeJavaScript('({scrollWidth: window.innerWidth - document.documentElement.clientWidth})'); + expect(scrollWidth).to.be.equal(0); + const { scrollHeight } = await w.webContents.executeJavaScript('({scrollHeight: window.innerHeight - document.documentElement.clientHeight})'); + expect(scrollHeight).to.be.equal(0); + }); + }); }); diff --git a/spec/fixtures/pages/scrollbars.html b/spec/fixtures/pages/scrollbars.html new file mode 100644 index 0000000000000..432309524da45 --- /dev/null +++ b/spec/fixtures/pages/scrollbars.html @@ -0,0 +1,18 @@ + + + + + Scrollbars Test + + + +

Scrollbars Test

+

This is some long content that will cause scrolling.

+ + \ No newline at end of file From 27a16c4a57e392fb3adeecb213c1ebad69ac79b4 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 13/68] feat: Use remote-debugging-address in devtools (#26) Use remote-debugging-address in devtools Cherry picked from: 6127ae41fe79706dbaebf7c04f9bb186e10b30c8: feat: Use remote-debugging-address in devtools (#26) --- patches/chromium/.patches | 1 + ...ebugging-address_to_content_switches.patch | 39 +++++++++++++++++++ shell/browser/ui/devtools_manager_delegate.cc | 11 +++++- spec/chromium-spec.ts | 32 +++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 patches/chromium/os-14354_brightsign_add_remote-debugging-address_to_content_switches.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index c3fe8607ffda8..54b5f785ae3c5 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -156,3 +156,4 @@ os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch feat_backported_add_storage_quota_capability_for_partitions.patch OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch +os-14354_brightsign_add_remote-debugging-address_to_content_switches.patch diff --git a/patches/chromium/os-14354_brightsign_add_remote-debugging-address_to_content_switches.patch b/patches/chromium/os-14354_brightsign_add_remote-debugging-address_to_content_switches.patch new file mode 100644 index 0000000000000..b0a125725be5d --- /dev/null +++ b/patches/chromium/os-14354_brightsign_add_remote-debugging-address_to_content_switches.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir +Date: Wed, 28 Jun 2023 16:37:03 +0100 +Subject: OS-14354 BrightSign: add remote-debugging-address to content switches + +A change has been added to Electron TCPServerSocketFactory to allow a custom +address to be used for remote debugging (i.e. use 0.0.0.0 instead of 127.0.0.1). +This change adds the remote-debugging-address switch to the content switches to +allow it to be used in Electron. + +diff --git a/content/public/common/content_switches.cc b/content/public/common/content_switches.cc +index c76de1882a425165cfed86a5fcc2cbe411ead5de..23d0ee01c1525f8f72ac685647ff971b696298f7 100644 +--- a/content/public/common/content_switches.cc ++++ b/content/public/common/content_switches.cc +@@ -603,6 +603,12 @@ const char kReduceUserAgentMinorVersion[] = "reduce-user-agent-minor-version"; + // "JSON" (the default) or "CBOR". + const char kRemoteDebuggingPipe[] = "remote-debugging-pipe"; + ++// Use the given address instead of the default loopback for accepting remote ++// debugging connections. Should be used together with --remote-debugging-port. ++// Note that the remote debugging protocol does not perform any authentication, ++// so exposing it too widely can be a security risk. ++const char kRemoteDebuggingAddress[] = "remote-debugging-address"; ++ + // Enables remote debug over HTTP on the specified port. + const char kRemoteDebuggingPort[] = "remote-debugging-port"; + +diff --git a/content/public/common/content_switches.h b/content/public/common/content_switches.h +index adc3f831bdea24ad1ec557b3e908b65d6398dca4..b39afedceb38fbe90a2fa4e64373af45f1a4ebb3 100644 +--- a/content/public/common/content_switches.h ++++ b/content/public/common/content_switches.h +@@ -171,6 +171,7 @@ CONTENT_EXPORT extern const char kReduceAcceptLanguageHTTP[]; + CONTENT_EXPORT extern const char kReduceUserAgentMinorVersion[]; + CONTENT_EXPORT extern const char kRemoteDebuggingPipe[]; + CONTENT_EXPORT extern const char kRemoteDebuggingPort[]; ++CONTENT_EXPORT extern const char kRemoteDebuggingAddress[]; + CONTENT_EXPORT extern const char kRemoteAllowOrigins[]; + CONTENT_EXPORT extern const char kRendererClientId[]; + extern const char kRendererCmdPrefix[]; diff --git a/shell/browser/ui/devtools_manager_delegate.cc b/shell/browser/ui/devtools_manager_delegate.cc index 2a5f9880817c8..bab78166e2055 100644 --- a/shell/browser/ui/devtools_manager_delegate.cc +++ b/shell/browser/ui/devtools_manager_delegate.cc @@ -78,7 +78,16 @@ std::unique_ptr CreateSocketFactory() { DLOG(WARNING) << "Invalid http debugger port number " << temp_port; } } - return std::make_unique("127.0.0.1", port); + net::IPAddress address; + std::string address_str = "127.0.0.1"; + if (command_line.HasSwitch(switches::kRemoteDebuggingAddress)) { + address_str = + command_line.GetSwitchValueASCII(switches::kRemoteDebuggingAddress); + if (!address.AssignFromIPLiteral(address_str)) { + DLOG(WARNING) << "Invalid devtools server address: " << address_str; + } + } + return std::make_unique(address_str, port); } const char kBrowserCloseMethod[] = "Browser.close"; diff --git a/spec/chromium-spec.ts b/spec/chromium-spec.ts index cab2b8252230e..f4de837bd12c4 100644 --- a/spec/chromium-spec.ts +++ b/spec/chromium-spec.ts @@ -733,6 +733,38 @@ describe('command line switches', () => { ); }); }); + + describe('--remote-debugging-address and remote-debugging-port switches', () => { + it('should display the discovery page', (done) => { + const electronPath = process.execPath; + let output = ''; + appProcess = ChildProcess.spawn(electronPath, ['--remote-debugging-address=0.0.0.0', '--remote-debugging-port=']); + appProcess.stdout.on('data', (data) => { + console.log(data); + }); + + appProcess.stderr.on('data', (data) => { + console.log(data); + output += data; + const m = /DevTools listening on ws:\/\/0.0.0.0:(\d+)\//.exec(output); + if (m) { + appProcess!.stderr.removeAllListeners('data'); + const port = m[1]; + http.get(`http://127.0.0.1:${port}`, (res) => { + try { + expect(res.statusCode).to.eql(200); + expect(parseInt(res.headers['content-length']!)).to.be.greaterThan(0); + done(); + } catch (e) { + done(e); + } finally { + res.destroy(); + } + }); + } + }); + }); + }); }); describe('chromium features', () => { From c754c5d5e384be116b1431653d16a02eaa42f605 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 14/68] feat: Add a webpreference to disable pinch to zoom (#24) OS-14345: Add a webpreference to disable pinch to zoom In chromium pinch to zoom can be disabled globally using the command line switch disable-pinch. BrightSign has a requirement to beable to disable pinch to zoom at a browser window level. This patch adds enable_pinch_zoom as a webpreference and sets its value in RenderWidgetHostViewEventHandler. The webpreference will present on all platforms, but the change to set it is only present in aura (linux). The webpreference will only be used if disable-pinch has not been set on the command line. Cherry picked from: 370cbab97cf9226c1512ae5f2a4e505601395b5c: feat: Add a webpreference to disable pinch to zoom (#24) --- docs/api/structures/web-preferences.md | 1 + patches/chromium/.patches | 1 + ...bpreference_to_disable_pinch_to_zoom.patch | 81 +++++++++++++++++++ shell/browser/web_contents_preferences.cc | 3 + shell/browser/web_contents_preferences.h | 1 + shell/common/options_switches.h | 2 + 6 files changed, 89 insertions(+) create mode 100644 patches/chromium/os-14345_brightsign_add_a_webpreference_to_disable_pinch_to_zoom.patch diff --git a/docs/api/structures/web-preferences.md b/docs/api/structures/web-preferences.md index 3f458729e29ef..b25747f25e202 100644 --- a/docs/api/structures/web-preferences.md +++ b/docs/api/structures/web-preferences.md @@ -161,6 +161,7 @@ when navigating. Default is `true`. * `hideScrollBars` boolean (optional) - Sets the Chromium WebPreferences hide_scrollbars field. Default is `false`. +* `enablePinchZoom` boolean (optional) - Sets whether to enable pinch to zoom in Chromium. Default is `true`. [chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment [runtime-enabled-features]: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/runtime_enabled_features.json5 diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 54b5f785ae3c5..5647dcf1a1a5b 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -157,3 +157,4 @@ feat_backported_add_storage_quota_capability_for_partitions.patch OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch os-14354_brightsign_add_remote-debugging-address_to_content_switches.patch +os-14345_brightsign_add_a_webpreference_to_disable_pinch_to_zoom.patch diff --git a/patches/chromium/os-14345_brightsign_add_a_webpreference_to_disable_pinch_to_zoom.patch b/patches/chromium/os-14345_brightsign_add_a_webpreference_to_disable_pinch_to_zoom.patch new file mode 100644 index 0000000000000..b78f5421bc7fb --- /dev/null +++ b/patches/chromium/os-14345_brightsign_add_a_webpreference_to_disable_pinch_to_zoom.patch @@ -0,0 +1,81 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir +Date: Thu, 22 Jun 2023 14:23:48 +0100 +Subject: OS-14345: BrightSign Add a webpreference to disable pinch to zoom + +In chromium pinch to zoom can be disabled globally using the command line switch +disable-pinch. BrightSign has a requirement to beable to disable pinch to zoom +at a browser window level. This patch adds enable_pinch_zoom as a webpreference +and sets its value in RenderWidgetHostViewEventHandler. The webpreference will +present on all platforms, but the change to set it is only present in aura (linux). +The webpreference will only be used if disable-pinch has not been set on the +command line. + +diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc +index 30218b43ce1d57e45e9c265de4edcdce496cda79..ab18b92618d5a05055ff8c6428b5c79982875b43 100644 +--- a/content/browser/renderer_host/render_widget_host_view_aura.cc ++++ b/content/browser/renderer_host/render_widget_host_view_aura.cc +@@ -378,6 +378,8 @@ RenderWidgetHostViewAura::RenderWidgetHostViewAura( + double_tap_to_zoom_enabled_ = + owner_delegate->GetWebkitPreferencesForWidget() + .double_tap_to_zoom_enabled; ++ ++ event_handler_->SetPinchZoomEnabled(owner_delegate->GetWebkitPreferencesForWidget().enable_pinch_zoom); + } + + #if BUILDFLAG(IS_WIN) +diff --git a/content/browser/renderer_host/render_widget_host_view_event_handler.cc b/content/browser/renderer_host/render_widget_host_view_event_handler.cc +index a51a44a147e89c1f71bf31d7d13b150a6bc1faee..74e8fd6c20cac940bb74f6ebe4a00f6b21a68e6c 100644 +--- a/content/browser/renderer_host/render_widget_host_view_event_handler.cc ++++ b/content/browser/renderer_host/render_widget_host_view_event_handler.cc +@@ -125,6 +125,16 @@ RenderWidgetHostViewEventHandler::~RenderWidgetHostViewEventHandler() { + DCHECK(!mouse_locked_); + } + ++void RenderWidgetHostViewEventHandler::SetPinchZoomEnabled(bool pinch_zoom_enabled) { ++ // Pinch to zoom is controlled globally by a command line switch and (as a BrightSign ++ // feature) also by a webpreference. If the command line switch has been used, this will ++ // disable pinch to zoom and override the web preference. Otherwise we use the web ++ // preference value that is passed. ++ if (input::switches::IsPinchToZoomEnabled()) { ++ pinch_zoom_enabled_ = pinch_zoom_enabled; ++ } ++} ++ + void RenderWidgetHostViewEventHandler::SetPopupChild( + RenderWidgetHostViewBase* popup_child_host_view, + ui::EventHandler* popup_child_event_handler) { +diff --git a/content/browser/renderer_host/render_widget_host_view_event_handler.h b/content/browser/renderer_host/render_widget_host_view_event_handler.h +index d975a34154643bc6da28ab3b2f610bdc78aae93e..f4b2dbf7bd81fb91800c6de4c427e7ea5d315182 100644 +--- a/content/browser/renderer_host/render_widget_host_view_event_handler.h ++++ b/content/browser/renderer_host/render_widget_host_view_event_handler.h +@@ -180,6 +180,8 @@ class CONTENT_EXPORT RenderWidgetHostViewEventHandler + .set_max_time_between_phase_ended_and_momentum_phase_began(timeout); + } + ++ void SetPinchZoomEnabled(bool pinch_zoom_enabled); ++ + private: + FRIEND_TEST_ALL_PREFIXES(InputMethodResultAuraTest, + FinishImeCompositionSession); +@@ -281,7 +283,7 @@ class CONTENT_EXPORT RenderWidgetHostViewEventHandler + + // Whether pinch-to-zoom should be enabled and pinch events forwarded to the + // renderer. +- const bool pinch_zoom_enabled_; ++ bool pinch_zoom_enabled_; + + // This flag when set ensures that we send over a notification to blink that + // the current view has focus. +diff --git a/third_party/blink/public/common/web_preferences/web_preferences.h b/third_party/blink/public/common/web_preferences/web_preferences.h +index e37fa2e8cb0896e61ef11259df13d97b8fbff548..8ea2fd648aab7e1d501a3bdfd13db47fdc69d073 100644 +--- a/third_party/blink/public/common/web_preferences/web_preferences.h ++++ b/third_party/blink/public/common/web_preferences/web_preferences.h +@@ -93,6 +93,7 @@ struct BLINK_COMMON_EXPORT WebPreferences { + bool privileged_webgl_extensions_enabled = false; + bool webgl_errors_to_console_enabled = true; + bool hide_scrollbars = false; ++ bool enable_pinch_zoom = false; + // If true, ignore ::-webkit-scrollbar-* CSS pseudo-elements in stylesheets + // and use default values for `ScrollbarWidth` and `ScrollbarColor` + // CSS properties. diff --git a/shell/browser/web_contents_preferences.cc b/shell/browser/web_contents_preferences.cc index de84a712d0084..12addf09ddcd4 100644 --- a/shell/browser/web_contents_preferences.cc +++ b/shell/browser/web_contents_preferences.cc @@ -123,6 +123,7 @@ void WebContentsPreferences::Clear() { text_areas_are_resizable_ = true; webgl_ = true; enable_preferred_size_mode_ = false; + enable_pinch_zoom_ = true; hide_scroll_bars_ = false; web_security_ = true; allow_running_insecure_content_ = false; @@ -184,6 +185,7 @@ void WebContentsPreferences::SetFromDictionary( web_preferences.Get(options::kWebGL, &webgl_); web_preferences.Get(options::kEnablePreferredSizeMode, &enable_preferred_size_mode_); + web_preferences.Get(options::kEnablePinchZoom, &enable_pinch_zoom_); web_preferences.Get(options::kHideScrollBars, &hide_scroll_bars_); web_preferences.Get(options::kWebSecurity, &web_security_); if (!web_preferences.Get(options::kAllowRunningInsecureContent, @@ -474,6 +476,7 @@ void WebContentsPreferences::OverrideWebkitPrefs( prefs->dom_paste_enabled = deprecated_paste_enabled_; prefs->hide_scrollbars = hide_scroll_bars_; + prefs->enable_pinch_zoom = enable_pinch_zoom_; } WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPreferences); diff --git a/shell/browser/web_contents_preferences.h b/shell/browser/web_contents_preferences.h index d8566bbdf5684..74fe7bcf83042 100644 --- a/shell/browser/web_contents_preferences.h +++ b/shell/browser/web_contents_preferences.h @@ -110,6 +110,7 @@ class WebContentsPreferences bool text_areas_are_resizable_; bool webgl_; bool enable_preferred_size_mode_; + bool enable_pinch_zoom_; bool hide_scroll_bars_; bool web_security_; bool allow_running_insecure_content_; diff --git a/shell/common/options_switches.h b/shell/common/options_switches.h index 9700619c1cb08..ca19a5098ced6 100644 --- a/shell/common/options_switches.h +++ b/shell/common/options_switches.h @@ -178,6 +178,8 @@ inline constexpr std::string_view kWebSecurity = "webSecurity"; inline constexpr std::string_view kHideScrollBars = "hideScrollBars"; +inline constexpr std::string_view kEnablePinchZoom = "enablePinchZoom"; + inline constexpr std::string_view kAllowRunningInsecureContent = "allowRunningInsecureContent"; From d2773bf999a9eeaec08507e4297be4b890e1089a Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 15/68] fix: Revert Modernize equality operators in StoragePartitionConfig --- patches/chromium/.patches | 1 + ...tionconfig_revert_modernize_equality.patch | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 patches/chromium/brightsign_storagepartitionconfig_revert_modernize_equality.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 5647dcf1a1a5b..d6450d85bb522 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -158,3 +158,4 @@ OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch os-14354_brightsign_add_remote-debugging-address_to_content_switches.patch os-14345_brightsign_add_a_webpreference_to_disable_pinch_to_zoom.patch +brightsign_storagepartitionconfig_revert_modernize_equality.patch diff --git a/patches/chromium/brightsign_storagepartitionconfig_revert_modernize_equality.patch b/patches/chromium/brightsign_storagepartitionconfig_revert_modernize_equality.patch new file mode 100644 index 0000000000000..95d093c364cc6 --- /dev/null +++ b/patches/chromium/brightsign_storagepartitionconfig_revert_modernize_equality.patch @@ -0,0 +1,60 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir +Date: Wed, 2 Jul 2025 11:02:18 +0100 +Subject: BrightSign StoragePartitionConfig: Preserve comparison semantics + +Chromium added https://chromium-review.googlesource.com/c/chromium/src/+/6554130 +which modernized the equality operators in StoragePartitionConfig and other places. +This change means that the quota std::optional we added is now included in the +equality and comparison operators, which is not what we want. Keep Chromium's +modern comparison surface but customize it for StoragePartitionConfig so quota is +ignored and the previous BrightSign behavior is preserved. + +diff --git a/content/public/browser/storage_partition_config.cc b/content/public/browser/storage_partition_config.cc +index ff56aedfc7361645be5968f7b017161d999c149f..3f080b4612d6f53d2310f30d680da45d744f0e30 100644 +--- a/content/public/browser/storage_partition_config.cc ++++ b/content/public/browser/storage_partition_config.cc +@@ -65,6 +65,26 @@ StoragePartitionConfig::GetFallbackForBlobUrls() const { + quota_size_); + } + ++bool StoragePartitionConfig::operator==( ++ const StoragePartitionConfig& rhs) const { ++ return partition_domain_ == rhs.partition_domain_ && ++ partition_name_ == rhs.partition_name_ && ++ in_memory_ == rhs.in_memory_ && ++ fallback_to_partition_domain_for_blob_urls_ == ++ rhs.fallback_to_partition_domain_for_blob_urls_; ++} ++std::strong_ordering StoragePartitionConfig::operator<=>( ++ const StoragePartitionConfig& rhs) const { ++ if (auto cmp = partition_domain_ <=> rhs.partition_domain_; cmp != 0) ++ return cmp; ++ if (auto cmp = partition_name_ <=> rhs.partition_name_; cmp != 0) ++ return cmp; ++ if (auto cmp = in_memory_ <=> rhs.in_memory_; cmp != 0) ++ return cmp; ++ return fallback_to_partition_domain_for_blob_urls_ <=> ++ rhs.fallback_to_partition_domain_for_blob_urls_; ++} ++ + std::ostream& operator<<(std::ostream& out, + const StoragePartitionConfig& config) { + out << "{"; +diff --git a/content/public/browser/storage_partition_config.h b/content/public/browser/storage_partition_config.h +index b1b708c1982a28f40ee961d363f6b2583619f8f0..e6f922c3a86bbafd4cff51d601bdc5bfb8cc9059 100644 +--- a/content/public/browser/storage_partition_config.h ++++ b/content/public/browser/storage_partition_config.h +@@ -84,10 +84,8 @@ class CONTENT_EXPORT StoragePartitionConfig { + } + std::optional GetFallbackForBlobUrls() const; + +- friend bool operator==(const StoragePartitionConfig&, +- const StoragePartitionConfig&) = default; +- friend auto operator<=>(const StoragePartitionConfig&, +- const StoragePartitionConfig&) = default; ++ bool operator==(const StoragePartitionConfig& rhs) const; ++ std::strong_ordering operator<=>(const StoragePartitionConfig& rhs) const; + + private: + friend StoragePartitionConfig CreateStoragePartitionConfigForTesting( From 0973b28a2403ecdd5ae4de497e94b5f675e431e1 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 16/68] fix: Allow websites from file:// domain to access cookies OS-16546 (#40) Allow websites from file:// domain to access cookies OS-16546 Cherry picked from: b2ca781b5216a8adc1e60c4b561db632bfff3c36: fix: Allow websites from file:// domain to access cookies OS-16546 (#40) --- patches/chromium/.patches | 1 + .../os-16546_enable_file_cookies.patch | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 patches/chromium/os-16546_enable_file_cookies.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index d6450d85bb522..c16920e8ee6a3 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -159,3 +159,4 @@ os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch os-14354_brightsign_add_remote-debugging-address_to_content_switches.patch os-14345_brightsign_add_a_webpreference_to_disable_pinch_to_zoom.patch brightsign_storagepartitionconfig_revert_modernize_equality.patch +os-16546_enable_file_cookies.patch diff --git a/patches/chromium/os-16546_enable_file_cookies.patch b/patches/chromium/os-16546_enable_file_cookies.patch new file mode 100644 index 0000000000000..3f59d4ddf1648 --- /dev/null +++ b/patches/chromium/os-16546_enable_file_cookies.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Mon, 15 Jan 2024 15:48:16 +0000 +Subject: OS-16546: enable file cookies + +Allow websites from file:// domain to access cookies +Cherry picked from QtWebEngine I638d2cd24ea426d78cdaa90a58b94559bab4588f + +diff --git a/net/cookies/cookie_monster.cc b/net/cookies/cookie_monster.cc +index 18f267aef11323164c73564d482b9c92a80be81c..452af34377a336686f3f5858cb4de55bddaaf3ed 100644 +--- a/net/cookies/cookie_monster.cc ++++ b/net/cookies/cookie_monster.cc +@@ -690,7 +690,7 @@ void CookieMonster::SetPersistSessionCookies(bool persist_session_cookies) { + + // static + std::vector CookieMonster::GetDefaultCookieableSchemes() { +- return std::vector{"http", "https", "ws", "wss"}; ++ return std::vector{"http", "https", "ws", "wss", "file"}; + } + + CookieChangeDispatcher& CookieMonster::GetChangeDispatcher() { From fb3b4ebbfd2ec1245cdf3b87b68cb77d5079b9db Mon Sep 17 00:00:00 2001 From: Caner Altinbasak Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 17/68] fix: Fix weston-keyboard backspace problem with existing text Weston backend refuses to send deletion requests outside selection area. Newly written text is in selection area, but old text is not. This casues old text to be not deletable. Deleting outside the selection area doesn't seem to be a problem as in EXO window manager mentioned in the code comment. Cherry picked from: 996ca5b9a0d1a19111fd5bfeebde3b7ed72ec71f: fix: Fix weston-keyboard backspace problem with existing text --- patches/chromium/.patches | 3 +- ...ext_add_support_for_deleting_outside.patch | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 patches/chromium/wayland_input_method_context_add_support_for_deleting_outside.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index c16920e8ee6a3..ea09a64cae525 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -152,7 +152,6 @@ fix_use_fresh_lazynow_for_onendworkitemimpl_after_didruntask.patch fix_make_macos_text_replacement_work_on_contenteditable.patch fix_constrain_allowuniversalaccessfromfileurls_to_file_origins_in.patch feat_os_14967_force_sniffing_file_urls.patch -os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch feat_backported_add_storage_quota_capability_for_partitions.patch OS-15118_add_bs_set_z_order_custom_feature_to_wayland.patch os-14339_brightsign_change_to_pass_window_boundary_x_and_y_offsets.patch @@ -160,3 +159,5 @@ os-14354_brightsign_add_remote-debugging-address_to_content_switches.patch os-14345_brightsign_add_a_webpreference_to_disable_pinch_to_zoom.patch brightsign_storagepartitionconfig_revert_modernize_equality.patch os-16546_enable_file_cookies.patch +os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch +wayland_input_method_context_add_support_for_deleting_outside.patch diff --git a/patches/chromium/wayland_input_method_context_add_support_for_deleting_outside.patch b/patches/chromium/wayland_input_method_context_add_support_for_deleting_outside.patch new file mode 100644 index 0000000000000..70d80a158a749 --- /dev/null +++ b/patches/chromium/wayland_input_method_context_add_support_for_deleting_outside.patch @@ -0,0 +1,44 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Caner Altinbasak +Date: Mon, 17 Jul 2023 15:12:57 +0100 +Subject: wayland_input_method_context: Add support for deleting outside + selection range + +Chromium wayland backend refuses to send the deletion request to +Chromium if user is trying to delete a text outside selection +area. Apparently this a problem on some systems. + +However, it is not a problem for us. I've excluded the case where +cursor needs to be moved first. Tha still reports an error. But +now it covers the cases where characters coming before and/or after +the cursor gets deleted. + +diff --git a/ui/ozone/platform/wayland/host/wayland_input_method_context.cc b/ui/ozone/platform/wayland/host/wayland_input_method_context.cc +index ced17f38742ced4c2336a09741ba54dbd75ecb84..430c5061133788e20b198fff7482c4b8dac59c22 100644 +--- a/ui/ozone/platform/wayland/host/wayland_input_method_context.cc ++++ b/ui/ozone/platform/wayland/host/wayland_input_method_context.cc +@@ -746,7 +746,23 @@ void WaylandInputMethodContext::OnDeleteSurroundingText(int32_t index, + base::UTF8ToUTF16AndAdjustOffsets(base::UTF16ToUTF8(surrounding_text), + &offsets_for_adjustment); + if (std::ranges::contains(offsets_for_adjustment, std::u16string::npos)) { +- LOG(DFATAL) << "The selection range for surrounding text is invalid."; ++ // Deleting a string outside selection range. Send the delete without ++ // adjustments. Incoming data gives position of index and how many ++ // characters needs to be deleted. Method we are calling is how many ++ // characters before and after current cursor position is deleted. ++ size_t before = 0; ++ size_t after = 0; ++ if (index < 0 && index + length >= 0) { ++ before = -index; ++ after = index + length; ++ } else if (index > 0 && index + length <= 0) { ++ before = index + length; ++ after = index; ++ } else { ++ LOG(DFATAL) << "The deletion range cannot change the index."; ++ return; ++ } ++ ime_delegate_->OnDeleteSurroundingText(before, after); + return; + } + From 16bb025b5516851637410c5a19ddba84778f3439 Mon Sep 17 00:00:00 2001 From: Caner Altinbasak Date: Wed, 6 May 2026 08:38:16 +0100 Subject: [PATCH 18/68] fix: Retry wayland display server connection before giving up (#29) wayland: Retry display server connection before giving up Brightsign weston server instance might not be up when Electron tries to connect to it. Added a patch to force 5 retries one second apart before giving up on it. Co-authored-by: Caner Altinbasak Cherry picked from: 7daf5991a2b3d935290527b4dae148b1fb8f9d0a: fix: Retry wayland display server connection before giving up (#29) --- patches/chromium/.patches | 1 + ...ting_to_display_server_for_5_seconds.patch | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 patches/chromium/wayland_connection_retry_connecting_to_display_server_for_5_seconds.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index ea09a64cae525..4ccd1f2f277cf 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -161,3 +161,4 @@ brightsign_storagepartitionconfig_revert_modernize_equality.patch os-16546_enable_file_cookies.patch os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch wayland_input_method_context_add_support_for_deleting_outside.patch +wayland_connection_retry_connecting_to_display_server_for_5_seconds.patch diff --git a/patches/chromium/wayland_connection_retry_connecting_to_display_server_for_5_seconds.patch b/patches/chromium/wayland_connection_retry_connecting_to_display_server_for_5_seconds.patch new file mode 100644 index 0000000000000..6c8c495efdcb7 --- /dev/null +++ b/patches/chromium/wayland_connection_retry_connecting_to_display_server_for_5_seconds.patch @@ -0,0 +1,43 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Caner Altinbasak +Date: Tue, 4 Jul 2023 15:01:40 +0100 +Subject: wayland_connection: Retry connecting to display server for 5 seconds + +Brightsign weston server instance might not be up, at the point where +Electron tries to connect to it. Retry for 5 seconds before giving up. + +diff --git a/ui/ozone/platform/wayland/host/wayland_connection.cc b/ui/ozone/platform/wayland/host/wayland_connection.cc +index 33749fd43e86e005313acfb29c501d0662795c21..6a6bebe3929ff2cc308695c6da3f8d8f3f2acf8f 100644 +--- a/ui/ozone/platform/wayland/host/wayland_connection.cc ++++ b/ui/ozone/platform/wayland/host/wayland_connection.cc +@@ -11,6 +11,7 @@ + #include + + #include ++#include + + #include "base/command_line.h" + #include "base/compiler_specific.h" +@@ -199,9 +200,20 @@ bool WaylandConnection::Initialize(bool use_threaded_polling) { + RegisterGlobalObjectFactory(ZwpPrimarySelectionDeviceManager::kInterfaceName, + &ZwpPrimarySelectionDeviceManager::Instantiate); + +- display_.reset(wl_display_connect(nullptr)); ++ static const int max_retries = 5; ++ int retries = 0; ++ while (!display_ && retries < max_retries) ++ { ++ display_.reset(wl_display_connect(nullptr)); ++ if (!display_) { ++ PLOG(ERROR) << "Failed to connect to Wayland display"; ++ std::this_thread::sleep_for(std::chrono::seconds(1)); ++ } ++ retries++; ++ } ++ + if (!display_) { +- PLOG(ERROR) << "Failed to connect to Wayland display"; ++ PLOG(ERROR) << "Failed to connect to Wayland display after " << max_retries << " retries"; + return false; + } + From 50868c6c6d017ebf95084ebfeb35b3ad4788d145 Mon Sep 17 00:00:00 2001 From: Caner Altinbasak Date: Wed, 6 May 2026 08:39:37 +0100 Subject: [PATCH 19/68] fix: Fix setOpacity test Electron doesn't support setting the opacity of the BrowserWindow on Linux platforms. Brightsign added support for changing the opacity of the window via wayland extensions. This is now a valid test for Linux. squash this to Add support for changing opacity on ozone wayland platform Cherry picked from: e35513226b08491d1903a13fb1142e8ba1d5dace: fix: Fix setOpacity test --- spec/api-browser-window-spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts index f7aa163affdaa..f960403721182 100755 --- a/spec/api-browser-window-spec.ts +++ b/spec/api-browser-window-spec.ts @@ -3390,13 +3390,16 @@ describe('BrowserWindow module', () => { }); }); - ifdescribe(process.platform === 'linux')('Linux', () => { + ifdescribe(process.platform === 'linux')(('Linux'), () => { + // BRIGHTSIGN: Brightsign uses wayland extensions + // to set the opacity of the window. This feature + // works. it('sets 1 regardless of parameter', () => { const w = new BrowserWindow({ show: false }); w.setOpacity(0); - expect(w.getOpacity()).to.equal(1.0); + expect(w.getOpacity()).to.equal(0.0); w.setOpacity(0.5); - expect(w.getOpacity()).to.equal(1.0); + expect(w.getOpacity()).to.equal(0.5); }); }); }); From d3dcefff2155a2913a1df378980d0fb604968464 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:40 +0100 Subject: [PATCH 20/68] feat: Add SetOpacity to linux ozone wayland: OS-16623 (#51) Add SetOpacity to linux ozone wayland: OS-16623 Cherry picked from: 4844399619bb5e32d7c027d40dc062e1c3e63256: feat: Add SetOpacity to linux ozone wayland: OS-16623 (#51) --- patches/chromium/.patches | 1 + ...etopacity_to_wayland_toplevel_window.patch | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 patches/chromium/os-16623_add_setopacity_to_wayland_toplevel_window.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 4ccd1f2f277cf..03623ea85f9c6 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -162,3 +162,4 @@ os-16546_enable_file_cookies.patch os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch wayland_input_method_context_add_support_for_deleting_outside.patch wayland_connection_retry_connecting_to_display_server_for_5_seconds.patch +os-16623_add_setopacity_to_wayland_toplevel_window.patch diff --git a/patches/chromium/os-16623_add_setopacity_to_wayland_toplevel_window.patch b/patches/chromium/os-16623_add_setopacity_to_wayland_toplevel_window.patch new file mode 100644 index 0000000000000..d930ec63285a7 --- /dev/null +++ b/patches/chromium/os-16623_add_setopacity_to_wayland_toplevel_window.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Fri, 3 May 2024 10:25:41 +0100 +Subject: OS-16623 add setopacity to wayland toplevel window + +This patch adds a setopacity method to the wayland_toplevel_window class to +allow the opacity of the window to be set. + +diff --git a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc +index 33b7d01af0da3564fa6ddc4f5467d9727d4adf17..5f5390ee8a0ca29cabddafaec7a6e2bbfb501e00 100644 +--- a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc ++++ b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc +@@ -356,6 +356,11 @@ void WaylandToplevelWindow::SizeConstraintsChanged() { + SetSizeConstraints(); + } + ++void WaylandToplevelWindow::SetOpacity(float opacity) { ++ root_surface()->set_opacity(opacity); ++ root_surface()->set_blending(true); ++} ++ + void WaylandToplevelWindow::SetZOrderLevel(ZOrderLevel order) { + // TODO(crbug.com/374244479): Linux/Wayland doesn't support zorder level. + // Consider complete removal of that. +diff --git a/ui/ozone/platform/wayland/host/wayland_toplevel_window.h b/ui/ozone/platform/wayland/host/wayland_toplevel_window.h +index 7e8f32b4b8f24c16fc1b903431ff915e056cf0da..235f0e5b178943d5e0962d2f9ea4fa2dbe510fee 100644 +--- a/ui/ozone/platform/wayland/host/wayland_toplevel_window.h ++++ b/ui/ozone/platform/wayland/host/wayland_toplevel_window.h +@@ -103,6 +103,7 @@ class WaylandToplevelWindow : public WaylandWindow, + void SetWindowIcons(const gfx::ImageSkia& window_icon, + const gfx::ImageSkia& app_icon) override; + void SizeConstraintsChanged() override; ++ void SetOpacity(float opacity) override; + // `SetZOrderLevel()` must be called on `z_order_` in + // `SetUpShellIntegration()`. + void SetZOrderLevel(ZOrderLevel order) override; From 87245593bb0c0b6de905fe233612a1e8ed539628 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:40 +0100 Subject: [PATCH 21/68] feat: Use same nssdb path for roKeyStore and Chromium OS-17053 (#56) Signed-off-by: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Cherry picked from: 5f3ff50c27a8a6fdc3b12c6bc888749d0659daea: feat: Use same nssdb path for roKeyStore and Chromium OS-17053 (#56) --- patches/chromium/.patches | 1 + ...sdb_path_for_rokeystore_and_chromium.patch | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 patches/chromium/os-17053_use_same_nssdb_path_for_rokeystore_and_chromium.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 03623ea85f9c6..af93472bbd6af 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -163,3 +163,4 @@ os-16547_patch_mime_sniffer_for_html_utf8_bom_svg_xml_comments.patch wayland_input_method_context_add_support_for_deleting_outside.patch wayland_connection_retry_connecting_to_display_server_for_5_seconds.patch os-16623_add_setopacity_to_wayland_toplevel_window.patch +os-17053_use_same_nssdb_path_for_rokeystore_and_chromium.patch diff --git a/patches/chromium/os-17053_use_same_nssdb_path_for_rokeystore_and_chromium.patch b/patches/chromium/os-17053_use_same_nssdb_path_for_rokeystore_and_chromium.patch new file mode 100644 index 0000000000000..5193f365a685e --- /dev/null +++ b/patches/chromium/os-17053_use_same_nssdb_path_for_rokeystore_and_chromium.patch @@ -0,0 +1,30 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Tue, 28 May 2024 15:34:10 +0100 +Subject: OS-17053: Use same nssdb path for roKeyStore and Chromium + +This change has been cherry picked from the QtWebEngine patch +I94eb625e805f9b2fe19c85f8f574f5725f0dbd46. + +roKeyStore creates/uses a nssdb store in /var/run/.pki/nssdb, whereas +Chromium expects it to be in user home directory, which is not writable +for us. + +Modified Chromium source code to use the same nssdb as roKeyStore. + +diff --git a/crypto/nss_util.cc b/crypto/nss_util.cc +index bfd752890aa73d43343590c85cdc14abcbbd6c3e..8ba42723a657f836422d484c887cb78fd628211d 100644 +--- a/crypto/nss_util.cc ++++ b/crypto/nss_util.cc +@@ -44,9 +44,9 @@ base::FilePath GetXdgDataNssdbDirectory() { + } + + base::FilePath GetHomeNssdbDirectory() { +- base::FilePath home_dir; +- base::PathService::Get(base::DIR_HOME, &home_dir); ++ base::FilePath home_dir("/var/run"); + if (home_dir.empty()) { ++ LOG(ERROR) << "Failed to get home directory /var/run."; + return {}; + } + From 4e3964afaff0292a8ddca73c6430849603a369fa Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:40 +0100 Subject: [PATCH 22/68] feat: Implement setting uid and groups for renderer process OS-17053 (#58) Implement setting uid and groups for renderer process OS-17053 Signed-off-by: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Cherry picked from: 301dfbcee0f52a8091d65273758d4a72f6456034: feat: Implement setting uid and groups for renderer process OS-17053 (#58) --- patches/chromium/.patches | 1 + ..._uid_and_groups_for_renderer_process.patch | 357 ++++++++++++++++++ shell/browser/electron_browser_context.cc | 24 ++ shell/browser/electron_browser_context.h | 3 + shell/common/options_switches.h | 5 + 5 files changed, 390 insertions(+) create mode 100644 patches/chromium/launcher_implement_setting_uid_and_groups_for_renderer_process.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index af93472bbd6af..b64da30f32337 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -164,3 +164,4 @@ wayland_input_method_context_add_support_for_deleting_outside.patch wayland_connection_retry_connecting_to_display_server_for_5_seconds.patch os-16623_add_setopacity_to_wayland_toplevel_window.patch os-17053_use_same_nssdb_path_for_rokeystore_and_chromium.patch +launcher_implement_setting_uid_and_groups_for_renderer_process.patch diff --git a/patches/chromium/launcher_implement_setting_uid_and_groups_for_renderer_process.patch b/patches/chromium/launcher_implement_setting_uid_and_groups_for_renderer_process.patch new file mode 100644 index 0000000000000..bbf2a467ae00c --- /dev/null +++ b/patches/chromium/launcher_implement_setting_uid_and_groups_for_renderer_process.patch @@ -0,0 +1,357 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Tue, 11 Jun 2024 15:55:18 +0100 +Subject: launcher: Implement setting uid and groups for renderer process + OS-17053 + +Cherry picked changes from QTWebEngine to allow setting the uid and groups +for the renderer process. This is needed to allow the renderer process to +run as a different user and group than the main process. + +The parameters are passed via switches --renderer-process-uid and +--renderer-process-gids. A safety check has been added to ensure the +renderer process is not run as root. + +diff --git a/base/process/launch.h b/base/process/launch.h +index 59eac5dc8c61b5c2df58d467149d4f3a9936611d..d8395fe5ec2214866dba9ada8e8da8b37756cbe4 100644 +--- a/base/process/launch.h ++++ b/base/process/launch.h +@@ -248,6 +248,12 @@ struct BASE_EXPORT LaunchOptions { + // Sets parent process death signal to SIGKILL. + bool kill_on_parent_death = false; + ++ // UID to run as (0 = don't change) ++ uid_t uid = 0; ++ ++ // Groups to run with ++ std::vector groups; ++ + // File descriptors of the parent process with FD_CLOEXEC flag to be removed + // before calling exec*(). + std::vector fds_to_remove_cloexec; +diff --git a/base/process/launch_posix.cc b/base/process/launch_posix.cc +index 4f9ffed9d90a2dd54317aaa06d9a0bfac85427c0..1075e49af89152a1ca3bf06c5e494bd98d0ee2c6 100644 +--- a/base/process/launch_posix.cc ++++ b/base/process/launch_posix.cc +@@ -19,6 +19,7 @@ + #include + #include + #include ++#include + + #include + #include +@@ -438,6 +439,18 @@ Process LaunchProcess(const std::vector& argv, + } + } + ++ gid_t gid = 0; ++ if (!options.groups.empty()) { ++ gid = options.groups[0]; ++ PCHECK(0 == setgroups(options.groups.size(),options.groups.data())); ++ } ++ if (options.uid > 0) ++ PCHECK(0 == prctl (PR_SET_KEEPCAPS, 1)); ++ if (gid > 0) ++ PCHECK(0 == setgid(gid)); ++ if (options.uid > 0) ++ PCHECK(0 == setuid(options.uid)); ++ + ResetChildSignalHandlersToDefaults(); + SetSignalMask(orig_sigmask); + +diff --git a/content/browser/child_process_launcher.cc b/content/browser/child_process_launcher.cc +index 58186e523fada3bb0192de063f3c595920dd44b7..adbdcd62ad8ff79f319c7b530ba8a938a0e2a125 100644 +--- a/content/browser/child_process_launcher.cc ++++ b/content/browser/child_process_launcher.cc +@@ -112,7 +112,9 @@ ChildProcessLauncher::ChildProcessLauncher( + scoped_refptr> + tracing_config_memory_region, + scoped_refptr> +- tracing_output_memory_region) ++ tracing_output_memory_region, ++ uid_t uid, ++ std::vector groups) + : client_(client), + starting_(true), + #if defined(ADDRESS_SANITIZER) || defined(LEAK_SANITIZER) || \ +@@ -138,6 +140,7 @@ ChildProcessLauncher::ChildProcessLauncher( + client_->CanUseWarmUpConnection(), client_->HasSpareRendererPriority(), + #endif + std::move(mojo_invitation), process_error_callback, std::move(file_data), ++ uid, std::move(groups), + std::move(histogram_memory_region), + std::move(tracing_config_memory_region), + std::move(tracing_output_memory_region)); +diff --git a/content/browser/child_process_launcher.h b/content/browser/child_process_launcher.h +index 65ffcb9491176722bb573be78ed15e81a9047c29..e22d6d21e5bb12920b261c92ab8b9bd3a7a0d2b5 100644 +--- a/content/browser/child_process_launcher.h ++++ b/content/browser/child_process_launcher.h +@@ -289,7 +289,9 @@ class CONTENT_EXPORT ChildProcessLauncher + scoped_refptr> + trace_config_memory_region = nullptr, + scoped_refptr> +- trace_output_memory_region = nullptr); ++ trace_output_memory_region = nullptr, ++ uid_t uid = 0, ++ std::vector groups = std::vector()); + + ChildProcessLauncher(const ChildProcessLauncher&) = delete; + ChildProcessLauncher& operator=(const ChildProcessLauncher&) = delete; +diff --git a/content/browser/child_process_launcher_helper.cc b/content/browser/child_process_launcher_helper.cc +index 08b3100781b34eba08fa24ec91209e01aec58a42..039797671c74fcca077b9123661b2e9c00ed43bf 100644 +--- a/content/browser/child_process_launcher_helper.cc ++++ b/content/browser/child_process_launcher_helper.cc +@@ -213,6 +213,8 @@ ChildProcessLauncherHelper::ChildProcessLauncherHelper( + mojo::OutgoingInvitation mojo_invitation, + const mojo::ProcessErrorCallback& process_error_callback, + std::unique_ptr file_data, ++ uid_t uid, ++ std::vector groups, + scoped_refptr> + histogram_memory_region, + scoped_refptr> +@@ -228,6 +230,8 @@ ChildProcessLauncherHelper::ChildProcessLauncherHelper( + mojo_invitation_(std::move(mojo_invitation)), + process_error_callback_(process_error_callback), + file_data_(std::move(file_data)), ++ uid_(uid), ++ groups_(std::move(groups)), + #if BUILDFLAG(IS_ANDROID) + can_use_warm_up_connection_(can_use_warm_up_connection), + is_spare_renderer_(is_spare_renderer), +@@ -300,13 +304,14 @@ void ChildProcessLauncherHelper::LaunchOnLauncherThread() { + int launch_result = LAUNCH_RESULT_FAILURE; + std::optional options; + base::LaunchOptions* options_ptr = nullptr; +- if (IsUsingLaunchOptions()) { ++ // if (IsUsingLaunchOptions()) { ++ // Always use launch options as we added uid and groups. + options.emplace(); + options_ptr = &*options; + #if BUILDFLAG(IS_WIN) + options_ptr->elevated = delegate_->ShouldLaunchElevated(); + #endif +- } ++ // } + + // Propagate the kWaitForDebugger switch to child process if the + // kWaitForDebuggerChildren is specified and matches the child process type. +diff --git a/content/browser/child_process_launcher_helper.h b/content/browser/child_process_launcher_helper.h +index acd23abf6d314a0644f8f4306e101c9d99bbb832..c9f74c49154fd7717e2089452f0ad4bc38e78809 100644 +--- a/content/browser/child_process_launcher_helper.h ++++ b/content/browser/child_process_launcher_helper.h +@@ -133,6 +133,8 @@ class ChildProcessLauncherHelper + mojo::OutgoingInvitation mojo_invitation, + const mojo::ProcessErrorCallback& process_error_callback, + std::unique_ptr file_data, ++ uid_t uid, ++ std::vector groups, + scoped_refptr> + histogram_memory_region, + scoped_refptr> +@@ -331,6 +333,8 @@ class ChildProcessLauncherHelper + mojo::OutgoingInvitation mojo_invitation_; + const mojo::ProcessErrorCallback process_error_callback_; + std::unique_ptr file_data_; ++ uid_t uid_; ++ const std::vector groups_; + + #if BUILDFLAG(IS_MAC) + std::unique_ptr seatbelt_exec_client_; +diff --git a/content/browser/child_process_launcher_helper_linux.cc b/content/browser/child_process_launcher_helper_linux.cc +index 878e5245266cfd4ea96bdbf8ca20c7ae0ac9a80e..2647658feb9c1001cb8e7dee0d7c050951d41e12 100644 +--- a/content/browser/child_process_launcher_helper_linux.cc ++++ b/content/browser/child_process_launcher_helper_linux.cc +@@ -55,7 +55,6 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread( + PosixFileDescriptorInfo& files_to_register, + base::LaunchOptions* options) { + if (options) { +- DCHECK(!GetZygoteForLaunch()); + // Convert FD mapping to FileHandleMappingVector + options->fds_to_remap = files_to_register.GetMappingWithIDAdjustment( + base::GlobalDescriptors::kBaseDescriptor); +@@ -82,6 +81,8 @@ bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread( + options->current_directory = delegate_->GetCurrentDirectory(); + options->environment = delegate_->GetEnvironment(); + options->clear_environment = !delegate_->ShouldInheritEnvironment(); ++ options->uid = uid_; ++ options->groups = groups_; + } else { + DCHECK(GetZygoteForLaunch()); + // Environment variables could be supported in the future, but are not +@@ -107,7 +108,7 @@ ChildProcessLauncherHelper::LaunchProcessOnLauncherThread( + // Additionally, the delegate could provide a UseGenericZygote() method. + base::ProcessHandle handle = zygote_handle->ForkRequest( + command_line()->argv(), files_to_register->GetMapping(), +- GetProcessType()); ++ GetProcessType(), options); + *launch_result = LAUNCH_RESULT_SUCCESS; + + #if !BUILDFLAG(IS_OPENBSD) +diff --git a/content/browser/renderer_host/render_process_host_impl.cc b/content/browser/renderer_host/render_process_host_impl.cc +index a5d1bafd8944afd9588d18ae8896c09f3bbce7f9..de5b4e88f20d8f98253799bdf0c85c08feb07c2e 100644 +--- a/content/browser/renderer_host/render_process_host_impl.cc ++++ b/content/browser/renderer_host/render_process_host_impl.cc +@@ -1981,6 +1981,9 @@ bool RenderProcessHostImpl::Init() { + file_data->files_to_preload = GetV8SnapshotFilesToPreload(*cmd_line); + #endif + ++ uid_t uid = GetBrowserContext()->GetUIDForRenderer(); ++ std::vector groups = GetBrowserContext()->GetGroupsForRenderer(); ++ + // Spawn the child process asynchronously to avoid blocking the UI thread. + // As long as there's no renderer prefix, we can use the zygote process + // at this stage. +@@ -1993,7 +1996,8 @@ bool RenderProcessHostImpl::Init() { + PROCESS_TYPE_RENDERER) + ? metrics_memory_region_ + : nullptr, +- tracing_config_memory_region_, tracing_output_memory_region_); ++ tracing_config_memory_region_, tracing_output_memory_region_, ++ uid, groups); + + // Send initialization messages to the renderer, before any other messages + // that might need them (e.g. navigation commits). +diff --git a/content/common/zygote/zygote_communication_linux.cc b/content/common/zygote/zygote_communication_linux.cc +index 2bf00e5b2f41908bc314bbadb3f018a6b79d1ed5..9ac2cc327294976f9a20196dfbe1de68c8581bfb 100644 +--- a/content/common/zygote/zygote_communication_linux.cc ++++ b/content/common/zygote/zygote_communication_linux.cc +@@ -95,7 +95,8 @@ void ZygoteCommunication::ReinitializeLogging( + pid_t ZygoteCommunication::ForkRequest( + const std::vector& argv, + const base::FileHandleMappingVector& mapping, +- const std::string& process_type) { ++ const std::string& process_type, ++ const base::LaunchOptions* options) { + DCHECK(init_); + + base::Pickle pickle; +@@ -115,6 +116,10 @@ pid_t ZygoteCommunication::ForkRequest( + icu::UnicodeString timezone_id; + pickle.WriteString16( + base::i18n::UnicodeStringToString16(timezone->getID(timezone_id))); ++ pickle.WriteInt(options->uid); ++ pickle.WriteInt(options->groups.size()); ++ for (const auto g: options->groups) ++ pickle.WriteInt(g); + + // Fork requests contain one file descriptor for the PID oracle, and one + // more for each file descriptor mapping for the child process. +diff --git a/content/common/zygote/zygote_communication_linux.h b/content/common/zygote/zygote_communication_linux.h +index 6a6e52f537fe8d4e23a3c01a36923bef5f5f0612..a6f878b49c11637206199066fa019aec791edd1f 100644 +--- a/content/common/zygote/zygote_communication_linux.h ++++ b/content/common/zygote/zygote_communication_linux.h +@@ -48,7 +48,8 @@ class CONTENT_EXPORT ZygoteCommunication { + // Returns its pid on success, otherwise base::kNullProcessHandle; + pid_t ForkRequest(const std::vector& command_line, + const base::FileHandleMappingVector& mapping, +- const std::string& process_type); ++ const std::string& process_type, ++ const base::LaunchOptions* options); + + void EnsureProcessTerminated(pid_t process); + +diff --git a/content/public/browser/browser_context.h b/content/public/browser/browser_context.h +index e03ed6361207d2b22c02533cee0c5c7668c2024e..fd4952628fc1b39ec25d8ce8f9b9d0bb5cfb28ca 100644 +--- a/content/public/browser/browser_context.h ++++ b/content/public/browser/browser_context.h +@@ -380,6 +380,12 @@ class CONTENT_EXPORT BrowserContext : public base::SupportsUserData { + // also off the record. + virtual bool IsOffTheRecord() = 0; + ++ // UID to run renderer as ++ virtual uid_t GetUIDForRenderer () const = 0; ++ ++ // Groups that renderer should be in ++ virtual std::vector GetGroupsForRenderer() const = 0; ++ + // Returns the DownloadManagerDelegate for this context. This will be called + // once per context. The embedder owns the delegate and is responsible for + // ensuring that it outlives DownloadManager. Note in particular that it is +diff --git a/content/renderer/renderer_main.cc b/content/renderer/renderer_main.cc +index 4b5f807cba7dc777e973cc5d852e92e2ed650449..e0b977af6b84f55f2a5edaa78a460221ce94e95e 100644 +--- a/content/renderer/renderer_main.cc ++++ b/content/renderer/renderer_main.cc +@@ -144,6 +144,12 @@ int RendererMain(MainFunctionParams parameters) { + // expect synchronous events around the main loop of a thread. + TRACE_EVENT_INSTANT0("startup", "RendererMain", TRACE_EVENT_SCOPE_THREAD); + ++#if BUILDFLAG(IS_LINUX) ++ if (getuid() == 0) { ++ LOG(FATAL) << "Renderer process is being run as root."; ++ } ++#endif ++ + #if BUILDFLAG(IS_MAC) + // Declare that this process has CPU security mitigations enabled (see + // RendererSandboxedProcessLauncherDelegate::EnableCpuSecurityMitigations). +diff --git a/content/zygote/zygote_linux.cc b/content/zygote/zygote_linux.cc +index f157fa13599637f0a5605af1e5cb11b18726bae3..2672a159f24dd69617b11276c97419e83e80ef93 100644 +--- a/content/zygote/zygote_linux.cc ++++ b/content/zygote/zygote_linux.cc +@@ -13,6 +13,7 @@ + #include + #include + #include ++#include + + #include + #include +@@ -431,7 +432,7 @@ int Zygote::ForkWithRealPid(const std::string& process_type, + pid = sandbox::NamespaceSandbox::ForkInNewPidNamespace( + /*drop_capabilities_in_child=*/true); + } else { +- pid = sandbox::Credentials::ForkAndDropCapabilitiesInChild(); ++ pid = fork(); + } + } + +@@ -545,6 +546,9 @@ base::ProcessId Zygote::ReadArgsAndFork(base::PickleIterator iter, + base::GlobalDescriptors::Mapping mapping; + std::string process_type; + ++ uid_t uid = 0; ++ std::vector groups; ++ + if (!iter.ReadString(&process_type)) + return -1; + if (!iter.ReadInt(&argc)) +@@ -566,6 +570,18 @@ base::ProcessId Zygote::ReadArgsAndFork(base::PickleIterator iter, + icu::TimeZone::adoptDefault(icu::TimeZone::createTimeZone( + icu::UnicodeString(false, timezone_id.data(), timezone_id.length()))); + ++ if (!iter.ReadUInt32(&uid)) ++ return -1; ++ int ngroups; ++ if (!iter.ReadInt(&ngroups)) ++ return -1; ++ for (int i = 0; i < ngroups; i++) { ++ gid_t g; ++ if (!iter.ReadUInt32(&g)) ++ return -1; ++ groups.push_back(g); ++ } ++ + if (!iter.ReadInt(&numfds)) + return -1; + if (numfds != static_cast(fds.size())) +@@ -615,6 +631,18 @@ base::ProcessId Zygote::ReadArgsAndFork(base::PickleIterator iter, + // (we don't have the original argv at this point). + base::SetProcessTitleFromCommandLine(nullptr); + ++ gid_t gid = 0; ++ if (!groups.empty()) { ++ gid = groups[0]; ++ PCHECK(0 == setgroups(groups.size(),groups.data())); ++ } ++ if (uid > 0) ++ PCHECK(0 == prctl (PR_SET_KEEPCAPS, 1)); ++ if (gid > 0) ++ PCHECK(0 == setgid(gid)); ++ if (uid > 0) ++ PCHECK(0 == setuid(uid)); ++ + // Linux-specific hack: Avoid taking the ~10-50ms jank penalty that a + // multithreaded process has to take when expanding its file descriptor + // table: diff --git a/shell/browser/electron_browser_context.cc b/shell/browser/electron_browser_context.cc index 10d8b4109f44d..7aaf7db3778fe 100644 --- a/shell/browser/electron_browser_context.cc +++ b/shell/browser/electron_browser_context.cc @@ -694,6 +694,30 @@ ElectronBrowserContext::GetFileSystemAccessPermissionContext() { return FileSystemAccessPermissionContextFactory::GetForBrowserContext(this); } +uid_t ElectronBrowserContext::GetUIDForRenderer() const { + uid_t uid = 0; + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + base::StringToUint( + command_line->GetSwitchValueASCII(switches::kRendererProcessUid), &uid); + return uid; +} + +std::vector ElectronBrowserContext::GetGroupsForRenderer() const { + std::vector gids; + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + std::string gids_str = + command_line->GetSwitchValueASCII(switches::kRendererProcessGids); + std::vector gid_list = base::SplitStringPiece( + gids_str, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + for (const auto& gid : gid_list) { + gid_t gid_value; + if (base::StringToUint(gid, &gid_value)) { + gids.push_back(gid_value); + } + } + return gids; +} + ResolveProxyHelper* ElectronBrowserContext::GetResolveProxyHelper() { if (!resolve_proxy_helper_) { resolve_proxy_helper_ = base::MakeRefCounted(this); diff --git a/shell/browser/electron_browser_context.h b/shell/browser/electron_browser_context.h index 2f9d9a7875e11..39abf63f74d18 100644 --- a/shell/browser/electron_browser_context.h +++ b/shell/browser/electron_browser_context.h @@ -135,6 +135,9 @@ class ElectronBrowserContext : public content::BrowserContext { return in_memory_pref_store_.get(); } + uid_t GetUIDForRenderer() const override; + std::vector GetGroupsForRenderer() const override; + ProtocolRegistry* protocol_registry() const { return protocol_registry_.get(); } diff --git a/shell/common/options_switches.h b/shell/common/options_switches.h index ca19a5098ced6..8073dcdc8f715 100644 --- a/shell/common/options_switches.h +++ b/shell/common/options_switches.h @@ -322,6 +322,11 @@ inline constexpr base::cstring_view kServiceWorkerPreload = // If set, flag node::ProcessInitializationFlags::kNoStdioInitialization would // be set for node initialization. inline constexpr base::cstring_view kNoStdioInit = "no-stdio-init"; +inline constexpr base::cstring_view kRendererProcessUid = + "renderer-process-uid"; + +inline constexpr base::cstring_view kRendererProcessGids = + "renderer-process-gids"; } // namespace switches From 31b0faef60c2224821e4be7c2c8007e3353a4894 Mon Sep 17 00:00:00 2001 From: Caner Altinbasak Date: Wed, 6 May 2026 08:39:40 +0100 Subject: [PATCH 23/68] feat: Add bs z-order absolute level This patch uses a the BrightSign SetZOrderLevel custom Weston extension to enable setting an absolute z-order for a window. Upstream-Status: Unsuitable Cherry picked from: 5a51e919406363da94cd84089d1ec6094ba6dd31: feat: Add bs z-order absolute level --- shell/browser/native_window_views.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index 9389703cbff6e..2cf2411a0d7d2 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -1151,7 +1151,12 @@ void NativeWindowViews::SetAlwaysOnTop(const ui::ZOrderLevel z_order, const bool level_changed = z_order != widget()->GetZOrderLevel(); const bool always_on_top = z_order != ui::ZOrderLevel::kNormal; - widget()->SetZOrderLevel(z_order); + if (z_order == ui::ZOrderLevel::kFloatingWindow) { + // Custom feature for BrightSign to beable to set an absolute Z-index + widget()->SetZOrderLevel((ui::ZOrderLevel)relativeLevel); + } else { + widget()->SetZOrderLevel(z_order); + } #if BUILDFLAG(IS_WIN) // Reset the placement flag. From 577cf2db133bdde8eb5fb996dc6c166c0db1573d Mon Sep 17 00:00:00 2001 From: Caner Altinbasak Date: Wed, 6 May 2026 08:39:40 +0100 Subject: [PATCH 24/68] feat: Add support for changing opacity on ozone wayland platform This change adds a call to SetOpacity for OZONE_PLATFORM_WAYLAND. It requires patches to Chromium that are included in the BrightSign Electron repo which contain the Chromium changes for supporting SetOpacity in the ozone wayland code. Upstream-Status: Unsuitable Cherry picked from: e16d7f02f8521d576a2d55660e58db7f1b25bd52: feat: Add support for changing opacity on ozone wayland platform --- shell/browser/native_window_views.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index 2cf2411a0d7d2..0d56a803966b9 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -1318,6 +1318,10 @@ void NativeWindowViews::SetOpacity(const double opacity) { SetLayered(); ::SetLayeredWindowAttributes(hwnd, 0, boundedOpacity * 255, LWA_ALPHA); opacity_ = boundedOpacity; +#elif BUILDFLAG(IS_OZONE_WAYLAND) + const double boundedOpacity = std::ranges::clamp(opacity, 0.0, 1.0); + widget()->SetOpacity(boundedOpacity); + opacity_ = boundedOpacity; #else opacity_ = 1.0; // setOpacity unsupported on Linux #endif From 9266dab5014893eaecdc44ea75a8b9207114d902 Mon Sep 17 00:00:00 2001 From: Caner Altinbasak Date: Wed, 6 May 2026 08:39:40 +0100 Subject: [PATCH 25/68] feat: Add support for SetIgnoreMouseEvents on Wayland platform Electron provides no implementation for SetIgnoreMouseEvents when using Wayland. This patch provides uses the Chromium mouse lock functionality to achieve a way of ignoring mouse events when using Wayland. The change goes together with changes in our Electron App to listen to the "focus" event and trigger a call to setIgnoreMouseEvents. This allows us to workaround the issue of mouse lock being automatically disabled when a BrowserWindow gains focus. Using mouse lock for disabling mouse events works fine for our use case. Cherry picked from: 703d152d7cff8f79c32d8c7d49972c124c7d0089: feat: Add support for SetIgnoreMouseEvents on Wayland platform --- shell/browser/native_window_views.cc | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index 0d56a803966b9..fa6865fdaa2e7 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -1318,7 +1318,7 @@ void NativeWindowViews::SetOpacity(const double opacity) { SetLayered(); ::SetLayeredWindowAttributes(hwnd, 0, boundedOpacity * 255, LWA_ALPHA); opacity_ = boundedOpacity; -#elif BUILDFLAG(IS_OZONE_WAYLAND) +#elif BUILDFLAG(SUPPORTS_OZONE_WAYLAND) const double boundedOpacity = std::ranges::clamp(opacity, 0.0, 1.0); widget()->SetOpacity(boundedOpacity); opacity_ = boundedOpacity; @@ -1332,6 +1332,24 @@ double NativeWindowViews::GetOpacity() const { } void NativeWindowViews::SetIgnoreMouseEvents(bool ignore, bool forward) { +#if BUILDFLAG(SUPPORTS_OZONE_WAYLAND) + // Custom BrightSign implementation for Wayland. + // This uses the Chromium mouse lock functionality together with a change + // in the Electron App to reapply the lock when the focus event is received. + // This works fine for our use case. + const gfx::AcceleratedWidget accelerated_widget = GetAcceleratedWidget(); + aura::WindowTreeHost* const host = + aura::WindowTreeHost::GetForAcceleratedWidget(accelerated_widget); + + aura::Window* const aura_window = host ? host->window() : nullptr; + if (aura_window) { + if (ignore) { + host->LockMouse(aura_window); + } else { + host->UnlockMouse(aura_window); + } + } +#endif #if BUILDFLAG(IS_WIN) LONG ex_style = ::GetWindowLong(GetAcceleratedWidget(), GWL_EXSTYLE); if (ignore) From f80b5958979903906ba434cb8df4fa6b99343070 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:40 +0100 Subject: [PATCH 26/68] fix: Increase file handle limit for renderer OS-17400 (#60) Increase file handle limit for renderer OS-17400 Signed-off-by: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Cherry picked from: 2400e2e249706fca368d325f13c0417a346eab81: fix: Increase file handle limit for renderer OS-17400 (#60) --- patches/chromium/.patches | 1 + ...e_file_open_handle_for_all_processes.patch | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 patches/chromium/os-17400_brightsign_increase_file_open_handle_for_all_processes.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index b64da30f32337..857949f101b7a 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -165,3 +165,4 @@ wayland_connection_retry_connecting_to_display_server_for_5_seconds.patch os-16623_add_setopacity_to_wayland_toplevel_window.patch os-17053_use_same_nssdb_path_for_rokeystore_and_chromium.patch launcher_implement_setting_uid_and_groups_for_renderer_process.patch +os-17400_brightsign_increase_file_open_handle_for_all_processes.patch diff --git a/patches/chromium/os-17400_brightsign_increase_file_open_handle_for_all_processes.patch b/patches/chromium/os-17400_brightsign_increase_file_open_handle_for_all_processes.patch new file mode 100644 index 0000000000000..0f4cd3d962b37 --- /dev/null +++ b/patches/chromium/os-17400_brightsign_increase_file_open_handle_for_all_processes.patch @@ -0,0 +1,79 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Tue, 25 Jun 2024 11:34:26 +0100 +Subject: OS-17400: BrightSign increase file open handle for all processes + +Chromium increases the file open handle limit as, according to +comments in the code, the linux default of 1024 is too low. This +increase is made in the browser main loop, but gets run in the +Electron main process. This means the renderer process is left +with the default value of 1024. +A customer scenario results in frequest crashes due to the Electron +renderer process "exceeded file handle limit". The test scenario +is reloading a specific website and the crash happens quite soon +after opening it, which means the web page needs a lot of tmp files +rather than the web page isn't freeing resources and it slowly +builds up over time. +This patch fixes the issue by calling base::IncreaseFdLimitTo in +the Zygote and LaunchProcess classes to ensure the file handle limit +is also increased in the other processes (including the renderer) that +are forked from here. The comments explaining the 8192 value have been +copied accross from the brower main call code. + +Note: This patch is based on the QtWebEngine change +I56506a41eb9c224b83ca529dce6d888d3fe00cfc. + +diff --git a/base/process/launch_posix.cc b/base/process/launch_posix.cc +index 1075e49af89152a1ca3bf06c5e494bd98d0ee2c6..f96a2fc3a16bc8134d1af1da338b3f9f885d71e9 100644 +--- a/base/process/launch_posix.cc ++++ b/base/process/launch_posix.cc +@@ -38,6 +38,7 @@ + #include "base/process/environment_internal.h" + #include "base/process/process.h" + #include "base/process/process_metrics.h" ++#include "base/process/process_metrics.h" + #include "base/synchronization/waitable_event.h" + #include "base/threading/platform_thread.h" + #include "base/threading/platform_thread_internal_posix.h" +@@ -451,6 +452,14 @@ Process LaunchProcess(const std::vector& argv, + if (options.uid > 0) + PCHECK(0 == setuid(options.uid)); + ++ // We use quite a few file descriptors for our IPC as well as disk the disk ++ // cache, and the default limit on Apple is low (256), so bump it up. ++ // Same for Linux. The default various per distro, but it is 1024 on Fedora. ++ // Low soft limits combined with liberal use of file descriptors means power ++ // users can easily hit this limit with many open tabs. Bump up the limit to ++ // an arbitrarily high number. See https://crbug.com/539567 ++ base::IncreaseFdLimitTo(8192); ++ + ResetChildSignalHandlersToDefaults(); + SetSignalMask(orig_sigmask); + +diff --git a/content/zygote/zygote_linux.cc b/content/zygote/zygote_linux.cc +index 2672a159f24dd69617b11276c97419e83e80ef93..3bbfaf0c6f24866ff262827d849b799a1f7c69c2 100644 +--- a/content/zygote/zygote_linux.cc ++++ b/content/zygote/zygote_linux.cc +@@ -37,6 +37,7 @@ + #include "base/process/launch.h" + #include "base/process/process.h" + #include "base/process/process_handle.h" ++#include "base/process/process_metrics.h" + #include "base/process/set_process_title.h" + #include "base/time/time.h" + #include "base/trace_event/trace_event.h" +@@ -643,6 +644,14 @@ base::ProcessId Zygote::ReadArgsAndFork(base::PickleIterator iter, + if (uid > 0) + PCHECK(0 == setuid(uid)); + ++ // We use quite a few file descriptors for our IPC as well as disk the disk ++ // cache, and the default limit on Apple is low (256), so bump it up. ++ // Same for Linux. The default various per distro, but it is 1024 on Fedora. ++ // Low soft limits combined with liberal use of file descriptors means power ++ // users can easily hit this limit with many open tabs. Bump up the limit to ++ // an arbitrarily high number. See https://crbug.com/539567 ++ base::IncreaseFdLimitTo(8192); ++ + // Linux-specific hack: Avoid taking the ~10-50ms jank penalty that a + // multithreaded process has to take when expanding its file descriptor + // table: From 1acb29bffaf5b316086b3a0bffa1d2bf8d8f7721 Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:39:40 +0100 Subject: [PATCH 27/68] feat: Add support for window rotation OS-17652 (#65) * OS-17652: Add support for window rotation * Mention windowTransform webpreference is for Linux * Add window rotation test case --------- Signed-off-by: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> test: Improve html window rotation tests (#67) Improve html window rotation tests fix: Add missing implementation for rotating popups in html widgets (#72) * Fix rotated popup windows * Add extra test for popup window rotation Cherry picked from: 0947088444a5d230fbab341057eb6d2937b41d25: feat: Add support for window rotation OS-17652 (#65) 5d0988413e28f4f65b0eee894372e01342dfaa22: test: Improve html window rotation tests (#67) 96a6d5d0f31f05de9a940fc3f085e6e5abc67538: fix: Add missing implementation for rotating popups in html widgets (#72) --- docs/api/browser-window.md | 10 + docs/api/structures/web-preferences.md | 5 + patches/chromium/.patches | 1 + ...7652_add_support_for_window_rotation.patch | 667 ++++++++++++++++++ shell/browser/api/electron_api_base_window.cc | 22 + shell/browser/api/electron_api_base_window.h | 3 + shell/browser/native_window.h | 5 + shell/browser/native_window_views.cc | 27 + shell/browser/native_window_views.h | 5 +- shell/browser/web_contents_preferences.cc | 23 + shell/browser/web_contents_preferences.h | 2 + spec/api-browser-window-spec.ts | 429 +++++++++++ spec/fixtures/pages/rotation-test.html | 26 + 13 files changed, 1224 insertions(+), 1 deletion(-) create mode 100644 patches/chromium/os-17652_add_support_for_window_rotation.patch create mode 100644 spec/fixtures/pages/rotation-test.html diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index 2779ed08fd84a..c335a00f07f4e 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -1803,6 +1803,16 @@ with `addBrowserView` or `setBrowserView`. The top-most BrowserView is the last > The `BrowserView` class is deprecated, and replaced by the new > [`WebContentsView`](web-contents-view.md) class. +#### `win.setWindowTransform(windowTransform)` _Linux_ + +* `windowTransform` string + * `none` - There is no transform (i.e. the widget content is oriented as landscape). + * `rot90` - The widget content is rotated to portrait at 90 degrees (clockwise). + * `rot180` - The widget content is rotated to portrait at 180 degrees (clockwise). + * `rot270` - The widget content is rotated to portrait at 270 degrees (clockwise). + +This method sets the browser window's transform. + #### `win.setTitleBarOverlay(options)` _Windows_ _Linux_ * `options` Object diff --git a/docs/api/structures/web-preferences.md b/docs/api/structures/web-preferences.md index b25747f25e202..d41881eab07ec 100644 --- a/docs/api/structures/web-preferences.md +++ b/docs/api/structures/web-preferences.md @@ -162,6 +162,11 @@ * `hideScrollBars` boolean (optional) - Sets the Chromium WebPreferences hide_scrollbars field. Default is `false`. * `enablePinchZoom` boolean (optional) - Sets whether to enable pinch to zoom in Chromium. Default is `true`. +* `windowTransform` string (optional) _Linux_ - Specifies whether to apply a transform to the window. Default is `none`. Accepted values are + * `none` - There is no transform (i.e. the widget content is oriented as landscape). + * `rot90` - The widget content is rotated to portrait at 90 degrees (clockwise). + * `rot180` - The widget content is rotated to portrait at 180 degrees (clockwise). + * `rot270` - The widget content is rotated to portrait at 270 degrees (clockwise). [chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment [runtime-enabled-features]: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/runtime_enabled_features.json5 diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 857949f101b7a..c19df81002e8e 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -166,3 +166,4 @@ os-16623_add_setopacity_to_wayland_toplevel_window.patch os-17053_use_same_nssdb_path_for_rokeystore_and_chromium.patch launcher_implement_setting_uid_and_groups_for_renderer_process.patch os-17400_brightsign_increase_file_open_handle_for_all_processes.patch +os-17652_add_support_for_window_rotation.patch diff --git a/patches/chromium/os-17652_add_support_for_window_rotation.patch b/patches/chromium/os-17652_add_support_for_window_rotation.patch new file mode 100644 index 0000000000000..2a4a08444b75f --- /dev/null +++ b/patches/chromium/os-17652_add_support_for_window_rotation.patch @@ -0,0 +1,667 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Tue, 24 Sep 2024 13:44:25 +0100 +Subject: OS-17652: Add support for window rotation + +This patch introduces a new WebPreference and browser window API method +setWindowTransform to control window rotation +(90, 180, or 270 degrees) in BrightSign. The preference is set in +WebContentsImpl and passed to RenderWidgetHostViewAura, which then +applies the display transform hint on WindowTreeHost if rotation is +requested. Method setWindowTransform also applies the display transform +hint on WindowTreeHost if rotation is requested. + +Method WindowTreeHost::GetTransformedWindowBounds was added to compute the +transformed bounds when DesktopNativeWidgetAura::SetBounds is invoked. + +Method WindowTreeHost::GetTransformedCompositorBounds was added to compute +the transformed bounds when Window::SetBoundsInternal and +WindowTreeHost::GetTransformedCompositorBounds are invoked. This method +handles the transformed bounds for both popups and windows. Since window +transforms have the same functionality for both compositor and window bounds, +the method uses GetTransformedWindowBounds to compute the transformed bounds +for windows. + +For windows these transform methods ensure bounds are transposed only once +by checking the aspect ratio of the current and new bounds, avoiding +multiple transpositions. This aspect ratio comparison approach is more robust +than relying on the original bounds, as Window::SetBoundsInternal may receive +bounds adjusted for padding or insets. +For popups we get the initial physical bounds during the initialisation of +the popup and so use this directly to calculate the transformed bounds, rather +than comparing aspect ratios. + +Method GetTransformedPopupBounds is used to compute the transformed bounds +of the physical popup window. This method is called in +RenderWidgetHostViewAura::InitAsPopup and RenderWidgetHostViewAura::SetBounds. +The transform used by this method is fetched from the parent RenderWidgetHostView +as this transform is based on the parent screen dimensions. + +For input event transforms: +When GetTransformedWindowBounds is invoked, it also computes an input +transform based on the display transform hint, which is subsequently +applied to mouse and touch events in WindowEventDispatcher::PreDispatchEvent. +The non_transformed_bounds method, added to Window, stores the original bounds, +enabling WindowTargeter to align hit-test rectangles with screen resolution, +thus ensuring consistent mouse and touch input alignment. +In addition WindowTargeter::FindTargetInRootWindow has been modified to use the +non_transformed_bounds to ensure the touch event location (which is not transformed) +is compared with non-transformed window bounds. + +diff --git a/components/viz/service/display/output_surface.h b/components/viz/service/display/output_surface.h +index 641bbfc732c88141ddd929a4c334360462259ee4..49e33fbe5b6645099f434d15e24eb9b44bd1821c 100644 +--- a/components/viz/service/display/output_surface.h ++++ b/components/viz/service/display/output_surface.h +@@ -102,7 +102,7 @@ class VIZ_SERVICE_EXPORT OutputSurface { + // to apply resize optimization by padding to its width/height. + bool supports_viewporter = false; + // OutputSurface's orientation mode. +- OrientationMode orientation_mode = OrientationMode::kLogic; ++ OrientationMode orientation_mode = OrientationMode::kHardware; + #if BUILDFLAG(IS_WIN) + // Whether this OutputSurface supports direct composition layers. + DCSupportLevel dc_support_level = DCSupportLevel::kNone; +diff --git a/components/viz/service/display_embedder/software_output_surface.cc b/components/viz/service/display_embedder/software_output_surface.cc +index 52290774df43290a4072944ab9e14aab1ce7a257..c5c98ff37349e9f599a36c968bfcd50c8d8a89a9 100644 +--- a/components/viz/service/display_embedder/software_output_surface.cc ++++ b/components/viz/service/display_embedder/software_output_surface.cc +@@ -139,8 +139,13 @@ void SoftwareOutputSurface::SetUpdateVSyncParametersCallback( + update_vsync_parameters_callback_ = std::move(callback); + } + ++void SoftwareOutputSurface::SetDisplayTransformHint( ++ gfx::OverlayTransform transform) { ++ display_transform_ = transform; ++} ++ + gfx::OverlayTransform SoftwareOutputSurface::GetDisplayTransform() { +- return gfx::OVERLAY_TRANSFORM_NONE; ++ return display_transform_; + } + + #if BUILDFLAG(IS_LINUX) +diff --git a/components/viz/service/display_embedder/software_output_surface.h b/components/viz/service/display_embedder/software_output_surface.h +index 9ff12d2c365172d08464f62fc01597cfcc699dbd..b964c316ac73df1b8811d03386ab92086344f2c8 100644 +--- a/components/viz/service/display_embedder/software_output_surface.h ++++ b/components/viz/service/display_embedder/software_output_surface.h +@@ -38,7 +38,7 @@ class VIZ_SERVICE_EXPORT SoftwareOutputSurface : public OutputSurface { + void SwapBuffers(OutputSurfaceFrame frame) override; + void SetUpdateVSyncParametersCallback( + UpdateVSyncParametersCallback callback) override; +- void SetDisplayTransformHint(gfx::OverlayTransform transform) override {} ++ void SetDisplayTransformHint(gfx::OverlayTransform transform) override; + gfx::OverlayTransform GetDisplayTransform() override; + #if BUILDFLAG(IS_LINUX) + void SetNeedsSwapSizeNotifications( +@@ -62,6 +62,8 @@ class VIZ_SERVICE_EXPORT SoftwareOutputSurface : public OutputSurface { + bool needs_swap_size_notifications_ = false; + #endif + ++ gfx::OverlayTransform display_transform_ = gfx::OVERLAY_TRANSFORM_NONE; ++ + base::WeakPtrFactory weak_factory_{this}; + }; + +diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc +index ab18b92618d5a05055ff8c6428b5c79982875b43..6e03d616d77d1c9da483b8777142925f78d1427c 100644 +--- a/content/browser/renderer_host/render_widget_host_view_aura.cc ++++ b/content/browser/renderer_host/render_widget_host_view_aura.cc +@@ -134,6 +134,7 @@ + #include "ui/base/ime/linux/text_edit_command_auralinux.h" + #include "ui/base/ime/text_input_flags.h" + #include "ui/linux/linux_ui.h" ++#include "ui/ozone/platform_selection.h" + #endif + + #if BUILDFLAG(IS_CHROMEOS) +@@ -380,6 +381,22 @@ RenderWidgetHostViewAura::RenderWidgetHostViewAura( + .double_tap_to_zoom_enabled; + + event_handler_->SetPinchZoomEnabled(owner_delegate->GetWebkitPreferencesForWidget().enable_pinch_zoom); ++ ++ switch (owner_delegate->GetWebkitPreferencesForWidget().window_transform_type) ++ { ++ case blink::mojom::WindowTransformType::kWindowTransformTypeRotate90: ++ window_transform_ = gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90; ++ break; ++ case blink::mojom::WindowTransformType::kWindowTransformTypeRotate180: ++ window_transform_ = gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_180; ++ break; ++ case blink::mojom::WindowTransformType::kWindowTransformTypeRotate270: ++ window_transform_ = gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270; ++ break; ++ default: ++ window_transform_ = gfx::OVERLAY_TRANSFORM_NONE; ++ break; ++ } + } + + #if BUILDFLAG(IS_WIN) +@@ -408,6 +425,11 @@ void RenderWidgetHostViewAura::InitAsChild(gfx::NativeView parent_view) { + UpdateSystemCursorSize(cursor_client->GetSystemCursorSize()); + } + ++ aura::WindowTreeHost* host = window_->GetHost(); ++ if (host && window_transform_ != gfx::OVERLAY_TRANSFORM_NONE) { ++ host->SetDisplayTransformHint(window_transform_); ++ } ++ + #if BUILDFLAG(IS_WIN) + // This will fetch and set the display features. + ObserveDevicePosturePlatformProvider(); +@@ -465,14 +487,31 @@ void RenderWidgetHostViewAura::InitAsPopup( + popup_parent_host_view_->window_, window_); + } + ++ window_transform_ = popup_parent_host_view_->GetDisplayTransform(); ++ gfx::Rect final_bounds_in_screen(bounds_in_screen); ++ aura::WindowTreeHost* parent_host = popup_parent_host_view_->window_->GetHost(); ++ if (parent_host) { ++ original_popup_bounds_ = bounds_in_screen; ++ final_bounds_in_screen = GetTransformedPopupBounds( ++ bounds_in_screen, parent_host->GetBoundsInPixels(), parent_host->GetPopupTransform()); ++ } ++ ++ // Disable using anchor rect for all platforms as when using it with Wayland it is ++ // not positioning pop ups in the right place when they are near to edges of the ++ // screen (they will get clipped). Also, Nexus doesn't use it. ++ // This will result in us having the same behavior as when using Nexus and also ++ // mean we don't have to add additional functionality to transform the anchor rect ++ // when the html window is rotated. ++#if 0 + ui::OwnedWindowAnchor owned_window_anchor = { + anchor_rect, ui::OwnedWindowAnchorPosition::kBottomLeft, + ui::OwnedWindowAnchorGravity::kBottomRight, + ui::OwnedWindowConstraintAdjustment::kAdjustmentFlipY}; + window_->SetProperty(aura::client::kOwnedWindowAnchor, owned_window_anchor); ++#endif + + aura::Window* root = popup_parent_host_view_->window_->GetRootWindow(); +- aura::client::ParentWindowWithContext(window_, root, bounds_in_screen, ++ aura::client::ParentWindowWithContext(window_, root, final_bounds_in_screen, + display::kInvalidDisplayId); + + SetBounds(bounds_in_screen); +@@ -496,6 +535,11 @@ void RenderWidgetHostViewAura::InitAsPopup( + if (cursor_client) + UpdateSystemCursorSize(cursor_client->GetSystemCursorSize()); + ++ aura::WindowTreeHost* host = window_->GetHost(); ++ if (host) { ++ host->SetPopupBounds(bounds_in_screen); ++ host->SetDisplayTransformHint(window_transform_); ++ } + #if BUILDFLAG(IS_WIN) + // This will fetch and set the display features. + ObserveDevicePosturePlatformProvider(); +@@ -516,7 +560,14 @@ void RenderWidgetHostViewAura::SetSize(const gfx::Size& size) { + } + + void RenderWidgetHostViewAura::SetBounds(const gfx::Rect& rect) { +- gfx::Point relative_origin(rect.origin()); ++ gfx::Rect transformed_rect(rect); ++ aura::WindowTreeHost* parent_host = popup_parent_host_view_->window_->GetHost();; ++ if (parent_host) { ++ transformed_rect = GetTransformedPopupBounds( ++ rect, parent_host->GetBoundsInPixels(), parent_host->GetPopupTransform()); ++ } ++ ++ gfx::Point relative_origin(transformed_rect.origin()); + + // RenderWidgetHostViewAura::SetBounds() takes screen coordinates, but + // Window::SetBounds() takes parent coordinates, so do the conversion here. +@@ -530,7 +581,7 @@ void RenderWidgetHostViewAura::SetBounds(const gfx::Rect& rect) { + } + } + +- InternalSetBounds(gfx::Rect(relative_origin, rect.size())); ++ InternalSetBounds(gfx::Rect(relative_origin, transformed_rect.size())); + } + + gfx::NativeView RenderWidgetHostViewAura::GetNativeView() { +@@ -3534,6 +3585,51 @@ ui::Compositor* RenderWidgetHostViewAura::GetCompositor() { + return window_->GetHost()->compositor(); + } + ++gfx::Rect RenderWidgetHostViewAura::GetTransformedPopupBounds(const gfx::Rect& new_bounds, const gfx::Rect& parent_bounds, const gfx::Transform& parent_popup_transform) { ++ gfx::Rect transformed_bounds(new_bounds); ++ ++ // Wayland draws the popup window in the parent window's coordinate space. However, X11 (for CI) ++ // and Nexus draw the popup window in the root window's coordinate space. ++ // This means we need to remove the parent window offset, then apply the parent_popup_transform, ++ // and then add the parent window offset back for platforms other than wayland. ++ transformed_bounds.Offset(-parent_bounds.OffsetFromOrigin()); ++ transformed_bounds.set_origin(parent_popup_transform.MapPoint(transformed_bounds.origin())); ++#if BUILDFLAG(IS_LINUX) ++ if (std::string("wayland") != ui::GetOzonePlatformName()) ++#endif ++ transformed_bounds.Offset(parent_bounds.OffsetFromOrigin()); ++ ++ switch (window_transform_) { ++ case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90: ++ transformed_bounds.set_size(gfx::Size(original_popup_bounds_.height(), original_popup_bounds_.width())); ++ transformed_bounds.Offset(-original_popup_bounds_.height(), 0); ++ break; ++ ++ case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_180: ++ transformed_bounds.Offset(-original_popup_bounds_.width(), -original_popup_bounds_.height()); ++ break; ++ ++ case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270: ++ transformed_bounds.set_size(gfx::Size(original_popup_bounds_.height(), original_popup_bounds_.width())); ++ transformed_bounds.Offset(0, -original_popup_bounds_.width()); ++ break; ++ ++ default: ++ break; ++ } ++ return transformed_bounds; ++} ++ ++gfx::OverlayTransform RenderWidgetHostViewAura::GetDisplayTransform() { ++ gfx::OverlayTransform transform = gfx::OVERLAY_TRANSFORM_NONE; ++ ++ aura::WindowTreeHost* host = window_->GetHost(); ++ if (host) { ++ transform = host->GetDisplayTransformHint(); ++ } ++ return transform; ++} ++ + #if BUILDFLAG(IS_WIN) + void RenderWidgetHostViewAura::ForwardArabicIndicCharEventWithLatencyInfo( + const ui::KeyEvent& event, +diff --git a/content/browser/renderer_host/render_widget_host_view_aura.h b/content/browser/renderer_host/render_widget_host_view_aura.h +index 47b2677d2878cb9d5544798ddc49b54d270e1688..92b3bba5268e99eedcc652f49c172ed05f1941c7 100644 +--- a/content/browser/renderer_host/render_widget_host_view_aura.h ++++ b/content/browser/renderer_host/render_widget_host_view_aura.h +@@ -46,6 +46,7 @@ + #include "ui/display/display_observer.h" + #include "ui/gfx/geometry/insets.h" + #include "ui/gfx/geometry/rect.h" ++#include "ui/gfx/overlay_transform.h" + #include "ui/gfx/selection_bound.h" + #include "ui/wm/public/activation_delegate.h" + +@@ -449,6 +450,10 @@ class CONTENT_EXPORT RenderWidgetHostViewAura + + ui::Compositor* GetCompositor() override; + ++ gfx::Rect GetTransformedPopupBounds( ++ const gfx::Rect& new_bounds, const gfx::Rect& parent_bounds, const gfx::Transform& parent_popup_transform); ++ gfx::OverlayTransform GetDisplayTransform(); ++ + DelegatedFrameHost* GetDelegatedFrameHostForTesting() const { + return delegated_frame_host_.get(); + } +@@ -887,6 +892,9 @@ class CONTENT_EXPORT RenderWidgetHostViewAura + + std::optional display_observer_; + ++ gfx::OverlayTransform window_transform_ = gfx::OVERLAY_TRANSFORM_NONE; ++ gfx::Rect original_popup_bounds_; ++ + base::WeakPtrFactory weak_ptr_factory_{this}; + }; + +diff --git a/third_party/blink/public/common/web_preferences/web_preferences.h b/third_party/blink/public/common/web_preferences/web_preferences.h +index 8ea2fd648aab7e1d501a3bdfd13db47fdc69d073..70b968674ce2cd70034dcfe2477268ff0fa54904 100644 +--- a/third_party/blink/public/common/web_preferences/web_preferences.h ++++ b/third_party/blink/public/common/web_preferences/web_preferences.h +@@ -452,6 +452,8 @@ struct BLINK_COMMON_EXPORT WebPreferences { + // blocking user's access to the background web content. + bool modal_context_menu = true; + ++ blink::mojom::WindowTransformType window_transform_type = blink::mojom::WindowTransformType::kWindowTransformTypeNone; ++ + // Whether the safe-area-insets should be changed dynamically based on + // browser controls shown ratio on Android. + bool dynamic_safe_area_insets_enabled = false; +diff --git a/third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h b/third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h +index ea3f8f3e30f76ebf71ed470f43e4f61995829932..b37b49e012a740117ccaf4ee93bfd1664d0f2c48 100644 +--- a/third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h ++++ b/third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h +@@ -898,6 +898,11 @@ struct BLINK_COMMON_EXPORT StructTraits standard_font_family_map; + map fixed_font_family_map; +@@ -529,6 +536,9 @@ struct WebPreferences { + // WebView and by `kWebPayments` feature flag everywhere. + bool payment_request_enabled = false; + ++ // Whether the window should be transformed. ++ WindowTransformType window_transform_type = kWindowTransformTypeNone; ++ + // Enables the origin trial Built-in AI APIs, for use within DevTools and + // devtools extension panels. + bool ai_ot_apis_enabled = false; +diff --git a/ui/aura/window.cc b/ui/aura/window.cc +index 1437ffcb458c0bfe4b60cf74e4de673314cc2929..42b961dcf5980972bf77ac872939434957ce4102 100644 +--- a/ui/aura/window.cc ++++ b/ui/aura/window.cc +@@ -1069,10 +1069,16 @@ bool Window::HitTest(const gfx::Point& local_point) { + + void Window::SetBoundsInternal(const gfx::Rect& new_bounds) { + gfx::Rect old_bounds = GetTargetBounds(); ++ gfx::Rect final_bounds(new_bounds); ++ WindowTreeHost* host = GetHost(); ++ if (host) { ++ // The ui:Layer bounds that we will be setting should match the compositor bounds. ++ final_bounds = host->GetTransformedCompositorBounds(new_bounds); ++ } + + // Always need to set the layer's bounds -- even if it is to the same thing. + // This may cause important side effects such as stopping animation. +- layer()->SetBounds(new_bounds); ++ layer()->SetBounds(final_bounds); + + // If we are currently not the layer's delegate, we will not get bounds + // changed notification from the layer (this typically happens after animating +@@ -1554,6 +1560,11 @@ void Window::OnLayerBoundsChanged(const gfx::Rect& old_bounds, + WindowOcclusionTracker::ScopedPause pause_occlusion_tracking; + + bounds_ = layer()->bounds(); ++ non_transformed_bounds_ = bounds_; ++ WindowTreeHost* host = GetHost(); ++ if (host) { ++ non_transformed_bounds_ = host->GetNonTransformedWindowBounds(bounds_); ++ } + + if (!IsRootWindow() && old_bounds.size() != bounds_.size() && + IsEmbeddingExternalContent()) { +diff --git a/ui/aura/window.h b/ui/aura/window.h +index 7329105f765b7bf341dffe94a58eee0423299bd4..d006ccbd75c68ed79f52afca43e5511336a87bb6 100644 +--- a/ui/aura/window.h ++++ b/ui/aura/window.h +@@ -212,6 +212,7 @@ class AURA_EXPORT Window : public ui::LayerDelegate, + const WindowDelegate* delegate() const { return delegate_; } + + const gfx::Rect& bounds() const { return bounds_; } ++ const gfx::Rect& non_transformed_bounds() const { return non_transformed_bounds_; } + + Window* parent() { return parent_; } + const Window* parent() const { return parent_; } +@@ -758,6 +759,11 @@ class AURA_EXPORT Window : public ui::LayerDelegate, + // is relative to the parent Window. + gfx::Rect bounds_; + ++ // The bounds_ of the layer will be transformed by the target transform. However, ++ // certain operations, such as hit testing, require the bounds_ to be in the ++ // non-transformed state. This is the non-transformed bounds_. ++ gfx::Rect non_transformed_bounds_; ++ + raw_ptr host_ = nullptr; + + client::WindowType type_; +diff --git a/ui/aura/window_event_dispatcher.cc b/ui/aura/window_event_dispatcher.cc +index ea66e6bb159a3b14e9c9abfed642c19356b0b9c1..ce64ff7db0d37083f3da19a08386bf911d664860 100644 +--- a/ui/aura/window_event_dispatcher.cc ++++ b/ui/aura/window_event_dispatcher.cc +@@ -547,6 +547,14 @@ ui::EventDispatchDetails WindowEventDispatcher::PreDispatchEvent( + return details; + } + ++ // Only convert the location for mouse and touch events as other events, such as gesture, scroll and pinch, ++ // are derived from these and so don't need their locations transformed. ++ if (event->IsMouseEvent() || event->IsTouchEvent()) { ++ ui::LocatedEvent* located_event = event->AsLocatedEvent(); ++ located_event->set_location(host_->GetTransformedPoint(located_event->location())); ++ located_event->set_root_location(host_->GetTransformedPoint(located_event->root_location())); ++ } ++ + DispatchDetails details; + if (event->IsMouseEvent()) { + details = PreDispatchMouseEvent(target_window, event->AsMouseEvent()); +diff --git a/ui/aura/window_targeter.cc b/ui/aura/window_targeter.cc +index 636e2d089280b174c24c2978c419dbaa70558507..6a0e791a6336e9874f6275a976224a41039e61c1 100644 +--- a/ui/aura/window_targeter.cc ++++ b/ui/aura/window_targeter.cc +@@ -39,7 +39,7 @@ bool WindowTargeter::GetHitTestRects(Window* window, + gfx::Rect* hit_test_rect_touch) const { + DCHECK(hit_test_rect_mouse); + DCHECK(hit_test_rect_touch); +- *hit_test_rect_mouse = *hit_test_rect_touch = window->bounds(); ++ *hit_test_rect_mouse = *hit_test_rect_touch = window->non_transformed_bounds(); + + if (ShouldUseExtendedBounds(window)) { + hit_test_rect_mouse->Inset(mouse_extend_); +@@ -141,7 +141,7 @@ Window* WindowTargeter::FindTargetInRootWindow(Window* root_window, + #else + // If the initial touch is outside the root window, target the root. + // TODO(lanwei): this code is likely not necessarily and will be removed. +- if (!root_window->bounds().Contains(event.location())) ++ if (!root_window->non_transformed_bounds().Contains(event.location())) + return root_window; + #endif + } +diff --git a/ui/aura/window_tree_host.cc b/ui/aura/window_tree_host.cc +index d2f51a75454c3efecbf788bb13560e3a609c306a..2d753066243c6cd5f95c3d356b62ef6003b9340b 100644 +--- a/ui/aura/window_tree_host.cc ++++ b/ui/aura/window_tree_host.cc +@@ -181,11 +181,109 @@ gfx::Transform WindowTreeHost::GetInverseRootTransform() const { + } + + void WindowTreeHost::SetDisplayTransformHint(gfx::OverlayTransform transform) { ++ transform_hint_ = transform; + if (compositor()->display_transform_hint() == transform) + return; + + compositor()->SetDisplayTransformHint(transform); +- UpdateCompositorScaleAndSize(GetBoundsInPixels().size()); ++ OnHostResizedInPixels(GetBoundsInPixels().size()); ++ CalculateInputTransform(transform); ++} ++ ++gfx::Point WindowTreeHost::GetTransformedPoint(const gfx::Point& point) { ++ return input_transform_.MapPoint(point); ++} ++ ++gfx::Rect WindowTreeHost::GetTransformedCompositorBounds(const gfx::Rect& new_bounds) { ++ if (IsPopup()) { ++ // Ensure that the compositor size is always the same as the original popup bounds. ++ return gfx::Rect(new_bounds.origin(), gfx::Size(original_popup_bounds_.width(), original_popup_bounds_.height())); ++ } ++ ++ return GetTransformedWindowBounds(new_bounds); ++} ++ ++gfx::Rect WindowTreeHost::GetTransformedWindowBounds(const gfx::Rect& new_bounds) { ++ gfx::Rect transformed_bounds(new_bounds); ++ ++ if (IsPopup()) { ++ if (transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90 || ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270) { ++ // Make the window bounds equal the original popup bounds with the width and height swapped. ++ transformed_bounds.set_size(gfx::Size(original_popup_bounds_.height(), original_popup_bounds_.width())); ++ } ++ } ++ else { ++ gfx::Rect window_bounds = GetBoundsInPixels(); ++ if ((transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90 || ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270) && ++ window_bounds.height() && window_bounds.width()) { ++ float window_aspect_ratio = (float)window_bounds.width() / (float)window_bounds.height(); ++ float new_aspect_ratio = (float)new_bounds.width() / (float)new_bounds.height(); ++ ++ // If the aspect ratio of the window and new bounds are the same it means that new_bounds ++ // has not already been transposed. ++ if ((window_aspect_ratio > 1.0 && new_aspect_ratio > 1.0) || ++ (window_aspect_ratio < 1.0 && new_aspect_ratio < 1.0)) { ++ transformed_bounds.Transpose(); ++ } ++ } ++ } ++ return transformed_bounds; ++} ++ ++gfx::Rect WindowTreeHost::GetNonTransformedWindowBounds(const gfx::Rect& new_bounds) { ++ gfx::Rect non_transformed_bounds(new_bounds); ++ gfx::Rect window_bounds = GetBoundsInPixels(); ++ ++ if ((transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90 || ++ transform_hint_ == gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270) && ++ window_bounds.height() && window_bounds.width()) { ++ float window_aspect_ratio = (float)window_bounds.width() / (float)window_bounds.height(); ++ float new_aspect_ratio = (float)new_bounds.width() / (float)new_bounds.height(); ++ ++ // If the aspect ratio of the window and new bounds are different it means that new_bounds ++ // has already been transposed, so we need to transpose it back. ++ if ((window_aspect_ratio > 1.0 && new_aspect_ratio < 1.0) || ++ (window_aspect_ratio < 1.0 && new_aspect_ratio > 1.0)) { ++ non_transformed_bounds.Transpose(); ++ } ++ } ++ return non_transformed_bounds; ++} ++ ++void WindowTreeHost::CalculateInputTransform(gfx::OverlayTransform transform) { ++ input_transform_.MakeIdentity(); ++ popup_transform_.MakeIdentity(); ++ ++ switch (transform) { ++ case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90: ++ input_transform_.Translate(0, GetBoundsInPixels().width()); ++ input_transform_.Rotate(-90); ++ ++ popup_transform_.Translate(GetBoundsInPixels().width(), 0); ++ popup_transform_.Rotate(90); ++ break; ++ ++ case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_180: ++ input_transform_.Translate(GetBoundsInPixels().width(), GetBoundsInPixels().height()); ++ input_transform_.Rotate(180); ++ ++ popup_transform_.Translate(GetBoundsInPixels().width(), GetBoundsInPixels().height()); ++ popup_transform_.Rotate(180); ++ break; ++ ++ case gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270: ++ input_transform_.Translate(GetBoundsInPixels().height(), 0); ++ input_transform_.Rotate(90); ++ ++ popup_transform_.Translate(0, GetBoundsInPixels().height()); ++ popup_transform_.Rotate(-90); ++ break; ++ ++ default: ++ break; ++ } + } + + gfx::Transform WindowTreeHost::GetRootTransformForLocalEventCoordinates() +@@ -204,19 +302,15 @@ gfx::Transform WindowTreeHost::GetInverseRootTransformForLocalEventCoordinates() + + void WindowTreeHost::UpdateCompositorScaleAndSize( + const gfx::Size& new_size_in_pixels) { +- gfx::Rect new_bounds(new_size_in_pixels); +- if (compositor_->display_transform_hint() == +- gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90 || +- compositor_->display_transform_hint() == +- gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270) { +- new_bounds.Transpose(); +- } ++ gfx::Rect orig_bounds(new_size_in_pixels); ++ gfx::Rect new_bounds = GetTransformedCompositorBounds(orig_bounds); + + // Allocate a new LocalSurfaceId for the new size or scale factor. + window_->AllocateLocalSurfaceId(); + ScopedLocalSurfaceIdValidator lsi_validator(window()); + compositor_->SetScaleAndSize(device_scale_factor_, new_bounds.size(), + window_->GetLocalSurfaceId()); ++ CalculateInputTransform(transform_hint_); + } + + void WindowTreeHost::ConvertDIPToScreenInPixels(gfx::Point* point) const { +diff --git a/ui/aura/window_tree_host.h b/ui/aura/window_tree_host.h +index e859d77fe109ea59b21550df740cf4c0e0e4e474..6580c3669b5f5ec0ad038c0da694896dc0133687 100644 +--- a/ui/aura/window_tree_host.h ++++ b/ui/aura/window_tree_host.h +@@ -29,6 +29,7 @@ + #include "ui/events/platform_event.h" + #include "ui/gfx/native_ui_types.h" + #include "ui/gfx/overlay_transform.h" ++#include "ui/gfx/geometry/transform.h" + + namespace gfx { + class DisplayColorSpacesRef; +@@ -127,6 +128,15 @@ class AURA_EXPORT WindowTreeHost : public ui::ImeKeyEventDispatcher, + + void SetDisplayTransformHint(gfx::OverlayTransform transform); + ++ gfx::Point GetTransformedPoint(const gfx::Point& point); ++ gfx::Rect GetTransformedCompositorBounds(const gfx::Rect& new_bounds); ++ gfx::Rect GetTransformedWindowBounds(const gfx::Rect& new_bounds); ++ gfx::Rect GetNonTransformedWindowBounds(const gfx::Rect& new_bounds); ++ gfx::Transform GetPopupTransform() const { return popup_transform_; } ++ gfx::OverlayTransform GetDisplayTransformHint() const { return transform_hint_; } ++ bool IsPopup() const { return !original_popup_bounds_.IsEmpty(); } ++ void SetPopupBounds(const gfx::Rect& bounds) { original_popup_bounds_ = bounds; } ++ + // These functions are used in event translation for translating the local + // coordinates of LocatedEvents. Default implementation calls to non-local + // ones (e.g. GetRootTransform()). +@@ -440,6 +450,8 @@ class AURA_EXPORT WindowTreeHost : public ui::ImeKeyEventDispatcher, + void OnFirstSurfaceActivation(ui::Compositor* compositor, + const viz::SurfaceInfo& surface_info) override; + ++ void CalculateInputTransform(gfx::OverlayTransform transform); ++ + // We don't use a std::unique_ptr for |window_| since we need this ptr to be + // valid during its deletion. (Window's dtor notifies observers that may + // attempt to reach back up to access this object which will be valid until +@@ -512,6 +524,12 @@ class AURA_EXPORT WindowTreeHost : public ui::ImeKeyEventDispatcher, + // This is only used when occlusion tracking is always enabled. + int video_capture_count_for_occlusion_tracking_ = 0; + ++ gfx::OverlayTransform transform_hint_ = gfx::OVERLAY_TRANSFORM_NONE; ++ ++ gfx::Transform input_transform_; ++ gfx::Transform popup_transform_; ++ gfx::Rect original_popup_bounds_; ++ + base::WeakPtrFactory weak_factory_{this}; + }; + +diff --git a/ui/views/widget/desktop_aura/desktop_native_widget_aura.cc b/ui/views/widget/desktop_aura/desktop_native_widget_aura.cc +index 446d82985dab4486f876fb957caea143de98a7ba..50b7e715f6562b17b436210eef539bd7a9bf8624 100644 +--- a/ui/views/widget/desktop_aura/desktop_native_widget_aura.cc ++++ b/ui/views/widget/desktop_aura/desktop_native_widget_aura.cc +@@ -904,7 +904,11 @@ void DesktopNativeWidgetAura::SetBounds(const gfx::Rect& bounds) { + if (!desktop_window_tree_host_) { + return; + } +- desktop_window_tree_host_->SetBoundsInDIP(bounds); ++ gfx::Rect final_bounds(bounds); ++ if (host_ && host_->IsPopup()) { ++ final_bounds = host_->GetTransformedWindowBounds(bounds); ++ } ++ desktop_window_tree_host_->SetBoundsInDIP(final_bounds); + } + + void DesktopNativeWidgetAura::SetBoundsConstrained(const gfx::Rect& bounds) { diff --git a/shell/browser/api/electron_api_base_window.cc b/shell/browser/api/electron_api_base_window.cc index 4fe4fd3ad6f8e..1e47aa66dd280 100644 --- a/shell/browser/api/electron_api_base_window.cc +++ b/shell/browser/api/electron_api_base_window.cc @@ -1144,6 +1144,25 @@ v8::Local BaseWindow::GetAccentColor() const { } #endif +#if BUILDFLAG(IS_LINUX) +void BaseWindow::SetWindowTransform(const std::string& transform) { + blink::mojom::WindowTransformType transform_type = + blink::mojom::WindowTransformType::kWindowTransformTypeNone; + + if (transform == "rot180") { + transform_type = + blink::mojom::WindowTransformType::kWindowTransformTypeRotate180; + } else if (transform == "rot270") { + transform_type = + blink::mojom::WindowTransformType::kWindowTransformTypeRotate270; + } else if (transform == "rot90") { + transform_type = + blink::mojom::WindowTransformType::kWindowTransformTypeRotate90; + } + window_->SetWindowTransform(transform_type); +} +#endif + #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) void BaseWindow::SetTitleBarOverlay(const gin_helper::Dictionary& options, gin::Arguments* args) { @@ -1328,6 +1347,9 @@ void BaseWindow::BuildPrototype(v8::Isolate* isolate, #endif #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) .SetMethod("setTitleBarOverlay", &BaseWindow::SetTitleBarOverlay) +#endif +#if BUILDFLAG(IS_LINUX) + .SetMethod("setWindowTransform", &BaseWindow::SetWindowTransform) #endif .SetProperty("id", &BaseWindow::GetID); } diff --git a/shell/browser/api/electron_api_base_window.h b/shell/browser/api/electron_api_base_window.h index b62d812781ebf..8c4775c4b1ce7 100644 --- a/shell/browser/api/electron_api_base_window.h +++ b/shell/browser/api/electron_api_base_window.h @@ -232,6 +232,9 @@ class BaseWindow : public gin_helper::TrackableObject, void PreviewFile(const std::string& path, gin::Arguments* args); void CloseFilePreview(); void SetGTKDarkThemeEnabled(bool use_dark_theme); +#if BUILDFLAG(IS_LINUX) + void SetWindowTransform(const std::string& transform); +#endif // Public getters of NativeWindow. v8::Local GetContentView() const; diff --git a/shell/browser/native_window.h b/shell/browser/native_window.h index 02a34be972aad..d55d6c3fb05e5 100644 --- a/shell/browser/native_window.h +++ b/shell/browser/native_window.h @@ -22,6 +22,7 @@ #include "extensions/browser/app_window/size_constraints.h" #include "shell/browser/native_window_observer.h" #include "third_party/abseil-cpp/absl/container/flat_hash_set.h" +#include "third_party/blink/public/mojom/webpreferences/web_preferences.mojom-shared.h" #include "ui/views/widget/widget_delegate.h" class SkRegion; @@ -196,6 +197,10 @@ class NativeWindow : public views::WidgetDelegate { virtual bool IsFocusable() const; virtual void SetMenu(ElectronMenuModel* menu) {} virtual void SetParentWindow(NativeWindow* parent); +#if BUILDFLAG(IS_LINUX) + virtual void SetWindowTransform( + blink::mojom::WindowTransformType transform_type) = 0; +#endif virtual content::DesktopMediaID GetDesktopMediaID() const = 0; virtual gfx::NativeView GetNativeView() const = 0; virtual gfx::NativeWindow GetNativeWindow() const = 0; diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index fa6865fdaa2e7..fe8d103d34812 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -529,6 +529,33 @@ void NativeWindowViews::SetGTKDarkThemeEnabled(bool use_dark_theme) { #endif } +#if BUILDFLAG(IS_LINUX) +void NativeWindowViews::SetWindowTransform( + blink::mojom::WindowTransformType transform_type) { + const gfx::AcceleratedWidget accelerated_widget = GetAcceleratedWidget(); + aura::WindowTreeHost* const host = + aura::WindowTreeHost::GetForAcceleratedWidget(accelerated_widget); + + gfx::OverlayTransform window_transform = gfx::OVERLAY_TRANSFORM_NONE; + + switch (transform_type) { + case blink::mojom::WindowTransformType::kWindowTransformTypeRotate90: + window_transform = gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_90; + break; + case blink::mojom::WindowTransformType::kWindowTransformTypeRotate180: + window_transform = gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_180; + break; + case blink::mojom::WindowTransformType::kWindowTransformTypeRotate270: + window_transform = gfx::OVERLAY_TRANSFORM_ROTATE_CLOCKWISE_270; + break; + default: + window_transform = gfx::OVERLAY_TRANSFORM_NONE; + break; + } + host->SetDisplayTransformHint(window_transform); +} +#endif + void NativeWindowViews::SetContentView(views::View* view) { if (content_view()) { root_view_.GetMainView()->RemoveChildView(content_view()); diff --git a/shell/browser/native_window_views.h b/shell/browser/native_window_views.h index eaeefd92c0fab..c7a90b3ea0ec4 100644 --- a/shell/browser/native_window_views.h +++ b/shell/browser/native_window_views.h @@ -150,7 +150,10 @@ class NativeWindowViews : public NativeWindow, bool IsVisibleOnAllWorkspaces() const override; void SetGTKDarkThemeEnabled(bool use_dark_theme) override; - +#if BUILDFLAG(IS_LINUX) + void SetWindowTransform( + blink::mojom::WindowTransformType transform_type) override; +#endif content::DesktopMediaID GetDesktopMediaID() const override; gfx::AcceleratedWidget GetAcceleratedWidget() const override; NativeWindowHandle GetNativeWindowHandle() const override; diff --git a/shell/browser/web_contents_preferences.cc b/shell/browser/web_contents_preferences.cc index 12addf09ddcd4..fd44dc63cae80 100644 --- a/shell/browser/web_contents_preferences.cc +++ b/shell/browser/web_contents_preferences.cc @@ -71,6 +71,23 @@ struct Converter { } }; +template <> +struct Converter { + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + blink::mojom::WindowTransformType* out) { + using Val = blink::mojom::WindowTransformType; + static constexpr auto Lookup = + base::MakeFixedFlatMap({ + {"none", Val::kWindowTransformTypeNone}, + {"rot180", Val::kWindowTransformTypeRotate180}, + {"rot270", Val::kWindowTransformTypeRotate270}, + {"rot90", Val::kWindowTransformTypeRotate90}, + }); + return FromV8WithLookup(isolate, val, Lookup, out); + } +}; + } // namespace gin namespace electron { @@ -158,6 +175,9 @@ void WebContentsPreferences::Clear() { #if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER) spellcheck_ = true; #endif + + window_transform_type_ = + blink::mojom::WindowTransformType::kWindowTransformTypeNone; } void WebContentsPreferences::SetFromDictionary( @@ -262,6 +282,8 @@ void WebContentsPreferences::SetFromDictionary( web_preferences.Get(options::kSpellcheck, &spellcheck_); #endif + web_preferences.Get("windowTransform", &window_transform_type_); + SaveLastPreferences(); } @@ -477,6 +499,7 @@ void WebContentsPreferences::OverrideWebkitPrefs( prefs->dom_paste_enabled = deprecated_paste_enabled_; prefs->hide_scrollbars = hide_scroll_bars_; prefs->enable_pinch_zoom = enable_pinch_zoom_; + prefs->window_transform_type = window_transform_type_; } WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPreferences); diff --git a/shell/browser/web_contents_preferences.h b/shell/browser/web_contents_preferences.h index 74fe7bcf83042..26452acab0728 100644 --- a/shell/browser/web_contents_preferences.h +++ b/shell/browser/web_contents_preferences.h @@ -145,6 +145,8 @@ class WebContentsPreferences bool spellcheck_; #endif + blink::mojom::WindowTransformType window_transform_type_; + // This is a snapshot of some relevant preferences at the time the renderer // was launched. base::Value last_web_preferences_; diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts index f960403721182..6b7fca9cbed21 100755 --- a/spec/api-browser-window-spec.ts +++ b/spec/api-browser-window-spec.ts @@ -7533,4 +7533,433 @@ describe('BrowserWindow module', () => { expect(scrollHeight).to.be.equal(0); }); }); + + ifdescribe(process.platform === 'linux')('window rotation', () => { + const screenSize = screen.getPrimaryDisplay().size; + const display = screen.getPrimaryDisplay(); + const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures'); + const url = `file://${fixturesPath}/pages/rotation-test.html`; + const landscapeBounds = { x: 0, y: 0, width: 600, height: 400 }; + const portraitBounds = { x: 0, y: 0, width: 500, height: 900 }; + const squareBounds = { x: 0, y: 0, width: 350, height: 350 }; + const offsetBounds = { x: 100, y: 200, width: 600, height: 400 }; + const negativeOffsetBounds = { x: -5, y: -5, width: 600, height: 400 }; + const fullScreenBounds = { x: 0, y: 0, width: screenSize.width - 1, height: screenSize.height - 1 }; + const SELECT_HEIGHT = 10; + const SELECT_WIDTH = 20; + const SELECT_OPTION_HEIGHT = 18; + + // Select popups and will have a border/shadow so want to pick a pixel in the middle when comparing the colour. + const SELECT_HEIGHT_OFFSET = SELECT_HEIGHT / 2; + const SELECT_WIDTH_OFFSET = SELECT_WIDTH / 2; + + // The first option will be highlighted so we want to pick the second. + // Also, select popup options have a border/shadow so we want to pick a pixel in the middle when comparing the colour. + const SELECT_OPTION_HEIGHT_OFFSET = SELECT_HEIGHT + SELECT_OPTION_HEIGHT * 1.5; + const SELECT_OPTION_WIDTH_OFFSET = SELECT_WIDTH / 2; + + const MOUSE_X_OFFSET = 10; + const MOUSE_Y_OFFSET = 5; + let w: BrowserWindow; + const clickMouse = (w: BrowserWindow, pos: {x: number, y: number}) => { + w.webContents.sendInputEvent({ type: 'mouseDown', clickCount: 1, x: pos.x, y: pos.y }); + w.webContents.sendInputEvent({ type: 'mouseUp', clickCount: 1, x: pos.x, y: pos.y }); + }; + // Enable to help debug problems. + // const dumpImageForDebug = async (filename: string) => { + // const screen = await captureScreen(); + // const pngImage = screen.toPNG(); + // fs.writeFileSync(filename, pngImage); + // } + + describe('webpreference', () => { + afterEach(closeAllWindows); + after(() => { w = null as unknown as BrowserWindow; }); + + [{ name: 'landscape', bounds: landscapeBounds }, { name: 'portrait', bounds: portraitBounds }, { name: 'square', bounds: squareBounds }, + { name: 'x,y offset', bounds: offsetBounds }, { name: 'x,y negative offset', bounds: negativeOffsetBounds }, + { name: 'full screen', bounds: fullScreenBounds }].forEach((item) => { + it(`identity: ${item.name}`, async () => { + w = new BrowserWindow({ ...item.bounds, frame: false, webPreferences: { windowTransform: 'none' } }); + await w.loadURL(url); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(item.bounds.width); + expect(windowHeight).to.be.equal(item.bounds.height); + expectBoundsEqual(w.getBounds(), item.bounds); + + await setTimeout(500); + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: item.bounds.x + SELECT_WIDTH_OFFSET, + y: item.bounds.y + SELECT_HEIGHT_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: item.bounds.x + SELECT_OPTION_WIDTH_OFFSET, + y: item.bounds.y + SELECT_OPTION_HEIGHT_OFFSET + }) + ); + }); + + it(`rot90: ${item.name}`, async () => { + w = new BrowserWindow({ ...item.bounds, frame: false, webPreferences: { windowTransform: 'rot90' } }); + await w.loadURL(url); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(item.bounds.height); + expect(windowHeight).to.be.equal(item.bounds.width); + expectBoundsEqual(w.getBounds(), item.bounds); + + await setTimeout(500); + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: item.bounds.x + item.bounds.width - SELECT_HEIGHT_OFFSET, + y: item.bounds.y + SELECT_WIDTH_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: item.bounds.x + item.bounds.width - SELECT_OPTION_HEIGHT_OFFSET, + y: item.bounds.y + SELECT_OPTION_WIDTH_OFFSET + }) + ); + }); + + it(`rot180: ${item.name}`, async () => { + w = new BrowserWindow({ ...item.bounds, frame: false, webPreferences: { windowTransform: 'rot180' } }); + await w.loadURL(url); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(item.bounds.width); + expect(windowHeight).to.be.equal(item.bounds.height); + expectBoundsEqual(w.getBounds(), item.bounds); + + await setTimeout(500); + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: item.bounds.x + item.bounds.width - SELECT_WIDTH_OFFSET, + y: item.bounds.y + item.bounds.height - SELECT_HEIGHT_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: item.bounds.x + item.bounds.width - SELECT_OPTION_WIDTH_OFFSET, + y: item.bounds.y + item.bounds.height - SELECT_OPTION_HEIGHT_OFFSET + }) + ); + }); + + it(`rot270: ${item.name}`, async () => { + w = new BrowserWindow({ ...item.bounds, frame: false, webPreferences: { windowTransform: 'rot270' } }); + await w.loadURL(url); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(item.bounds.height); + expect(windowHeight).to.be.equal(item.bounds.width); + expectBoundsEqual(w.getBounds(), item.bounds); + + await setTimeout(500); + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: item.bounds.x + SELECT_HEIGHT_OFFSET, + y: item.bounds.y + item.bounds.height - SELECT_WIDTH_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: item.bounds.x + SELECT_OPTION_HEIGHT_OFFSET, + y: item.bounds.y + item.bounds.height - SELECT_OPTION_WIDTH_OFFSET + }) + ); + }); + }); + }); + + describe('SetWindowTransform', () => { + before(async () => { + w = new BrowserWindow({ ...landscapeBounds, frame: false, webPreferences: { windowTransform: 'none' } }); + await w.loadURL(url); + }); + after(async () => { + await closeWindow(w); + w = null as unknown as BrowserWindow; + }); + afterEach(() => { clickMouse(w, { x: 500, y: 500 }); }); + + it('rot90', async () => { + w.setWindowTransform('rot90'); + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(landscapeBounds.height); + expect(windowHeight).to.be.equal(landscapeBounds.width); + expectBoundsEqual(w.getBounds(), landscapeBounds); + + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: landscapeBounds.x + landscapeBounds.width - SELECT_HEIGHT_OFFSET, + y: landscapeBounds.y + SELECT_WIDTH_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: landscapeBounds.x + landscapeBounds.width - SELECT_OPTION_HEIGHT_OFFSET, + y: landscapeBounds.y + SELECT_OPTION_WIDTH_OFFSET + }) + ); + }); + + it('rot180', async () => { + w.setWindowTransform('rot180'); + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(landscapeBounds.width); + expect(windowHeight).to.be.equal(landscapeBounds.height); + expectBoundsEqual(w.getBounds(), landscapeBounds); + + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: landscapeBounds.x + landscapeBounds.width - SELECT_WIDTH_OFFSET, + y: landscapeBounds.y + landscapeBounds.height - SELECT_HEIGHT_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: landscapeBounds.x + landscapeBounds.width - SELECT_OPTION_WIDTH_OFFSET, + y: landscapeBounds.y + landscapeBounds.height - SELECT_OPTION_HEIGHT_OFFSET + }) + ); + }); + + it('rot270', async () => { + w.setWindowTransform('rot270'); + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(landscapeBounds.height); + expect(windowHeight).to.be.equal(landscapeBounds.width); + expectBoundsEqual(w.getBounds(), landscapeBounds); + + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: landscapeBounds.x + SELECT_HEIGHT_OFFSET, + y: landscapeBounds.y + landscapeBounds.height - SELECT_WIDTH_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: landscapeBounds.x + SELECT_OPTION_HEIGHT_OFFSET, + y: landscapeBounds.y + landscapeBounds.height - SELECT_OPTION_WIDTH_OFFSET + }) + ); + }); + + it('Resize while rotated to portraitBounds', async () => { + w.setBounds(portraitBounds); + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(portraitBounds.height); + expect(windowHeight).to.be.equal(portraitBounds.width); + expectBoundsEqual(w.getBounds(), portraitBounds); + + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: portraitBounds.x + SELECT_HEIGHT_OFFSET, + y: portraitBounds.y + portraitBounds.height - SELECT_WIDTH_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: portraitBounds.x + SELECT_OPTION_HEIGHT_OFFSET, + y: portraitBounds.y + portraitBounds.height - SELECT_OPTION_WIDTH_OFFSET + }) + ); + }); + + it('Resize while rotated to squareBounds', async () => { + w.setBounds(squareBounds); + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(squareBounds.height); + expect(windowHeight).to.be.equal(squareBounds.width); + expectBoundsEqual(w.getBounds(), squareBounds); + + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: squareBounds.x + SELECT_HEIGHT_OFFSET, + y: squareBounds.y + squareBounds.height - SELECT_WIDTH_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: squareBounds.x + SELECT_OPTION_HEIGHT_OFFSET, + y: squareBounds.y + squareBounds.height - SELECT_OPTION_WIDTH_OFFSET + }) + ); + }); + + it('Resize while rotated to bounds with x,y offsets', async () => { + w.setBounds(offsetBounds); + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(offsetBounds.height); + expect(windowHeight).to.be.equal(offsetBounds.width); + expectBoundsEqual(w.getBounds(), offsetBounds); + + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: offsetBounds.x + SELECT_HEIGHT_OFFSET, + y: offsetBounds.y + offsetBounds.height - SELECT_WIDTH_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: offsetBounds.x + SELECT_OPTION_HEIGHT_OFFSET, + y: offsetBounds.y + offsetBounds.height - SELECT_OPTION_WIDTH_OFFSET + }) + ); + }); + + it('Resize while rotated to bounds with negative x,y offsets', async () => { + w.setBounds(negativeOffsetBounds); + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(negativeOffsetBounds.height); + expect(windowHeight).to.be.equal(negativeOffsetBounds.width); + expectBoundsEqual(w.getBounds(), negativeOffsetBounds); + + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: negativeOffsetBounds.x + SELECT_HEIGHT_OFFSET, + y: negativeOffsetBounds.y + negativeOffsetBounds.height - SELECT_WIDTH_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: negativeOffsetBounds.x + SELECT_OPTION_HEIGHT_OFFSET, + y: negativeOffsetBounds.y + negativeOffsetBounds.height - SELECT_OPTION_WIDTH_OFFSET + }) + ); + }); + + it('identity', async () => { + w.setWindowTransform('none'); + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(negativeOffsetBounds.width); + expect(windowHeight).to.be.equal(negativeOffsetBounds.height); + expectBoundsEqual(w.getBounds(), negativeOffsetBounds); + + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: negativeOffsetBounds.x + SELECT_WIDTH_OFFSET, + y: negativeOffsetBounds.y + SELECT_HEIGHT_OFFSET + }) + ); + + clickMouse(w, { x: MOUSE_X_OFFSET, y: MOUSE_Y_OFFSET }); + await setTimeout(500); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: negativeOffsetBounds.x + SELECT_OPTION_WIDTH_OFFSET, + y: negativeOffsetBounds.y + SELECT_OPTION_HEIGHT_OFFSET + }) + ); + }); + + it('Check fullscreen bottom right option in correct location', async () => { + w.setBounds(fullScreenBounds); + await setTimeout(500); + const { windowWidth, windowHeight } = await w.webContents.executeJavaScript('({windowWidth: window.innerWidth, windowHeight: window.innerHeight})'); + expect(windowWidth).to.be.equal(fullScreenBounds.width); + expect(windowHeight).to.be.equal(fullScreenBounds.height); + expectBoundsEqual(w.getBounds(), fullScreenBounds); + + const screenCapture = new ScreenCapture(display); + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.RED, + () => ({ + x: fullScreenBounds.width - SELECT_WIDTH_OFFSET, + y: fullScreenBounds.height - SELECT_HEIGHT_OFFSET + }) + ); + + clickMouse(w, { x: fullScreenBounds.width - MOUSE_X_OFFSET, y: fullScreenBounds.height - MOUSE_Y_OFFSET }); + await setTimeout(500); + // Check when fullscreen the option for the bottom right select element is rendered above the element and inside the window + await screenCapture.expectColorAtPointOnDisplayMatches( + HexColors.GREEN, + () => ({ + x: fullScreenBounds.width - SELECT_OPTION_WIDTH_OFFSET, + y: fullScreenBounds.height - SELECT_HEIGHT - SELECT_OPTION_HEIGHT / 2 + }) + ); + }); + }); + }); }); diff --git a/spec/fixtures/pages/rotation-test.html b/spec/fixtures/pages/rotation-test.html new file mode 100644 index 0000000000000..7a86e48c43ddc --- /dev/null +++ b/spec/fixtures/pages/rotation-test.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + From a50dad1971115afae8a59123d58e2df16c6f8d67 Mon Sep 17 00:00:00 2001 From: Jon Dye Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 28/68] fix: Disable changing mouse cursor (OS-17765) Cherry picked from: fb38bc791ca160f96606cb9199572515a934f3a0: fix: Disable changing mouse cursor (OS-17765) --- patches/chromium/.patches | 1 + ...-17765_disable_changing_mouse_cursor.patch | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 patches/chromium/os-17765_disable_changing_mouse_cursor.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index c19df81002e8e..e08df1133d398 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -167,3 +167,4 @@ os-17053_use_same_nssdb_path_for_rokeystore_and_chromium.patch launcher_implement_setting_uid_and_groups_for_renderer_process.patch os-17400_brightsign_increase_file_open_handle_for_all_processes.patch os-17652_add_support_for_window_rotation.patch +os-17765_disable_changing_mouse_cursor.patch diff --git a/patches/chromium/os-17765_disable_changing_mouse_cursor.patch b/patches/chromium/os-17765_disable_changing_mouse_cursor.patch new file mode 100644 index 0000000000000..95e960d73354c --- /dev/null +++ b/patches/chromium/os-17765_disable_changing_mouse_cursor.patch @@ -0,0 +1,51 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jon Dye +Date: Thu, 14 Nov 2024 10:22:18 +0000 +Subject: OS-17765: Disable changing mouse cursor + +We don't want chromium to set the mouse cursor as we're setting it from +within the brightsign app. This patch disables calls to SetCursor in +chromium so it doesn't override the cursor we've applied. We observed +the cursor being overridden when the screen was rotated. + +diff --git a/ui/views/widget/desktop_aura/desktop_native_cursor_manager.cc b/ui/views/widget/desktop_aura/desktop_native_cursor_manager.cc +index 9e25f070df09b6c407aab52ac09da14fb69be8df..32c7883388ef22fc480597ef3129a4d8685c9c65 100644 +--- a/ui/views/widget/desktop_aura/desktop_native_cursor_manager.cc ++++ b/ui/views/widget/desktop_aura/desktop_native_cursor_manager.cc +@@ -41,15 +41,14 @@ void DesktopNativeCursorManager::SetDisplay( + void DesktopNativeCursorManager::SetCursor( + gfx::NativeCursor cursor, + wm::NativeCursorManagerDelegate* delegate) { +- gfx::NativeCursor new_cursor = cursor; +- cursor_loader_.SetPlatformCursor(&new_cursor); +- delegate->CommitCursor(new_cursor); +- +- if (delegate->IsCursorVisible()) { +- for (aura::WindowTreeHost* host : hosts_) { +- host->SetCursor(new_cursor); +- } +- } ++ gfx::NativeCursor invisible_cursor(ui::mojom::CursorType::kNone); ++ cursor_loader_.SetPlatformCursor(&invisible_cursor); ++ delegate->CommitCursor(invisible_cursor); ++ ++ //if (delegate->IsCursorVisible()) { ++ // for (auto* host : hosts_) ++ // host->SetCursor(new_cursor); ++ //} + } + + void DesktopNativeCursorManager::SetVisibility( +@@ -64,9 +63,9 @@ void DesktopNativeCursorManager::SetVisibility( + } else { + gfx::NativeCursor invisible_cursor(ui::mojom::CursorType::kNone); + cursor_loader_.SetPlatformCursor(&invisible_cursor); +- for (aura::WindowTreeHost* host : hosts_) { +- host->SetCursor(invisible_cursor); +- } ++ // for (aura::WindowTreeHost* host : hosts_) { ++ // host->SetCursor(invisible_cursor); ++ // } + } + + for (aura::WindowTreeHost* host : hosts_) { From 8e06ec8b3825d5654873d847a87e8054d34bba07 Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 29/68] Add support for eglGetPlatformDisplayExt fix: Fix eglGetPlatformDisplayEXT OS-18172 eglGetPlatformDisplayEXT wasn't working as expected. Because it was registered as EGL extension, but it should have been a EGL client extension. After this change, EGL display gets initialised correctly and compositor gets HW accelerated. Cherry picked from: a9d3b194b7ca3ee3df5d66ff03a7923cf19d7eb6: Add support for eglGetPlatformDisplayExt 9e16132872776a3d6f10850a9859df5735a8fd91: fix: Fix eglGetPlatformDisplayEXT OS-18172 --- patches/chromium/.patches | 1 + ...r_eglgetplatformdisplayext_extension.patch | 248 ++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 patches/chromium/gl_add_support_for_eglgetplatformdisplayext_extension.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index e08df1133d398..2d3dd0b6bd903 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -168,3 +168,4 @@ launcher_implement_setting_uid_and_groups_for_renderer_process.patch os-17400_brightsign_increase_file_open_handle_for_all_processes.patch os-17652_add_support_for_window_rotation.patch os-17765_disable_changing_mouse_cursor.patch +gl_add_support_for_eglgetplatformdisplayext_extension.patch diff --git a/patches/chromium/gl_add_support_for_eglgetplatformdisplayext_extension.patch b/patches/chromium/gl_add_support_for_eglgetplatformdisplayext_extension.patch new file mode 100644 index 0000000000000..244208af1fe40 --- /dev/null +++ b/patches/chromium/gl_add_support_for_eglgetplatformdisplayext_extension.patch @@ -0,0 +1,248 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Caner Altinbasak +Date: Mon, 21 Oct 2024 14:57:01 +0100 +Subject: gl: Add support for eglGetPlatformDisplayExt extension + +This change is needed for Thor platform which requires eglGetPlatformDisplayExt +for wayland backend + +diff --git a/ui/gl/egl_bindings_autogen_mock.cc b/ui/gl/egl_bindings_autogen_mock.cc +index 646dcb359a794d5c90a76601429ed41e1ee2f523..1644192528bccab61303ca71dea5b91cc38e6bb2 100644 +--- a/ui/gl/egl_bindings_autogen_mock.cc ++++ b/ui/gl/egl_bindings_autogen_mock.cc +@@ -421,6 +421,15 @@ MockEGLInterface::Mock_eglGetPlatformDisplay(EGLenum platform, + return interface_->GetPlatformDisplay(platform, native_display, attrib_list); + } + ++EGLDisplay GL_BINDING_CALL ++MockEGLInterface::Mock_eglGetPlatformDisplayEXT(EGLenum platform, ++ void* native_display, ++ const EGLAttrib* attrib_list) { ++ MakeEglMockFunctionUnique("eglGetPlatformDisplayEXT"); ++ return interface_->GetPlatformDisplayEXT(platform, native_display, ++ attrib_list); ++} ++ + __eglMustCastToProperFunctionPointerType GL_BINDING_CALL + MockEGLInterface::Mock_eglGetProcAddress(const char* procname) { + MakeEglMockFunctionUnique("eglGetProcAddress"); +@@ -943,6 +952,9 @@ MockEGLInterface::GetGLProcAddress(const char* name) { + Mock_eglGetNextFrameIdANDROID); + if (strcmp(name, "eglGetPlatformDisplay") == 0) + return reinterpret_cast(Mock_eglGetPlatformDisplay); ++ if (strcmp(name, "eglGetPlatformDisplayEXT") == 0) ++ return reinterpret_cast( ++ Mock_eglGetPlatformDisplayEXT); + if (strcmp(name, "eglGetProcAddress") == 0) + return reinterpret_cast(Mock_eglGetProcAddress); + if (strcmp(name, "eglGetSyncAttrib") == 0) +diff --git a/ui/gl/egl_bindings_autogen_mock.h b/ui/gl/egl_bindings_autogen_mock.h +index d506a895bd873897e56cd583b9391c1538923663..21a0ac3a3a3c8f614b1c2580ca7b645f6e7b9f74 100644 +--- a/ui/gl/egl_bindings_autogen_mock.h ++++ b/ui/gl/egl_bindings_autogen_mock.h +@@ -186,6 +186,10 @@ static EGLDisplay GL_BINDING_CALL + Mock_eglGetPlatformDisplay(EGLenum platform, + void* native_display, + const EGLAttrib* attrib_list); ++static EGLDisplay GL_BINDING_CALL ++Mock_eglGetPlatformDisplayEXT(EGLenum platform, ++ void* native_display, ++ const EGLAttrib* attrib_list); + static __eglMustCastToProperFunctionPointerType GL_BINDING_CALL + Mock_eglGetProcAddress(const char* procname); + static EGLBoolean GL_BINDING_CALL Mock_eglGetSyncAttrib(EGLDisplay dpy, +diff --git a/ui/gl/generate_bindings.py b/ui/gl/generate_bindings.py +index 43aaf1b54aad802bd882fac33ed14d05aa7b28bf..f8e1514f1db957ecdf3b43c6c70fb1e5a7a4b961 100755 +--- a/ui/gl/generate_bindings.py ++++ b/ui/gl/generate_bindings.py +@@ -1976,6 +1976,13 @@ EGL_FUNCTIONS = [ + 'names': ['eglGetPlatformDisplay'], + 'arguments': 'EGLenum platform, void* native_display, ' + 'const EGLAttrib* attrib_list', }, ++{ 'return_type': 'EGLDisplay', ++ 'versions': [{'name': 'eglGetPlatformDisplayEXT', ++ 'client_extensions': [ ++ 'EGL_EXT_platform_wayland', ++ 'EGL_KHR_platform_wayland']}], ++ 'arguments': 'EGLenum platform, void* native_display, ' ++ 'const EGLAttrib* attrib_list', }, + { 'return_type': '__eglMustCastToProperFunctionPointerType', + 'names': ['eglGetProcAddress'], + 'arguments': 'const char* procname', +diff --git a/ui/gl/gl_bindings_api_autogen_egl.h b/ui/gl/gl_bindings_api_autogen_egl.h +index e6d577edb9335211eca6f44a0279f244b8a85205..20fb0775a03040699e6541a028add62c12d6c628 100644 +--- a/ui/gl/gl_bindings_api_autogen_egl.h ++++ b/ui/gl/gl_bindings_api_autogen_egl.h +@@ -159,6 +159,9 @@ EGLBoolean eglGetNextFrameIdANDROIDFn(EGLDisplay dpy, + EGLDisplay eglGetPlatformDisplayFn(EGLenum platform, + void* native_display, + const EGLAttrib* attrib_list) override; ++EGLDisplay eglGetPlatformDisplayEXTFn(EGLenum platform, ++ void* native_display, ++ const EGLAttrib* attrib_list) override; + __eglMustCastToProperFunctionPointerType eglGetProcAddressFn( + const char* procname) override; + EGLBoolean eglGetSyncAttribFn(EGLDisplay dpy, +diff --git a/ui/gl/gl_bindings_autogen_egl.cc b/ui/gl/gl_bindings_autogen_egl.cc +index bc04b642e4fc79b06e3ab7cb623a95c22092f9fb..2639914f9d32871b2226a502fc73bc4db51f9c83 100644 +--- a/ui/gl/gl_bindings_autogen_egl.cc ++++ b/ui/gl/gl_bindings_autogen_egl.cc +@@ -145,6 +145,9 @@ void DriverEGL::InitializeStaticBindings( + get_proc_address("eglGetNextFrameIdANDROID")); + fn.eglGetPlatformDisplayFn = reinterpret_cast( + get_proc_address("eglGetPlatformDisplay")); ++ fn.eglGetPlatformDisplayEXTFn = ++ reinterpret_cast( ++ GetGLProcAddress("eglGetPlatformDisplayEXT")); + fn.eglGetProcAddressFn = reinterpret_cast( + get_proc_address("eglGetProcAddress")); + fn.eglGetSyncAttribFn = reinterpret_cast( +@@ -312,7 +315,11 @@ void ClientExtensionsEGL::InitializeClientExtensionSettings() { + gfx::HasExtension(extensions, "EGL_EXT_platform_base"); + b_EGL_EXT_platform_device = + gfx::HasExtension(extensions, "EGL_EXT_platform_device"); ++ b_EGL_EXT_platform_wayland = ++ gfx::HasExtension(extensions, "EGL_EXT_platform_wayland"); + b_EGL_KHR_debug = gfx::HasExtension(extensions, "EGL_KHR_debug"); ++ b_EGL_KHR_platform_wayland = ++ gfx::HasExtension(extensions, "EGL_KHR_platform_wayland"); + b_EGL_KHR_platform_gbm = + gfx::HasExtension(extensions, "EGL_KHR_platform_gbm"); + b_EGL_MESA_platform_surfaceless = +@@ -755,6 +762,14 @@ EGLDisplay EGLApiBase::eglGetPlatformDisplayFn(EGLenum platform, + attrib_list); + } + ++EGLDisplay EGLApiBase::eglGetPlatformDisplayEXTFn( ++ EGLenum platform, ++ void* native_display, ++ const EGLAttrib* attrib_list) { ++ return driver_->fn.eglGetPlatformDisplayEXTFn(platform, native_display, ++ attrib_list); ++} ++ + __eglMustCastToProperFunctionPointerType EGLApiBase::eglGetProcAddressFn( + const char* procname) { + return driver_->fn.eglGetProcAddressFn(procname); +@@ -1434,6 +1449,15 @@ EGLDisplay TraceEGLApi::eglGetPlatformDisplayFn(EGLenum platform, + attrib_list); + } + ++EGLDisplay TraceEGLApi::eglGetPlatformDisplayEXTFn( ++ EGLenum platform, ++ void* native_display, ++ const EGLAttrib* attrib_list) { ++ TRACE_EVENT_BINARY_EFFICIENT0("gpu", "TraceEGLAPI::eglGetPlatformDisplayEXT"); ++ return egl_api_->eglGetPlatformDisplayEXTFn(platform, native_display, ++ attrib_list); ++} ++ + __eglMustCastToProperFunctionPointerType TraceEGLApi::eglGetProcAddressFn( + const char* procname) { + TRACE_EVENT_BINARY_EFFICIENT0("gpu", "TraceEGLAPI::eglGetProcAddress"); +@@ -2345,6 +2369,19 @@ EGLDisplay LogEGLApi::eglGetPlatformDisplayFn(EGLenum platform, + return result; + } + ++EGLDisplay LogEGLApi::eglGetPlatformDisplayEXTFn(EGLenum platform, ++ void* native_display, ++ const EGLAttrib* attrib_list) { ++ GL_SERVICE_LOG("eglGetPlatformDisplayEXT" ++ << "(" << platform << ", " ++ << static_cast(native_display) << ", " ++ << static_cast(attrib_list) << ")"); ++ EGLDisplay result = egl_api_->eglGetPlatformDisplayEXTFn( ++ platform, native_display, attrib_list); ++ GL_SERVICE_LOG("GL_RESULT: " << result); ++ return result; ++} ++ + __eglMustCastToProperFunctionPointerType LogEGLApi::eglGetProcAddressFn( + const char* procname) { + GL_SERVICE_LOG("eglGetProcAddress" << "(" << procname << ")"); +diff --git a/ui/gl/gl_bindings_autogen_egl.h b/ui/gl/gl_bindings_autogen_egl.h +index eb05c8d63c809c6d9303d5e2cfbeda55eaa8d6c8..4657eb78a8e8848027362bdef85c00e41d353161 100644 +--- a/ui/gl/gl_bindings_autogen_egl.h ++++ b/ui/gl/gl_bindings_autogen_egl.h +@@ -199,6 +199,10 @@ typedef EGLDisplay(GL_BINDING_CALL* eglGetPlatformDisplayProc)( + EGLenum platform, + void* native_display, + const EGLAttrib* attrib_list); ++typedef EGLDisplay(GL_BINDING_CALL* eglGetPlatformDisplayEXTProc)( ++ EGLenum platform, ++ void* native_display, ++ const EGLAttrib* attrib_list); + typedef __eglMustCastToProperFunctionPointerType( + GL_BINDING_CALL* eglGetProcAddressProc)(const char* procname); + typedef EGLBoolean(GL_BINDING_CALL* eglGetSyncAttribProc)(EGLDisplay dpy, +@@ -388,7 +392,9 @@ struct GL_EXPORT ClientExtensionsEGL { + bool b_EGL_EXT_device_query; + bool b_EGL_EXT_platform_base; + bool b_EGL_EXT_platform_device; ++ bool b_EGL_EXT_platform_wayland; + bool b_EGL_KHR_debug; ++ bool b_EGL_KHR_platform_wayland; + bool b_EGL_KHR_platform_gbm; + bool b_EGL_MESA_platform_surfaceless; + +@@ -520,6 +526,7 @@ struct ProcsEGL { + eglGetNativeClientBufferANDROIDProc eglGetNativeClientBufferANDROIDFn; + eglGetNextFrameIdANDROIDProc eglGetNextFrameIdANDROIDFn; + eglGetPlatformDisplayProc eglGetPlatformDisplayFn; ++ eglGetPlatformDisplayEXTProc eglGetPlatformDisplayEXTFn; + eglGetProcAddressProc eglGetProcAddressFn; + eglGetSyncAttribProc eglGetSyncAttribFn; + eglGetSyncAttribKHRProc eglGetSyncAttribKHRFn; +@@ -736,6 +743,10 @@ class GL_EXPORT EGLApi { + virtual EGLDisplay eglGetPlatformDisplayFn(EGLenum platform, + void* native_display, + const EGLAttrib* attrib_list) = 0; ++ virtual EGLDisplay eglGetPlatformDisplayEXTFn( ++ EGLenum platform, ++ void* native_display, ++ const EGLAttrib* attrib_list) = 0; + virtual __eglMustCastToProperFunctionPointerType eglGetProcAddressFn( + const char* procname) = 0; + virtual EGLBoolean eglGetSyncAttribFn(EGLDisplay dpy, +@@ -954,6 +965,8 @@ class GL_EXPORT EGLApi { + ::gl::g_current_egl_context->eglGetNextFrameIdANDROIDFn + #define eglGetPlatformDisplay \ + ::gl::g_current_egl_context->eglGetPlatformDisplayFn ++#define eglGetPlatformDisplayEXT \ ++ ::gl::g_current_egl_context->eglGetPlatformDisplayEXTFn + #define eglGetProcAddress ::gl::g_current_egl_context->eglGetProcAddressFn + #define eglGetSyncAttrib ::gl::g_current_egl_context->eglGetSyncAttribFn + #define eglGetSyncAttribKHR ::gl::g_current_egl_context->eglGetSyncAttribKHRFn +diff --git a/ui/gl/gl_display.cc b/ui/gl/gl_display.cc +index 06f8b6cab6e653d9b7d5c1e5434b641e0de5260e..ab1ad523ad1e3988abc9059517999acfbd92a6a6 100644 +--- a/ui/gl/gl_display.cc ++++ b/ui/gl/gl_display.cc +@@ -174,7 +174,12 @@ EGLDisplay GetDisplayFromType( + switch (display_type) { + case DEFAULT: + case SWIFT_SHADER: { +- if (native_display.GetPlatform() != 0) { ++ if (g_driver_egl.client_ext.b_EGL_EXT_platform_wayland || ++ g_driver_egl.client_ext.b_EGL_KHR_platform_wayland) { ++ return eglGetPlatformDisplayEXT(EGL_PLATFORM_WAYLAND_KHR, ++ reinterpret_cast(display), ++ nullptr); ++ } else if (native_display.GetPlatform() != 0) { + return eglGetPlatformDisplay(native_display.GetPlatform(), + reinterpret_cast(display), nullptr); + } +diff --git a/ui/gl/gl_mock_autogen_egl.h b/ui/gl/gl_mock_autogen_egl.h +index b097872aac9cea43ca3c6f86f0cb952251029ecf..1fd982bca670d0db00be34ec46a6e2124a840ced 100644 +--- a/ui/gl/gl_mock_autogen_egl.h ++++ b/ui/gl/gl_mock_autogen_egl.h +@@ -174,6 +174,10 @@ MOCK_METHOD3(GetPlatformDisplay, + EGLDisplay(EGLenum platform, + void* native_display, + const EGLAttrib* attrib_list)); ++MOCK_METHOD3(GetPlatformDisplayEXT, ++ EGLDisplay(EGLenum platform, ++ void* native_display, ++ const EGLAttrib* attrib_list)); + MOCK_METHOD1(GetProcAddress, + __eglMustCastToProperFunctionPointerType(const char* procname)); + MOCK_METHOD4(GetSyncAttrib, From 477cf34dbc1fa81b063579ba0ccfe36da2acdb53 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 30/68] feat: Allow MSE to be disabled with disable-blink-features: OS-18170 (#77) Allow MSE to be disabled with disable-blink-features: OS-18170 Cherry picked from: a151faa2589db76110046255e2152cf765d2bb78: feat: Allow MSE to be disabled with disable-blink-features: OS-18170 (#77) --- patches/chromium/.patches | 1 + ...allow_mse_mediasource_to_be_disabled.patch | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 patches/chromium/os-18170_allow_mse_mediasource_to_be_disabled.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 2d3dd0b6bd903..952a85d10d2aa 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -169,3 +169,4 @@ os-17400_brightsign_increase_file_open_handle_for_all_processes.patch os-17652_add_support_for_window_rotation.patch os-17765_disable_changing_mouse_cursor.patch gl_add_support_for_eglgetplatformdisplayext_extension.patch +os-18170_allow_mse_mediasource_to_be_disabled.patch diff --git a/patches/chromium/os-18170_allow_mse_mediasource_to_be_disabled.patch b/patches/chromium/os-18170_allow_mse_mediasource_to_be_disabled.patch new file mode 100644 index 0000000000000..46e4a60d73046 --- /dev/null +++ b/patches/chromium/os-18170_allow_mse_mediasource_to_be_disabled.patch @@ -0,0 +1,63 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Tue, 3 Dec 2024 15:49:57 +0000 +Subject: OS-18170: Allow MSE MediaSource to be disabled + +The BrightSign native media player doesn't support MSE. This patch adds back +changes to allow MediaSource to be disabled using the disable-blink-features +command line flag. + +diff --git a/third_party/blink/renderer/modules/mediasource/media_source.idl b/third_party/blink/renderer/modules/mediasource/media_source.idl +index 060baac1694e7bfb4a023e39401ccff0553a0451..1093043ee3e71e49bfb3313d9baf0effccc2006f 100644 +--- a/third_party/blink/renderer/modules/mediasource/media_source.idl ++++ b/third_party/blink/renderer/modules/mediasource/media_source.idl +@@ -37,7 +37,7 @@ enum EndOfStreamError { + + [ + ActiveScriptWrappable, +- Exposed=(Window, DedicatedWorker) ++ Exposed(Window MediaSourceStable, DedicatedWorker MediaSourceStable) + ] interface MediaSource : EventTarget { + [CallWith=ExecutionContext] constructor(); + // All the source buffers created by this object. +diff --git a/third_party/blink/renderer/modules/mediasource/source_buffer.idl b/third_party/blink/renderer/modules/mediasource/source_buffer.idl +index 21975a0bd60dae6360c87a064ab04c90701873e6..1ab9e499aaf4564de23639318ff2fcde795e043b 100644 +--- a/third_party/blink/renderer/modules/mediasource/source_buffer.idl ++++ b/third_party/blink/renderer/modules/mediasource/source_buffer.idl +@@ -41,7 +41,7 @@ enum AppendMode { + + [ + ActiveScriptWrappable, +- Exposed=(Window, DedicatedWorker) ++ Exposed(Window MediaSourceStable, DedicatedWorker MediaSourceStable) + ] interface SourceBuffer : EventTarget { + + // Gets or sets the AppendMode. +diff --git a/third_party/blink/renderer/modules/mediasource/source_buffer_list.idl b/third_party/blink/renderer/modules/mediasource/source_buffer_list.idl +index 75dcd69646123551f14fba569b2acbafd8cde1d0..c11505a0d6dcada81dd8ac6ab9e0e5ac1bc2d8f0 100644 +--- a/third_party/blink/renderer/modules/mediasource/source_buffer_list.idl ++++ b/third_party/blink/renderer/modules/mediasource/source_buffer_list.idl +@@ -29,7 +29,7 @@ + */ + + [ +- Exposed=(Window, DedicatedWorker) ++ Exposed(Window MediaSourceStable, DedicatedWorker MediaSourceStable) + ] interface SourceBufferList : EventTarget { + readonly attribute unsigned long length; + attribute EventHandler onaddsourcebuffer; +diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5 +index 9c207b3927535ceeecb6f73a36f51566500c9ebb..287aa88900e8e9054685b2339894ba40389d4eae 100644 +--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5 ++++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5 +@@ -3794,6 +3794,10 @@ + base_feature: "none", + origin_trial_feature_name: "MediaSourceExtensionsForWebCodecs", + }, ++ { ++ name: "MediaSourceStable", ++ status: "stable", ++ }, + { + name: "MediaStreamTrackTransfer", + status: "test", From 4c0872ee601aa4a2530aaf7c0f2123592400ba6f Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 31/68] fix: Do not convert tooltip position to screen coordinates for wayland OS-15658 (#78) Do not convert tooltip position to screen coordinates for wayland OS-15658 Cherry picked from: dfb34c887cf97bfae98b87032a9981508f716b55: fix: Do not convert tooltip position to screen coordinates for wayland OS-15658 (#78) --- patches/chromium/.patches | 1 + ...p_position_to_screen_coordinates_for.patch | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 patches/chromium/os-15658_do_not_convert_tooltip_position_to_screen_coordinates_for.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 952a85d10d2aa..defc58597bda4 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -170,3 +170,4 @@ os-17652_add_support_for_window_rotation.patch os-17765_disable_changing_mouse_cursor.patch gl_add_support_for_eglgetplatformdisplayext_extension.patch os-18170_allow_mse_mediasource_to_be_disabled.patch +os-15658_do_not_convert_tooltip_position_to_screen_coordinates_for.patch diff --git a/patches/chromium/os-15658_do_not_convert_tooltip_position_to_screen_coordinates_for.patch b/patches/chromium/os-15658_do_not_convert_tooltip_position_to_screen_coordinates_for.patch new file mode 100644 index 0000000000000..b18a0cebf3f27 --- /dev/null +++ b/patches/chromium/os-15658_do_not_convert_tooltip_position_to_screen_coordinates_for.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Wed, 4 Dec 2024 15:43:31 +0000 +Subject: OS-15658: do not convert tooltip position to screen coordinates for + wayland + +Weston wayland draws popup windows and tooltips in the parent window's coordinate space +and not in screen space. This patch ensures that the tooltip position is not converted to +screen coordinates when running on wayland so its position is correct. + +diff --git a/ui/views/corewm/tooltip_aura.cc b/ui/views/corewm/tooltip_aura.cc +index 4a593a63bfea007bb08f88d8de158576f2753911..78a75703d7fdf12d99a245f732e36bd136451f98 100644 +--- a/ui/views/corewm/tooltip_aura.cc ++++ b/ui/views/corewm/tooltip_aura.cc +@@ -250,10 +250,13 @@ void TooltipAura::Update(aura::Window* window, + + // Convert `position` to screen coordinates. + gfx::Point anchor_point = position; ++ // Weston wayland compositor doesn't want tooltip co-ordinates in screen space. ++#if !BUILDFLAG(SUPPORTS_OZONE_WAYLAND) + aura::client::ScreenPositionClient* screen_position_client = + aura::client::GetScreenPositionClient(window->GetRootWindow()); + CHECK(screen_position_client); + screen_position_client->ConvertPointToScreen(window, &anchor_point); ++#endif + + new_tooltip_view->SetMaxWidth(GetMaxWidth(anchor_point)); + new_tooltip_view->SetText(tooltip_text); From 04ac8a25c027dc79bf597b77261ade7ca260bf57 Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 32/68] feat: Add virtual-keyboard-visibility-changed event OS-18421 Added virtual-keyboard-visibility-changed event on BaseWindow(and BrowserWindow) which gets raised when a virtual keyboard is shown or hidden. fix(wayland_window): Always dispatch KeyEvent OS-18589 This change adds a patch which removes the event target check for KeyEvents. Window itself knows if it is focusable or not. We will rely on window doing the right thing. fix(wayland): Track keyboard focus OS-18548, OS-17052 We have disabled focus tracking in Weston. Weston no longer calls back to Electron for focus updates. Modified WaylandWindowManager and WaylandWindow to track the focused window and send key events to the focused window. fix(wayland_window): Focus on newly created widget OS-18791 Electron no longer receives focus widget update callback from Weston, and it tracks its own focus. Weston's default behaviour is to focus a widget when it is first created. Our focus tracking was differing from Weston's default behaviour and we have applications which depend on the default behaviour. Focus on widget in Initialize call to mimic Weston's behaviour. Weston doesn't have a concept of "non-focusable". Only difference is, we don't focus on "non-focusable" widgets. Cherry picked from: 402d3042f4f4cd8844271d8831015696427d92d0: feat: Add virtual-keyboard-visibility-changed event OS-18421 4786ac907b82e475b78e760a49b0c93ac1ce9348: fix(wayland_window): Always dispatch KeyEvent OS-18589 771a51ae4f2f97cb225535235c70fa8e1d336c2e: fix(wayland): Track keyboard focus OS-18548, OS-17052 806b3560b0bd7ea66bc0d846b934a71c9e5a5f93: fix(wayland_window): Focus on newly created widget OS-18791 --- docs/api/browser-window.md | 9 ++ patches/chromium/.patches | 1 + ...x_wayland_keyboard_and_focus_changes.patch | 150 ++++++++++++++++++ shell/browser/api/electron_api_base_window.cc | 4 + shell/browser/api/electron_api_base_window.h | 1 + shell/browser/native_window.cc | 5 + shell/browser/native_window.h | 2 + shell/browser/native_window_observer.h | 3 + shell/browser/native_window_views.cc | 19 +++ shell/browser/native_window_views.h | 14 +- spec/api-browser-window-spec.ts | 53 +++++++ 11 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 patches/chromium/fix_wayland_keyboard_and_focus_changes.patch diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index c335a00f07f4e..7aee2e99ce115 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -457,6 +457,15 @@ Calling `event.preventDefault()` will prevent the menu from being displayed. To convert `point` to DIP, use [`screen.screenToDipPoint(point)`](./screen.md#screenscreentodippointpoint-windows-linux). +#### Event: 'virtual-keyboard-visibility-changed' + +Returns: + +* `event` Event +* `show` boolean + +Emitted when Browser requests to show or hide a virtual keyboard + ### Static Methods The `BrowserWindow` class has the following static methods: diff --git a/patches/chromium/.patches b/patches/chromium/.patches index defc58597bda4..d9d06726c34e6 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -171,3 +171,4 @@ os-17765_disable_changing_mouse_cursor.patch gl_add_support_for_eglgetplatformdisplayext_extension.patch os-18170_allow_mse_mediasource_to_be_disabled.patch os-15658_do_not_convert_tooltip_position_to_screen_coordinates_for.patch +fix_wayland_keyboard_and_focus_changes.patch diff --git a/patches/chromium/fix_wayland_keyboard_and_focus_changes.patch b/patches/chromium/fix_wayland_keyboard_and_focus_changes.patch new file mode 100644 index 0000000000000..ba93c2e3adf50 --- /dev/null +++ b/patches/chromium/fix_wayland_keyboard_and_focus_changes.patch @@ -0,0 +1,150 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Caner Altinbasak +Date: Wed, 26 Feb 2025 10:30:13 +0000 +Subject: fix(wayland): keyboard and focus changes + +Track keyboard focus OS-18548 +We have disabled focus tracking in Weston. Weston no longer calls +back to Electron for focus updates. Modified WaylandWindowManager +and WaylandWindow to track the focused window and send key events +to the focused window. + +Always dispatch KeyEvent OS-18589 +In the case where topmost window is unfocusable, event target is empty. +This causes event to be swallowed here. Windows itself know if they are +focused or not and ignore the event if they are not focused. Pass key +events to all windows and let windows decide if they want to handle it +or not. + +Focus on newly created widget OS-18791 + +Electron no longer receives focus widget update callback +from Weston, and it tracks its own focus. Weston's default +behaviour is to focus a widget when it is first created. + +Our focus tracking was differing from Weston's default +behaviour and we have applications which depend on the +default behaviour. Focus on widget in Initialize call +to mimic Weston's behaviour. + +Weston doesn't have a concept of "non-focusable". Only +difference is, we don't focus on "non-focusable" widgets. + +diff --git a/ui/ozone/platform/wayland/host/wayland_window.cc b/ui/ozone/platform/wayland/host/wayland_window.cc +index 10b2f0275628694a9d6947b731b8029a8e61d2a2..b25154d2140883fbadb34ff2c7085f0d43548257 100644 +--- a/ui/ozone/platform/wayland/host/wayland_window.cc ++++ b/ui/ozone/platform/wayland/host/wayland_window.cc +@@ -696,6 +696,16 @@ bool WaylandWindow::CanDispatchEvent(const PlatformEvent& event) { + uint32_t WaylandWindow::DispatchEvent(const PlatformEvent& native_event) { + Event* event = static_cast(native_event); + ++ if (event->IsMouseEvent()) { ++ // If it is mouse button event, set window as keyboard grabber. ++ if (event->AsMouseEvent()->IsAnyButton() && activatable_) { ++ connection_->window_manager()->GrabKeyboardEvents(this); ++ } ++ } else if (event->IsTouchEvent() && activatable_) { ++ // If it is touch event, set window as keyboard grabber. ++ connection_->window_manager()->GrabKeyboardEvents(this); ++ } ++ + if (event->IsLocatedEvent()) { + auto* event_grabber = + connection_->window_manager()->located_events_grabber(); +@@ -761,6 +771,9 @@ bool WaylandWindow::CanAcceptEvent(const Event& event) { + DCHECK(event.target()); + } + #endif ++ if(event.IsKeyEvent()) { ++ return true; ++ } + return this == event.target(); + } + +@@ -971,6 +984,10 @@ bool WaylandWindow::Initialize(PlatformWindowInitProperties properties) { + state.window_scale * state.ui_scale) + .size(); + ++ if (activatable_) { ++ connection_->window_manager()->GrabKeyboardEvents(this); ++ } ++ + applied_state_ = state; + latched_state_ = state; + +diff --git a/ui/ozone/platform/wayland/host/wayland_window.h b/ui/ozone/platform/wayland/host/wayland_window.h +index e0e6bb8d525b07a955bbcc5827ff93105298a0d9..3866c5632aaa751e84d5432f331e1bc50a79122f 100644 +--- a/ui/ozone/platform/wayland/host/wayland_window.h ++++ b/ui/ozone/platform/wayland/host/wayland_window.h +@@ -723,6 +723,8 @@ class WaylandWindow : public PlatformWindow, + + base::OnceClosure drag_loop_quit_closure_; + ++ bool activatable_ = false; ++ + #if DCHECK_IS_ON() + bool disable_null_target_dcheck_for_test_ = false; + #endif +diff --git a/ui/ozone/platform/wayland/host/wayland_window_manager.cc b/ui/ozone/platform/wayland/host/wayland_window_manager.cc +index 50db3bd8e6060d2e7326b33e1fb51cabbdf6abec..1444abb8f80761e1047f7b24dd68126e0c99f552 100644 +--- a/ui/ozone/platform/wayland/host/wayland_window_manager.cc ++++ b/ui/ozone/platform/wayland/host/wayland_window_manager.cc +@@ -14,6 +14,7 @@ + #include "ui/ozone/platform/wayland/host/wayland_window.h" + #include "ui/ozone/platform/wayland/host/wayland_window_drag_controller.h" + #include "ui/ozone/platform/wayland/host/wayland_window_observer.h" ++#include "wayland_window_manager.h" + + namespace ui { + +@@ -85,6 +86,7 @@ void WaylandWindowManager::GrabLocatedEvents(WaylandWindow* window) { + located_events_grabber_ = window; + if (old_grabber) + old_grabber->OnWindowLostCapture(); ++ + } + + void WaylandWindowManager::UngrabLocatedEvents(WaylandWindow* window) { +@@ -94,6 +96,15 @@ void WaylandWindowManager::UngrabLocatedEvents(WaylandWindow* window) { + old_grabber->OnWindowLostCapture(); + } + ++void WaylandWindowManager::GrabKeyboardEvents(WaylandWindow* window) { ++ keyboard_events_grabber_ = window; ++} ++ ++void WaylandWindowManager::UngrabKeyboardEvents(WaylandWindow* window) { ++ if (keyboard_events_grabber_ == window) ++ keyboard_events_grabber_ = nullptr; ++} ++ + WaylandWindow* WaylandWindowManager::GetWindow( + gfx::AcceleratedWidget widget) const { + auto it = window_map_.find(widget); +diff --git a/ui/ozone/platform/wayland/host/wayland_window_manager.h b/ui/ozone/platform/wayland/host/wayland_window_manager.h +index 7735975074d667ab2b8ff13dc385bc0ec3432272..7f80aa78d5823742f946adaf95c474c26f44fd10 100644 +--- a/ui/ozone/platform/wayland/host/wayland_window_manager.h ++++ b/ui/ozone/platform/wayland/host/wayland_window_manager.h +@@ -56,6 +56,14 @@ class WaylandWindowManager { + return located_events_grabber_; + } + ++ void GrabKeyboardEvents(WaylandWindow* event_grabber); ++ void UngrabKeyboardEvents(WaylandWindow* event_grabber); ++ ++ // Returns the keyboard event grabber. ++ WaylandWindow* keyboard_events_grabber() const { ++ return keyboard_events_grabber_; ++ } ++ + // Returns a window found by |widget|. + WaylandWindow* GetWindow(gfx::AcceleratedWidget widget) const; + +@@ -162,6 +170,7 @@ class WaylandWindowManager { + std::unique_ptr subsurface_recycle_cache_; + + raw_ptr located_events_grabber_ = nullptr; ++ raw_ptr keyboard_events_grabber_ = nullptr; + + // Stores strictly monotonically increasing counter for allocating unique + // AccelerateWidgets. diff --git a/shell/browser/api/electron_api_base_window.cc b/shell/browser/api/electron_api_base_window.cc index 1e47aa66dd280..7565790d3165e 100644 --- a/shell/browser/api/electron_api_base_window.cc +++ b/shell/browser/api/electron_api_base_window.cc @@ -125,6 +125,10 @@ BaseWindow::BaseWindow(v8::Isolate* isolate, #endif } +void BaseWindow::OnVirtualKeyboardVisibilityChanged(bool show) { + Emit("virtual-keyboard-visibility-changed", show); +} + BaseWindow::BaseWindow(gin::Arguments* args, const gin_helper::Dictionary& options) : BaseWindow(args->isolate(), options) { diff --git a/shell/browser/api/electron_api_base_window.h b/shell/browser/api/electron_api_base_window.h index 8c4775c4b1ce7..fcd200e8fbdfe 100644 --- a/shell/browser/api/electron_api_base_window.h +++ b/shell/browser/api/electron_api_base_window.h @@ -100,6 +100,7 @@ class BaseWindow : public gin_helper::TrackableObject, #if BUILDFLAG(IS_WIN) void OnWindowMessage(UINT message, WPARAM w_param, LPARAM l_param) override; #endif + void OnVirtualKeyboardVisibilityChanged(bool is_visible) override; // Public APIs of NativeWindow. void SetContentView(gin_helper::Handle view); diff --git a/shell/browser/native_window.cc b/shell/browser/native_window.cc index 25d39035b43e5..f15c6556b2521 100644 --- a/shell/browser/native_window.cc +++ b/shell/browser/native_window.cc @@ -645,6 +645,11 @@ void NativeWindow::NotifyWindowMessage(UINT message, } #endif +void NativeWindow::NotifyVirtualKeyboardVisibilityChanged(bool is_visible) { + for (NativeWindowObserver& observer : observers_) + observer.OnVirtualKeyboardVisibilityChanged(is_visible); +} + int NativeWindow::NonClientHitTest(const gfx::Point& point) { #if !BUILDFLAG(IS_MAC) // We need to ensure we account for resizing borders on Windows and Linux. diff --git a/shell/browser/native_window.h b/shell/browser/native_window.h index d55d6c3fb05e5..a54de35758e5d 100644 --- a/shell/browser/native_window.h +++ b/shell/browser/native_window.h @@ -345,6 +345,8 @@ class NativeWindow : public views::WidgetDelegate { void NotifyWindowSystemContextMenu(int x, int y, bool* prevent_default); void NotifyLayoutWindowControlsOverlay(); + void NotifyVirtualKeyboardVisibilityChanged(bool is_visible); + #if BUILDFLAG(IS_WIN) void NotifyWindowMessage(UINT message, WPARAM w_param, LPARAM l_param); virtual void SetAccentColor( diff --git a/shell/browser/native_window_observer.h b/shell/browser/native_window_observer.h index 82bb2aed9c066..72520f5049983 100644 --- a/shell/browser/native_window_observer.h +++ b/shell/browser/native_window_observer.h @@ -100,6 +100,9 @@ class NativeWindowObserver : public base::CheckedObserver { virtual void OnNewWindowForTab() {} virtual void OnSystemContextMenu(int x, int y, bool* prevent_default) {} + // Called when virtual keyboard visibility changes + virtual void OnVirtualKeyboardVisibilityChanged(bool is_visible) {} + // Called when window message received #if BUILDFLAG(IS_WIN) virtual void OnWindowMessage(UINT message, WPARAM w_param, LPARAM l_param) {} diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index fe8d103d34812..80ebd472a757b 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -40,6 +40,7 @@ #include "shell/common/options_switches.h" #include "ui/aura/window_tree_host.h" #include "ui/base/hit_test.h" +#include "ui/base/ime/input_method_base.h" #include "ui/compositor/compositor.h" #include "ui/display/screen.h" #include "ui/gfx/geometry/insets.h" @@ -449,9 +450,17 @@ NativeWindowViews::NativeWindowViews(const int32_t base_window_id, if (!GetRestoredFrameBorderInsets().IsEmpty()) SetBounds(gfx::Rect(GetPosition(), size), false); #endif + + ui::InputMethod* input_method = widget()->GetInputMethod(); + if (input_method) + input_method->AddObserver(this); } NativeWindowViews::~NativeWindowViews() { + // Remove observers. + ui::InputMethod* input_method = widget()->GetInputMethod(); + if (input_method) + input_method->RemoveObserver(this); widget()->RemoveObserver(this); #if BUILDFLAG(IS_WIN) @@ -1925,6 +1934,16 @@ void NativeWindowViews::OnWidgetBoundsChanged(views::Widget* changed_widget, } } +void NativeWindowViews::OnTextInputStateChanged( + const ui::TextInputClient* client) { + bool should_show = false; + if (client) { + // Check if virtual keyboard needs to be shown or hidden + should_show = client->GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE; + } + NotifyVirtualKeyboardVisibilityChanged(should_show); +} + void NativeWindowViews::OnWidgetDestroying(views::Widget* widget) { if (aura::Window* window = GetNativeWindow()) window->RemovePreTargetHandler(this); diff --git a/shell/browser/native_window_views.h b/shell/browser/native_window_views.h index c7a90b3ea0ec4..19544587a31f0 100644 --- a/shell/browser/native_window_views.h +++ b/shell/browser/native_window_views.h @@ -15,6 +15,8 @@ #include "base/no_destructor.h" #include "shell/browser/ui/views/root_view.h" #include "third_party/abseil-cpp/absl/container/flat_hash_set.h" +#include "ui/base/ime/input_method_observer.h" +#include "ui/base/ime/text_input_client.h" #include "ui/base/ozone_buildflags.h" #include "ui/gfx/geometry/insets.h" #include "ui/views/controls/webview/unhandled_keyboard_event_handler.h" @@ -47,8 +49,9 @@ gfx::Rect ScreenToDIPRect(HWND hwnd, const gfx::Rect& pixel_bounds); #endif class NativeWindowViews : public NativeWindow, - private views::WidgetObserver, - private ui::EventHandler { + public views::WidgetObserver, + public ui::EventHandler, + public ui::InputMethodObserver { public: NativeWindowViews(int32_t base_window_id, const gin_helper::Dictionary& options, @@ -224,6 +227,13 @@ class NativeWindowViews : public NativeWindow, void OnWidgetDestroying(views::Widget* widget) override; void OnWidgetDestroyed(views::Widget* widget) override; + // ui::InputMethodObserver: + void OnFocus() override {} + void OnBlur() override {} + void OnCaretBoundsChanged(const ui::TextInputClient* client) override {} + void OnTextInputStateChanged(const ui::TextInputClient* client) override; + void OnInputMethodDestroyed(const ui::InputMethod* input_method) override {} + // views::WidgetDelegate: views::View* GetInitiallyFocusedView() override; bool CanMaximize() const override; diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts index 6b7fca9cbed21..ddb0b3d80a050 100755 --- a/spec/api-browser-window-spec.ts +++ b/spec/api-browser-window-spec.ts @@ -281,6 +281,59 @@ describe('BrowserWindow module', () => { }); }); + describe('virtual-keyboard-visibility-changed event', () => { + const triggeringInputTypes = [ + 'text', 'tel', 'search', 'email', 'password', 'url' + ]; + + const nonTriggeringInputTypes = [ + 'button', 'checkbox', 'color', 'date', 'datetime-local', 'file', 'hidden', + 'image', 'month', 'number', 'radio', 'range', 'reset', 'submit', 'time', 'week' + ]; + + let w: BrowserWindow; + + beforeEach(() => { + w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: false, contextIsolation: true } }); + }); + + afterEach(async () => { + await closeWindow(w); + w = null as unknown as BrowserWindow; + }); + + for (const type of triggeringInputTypes) { + it(`should emit virtual-keyboard-visibility-changed for when focused/blurred`, async () => { + const load = once(w, 'virtual-keyboard-visibility-changed'); + await w.loadURL(`data:text/html,`); + expect((await load)[1]).to.be.false(); + + let shown = once(w, 'virtual-keyboard-visibility-changed'); + await w.webContents.executeJavaScript('document.getElementById("test").focus()', true); + expect((await shown)[1]).to.be.true(); + + shown = once(w, 'virtual-keyboard-visibility-changed'); + await w.webContents.executeJavaScript('document.getElementById("test").blur()', true); + expect((await shown)[1]).to.be.false(); + }); + } + + for (const type of nonTriggeringInputTypes) { + it(`should not emit virtual-keyboard-visibility-changed for when focused/blurred`, async () => { + const eventSpy = once(w, 'virtual-keyboard-visibility-changed'); + await w.loadURL(`data:text/html,`); + + await w.webContents.executeJavaScript('document.getElementById("test").focus()', true); + + const didTimeout = await Promise.race([ + eventSpy.then((args) => !args[1]), // show flag should be false, if triggered. + new Promise((resolve) => global.setTimeout(() => resolve(true), 500)) // Timeout after 500ms (expected) + ]); + expect(didTimeout).to.be.true(); + }); + } + }); + describe('window.close()', () => { let w: BrowserWindow; beforeEach(() => { From 6eadf6007dae5bcb73a1d05c9b0338a1657c4d4d Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 33/68] feat: Short-circuit same origin check for frame load if websecurity is disabled OS-17051 (#88) AncestorThrottle: Short-circuit same origin check for frame load if websecurity is disabled Cherry picked from: 4fe26ffa27693161dfc39b5fbd70f6b05c4b3c25: feat: Short-circuit same origin check for frame load if websecurity is disabled OS-17051 (#88) --- patches/chromium/.patches | 1 + ..._same_origin_check_for_frame_load_if.patch | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 patches/chromium/os-17051_short-circuit_same_origin_check_for_frame_load_if.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index d9d06726c34e6..942167ccbcaca 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -172,3 +172,4 @@ gl_add_support_for_eglgetplatformdisplayext_extension.patch os-18170_allow_mse_mediasource_to_be_disabled.patch os-15658_do_not_convert_tooltip_position_to_screen_coordinates_for.patch fix_wayland_keyboard_and_focus_changes.patch +os-17051_short-circuit_same_origin_check_for_frame_load_if.patch diff --git a/patches/chromium/os-17051_short-circuit_same_origin_check_for_frame_load_if.patch b/patches/chromium/os-17051_short-circuit_same_origin_check_for_frame_load_if.patch new file mode 100644 index 0000000000000..b03994a2d2de9 --- /dev/null +++ b/patches/chromium/os-17051_short-circuit_same_origin_check_for_frame_load_if.patch @@ -0,0 +1,35 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> +Date: Thu, 27 Feb 2025 14:29:55 +0000 +Subject: OS-17051: Short-circuit same origin check for frame load if + websecurity is disabled + +Disable the same origin check for frame loads if web security is disabled. +This is to allow the loading of iframes from different origins when web +security is disabled. + +This is a cherry pick of https://cam-gerrit.brightsign.info/c/qtwebengine-chromium/+/6236 + +diff --git a/content/browser/renderer_host/ancestor_throttle.cc b/content/browser/renderer_host/ancestor_throttle.cc +index d007b254c4689966e39ebc5ab04c0b148218eccb..8a4cafe6e47fdf8a5b59d00df40898d0cd588ff4 100644 +--- a/content/browser/renderer_host/ancestor_throttle.cc ++++ b/content/browser/renderer_host/ancestor_throttle.cc +@@ -25,6 +25,7 @@ + #include "content/public/browser/navigation_handle.h" + #include "content/public/browser/navigation_throttle.h" + #include "content/public/browser/storage_partition.h" ++#include "content/public/browser/web_contents.h" + #include "content/public/common/content_client.h" + #include "net/http/http_response_headers.h" + #include "services/network/public/cpp/content_security_policy/content_security_policy.h" +@@ -284,7 +285,9 @@ AncestorThrottle::CheckResult AncestorThrottle::EvaluateXFrameOptions( + url::Origin current_origin = + url::Origin::Create(navigation_handle()->GetURL()); + while (parent) { +- if (!parent->GetLastCommittedOrigin().IsSameOriginWith( ++ if ((!navigation_handle()->GetWebContents() ++ || navigation_handle()->GetWebContents()->GetOrCreateWebPreferences().web_security_enabled) ++ && !parent->GetLastCommittedOrigin().IsSameOriginWith( + current_origin)) { + if (logging == LoggingDisposition::LOG_TO_CONSOLE) + ConsoleErrorXFrameOptions(disposition); From ba37178074f4143e54f9f2e2b953309671a949ac Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 34/68] feat: Add blob data fetching OS-18647 (#94) * Add blob data fetching OS-18647 * Rename url to blob url and remove BS_DEBUG * Fix test case feat: Add getMediaResource to fetch cookie and auth params OS-16991 (#95) Add getMediaResource to fetch cookie and auth params Cherry picked from: 7629cc75c1bbc73d36c4919ae431681af158e3d9: feat: Add blob data fetching OS-18647 (#94) 661ace1492a3fa2886b4665cfe088a5ab0f3521e: feat: Add getMediaResource to fetch cookie and auth params OS-16991 (#95) fix: Ensure callbacks are called on UI thread in error cases OS-20127 (#109) BlobData: Ensure callbacks are called on UI thread in error cases OS-20127 --- docs/api/web-contents.md | 20 ++ filenames.gni | 2 + patches/chromium/.patches | 1 + ...pserverbasicauthcredentials_os-18647.patch | 76 +++++ .../browser/api/electron_api_web_contents.cc | 117 +++++++ shell/browser/api/electron_api_web_contents.h | 23 ++ .../media/media_resource_getter_impl.cc | 306 ++++++++++++++++++ .../media/media_resource_getter_impl.h | 89 +++++ spec/api-session-spec.ts | 2 + spec/api-web-contents-spec.ts | 77 +++++ 10 files changed, 713 insertions(+) create mode 100644 patches/chromium/brightsign_add_back_lookupserverbasicauthcredentials_os-18647.patch create mode 100644 shell/browser/media/media_resource_getter_impl.cc create mode 100644 shell/browser/media/media_resource_getter_impl.h diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 37b35d57dd726..203e900ea943e 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1131,6 +1131,26 @@ const win = new BrowserWindow() win.loadFile('src/index.html') ``` +#### `contents.getBlobData(blobUrl, location, size)` + +* `blobUrl` string - Valid Blob Url. +* `location` Integer - The offset of the range. +* `size` Integer - The length of the range. + +Returns `Promise` - Resolves with the Blob data. + +#### `contents.getMediaResource(url, urlForCookies, origin)` + +* `url` string - Valid Url. +* `urlForCookies` string - The URL to use for cookies. +* `origin` string - The origin of the request. + +Returns `Promise` - Resolve with an object containing the following: + +* `username` string (optional) +* `password` string (optional) +* `cookie` string (optional) + #### `contents.downloadURL(url[, options])` * `url` string diff --git a/filenames.gni b/filenames.gni index 6ddb1a8140174..903b4176498c6 100644 --- a/filenames.gni +++ b/filenames.gni @@ -436,6 +436,8 @@ filenames = { "shell/browser/metrics/electron_metrics_log_uploader.h", "shell/browser/metrics/electron_metrics_service_client.cc", "shell/browser/metrics/electron_metrics_service_client.h", + "shell/browser/media/media_resource_getter_impl.cc", + "shell/browser/media/media_resource_getter_impl.h", "shell/browser/microtasks_runner.cc", "shell/browser/microtasks_runner.h", "shell/browser/native_window.cc", diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 942167ccbcaca..36aec57a3c943 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -173,3 +173,4 @@ os-18170_allow_mse_mediasource_to_be_disabled.patch os-15658_do_not_convert_tooltip_position_to_screen_coordinates_for.patch fix_wayland_keyboard_and_focus_changes.patch os-17051_short-circuit_same_origin_check_for_frame_load_if.patch +brightsign_add_back_lookupserverbasicauthcredentials_os-18647.patch diff --git a/patches/chromium/brightsign_add_back_lookupserverbasicauthcredentials_os-18647.patch b/patches/chromium/brightsign_add_back_lookupserverbasicauthcredentials_os-18647.patch new file mode 100644 index 0000000000000..b44ee24551ae4 --- /dev/null +++ b/patches/chromium/brightsign_add_back_lookupserverbasicauthcredentials_os-18647.patch @@ -0,0 +1,76 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir +Date: Thu, 30 Apr 2026 11:05:57 +0100 +Subject: Brightsign add back LookupServerBasicAuthCredentials OS-18647 + +Add back network::mojom::NetworkContext::LookupServerBasicAuthCredentials +as we use this for MediaResourceGetterImpl. This patch partially +reverses: +https://chromium-review.googlesource.com/c/chromium/src/+/6881109 + +diff --git a/services/network/network_context.cc b/services/network/network_context.cc +index fd0ef3275b205e94e3fa1914d4e64023cdf702dd..159225eec24ba75a7ea0fb72bd622d2836375a05 100644 +--- a/services/network/network_context.cc ++++ b/services/network/network_context.cc +@@ -2778,6 +2778,24 @@ void NetworkContext::SetCorsNonWildcardRequestHeadersSupport(bool value) { + cors::NonWildcardRequestHeadersSupport(value); + } + ++void NetworkContext::LookupServerBasicAuthCredentials( ++ const GURL& url, ++ const net::NetworkAnonymizationKey& network_anonymization_key, ++ LookupServerBasicAuthCredentialsCallback callback) { ++ net::HttpAuthCache* http_auth_cache = ++ url_request_context_->http_transaction_factory() ++ ->GetSession() ++ ->http_auth_cache(); ++ net::HttpAuthCache::Entry* entry = http_auth_cache->LookupByPath( ++ url::SchemeHostPort(url), net::HttpAuth::AUTH_SERVER, ++ network_anonymization_key, std::string(url.path())); ++ if (entry && entry->scheme() == net::HttpAuth::AUTH_SCHEME_BASIC) { ++ std::move(callback).Run(entry->credentials()); ++ } else { ++ std::move(callback).Run(std::nullopt); ++ } ++} ++ + #if BUILDFLAG(IS_CHROMEOS) + void NetworkContext::LookupProxyAuthCredentials( + const net::ProxyServer& proxy_server, +diff --git a/services/network/network_context.h b/services/network/network_context.h +index dad6a445ee3f6a0eb3d55d29fae643bde5bf706d..7040ef9f5a70d27ee329ff4f0ee715479bd2e650 100644 +--- a/services/network/network_context.h ++++ b/services/network/network_context.h +@@ -536,7 +536,12 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) NetworkContext + AddAuthCacheEntryCallback callback) override; + void SetCorsNonWildcardRequestHeadersSupport(bool value) override; + void SetDohFallbackUpgradeAllowed(bool allowed) override; +- ++ // TODO(mmenke): Rename this method and update Mojo docs to make it clear this ++ // doesn't give proxy auth credentials. ++ void LookupServerBasicAuthCredentials( ++ const GURL& url, ++ const net::NetworkAnonymizationKey& network_anonymization_key, ++ LookupServerBasicAuthCredentialsCallback callback) override; + #if BUILDFLAG(IS_CHROMEOS) + void LookupProxyAuthCredentials( + const net::ProxyServer& proxy_server, +diff --git a/services/network/public/mojom/network_context.mojom b/services/network/public/mojom/network_context.mojom +index 7493bd8dd37b36a3e7a96b1373619fee0b6a9d8e..d7c45d2fedbb5c96baa52e67b47467bfd467485c 100644 +--- a/services/network/public/mojom/network_context.mojom ++++ b/services/network/public/mojom/network_context.mojom +@@ -1689,6 +1689,14 @@ interface NetworkContext { + // supported in this network context. + SetCorsNonWildcardRequestHeadersSupport(bool value); + ++ // Looks up credentials in the HttpAuthCache using the origin and path from ++ // |url|. Only supports basic auth scheme. Only looks up server (not proxy) ++ // auth credentials, and only those that are usable in the scope of ++ // |network_anonymization_key|. ++ LookupServerBasicAuthCredentials( ++ url.mojom.Url url, NetworkAnonymizationKey network_anonymization_key) ++ => (AuthCredentials? credentials); ++ + // Looks up the proxy authentication credentials associated with + // |proxy_server|, |auth_scheme| and |realm| in the HttpAuthCache. + // |auth_scheme| is the authentication scheme of the challenge and it's diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index ec746904f6f7a..9593423a06b81 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -1202,6 +1202,121 @@ void WebContents::Close(std::optional options) { } } +v8::Local WebContents::GetBlobData(v8::Isolate* isolate, + const std::string& blob_url, + uint64_t location, + uint64_t size) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + gin_helper::Promise> promise(isolate); + v8::Local handle = promise.GetHandle(); + + if (!media_resource_getter_.get()) { + auto* rfh = web_contents()->GetPrimaryMainFrame(); + if (!rfh) { + promise.Resolve(v8::Null(isolate)); + return handle; + } + int32_t frame_process_id = rfh->GetProcess()->GetID().GetUnsafeValue(); + int frame_routing_id = rfh->GetRoutingID(); + content::BrowserContext* context = GetBrowserContext(); + media_resource_getter_ = std::make_unique( + context, frame_process_id, frame_routing_id); + } + + content::MediaResourceGetterImpl::GetMediaDataCB callback = + base::BindRepeating(&WebContents::OnGetBlobData, GetWeakPtr(), + base::Passed(&promise)); + media_resource_getter_->ReadMediaData(blob_url, location, size, + std::move(callback)); + return handle; +} + +void WebContents::OnGetBlobData( + gin_helper::Promise> promise, + scoped_refptr io_buf) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + v8::Isolate* isolate = promise.isolate(); + gin_helper::Locker locker(isolate); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope( + v8::Local::New(isolate, promise.GetContext())); + + if (io_buf) { + v8::Local buffer = + node::Buffer::Copy(isolate, + reinterpret_cast(io_buf->data()), + io_buf->size()) + .ToLocalChecked(); + promise.Resolve(buffer); + } else { + promise.Resolve(v8::Null(isolate)); + } +} + +v8::Local WebContents::GetMediaResource( + v8::Isolate* isolate, + const std::string& url, + const std::string& url_for_cookies, + const std::string& origin) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + gin_helper::Promise promise(isolate); + v8::Local handle = promise.GetHandle(); + + if (!media_resource_getter_.get()) { + auto* rfh = web_contents()->GetPrimaryMainFrame(); + if (!rfh) { + promise.Resolve(gin_helper::Dictionary::CreateEmpty(isolate)); + return handle; + } + int32_t frame_process_id = rfh->GetProcess()->GetID().GetUnsafeValue(); + int frame_routing_id = rfh->GetRoutingID(); + content::BrowserContext* context = GetBrowserContext(); + media_resource_getter_ = std::make_unique( + context, frame_process_id, frame_routing_id); + } + + content::MediaResourceGetterImpl::GetCookieCB callback = base::BindOnce( + &WebContents::OnGetCookieData, GetWeakPtr(), std::move(promise), url); + media_resource_getter_->GetCookies( + GURL(url), net::SiteForCookies::FromUrl(GURL(url_for_cookies)), + url::Origin::Create(GURL(origin)), + net::StorageAccessApiStatus::kAccessViaAPI, std::move(callback)); + return handle; +} + +void WebContents::OnGetCookieData( + gin_helper::Promise promise, + const std::string& url, + const std::string& cookie) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + content::MediaResourceGetterImpl::GetAuthCredentialsCB callback = + base::BindOnce(&WebContents::OnGetAuthData, GetWeakPtr(), + std::move(promise), cookie); + media_resource_getter_->GetAuthCredentials(GURL(url), std::move(callback)); +} + +void WebContents::OnGetAuthData( + gin_helper::Promise promise, + const std::string& cookie, + const std::u16string& username, + const std::u16string& password) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + v8::Isolate* isolate = promise.isolate(); + gin_helper::Locker locker(isolate); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope( + v8::Local::New(isolate, promise.GetContext())); + + auto dict = gin_helper::Dictionary::CreateEmpty(isolate); + dict.Set("username", username); + dict.Set("password", password); + dict.Set("cookie", cookie); + promise.Resolve(dict); +} + void WebContents::OnDidAddMessageToConsole( content::RenderFrameHost* source_frame, blink::mojom::ConsoleMessageLevel level, @@ -4604,6 +4719,8 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate, // gin::ObjectTemplateBuilder here to handle the fact that WebContents is // destroyable. gin_helper::ObjectTemplateBuilder(isolate, templ) + .SetMethod("getBlobData", &WebContents::GetBlobData) + .SetMethod("getMediaResource", &WebContents::GetMediaResource) .SetMethod("destroy", &WebContents::Destroy) .SetMethod("close", &WebContents::Close) .SetMethod("getBackgroundThrottling", diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index b99362c7d3d44..401892ca05e35 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -39,6 +39,7 @@ #include "shell/browser/background_throttling_source.h" #include "shell/browser/event_emitter_mixin.h" #include "shell/browser/extended_web_contents_observer.h" +#include "shell/browser/media/media_resource_getter_impl.h" #include "shell/browser/osr/osr_paint_event.h" #include "shell/browser/preload_script.h" #include "shell/browser/ui/inspectable_web_contents_delegate.h" @@ -185,6 +186,14 @@ class WebContents final : public ExclusiveAccessContext, // gin_helper::CleanedUpAtExit void WillBeDestroyed() override; + v8::Local GetBlobData(v8::Isolate*, + const std::string& blob_url, + uint64_t location, + uint64_t size); + v8::Local GetMediaResource(v8::Isolate*, + const std::string& url, + const std::string& url_for_cookies, + const std::string& origin); void Destroy(); void Close(std::optional options); base::WeakPtr GetWeakPtr() { return weak_factory_.GetWeakPtr(); } @@ -496,6 +505,18 @@ class WebContents final : public ExclusiveAccessContext, extensions::mojom::ViewType view_type); #endif + void OnGetBlobData(gin_helper::Promise> promise, + scoped_refptr io_buf); + + void OnGetCookieData(gin_helper::Promise promise, + const std::string& url, + const std::string& cookie); + + void OnGetAuthData(gin_helper::Promise promise, + const std::string& cookie, + const std::u16string& username, + const std::u16string& password); + // content::WebContentsDelegate: bool IsWebContentsCreationOverridden( content::RenderFrameHost* opener, @@ -890,6 +911,8 @@ class WebContents final : public ExclusiveAccessContext, std::unique_ptr draggable_region_; + std::unique_ptr media_resource_getter_; + base::WeakPtrFactory weak_factory_{this}; }; diff --git a/shell/browser/media/media_resource_getter_impl.cc b/shell/browser/media/media_resource_getter_impl.cc new file mode 100644 index 0000000000000..9892eb30a861e --- /dev/null +++ b/shell/browser/media/media_resource_getter_impl.cc @@ -0,0 +1,306 @@ + +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "shell/browser/media/media_resource_getter_impl.h" + +#include "base/functional/bind.h" +#include "base/path_service.h" +#include "base/task/single_thread_task_runner.h" +#include "content/browser/renderer_host/render_frame_host_impl.h" +#include "content/browser/storage_partition_impl.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/content_browser_client.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/common/content_client.h" +#include "content/public/common/url_constants.h" +#include "ipc/ipc_message.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "net/base/auth.h" +#include "net/base/isolation_info.h" +#include "net/base/network_anonymization_key.h" +#include "net/cookies/cookie_setting_override.h" +#include "net/http/http_auth.h" +#include "services/network/public/mojom/network_context.mojom.h" +#include "services/network/public/mojom/restricted_cookie_manager.mojom.h" +#include "storage/browser/blob/blob_data_item.h" +#include "storage/browser/blob/blob_data_snapshot.h" +#include "storage/browser/blob/blob_reader.h" +#include "storage/browser/blob/blob_storage_context.h" +#include "storage/browser/blob/blob_url_registry.h" +#include "third_party/blink/public/common/storage_key/storage_key.h" +#include "url/gurl.h" +#include "url/origin.h" + +using storage::BlobReader; + +namespace content { + +namespace { + +// Returns the cookie manager for the `browser_context` at the client end of the +// mojo pipe. This will be restricted to the origin of `url`, and will apply +// policies from user and ContentBrowserClient to cookie operations. +mojo::PendingRemote +GetRestrictedCookieManagerForContext( + BrowserContext* browser_context, + const GURL& url, + const net::SiteForCookies& site_for_cookies, + const url::Origin& top_frame_origin, + RenderFrameHostImpl* render_frame_host) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + url::Origin request_origin = url::Origin::Create(url); + StoragePartition* storage_partition = + browser_context->GetDefaultStoragePartition(); + + // `request_origin` cannot be used to create `isolation_info` since it + // represents the media resource, not the frame origin. Here we use the + // `top_frame_origin` as the frame origin to ensure the consistency check + // passes when creating `isolation_info`. This is ok because + // `isolation_info.frame_origin` is unused in RestrictedCookieManager. + DCHECK(site_for_cookies.IsNull() || + site_for_cookies.IsFirstParty(top_frame_origin.GetURL())); + net::IsolationInfo isolation_info = net::IsolationInfo::Create( + net::IsolationInfo::RequestType::kOther, top_frame_origin, + top_frame_origin, site_for_cookies, absl::nullopt); + + net::CookieSettingOverrides devtools_cookie_setting_overrides; + + mojo::PendingRemote pipe; + static_cast(storage_partition) + ->CreateRestrictedCookieManager( + network::mojom::RestrictedCookieManagerRole::NETWORK, request_origin, + std::move(isolation_info), + /* is_service_worker = */ false, + render_frame_host + ? render_frame_host->GetProcess()->GetID().GetUnsafeValue() + : -1, + render_frame_host ? render_frame_host->GetRoutingID() + : IPC::mojom::kRoutingIdNone, + render_frame_host ? render_frame_host->GetCookieSettingOverrides() + : net::CookieSettingOverrides(), + /*devtools_cookie_setting_overrides=*/ + devtools_cookie_setting_overrides, + pipe.InitWithNewPipeAndPassReceiver(), + render_frame_host ? render_frame_host->CreateCookieAccessObserver( + CookieAccessDetails::Source::kNonNavigation) + : mojo::NullRemote()); + return pipe; +} + +void ReturnResultOnUIThread( + base::OnceCallback callback, + const std::string& result) { + GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), result)); +} + +void ReturnResultOnUIThreadAndClosePipe( + mojo::Remote pipe, + base::OnceCallback callback, + uint64_t version, + base::ReadOnlySharedMemoryRegion shared_memory_region, + const std::string& result) { + // Clients of GetCookiesString() are free to use |shared_memory_region| and + // |result| to avoid IPCs when possible. This class has not proven to be a + // high enough source of IPC traffic to warrant wiring this up. Using them + // is completely optional so they are simply dropped here. + GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), result)); +} + +} // namespace + +MediaResourceGetterImpl::MediaResourceGetterImpl( + BrowserContext* browser_context, + int render_process_id, + int render_frame_id) + : browser_context_(browser_context), + render_process_id_(render_process_id), + render_frame_id_(render_frame_id) {} + +MediaResourceGetterImpl::~MediaResourceGetterImpl() {} + +void MediaResourceGetterImpl::GetAuthCredentials( + const GURL& url, + GetAuthCredentialsCB callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + // Non-standard URLs, such as data, will not be found in HTTP auth cache + // anyway, because they have no valid origin, so don't waste the time. + if (!url.IsStandard()) { + GetAuthCredentialsCallback(std::move(callback), absl::nullopt); + return; + } + + RenderFrameHostImpl* render_frame_host = + RenderFrameHostImpl::FromID(render_process_id_, render_frame_id_); + // Can't get a NetworkAnonymizationKey to get credentials if the + // RenderFrameHost has already been destroyed. + if (!render_frame_host) { + GetAuthCredentialsCallback(std::move(callback), absl::nullopt); + return; + } + + browser_context_->GetDefaultStoragePartition() + ->GetNetworkContext() + ->LookupServerBasicAuthCredentials( + url, + render_frame_host->GetIsolationInfoForSubresources() + .network_anonymization_key(), + base::BindOnce(&MediaResourceGetterImpl::GetAuthCredentialsCallback, + weak_factory_.GetWeakPtr(), std::move(callback))); +} + +void MediaResourceGetterImpl::GetCookies( + const GURL& url, + const net::SiteForCookies& site_for_cookies, + const url::Origin& top_frame_origin, + net::StorageAccessApiStatus storage_access_api_status, + GetCookieCB callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + ChildProcessSecurityPolicyImpl* policy = + ChildProcessSecurityPolicyImpl::GetInstance(); + if (!policy->CanAccessDataForOrigin(render_process_id_, + url::Origin::Create(url))) { + // Running the callback asynchronously on the caller thread to avoid + // reentrancy issues. + ReturnResultOnUIThread(std::move(callback), std::string()); + return; + } + + mojo::Remote cookie_manager( + GetRestrictedCookieManagerForContext( + browser_context_, url, site_for_cookies, top_frame_origin, + RenderFrameHostImpl::FromID(render_process_id_, render_frame_id_))); + network::mojom::RestrictedCookieManager* cookie_manager_ptr = + cookie_manager.get(); + cookie_manager_ptr->GetCookiesString( + url, site_for_cookies, top_frame_origin, storage_access_api_status, + /*get_version_shared_memory=*/false, /*is_ad_tagged=*/false, + /*apply_devtools_overrides=*/true, + /*force_disable_third_party_cookies=*/false, + base::BindOnce(&ReturnResultOnUIThreadAndClosePipe, + std::move(cookie_manager), std::move(callback))); +} + +void MediaResourceGetterImpl::GetAuthCredentialsCallback( + GetAuthCredentialsCB callback, + const std::optional& credentials) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (credentials) + std::move(callback).Run(credentials->username(), credentials->password()); + else + std::move(callback).Run(std::u16string(), std::u16string()); +} + +/*************************************** + * BLOB READER * + ***************************************/ + +void OnReadCompleteOnIO(MediaResourceGetterImpl::GetMediaDataCB callback, + scoped_refptr io_buf, + std::shared_ptr reader, + int bytes_read) { + GetUIThreadTaskRunner({})->PostTask(FROM_HERE, + base::BindRepeating(callback, io_buf)); +} + +void CalculateSizeComplete(MediaResourceGetterImpl::GetMediaDataCB callback, + uint64_t location, + uint64_t size, + std::shared_ptr reader, + int value) { + DVLOG(1) << "TotalSize is " << reader->total_size(); + DVLOG(1) << "IsInMemory: " << (reader->IsInMemory() ? "true" : "false"); + if (location > reader->total_size()) { + // Use OnReadCompleteOnIO to ensure callback is called on the UI thread. + OnReadCompleteOnIO(callback, nullptr, reader, /*bytes_read=*/0); + return; + } + + if (location + size > reader->total_size()) { + size = reader->total_size() - location; + } + + if (reader->SetReadRange(location, size) != + storage::BlobReader::Status::DONE) { + // Use OnReadCompleteOnIO to ensure callback is called on the UI thread. + OnReadCompleteOnIO(callback, nullptr, reader, /*bytes_read=*/0); + } else { + auto buffer = base::MakeRefCounted(size); + int bytes_read; + BlobReader::Status status = reader->Read( + buffer.get(), size, &bytes_read, + base::BindOnce(&OnReadCompleteOnIO, callback, buffer, reader)); + if (status == BlobReader::Status::IO_PENDING) + return; + if (status == BlobReader::Status::NET_ERROR) + bytes_read = reader->net_error(); + OnReadCompleteOnIO(callback, buffer, reader, bytes_read); + } +} + +void ReceivedBlobDataHandleOnIO( + uint64_t location, + uint64_t size, + MediaResourceGetterImpl::GetMediaDataCB callback, + std::unique_ptr handle) { + std::string result; + if (handle) { + std::shared_ptr reader = handle->CreateReader(); + auto status = reader->CalculateSize(base::BindOnce( + &CalculateSizeComplete, callback, location, size, reader)); + if (status == BlobReader::Status::IO_PENDING) + return; + if (status == storage::BlobReader::Status::DONE) + CalculateSizeComplete(callback, location, size, reader, + reader->total_size()); + } +} + +void RequestBlobDataHandleOnIO( + uint64_t location, + uint64_t size, + BrowserContext::BlobContextGetter blob_context_getter, + mojo::PendingRemote blob_ptr, + MediaResourceGetterImpl::GetMediaDataCB callback) { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + base::OnceCallback)> cb = + base::BindOnce(&ReceivedBlobDataHandleOnIO, location, size, callback); + auto blob_context = blob_context_getter.Run(); + if (blob_context) + blob_context->GetBlobDataFromBlobRemote(std::move(blob_ptr), std::move(cb)); + else + // Use OnReadCompleteOnIO to ensure callback is called on the UI thread. + OnReadCompleteOnIO(callback, nullptr, nullptr, /*bytes_read=*/0); +} + +void MediaResourceGetterImpl::ReadMediaData(const std::string& blob_url, + uint64_t location, + uint64_t size, + GetMediaDataCB callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + DCHECK(callback); + StoragePartitionImpl* storage_partititon = static_cast( + browser_context_->GetStoragePartitionForUrl(GURL(blob_url))); + auto blob_ptr = + storage_partititon->GetBlobUrlRegistry()->GetBlobFromUrl(GURL(blob_url)); + + if (blob_ptr) { + BrowserContext::BlobContextGetter blob_context_getter = + browser_context_->GetBlobStorageContext(); + content::GetIOThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(&RequestBlobDataHandleOnIO, location, size, + std::move(blob_context_getter), + std::move(blob_ptr), callback)); + } else { + callback.Run(nullptr); + } +} + +} // namespace content diff --git a/shell/browser/media/media_resource_getter_impl.h b/shell/browser/media/media_resource_getter_impl.h new file mode 100644 index 0000000000000..d9d1167a7467d --- /dev/null +++ b/shell/browser/media/media_resource_getter_impl.h @@ -0,0 +1,89 @@ + +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_MEDIA_RESOURCE_GETTER_IMPL_H_ +#define ELECTRON_SHELL_BROWSER_MEDIA_RESOURCE_GETTER_IMPL_H_ + +#include "base/functional/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/synchronization/waitable_event.h" +#include "net/base/auth.h" +#include "net/base/io_buffer.h" +#include "net/cookies/canonical_cookie.h" +#include "net/cookies/site_for_cookies.h" +#include "net/storage_access_api/status.h" + +namespace content { + +class BrowserContext; +class ResourceContext; + +// This class implements media::MediaResourceGetter to retrieve resources +// asynchronously on the UI thread. +class MediaResourceGetterImpl { + public: + typedef base::RepeatingCallback)> + GetMediaDataCB; + + // Callback to get the cookies. Args: cookies string. + typedef base::OnceCallback GetCookieCB; + + // Callback to get the auth credentials. Args: username and password. + typedef base::OnceCallback + GetAuthCredentialsCB; + + // Callback to get the media metadata. Args: duration, width, height, and + // whether the information is retrieved successfully. + typedef base::OnceCallback + ExtractMediaMetadataCB; + + // Construct a MediaResourceGetterImpl object. `browser_context` and + // `render_process_id` are passed to retrieve the CookieStore. + MediaResourceGetterImpl(BrowserContext* browser_context, + int render_process_id, + int render_frame_id); + + MediaResourceGetterImpl(const MediaResourceGetterImpl&) = delete; + MediaResourceGetterImpl& operator=(const MediaResourceGetterImpl&) = delete; + + ~MediaResourceGetterImpl(); + + // media::MediaResourceGetter implementation. + // Must be called on the UI thread. + void GetAuthCredentials(const GURL& url, GetAuthCredentialsCB callback); + void GetCookies(const GURL& url, + const net::SiteForCookies& site_for_cookies, + const url::Origin& top_frame_origin, + net::StorageAccessApiStatus storage_access_api_status, + GetCookieCB callback); + + void ReadMediaData(const std::string& blob_url, + uint64_t location, + uint64_t size, + GetMediaDataCB callback); + + private: + // Called when GetAuthCredentials() finishes. + void GetAuthCredentialsCallback( + GetAuthCredentialsCB callback, + const std::optional& credentials); + + // BrowserContext to retrieve URLRequestContext and ResourceContext. + raw_ptr browser_context_; + + // Render process id, used to check whether the process can access cookies. + int render_process_id_; + + // Render frame id, used to check tab specific cookie policy. + int render_frame_id_; + + // NOTE: Weak pointers must be invalidated before all other member variables. + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace content + +#endif // ELECTRON_SHELL_BROWSER_MEDIA_RESOURCE_GETTER_IMPL_H_ diff --git a/spec/api-session-spec.ts b/spec/api-session-spec.ts index 7a38d7d00ed21..d3b481df6d2bd 100644 --- a/spec/api-session-spec.ts +++ b/spec/api-session-spec.ts @@ -156,6 +156,8 @@ describe('session module', () => { const list = await w.webContents.session.cookies.get({ url }); const cookie = list.find((cookie) => cookie.name === name); expect(cookie).to.exist.and.to.have.property('value', value); + const authDetails = await w.webContents.getMediaResource(`${url}:${port}`, `${url}:${port}`, `${url}:${port}`); + expect(authDetails).to.deep.equal({ username: '', password: '', cookie: `${cookie?.name}=${cookie?.value}` }); }); it('sets cookies', async () => { diff --git a/spec/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts index c145080649ba3..3081dd418aaee 100644 --- a/spec/api-web-contents-spec.ts +++ b/spec/api-web-contents-spec.ts @@ -3366,6 +3366,8 @@ describe('webContents module', () => { expect(eventAuthInfo.host).to.equal('127.0.0.1'); expect(eventAuthInfo.port).to.equal(serverPort); expect(eventAuthInfo.realm).to.equal('Foo'); + const authDetails = await w.webContents.getMediaResource(`${serverUrl}`, '', `${serverUrl}`); + expect(authDetails).to.deep.equal({ username: user, password: pass, cookie: '' }); }); it('is emitted when a proxy requests authorization', async () => { @@ -3759,4 +3761,79 @@ describe('webContents module', () => { expect(mimeType).to.equal('text/xml'); }); }); + + describe('getblobdata', () => { + const code = ` + + `; + const buffer = Buffer.from(code); + const data = buffer.toString('base64'); + const url = (`data:text/html;base64,${data}`); + let w: BrowserWindow; + let downloadUrl = ''; + + before(async () => { + w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: false } }); + await w.loadURL(url); + downloadUrl = await w.webContents.executeJavaScript('downloadUrl'); + }); + after(closeAllWindows); + + it('fetch all data', async () => { + const result = await w.webContents.getBlobData(downloadUrl, 0, 10); + expect(result.toString()).to.equal('BrightSign'); + expect(result.length).to.equal(10); + }); + + it('fetch in parallel', (done) => { + let numResults = 0; + w.webContents.getBlobData(downloadUrl, 0, 10).then((result) => { + expect(result.toString()).to.equal('BrightSign'); + expect(result.length).to.equal(10); + numResults++; + if (numResults === 2) { + done(); + } + }); + w.webContents.getBlobData(downloadUrl, 5, 5).then((result) => { + expect(result.toString()).to.equal('tSign'); + expect(result.length).to.equal(5); + numResults++; + if (numResults === 2) { + done(); + } + }); + }); + + it('fetch partial data', async () => { + let result = await w.webContents.getBlobData(downloadUrl, 5, 5); + expect(result.toString()).to.equal('tSign'); + expect(result.length).to.equal(5); + + result = await w.webContents.getBlobData(downloadUrl, 2, 3); + expect(result.toString()).to.equal('igh'); + expect(result.length).to.equal(3); + }); + + it('fetch out of range params', async () => { + let result = await w.webContents.getBlobData(downloadUrl, 0, 50); + expect(result.toString()).to.equal('BrightSign'); + expect(result.length).to.equal(10); + + result = await w.webContents.getBlobData(downloadUrl, 10, 10); + expect(result.length).to.equal(0); + }); + + it('fetch non existant url', async () => { + const result = await w.webContents.getBlobData('blob:null/none', 0, 10); + expect(result).to.equal(null); + }); + }); }); From f401b2a9d963048cffe0327930d65ccf2ca5acbe Mon Sep 17 00:00:00 2001 From: Tariq Bashir Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 35/68] feat: Add addFont api call to app: OS-14352 (#101) Add addFont api call to app: OS-14352 Cherry picked from: a9df0d804c3d00bf296874fe558d77e63c43bd6d: feat: Add addFont api call to app: OS-14352 (#101) --- docs/api/app.md | 12 ++++++++++++ shell/browser/api/electron_api_app.cc | 16 ++++++++++++++++ shell/browser/api/electron_api_app.h | 3 +++ 3 files changed, 31 insertions(+) diff --git a/docs/api/app.md b/docs/api/app.md index 41e4a5f7f83e3..2591b6f32448d 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -1726,6 +1726,18 @@ app.setClientCertRequestPasswordHandler(async ({ hostname, tokenName, isRetry }) }) ``` +### `app.addFont(path)` _Linux_ + +* `path` string - A path to the ttf font file. + +Returns `boolean` - Whether the font was successfully added. + +This method calls fontconfig's `FcConfigAppFontAddFile` to add a font to the +application's font configuration. This allows the application to use the font +without having to install it system-wide. The font will be available to all +web contents in the application, but will not be available to other applications +or the system. + ## Properties ### `app.accessibilitySupportEnabled` _macOS_ _Windows_ diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index ae9de2156f892..3ca81938c2e7c 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/shell/browser/api/electron_api_app.cc @@ -87,6 +87,10 @@ #include "v8/include/cppgc/allocation.h" #include "v8/include/v8-traced-handle.h" +#if BUILDFLAG(IS_LINUX) +#include +#endif + #if BUILDFLAG(IS_WIN) #include "base/strings/utf_string_conversions.h" #include "shell/browser/notifications/win/windows_toast_activator.h" @@ -1638,6 +1642,15 @@ v8::Local App::ResolveProxy(gin::Arguments* args) { return handle; } +#if BUILDFLAG(IS_LINUX) +bool App::AddFont(const base::FilePath& path) { + if (FcConfigAppFontAddFile(NULL, (FcChar8*)path.value().c_str()) == FcTrue) { + return true; + } else { + return false; + } +} +#endif void App::SetUserAgentFallback(const std::string& user_agent) { ElectronBrowserClient::Get()->SetUserAgent(user_agent); @@ -1994,6 +2007,9 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) { &App::SetUserAgentFallback) .SetMethod("configureHostResolver", &ConfigureHostResolver) .SetMethod("enableSandbox", &App::EnableSandbox) +#if BUILDFLAG(IS_LINUX) + .SetMethod("addFont", &App::AddFont) +#endif .SetMethod("setProxy", &App::SetProxy) .SetMethod("resolveProxy", &App::ResolveProxy); } diff --git a/shell/browser/api/electron_api_app.h b/shell/browser/api/electron_api_app.h index e3f487934d64f..a77e05bcb0d57 100644 --- a/shell/browser/api/electron_api_app.h +++ b/shell/browser/api/electron_api_app.h @@ -236,6 +236,9 @@ class App final : public gin::Wrappable, v8::Local GetGPUInfo(v8::Isolate* isolate, const std::string& info_type); void EnableSandbox(gin_helper::ErrorThrower thrower); +#if BUILDFLAG(IS_LINUX) + bool AddFont(const base::FilePath& path); +#endif void SetUserAgentFallback(const std::string& user_agent); std::string GetUserAgentFallback(); v8::Local SetProxy(gin::Arguments* args); From cfca0fa52a5f52d6cd2f381900d67469d239b83a Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 36/68] feat: Add switch to disable font matching cache: OS-14352 (#102) Add switch to disable font matching cache: OS-14352 Cherry picked from: b5c16422b54bfb084e9beec342756ace5e067691: feat: Add switch to disable font matching cache: OS-14352 (#102) --- patches/chromium/.patches | 1 + ...disable_font_matching_cache_os-14352.patch | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 patches/chromium/add_switch_to_disable_font_matching_cache_os-14352.patch diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 36aec57a3c943..e4e8699d90fba 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -174,3 +174,4 @@ os-15658_do_not_convert_tooltip_position_to_screen_coordinates_for.patch fix_wayland_keyboard_and_focus_changes.patch os-17051_short-circuit_same_origin_check_for_frame_load_if.patch brightsign_add_back_lookupserverbasicauthcredentials_os-18647.patch +add_switch_to_disable_font_matching_cache_os-14352.patch diff --git a/patches/chromium/add_switch_to_disable_font_matching_cache_os-14352.patch b/patches/chromium/add_switch_to_disable_font_matching_cache_os-14352.patch new file mode 100644 index 0000000000000..e62e3a440844d --- /dev/null +++ b/patches/chromium/add_switch_to_disable_font_matching_cache_os-14352.patch @@ -0,0 +1,68 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tariq Bashir +Date: Thu, 12 Jun 2025 12:30:02 +0100 +Subject: Add switch to disable font matching cache: OS-14352 + +This patch adds a switch to disable font family matching cache in FontServiceApp. This cache +is created and lives in the main process and is not cleared as its a LRU cache. This causes +issues when the AddFont API is called in between using multiple roHtmlWidgets, as the cache +isn't cleared as the main process isn't restarted. + +An analysis was made on how to clear the cache when the AddFont API is called from Electron, +but this was found to be difficult and not worth the effort: +The way that FontServiceApp, which runs in the main process, is accessed is via FontLoader, +which is called in the render process. We call AddFont in the main process and so cannot use +FontLoader to call a new CacheClear method in FontServiceApp. Also, trying to get a reference +and directly call FontServiceApp would be very messy. Finally, since we call AddFont before the +render process has been created, to add a call from there triggered by Electron is also not easy. + +diff --git a/base/base_switches.h b/base/base_switches.h +index 1c8c6d07bd62f92d19c95bfd192cb84c41ccbc82..b4f30a51cefd5c56b6870ebdb47563dc59d9e851 100644 +--- a/base/base_switches.h ++++ b/base/base_switches.h +@@ -20,6 +20,12 @@ inline constexpr char kDisableBreakpad[] = "disable-breakpad"; + // Comma-separated list of feature names to disable. See also kEnableFeatures. + inline constexpr char kDisableFeatures[] = "disable-features"; + ++// Disables font family matching cache in FontServiceApp. This is required for ++// when using the AddFont API to add a font in FontConfig as the cache lives ++// as long as the main process is running and isn't manually cleared. ++inline constexpr char kDisableFontFamilyMatchingCache[] = ++ "disable-font-family-matching-cache"; ++ + // Force disabling of low-end device mode when set. + inline constexpr char kDisableLowEndDeviceMode[] = + "disable-low-end-device-mode"; +diff --git a/components/services/font/font_service_app.cc b/components/services/font/font_service_app.cc +index 0605e678e471fe2c124cdea16113261bea396801..c4f77cf253fa9a2c3095e061deb416e17338bb8c 100644 +--- a/components/services/font/font_service_app.cc ++++ b/components/services/font/font_service_app.cc +@@ -10,6 +10,7 @@ + #include + #include + ++#include "base/base_switches.h" + #include "base/command_line.h" + #include "base/containers/span.h" + #include "base/feature_list.h" +@@ -152,12 +153,14 @@ void FontServiceApp::MatchFamilyName(const std::string& family_name, + style->slant = static_cast(SkFontStyle().slant()); + } + +- // Add to the cache. +- MatchCacheValue value; +- value.family_name = result_family_cppstring; +- value.identity = identity ? identity.Clone() : nullptr; +- value.style = style.Clone(); +- match_cache_.Put(key, std::move(value)); ++ if (!base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kDisableFontFamilyMatchingCache)) { ++ // Add to the cache. ++ MatchCacheValue value; ++ value.family_name = result_family_cppstring; ++ value.identity = identity ? identity.Clone() : nullptr; ++ value.style = style.Clone(); ++ match_cache_.Put(key, std::move(value)); ++ } + + std::move(callback).Run(std::move(identity), result_family_cppstring, + std::move(style)); From a5433729cf2b5797d2459e7a24f15856e8cc9c93 Mon Sep 17 00:00:00 2001 From: Tariq Bashir <120014322+t-bashir-bs@users.noreply.github.com> Date: Wed, 6 May 2026 08:39:41 +0100 Subject: [PATCH 37/68] feat: Add will-open-dialog event to webcontents (#30) * Add will-open-dialog event to webcontent * Added test cases to api-web-contents-spec * Fix originUrl param being set as the path Cherry picked from: 566d8c77b24e349126dec0567c09ef2b4fb9fab2: feat: Add will-open-dialog event to webcontents (#30) --- docs/api/web-contents.md | 22 +++++++ .../browser/api/electron_api_web_contents.cc | 60 ++++++++++++++++++ spec/api-web-contents-spec.ts | 61 +++++++++++++++++++ 3 files changed, 143 insertions(+) diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 203e900ea943e..9588fb9739ae0 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1073,6 +1073,28 @@ Returns: Emitted when the [mainFrame](web-contents.md#contentsmainframe-readonly), an `