capsolver-core 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- capsolver_core/__init__.py +44 -0
- capsolver_core/__main__.py +99 -0
- capsolver_core/browser/__init__.py +6 -0
- capsolver_core/browser/adapter.py +116 -0
- capsolver_core/browser/driver.py +26 -0
- capsolver_core/browser/inject/__init__.py +1 -0
- capsolver_core/browser/inject/cloudflare.py +67 -0
- capsolver_core/browser/inject/recaptcha.py +162 -0
- capsolver_core/capsolver.py +206 -0
- capsolver_core/captcha/__init__.py +20 -0
- capsolver_core/captcha/handler.py +55 -0
- capsolver_core/captcha/handlers/__init__.py +20 -0
- capsolver_core/captcha/handlers/cloudflare.py +83 -0
- capsolver_core/captcha/handlers/recaptcha.py +106 -0
- capsolver_core/captcha/handlers/support.py +30 -0
- capsolver_core/captcha/registry.py +56 -0
- capsolver_core/captcha/types.py +60 -0
- capsolver_core/core/__init__.py +7 -0
- capsolver_core/core/client.py +163 -0
- capsolver_core/core/errors.py +55 -0
- capsolver_core/core/http.py +137 -0
- capsolver_core/core/tasks.py +134 -0
- capsolver_core/core/types.py +157 -0
- capsolver_core/py.typed +0 -0
- capsolver_core-0.1.0.dist-info/METADATA +206 -0
- capsolver_core-0.1.0.dist-info/RECORD +29 -0
- capsolver_core-0.1.0.dist-info/WHEEL +4 -0
- capsolver_core-0.1.0.dist-info/entry_points.txt +2 -0
- capsolver_core-0.1.0.dist-info/licenses/LICENSE +15 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""CapSolver SDK for Python — detect, solve and autofill captchas via the CapSolver API."""
|
|
2
|
+
|
|
3
|
+
from capsolver_core.capsolver import Capsolver, create_capsolver
|
|
4
|
+
from capsolver_core.core.types import CaptchaType
|
|
5
|
+
from capsolver_core.core.errors import CapsolverError, CapsolverTimeoutError, NetworkError, RateLimitError
|
|
6
|
+
from capsolver_core.core.client import CapsolverClient, WaitOptions
|
|
7
|
+
from capsolver_core.captcha.types import CaptchaInfo, Solution
|
|
8
|
+
from capsolver_core.captcha.handler import CaptchaHandler
|
|
9
|
+
from capsolver_core.captcha.registry import HandlerRegistry
|
|
10
|
+
from capsolver_core.captcha.handlers import (
|
|
11
|
+
RecaptchaHandler,
|
|
12
|
+
CloudflareHandler,
|
|
13
|
+
default_handlers,
|
|
14
|
+
)
|
|
15
|
+
from capsolver_core.browser.driver import PageDriver
|
|
16
|
+
from capsolver_core.browser.adapter import from_playwright_page, to_driver
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# Main entry
|
|
20
|
+
"Capsolver",
|
|
21
|
+
"create_capsolver",
|
|
22
|
+
# Core
|
|
23
|
+
"CaptchaType",
|
|
24
|
+
"CapsolverError",
|
|
25
|
+
"CapsolverTimeoutError",
|
|
26
|
+
"NetworkError",
|
|
27
|
+
"RateLimitError",
|
|
28
|
+
"CapsolverClient",
|
|
29
|
+
"WaitOptions",
|
|
30
|
+
# Captcha
|
|
31
|
+
"CaptchaInfo",
|
|
32
|
+
"Solution",
|
|
33
|
+
"CaptchaHandler",
|
|
34
|
+
"HandlerRegistry",
|
|
35
|
+
"RecaptchaHandler",
|
|
36
|
+
"CloudflareHandler",
|
|
37
|
+
"default_handlers",
|
|
38
|
+
# Browser
|
|
39
|
+
"PageDriver",
|
|
40
|
+
"from_playwright_page",
|
|
41
|
+
"to_driver",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""CLI entry point for ``python -m capsolver_core`` and the ``capsolver`` console script.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
capsolver info # show version, Python, optional deps
|
|
5
|
+
capsolver balance # check API balance (needs CAPSOLVER_API_KEY)
|
|
6
|
+
capsolver list-types # list supported captcha types
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from importlib.metadata import version as pkg_version
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _cmd_info(_args: argparse.Namespace) -> None:
|
|
18
|
+
"""Print SDK version, Python version, and optional-dependency availability."""
|
|
19
|
+
print(f"capsolver-core {pkg_version('capsolver-core')}")
|
|
20
|
+
print(f"python {sys.version.split()[0]}")
|
|
21
|
+
|
|
22
|
+
optionals = {"playwright": "playwright"}
|
|
23
|
+
for label, module in optionals.items():
|
|
24
|
+
try:
|
|
25
|
+
mod_ver = pkg_version(module)
|
|
26
|
+
print(f"{label:<14} {mod_ver}")
|
|
27
|
+
except Exception:
|
|
28
|
+
print(f"{label:<14} not installed")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _cmd_balance(args: argparse.Namespace) -> None:
|
|
32
|
+
"""Check API balance — requires CAPSOLVER_API_KEY."""
|
|
33
|
+
import asyncio
|
|
34
|
+
import os
|
|
35
|
+
|
|
36
|
+
key = args.api_key or os.environ.get("CAPSOLVER_API_KEY", "")
|
|
37
|
+
if not key:
|
|
38
|
+
print("Error: CAPSOLVER_API_KEY is not set.", file=sys.stderr)
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
from capsolver_core import Capsolver
|
|
42
|
+
|
|
43
|
+
async def _run() -> None:
|
|
44
|
+
cs = Capsolver(api_key=key)
|
|
45
|
+
balance = await cs.get_balance()
|
|
46
|
+
data = {"balance": balance.balance, "packages": balance.packages}
|
|
47
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
48
|
+
|
|
49
|
+
asyncio.run(_run())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _cmd_list_types(_args: argparse.Namespace) -> None:
|
|
53
|
+
"""List all supported captcha types."""
|
|
54
|
+
from capsolver_core import Capsolver, CaptchaType
|
|
55
|
+
|
|
56
|
+
cs = Capsolver()
|
|
57
|
+
handlers = cs.get_supported_captchas()
|
|
58
|
+
all_types = [t.value for t in CaptchaType]
|
|
59
|
+
|
|
60
|
+
print("All captcha types:")
|
|
61
|
+
for t in all_types:
|
|
62
|
+
marker = " *" if t in handlers else ""
|
|
63
|
+
print(f" {t}{marker}")
|
|
64
|
+
|
|
65
|
+
if handlers:
|
|
66
|
+
print(f"\nRegistered handlers ({len(handlers)}): {', '.join(handlers)}")
|
|
67
|
+
print("\n(* = handler registered)")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main() -> None:
|
|
71
|
+
parser = argparse.ArgumentParser(
|
|
72
|
+
prog="capsolver",
|
|
73
|
+
description="CapSolver SDK — diagnostics and info CLI.",
|
|
74
|
+
)
|
|
75
|
+
sub = parser.add_subparsers(dest="command")
|
|
76
|
+
|
|
77
|
+
sub.add_parser("info", help="Show version and environment info.")
|
|
78
|
+
|
|
79
|
+
bal = sub.add_parser("balance", help="Check CapSolver account balance.")
|
|
80
|
+
bal.add_argument("--api-key", default=None, help="API key (default: CAPSOLVER_API_KEY env).")
|
|
81
|
+
|
|
82
|
+
sub.add_parser("list-types", help="List supported captcha types.")
|
|
83
|
+
|
|
84
|
+
args = parser.parse_args()
|
|
85
|
+
|
|
86
|
+
if args.command is None:
|
|
87
|
+
parser.print_help()
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
90
|
+
dispatch = {
|
|
91
|
+
"info": _cmd_info,
|
|
92
|
+
"balance": _cmd_balance,
|
|
93
|
+
"list-types": _cmd_list_types,
|
|
94
|
+
}
|
|
95
|
+
dispatch[args.command](args)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
main()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Page adapters.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's browser/adapter.ts. Wraps a Playwright ``Page``
|
|
4
|
+
(or any structurally-compatible object) as a ``PageDriver``.
|
|
5
|
+
|
|
6
|
+
The SDK deliberately does **not** import Playwright at module level so
|
|
7
|
+
it stays dependency-free at import time — callers pass their own page.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Protocol, cast, runtime_checkable
|
|
13
|
+
|
|
14
|
+
from capsolver_core.browser.driver import PageDriver
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class EvaluatablePage(Protocol):
|
|
19
|
+
"""The minimal surface the SDK needs from a Playwright/Selenium page."""
|
|
20
|
+
|
|
21
|
+
async def evaluate(self, expression: str, arg: Any = None) -> Any: ...
|
|
22
|
+
def url(self) -> str: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _PlaywrightDriver:
|
|
26
|
+
"""Wraps a Playwright ``Page`` as a ``PageDriver``."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, page: Any) -> None:
|
|
29
|
+
self._page = page
|
|
30
|
+
|
|
31
|
+
async def evaluate(self, script: str, arg: Any = None) -> Any:
|
|
32
|
+
if arg is not None:
|
|
33
|
+
return await self._page.evaluate(script, arg)
|
|
34
|
+
return await self._page.evaluate(script)
|
|
35
|
+
|
|
36
|
+
async def url(self) -> str:
|
|
37
|
+
return cast(str, self._page.url)
|
|
38
|
+
|
|
39
|
+
async def wait_for_selector(self, selector: str, *, timeout: float | None = None) -> None:
|
|
40
|
+
opts: dict[str, Any] = {}
|
|
41
|
+
if timeout is not None:
|
|
42
|
+
opts["timeout"] = timeout
|
|
43
|
+
await self._page.wait_for_selector(selector, **opts)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _GenericDriver:
|
|
47
|
+
"""Wraps any object that exposes ``evaluate`` and ``url`` (async or sync)."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, page: Any) -> None:
|
|
50
|
+
self._page = page
|
|
51
|
+
|
|
52
|
+
async def evaluate(self, script: str, arg: Any = None) -> Any:
|
|
53
|
+
fn = self._page.evaluate
|
|
54
|
+
import asyncio
|
|
55
|
+
|
|
56
|
+
if arg is not None:
|
|
57
|
+
result = fn(script, arg)
|
|
58
|
+
else:
|
|
59
|
+
result = fn(script)
|
|
60
|
+
if asyncio.iscoroutine(result):
|
|
61
|
+
return await result
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
async def url(self) -> str:
|
|
65
|
+
result = self._page.url
|
|
66
|
+
import asyncio
|
|
67
|
+
|
|
68
|
+
if asyncio.iscoroutine(result):
|
|
69
|
+
return cast(str, await result)
|
|
70
|
+
if callable(result):
|
|
71
|
+
r = result()
|
|
72
|
+
if asyncio.iscoroutine(r):
|
|
73
|
+
return cast(str, await r)
|
|
74
|
+
return cast(str, r)
|
|
75
|
+
return cast(str, result)
|
|
76
|
+
|
|
77
|
+
async def wait_for_selector(self, selector: str, *, timeout: float | None = None) -> None:
|
|
78
|
+
fn = getattr(self._page, "wait_for_selector", None)
|
|
79
|
+
if fn is None:
|
|
80
|
+
return
|
|
81
|
+
import asyncio
|
|
82
|
+
|
|
83
|
+
opts: dict[str, Any] = {}
|
|
84
|
+
if timeout is not None:
|
|
85
|
+
opts["timeout"] = timeout
|
|
86
|
+
result = fn(selector, **opts)
|
|
87
|
+
if asyncio.iscoroutine(result):
|
|
88
|
+
await result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def from_playwright_page(page: Any) -> PageDriver:
|
|
92
|
+
"""Wrap a Playwright ``Page`` as a ``PageDriver``."""
|
|
93
|
+
return _PlaywrightDriver(page)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def from_generic_page(page: Any) -> PageDriver:
|
|
97
|
+
"""Wrap any evaluate/url-compatible object as a ``PageDriver``."""
|
|
98
|
+
return _GenericDriver(page)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def to_driver(page: Any) -> PageDriver:
|
|
102
|
+
"""Normalize a page-like object into a ``PageDriver``.
|
|
103
|
+
|
|
104
|
+
If it already satisfies ``PageDriver`` it is returned as-is;
|
|
105
|
+
otherwise it is wrapped with the generic adapter.
|
|
106
|
+
"""
|
|
107
|
+
# A raw Playwright ``Page`` exposes ``url`` as a string property, whereas a
|
|
108
|
+
# ``PageDriver`` exposes it as an async method. Detect the Playwright page
|
|
109
|
+
# FIRST: a raw page also structurally satisfies the runtime-checkable
|
|
110
|
+
# PageDriver protocol, so the isinstance check below would otherwise return
|
|
111
|
+
# it unwrapped — and ``await page.url()`` would then fail on the string.
|
|
112
|
+
if hasattr(page, "url") and not callable(getattr(page, "url", None)):
|
|
113
|
+
return _PlaywrightDriver(page)
|
|
114
|
+
if isinstance(page, PageDriver):
|
|
115
|
+
return page
|
|
116
|
+
return _GenericDriver(page)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""PageDriver — the browser abstraction handlers use to read the DOM.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's browser/driver.ts. Uses ``Protocol`` so any
|
|
4
|
+
object with the right shape works without inheritance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class PageDriver(Protocol):
|
|
14
|
+
"""Minimal browser page interface."""
|
|
15
|
+
|
|
16
|
+
async def evaluate(self, script: str, arg: Any = None) -> Any:
|
|
17
|
+
"""Run a JavaScript snippet in the page context and return its result."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
async def url(self) -> str:
|
|
21
|
+
"""The page's current URL."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
async def wait_for_selector(self, selector: str, *, timeout: float | None = None) -> None:
|
|
25
|
+
"""Optionally wait for a selector to appear."""
|
|
26
|
+
...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Inject script package — JS snippets for in-page captcha detection/fill."""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""In-page Cloudflare Turnstile scripts (ported from the Node SDK's cloudflare.inject.ts).
|
|
2
|
+
|
|
3
|
+
Self-contained JS snippets — see recaptcha.py for the pattern.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# ── Cheap presence check ──────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
DETECT_CLOUDFLARE_JS = """
|
|
9
|
+
() => {
|
|
10
|
+
// Match Cloudflare-specific markers only. A bare [data-sitekey] is NOT
|
|
11
|
+
// Cloudflare-exclusive (reCAPTCHA/hCaptcha use it too) and would cause a
|
|
12
|
+
// reCAPTCHA page to be mis-detected as Cloudflare.
|
|
13
|
+
return !!document.querySelector(
|
|
14
|
+
'input[name="cf-turnstile-response"], .cf-turnstile, iframe[src*="challenges.cloudflare.com"]'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# ── Extract info for Cloudflare Turnstile widgets on the page ─────
|
|
20
|
+
|
|
21
|
+
GET_CLOUDFLARE_INFOS_JS = """
|
|
22
|
+
() => {
|
|
23
|
+
// Only consider Cloudflare-specific containers — never a bare
|
|
24
|
+
// [data-sitekey], which would grab a reCAPTCHA/hCaptcha sitekey and
|
|
25
|
+
// build an invalid Turnstile task.
|
|
26
|
+
const widget =
|
|
27
|
+
document.querySelector('.cf-turnstile[data-sitekey]') ||
|
|
28
|
+
document.querySelector('.cf-turnstile');
|
|
29
|
+
|
|
30
|
+
let websiteKey = widget ? widget.getAttribute('data-sitekey') : null;
|
|
31
|
+
let action = widget ? widget.getAttribute('data-action') : null;
|
|
32
|
+
let cdata = widget ? widget.getAttribute('data-cdata') : null;
|
|
33
|
+
|
|
34
|
+
// Fall back to the widget iframe's query string.
|
|
35
|
+
if (!websiteKey) {
|
|
36
|
+
const iframe = document.querySelector('iframe[src*="challenges.cloudflare.com"]');
|
|
37
|
+
if (iframe && iframe.src) {
|
|
38
|
+
const params = new URL(iframe.src.replace('#', '?')).searchParams;
|
|
39
|
+
websiteKey = params.get('sitekey');
|
|
40
|
+
action = action || params.get('action');
|
|
41
|
+
cdata = cdata || params.get('cData');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Identify a container for fill / autofill.
|
|
46
|
+
const input = document.querySelector('input[name="cf-turnstile-response"]');
|
|
47
|
+
const container = (input ? input.parentElement : widget) || null;
|
|
48
|
+
if (container && !container.id) container.id = 'cloudflare-container-' + Date.now();
|
|
49
|
+
|
|
50
|
+
if (!websiteKey) return [];
|
|
51
|
+
return [{ websiteKey, action, cdata, containerId: container ? container.id : null }];
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# ── Write the token back into the hidden input ────────────────────
|
|
56
|
+
|
|
57
|
+
FILL_CLOUDFLARE_JS = """
|
|
58
|
+
(args) => {
|
|
59
|
+
const scope = args.containerId ? document.getElementById(args.containerId) : document;
|
|
60
|
+
const input =
|
|
61
|
+
(scope ? scope.querySelector('input[name="cf-turnstile-response"]') : null) ||
|
|
62
|
+
document.querySelector('input[name="cf-turnstile-response"]');
|
|
63
|
+
if (!input) return false;
|
|
64
|
+
input.value = args.token;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
"""
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""In-page reCAPTCHA scripts (ported from the Node SDK's recaptcha.inject.ts).
|
|
2
|
+
|
|
3
|
+
Each constant is a self-contained JavaScript snippet that runs inside the
|
|
4
|
+
page via ``page.evaluate``. No imports, no module-scope references — only
|
|
5
|
+
the page's ``window``/``document`` and their own arguments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# ── Cheap presence check ──────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
DETECT_RECAPTCHA_JS = """
|
|
11
|
+
() => {
|
|
12
|
+
const cfg = window.___grecaptcha_cfg;
|
|
13
|
+
return !!(cfg && cfg.clients && Object.keys(cfg.clients).length > 0);
|
|
14
|
+
}
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# ── Extract a descriptor for every reCAPTCHA widget on the page ───
|
|
18
|
+
|
|
19
|
+
GET_RECAPTCHA_INFOS_JS = """
|
|
20
|
+
() => {
|
|
21
|
+
function getSaParam() {
|
|
22
|
+
const reCap = document.querySelector('iframe[title="reCAPTCHA"]');
|
|
23
|
+
const src = reCap ? reCap.getAttribute('src') : null;
|
|
24
|
+
if (!src) return null;
|
|
25
|
+
return new URL(src).searchParams.get('sa');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getWidgetInfo(widget) {
|
|
29
|
+
const info = {
|
|
30
|
+
captchaType: 'reCaptcha',
|
|
31
|
+
version: 'v2',
|
|
32
|
+
sitekey: null,
|
|
33
|
+
action: null,
|
|
34
|
+
s: null,
|
|
35
|
+
callback: null,
|
|
36
|
+
enterprise: !!(window.grecaptcha && window.grecaptcha.enterprise),
|
|
37
|
+
containerId: null,
|
|
38
|
+
bindedButtonId: null,
|
|
39
|
+
invisible: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Detect the v3 badge.
|
|
43
|
+
let isBadge = false;
|
|
44
|
+
badge: for (const f in widget) {
|
|
45
|
+
if (typeof widget[f] !== 'object') continue;
|
|
46
|
+
for (const g in widget[f]) {
|
|
47
|
+
if (widget[f][g] && widget[f][g].classList && widget[f][g].classList.contains('grecaptcha-badge')) {
|
|
48
|
+
isBadge = true;
|
|
49
|
+
break badge;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (isBadge) {
|
|
54
|
+
info.version = 'v3';
|
|
55
|
+
info.captchaType = 'reCaptcha3';
|
|
56
|
+
for (const h in widget) {
|
|
57
|
+
const i = widget[h];
|
|
58
|
+
if (typeof i !== 'object') continue;
|
|
59
|
+
for (const j in i) {
|
|
60
|
+
if (typeof i[j] !== 'string') continue;
|
|
61
|
+
if (i[j] === 'fullscreen') {
|
|
62
|
+
info.version = 'v2';
|
|
63
|
+
info.captchaType = 'reCaptcha';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Find the container element id.
|
|
70
|
+
let candidate = null;
|
|
71
|
+
for (const k in widget) {
|
|
72
|
+
if (widget[k] && widget[k].nodeType) {
|
|
73
|
+
if (widget[k].id) {
|
|
74
|
+
info.containerId = widget[k].id;
|
|
75
|
+
} else if (widget[k].dataset && widget[k].dataset.sitekey) {
|
|
76
|
+
widget[k].id = 'recaptcha-container-' + Date.now();
|
|
77
|
+
info.containerId = widget[k].id;
|
|
78
|
+
} else if (!candidate) {
|
|
79
|
+
candidate = widget[k];
|
|
80
|
+
} else if (widget[k].isSameNode(candidate)) {
|
|
81
|
+
widget[k].id = 'recaptcha-container-' + Date.now();
|
|
82
|
+
info.containerId = widget[k].id;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Find sitekey / action / s / callback / bind / size.
|
|
89
|
+
for (const k1 in widget) {
|
|
90
|
+
const obj = widget[k1];
|
|
91
|
+
if (typeof obj !== 'object') continue;
|
|
92
|
+
for (const k2 in obj) {
|
|
93
|
+
if (obj[k2] === null || typeof obj[k2] !== 'object') continue;
|
|
94
|
+
if (obj[k2].sitekey === undefined || obj[k2].action === undefined) continue;
|
|
95
|
+
for (const k3 in obj[k2]) {
|
|
96
|
+
if (k3 === 'sitekey') info.sitekey = obj[k2][k3];
|
|
97
|
+
if (k3 === 'action') info.action = obj[k2][k3];
|
|
98
|
+
if (k3 === 's') info.s = obj[k2][k3];
|
|
99
|
+
if (k3 === 'callback' || k3 === 'promise-callback') info.callback = obj[k2][k3];
|
|
100
|
+
if (k3 === 'bind' && obj[k2][k3]) {
|
|
101
|
+
const bind = obj[k2][k3];
|
|
102
|
+
if (typeof bind === 'string') {
|
|
103
|
+
info.bindedButtonId = bind;
|
|
104
|
+
} else {
|
|
105
|
+
if (bind.id === undefined) bind.id = 'recaptchaBindedElement' + widget.id;
|
|
106
|
+
info.bindedButtonId = bind.id;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (k3 === 'size' && obj[k2][k3] === 'invisible') info.invisible = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Stash a function callback under a global key so fill can invoke it.
|
|
115
|
+
if (typeof info.callback === 'function') {
|
|
116
|
+
const callbackKey = 'reCaptchaWidgetCallback' + widget.id;
|
|
117
|
+
window[callbackKey] = info.callback;
|
|
118
|
+
info.callback = callbackKey;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (info.captchaType === 'reCaptcha') info.action = getSaParam();
|
|
122
|
+
|
|
123
|
+
return info;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const cfg = window.___grecaptcha_cfg;
|
|
127
|
+
if (!cfg || !cfg.clients) return [];
|
|
128
|
+
|
|
129
|
+
const infos = [];
|
|
130
|
+
for (const widgetId in cfg.clients) {
|
|
131
|
+
infos.push(getWidgetInfo(cfg.clients[widgetId]));
|
|
132
|
+
}
|
|
133
|
+
return infos;
|
|
134
|
+
}
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
# ── Write the token back into the response textarea and fire the callback ─
|
|
138
|
+
|
|
139
|
+
FILL_RECAPTCHA_JS = """
|
|
140
|
+
(args) => {
|
|
141
|
+
let textarea = null;
|
|
142
|
+
if (args.containerId) {
|
|
143
|
+
textarea = document.querySelector('#' + args.containerId + ' textarea[name=g-recaptcha-response]');
|
|
144
|
+
}
|
|
145
|
+
if (!textarea) {
|
|
146
|
+
textarea = document.querySelector('textarea[name=g-recaptcha-response]');
|
|
147
|
+
}
|
|
148
|
+
if (textarea) {
|
|
149
|
+
textarea.innerHTML = args.token;
|
|
150
|
+
textarea.value = args.token;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (args.callback && typeof window[args.callback] === 'function') {
|
|
154
|
+
try {
|
|
155
|
+
window[args.callback](args.token);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
/* callback threw — token is still set in the textarea */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return !!textarea;
|
|
161
|
+
}
|
|
162
|
+
"""
|