├── package.json ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ ├── regression.md │ └── bug.md └── workflows │ ├── patchright_workflow.yml │ └── patch_release_workflow.yml ├── driver_patches ├── serverRegistryIndex.js ├── index.js ├── recorderPatch.js ├── clockPatch.js ├── chromiumPatch.js ├── crDevToolsPatch.js ├── frameDispatcherPatch.js ├── crServiceWorkerPatch.js ├── chromiumSwitchesPatch.js ├── jsHandleDispatcherPatch.js ├── pageDispatcherPatch.js ├── XPathSelectorEnginePatch.js ├── javascriptPatch.js ├── crBrowserPatch.js ├── browserContextPatch.js ├── pageBindingPatch.js ├── pagePatch.js ├── utilityScriptSerializersPatch.js ├── frameSelectorsPatch.js ├── crNetworkManagerPatch.js ├── crPagePatch.js └── framesPatch.js ├── utils ├── release_version_check.sh └── release_driver.sh ├── .gitignore ├── patchright_driver_patch.js ├── README.md └── LICENSE /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "ts-morph": "^23.0.0", 4 | "yaml": "^2.7.0" 5 | }, 6 | "type": "module" 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Join our Discord Server 3 | url: https://aka.ms/playwright/discord 4 | about: Ask questions and discuss with other community members 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request new features to be added 4 | title: "[Feature]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Let us know what functionality you'd like to see in Playwright and what your use case is. 11 | Do you think others might benefit from this as well? 12 | -------------------------------------------------------------------------------- /driver_patches/serverRegistryIndex.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/registry/index.ts 5 | // ---------------------------- 6 | export function patchServerRegistryIndex(project) { 7 | const sourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/registry/index.ts"); 8 | 9 | const fn = sourceFile.getFunctionOrThrow("buildPlaywrightCLICommand"); 10 | 11 | const switchStmt = fn.getFirstDescendantByKindOrThrow(SyntaxKind.SwitchStatement); 12 | 13 | const defaultClause = switchStmt.getFirstDescendantByKindOrThrow(SyntaxKind.DefaultClause); 14 | 15 | const returnStmt = defaultClause.getFirstDescendantByKindOrThrow(SyntaxKind.ReturnStatement); 16 | 17 | const oldText = returnStmt.getText(); 18 | const newText = oldText.replace("playwright", "patchright"); 19 | returnStmt.replaceWithText(newText); 20 | } 21 | -------------------------------------------------------------------------------- /driver_patches/index.js: -------------------------------------------------------------------------------- 1 | export * from "./browserContextPatch.js"; 2 | export * from "./chromiumPatch.js"; 3 | export * from "./chromiumSwitchesPatch.js"; 4 | export * from "./clockPatch.js"; 5 | export * from "./crBrowserPatch.js"; 6 | export * from "./crDevToolsPatch.js"; 7 | export * from "./crNetworkManagerPatch.js"; 8 | export * from "./crPagePatch.js"; 9 | export * from "./crServiceWorkerPatch.js"; 10 | export * from "./frameDispatcherPatch.js"; 11 | export * from "./frameSelectorsPatch.js"; 12 | export * from "./framesPatch.js"; 13 | export * from "./javascriptPatch.js"; 14 | export * from "./jsHandleDispatcherPatch.js"; 15 | export * from "./pageBindingPatch.js"; 16 | export * from "./pageDispatcherPatch.js"; 17 | export * from "./pagePatch.js"; 18 | export * from "./utilityScriptSerializersPatch.js"; 19 | export * from "./XPathSelectorEnginePatch.js"; 20 | export * from "./serverRegistryIndex.js"; 21 | export * from "./recorderPatch.js"; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/regression.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report regression 3 | about: Functionality that used to work and does not any more 4 | title: "[REGRESSION]: " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Context:** 11 | - GOOD Playwright Version: [what Playwright version worked nicely?] 12 | - BAD Playwright Version: [what Playwright version doesn't work any more?] 13 | - Operating System: [e.g. Windows, Linux or Mac] 14 | - Extra: [any specific details about your environment] 15 | 16 | **Code Snippet** 17 | 18 | Help us help you! Put down a short code snippet that illustrates your bug and 19 | that we can run and debug locally. For example: 20 | 21 | ```python 22 | from undetected_playwright.sync_api import sync_playwright 23 | 24 | with sync_playwright() as p: 25 | browser = p.chromium.launch() 26 | page = browser.new_page() 27 | # ... 28 | browser.close() 29 | ``` 30 | 31 | **Describe the bug** 32 | 33 | Add any other details about the problem here. 34 | -------------------------------------------------------------------------------- /driver_patches/recorderPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/registry/index.ts 5 | // ---------------------------- 6 | export function patchRecorder(project) { 7 | const sourceFile = project.addSourceFileAtPath("packages/recorder/src/recorder.tsx"); 8 | 9 | // ------- Recorder Const ------- 10 | const recorderDecl = sourceFile.getVariableDeclarationOrThrow("Recorder"); 11 | // Get the arrow function assigned to Recorder 12 | const recorderFn = recorderDecl.getInitializerIfKindOrThrow(SyntaxKind.ArrowFunction); 13 | const recorderBody = recorderFn.getBody(); 14 | 15 | // Add try-catch block around the existing React.useEffect body 16 | const useEffectCall = recorderBody.getDescendantsOfKind(SyntaxKind.CallExpression).find(call => call.getExpression().getText() === "React.useEffect"); 17 | const effectCallback = useEffectCall.getArguments()[0]; 18 | effectCallback.setBodyText(`try { window.dispatch({ event: 'setAutoExpect', params: { autoExpect } }); } catch {}`); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /driver_patches/clockPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/clock.ts 5 | // ---------------------------- 6 | export function patchClock(project) { 7 | // Add source file to the project 8 | const clockSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/clock.ts"); 9 | 10 | // ------- Page Class ------- 11 | const clockClass = clockSourceFile.getClass("Clock"); 12 | 13 | // -- _evaluateInFrames Method -- 14 | const evaluateInFramesMethod = clockClass.getMethod("_evaluateInFrames"); 15 | // Modify the constructor's body to include Custom Code 16 | const evaluateInFramesMethodBody = evaluateInFramesMethod.getBody(); 17 | evaluateInFramesMethodBody.insertStatements(0, ` 18 | // Dont ask me why this works 19 | await Promise.all(this._browserContext.pages().map(async page => { 20 | await Promise.all(page.frames().map(async frame => { 21 | try { 22 | await frame.evaluateExpression(""); 23 | } catch (e) {} 24 | })); 25 | })); 26 | `); 27 | } -------------------------------------------------------------------------------- /driver_patches/chromiumPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/chromium/chromium.ts 5 | // ---------------------------- 6 | export function patchChromium(project) { 7 | // Add source file to the project 8 | const chromiumSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/chromium/chromium.ts"); 9 | 10 | // ------- Chromium Class ------- 11 | const chromiumClass = chromiumSourceFile.getClass("Chromium"); 12 | 13 | // -- _innerDefaultArgs Method -- 14 | const innerDefaultArgsMethod = chromiumClass.getMethod("_innerDefaultArgs"); 15 | // Get all the if statements in the method 16 | const innerDefaultArgsMethodStatements = innerDefaultArgsMethod.getDescendantsOfKind(SyntaxKind.IfStatement); 17 | // Modifying the Code to always use the --headless=new flag 18 | innerDefaultArgsMethodStatements.forEach((ifStatement) => { 19 | const condition = ifStatement.getExpression().getText(); 20 | if (condition.includes("process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW")) { 21 | ifStatement.replaceWithText("chromeArguments.push('--headless=new');"); 22 | } 23 | }); 24 | } -------------------------------------------------------------------------------- /driver_patches/crDevToolsPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/chromium/crDevTools.ts 5 | // ---------------------------- 6 | export function patchCRDevTools(project) { 7 | // Add source file to the project 8 | const crDevToolsSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/chromium/crDevTools.ts"); 9 | 10 | // ------- CRDevTools Class ------- 11 | const crDevToolsClass = crDevToolsSourceFile.getClass("CRDevTools"); 12 | 13 | // -- Install Method -- 14 | const installMethod = crDevToolsClass.getMethod("install"); 15 | // Find the specific `Promise.all` call 16 | const promiseAllCalls = installMethod 17 | .getDescendantsOfKind(SyntaxKind.CallExpression) 18 | .filter((call) => call.getExpression().getText() === "Promise.all"); 19 | // Removing Runtime.enable from the Promise.all call 20 | promiseAllCalls.forEach((call) => { 21 | const arrayLiteral = call.getFirstDescendantByKind( 22 | SyntaxKind.ArrayLiteralExpression, 23 | ); 24 | if (arrayLiteral) { 25 | arrayLiteral.getElements().forEach((element) => { 26 | if (element.getText().includes("session.send('Runtime.enable'")) { 27 | arrayLiteral.removeElement(element); 28 | } 29 | }); 30 | } 31 | }); 32 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Something doesn't work like it should? Tell us! 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ### System info 17 | - Playwright Version: [v1.XX] 18 | - Operating System: [All, Windows 11, Ubuntu 20, macOS 13.2, etc.] 19 | - Browser: [All, Chromium, Firefox, WebKit] 20 | - Other info: 21 | 22 | ### Source code 23 | 24 | - [ ] I provided exact source code that allows reproducing the issue locally. 25 | 26 | 27 | 28 | 29 | 30 | 31 | **Link to the GitHub repository with the repro** 32 | 33 | [https://github.com/your_profile/playwright_issue_title] 34 | 35 | or 36 | 37 | **Test file (self-contained)** 38 | 39 | ```python 40 | from undetected_playwright.sync_api import sync_playwright 41 | 42 | with sync_playwright() as p: 43 | browser = p.chromium.launch() 44 | page = browser.new_page() 45 | # ... 46 | browser.close() 47 | ``` 48 | 49 | **Steps** 50 | - [Run the test] 51 | - [...] 52 | 53 | **Expected** 54 | 55 | [Describe expected behavior] 56 | 57 | **Actual** 58 | 59 | [Describe actual behavior] 60 | -------------------------------------------------------------------------------- /driver_patches/frameDispatcherPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/dispatchers/frameDispatcher.ts 5 | // ---------------------------- 6 | export function patchFrameDispatcher(project) { 7 | // Add source file to the project 8 | const frameDispatcherSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/dispatchers/frameDispatcher.ts"); 9 | 10 | // ------- frameDispatcher Class ------- 11 | const frameDispatcherClass = frameDispatcherSourceFile.getClass("FrameDispatcher"); 12 | 13 | // -- evaluateExpression Method -- 14 | const frameEvaluateExpressionMethod = frameDispatcherClass.getMethod("evaluateExpression"); 15 | frameEvaluateExpressionMethod.setBodyText(` 16 | return { value: serializeResult(await progress.race(this._frame.evaluateExpression(params.expression, { isFunction: params.isFunction, world: params.isolatedContext ? 'utility': 'main' }, parseArgument(params.arg)))) }; 17 | `); 18 | 19 | // -- evaluateExpressionHandle Method -- 20 | const frameEvaluateExpressionHandleMethod = frameDispatcherClass.getMethod("evaluateExpressionHandle"); 21 | frameEvaluateExpressionHandleMethod.setBodyText(` 22 | return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await progress.race(this._frame.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction, world: params.isolatedContext ? 'utility': 'main' }, parseArgument(params.arg)))) }; 23 | `); 24 | 25 | // -- evaluateExpression Method -- 26 | const frameEvalOnSelectorAllExpressionMethod = frameDispatcherClass.getMethod("evalOnSelectorAll"); 27 | frameEvalOnSelectorAllExpressionMethod.setBodyText(` 28 | return { value: serializeResult(await this._frame.evalOnSelectorAll(params.selector, params.expression, params.isFunction, parseArgument(params.arg), null, params.isolatedContext)) }; 29 | `); 30 | } -------------------------------------------------------------------------------- /driver_patches/crServiceWorkerPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/chromium/crServiceWorker.ts 5 | // ---------------------------- 6 | export function patchCRServiceWorker(project) { 7 | // Add source file to the project 8 | const crServiceWorkerSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/chromium/crServiceWorker.ts"); 9 | 10 | // ------- CRServiceWorker Class ------- 11 | const crServiceWorkerClass = crServiceWorkerSourceFile.getClass("CRServiceWorker"); 12 | 13 | // -- CRServiceWorker Constructor -- 14 | const crServiceWorkerConstructorDeclaration = crServiceWorkerClass 15 | .getConstructors() 16 | .find((ctor) => 17 | ctor 18 | .getText() 19 | .includes("constructor(browserContext: CRBrowserContext, session: CRSession, url: string)") 20 | ); 21 | const crServiceWorkerConstructorBody = crServiceWorkerConstructorDeclaration.getBody(); 22 | // Find the Runtime.enable statement to remove 23 | const statementToRemove = crServiceWorkerConstructorBody 24 | .getStatements() 25 | .find((statement) => 26 | statement 27 | .getText() 28 | .includes("session.send('Runtime.enable', {}).catch(e => { });") 29 | ); 30 | if (statementToRemove) statementToRemove.remove(); 31 | 32 | crServiceWorkerConstructorBody.addStatements(` 33 | session._sendMayFail("Runtime.evaluate", { 34 | expression: "globalThis", 35 | serializationOptions: { serialization: "idOnly" } 36 | }).then(globalThis => { 37 | if (globalThis && globalThis.result) { 38 | var globalThisObjId = globalThis.result.objectId; 39 | var executionContextId = parseInt(globalThisObjId.split(".")[1], 10); 40 | this.createExecutionContext(new CRExecutionContext(session, { id: executionContextId })); 41 | } 42 | }); 43 | `); 44 | } -------------------------------------------------------------------------------- /utils/release_version_check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to get the latest release version from a GitHub repository 4 | get_latest_release() { 5 | local repo=$1 6 | local response=$(curl --silent "https://api.github.com/repos/$repo/releases/latest") 7 | local version=$(echo "$response" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') 8 | 9 | # Check if version is empty (meaning no releases found) 10 | if [ -z "$version" ]; then 11 | version="v0.0.0" 12 | fi 13 | 14 | echo "$version" 15 | } 16 | 17 | # Function to compare two semantic versions (ignoring 'v' prefix) 18 | version_is_behind() { 19 | local version1=${1//v/} # Remove 'v' prefix from version1 20 | local version2=${2//v/} # Remove 'v' prefix from version2 21 | 22 | IFS='.' read ver1_1 ver1_2 ver1_3 <<< "$version1" 23 | IFS='.' read ver2_1 ver2_2 ver2_3 <<< "$version2" 24 | 25 | ver1_1=${ver1_1:-0} 26 | ver1_2=${ver1_2:-0} 27 | ver1_3=${ver1_3:-0} 28 | ver2_1=${ver2_1:-0} 29 | ver2_2=${ver2_2:-0} 30 | ver2_3=${ver2_3:-0} 31 | 32 | if ((10#$ver1_1 < 10#$ver2_1)) || ((10#$ver1_1 == 10#$ver2_1 && 10#$ver1_2 < 10#$ver2_2)) || ((10#$ver1_1 == 10#$ver2_1 && 10#$ver1_2 == 10#$ver2_2 && 10#$ver1_3 < 10#$ver2_3)); then 33 | return 0 34 | else 35 | return 1 36 | fi 37 | } 38 | 39 | # Get the latest release version of microsoft/playwright 40 | playwright_version=$(get_latest_release "microsoft/playwright") 41 | echo "Latest release of the Playwright Driver: $playwright_version" 42 | 43 | # Get the latest release version of Patchright 44 | patchright_version=$(get_latest_release $REPO) 45 | echo "Latest release of the Patchright Driver: $patchright_version" 46 | 47 | # Compare the versions 48 | if version_is_behind "$patchright_version" "$playwright_version"; then 49 | echo "$REPO is behind microsoft/playwright. Building & Patching..." 50 | echo "proceed=true" >>$GITHUB_OUTPUT 51 | echo "playwright_version=$playwright_version" >>$GITHUB_ENV 52 | else 53 | echo "$REPO is up to date with microsoft/playwright." 54 | echo "proceed=false" >>$GITHUB_OUTPUT 55 | fi 56 | -------------------------------------------------------------------------------- /utils/release_driver.sh: -------------------------------------------------------------------------------- 1 | echo "Patching complete. Uploading to GitHub..." 2 | VERSION_NUMBER="${playwright_version#v}" 3 | RELEASE_DESCRIPTION="This is an automatic deployment in response to a new release of [microsoft/playwright](https://github.com/microsoft/playwright).\nThe original Release can be seen [here](https://github.com/microsoft/playwright/releases/tag/$playwright_version)." 4 | 5 | # Step 1: Create a new GitHub release and get the upload URL 6 | RELEASE_RESPONSE=$(curl -s -X POST \ 7 | -H "Authorization: token $GITHUB_TOKEN" \ 8 | -H "Content-Type: application/json" \ 9 | -d "{\"tag_name\": \"$playwright_version\", \"name\": \"$playwright_version\", \"body\": \"$RELEASE_DESCRIPTION\", \"draft\": false, \"prerelease\": false}" \ 10 | "https://api.github.com/repos/$REPO/releases") 11 | 12 | echo "$RELEASE_RESPONSE" 13 | 14 | # Remove line breaks from the response 15 | RELEASE_RESPONSE=$(echo "$RELEASE_RESPONSE" | tr -d '\n') 16 | # Extract the upload URL from the release response 17 | UPLOAD_URL=$(echo $RELEASE_RESPONSE | sed 's/$/\\n/' | tr -d '\n' | sed -e 's/“/"/g' -e 's/”/"/g' | sed '$ s/\\n$//' | jq -r .upload_url | sed "s/{?name,label}//") 18 | 19 | # Check if upload URL was extracted correctly 20 | if [ -z "$UPLOAD_URL" ]; then 21 | echo "Failed to create release. Exiting." 22 | exit 1 23 | fi 24 | 25 | # Step 2: Upload each .zip file in the directory as an asset 26 | for ZIP_FILE in "/playwright-$VERSION_NUMBER-mac.zip" "/playwright-$VERSION_NUMBER-mac-arm64.zip" "/playwright-$VERSION_NUMBER-linux.zip" "/playwright-$VERSION_NUMBER-linux-arm64.zip" "/playwright-$VERSION_NUMBER-win32_x64.zip" "/playwright-$VERSION_NUMBER-win32_arm64.zip"; 27 | do 28 | FILE_NAME=$(basename "$ZIP_FILE") 29 | echo "Uploading $FILE_NAME..." 30 | echo "token $GITHUB_TOKEN" 31 | 32 | curl -s -X POST \ 33 | -H "Authorization: token $GITHUB_TOKEN" \ 34 | -H "Content-Type: application/zip" \ 35 | --data-binary @"./playwright/utils/build/output/$ZIP_FILE" \ 36 | "$UPLOAD_URL?name=$FILE_NAME" 37 | done 38 | 39 | echo "\n\nRelease and assets uploaded successfully!" -------------------------------------------------------------------------------- /driver_patches/chromiumSwitchesPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/chromium/chromiumSwitches.ts 5 | // ---------------------------- 6 | export function patchChromiumSwitches(project) { 7 | // Add source file to the project 8 | const chromiumSwitchesSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/chromium/chromiumSwitches.ts"); 9 | 10 | // -- chromiumSwitches Array Variable -- 11 | const chromiumSwitchesArray = chromiumSwitchesSourceFile 12 | .getVariableDeclarationOrThrow("chromiumSwitches") 13 | .getInitializerIfKindOrThrow(SyntaxKind.ArrowFunction) 14 | .getBody() 15 | .getDescendantsOfKind(SyntaxKind.ArrayLiteralExpression)[0]; 16 | // Patchright defined switches to disable 17 | const switchesToDisable = [ 18 | "assistantMode ? '' : '--enable-automation'", 19 | "'--disable-popup-blocking'", 20 | "'--disable-component-update'", 21 | "'--disable-default-apps'", 22 | "'--disable-extensions'", 23 | "'--disable-client-side-phishing-detection'", 24 | "'--disable-component-extensions-with-background-pages'", 25 | "'--allow-pre-commit-input'", 26 | "'--disable-ipc-flooding-protection'", 27 | "'--metrics-recording-only'", 28 | "'--unsafely-disable-devtools-self-xss-warnings'", 29 | "'--disable-back-forward-cache'", 30 | "'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker'" 31 | ]; 32 | chromiumSwitchesArray.getElements().forEach((element) => { 33 | if (switchesToDisable.includes(element.getText())) { 34 | chromiumSwitchesArray.removeElement(element); 35 | } 36 | }); 37 | // Add custom switches to the array 38 | chromiumSwitchesArray.addElement( 39 | `'--disable-blink-features=AutomationControlled'`, 40 | ); 41 | } -------------------------------------------------------------------------------- /driver_patches/jsHandleDispatcherPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/dispatchers/jsHandleDispatcher.ts 5 | // ---------------------------- 6 | export function patchJSHandleDispatcher(project) { 7 | // Add source file to the project 8 | const jsHandleDispatcherSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts"); 9 | 10 | // ------- workerDispatcher Class ------- 11 | const jsHandleDispatcherClass = jsHandleDispatcherSourceFile.getClass("JSHandleDispatcher"); 12 | 13 | // -- evaluateExpression Method -- 14 | const jsHandleDispatcherEvaluateExpressionMethod = jsHandleDispatcherClass.getMethod("evaluateExpression"); 15 | const jsHandleDispatcherEvaluateExpressionReturn = jsHandleDispatcherEvaluateExpressionMethod.getFirstDescendantByKind(SyntaxKind.ReturnStatement); 16 | const jsHandleDispatcherEvaluateExpressionCall = jsHandleDispatcherEvaluateExpressionReturn.getFirstDescendantByKind(SyntaxKind.CallExpression).getFirstDescendantByKind(SyntaxKind.CallExpression); 17 | // add isolatedContext Bool Param 18 | if (jsHandleDispatcherEvaluateExpressionCall && jsHandleDispatcherEvaluateExpressionCall.getExpression().getText().includes("this._object.evaluateExpression")) { 19 | // Add the new argument to the function call 20 | jsHandleDispatcherEvaluateExpressionCall.addArgument("params.isolatedContext"); 21 | } 22 | 23 | // -- evaluateExpressionHandle Method -- 24 | const jsHandleDispatcherEvaluateExpressionHandleMethod = jsHandleDispatcherClass.getMethod("evaluateExpressionHandle"); 25 | const jsHandleDispatcherEvaluateExpressionHandleCall = jsHandleDispatcherEvaluateExpressionHandleMethod.getFirstDescendantByKind(SyntaxKind.CallExpression); 26 | // add isolatedContext Bool Param 27 | if (jsHandleDispatcherEvaluateExpressionHandleCall && jsHandleDispatcherEvaluateExpressionHandleCall.getExpression().getText().includes("this._object.evaluateExpression")) { 28 | // Add the new argument to the function call 29 | jsHandleDispatcherEvaluateExpressionHandleCall.addArgument("params.isolatedContext"); 30 | } 31 | } -------------------------------------------------------------------------------- /driver_patches/pageDispatcherPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/dispatchers/pageDispatcher.ts 5 | // ---------------------------- 6 | export function patchPageDispatcher(project) { 7 | // Add source file to the project 8 | const pageDispatcherSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/dispatchers/pageDispatcher.ts"); 9 | 10 | // ------- workerDispatcher Class ------- 11 | const workerDispatcherClass = pageDispatcherSourceFile.getClass("WorkerDispatcher"); 12 | 13 | // -- evaluateExpression Method -- 14 | const workerDispatcherEvaluateExpressionMethod = workerDispatcherClass.getMethod("evaluateExpression"); 15 | const workerDispatcherEvaluateExpressionReturn = workerDispatcherEvaluateExpressionMethod.getFirstDescendantByKind(SyntaxKind.ReturnStatement); 16 | const workerDispatcherEvaluateExpressionCall = workerDispatcherEvaluateExpressionReturn.getFirstDescendantByKind(SyntaxKind.CallExpression).getFirstDescendantByKind(SyntaxKind.CallExpression); 17 | // add isolatedContext Bool Param 18 | if (workerDispatcherEvaluateExpressionCall && workerDispatcherEvaluateExpressionCall.getExpression().getText().includes("this._object.evaluateExpression")) { 19 | // Add the new argument to the function call 20 | workerDispatcherEvaluateExpressionCall.addArgument("params.isolatedContext"); 21 | } 22 | 23 | // -- evaluateExpressionHandle Method -- 24 | const workerDispatcherEvaluateExpressionHandleMethod = workerDispatcherClass.getMethod("evaluateExpressionHandle"); 25 | const workerDispatcherEvaluateExpressionHandleReturn = workerDispatcherEvaluateExpressionHandleMethod.getFirstDescendantByKind(SyntaxKind.ReturnStatement); 26 | const workerDispatcherEvaluateExpressionHandleCall = workerDispatcherEvaluateExpressionHandleReturn.getFirstDescendantByKind(SyntaxKind.CallExpression).getFirstDescendantByKind(SyntaxKind.CallExpression); 27 | // add isolatedContext Bool Param 28 | if (workerDispatcherEvaluateExpressionHandleCall && workerDispatcherEvaluateExpressionHandleCall.getExpression().getText().includes("this._object.evaluateExpression")) { 29 | // Add the new argument to the function call 30 | workerDispatcherEvaluateExpressionHandleCall.addArgument("params.isolatedContext"); 31 | } 32 | } -------------------------------------------------------------------------------- /driver_patches/XPathSelectorEnginePatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // injected/src/xpathSelectorEngine.ts 5 | // ---------------------------- 6 | export function patchXPathSelectorEngine(project) { 7 | // Add source file to the project 8 | const xpathSelectorEngineSourceFile = project.addSourceFileAtPath("packages/injected/src/xpathSelectorEngine.ts"); 9 | 10 | // ------- XPathEngine Class ------- 11 | const xPathEngineLiteral = xpathSelectorEngineSourceFile.getVariableDeclarationOrThrow("XPathEngine").getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression); 12 | 13 | // -- evaluateExpression Method -- 14 | const queryAllMethod = xPathEngineLiteral.getProperty("queryAll"); 15 | const queryAllMethodBody = queryAllMethod.getBody(); 16 | queryAllMethodBody.insertStatements(0, ` 17 | if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 18 | const result: Element[] = []; 19 | // Custom ClosedShadowRoot XPath Engine 20 | const parser = new DOMParser(); 21 | // Function to (recursively) get all elements in the shadowRoot 22 | function getAllChildElements(node) { 23 | const elements = []; 24 | const traverse = (currentNode) => { 25 | if (currentNode.nodeType === Node.ELEMENT_NODE) elements.push(currentNode); 26 | currentNode.childNodes?.forEach(traverse); 27 | }; 28 | if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE) { 29 | traverse(node); 30 | } 31 | return elements; 32 | } 33 | // Setting innerHTMl and childElements (all, recursive) to avoid race conditions 34 | const csrHTMLContent = root.innerHTML; 35 | const csrChildElements = getAllChildElements(root); 36 | const htmlDoc = parser.parseFromString(csrHTMLContent, 'text/html'); 37 | const rootDiv = htmlDoc.body 38 | const rootDivChildElements = getAllChildElements(rootDiv); 39 | // Use the namespace prefix in the XPath expression 40 | const it = htmlDoc.evaluate(selector, htmlDoc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE); 41 | for (let node = it.iterateNext(); node; node = it.iterateNext()) { 42 | // -1 for the body element 43 | const nodeIndex = rootDivChildElements.indexOf(node) - 1; 44 | if (nodeIndex >= 0) { 45 | const originalNode = csrChildElements[nodeIndex]; 46 | if (originalNode.nodeType === Node.ELEMENT_NODE) 47 | result.push(originalNode as Element); 48 | } 49 | } 50 | return result; 51 | } 52 | `); 53 | } -------------------------------------------------------------------------------- /driver_patches/javascriptPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/javascript.ts 5 | // ---------------------------- 6 | export function patchJavascript(project) { 7 | // Add source file to the project 8 | const javascriptSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/javascript.ts"); 9 | 10 | // -------JSHandle Class ------- 11 | const jsHandleClass = javascriptSourceFile.getClass("JSHandle"); 12 | 13 | // -- evaluateExpression Method -- 14 | const jsHandleEvaluateExpressionMethod = jsHandleClass.getMethod("evaluateExpression"); 15 | jsHandleEvaluateExpressionMethod.addParameter({ 16 | name: "isolatedContext", 17 | type: "boolean", 18 | hasQuestionToken: true, 19 | }); 20 | const jsHandleEvaluateExpressionMethodBody = jsHandleEvaluateExpressionMethod.getBody(); 21 | jsHandleEvaluateExpressionMethodBody.replaceWithText( 22 | jsHandleEvaluateExpressionMethodBody.getText().replace(/this\._context/g, "context") 23 | ); 24 | // Insert the new line of code after the responseAwaitStatement 25 | jsHandleEvaluateExpressionMethodBody.insertStatements(0,` 26 | let context = this._context; 27 | if (context.constructor.name === "FrameExecutionContext") { 28 | const frame = context.frame; 29 | if (frame) { 30 | if (isolatedContext === true) context = await frame._utilityContext(); 31 | else if (isolatedContext === false) context = await frame._mainContext(); 32 | } 33 | } 34 | `); 35 | 36 | // -- evaluateExpressionHandle Method -- 37 | const jsHandleEvaluateExpressionHandleMethod = jsHandleClass.getMethod("evaluateExpressionHandle"); 38 | jsHandleEvaluateExpressionHandleMethod.addParameter({ 39 | name: "isolatedContext", 40 | type: "boolean", 41 | hasQuestionToken: true, 42 | }); 43 | const jsHandleEvaluateExpressionHandleMethodBody = jsHandleEvaluateExpressionHandleMethod.getBody(); 44 | jsHandleEvaluateExpressionHandleMethodBody.replaceWithText( 45 | jsHandleEvaluateExpressionHandleMethodBody.getText().replace(/this\._context/g, "context") 46 | ); 47 | // Insert the new line of code after the responseAwaitStatement 48 | jsHandleEvaluateExpressionHandleMethodBody.insertStatements(0,` 49 | let context = this._context; 50 | if (context.constructor.name === "FrameExecutionContext") { 51 | const frame = this._context.frame; 52 | if (frame) { 53 | if (isolatedContext === true) context = await frame._utilityContext(); 54 | else if (isolatedContext === false) context = await frame._mainContext(); 55 | } 56 | } 57 | `); 58 | } -------------------------------------------------------------------------------- /driver_patches/crBrowserPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/chromium/crBrowser.ts 5 | // ---------------------------- 6 | export function patchCRBrowser(project) { 7 | // Add source file to the project 8 | const crBrowserSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/chromium/crBrowser.ts"); 9 | 10 | // ------- CRDevTools Class ------- 11 | const crBrowserContextClass = crBrowserSourceFile.getClass("CRBrowserContext"); 12 | 13 | // -- doRemoveNonInternalInitScripts Method -- 14 | // crBrowserContextClass.getMethod("doRemoveNonInternalInitScripts").remove(); 15 | 16 | // -- doRemoveInitScripts Method -- 17 | // crBrowserContextClass.addMethod({ 18 | // name: "doRemoveInitScripts", 19 | // scope: "protected", 20 | // isAbstract: true, 21 | // returnType: "Promise", 22 | // }); 23 | 24 | // -- doExposeBinding Method -- 25 | //crBrowserContextClass.addMethod({ 26 | // name: "doExposeBinding", 27 | // scope: "protected", 28 | // isAbstract: true, 29 | // parameters: [{ name: "binding", type: "PageBinding" }], 30 | // returnType: "Promise", 31 | //}); 32 | 33 | // -- doRemoveExposedBindings Method -- 34 | //crBrowserContextClass.addMethod({ 35 | // name: "doRemoveExposedBindings", 36 | // scope: "protected", 37 | // isAbstract: true, 38 | // returnType: "Promise", 39 | //}); 40 | 41 | // -- doRemoveInitScripts Method -- 42 | // crBrowserContextClass.addMethod({ 43 | // name: "doRemoveInitScripts", 44 | // isAsync: true, 45 | // }); 46 | const doRemoveInitScriptsMethod = crBrowserContextClass.getMethod( 47 | "doRemoveInitScripts", 48 | ); 49 | doRemoveInitScriptsMethod.setBodyText(` 50 | for (const page of this.pages()) await (page.delegate as CRPage).removeInitScripts(); 51 | `); 52 | 53 | 54 | // ------- CRBrowserContext Class ------- 55 | const crBrowserClass = crBrowserSourceFile.getClass("CRBrowserContext"); 56 | 57 | // -- doExposeBinding Method -- 58 | crBrowserClass.addMethod({ 59 | name: "doExposeBinding", 60 | isAsync: true, 61 | parameters: [{ name: "binding", type: "PageBinding" }], 62 | }); 63 | const doExposeBindingMethod = crBrowserClass.getMethod("doExposeBinding"); 64 | doExposeBindingMethod.setBodyText(` 65 | for (const page of this.pages()) await (page.delegate as CRPage).exposeBinding(binding); 66 | `); 67 | 68 | // -- doRemoveExposedBindings Method -- 69 | crBrowserClass.addMethod({ 70 | name: "doRemoveExposedBindings", 71 | isAsync: true, 72 | }); 73 | const doRemoveExposedBindingsMethod = crBrowserClass.getMethod( 74 | "doRemoveExposedBindings", 75 | ); 76 | doRemoveExposedBindingsMethod.setBodyText(` 77 | for (const page of this.pages()) await (page.delegate as CRPage).removeExposedBindings(); 78 | `); 79 | } -------------------------------------------------------------------------------- /.github/workflows/patchright_workflow.yml: -------------------------------------------------------------------------------- 1 | name: PatchRight Workflow 2 | 3 | on: 4 | # enabling manual trigger 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | description: 'Playwright Version' 9 | default: '' 10 | # running every hour 11 | schedule: 12 | - cron: '48 * * * *' 13 | 14 | 15 | permissions: 16 | actions: none 17 | attestations: none 18 | checks: none 19 | contents: write 20 | deployments: none 21 | id-token: none 22 | issues: none 23 | discussions: none 24 | packages: none 25 | pages: none 26 | pull-requests: none 27 | repository-projects: none 28 | security-events: none 29 | statuses: none 30 | 31 | 32 | env: 33 | REPO: ${{ github.repository }} 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | jobs: 37 | patchright-workflow: 38 | name: "Patchright Workflow: Install, Patch, Build and Publish Patchright Driver" 39 | runs-on: ubuntu-24.04 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - name: Configure Fast APT Mirror 44 | uses: vegardit/fast-apt-mirror.sh@v1 45 | 46 | - name: Setup Node v18 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: 18 50 | registry-url: 'https://registry.npmjs.org' 51 | 52 | - name: Install TS-Morph 53 | run: npm install 54 | 55 | - name: Check Release Version 56 | id: version_check 57 | run: | 58 | if [ -n "${{ github.event.inputs.version }}" ]; then 59 | echo "proceed=true" >>$GITHUB_OUTPUT 60 | echo "playwright_version=${{ github.event.inputs.version }}" >> $GITHUB_ENV 61 | else 62 | chmod +x utils/release_version_check.sh 63 | utils/release_version_check.sh 64 | fi 65 | 66 | - name: Install Playwright Driver 67 | if: steps.version_check.outputs.proceed == 'true' 68 | run: | 69 | git clone https://github.com/microsoft/playwright --branch ${{ env.playwright_version }} 70 | cd playwright 71 | npm ci 72 | 73 | - name: Patch Playwright Driver 74 | if: steps.version_check.outputs.proceed == 'true' 75 | run: | 76 | cd playwright 77 | node "../patchright_driver_patch.js" 78 | 79 | - name: Generate Playwright Channels 80 | if: steps.version_check.outputs.proceed == 'true' 81 | # Ignore the error exit code, as the script exits 1 when a file is modified. 82 | continue-on-error: true 83 | run: | 84 | cd playwright 85 | node utils/generate_channels.js 86 | 87 | - name: Build Patchright Driver 88 | if: steps.version_check.outputs.proceed == 'true' 89 | run: | 90 | cd playwright 91 | npm run build 92 | npx playwright install-deps 93 | chmod +x utils/build/build-playwright-driver.sh 94 | utils/build/build-playwright-driver.sh 95 | 96 | - name: Publish Patchright Driver 97 | if: steps.version_check.outputs.proceed == 'true' 98 | run: | 99 | chmod +x utils/release_driver.sh 100 | utils/release_driver.sh 101 | -------------------------------------------------------------------------------- /.github/workflows/patch_release_workflow.yml: -------------------------------------------------------------------------------- 1 | name: Trigger on Commit and Manual Request 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | permissions: 9 | actions: none 10 | attestations: none 11 | checks: none 12 | contents: write 13 | deployments: none 14 | id-token: none 15 | issues: none 16 | discussions: none 17 | packages: none 18 | pages: none 19 | pull-requests: none 20 | repository-projects: none 21 | security-events: none 22 | statuses: none 23 | 24 | jobs: 25 | run_action: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Configure Fast APT Mirror 31 | uses: vegardit/fast-apt-mirror.sh@v1 32 | 33 | - name: Setup Node v18 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 18 37 | registry-url: 'https://registry.npmjs.org' 38 | 39 | - name: Install TS-Morph 40 | run: npm install 41 | 42 | - name: Get Commit Author 43 | id: get_author 44 | run: | 45 | AUTHOR=$(git log -1 --pretty=format:'%an') 46 | echo "Commit author: $AUTHOR" 47 | echo "AUTHOR=$AUTHOR" >> $GITHUB_ENV 48 | 49 | - name: Get Original Commit Message 50 | id: get_commit_message 51 | run: | 52 | COMMIT_MESSAGE=$(git log -1 --pretty=format:'%s') 53 | echo "COMMIT_MESSAGE=$COMMIT_MESSAGE" >> $GITHUB_ENV 54 | 55 | - name: Skip if Commit is from GitHub Actions 56 | if: env.AUTHOR == 'github-actions' 57 | run: echo "Skipping action as the commit is from github-actions" && exit 0 58 | 59 | - name: Install Playwright Driver 60 | run: | 61 | response=$(curl --silent "https://api.github.com/repos/microsoft/playwright/releases/latest") 62 | version=$(echo "$response" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') 63 | git clone https://github.com/microsoft/playwright --branch "$version" 64 | cd playwright 65 | npm ci 66 | 67 | - name: Copy Driver Directory to prepare for Patch-Comparison 68 | run: | 69 | cp -r playwright playwright_not_patched 70 | 71 | - name: Patch Playwright Driver 72 | run: | 73 | cd playwright 74 | node "../patchright_driver_patch.js" 75 | 76 | - name: Rename Patched Directory 77 | run: | 78 | mv playwright patchright 79 | mv playwright_not_patched playwright 80 | 81 | - name: Compare Directories and Create Patch File 82 | run: | 83 | echo "# NOTE: This patch file is generated automatically and is not used, it is only for documentation. The driver is actually patched using [patchright_driver_patch](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/blob/main/patchright_driver_patch.js), see [the workflow](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/blob/main/.github/workflows/patchright_workflow.yml)" > patchright.patch 84 | diff -ruN playwright patchright | sed -E 's/^(---|\+\+\+) .*/\1/' >> patchright.patch || true 85 | 86 | - name: Commit and Push Patch File 87 | uses: stefanzweifel/git-auto-commit-action@v5 88 | with: 89 | commit_message: "[Patch-Comparison] Automatic Commit: ${{ env.COMMIT_MESSAGE }}" 90 | branch: "main" 91 | file_pattern: "patchright.patch" 92 | skip_fetch: true 93 | -------------------------------------------------------------------------------- /driver_patches/browserContextPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/browserContext.ts 5 | // ---------------------------- 6 | export function patchBrowserContext(project) { 7 | // Add source file to the project 8 | const browserContextSourceFile = project.addSourceFileAtPath( 9 | "packages/playwright-core/src/server/browserContext.ts", 10 | ); 11 | 12 | // ------- BrowserContext Class ------- 13 | const browserContextClass = browserContextSourceFile.getClass("BrowserContext"); 14 | 15 | // -- _initialize Method -- 16 | const initializeMethod = browserContextClass.getMethod("_initialize"); 17 | // Getting the service worker registration call 18 | const initializeMethodCall = initializeMethod 19 | .getDescendantsOfKind(SyntaxKind.CallExpression) 20 | .find((call) => { 21 | return ( 22 | call.getExpression().getText().includes("addInitScript") && 23 | call 24 | .getArguments() 25 | .some((arg) => 26 | arg.getText().includes("navigator.serviceWorker.register"), 27 | ) 28 | ); 29 | }); 30 | // Replace the service worker registration call with a custom one, which is less obvious 31 | initializeMethodCall 32 | .getArguments()[1] 33 | .replaceWithText("`navigator.serviceWorker.register = async () => { };`"); 34 | 35 | // -- exposeBinding Method -- 36 | const exposeBindingMethod = browserContextClass.getMethod("exposeBinding"); 37 | // Remove old loop and logic for localFrames and isolated world creation 38 | exposeBindingMethod.getStatements().forEach((statement) => { 39 | const text = statement.getText(); 40 | // Check if the statement matches the patterns 41 | if (text.includes("this.doAddInitScript(binding.initScript)")) 42 | statement.replaceWithText("await this.doExposeBinding(binding);"); 43 | else if ( 44 | text.includes("this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main')") || 45 | text.includes("this.exposePlaywrightBindingIfNeeded()") 46 | ) 47 | statement.remove(); 48 | }); 49 | 50 | // -- _removeExposedBindings Method -- 51 | const removeExposedBindingsMethod = browserContextClass.getMethod("removeExposedBindings"); 52 | removeExposedBindingsMethod.setBodyText(` 53 | for (const key of this._pageBindings.keys()) { 54 | if (!key.startsWith('__pw')) this._pageBindings.delete(key); 55 | } 56 | await this.doRemoveExposedBindings(); 57 | `); 58 | 59 | // -- _removeInitScripts Method -- 60 | const removeInitScriptsMethod = browserContextClass.getMethod("removeInitScripts"); 61 | removeInitScriptsMethod.setBodyText(` 62 | this.initScripts.splice(0, this.initScripts.length); 63 | await this.doRemoveInitScripts(); 64 | `); 65 | 66 | // Add focusControl Parameter 67 | const defaultContextParams = browserContextSourceFile.getVariableDeclaration("defaultNewContextParamValues"); 68 | const defaultContextExpression = defaultContextParams.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression); 69 | if (defaultContextExpression && !defaultContextExpression.getProperty("focusControl")) { 70 | defaultContextExpression.addPropertyAssignment({ 71 | name: "focusControl", 72 | initializer: "false", 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | .DS_Store 164 | 165 | # Node.js 166 | node_modules/ 167 | package-lock.json 168 | playwright/ 169 | -------------------------------------------------------------------------------- /patchright_driver_patch.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "path"; 3 | import { Project, SyntaxKind, IndentationText, ObjectLiteralExpression } from "ts-morph"; 4 | import YAML from "yaml"; 5 | 6 | import * as patches from "./driver_patches/index.js"; 7 | 8 | const project = new Project({ 9 | manipulationSettings: { 10 | indentationText: IndentationText.TwoSpaces, 11 | }, 12 | }); 13 | 14 | // ---------------------------- 15 | // server/browserContext.ts 16 | // ---------------------------- 17 | patches.patchBrowserContext(project); 18 | 19 | // ---------------------------- 20 | // server/chromium/chromium.ts 21 | // ---------------------------- 22 | patches.patchChromium(project); 23 | 24 | // ---------------------------- 25 | // server/chromium/chromiumSwitches.ts 26 | // ---------------------------- 27 | patches.patchChromiumSwitches(project); 28 | 29 | // ---------------------------- 30 | // server/chromium/crBrowser.ts 31 | // ---------------------------- 32 | patches.patchCRBrowser(project); 33 | 34 | // ---------------------------- 35 | // server/chromium/crDevTools.ts 36 | // ---------------------------- 37 | patches.patchCRDevTools(project); 38 | 39 | // ---------------------------- 40 | // server/chromium/crNetworkManager.ts 41 | // ---------------------------- 42 | patches.patchCRNetworkManager(project); 43 | 44 | // ---------------------------- 45 | // server/chromium/crServiceWorker.ts 46 | // ---------------------------- 47 | patches.patchCRServiceWorker(project); 48 | 49 | // ---------------------------- 50 | // server/frames.ts 51 | // ---------------------------- 52 | patches.patchFrames(project); 53 | 54 | // ---------------------------- 55 | // server/frameSelectors.ts 56 | // ---------------------------- 57 | patches.patchFrameSelectors(project); 58 | 59 | // ---------------------------- 60 | // server/chromium/crPage.ts 61 | // ---------------------------- 62 | patches.patchCRPage(project); 63 | 64 | // ---------------------------- 65 | // server/page.ts 66 | // ---------------------------- 67 | patches.patchPage(project); 68 | 69 | // ---------------------------- 70 | // utils/isomorphic/utilityScriptSerializers.ts 71 | // ---------------------------- 72 | patches.patchUtilityScriptSerializers(project); 73 | 74 | // ---------------------------- 75 | // server/pageBinding.ts 76 | // ---------------------------- 77 | patches.patchPageBinding(project); 78 | // ---------------------------- 79 | // server/clock.ts 80 | // ---------------------------- 81 | patches.patchClock(project); 82 | 83 | // ---------------------------- 84 | // server/javascript.ts 85 | // ---------------------------- 86 | patches.patchJavascript(project); 87 | 88 | // ---------------------------- 89 | // server/dispatchers/frameDispatcher.ts 90 | // ---------------------------- 91 | patches.patchFrameDispatcher(project); 92 | 93 | // ---------------------------- 94 | // server/dispatchers/jsHandleDispatcher.ts 95 | // ---------------------------- 96 | patches.patchJSHandleDispatcher(project); 97 | 98 | // ---------------------------- 99 | // server/dispatchers/pageDispatcher.ts 100 | // ---------------------------- 101 | patches.patchPageDispatcher(project); 102 | 103 | // ---------------------------- 104 | // injected/src/xpathSelectorEngine.ts 105 | // ---------------------------- 106 | patches.patchXPathSelectorEngine(project); 107 | 108 | // ---------------------------- 109 | // server/registry/index.ts 110 | // ---------------------------- 111 | patches.patchServerRegistryIndex(project); 112 | 113 | // ---------------------------- 114 | // recorder/src/recorder.tsx 115 | // ---------------------------- 116 | patches.patchRecorder(project); 117 | 118 | // Save the changes without reformatting 119 | project.saveSync(); 120 | 121 | // ---------------------------- 122 | // protocol/protocol.yml 123 | // ---------------------------- 124 | // isolatedContext parameters 125 | const protocol = YAML.parse(await fs.readFile("packages/protocol/src/protocol.yml", "utf8")); 126 | for (const type of ["Frame", "JSHandle", "Worker"]) { 127 | const commands = protocol[type].commands; 128 | commands.evaluateExpression.parameters.isolatedContext = "boolean?"; 129 | commands.evaluateExpressionHandle.parameters.isolatedContext = "boolean?"; 130 | } 131 | protocol["Frame"].commands.evalOnSelectorAll.parameters.isolatedContext = "boolean?"; 132 | // focusControl parameter 133 | protocol["ContextOptions"].properties.focusControl = "boolean?"; 134 | 135 | await fs.writeFile("packages/protocol/src/protocol.yml", YAML.stringify(protocol)); 136 | -------------------------------------------------------------------------------- /driver_patches/pageBindingPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/pageBinding.ts 5 | // ---------------------------- 6 | export function patchPageBinding(project) { 7 | // Content is modified from https://raw.githubusercontent.com/microsoft/playwright/471930b1ceae03c9e66e0eb80c1364a1a788e7db/packages/playwright-core/src/server/pageBinding.ts 8 | const pageBindingSourceContent = ` 9 | /** 10 | * Copyright (c) Microsoft Corporation. 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | import { source } from '../utils/isomorphic/oldUtilityScriptSerializers'; 25 | 26 | import type { SerializedValue } from '../utils/isomorphic/oldUtilityScriptSerializers'; 27 | 28 | export type BindingPayload = { 29 | name: string; 30 | seq: number; 31 | serializedArgs?: SerializedValue[], 32 | }; 33 | 34 | function addPageBinding(bindingName: string, needsHandle: boolean, utilityScriptSerializersFactory: typeof source) { 35 | const { serializeAsCallArgument } = utilityScriptSerializersFactory; 36 | // eslint-disable-next-line no-restricted-globals 37 | const binding = (globalThis as any)[bindingName]; 38 | if (!binding || binding.toString().startsWith("(...args) => {")) return 39 | // eslint-disable-next-line no-restricted-globals 40 | (globalThis as any)[bindingName] = (...args: any[]) => { 41 | // eslint-disable-next-line no-restricted-globals 42 | const me = (globalThis as any)[bindingName]; 43 | if (needsHandle && args.slice(1).some(arg => arg !== undefined)) 44 | throw new Error(\`exposeBindingHandle supports a single argument, \${args.length} received\`); 45 | let callbacks = me['callbacks']; 46 | if (!callbacks) { 47 | callbacks = new Map(); 48 | me['callbacks'] = callbacks; 49 | } 50 | const seq: number = (me['lastSeq'] || 0) + 1; 51 | me['lastSeq'] = seq; 52 | let handles = me['handles']; 53 | if (!handles) { 54 | handles = new Map(); 55 | me['handles'] = handles; 56 | } 57 | const promise = new Promise((resolve, reject) => callbacks.set(seq, { resolve, reject })); 58 | let payload: BindingPayload; 59 | if (needsHandle) { 60 | handles.set(seq, args[0]); 61 | payload = { name: bindingName, seq }; 62 | } else { 63 | const serializedArgs = []; 64 | for (let i = 0; i < args.length; i++) { 65 | serializedArgs[i] = serializeAsCallArgument(args[i], v => { 66 | return { fallThrough: v }; 67 | }); 68 | } 69 | payload = { name: bindingName, seq, serializedArgs }; 70 | } 71 | binding(JSON.stringify(payload)); 72 | return promise; 73 | }; 74 | // eslint-disable-next-line no-restricted-globals 75 | } 76 | 77 | export function takeBindingHandle(arg: { name: string, seq: number }) { 78 | // eslint-disable-next-line no-restricted-globals 79 | const handles = (globalThis as any)[arg.name]['handles']; 80 | const handle = handles.get(arg.seq); 81 | handles.delete(arg.seq); 82 | return handle; 83 | } 84 | 85 | export function deliverBindingResult(arg: { name: string, seq: number, result?: any, error?: any }) { 86 | // eslint-disable-next-line no-restricted-globals 87 | const callbacks = (globalThis as any)[arg.name]['callbacks']; 88 | if ('error' in arg) 89 | callbacks.get(arg.seq).reject(arg.error); 90 | else 91 | callbacks.get(arg.seq).resolve(arg.result); 92 | callbacks.delete(arg.seq); 93 | } 94 | 95 | export function createPageBindingScript(name: string, needsHandle: boolean) { 96 | return \`(\${addPageBinding.toString()})(\${JSON.stringify(name)}, \${needsHandle}, (\${source})())\`; 97 | } 98 | `; 99 | 100 | const pageBindingSourceFile = project.createSourceFile("packages/playwright-core/src/server/pageBinding.ts", pageBindingSourceContent, { overwrite: true }); 101 | } -------------------------------------------------------------------------------- /driver_patches/pagePatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/page.ts 5 | // ---------------------------- 6 | export function patchPage(project) { 7 | // Add source file to the project 8 | const pageSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/page.ts"); 9 | // Add the custom import and comment at the start of the file 10 | pageSourceFile.insertStatements(0, [ 11 | "// patchright - custom imports", 12 | "import { createPageBindingScript, deliverBindingResult, takeBindingHandle } from './pageBinding';", 13 | "", 14 | ]); 15 | 16 | // ------- Page Class ------- 17 | const pageClass = pageSourceFile.getClass("Page"); 18 | 19 | // -- exposeBinding Method -- 20 | const pageExposeBindingMethod = pageClass.getMethod("exposeBinding"); 21 | pageExposeBindingMethod.setBodyText(` 22 | if (this._pageBindings.has(name)) 23 | throw new Error(\`Function "\${name}" has been already registered\`); 24 | if (this.browserContext._pageBindings.has(name)) 25 | throw new Error(\`Function "\${name}" has been already registered in the browser context\`); 26 | const binding = new PageBinding(name, playwrightBinding, needsHandle); 27 | this._pageBindings.set(name, binding); 28 | await this.delegate.exposeBinding(binding); 29 | `); 30 | 31 | // -- _removeExposedBindings Method -- 32 | const pageRemoveExposedBindingsMethod = pageClass.getMethod("removeExposedBindings"); 33 | pageRemoveExposedBindingsMethod.setBodyText(` 34 | for (const key of this._pageBindings.keys()) { 35 | if (!key.startsWith('__pw')) 36 | this._pageBindings.delete(key); 37 | } 38 | await this.delegate.removeExposedBindings(); 39 | `); 40 | 41 | // -- _removeInitScripts Method -- 42 | const pageRemoveInitScriptsMethod = pageClass.getMethod("removeInitScripts"); 43 | pageRemoveInitScriptsMethod.setBodyText(` 44 | this.initScripts.splice(0, this.initScripts.length); 45 | await this.delegate.removeInitScripts(); 46 | `); 47 | 48 | // -- allInitScripts Method -- 49 | pageClass.getMethod("allInitScripts").remove(); 50 | 51 | // -- allBindings Method -- 52 | pageClass.addMethod({ 53 | name: "allBindings", 54 | }); 55 | const allBindingsMethod = pageClass.getMethod("allBindings"); 56 | allBindingsMethod.setBodyText(` 57 | return [...this.browserContext._pageBindings.values(), ...this._pageBindings.values()]; 58 | `); 59 | 60 | 61 | // ------- PageBinding Class ------- 62 | const pageBindingClass = pageSourceFile.getClass("PageBinding"); 63 | // Content modified from https://raw.githubusercontent.com/microsoft/playwright/471930b1ceae03c9e66e0eb80c1364a1a788e7db/packages/playwright-core/src/server/page.ts 64 | pageBindingClass.replaceWithText(` 65 | export class PageBinding { 66 | readonly source: string; 67 | readonly name: string; 68 | readonly playwrightFunction: frames.FunctionWithSource; 69 | readonly needsHandle: boolean; 70 | readonly internal: boolean; 71 | 72 | constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) { 73 | this.name = name; 74 | this.playwrightFunction = playwrightFunction; 75 | this.source = createPageBindingScript(name, needsHandle); 76 | this.needsHandle = needsHandle; 77 | } 78 | 79 | static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) { 80 | const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload; 81 | try { 82 | assert(context.world); 83 | const binding = page.getBinding(name); 84 | if (!binding) 85 | throw new Error(\`Function "\${name}" is not exposed\`); 86 | let result: any; 87 | if (binding.needsHandle) { 88 | const handle = await context.evaluateHandle(takeBindingHandle, { name, seq }).catch(e => null); 89 | result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, handle); 90 | } else { 91 | if (!Array.isArray(serializedArgs)) 92 | throw new Error(\`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly\`); 93 | const args = serializedArgs!.map(a => parseEvaluationResultValue(a)); 94 | result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args); 95 | } 96 | context.evaluate(deliverBindingResult, { name, seq, result }).catch(e => debugLogger.log('error', e)); 97 | } catch (error) { 98 | context.evaluate(deliverBindingResult, { name, seq, error }).catch(e => debugLogger.log('error', e)); 99 | } 100 | } 101 | } 102 | `); 103 | 104 | // ------- InitScript Class ------- 105 | const initScriptClass = pageSourceFile.getClass("InitScript"); 106 | // -- InitScript Constructor -- 107 | const initScriptConstructor = initScriptClass.getConstructors()[0]; 108 | const initScriptConstructorAssignment = initScriptConstructor 109 | .getBody() 110 | ?.getStatements() 111 | .find( 112 | (statement) => 113 | statement.getKind() === SyntaxKind.ExpressionStatement && 114 | statement.getText().includes("this.source = `(() => {"), 115 | ); 116 | // Remove unnecessary, detectable code from the constructor 117 | if (initScriptConstructorAssignment) { 118 | initScriptConstructorAssignment.replaceWithText(` 119 | this.source = \`(() => { \${source} })();\`; 120 | `); 121 | } 122 | 123 | // ------- Worker Class ------- 124 | const workerClass = pageSourceFile.getClass("Worker"); 125 | // -- evaluateExpression Method -- 126 | const workerEvaluateExpressionMethod = workerClass.getMethod("evaluateExpression"); 127 | workerEvaluateExpressionMethod.addParameter({ 128 | name: "isolatedContext", 129 | type: "boolean", 130 | hasQuestionToken: true, 131 | }); 132 | const workerEvaluateExpressionMethodBody = workerEvaluateExpressionMethod.getBody(); 133 | workerEvaluateExpressionMethodBody.replaceWithText( 134 | workerEvaluateExpressionMethodBody.getText().replace(/await this\._executionContextPromise/g, "context") 135 | ); 136 | // Insert the new line of code after the responseAwaitStatement 137 | workerEvaluateExpressionMethodBody.insertStatements(0, ` 138 | let context = await this._executionContextPromise; 139 | if (context.constructor.name === "FrameExecutionContext") { 140 | const frame = context.frame; 141 | if (frame) { 142 | if (isolatedContext) context = await frame._utilityContext(); 143 | else if (!isolatedContext) context = await frame._mainContext(); 144 | } 145 | } 146 | `); 147 | // -- evaluateExpressionHandle Method -- 148 | const workerEvaluateExpressionHandleMethod = workerClass.getMethod("evaluateExpressionHandle"); 149 | workerEvaluateExpressionHandleMethod.addParameter({ 150 | name: "isolatedContext", 151 | type: "boolean", 152 | hasQuestionToken: true, 153 | }); 154 | const workerEvaluateExpressionHandleMethodBody = workerEvaluateExpressionHandleMethod.getBody(); 155 | workerEvaluateExpressionHandleMethodBody.replaceWithText( 156 | workerEvaluateExpressionHandleMethodBody.getText().replace(/await this\._executionContextPromise/g, "context") 157 | ); 158 | // Insert the new line of code after the responseAwaitStatement 159 | workerEvaluateExpressionHandleMethodBody.insertStatements(0, ` 160 | let context = await this._executionContextPromise; 161 | if (context.constructor.name === "FrameExecutionContext") { 162 | const frame = this._context.frame; 163 | if (frame) { 164 | if (isolatedContext) context = await frame._utilityContext(); 165 | else if (!isolatedContext) context = await frame._mainContext(); 166 | } 167 | } 168 | `); 169 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 🎭 Patchright 3 |

4 | 5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Patchright Version 21 | 22 |
23 | 24 | 25 | 26 | 27 | Python Downloads 28 | 29 | 30 | 31 | 32 | 33 | NodeJS Downloads 34 | 35 | 36 | 37 | 38 | 39 | .Net Downloads 40 | 41 |

42 | 43 | #### Patchright is a patched and undetected version of the Playwright Testing and Automation Framework.
It can be used as a drop-in replacement for Playwright. 44 | 45 | > [!NOTE] 46 | > This repository serves the Patchright Driver. To use Patchright, check out the [Python Package](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python), the [NodeJS Package](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-nodejs) or the _community-driven_ [.Net Package](https://github.com/DevEnterpriseSoftware/patchright-dotnet/) 47 | 48 | > [!IMPORTANT] 49 | > Patchright only patches CHROMIUM based browsers. Firefox and Webkit are not supported. 50 | 51 | --- 52 | 53 |
54 |

Sponsors

55 | 56 | Hyperbrowser Banner Ad 57 |
58 | If you’re looking for a high-performance browser automation platform checkout [**Hyperbrowser**](https://hyperbrowser.ai/). It’s ideal for AI Agents, large-scale web scraping and automated testing.
59 | [**Hyperbrowser**](https://hyperbrowser.ai/) delivers cloud-based browser infrastructure that scales instantly from a few sessions to thousands, with built-in CAPTCHA solving, stealth fingerprinting, and a global proxy network. It integrates seamlessly with Puppeteer, Playwright, and Selenium, so you can be up and running in minutes, no server or proxy management required. 60 | 61 | Key Features: 62 | - **Instant scaling**: Launch 1,000+ concurrent browsers 63 | - **Captcha Solving**: Handles reCAPTCHA, Cloudflare, AWS, and more 64 | - **Stealth mode**: Dynamic, human-like fingerprints for undetectable automation 65 | - **Global proxy network**: 170+ countries with rotation and geo-targeting 66 | - **Built-in debugging**: Live view and session replay for real-time monitoring 67 | - **1-line integration**: Works with Puppeteer, Playwright, Selenium in Node.js or Python 68 | 69 | 👉 Learn more at [**hyperbrowser.ai**](https://hyperbrowser.ai/) 70 | 71 | --- 72 | 73 | 74 | Thordata Banner 75 | 76 | 77 | [Thordata](https://www.thordata.com/?ls=github&lk=Vinyzu) - Your First Plan is on Us! 💰Get 100% of your first residential proxy purchase back as wallet balance, up to $900. 78 | ### **⚡ Why Thordata?** 79 | 🌍 190+ real residential & ISP IP locations\ 80 | 🔐 Fully encrypted, ultra-secure connections\ 81 | 🚀 Optimized for web scraping, ad verification & automation workflows 82 | 83 | 🔥Don’t wait — this is your **best time to start** with [Thordata](https://www.thordata.com/?ls=github&lk=Vinyzu) and experience the safest, fastest proxy network. 84 | 85 |
86 | 87 | --- 88 | 89 | ## Patches 90 | 91 | ### [Runtime.enable](https://vanilla.aslushnikov.com/?Runtime.enable) Leak 92 | This is the biggest Patch Patchright uses. To avoid detection by this leak, patchright avoids using [Runtime.enable](https://vanilla.aslushnikov.com/?Runtime.enable) by executing Javascript in (isolated) ExecutionContexts. 93 | 94 | ### [Console.enable](https://vanilla.aslushnikov.com/?Console.enable) Leak 95 | Patchright patches this leak by disabling the Console API all together. This means, console functionality will not work in Patchright. If you really need the console, you might be better off using Javascript loggers, although they also can be easily detected. 96 | 97 | ### Command Flags Leaks 98 | Patchright tweaks the Playwright Default Args to avoid detection by Command Flag Leaks. This (most importantly) affects: 99 | - `--disable-blink-features=AutomationControlled` (added) to avoid navigator.webdriver detection. 100 | - `--enable-automation` (removed) to avoid navigator.webdriver detection. 101 | - `--disable-popup-blocking` (removed) to avoid popup crashing. 102 | - `--disable-component-update` (removed) to avoid detection as a Stealth Driver. 103 | - `--disable-default-apps` (removed) to enable default apps. 104 | - `--disable-extensions` (removed) to enable extensions 105 | 106 | ### General Leaks 107 | Patchright patches some general leaks in the Playwright codebase. This mainly includes poor setups and obvious detection points. 108 | 109 | ### Closed Shadow Roots 110 | Patchright is able to interact with elements in Closed Shadow Roots. Just use normal locators and Patchright will do the rest. 111 |
112 | Patchright is now also able to use XPaths in Closed Shadow Roots. 113 | 114 | --- 115 | 116 | ## Stealth 117 | 118 | With the right setup, Patchright currently is considered undetectable. 119 | Patchright passes: 120 | - [Brotector](https://kaliiiiiiiiii.github.io/brotector/) ✅ (with [CDP-Patches](https://github.com/Kaliiiiiiiiii-Vinyzu/CDP-Patches/)) 121 | - [Cloudflare](https://cloudflare.com/) ✅ 122 | - [Kasada](https://www.kasada.io/) ✅ 123 | - [Akamai](https://www.akamai.com/products/bot-manager/) ✅ 124 | - [Shape/F5](https://www.f5.com/) ✅ 125 | - [Bet365](https://bet365.com/) ✅ 126 | - [Datadome](https://datadome.co/products/bot-protection/) ✅ 127 | - [Fingerprint.com](https://fingerprint.com/products/bot-detection/) ✅ 128 | - [CreepJS](https://abrahamjuliot.github.io/creepjs/) ✅ 129 | - [Sannysoft](https://bot.sannysoft.com/) ✅ 130 | - [Incolumitas](https://bot.incolumitas.com/) ✅ 131 | - [IPHey](https://iphey.com/) ✅ 132 | - [Browserscan](https://browserscan.net/) ✅ 133 | - [Pixelscan](https://pixelscan.net/) ✅ 134 | 135 | --- 136 | 137 | ## Bugs 138 | #### Even though we have spent a lot of time to make Patchright as stable as possible, bugs may still occur. If you encounter any bugs, please report them in the [Issues](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/issues). 139 | #### Patchright is now tested against the Playwright Tests after every release. See [here](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python/actions/workflows/patchright_tests.yml) 140 | 141 | > [!WARNING] 142 | > Patchright passes most, but not all the Playwright tests. Some bugs are considered impossible to solve, some are just not relevant. See the list of bugs and their explanation [here](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/issues/30). 143 | 144 | #### Based on the Playwright Tests, we concluded that its highly unlikely that you will be affected by these bugs in regular usecases. 145 | 146 |
147 | Init Script Shenanigans 148 | 149 | ### Explanation 150 | To be able to use InitScripts without [Runtime.enable](https://vanilla.aslushnikov.com/?Runtime.enable), Patchright uses Playwright Routes to inject JavaScript into HTML requests. 151 | 152 | ### Bugs 153 | Playwright Routes may cause some bugs in other parts of your code. Patchright InitScripts won't cause any bugs that wouldn't be caused by normal Playwright Routes.
If you want any of these bugs fixed, you'll have to contact the Playwright team. 154 | 155 | ### Leaks 156 | Patchright InitScripts can be detected by Timing Attacks. However, no antibot currently checks for this kind of Timing Attack and they probably won't for a good amount of time.
We consider them not to be a big risk of detection. 157 | 158 |
159 | 160 | --- 161 | 162 | ### TODO 163 | - [x] Implement Option to choose Execution Context (Main/Isolated). 164 | - [x] Fix Fixable Bugs. 165 | - [x] Implement .patch Updater to easily show Patchright's patches. 166 | - [x] Setup Automated Testing on new Release. 167 | - [x] Implement Patchright on .NET. 168 | - [ ] Implement Patchright on Java. 169 | 170 | --- 171 | 172 | ## Development 173 | 174 | Deployment of new Patchright versions are automatic, but bugs due to Playwright codebase changes may occur. Fixes for these bugs might take a few days to be released. 175 | 176 | --- 177 | 178 | ## Support our work 179 | 180 | If you choose to support our work, please contact [@vinyzu](https://discord.com/users/935224495126487150) or [@steve_abcdef](https://discord.com/users/936292409426477066) on Discord. 181 | 182 | --- 183 | 184 | ## Copyright and License 185 | © [Vinyzu](https://github.com/Vinyzu/) 186 | 187 | Patchright is licensed [Apache 2.0](https://choosealicense.com/licenses/apache-2.0/) 188 | 189 | [Some Parts](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/blob/main/patchright_driver_patch.js#L435-L448) of the Codebase are inspired by [Driverless](https://github.com/kaliiiiiiiiii/Selenium-Driverless). 190 | Thanks to [Nick Webson](https://github.com/rebrowser/rebrowser-patches) for the idea of .patch-File Documentation. 191 | 192 | --- 193 | 194 | ## Disclaimer 195 | 196 | This repository is provided for **educational purposes only**. \ 197 | No warranties are provided regarding accuracy, completeness, or suitability for any purpose. **Use at your own risk**—the authors and maintainers assume **no liability** for **any damages**, **legal issues**, or **warranty breaches** resulting from use, modification, or distribution of this code.\ 198 | **Any misuse or legal violations are the sole responsibility of the user**. 199 | 200 | --- 201 | 202 | ## Authors 203 | 204 | #### Active Maintainer: [Vinyzu](https://github.com/Vinyzu/)
Co-Maintainer: [Kaliiiiiiiiii](https://github.com/kaliiiiiiiiii/) 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Vinyzu 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /driver_patches/utilityScriptSerializersPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // utils/isomorphic/utilityScriptSerializers.ts 5 | // ---------------------------- 6 | export function patchUtilityScriptSerializers(project) { 7 | // Create oldUtilityScriptSerializers.ts with custom code for use in pageBinding.ts 8 | // Content is modified from https://raw.githubusercontent.com/microsoft/playwright/471930b1ceae03c9e66e0eb80c1364a1a788e7db/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts 9 | const oldUtilityScriptSerializerSourceContent = ` 10 | /** 11 | * Copyright (c) Microsoft Corporation. 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. 24 | */ 25 | 26 | type TypedArrayKind = 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64'; 27 | 28 | export type SerializedValue = 29 | undefined | boolean | number | string | 30 | { v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } | 31 | { d: string } | 32 | { u: string } | 33 | { bi: string } | 34 | { e: { n: string, m: string, s: string } } | 35 | { r: { p: string, f: string } } | 36 | { a: SerializedValue[], id: number } | 37 | { o: { k: string, v: SerializedValue }[], id: number } | 38 | { ref: number } | 39 | { h: number } | 40 | { ta: { b: string, k: TypedArrayKind } }; 41 | 42 | type HandleOrValue = { h: number } | { fallThrough: any }; 43 | 44 | type VisitorInfo = { 45 | visited: Map; 46 | lastId: number; 47 | }; 48 | 49 | export function source() { 50 | 51 | function isRegExp(obj: any): obj is RegExp { 52 | try { 53 | return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; 54 | } catch (error) { 55 | return false; 56 | } 57 | } 58 | 59 | function isDate(obj: any): obj is Date { 60 | try { 61 | return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]'; 62 | } catch (error) { 63 | return false; 64 | } 65 | } 66 | 67 | function isURL(obj: any): obj is URL { 68 | try { 69 | return obj instanceof URL || Object.prototype.toString.call(obj) === '[object URL]'; 70 | } catch (error) { 71 | return false; 72 | } 73 | } 74 | 75 | function isError(obj: any): obj is Error { 76 | try { 77 | return obj instanceof Error || (obj && Object.getPrototypeOf(obj)?.name === 'Error'); 78 | } catch (error) { 79 | return false; 80 | } 81 | } 82 | 83 | function isTypedArray(obj: any, constructor: Function): boolean { 84 | try { 85 | return obj instanceof constructor || Object.prototype.toString.call(obj) === \`[object \${constructor.name}]\`; 86 | } catch (error) { 87 | return false; 88 | } 89 | } 90 | 91 | const typedArrayConstructors: Record = { 92 | i8: Int8Array, 93 | ui8: Uint8Array, 94 | ui8c: Uint8ClampedArray, 95 | i16: Int16Array, 96 | ui16: Uint16Array, 97 | i32: Int32Array, 98 | ui32: Uint32Array, 99 | // TODO: add Float16Array once it's in baseline 100 | f32: Float32Array, 101 | f64: Float64Array, 102 | bi64: BigInt64Array, 103 | bui64: BigUint64Array, 104 | }; 105 | 106 | function typedArrayToBase64(array: any) { 107 | /** 108 | * Firefox does not support iterating over typed arrays, so we use \`.toBase64\`. 109 | * Error: 'Accessing TypedArray data over Xrays is slow, and forbidden in order to encourage performant code. To copy TypedArrays across origin boundaries, consider using Components.utils.cloneInto().' 110 | */ 111 | if ('toBase64' in array) 112 | return array.toBase64(); 113 | const binary = Array.from(new Uint8Array(array.buffer, array.byteOffset, array.byteLength)).map(b => String.fromCharCode(b)).join(''); 114 | return btoa(binary); 115 | } 116 | 117 | function base64ToTypedArray(base64: string, TypedArrayConstructor: any) { 118 | const binary = atob(base64); 119 | const bytes = new Uint8Array(binary.length); 120 | for (let i = 0; i < binary.length; i++) 121 | bytes[i] = binary.charCodeAt(i); 122 | return new TypedArrayConstructor(bytes.buffer); 123 | } 124 | 125 | function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map = new Map()): any { 126 | if (Object.is(value, undefined)) 127 | return undefined; 128 | if (typeof value === 'object' && value) { 129 | if ('ref' in value) 130 | return refs.get(value.ref); 131 | if ('v' in value) { 132 | if (value.v === 'undefined') 133 | return undefined; 134 | if (value.v === 'null') 135 | return null; 136 | if (value.v === 'NaN') 137 | return NaN; 138 | if (value.v === 'Infinity') 139 | return Infinity; 140 | if (value.v === '-Infinity') 141 | return -Infinity; 142 | if (value.v === '-0') 143 | return -0; 144 | return undefined; 145 | } 146 | if ('d' in value) 147 | return new Date(value.d); 148 | if ('u' in value) 149 | return new URL(value.u); 150 | if ('bi' in value) 151 | return BigInt(value.bi); 152 | if ('e' in value) { 153 | const error = new Error(value.e.m); 154 | error.name = value.e.n; 155 | error.stack = value.e.s; 156 | return error; 157 | } 158 | if ('r' in value) 159 | return new RegExp(value.r.p, value.r.f); 160 | if ('a' in value) { 161 | const result: any[] = []; 162 | refs.set(value.id, result); 163 | for (const a of value.a) 164 | result.push(parseEvaluationResultValue(a, handles, refs)); 165 | return result; 166 | } 167 | if ('o' in value) { 168 | const result: any = {}; 169 | refs.set(value.id, result); 170 | for (const { k, v } of value.o) 171 | result[k] = parseEvaluationResultValue(v, handles, refs); 172 | return result; 173 | } 174 | if ('h' in value) 175 | return handles[value.h]; 176 | if ('ta' in value) 177 | return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]); 178 | } 179 | return value; 180 | } 181 | 182 | function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue { 183 | return serialize(value, handleSerializer, { visited: new Map(), lastId: 0 }); 184 | } 185 | 186 | function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue { 187 | if (value && typeof value === 'object') { 188 | // eslint-disable-next-line no-restricted-globals 189 | if (typeof globalThis.Window === 'function' && value instanceof globalThis.Window) 190 | return 'ref: '; 191 | // eslint-disable-next-line no-restricted-globals 192 | if (typeof globalThis.Document === 'function' && value instanceof globalThis.Document) 193 | return 'ref: '; 194 | // eslint-disable-next-line no-restricted-globals 195 | if (typeof globalThis.Node === 'function' && value instanceof globalThis.Node) 196 | return 'ref: '; 197 | } 198 | return innerSerialize(value, handleSerializer, visitorInfo); 199 | } 200 | 201 | function innerSerialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue { 202 | const result = handleSerializer(value); 203 | if ('fallThrough' in result) 204 | value = result.fallThrough; 205 | else 206 | return result; 207 | 208 | if (typeof value === 'symbol') 209 | return { v: 'undefined' }; 210 | if (Object.is(value, undefined)) 211 | return { v: 'undefined' }; 212 | if (Object.is(value, null)) 213 | return { v: 'null' }; 214 | if (Object.is(value, NaN)) 215 | return { v: 'NaN' }; 216 | if (Object.is(value, Infinity)) 217 | return { v: 'Infinity' }; 218 | if (Object.is(value, -Infinity)) 219 | return { v: '-Infinity' }; 220 | if (Object.is(value, -0)) 221 | return { v: '-0' }; 222 | 223 | if (typeof value === 'boolean') 224 | return value; 225 | if (typeof value === 'number') 226 | return value; 227 | if (typeof value === 'string') 228 | return value; 229 | if (typeof value === 'bigint') 230 | return { bi: value.toString() }; 231 | 232 | if (isError(value)) { 233 | let stack; 234 | if (value.stack?.startsWith(value.name + ': ' + value.message)) { 235 | // v8 236 | stack = value.stack; 237 | } else { 238 | stack = \`\${value.name}: \${value.message}\n\${value.stack}\`; 239 | } 240 | return { e: { n: value.name, m: value.message, s: stack } }; 241 | } 242 | if (isDate(value)) 243 | return { d: value.toJSON() }; 244 | if (isURL(value)) 245 | return { u: value.toJSON() }; 246 | if (isRegExp(value)) 247 | return { r: { p: value.source, f: value.flags } }; 248 | for (const [k, ctor] of Object.entries(typedArrayConstructors) as [TypedArrayKind, Function][]) { 249 | if (isTypedArray(value, ctor)) 250 | return { ta: { b: typedArrayToBase64(value), k } }; 251 | } 252 | 253 | const id = visitorInfo.visited.get(value); 254 | if (id) 255 | return { ref: id }; 256 | 257 | if (Array.isArray(value)) { 258 | const a = []; 259 | const id = ++visitorInfo.lastId; 260 | visitorInfo.visited.set(value, id); 261 | for (let i = 0; i < value.length; ++i) 262 | a.push(serialize(value[i], handleSerializer, visitorInfo)); 263 | return { a, id }; 264 | } 265 | 266 | if (typeof value === 'object') { 267 | const o: { k: string, v: SerializedValue }[] = []; 268 | const id = ++visitorInfo.lastId; 269 | visitorInfo.visited.set(value, id); 270 | for (const name of Object.keys(value)) { 271 | let item; 272 | try { 273 | item = value[name]; 274 | } catch (e) { 275 | continue; // native bindings will throw sometimes 276 | } 277 | if (name === 'toJSON' && typeof item === 'function') 278 | o.push({ k: name, v: { o: [], id: 0 } }); 279 | else 280 | o.push({ k: name, v: serialize(item, handleSerializer, visitorInfo) }); 281 | } 282 | 283 | let jsonWrapper; 284 | try { 285 | // If Object.keys().length === 0 we fall back to toJSON if it exists 286 | if (o.length === 0 && value.toJSON && typeof value.toJSON === 'function') 287 | jsonWrapper = { value: value.toJSON() }; 288 | } catch (e) { 289 | } 290 | if (jsonWrapper) 291 | return innerSerialize(jsonWrapper.value, handleSerializer, visitorInfo); 292 | 293 | return { o, id }; 294 | } 295 | } 296 | 297 | return { parseEvaluationResultValue, serializeAsCallArgument }; 298 | } 299 | `; 300 | 301 | project.createSourceFile( 302 | "packages/playwright-core/src/utils/isomorphic/oldUtilityScriptSerializers.ts", 303 | oldUtilityScriptSerializerSourceContent, 304 | { overwrite: true } 305 | ); 306 | } -------------------------------------------------------------------------------- /driver_patches/frameSelectorsPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/frameSelectors.ts 5 | // ---------------------------- 6 | export function patchFrameSelectors(project) { 7 | // Add source file to the project 8 | const frameSelectorsSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/frameSelectors.ts"); 9 | // Add the custom import and comment at the start of the file 10 | frameSelectorsSourceFile.insertStatements(0, [ 11 | "// patchright - custom imports", 12 | "import { ElementHandle } from './dom';", 13 | "", 14 | ]); 15 | 16 | // ------- FrameSelectors Class ------- 17 | const frameSelectorsClass = frameSelectorsSourceFile.getClass("FrameSelectors"); 18 | 19 | // -- queryArrayInMainWorld Method -- 20 | const queryArrayInMainWorldMethod = frameSelectorsClass.getMethod("queryArrayInMainWorld"); 21 | queryArrayInMainWorldMethod.addParameter({ 22 | name: "isolatedContext", 23 | type: "boolean", 24 | hasQuestionToken: true, 25 | }); 26 | const queryArrayInMainWorldMethodCalls = queryArrayInMainWorldMethod.getDescendantsOfKind(SyntaxKind.CallExpression); 27 | for (const callExpr of queryArrayInMainWorldMethodCalls) { 28 | const exprText = callExpr.getExpression().getText(); 29 | if (exprText === "this.resolveInjectedForSelector") { 30 | const args = callExpr.getArguments(); 31 | if (args.length > 1 && args[1].getKind() === SyntaxKind.ObjectLiteralExpression) { 32 | const objLiteral = args[1]; 33 | 34 | const mainWorldProp = objLiteral.getProperty("mainWorld"); 35 | if (mainWorldProp && mainWorldProp.getText() === "mainWorld: true") { 36 | mainWorldProp.replaceWithText("mainWorld: !isolatedContext"); 37 | break; 38 | } 39 | } 40 | } 41 | } 42 | 43 | // -- resolveFrameForSelector Method -- 44 | const resolveFrameForSelectorMethod = frameSelectorsClass.getMethod("resolveFrameForSelector"); 45 | const constElementDeclaration = resolveFrameForSelectorMethod.getDescendantsOfKind(SyntaxKind.VariableStatement) 46 | .find(declaration => declaration.getText().includes("const element = handle.asElement()")); 47 | constElementDeclaration.setDeclarationKind("let"); 48 | 49 | const resolveFrameForSelectorIfStatement = resolveFrameForSelectorMethod.getDescendantsOfKind(SyntaxKind.IfStatement).find(statement => statement.getExpression().getText() === "!element" && statement.getThenStatement().getText() === "return null;"); 50 | resolveFrameForSelectorIfStatement.replaceWithText(` 51 | if (!element) { 52 | try { 53 | var client = frame._page.delegate._sessionForFrame(frame)._client; 54 | } catch (e) { 55 | var client = frame._page.delegate._mainFrameSession._client; 56 | } 57 | var mainContext = await frame._context("main"); 58 | const documentNode = await client.send("Runtime.evaluate", { 59 | expression: "document", 60 | serializationOptions: { 61 | serialization: "idOnly" 62 | }, 63 | contextId: mainContext.delegate._contextId 64 | }); 65 | const documentScope = new ElementHandle(mainContext, documentNode.result.objectId); 66 | var check = await this._customFindFramesByParsed(injectedScript, client, mainContext, documentScope, info.parsed); 67 | if (check.length > 0) { 68 | element = check[0]; 69 | } else { 70 | return null; 71 | } 72 | } 73 | `); 74 | 75 | // -- resolveInjectedForSelector Method -- 76 | const resolveInjectedForSelectorMethod = frameSelectorsClass.getMethod("resolveInjectedForSelector") 77 | const contextInjectedStatementIndex = resolveInjectedForSelectorMethod 78 | .getStatements() 79 | .findIndex(stmt => { 80 | const decl = stmt.asKind(SyntaxKind.VariableStatement)?.getDeclarations()[0]; 81 | 82 | const callExpr = decl 83 | ?.getInitializerIfKind(SyntaxKind.AwaitExpression) 84 | ?.getExpressionIfKind(SyntaxKind.CallExpression); 85 | 86 | return decl?.getName() === "injected" && callExpr?.getExpression().getText() === "context.injectedScript" 87 | }); 88 | console.error(contextInjectedStatementIndex) 89 | resolveInjectedForSelectorMethod.insertStatements( 90 | contextInjectedStatementIndex+1, 91 | `if (!context) throw new Error("Frame was detached");` 92 | ) 93 | 94 | 95 | // -- _customFindFramesByParsed Method -- 96 | frameSelectorsClass.addMethod({ 97 | name: "_customFindFramesByParsed", 98 | isAsync: true, 99 | parameters: [ 100 | { name: "injected" }, 101 | { name: "client" }, 102 | { name: "context" }, 103 | { name: "documentScope" }, 104 | { name: "parsed" }, 105 | ], 106 | }); 107 | const customFindFramesByParsedSelectorsMethod = frameSelectorsClass.getMethod("_customFindFramesByParsed"); 108 | customFindFramesByParsedSelectorsMethod.setBodyText(` 109 | var parsedEdits = { ...parsed }; 110 | var currentScopingElements = [documentScope]; 111 | while (parsed.parts.length > 0) { 112 | var part = parsed.parts.shift(); 113 | parsedEdits.parts = [part]; 114 | var elements = []; 115 | var elementsIndexes = []; 116 | if (part.name == "nth") { 117 | const partNth = Number(part.body); 118 | if (partNth > currentScopingElements.length || partNth < -currentScopingElements.length) { 119 | return continuePolling; 120 | } else { 121 | currentScopingElements = [currentScopingElements.at(partNth)]; 122 | continue; 123 | } 124 | } else if (part.name == "internal:or") { 125 | var orredElements = await this._customFindFramesByParsed(injected, client, context, documentScope, part.body.parsed); 126 | elements = currentScopingElements.concat(orredElements); 127 | } else if (part.name == "internal:and") { 128 | var andedElements = await this._customFindFramesByParsed(injected, client, context, documentScope, part.body.parsed); 129 | const backendNodeIds = new Set(andedElements.map((item) => item.backendNodeId)); 130 | elements = currentScopingElements.filter((item) => backendNodeIds.has(item.backendNodeId)); 131 | } else { 132 | for (const scope of currentScopingElements) { 133 | const describedScope = await client.send("DOM.describeNode", { 134 | objectId: scope._objectId, 135 | depth: -1, 136 | pierce: true 137 | }); 138 | var queryingElements = []; 139 | let findClosedShadowRoots2 = function(node, results = []) { 140 | if (!node || typeof node !== "object") return results; 141 | if (node.shadowRoots && Array.isArray(node.shadowRoots)) { 142 | for (const shadowRoot2 of node.shadowRoots) { 143 | if (shadowRoot2.shadowRootType === "closed" && shadowRoot2.backendNodeId) { 144 | results.push(shadowRoot2.backendNodeId); 145 | } 146 | findClosedShadowRoots2(shadowRoot2, results); 147 | } 148 | } 149 | if (node.nodeName !== "IFRAME" && node.children && Array.isArray(node.children)) { 150 | for (const child of node.children) { 151 | findClosedShadowRoots2(child, results); 152 | } 153 | } 154 | return results; 155 | }; 156 | var findClosedShadowRoots = findClosedShadowRoots2; 157 | var shadowRootBackendIds = findClosedShadowRoots2(describedScope.node); 158 | var shadowRoots = []; 159 | for (var shadowRootBackendId of shadowRootBackendIds) { 160 | var resolvedShadowRoot = await client.send("DOM.resolveNode", { 161 | backendNodeId: shadowRootBackendId, 162 | contextId: context.delegate._contextId 163 | }); 164 | shadowRoots.push(new ElementHandle(context, resolvedShadowRoot.object.objectId)); 165 | } 166 | for (var shadowRoot of shadowRoots) { 167 | const shadowElements = await shadowRoot.evaluateHandleInUtility(([injected, node, { parsed: parsed2 }]) => { 168 | const elements2 = injected.querySelectorAll(parsed2, node); 169 | return elements2; 170 | }, { 171 | parsed: parsedEdits, 172 | }); 173 | const shadowElementsAmount = await shadowElements.getProperty("length"); 174 | queryingElements.push([shadowElements, shadowElementsAmount, shadowRoot]); 175 | } 176 | const rootElements = await scope.evaluateHandleInUtility(([injected, node, { parsed: parsed2 }]) => { 177 | const elements2 = injected.querySelectorAll(parsed2, node); 178 | return elements2; 179 | }, { 180 | parsed: parsedEdits 181 | }); 182 | const rootElementsAmount = await rootElements.getProperty("length"); 183 | queryingElements.push([rootElements, rootElementsAmount, injected]); 184 | for (var queryedElement of queryingElements) { 185 | var elementsToCheck = queryedElement[0]; 186 | var elementsAmount = await queryedElement[1].jsonValue(); 187 | var parentNode = queryedElement[2]; 188 | for (var i = 0; i < elementsAmount; i++) { 189 | if (parentNode.constructor.name == "ElementHandle") { 190 | var elementToCheck = await parentNode.evaluateHandleInUtility(([injected, node, { index, elementsToCheck: elementsToCheck2 }]) => { 191 | return elementsToCheck2[index]; 192 | }, { index: i, elementsToCheck }); 193 | } else { 194 | var elementToCheck = await parentNode.evaluateHandle((injected, { index, elementsToCheck: elementsToCheck2 }) => { 195 | return elementsToCheck2[index]; 196 | }, { index: i, elementsToCheck }); 197 | } 198 | elementToCheck.parentNode = parentNode; 199 | var resolvedElement = await client.send("DOM.describeNode", { 200 | objectId: elementToCheck._objectId, 201 | depth: -1 202 | }); 203 | elementToCheck.backendNodeId = resolvedElement.node.backendNodeId; 204 | elementToCheck.nodePosition = this._findElementPositionInDomTree(elementToCheck, describedScope.node, context, ""); 205 | elements.push(elementToCheck); 206 | } 207 | } 208 | } 209 | } 210 | // Sorting elements by their nodePosition, which is a index to the Element in the DOM tree 211 | const getParts = (pos) => (pos?.match(/../g) || []).map(Number); 212 | elements.sort((a, b) => { 213 | const partA = getParts(a.nodePosition); 214 | const partB = getParts(b.nodePosition); 215 | const maxLength = Math.max(partA.length, partB.length); 216 | 217 | for (let i = 0; i < maxLength; i++) { 218 | const aVal = partA[i] ?? -1; 219 | const bVal = partB[i] ?? -1; 220 | if (aVal !== bVal) return aVal - bVal; 221 | } 222 | return 0; 223 | }); 224 | 225 | // Remove duplicates by nodePosition, keeping the first occurrence 226 | currentScopingElements = Array.from( 227 | new Map(elements.map(e => [e.nodePosition, e])).values() 228 | ); 229 | } 230 | return currentScopingElements; 231 | `); 232 | 233 | // -- _findElementPositionInDomTree Method -- 234 | frameSelectorsClass.addMethod({ 235 | name: "_findElementPositionInDomTree", 236 | isAsync: false, 237 | parameters: [ 238 | { name: "element" }, 239 | { name: "queryingElement" }, 240 | { name: "documentScope" }, 241 | { name: "currentIndex" }, 242 | ], 243 | }); 244 | const findElementPositionInDomTreeMethod = frameSelectorsClass.getMethod("_findElementPositionInDomTree"); 245 | findElementPositionInDomTreeMethod.setBodyText(` 246 | // Get Element Position in DOM Tree by Indexing it via their children indexes, like a search tree index 247 | // Check if backendNodeId matches, if so, return currentIndex 248 | if (element.backendNodeId === queryingElement.backendNodeId) { 249 | return currentIndex; 250 | } 251 | // Iterating through children of queryingElement 252 | for (const child of queryingElement.children || []) { 253 | // Getting index of child in queryingElement's children 254 | const childrenNodeIndex = queryingElement.children.indexOf(child); 255 | // Further querying the child recursively and appending the children index to the currentIndex 256 | const childIndex = this._findElementPositionInDomTree(element, child, documentScope, currentIndex + "." + childrenNodeIndex.toString()); 257 | if (childIndex !== null) return childIndex; 258 | } 259 | if (queryingElement.shadowRoots && Array.isArray(queryingElement.shadowRoots)) { 260 | // Basically same for CSRs, but we dont have to append its index because patchright treats CSRs like they dont exist 261 | for (const shadowRoot of queryingElement.shadowRoots) { 262 | if (shadowRoot.shadowRootType === "closed" && shadowRoot.backendNodeId) { 263 | const shadowRootHandle = new ElementHandle(documentScope, shadowRoot.backendNodeId); 264 | const childIndex = this._findElementPositionInDomTree(element, shadowRootHandle, documentScope, currentIndex); 265 | if (childIndex !== null) return childIndex; 266 | } 267 | } 268 | } 269 | return null; 270 | `); 271 | } -------------------------------------------------------------------------------- /driver_patches/crNetworkManagerPatch.js: -------------------------------------------------------------------------------- 1 | import { Project, SyntaxKind } from "ts-morph"; 2 | 3 | // ---------------------------- 4 | // server/chromium/crNetworkManager.ts 5 | // ---------------------------- 6 | export function patchCRNetworkManager(project) { 7 | // Add source file to the project 8 | const crNetworkManagerSourceFile = project.addSourceFileAtPath("packages/playwright-core/src/server/chromium/crNetworkManager.ts"); 9 | // Add the custom import and comment at the start of the file 10 | crNetworkManagerSourceFile.insertStatements(0, [ 11 | "// patchright - custom imports", 12 | "import crypto from 'crypto';", 13 | "", 14 | ]); 15 | 16 | // ------- CRNetworkManager Class ------- 17 | const crNetworkManagerClass = crNetworkManagerSourceFile.getClass("CRNetworkManager"); 18 | crNetworkManagerClass.addProperties([ 19 | { 20 | name: "_alreadyTrackedNetworkIds", 21 | type: "Set", 22 | initializer: "new Set()", 23 | }, 24 | ]); 25 | 26 | // -- _onRequest Method -- 27 | const onRequestMethod = crNetworkManagerClass.getMethod("_onRequest"); 28 | // Find the assignment statement you want to modify 29 | const routeAssignment = onRequestMethod 30 | .getDescendantsOfKind(SyntaxKind.BinaryExpression) 31 | .find((expr) => 32 | expr 33 | .getText() 34 | .includes( 35 | "route = new RouteImpl(requestPausedSessionInfo!.session, requestPausedEvent.requestId)", 36 | ), 37 | ); 38 | // Adding new parameter to the RouteImpl call 39 | if (routeAssignment) { 40 | routeAssignment 41 | .getRight() 42 | .replaceWithText( 43 | "new RouteImpl(requestPausedSessionInfo!.session, requestPausedEvent.requestId, this._page, requestPausedEvent.networkId, this)", 44 | ); 45 | } 46 | 47 | // -- _updateProtocolRequestInterceptionForSession Method -- 48 | const updateProtocolRequestInterceptionForSessionMethod = crNetworkManagerClass.getMethod("_updateProtocolRequestInterceptionForSession"); 49 | // Remove old loop and logic for localFrames and isolated world creation 50 | updateProtocolRequestInterceptionForSessionMethod.getStatements().forEach((statement) => { 51 | const text = statement.getText(); 52 | // Check if the statement matches the patterns 53 | if (text.includes('const cachePromise = info.session.send(\'Network.setCacheDisabled\', { cacheDisabled: enabled });')) 54 | statement.replaceWithText('const cachePromise = info.session.send(\'Network.setCacheDisabled\', { cacheDisabled: false });'); 55 | }); 56 | 57 | // -- _handleRequestRedirect Method -- 58 | //const handleRequestRedirectMethod = crNetworkManagerClass.getMethod("_handleRequestRedirect"); 59 | //handleRequestRedirectMethod.setBodyText('return;') 60 | 61 | // -- _onRequest Method -- 62 | const crOnRequestMethod = crNetworkManagerClass.getMethod("_onRequest"); 63 | const crOnRequestMethodBody = crOnRequestMethod.getBody(); 64 | crOnRequestMethodBody.insertStatements(0, 'if (this._alreadyTrackedNetworkIds.has(requestWillBeSentEvent.initiator.requestId)) return;') 65 | 66 | // -- _onRequestPaused Method -- 67 | const onRequestPausedMethod = crNetworkManagerClass.getMethod("_onRequestPaused"); 68 | const onRequestPausedMethodBody = onRequestPausedMethod.getBody(); 69 | onRequestPausedMethodBody.insertStatements(0, 'if (this._alreadyTrackedNetworkIds.has(event.networkId)) return;') 70 | 71 | 72 | // ------- RouteImpl Class ------- 73 | const routeImplClass = crNetworkManagerSourceFile.getClass("RouteImpl"); 74 | 75 | // -- RouteImpl Constructor -- 76 | const constructorDeclaration = routeImplClass 77 | .getConstructors() 78 | .find((ctor) => 79 | ctor 80 | .getText() 81 | .includes("constructor(session: CRSession, interceptionId: string)"), 82 | ); 83 | // Get current parameters and add the new `page` parameter 84 | const parameters = constructorDeclaration.getParameters(); 85 | // Adding the 'page' parameter 86 | constructorDeclaration.insertParameter(parameters.length, { name: "page" }); 87 | constructorDeclaration.insertParameter(parameters.length+1, { name: "networkId" }); 88 | constructorDeclaration.insertParameter(parameters.length+2, { name: "sessionManager" }); 89 | // Modify the constructor's body to include `this._page = page;` and other properties 90 | const body = constructorDeclaration.getBody(); 91 | body.insertStatements(0, "this._page = void 0;"); 92 | body.insertStatements(0, "this._networkId = void 0;"); 93 | body.insertStatements(0, "this._sessionManager = void 0;"); 94 | body.addStatements("this._page = page;"); 95 | body.addStatements("this._networkId = networkId;"); 96 | body.addStatements("this._sessionManager = sessionManager;"); 97 | body.addStatements("eventsHelper.addEventListener(this._session, 'Fetch.requestPaused', async e => await this._networkRequestIntercepted(e));"); 98 | 99 | // -- _fixCSP Method -- 100 | routeImplClass.addMethod({ 101 | name: "_fixCSP", 102 | isAsync: false, 103 | parameters: [ 104 | { name: "csp" }, 105 | { name: "scriptNonce" }, 106 | ] 107 | }); 108 | const fixCSPMethod = routeImplClass.getMethod("_fixCSP"); 109 | fixCSPMethod.setBodyText(` 110 | if (!csp || typeof csp !== 'string') return csp; 111 | 112 | // Split by semicolons and clean up 113 | const directives = csp.split(';') 114 | .map(d => d.trim()) 115 | .filter(d => d && d.length > 0); 116 | 117 | const fixedDirectives = []; 118 | let hasScriptSrc = false; 119 | 120 | for (let directive of directives) { 121 | // Skip empty directives 122 | if (!directive.trim()) continue; 123 | 124 | // Improved directive parsing to handle more edge cases 125 | const directiveMatch = directive.match(/^([a-zA-Z-]+)\\s+(.*)$/); 126 | if (!directiveMatch) { 127 | fixedDirectives.push(directive); 128 | continue; 129 | } 130 | 131 | const directiveName = directiveMatch[1].toLowerCase(); 132 | const directiveValues = directiveMatch[2].split(/\\s+/).filter(v => v.length > 0); 133 | 134 | switch (directiveName) { 135 | case 'script-src': 136 | hasScriptSrc = true; 137 | let values = [...directiveValues]; 138 | 139 | // Add nonce if we have one and it's not already present 140 | if (scriptNonce && !values.some(v => v.includes(\`nonce-\${scriptNonce}\`))) { 141 | values.push(\`'nonce-\${scriptNonce}'\`); 142 | } 143 | 144 | // Add 'unsafe-eval' if not present 145 | if (!values.includes("'unsafe-eval'")) { 146 | values.push("'unsafe-eval'"); 147 | } 148 | 149 | // Add unsafe-inline if not present and no nonce is being used 150 | if (!values.includes("'unsafe-inline'") && !scriptNonce) { 151 | values.push("'unsafe-inline'"); 152 | } 153 | 154 | // Add wildcard for external scripts if not already present 155 | if (!values.includes("*") && !values.includes("'self'") && !values.some(v => v.includes("https:"))) { 156 | values.push("*"); 157 | } 158 | 159 | fixedDirectives.push(\`script-src \${values.join(' ')}\`); 160 | break; 161 | 162 | case 'style-src': 163 | let styleValues = [...directiveValues]; 164 | // Add 'unsafe-inline' for styles if not present 165 | if (!styleValues.includes("'unsafe-inline'")) { 166 | styleValues.push("'unsafe-inline'"); 167 | } 168 | fixedDirectives.push(\`style-src \${styleValues.join(' ')}\`); 169 | break; 170 | 171 | case 'img-src': 172 | let imgValues = [...directiveValues]; 173 | // Allow data: URLs for images if not already allowed 174 | if (!imgValues.includes("data:") && !imgValues.includes("*")) { 175 | imgValues.push("data:"); 176 | } 177 | fixedDirectives.push(\`img-src \${imgValues.join(' ')}\`); 178 | break; 179 | 180 | case 'font-src': 181 | let fontValues = [...directiveValues]; 182 | // Allow data: URLs for fonts if not already allowed 183 | if (!fontValues.includes("data:") && !fontValues.includes("*")) { 184 | fontValues.push("data:"); 185 | } 186 | fixedDirectives.push(\`font-src \${fontValues.join(' ')}\`); 187 | break; 188 | 189 | case 'connect-src': 190 | let connectValues = [...directiveValues]; 191 | // Allow WebSocket connections if not already allowed 192 | const hasWs = connectValues.some(v => v.includes("ws:") || v.includes("wss:") || v === "*"); 193 | if (!hasWs) { 194 | connectValues.push("ws:", "wss:"); 195 | } 196 | fixedDirectives.push(\`connect-src \${connectValues.join(' ')}\`); 197 | break; 198 | 199 | case 'frame-ancestors': 200 | let frameAncestorValues = [...directiveValues]; 201 | // If completely blocked with 'none', allow 'self' at least 202 | if (frameAncestorValues.includes("'none'")) { 203 | frameAncestorValues = ["'self'"]; 204 | } 205 | fixedDirectives.push(\`frame-ancestors \${frameAncestorValues.join(' ')}\`); 206 | break; 207 | 208 | default: 209 | // Keep other directives as-is 210 | fixedDirectives.push(directive); 211 | break; 212 | } 213 | } 214 | 215 | // Add script-src if it doesn't exist (for our injected scripts) 216 | if (!hasScriptSrc) { 217 | if (scriptNonce) { 218 | fixedDirectives.push(\`script-src 'self' 'unsafe-eval' 'nonce-\${scriptNonce}' *\`); 219 | } else { 220 | fixedDirectives.push(\`script-src 'self' 'unsafe-eval' 'unsafe-inline' *\`); 221 | } 222 | } 223 | 224 | return fixedDirectives.join('; '); 225 | `); 226 | 227 | // -- fulfill Method -- 228 | const fulfillMethod = routeImplClass.getMethodOrThrow("fulfill"); 229 | // Replace the body of the fulfill method with custom code 230 | fulfillMethod.setBodyText(` 231 | const isTextHtml = response.headers.some((header) => header.name.toLowerCase() === "content-type" && header.value.includes("text/html")); 232 | var allInjections = [...this._page.delegate._mainFrameSession._evaluateOnNewDocumentScripts]; 233 | for (const binding of this._page.delegate._browserContext._pageBindings.values()) { 234 | if (!allInjections.includes(binding)) allInjections.push(binding); 235 | } 236 | if (isTextHtml && allInjections.length) { 237 | let useNonce = false; 238 | let scriptNonce = null; 239 | // Decode body if needed 240 | if (response.isBase64) { 241 | response.isBase64 = false; 242 | response.body = Buffer.from(response.body, "base64").toString("utf-8"); 243 | } 244 | // === CSP Detection and Fixing === 245 | const cspHeaderNames = ["content-security-policy", "content-security-policy-report-only"]; 246 | // Fix CSP in headers 247 | for (let i = 0; i < response.headers.length; i++) { 248 | const headerName = response.headers[i].name.toLowerCase(); 249 | if (cspHeaderNames.includes(headerName)) { 250 | const originalCsp = response.headers[i].value || ""; 251 | // Extract nonce if present 252 | if (!useNonce) { 253 | const nonceMatch = originalCsp.match(/script-src[^;]*'nonce-([^'"\\s;]+)'/i); 254 | if (nonceMatch && nonceMatch[1]) { 255 | scriptNonce = nonceMatch[1]; 256 | useNonce = true; 257 | } 258 | } 259 | 260 | const fixedCsp = this._fixCSP(originalCsp, scriptNonce); 261 | response.headers[i].value = fixedCsp; 262 | } 263 | } 264 | 265 | // Fix CSP in meta tags 266 | if (typeof response.body === "string" && response.body.length) { 267 | response.body = response.body.replace( 268 | /]*http-equiv=(?:"|')?Content-Security-Policy(?:"|')?[^>]*>/gi, 269 | (match) => { 270 | const contentMatch = match.match(/\bcontent=(?:"|')([^"']*)(?:"|')/i); 271 | if (contentMatch && contentMatch[1]) { 272 | let originalCsp = contentMatch[1]; 273 | 274 | // Decode HTML entities 275 | originalCsp = originalCsp.replace(/&/g, '&') // Must be first! 276 | .replace(/</g, '<') 277 | .replace(/>/g, '>') 278 | .replace(/"/g, '"') 279 | .replace(/'/g, "'") 280 | .replace(/"/g, '"') 281 | .replace(/ /g, ' ') 282 | .replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec)) 283 | .replace(/&#x([0-9a-fA-F]+);/g, (match, hex) => String.fromCharCode(parseInt(hex, 16))); 284 | 285 | // Extract nonce if not already found 286 | if (!useNonce) { 287 | const nonceMatch = originalCsp.match(/script-src[^;]*'nonce-([^'"\\s;]+)'/i); 288 | if (nonceMatch && nonceMatch[1]) { 289 | scriptNonce = nonceMatch[1]; 290 | useNonce = true; 291 | } 292 | } 293 | 294 | const fixedCsp = this._fixCSP(originalCsp, scriptNonce); 295 | // Re-encode for HTML 296 | const encodedCsp = fixedCsp.replace(/'/g, ''').replace(/"/g, '"'); 297 | return match.replace(contentMatch[1], encodedCsp); 298 | } 299 | return match; 300 | } 301 | ); 302 | } 303 | 304 | // Build injection HTML - only use nonce if one was found in existing CSP 305 | let injectionHTML = ""; 306 | allInjections.forEach((script) => { 307 | let scriptId = crypto.randomBytes(22).toString("hex"); 308 | let scriptSource = script.source || script; 309 | const nonceAttr = useNonce ? \`nonce="\${scriptNonce}"\` : ''; 310 | injectionHTML += \`\`; 311 | }); 312 | 313 | // Inject at END of 314 | const lower = response.body.toLowerCase(); 315 | const headStartIndex = lower.indexOf("", headStartIndex); 318 | if (headEndTagIndex !== -1) { 319 | // Find the head opening tag end 320 | const headOpenEnd = response.body.indexOf(">", headStartIndex) + 1; 321 | const headContent = response.body.slice(headOpenEnd, headEndTagIndex); 322 | const headContentLower = headContent.toLowerCase(); 323 | 324 | // Look for the first