new chart chartjs pandora_enterprise#9554

This commit is contained in:
Daniel Barbero 2022-12-13 09:10:13 +01:00
parent 1ebe09698b
commit 01d14ae3b8
444 changed files with 65867 additions and 2575 deletions

View File

@ -9,13 +9,15 @@
],
"config": {
"platform": {
"php": "7.2.0"
"php": "8.0.0"
}
},
"require": {
"mpdf/mpdf": "^8.0.15",
"swiftmailer/swiftmailer": "^6.0",
"amphp/parallel-functions": "^1.0"
"amphp/parallel-functions": "^1.0",
"nutsy/phpchartjs": "*",
"chrome-php/chrome": "^1.7.1"
},
"autoload": {
"psr-4": {

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "643ac0dc8a8e1f129104399054f8dd0c",
"content-hash": "8df19e49d9715736491944daea32b45c",
"packages": [
{
"name": "amphp/amp",
@ -549,6 +549,142 @@
],
"time": "2021-10-25T18:29:10+00:00"
},
{
"name": "chrome-php/chrome",
"version": "v1.7.1",
"source": {
"type": "git",
"url": "https://github.com/chrome-php/chrome.git",
"reference": "5783c749b2ee385d1c481b0906f1b8acef0296e4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chrome-php/chrome/zipball/5783c749b2ee385d1c481b0906f1b8acef0296e4",
"reference": "5783c749b2ee385d1c481b0906f1b8acef0296e4",
"shasum": ""
},
"require": {
"chrome-php/wrench": "^1.3",
"evenement/evenement": "^3.0.1",
"monolog/monolog": "^1.27.1 || ^2.8 || ^3.2",
"php": "^7.3 || ^8.0",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"symfony/filesystem": "^4.4 || ^5.0 || ^6.0",
"symfony/polyfill-mbstring": "^1.26",
"symfony/process": "^4.4 || ^5.0 || ^6.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^9.5.23",
"symfony/var-dumper": "^4.4 || ^5.0 || ^6.0"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"HeadlessChromium\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Enrico Dias",
"email": "enrico@enricodias.com",
"homepage": "https://github.com/enricodias"
}
],
"description": "Instrument headless chrome/chromium instances from PHP",
"keywords": [
"browser",
"chrome",
"chromium",
"crawl",
"headless",
"pdf",
"puppeteer",
"screenshot"
],
"support": {
"issues": "https://github.com/chrome-php/chrome/issues",
"source": "https://github.com/chrome-php/chrome/tree/v1.7.1"
},
"time": "2022-09-04T21:11:00+00:00"
},
{
"name": "chrome-php/wrench",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/chrome-php/wrench.git",
"reference": "68b8282d5d0d54a519c3212ee3e4c35bef40b7d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chrome-php/wrench/zipball/68b8282d5d0d54a519c3212ee3e4c35bef40b7d9",
"reference": "68b8282d5d0d54a519c3212ee3e4c35bef40b7d9",
"shasum": ""
},
"require": {
"ext-sockets": "*",
"php": "^7.3 || ^8.0",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"symfony/polyfill-php80": "^1.26"
},
"conflict": {
"wrench/wrench": "*"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^9.5.23"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"Wrench\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "A simple PHP WebSocket implementation",
"keywords": [
"WebSockets",
"hybi",
"websocket"
],
"support": {
"issues": "https://github.com/chrome-php/wrench/issues",
"source": "https://github.com/chrome-php/wrench/tree/v1.3.0"
},
"time": "2022-08-28T11:42:16+00:00"
},
{
"name": "doctrine/lexer",
"version": "1.2.2",
@ -693,6 +829,265 @@
],
"time": "2021-10-11T09:18:27+00:00"
},
{
"name": "evenement/evenement",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/igorw/evenement.git",
"reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7",
"reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0"
},
"type": "library",
"autoload": {
"psr-0": {
"Evenement": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
}
],
"description": "Événement is a very simple event dispatching library for PHP",
"keywords": [
"event-dispatcher",
"event-emitter"
],
"support": {
"issues": "https://github.com/igorw/evenement/issues",
"source": "https://github.com/igorw/evenement/tree/master"
},
"time": "2017-07-23T21:35:13+00:00"
},
{
"name": "halfpastfouram/collection",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/halfpastfouram/collection.git",
"reference": "0862d0b431fef9dc2245518dc06b86ff00dcd102"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/halfpastfouram/collection/zipball/0862d0b431fef9dc2245518dc06b86ff00dcd102",
"reference": "0862d0b431fef9dc2245518dc06b86ff00dcd102",
"shasum": ""
},
"require": {
"php": ">=5.6.0 || ^7.0"
},
"require-dev": {
"codeclimate/php-test-reporter": "dev-master",
"phpunit/phpunit": "5.2.*"
},
"type": "package",
"autoload": {
"psr-4": {
"Test\\": "test/",
"Halfpastfour\\Collection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"AGPL 3.0"
],
"authors": [
{
"name": "Bob Kruithof"
}
],
"description": "A flexible PHP Collection complete with custom Iterator.",
"homepage": "http://github.com/halfpastfouram/collection",
"keywords": [
"collection",
"php"
],
"support": {
"issues": "https://github.com/halfpastfouram/collection/issues",
"source": "https://github.com/halfpastfouram/collection/tree/master"
},
"time": "2016-12-18T13:04:48+00:00"
},
{
"name": "laminas/laminas-json",
"version": "3.5.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-json.git",
"reference": "7a8a1d7bf2d05dd6c1fbd7c0868d3848cf2b57ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-json/zipball/7a8a1d7bf2d05dd6c1fbd7c0868d3848cf2b57ec",
"reference": "7a8a1d7bf2d05dd6c1fbd7c0868d3848cf2b57ec",
"shasum": ""
},
"require": {
"php": "~8.0.0 || ~8.1.0 || ~8.2.0"
},
"conflict": {
"zendframework/zend-json": "*"
},
"require-dev": {
"laminas/laminas-coding-standard": "~2.4.0",
"laminas/laminas-stdlib": "^2.7.7 || ^3.1",
"phpunit/phpunit": "^9.5.25"
},
"suggest": {
"laminas/laminas-json-server": "For implementing JSON-RPC servers",
"laminas/laminas-xml2json": "For converting XML documents to JSON"
},
"type": "library",
"autoload": {
"psr-4": {
"Laminas\\Json\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP",
"homepage": "https://laminas.dev",
"keywords": [
"json",
"laminas"
],
"support": {
"chat": "https://laminas.dev/chat",
"docs": "https://docs.laminas.dev/laminas-json/",
"forum": "https://discourse.laminas.dev",
"issues": "https://github.com/laminas/laminas-json/issues",
"rss": "https://github.com/laminas/laminas-json/releases.atom",
"source": "https://github.com/laminas/laminas-json"
},
"funding": [
{
"url": "https://funding.communitybridge.org/projects/laminas-project",
"type": "community_bridge"
}
],
"time": "2022-10-17T04:06:45+00:00"
},
{
"name": "monolog/monolog",
"version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "720488632c590286b88b80e62aa3d3d551ad4a50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/720488632c590286b88b80e62aa3d3d551ad4a50",
"reference": "720488632c590286b88b80e62aa3d3d551ad4a50",
"shasum": ""
},
"require": {
"php": ">=7.2",
"psr/log": "^1.0.1 || ^2.0 || ^3.0"
},
"provide": {
"psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0"
},
"require-dev": {
"aws/aws-sdk-php": "^2.4.9 || ^3.0",
"doctrine/couchdb": "~1.0@dev",
"elasticsearch/elasticsearch": "^7 || ^8",
"ext-json": "*",
"graylog2/gelf-php": "^1.4.2",
"guzzlehttp/guzzle": "^7.4",
"guzzlehttp/psr7": "^2.2",
"mongodb/mongodb": "^1.8",
"php-amqplib/php-amqplib": "~2.4 || ^3",
"phpspec/prophecy": "^1.15",
"phpstan/phpstan": "^0.12.91",
"phpunit/phpunit": "^8.5.14",
"predis/predis": "^1.1 || ^2.0",
"rollbar/rollbar": "^1.3 || ^2 || ^3",
"ruflin/elastica": "^7",
"swiftmailer/swiftmailer": "^5.3|^6.0",
"symfony/mailer": "^5.4 || ^6",
"symfony/mime": "^5.4 || ^6"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
"ext-mbstring": "Allow to work properly with unicode symbols",
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"ext-openssl": "Required to send log messages using SSL",
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "https://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "https://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
"support": {
"issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/2.8.0"
},
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
"type": "tidelift"
}
],
"time": "2022-07-24T11:55:47+00:00"
},
{
"name": "mpdf/mpdf",
"version": "v8.0.15",
@ -823,6 +1218,62 @@
],
"time": "2020-11-13T09:40:50+00:00"
},
{
"name": "nutsy/phpchartjs",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/danielbarmar85/phpchartjs.git",
"reference": "a6c7683198e6c4d4536fd8063a38e6277598a1b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/danielbarmar85/phpchartjs/zipball/a6c7683198e6c4d4536fd8063a38e6277598a1b8",
"reference": "a6c7683198e6c4d4536fd8063a38e6277598a1b8",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-json": "*",
"halfpastfouram/collection": "1.0.0",
"laminas/laminas-json": ">3.1.2",
"php": ">=7.2",
"symfony/var-dumper": "^3.4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpunit/phpunit": "^9.5",
"sensiolabs/security-checker": "^5.0",
"squizlabs/php_codesniffer": "3.5.3"
},
"type": "package",
"autoload": {
"psr-4": {
"Test\\": "test/",
"Nutsy\\PHPChartJS\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"AGPL-3.0-or-later"
],
"authors": [
{
"name": "Daniel Barbero"
}
],
"description": "PHP library for ChartJS",
"homepage": "https://thenutsycompany.com/",
"keywords": [
"chartjs",
"graph",
"php"
],
"support": {
"source": "https://github.com/danielbarmar85/phpchartjs/tree/v1.0.0"
},
"time": "2022-11-23T13:00:43+00:00"
},
{
"name": "opis/closure",
"version": "3.6.3",
@ -940,30 +1391,30 @@
},
{
"name": "psr/log",
"version": "1.1.4",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
"reference": "ef29f6d262798707a9edd554e2b82517ef3a9376"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376",
"reference": "ef29f6d262798707a9edd554e2b82517ef3a9376",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -984,9 +1435,9 @@
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/1.1.4"
"source": "https://github.com/php-fig/log/tree/2.0.0"
},
"time": "2021-05-03T11:20:27+00:00"
"time": "2021-07-14T16:41:46+00:00"
},
{
"name": "setasign/fpdi",
@ -1136,6 +1587,152 @@
"abandoned": "symfony/mailer",
"time": "2021-10-18T15:26:12+00:00"
},
{
"name": "symfony/filesystem",
"version": "v5.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "ac09569844a9109a5966b9438fc29113ce77cf51"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/ac09569844a9109a5966b9438fc29113ce77cf51",
"reference": "ac09569844a9109a5966b9438fc29113ce77cf51",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8",
"symfony/polyfill-php80": "^1.16"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v5.4.13"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-09-21T19:53:16+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
},
{
"name": "symfony/polyfill-iconv",
"version": "v1.24.0",
@ -1392,16 +1989,16 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.24.0",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"shasum": ""
},
"require": {
@ -1416,7 +2013,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1424,12 +2021,12 @@
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@ -1455,7 +2052,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
},
"funding": [
{
@ -1471,7 +2068,7 @@
"type": "tidelift"
}
],
"time": "2021-11-30T18:21:41+00:00"
"time": "2022-11-03T14:55:06+00:00"
},
{
"name": "symfony/polyfill-php72",
@ -1548,6 +2145,232 @@
}
],
"time": "2021-05-27T09:17:38+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
},
{
"name": "symfony/process",
"version": "v5.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1",
"reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.16"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v5.4.11"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-06-27T16:58:25+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v3.4.47",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "0719f6cf4633a38b2c1585140998579ce23b4b7d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/0719f6cf4633a38b2c1585140998579ce23b4b7d",
"reference": "0719f6cf4633a38b2c1585140998579ce23b4b7d",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
},
"require-dev": {
"ext-iconv": "*",
"twig/twig": "~1.34|~2.4"
},
"suggest": {
"ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
"ext-intl": "To show region name in time zone dump",
"ext-symfony_debug": ""
},
"type": "library",
"autoload": {
"files": [
"Resources/functions/dump.php"
],
"psr-4": {
"Symfony\\Component\\VarDumper\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony mechanism for exploring and dumping PHP variables",
"homepage": "https://symfony.com",
"keywords": [
"debug",
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v3.4.47"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-24T10:57:07+00:00"
}
],
"packages-dev": [],
@ -1559,7 +2382,7 @@
"platform": [],
"platform-dev": [],
"platform-overrides": {
"php": "7.2.0"
"php": "8.0.0"
},
"plugin-api-version": "2.3.0"
}

View File

@ -2018,8 +2018,10 @@ if ($total_event_graph) {
include_once $config['homedir'].'/include/functions_graph.php';
$prueba = grafico_eventos_total('', 280, 150, false, true);
echo $prueba;
$out = '<div style="flex: 0 0 300px; width:99%; height:100%;">';
$out .= grafico_eventos_total('', 0, 0, false, true);
$out .= '<div>';
echo $out;
return;
}
@ -2028,8 +2030,10 @@ if ($graphic_event_group) {
include_once $config['homedir'].'/include/functions_graph.php';
$prueba = grafico_eventos_grupo(280, 150, '', false, true);
echo $prueba;
$out = '<div style="flex: 0 0 300px; width:99%; height:100%;">';
$out .= grafico_eventos_grupo(0, 0, '', false, true);
$out .= '<div>';
echo $out;
return;
}

View File

@ -42,40 +42,15 @@ require_once $config['homedir'].'/include/functions_agents.php';
require_once $config['homedir'].'/include/functions_tags.php';
$data_raw = get_parameter('data');
$data_decoded = json_decode(base64_decode($data_raw), true);
$data_decoded = json_decode(io_safe_output($data_raw), true);
if (json_last_error() === JSON_ERROR_NONE) {
$data = urldecode($data_decoded['data']);
$session_id = urldecode($data_decoded['session_id']);
$data_combined = urldecode($data_decoded['data_combined']);
$data_module_list = urldecode($data_decoded['data_module_list']);
$type_graph_pdf = urldecode($data_decoded['type_graph_pdf']);
$viewport_width = urldecode($data_decoded['viewport_width']);
$data = $data_decoded['data'];
$session_id = $data_decoded['session_id'];
$data_combined = $data_decoded['data_combined'];
$data_module_list = $data_decoded['data_module_list'];
$type_graph_pdf = $data_decoded['type_graph_pdf'];
}
/**
* Echo to stdout a PhantomJS callback call.
*
* @return void
*/
function echoPhantomCallback()
{
?>
<script type="text/javascript">
$('document').ready(function () {
setTimeout(function () {
try {
var status = window.callPhantom({ status: "loaded" });
} catch (error) {
console.log("CALLBACK ERROR", error.message)
}
}, 100);
});
</script>
<?php
}
// Initialize session.
global $config;
@ -99,7 +74,6 @@ if (check_login(false) === false) {
</head>
<body>
<h1>Access is not granted</h1>
<?php echoPhantomCallback(); ?>
</body>
</html>
@ -108,7 +82,7 @@ if (check_login(false) === false) {
}
// Access granted.
$params = json_decode($data, true);
$params = $data;
// Metaconsole connection to the node.
$server_id = $params['server_id'];
@ -124,7 +98,6 @@ if (is_metaconsole() === true && empty($server_id) === false) {
ui_print_error_message(
__('There was a problem connecting with the node')
);
echoPhantomCallback();
?>
</body>
</html>
@ -133,7 +106,6 @@ if (is_metaconsole() === true && empty($server_id) === false) {
}
}
$user_language = get_user_language($config['id_user']);
if (file_exists('languages/'.$user_language.'.mo') === true) {
$cfr = new CachedFileReader('languages/'.$user_language.'.mo');
@ -146,16 +118,16 @@ if (file_exists('languages/'.$user_language.'.mo') === true) {
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Pandora FMS Graph (<?php echo agents_get_alias($agent_id).' - '.$interface_name; ?>)</title>
<title>Pandora FMS Graph</title>
<link rel="stylesheet" href="styles/pandora.css" type="text/css" />
<link rel="stylesheet" href="styles/pandora_minimal.css" type="text/css" />
<link rel="stylesheet" href="styles/js/jquery-ui.min.css" type="text/css" />
<link rel="stylesheet" href="styles/js/jquery-ui_custom.css" type="text/css" />
<script language="javascript" type='text/javascript' src='javascript/pandora.js'></script>
<script language="javascript" type='text/javascript' src='javascript/pandora_ui.js'></script>
<script language="javascript" type='text/javascript' src='javascript/jquery.current.js'></script>
<script language="javascript" type='text/javascript' src='javascript/jquery.pandora.js'></script>
<script language="javascript" type='text/javascript' src='javascript/jquery-ui.min.js'></script>
<script language="javascript" type='text/javascript' src='javascript/pandora.js'></script>
<script language="javascript" type="text/javascript" src="graphs/flot/jquery.flot.js"></script>
<script language="javascript" type="text/javascript" src="graphs/flot/jquery.flot.min.js"></script>
<script language="javascript" type="text/javascript" src="graphs/flot/jquery.flot.time.js"></script>
@ -170,6 +142,7 @@ if (file_exists('languages/'.$user_language.'.mo') === true) {
<script language="javascript" type="text/javascript" src="graphs/flot/jquery.flot.exportdata.pandora.js"></script>
<script language="javascript" type="text/javascript" src="graphs/flot/jquery.flot.axislabels.js"></script>
<script language="javascript" type="text/javascript" src="graphs/flot/pandora.flot.js"></script>
<script language="javascript" type="text/javascript" src="graphs/chartjs/chart.js"></script>
</head>
<body style='background-color: <?php echo $params['backgroundColor']; ?>;'>
<?php
@ -177,41 +150,38 @@ if (file_exists('languages/'.$user_language.'.mo') === true) {
$params['menu'] = false;
$params['disable_black'] = true;
$params_combined = json_decode($data_combined, true);
$module_list = json_decode($data_module_list, true);
$params_combined = $data_combined;
$module_list = $data_module_list;
if (isset($params['vconsole']) === false || $params['vconsole'] === false) {
if ((int) $viewport_width > 0) {
$params['width'] = (int) $viewport_width;
}
$viewport = [
'width' => 0,
'height' => 0,
];
if ((isset($params['width']) === false
|| ($params['width'] <= 0))
) {
if ((int) $params['width'] <= 0) {
$params['width'] = 650;
}
if ((int) $params['landscape'] === 1) {
$params['width'] = 850;
}
if ($type_graph_pdf === 'slicebar') {
$params['width'] = 100;
$params['height'] = 70;
}
}
if (isset($params['options']['viewport']) === true) {
$viewport = $params['viewport'];
}
echo '<div>';
$style = '';
if (empty($params['options']['viewport']['width']) === false) {
$style .= 'width:'.$params['options']['viewport']['width'].'px;';
}
if (empty($params['options']['viewport']['height']) === false) {
$style .= 'height:'.$params['options']['viewport']['height'].'px;';
}
echo '<div id="container-chart-generator-item" style="'.$style.' margin:0px;">';
switch ($type_graph_pdf) {
case 'combined':
$params['pdf'] = true;
echo graphic_combined_module(
$result = graphic_combined_module(
$module_list,
$params,
$params_combined
);
echo $result;
break;
case 'sparse':
@ -219,20 +189,15 @@ if (file_exists('languages/'.$user_language.'.mo') === true) {
echo grafico_modulo_sparse($params);
break;
case 'pie_chart':
case 'pie_graph':
$params['pdf'] = true;
echo flot_pie_chart(
$params['values'],
$params['keys'],
$params['width'],
$params['height'],
$params['water_mark_url'],
$params['font'],
$config['font_size'],
$params['legend_position'],
$params['colors'],
$params['hide_labels']
$chart = get_build_setup_charts(
'PIE',
$params['options'],
$params['chart_data']
);
echo $chart->render(true);
break;
case 'vbar':
@ -259,27 +224,16 @@ if (file_exists('languages/'.$user_language.'.mo') === true) {
case 'ring_graph':
$params['pdf'] = true;
echo flot_custom_pie_chart(
$params['chart_data'],
$params['width'],
$params['height'],
$params['colors'],
$params['module_name_list'],
$params['long_index'],
$params['no_data'],
false,
'',
$params['water_mark'],
$params['font'],
$config['font_size'],
$params['unit'],
$params['ttl'],
$params['homeurl'],
$params['background_color'],
$params['legend_position'],
$params['background_color'],
$params['pdf']
$params['options']['width'] = 500;
$params['options']['height'] = 500;
$chart = get_build_setup_charts(
'DOUGHNUT',
$params['options'],
$params['chart_data']
);
echo $chart->render(true);
break;
case 'slicebar':
@ -313,7 +267,6 @@ if (file_exists('languages/'.$user_language.'.mo') === true) {
}
echo '</div>';
echoPhantomCallback();
?>
</body>
</html>

View File

@ -16,7 +16,11 @@
* @subpackage Generic_Functions
*/
/**
use HeadlessChromium\BrowserFactory;
use HeadlessChromium\Clip;
use HeadlessChromium\Page;
/*
* Include the html and ui functions.
*/
require_once 'functions_html.php';
@ -4209,8 +4213,7 @@ function generator_chart_to_pdf(
$hack_metaconsole = '';
}
$file_js = $config['homedir'].'/include/web2image.js';
$url = ui_get_full_url(false).$hack_metaconsole.'/include/chart_generator.php';
$url = ui_get_full_url(false).$hack_metaconsole.'/include/chart_generator.php';
if (!$params['return_img_base_64']) {
$img_file = 'img_'.uniqid().'.png';
@ -4218,65 +4221,65 @@ function generator_chart_to_pdf(
$img_url = ui_get_full_url(false).$hack_metaconsole.'/attachment/'.$img_file;
}
if ($type_graph_pdf === 'vbar') {
$width_img = $params['generals']['pdf']['width'];
$height_img = $params['generals']['pdf']['height'];
} else if ($type_graph_pdf === 'combined'
&& $params_combined['stacked'] == CUSTOM_GRAPH_VBARS
) {
$width_img = 650;
$height_img = ($params['height'] + 50);
} else if ($type_graph_pdf === 'hbar' || $type_graph_pdf === 'pie_chart') {
$width_img = ($params['width'] ?? 550);
$height_img = $params['height'];
} else {
$width_img = 550;
$height_img = $params['height'];
if ((int) $params['landscape'] === 1) {
$height_img = 150;
$params['height'] = 150;
}
}
$params_encode_json = urlencode(json_encode($params));
if ($params_combined) {
$params_combined = urlencode(json_encode($params_combined));
}
if ($module_list) {
$module_list = urlencode(json_encode($module_list));
}
$session_id = session_id();
$cache_dir = $config['homedir'].'/attachment/cache';
if ($type_graph_pdf === 'combined') {
$data = [
'data' => $params,
'session_id' => $session_id,
'type_graph_pdf' => $type_graph_pdf,
'data_module_list' => $module_list,
'data_combined' => $params_combined,
];
} else {
$data = [
'data' => $params,
'session_id' => $session_id,
'type_graph_pdf' => $type_graph_pdf,
];
}
$cmd = '"'.io_safe_output($config['phantomjs_bin']);
$cmd .= DIRECTORY_SEPARATOR.'phantomjs" ';
$cmd .= ' --disk-cache=true --disk-cache-path="'.$cache_dir.'"';
$cmd .= ' --max-disk-cache-size=10000 ';
$cmd .= ' --ssl-protocol=any --ignore-ssl-errors=true ';
$cmd .= '"'.$file_js.'" "'.$url.'" "'.$type_graph_pdf.'"';
$cmd .= ' "'.$params_encode_json.'" "'.$params_combined.'"';
$cmd .= ' "'.$module_list.'" "'.$img_path.'"';
$cmd .= ' "'.$width_img.'" "'.$height_img.'"';
$cmd .= ' "'.$session_id.'" "'.$params['return_img_base_64'].'"';
$browserFactory = new BrowserFactory('chromium-browser');
$result = null;
$retcode = null;
exec($cmd, $result, $retcode);
// Starts headless chrome.
$browser = $browserFactory->createBrowser(['noSandbox' => true]);
try {
// Creates a new page and navigate to an URL.
$page = $browser->createPage();
$img_content = join("\n", $result);
$page->navigate($url.'?data='.urlencode(json_encode($data)))->waitForNavigation(Page::DOM_CONTENT_LOADED);
$dynamic_height = $page->evaluate('document.getElementById("container-chart-generator-item").clientHeight')->getReturnValue();
if (empty($dynamic_height) === true) {
$dynamic_height = 200;
}
if ($params['return_img_base_64']) {
$width = 794;
if (isset($params['options']['viewport']) === true
&& isset($params['options']['viewport']['width']) === true
) {
$width = $params['options']['viewport']['width'];
}
$clip = new Clip(0, 0, $width, $dynamic_height);
$b64 = $page->screenshot(['clip' => $clip])->getBase64();
} catch (\Throwable $th) {
hd($th, true);
} finally {
$browser->close();
}
// TODO: XXX chartjs.
/*
if ($params['return_img_base_64']) {
// To be used in alerts.
return $img_content;
} else {
} else {
// To be used in PDF files.
$config['temp_images'][] = $img_path;
return '<img src="'.$img_url.'" />';
}
}
*/
return $b64;
}

View File

@ -2600,27 +2600,6 @@ function events_print_event_table(
$events_table = html_print_table($table, true);
$out = $events_table;
if (!$tactical_view) {
$out .= '<table width="100%"><tr><td class="w90p align-top pdd_t_0px">';
if ($agent_id != 0) {
$out .= '</td><td class="w200px align-top">';
$out .= '<table cellpadding=0 cellspacing=0 class="databox"><tr><td>';
$out .= '<fieldset class="databox tactical_set">
<legend>'.__('Events -by module-').'</legend>'.graph_event_module(180, 100, $event['id_agente']).'</fieldset>';
$out .= '</td></tr></table>';
} else {
$out .= '</td><td class="w200px align-top">';
$out .= '<table cellpadding=0 cellspacing=0 class="databox"><tr><td>';
$out .= '<fieldset class="databox tactical_set">
<legend>'.__('Event graph').'</legend>'.grafico_eventos_total('', 180, 60).'</fieldset>';
$out .= '<fieldset class="databox tactical_set">
<legend>'.__('Event graph by agent').'</legend>'.grafico_eventos_grupo(180, 60).'</fieldset>';
$out .= '</td></tr></table>';
}
$out .= '</td></tr></table>';
}
unset($table);
if ($return) {

View File

@ -814,14 +814,7 @@ function grafico_modulo_sparse($params)
}
if (isset($params['agent_module_id']) === false) {
return graph_nodata_image(
$params['width'],
$params['height'],
'area',
'',
false,
$params['pdf']
);
return graph_nodata_image($params);
} else {
$agent_module_id = $params['agent_module_id'];
}
@ -1024,14 +1017,7 @@ function grafico_modulo_sparse($params)
$array_events_alerts
);
} else {
$return = graph_nodata_image(
$params['width'],
$params['height'],
'area',
'',
false,
$params['pdf']
);
$return = graph_nodata_image($params);
}
$return .= '<br>';
@ -1058,14 +1044,7 @@ function grafico_modulo_sparse($params)
$array_events_alerts
);
} else {
$return = graph_nodata_image(
$params['width'],
$params['height'],
'area',
'',
false,
$params['pdf']
);
$return = graph_nodata_image($params);
}
} else {
if (empty($array_data) === false) {
@ -1082,14 +1061,7 @@ function grafico_modulo_sparse($params)
$array_events_alerts
);
} else {
$return = graph_nodata_image(
$params['width'],
$params['height'],
'area',
__('No data to display within the selected interval'),
false,
$params['pdf']
);
$return = graph_nodata_image($params);
}
}
@ -1550,6 +1522,12 @@ function graphic_combined_module(
$height = $params['height'];
$homeurl = $params['homeurl'];
$ttl = $params['ttl'];
$date_array = [];
$date_array['period'] = $params['period'];
$date_array['final_date'] = $params['date'];
$date_array['start_date'] = ($params['date'] - $params['period']);
$background_color = $params['backgroundColor'];
$datelimit = $date_array['start_date'];
$fixed_font_size = $config['font_size'];
@ -1733,10 +1711,10 @@ function graphic_combined_module(
if (empty($array_data) === true) {
if ($params_combined['return']) {
return graph_nodata_image($width, $height);
return graph_nodata_image($params);
}
echo graph_nodata_image($width, $height);
echo graph_nodata_image($params);
return false;
}
@ -2409,8 +2387,7 @@ function graphic_combined_module(
}
}
$temp['total_modules'] = $total_modules;
// $temp['total_modules'] = ['value' => $total_modules];
$graph_values = $temp;
if ($params['vconsole'] === false) {
@ -2420,24 +2397,25 @@ function graphic_combined_module(
$water_mark = false;
}
// TODO: XXX chartjs.
$color = color_graph_array();
$width = null;
$height = null;
$output = ring_graph(
$graph_values,
$width,
$height,
$others_str,
$homeurl,
$water_mark,
$config['fontpath'],
($config['font_size'] + 1),
$ttl,
false,
$color,
false,
$background_color,
$params['pdf']
);
$options = [
'width' => $width,
'height' => $height,
'waterMark' => $water_mark,
'ttl' => $ttl,
'background' => $background_color,
'pdf' => $params['pdf'],
];
$output = '<div style="display: flex; flex-direction:row; justify-content: center; align-items: center; align-content: center; width:100%; height:100%;">';
$output .= '<div style="flex: 0 0 auto; width:99%; height:100%;">';
$output .= ring_graph($graph_values, $options);
$output .= '</div>';
$output .= '</div>';
break;
}
@ -2651,19 +2629,16 @@ function graph_alert_status($defined_alerts, $fired_alerts, $width=300, $height=
];
}
$options = [
'width' => $width,
'height' => $height,
'colors' => $colors,
'legend' => ['display' => false],
];
$out = pie_graph(
$data,
$width,
$height,
__('other'),
'',
'',
$config['fontpath'],
$config['font_size'],
1,
'hidden',
$colors,
false
$options
);
if ($return) {
@ -2776,30 +2751,23 @@ function graph_agent_status(
$data = [];
}
$options = [
'width' => $width,
'height' => $height,
'colors' => array_values($colors),
'legend' => ['display' => false],
];
if ($donut_narrow_graph == true) {
$data_total = array_sum($data);
$out = print_donut_narrow_graph(
$colors,
$width,
$height,
$out = ring_graph(
$data,
$data_total
$options
);
return $out;
} else {
$out = pie_graph(
$data,
$width,
$height,
__('other'),
ui_get_full_url(false, false, false, false),
'',
$config['fontpath'],
$config['font_size'],
1,
'hidden',
$colors,
0
$options
);
if ($return) {
@ -2885,6 +2853,8 @@ function graph_event_module($width=300, $height=200, $id_agent=null)
];
}
hd('aaaaaaaaaaa');
return pie_graph(
$data,
$width,
@ -3411,17 +3381,20 @@ function grafico_eventos_grupo($width=300, $height=200, $url='', $noWaterMark=tr
$water_mark = [];
}
$options = [
'width' => $width,
'height' => $height,
'water_mark' => $water_mark,
'legend' => [
'display' => true,
'position' => 'right',
'align' => 'center',
],
];
return pie_graph(
$data,
$width,
$height,
__('Other'),
'',
$water_mark,
$config['fontpath'],
$config['font_size'],
1,
'bottom'
$options
);
}
@ -3438,7 +3411,7 @@ function grafico_eventos_total($filter='', $width=320, $height=200, $noWaterMark
$filter = str_replace('\\', '', $filter);
// Add tags condition to filter
// Add tags condition to filter.
$tags_condition = tags_get_acl_tags($config['id_user'], 0, 'ER', 'event_condition', 'AND');
$filter .= $tags_condition;
if ($time_limit && $config['event_view_hr']) {
@ -3519,82 +3492,21 @@ function grafico_eventos_total($filter='', $width=320, $height=200, $noWaterMark
$water_mark = [];
}
return pie_graph(
$data,
$width,
$height,
__('Other'),
'',
$water_mark,
$config['fontpath'],
$config['font_size'],
1,
'bottom',
$colors
);
}
/**
* Print a pie graph with events data of users
*
* @param integer height pie graph height
* @param integer period time period
*/
function grafico_eventos_usuario($width, $height)
{
global $config;
global $graphic_type;
$data = [];
$max_items = 5;
$where = '';
if (!users_is_admin()) {
$where = 'WHERE event_type NOT IN (\'recon_host_detected\', \'system\',\'error\', \'new_agent\', \'configuration_change\')';
}
$sql = sprintf(
'SELECT COUNT(id_evento) events, id_usuario
FROM tevento %s
GROUP BY id_usuario
ORDER BY 1 DESC LIMIT %d',
$where,
$max_items
);
$events = db_get_all_rows_sql($sql);
if ($events === false) {
$events = [];
}
foreach ($events as $event) {
if ($event['id_usuario'] == '0') {
$data[__('System')] = $event['events'];
} else if ($event['id_usuario'] == '') {
$data[__('System')] = $event['events'];
} else {
$data[$event['id_usuario']] = $event['events'];
}
}
$water_mark = [
'file' => $config['homedir'].'/images/logo_vertical_water.png',
'url' => ui_get_full_url('/images/logo_vertical_water.png', false, false, false),
$options = [
'width' => $width,
'height' => $height,
'water_mark' => $water_mark,
'colors' => array_values($colors),
'legend' => [
'display' => true,
'position' => 'right',
'align' => 'center',
],
];
return pie_graph(
$data,
$width,
$height,
__('Other'),
'',
$water_mark,
$config['fontpath'],
$config['font_size'],
1,
'bottom'
$options
);
}
@ -3854,18 +3766,30 @@ function graph_custom_sql_graph(
break;
case 'sql_graph_pie':
$options = [
'width' => $width,
'height' => $height,
'waterMark' => $water_mark,
'ttl' => $ttl,
];
if ((int) $ttl === 2) {
$output .= '<img src="data:image/png;base64,';
} else {
$output .= '<div style="margin: 0 auto; width:'.$options_charts['width'].'px;">';
}
// Pie.
$output .= pie_graph(
$data,
$width,
$height,
__('other'),
$homeurl,
$water_mark,
$config['fontpath'],
$config['font_size'],
$ttl
$options
);
if ((int) $ttl === 2) {
$output .= '" />';
} else {
$output .= '</div>';
}
break;
}
@ -4129,8 +4053,7 @@ function graph_graphic_moduleevents(
*/
function fs_error_image($width=300, $height=110)
{
global $config;
return graph_nodata_image($width, $height, 'area');
return graph_nodata_image(['height' => $height]);
}
@ -5003,40 +4926,52 @@ function graphic_module_events($id_module, $width, $height, $period=0, $homeurl=
}
function graph_nodata_image(
$width=300,
$height=110,
$type='area',
$text='',
$percent=false,
$base64=false
) {
function graph_nodata_image($options)
{
global $config;
if ($base64 === true) {
$height = 200;
if (isset($options['height']) === true
&& empty($options['height']) === false
) {
$height = $options['height'];
}
return html_print_image(
'images/image_problem_area.png',
true,
[
'title' => __('No data'),
'style' => 'height:'.$height.'px;',
]
);
/*
if ($base64 === true) {
$dataImg = file_get_contents(
$config['homedir'].'/images/image_problem_area_150.png'
);
return base64_encode($dataImg);
}
}
$image = ui_get_full_url(
$image = ui_get_full_url(
'images/image_problem_area.png',
false,
false,
false
);
);
$style = 'text-align:center; padding: 30px 0; display:block; font-size:9.5pt;';
$text_div = '<div class="nodata_text" style="'.$style.'">';
$text_div .= $text;
$text_div .= '</div>';
$style = 'text-align:center; padding: 30px 0; display:block; font-size:9.5pt;';
$text_div = '<div class="nodata_text" style="'.$style.'">';
$text_div .= $text;
$text_div .= '</div>';
$style = 'background-size: contain;background-image: url(\''.$image.'\');';
$image_div = '<div class="nodata_container" style="'.$style.'"></div>';
$style = 'background-size: contain;background-image: url(\''.$image.'\');';
$image_div = '<div class="nodata_container" style="'.$style.'"></div>';
if ($percent === true) {
if ($percent === true) {
$div = $image_div;
} else {
} else {
if (strpos($width, '%') === false) {
$width = 'width: '.$width.'px;';
} else {
@ -5046,9 +4981,10 @@ function graph_nodata_image(
$style = $width.' height:'.$height.'px;';
$style .= 'margin: 0 auto;';
$div = '<div style="'.$style.'">'.$image_div.'</div>';
}
}
return $div;
return $div;
*/
}

View File

@ -1479,7 +1479,7 @@ function netflow_draw_item(
}
if ($output === 'HTML' || $output === 'PDF') {
return graph_nodata_image(300, 110, 'data');
return graph_nodata_image(['height' => 110]);
}
}

View File

@ -844,7 +844,8 @@ function reporting_make_reporting_data(
$content,
$type,
$force_width_chart,
$force_height_chart
$force_height_chart,
$pdf
);
if ($report_control['total_events'] == 0 && $content['hide_no_data'] == 1) {
break;
@ -2008,7 +2009,8 @@ function reporting_event_report_group(
$content,
$type='dinamic',
$force_width_chart=null,
$force_height_chart=null
$force_height_chart=null,
$pdf=false
) {
global $config;
@ -2154,6 +2156,23 @@ function reporting_event_report_group(
$return['chart']['by_criticity'] = null;
$return['chart']['validated_vs_unvalidated'] = null;
$options_charts = [
'width' => 500,
'height' => 150,
'radius' => null,
'viewport' => [
'width' => 500,
'height' => 0,
],
'legend' => [
'display' => true,
'position' => 'right',
'align' => 'center',
],
'pdf' => $pdf,
'ttl' => $ttl,
];
if ($event_graph_by_agent) {
$data_graph_by_agent = [];
if (empty($data) === false) {
@ -2171,32 +2190,42 @@ function reporting_event_report_group(
}
}
$return['chart']['by_agent'] = pie_graph(
if ($pdf === true) {
$return['chart']['by_agent'] = '<img src="data:image/png;base64,';
} else {
$return['chart']['by_agent'] = '<div style="margin: 0 auto; width:'.$options_charts['width'].'px;">';
}
$return['chart']['by_agent'] .= pie_graph(
$data_graph_by_agent,
500,
150,
__('other'),
ui_get_full_url(false, false, false, false),
ui_get_full_url(false, false, false, false).'/images/logo_vertical_water.png',
$config['fontpath'],
$config['font_size'],
$ttl
$options_charts
);
if ($pdf === true) {
$return['chart']['by_agent'] .= '" />';
} else {
$return['chart']['by_agent'] .= '</div>';
}
}
if ($event_graph_by_user_validator) {
$data_graph_by_user = events_get_count_events_validated_by_user($data);
$return['chart']['by_user_validator'] = pie_graph(
if ($pdf === true) {
$return['chart']['by_user_validator'] = '<img src="data:image/png;base64,';
} else {
$return['chart']['by_user_validator'] = '<div style="margin: 0 auto; width:'.$options_charts['width'].'px;">';
}
$return['chart']['by_user_validator'] .= pie_graph(
$data_graph_by_user,
500,
150,
__('other'),
ui_get_full_url(false, false, false, false),
ui_get_full_url(false, false, false, false).'/images/logo_vertical_water.png',
$config['fontpath'],
$config['font_size'],
$ttl
$options_charts
);
if ($pdf === true) {
$return['chart']['by_user_validator'] .= '" />';
} else {
$return['chart']['by_user_validator'] .= '</div>';
}
}
if ($event_graph_by_criticity) {
@ -2213,20 +2242,26 @@ function reporting_event_report_group(
}
$colors = get_criticity_pie_colors($data_graph_by_criticity);
$options_charts['colors'] = array_values($colors);
$return['chart']['by_criticity'] = pie_graph(
if ($pdf === true) {
$return['chart']['by_criticity'] = '<img src="data:image/png;base64,';
} else {
$return['chart']['by_criticity'] = '<div style="margin: 0 auto; width:'.$options_charts['width'].'px;">';
}
$return['chart']['by_criticity'] .= pie_graph(
$data_graph_by_criticity,
500,
150,
__('other'),
ui_get_full_url(false, false, false, false),
ui_get_full_url(false, false, false, false).'/images/logo_vertical_water.png',
$config['fontpath'],
$config['font_size'],
$ttl,
false,
$colors
$options_charts
);
if ($pdf === true) {
$return['chart']['by_criticity'] .= '" />';
} else {
$return['chart']['by_criticity'] .= '</div>';
}
unset($options_charts['colors']);
}
if ($event_graph_validated_vs_unvalidated) {
@ -2246,17 +2281,22 @@ function reporting_event_report_group(
}
}
$return['chart']['validated_vs_unvalidated'] = pie_graph(
if ($pdf === true) {
$return['chart']['validated_vs_unvalidated'] = '<img src="data:image/png;base64,';
} else {
$return['chart']['validated_vs_unvalidated'] = '<div style="margin: 0 auto; width:'.$options_charts['width'].'px;">';
}
$return['chart']['validated_vs_unvalidated'] .= pie_graph(
$data_graph_by_status,
500,
150,
__('other'),
ui_get_full_url(false, false, false, false),
ui_get_full_url(false, false, false, false).'/images/logo_vertical_water.png',
$config['fontpath'],
$config['font_size'],
$ttl
$options_charts
);
if ($pdf === true) {
$return['chart']['validated_vs_unvalidated'] .= '" />';
} else {
$return['chart']['validated_vs_unvalidated'] .= '</div>';
}
}
// Total events.
@ -15311,11 +15351,7 @@ function reporting_module_histogram_graph($report, $content, $pdf=0)
$report['datetime']
);
} else {
$return['chart'] = graph_nodata_image(
$width_graph,
$height_graph,
'area'
);
$return['chart'] = graph_nodata_image(['height' => $height_graph]);
}
if ($metaconsole_on && $server_name != '') {

View File

@ -5131,7 +5131,17 @@ function reporting_get_stats_summary($data, $graph_width, $graph_height)
// Fixed width non interactive charts.
$status_chart_width = $graph_width;
$tdata[0] = '<div style="margin: auto; width: '.$graph_width.'px;"><div id="status_pie" style="margin: auto; width: '.$graph_width.'">'.graph_agent_status(false, $graph_width, $graph_height, true, true).'</div></div>';
$tdata[0] = '<div style="margin: auto; width: '.$graph_width.'px;">';
$tdata[0] .= '<div id="status_pie" style="margin: auto; width: '.$graph_width.'">';
$tdata[0] .= graph_agent_status(
false,
$graph_width,
$graph_height,
true,
true
);
$tdata[0] .= '</div>';
$tdata[0] .= '</div>';
} else {
$tdata[2] = html_print_image(
'images/image_problem_area_small.png',
@ -5141,7 +5151,16 @@ function reporting_get_stats_summary($data, $graph_width, $graph_height)
}
if ($data['monitor_alerts'] > 0) {
$tdata[2] = '<div style="margin: auto; width: '.$graph_width.'px;">'.graph_alert_status($data['monitor_alerts'], $data['monitor_alerts_fired'], $graph_width, $graph_height, true, true).'</div>';
$tdata[2] = '<div style="margin: auto; width: '.$graph_width.'px;">';
$tdata[2] .= graph_alert_status(
$data['monitor_alerts'],
$data['monitor_alerts_fired'],
$graph_width,
$graph_height,
true,
true
);
$tdata[2] .= '</div>';
} else {
$tdata[2] = html_print_image(
'images/image_problem_area_small.png',
@ -5150,8 +5169,8 @@ function reporting_get_stats_summary($data, $graph_width, $graph_height)
);
}
$table_sum->rowclass[] = '';
$table_sum->data[] = $tdata;
$table_sum->rowclass[] = '';
$table_sum->data[] = $tdata;
$output = '<fieldset class="databox tactical_set">
<legend>'.__('Summary').'</legend>'.html_print_table($table_sum, true).'</fieldset>';

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,8 @@
// Turn on output buffering.
// The entire buffer will be discarded later so that any accidental output
// does not corrupt images generated by fgraph.
use Nutsy\PHPChartJS\Factory;
ob_start();
global $config;
@ -566,14 +568,7 @@ function vbar_graph(
}
if (empty($params['data']) === true) {
return graph_nodata_image(
0,
0,
'vbar',
'',
true,
($ttl === 2) ? true : false
);
return graph_nodata_image($options);
}
if ((int) $ttl === 2) {
@ -730,7 +725,7 @@ function hbar_graph(
}
if ($chart_data === false || empty($chart_data) === true) {
return graph_nodata_image($width, $height, 'hbar');
return graph_nodata_image($options);
}
if ($ttl == 2) {
@ -776,38 +771,57 @@ function hbar_graph(
function pie_graph(
$chart_data,
$width,
$height,
$others_str='other',
$homedir='',
$water_mark='',
$font='',
$font_size=8,
$ttl=1,
$legend_position=false,
$colors='',
$hide_labels=false,
$max_values=9
$options
) {
/*
$width,
$height,
$others_str='other',
$homedir='',
$water_mark='',
$font='',
$font_size=8,
$ttl=1,
$legend_position=false,
$colors='',
$hide_labels=false,
$max_values=9
*/
if (empty($chart_data) === true) {
return graph_nodata_image($width, $height, 'pie');
return graph_nodata_image($options);
}
if ($water_mark !== false) {
if ((int) $options['ttl'] === 2) {
$params = [
'chart_data' => $chart_data,
'options' => $options,
'return_img_base_64' => true,
];
return generator_chart_to_pdf('pie_graph', $params);
}
$chart = get_build_setup_charts('PIE', $options, $chart_data);
$output = $chart->render(true, true);
return $output;
/*
if ($water_mark !== false) {
setup_watermark($water_mark, $water_mark_file, $water_mark_url);
}
}
// This library allows only 8 colors.
// $max_values = 9;
// Remove the html_entities.
$temp = [];
foreach ($chart_data as $key => $value) {
// This library allows only 8 colors.
// $max_values = 9;
// Remove the html_entities.
$temp = [];
foreach ($chart_data as $key => $value) {
$temp[io_safe_output($key)] = $value;
}
}
$chart_data = $temp;
$chart_data = $temp;
if (count($chart_data) > $max_values) {
if (count($chart_data) > $max_values) {
$chart_data_trunc = [];
$n = 1;
foreach ($chart_data as $key => $value) {
@ -825,9 +839,9 @@ function pie_graph(
}
$chart_data = $chart_data_trunc;
}
}
if ($ttl == 2) {
if ($ttl == 2) {
$params = [
'values' => array_values($chart_data),
'keys' => array_keys($chart_data),
@ -842,9 +856,9 @@ function pie_graph(
];
return generator_chart_to_pdf('pie_chart', $params);
}
}
return flot_pie_chart(
return flot_pie_chart(
array_values($chart_data),
array_keys($chart_data),
$width,
@ -855,77 +869,293 @@ function pie_graph(
$legend_position,
$colors,
$hide_labels
);
);
*/
}
/**
* Rin graph DOUGHNUT.
*
* @param array $chart_data Data.
* @param array $options Options.
*
* @return string Output html charts
*/
function ring_graph(
$chart_data,
$width,
$height,
$others_str='other',
$homedir='',
$water_mark='',
$font='',
$font_size='',
$ttl=1,
$legend_position=false,
$colors='',
$hide_labels=false,
$background_color='white',
$pdf=false
$options
) {
if (empty($chart_data)) {
return graph_nodata_image($width, $height, 'pie');
global $config;
// TODO: XXX chartjs.
// $ttl
// $hide_labels
// $background_color
// $pdf
if (empty($chart_data) === true) {
return graph_nodata_image($options);
}
setup_watermark($water_mark, $water_mark_file, $water_mark_url);
// This library allows only 8 colors
$max_values = 18;
if ($ttl == 2) {
if ((int) $options['ttl'] === 2) {
$params = [
'chart_data' => $chart_data,
'width' => $width,
'height' => $height,
'colors' => $colors,
'module_name_list' => $module_name_list,
'long_index' => $long_index,
'no_data' => $no_data,
'water_mark' => $water_mark,
'font' => $font,
'font_size' => $font_size,
'unit' => $unit,
'ttl' => $ttl,
'homeurl' => $homeurl,
'background_color' => $background_color,
'legend_position' => $legend_position,
'pdf' => $pdf,
'chart_data' => $chart_data,
'options' => $options,
'return_img_base_64' => true,
];
return generator_chart_to_pdf('ring_graph', $params);
}
return flot_custom_pie_chart(
$chart_data,
$width,
$height,
$colors,
$module_name_list,
$long_index,
$no_data,
false,
'',
$water_mark,
$font,
$font_size,
$unit,
$ttl,
$homeurl,
$background_color,
$legend_position,
$background_color,
$pdf
);
$chart = get_build_setup_charts('DOUGHNUT', $options, $chart_data);
$output = $chart->render(true, true);
return $output;
}
function get_build_setup_charts($type, $options, $data)
{
global $config;
$factory = new Factory();
switch ($type) {
case 'DOUGHNUT':
$chart = $factory->create($factory::DOUGHNUT);
break;
case 'PIE':
$chart = $factory->create($factory::PIE);
break;
default:
// code...
break;
}
$example = [
'id' => null,
'width' => null,
'height' => null,
'maintainAspectRatio' => false,
'responsive' => true,
'radius' => null,
'rotation' => null,
'circumference' => null,
'legend' => [
'display' => true,
'position' => 'top',
'align' => 'center',
'font' => [
'family' => '',
'size' => 12,
'style' => 'normal',
'weight' => null,
'lineHeight' => 1.2,
],
],
'title' => [
'display' => true,
'position' => 'top',
'color' => '',
'align' => 'center',
'text' => '',
'font' => [
'family' => '',
'size' => 12,
'style' => 'normal',
'weight' => null,
'lineHeight' => 1.2,
],
],
];
// Set Id.
$id = uniqid('graph_');
if (isset($options['id']) === true && empty($options['id']) === false) {
$id = $options['id'];
}
$chart->setId($id);
// Height is null maximum possible.
if (isset($options['height']) === true
&& empty($options['height']) === false
) {
$chart->setHeight($options['height']);
}
// Width is null maximum possible.
if (isset($options['width']) === true
&& empty($options['width']) === false
) {
$chart->setWidth($options['width']);
}
// Fonts.
// $chart->defaults()->getFonts()->setFamily($config['fontpath']);
// $chart->defaults()->getFonts()->setStyle('normal');
$chart->defaults()->getFonts()->setSize(($config['font_size'] + 5));
if (isset($options['waterMark']) === true
&& empty($options['waterMark']) === false
) {
// WaterMark.
$chart->defaults()->getWaterMark()->setWidth(88);
$chart->defaults()->getWaterMark()->setHeight(16);
$chart->defaults()->getWaterMark()->setSrc($options['waterMark']['url']);
$chart->defaults()->getWaterMark()->setPosition('end');
$chart->defaults()->getWaterMark()->setAlign('top');
}
if (isset($options['pdf']) === true && $options['pdf'] === true) {
$chart->options()->disableAnimation(false);
}
// Set Maintain Aspect Ratio for responsive charts.
$maintainAspectRatio = false;
if (isset($options['maintainAspectRatio']) === true
&& empty($options['maintainAspectRatio']) === false
) {
$maintainAspectRatio = $options['maintainAspectRatio'];
}
$chart->options()->setMaintainAspectRatio($maintainAspectRatio);
// Set Responsive for responsive charts.
$responsive = true;
if (isset($options['responsive']) === true
&& empty($options['responsive']) === false
) {
$responsive = $options['responsive'];
}
$chart->options()->setResponsive($responsive);
// LEGEND.
// Set Display legends.
$legendDisplay = true;
if (isset($options['legend']['display']) === true) {
$legendDisplay = $options['legend']['display'];
}
$chart->options()->getPlugins()->getLegend()->setDisplay($legendDisplay);
// Set Position legends.
$legendPosition = 'top';
if (isset($options['legend']['position']) === true
&& empty($options['legend']['position']) === false
) {
$legendPosition = $options['legend']['position'];
}
$chart->options()->getPlugins()->getLegend()->setPosition($legendPosition);
// Set Align legends.
$legendAlign = 'center';
if (isset($options['legend']['align']) === true
&& empty($options['legend']['align']) === false
) {
$legendAlign = $options['legend']['align'];
}
$chart->options()->getPlugins()->getLegend()->setAlign($legendAlign);
// Title.
if (isset($options['title']) === true
&& empty($options['title']) === false
&& is_array($options['title']) === true
) {
$display = false;
if (isset($options['title']['display']) === true) {
$display = $options['title']['display'];
}
$chart->options()->getPlugins()->getTitle()->setDisplay($display);
$text = __('Title');
if (isset($options['title']['text']) === true) {
$text = $options['title']['text'];
}
$chart->options()->getPlugins()->getTitle()->setText($text);
$position = 'top';
if (isset($options['title']['position']) === true) {
$position = $options['title']['position'];
}
$chart->options()->getPlugins()->getTitle()->setPosition($position);
$color = 'top';
if (isset($options['title']['color']) === true) {
$color = $options['title']['color'];
}
$chart->options()->getPlugins()->getTitle()->setColor($color);
if (isset($options['title']['fonts']) === true
&& empty($options['title']['fonts']) === false
&& is_array($options['title']['fonts']) === true
) {
if (isset($options['title']['fonts']['size']) === true) {
$chart->options()->getPlugins()->getTitle()->getFonts()->setSize($options['title']['fonts']['size']);
}
if (isset($options['title']['fonts']['style']) === true) {
$chart->options()->getPlugins()->getTitle()->getFonts()->setStyle($options['title']['fonts']['style']);
}
if (isset($options['title']['fonts']['family']) === true) {
$chart->options()->getPlugins()->getTitle()->getFonts()->setFamily($options['title']['fonts']['family']);
}
}
}
// Radius is null maximum possible.
if (isset($options['radius']) === true
&& empty($options['radius']) === false
) {
$chart->setRadius($options['radius']);
}
// Rotation is null 0º.
if (isset($options['rotation']) === true
&& empty($options['rotation']) === false
) {
$chart->setRotation($options['rotation']);
}
// Circumferende is null 360º.
if (isset($options['circumference']) === true
&& empty($options['circumference']) === false
) {
$chart->setCircumference($options['circumference']);
}
// Color.
if (isset($options['colors']) === true
&& empty($options['colors']) === false
&& is_array($options['colors']) === true
) {
$colors = $options['colors'];
} else {
// Colors.
$defaultColor = [];
$defaultColorArray = color_graph_array();
foreach ($defaultColorArray as $key => $value) {
$defaultColor[$key] = $value['color'];
}
$colors = array_values($defaultColor);
}
// Set labels.
$chart->labels()->exchangeArray(array_keys($data));
// Add Datasets.
$setData = $chart->createDataSet();
$setData->setLabel('data')->setBackgroundColor($colors)->data()->exchangeArray(array_values($data));
$chart->addDataSet($setData);
return $chart;
}

View File

@ -756,71 +756,3 @@ function print_clock_digital_1($time_format, $timezone, $clock_animation, $width
return $output;
}
/**
* Print dougnhnut.
*
* @param array $colors Colors.
* @param integer $width Width.
* @param integer $height Height.
* @param array $data Data.
* @param mixed $data_total Data_total.
*
* @return string HTML.
*/
function print_donut_narrow_graph(
array $colors,
$width,
$height,
array $data,
$data_total
) {
global $config;
if (empty($data)) {
return graph_nodata_image($width, $height, 'pie');
}
$series = count($data);
if (($series != count($colors)) || ($series == 0)) {
return '';
}
$data = json_encode($data);
$colors = json_encode($colors);
$graph_id = uniqid('graph_');
// This is for "Style template" in visual styles.
switch ($config['style']) {
case 'pandora':
$textColor = '#000';
$strokeColor = '#fff';
break;
case 'pandora_black':
$textColor = '#fff';
$strokeColor = '#222';
break;
default:
$textColor = '#000';
$strokeColor = '#fff';
break;
}
$textColor = json_encode($textColor);
$strokeColor = json_encode($strokeColor);
$out = "<div id='$graph_id'></div>";
$out .= include_javascript_d3(true);
$out .= "<script type='text/javascript'>
donutNarrowGraph($colors, $width, $height, $data_total, $textColor, $strokeColor)
.donutbody(d3.select($graph_id))
.data($data)
.render();
</script>";
return $out;
}

View File

@ -52,6 +52,10 @@ function include_javascript_dependencies_flot_graph($return=false, $mobile=false
<script language="javascript" type="text/javascript" src="'.ui_get_full_url($metaconsole_hack.'/include/graphs/flot/jquery.flot.exportdata.pandora.js').'"></script>
<script language="javascript" type="text/javascript" src="'.ui_get_full_url($metaconsole_hack.'/include/graphs/flot/jquery.flot.axislabels.js').'"></script>
<script language="javascript" type="text/javascript" src="'.ui_get_full_url($metaconsole_hack.'/include/graphs/flot/pandora.flot.js').'"></script>';
// Chartjs.
$output .= '<script language="javascript" type="text/javascript" src="'.ui_get_full_url($metaconsole_hack.'/include/graphs/chartjs/chart.js').'"></script>';
$output .= "
<script type='text/javascript'>
var precision_graph = ".$config['graph_precision'].";

View File

@ -2797,229 +2797,3 @@ function valueToBytes(value) {
// This will actually do the rounding and the decimals.
return value.toFixed(2) + shorts[pos] + "B";
}
function donutNarrowGraph(
colores,
width,
height,
total,
textColor,
strokeColor
) {
// Default settings
var donutbody = d3.select("body");
var data = {};
// var showTitle = true;
if (width == "") {
width = 180;
}
if (height == "") {
height = 180;
}
var radius = Math.min(width, height) / 2;
var currentVal;
//var color = d3.scale.category20();
var colores_index = [];
var colores_value = [];
$.each(colores, function(index, value) {
colores_index.push(index);
colores_value.push(value);
});
var color = d3.scale
.ordinal()
.domain(colores_index)
.range(colores_value);
var pie = d3.layout
.pie()
.sort(null)
.value(function(d) {
return d.value;
});
var svg, g, arc;
var object = {};
// Method for render/refresh graph
object.render = function() {
if (!svg) {
// Show normal status by default. This variable must be initialized here, before clearing data.
var normal_status = data.Normal;
// Don't draw 0 or invalid values. console.log(data);
var data_map = $.map(data, function(value, index) {
if (value == 0 || isNaN(value)) {
return index;
}
});
$.each(data_map, function(i, val) {
delete data[val];
});
//New data: console.log(data);
arc = d3.svg
.arc()
.outerRadius(radius)
.innerRadius(radius - radius / 4);
svg = donutbody
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
g = svg
.selectAll(".arc")
.data(pie(d3.entries(data)))
.enter()
.append("g")
.attr("class", "arc");
g.append("path")
// Attach current value to g so that we can use it for animation
.each(function(d) {
this._current = d;
})
.attr("d", arc)
.attr("stroke", strokeColor)
.style("fill", function(d) {
return color(d.data.key);
});
// This is to show labels on the graph
/* g.append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")";
})
.attr("dy", ".35em")
.style("text-anchor", "middle");
g.select("text").text(function(d) {
return d.data.key;
});*/
// Show normal status by default.
var percentage_normal;
svg
.append("text")
.datum(data)
.attr("x", 0)
.attr("y", 0 + radius / 10)
.attr("class", "text-tooltip")
.style("text-anchor", "middle")
.attr("fill", textColor)
.style("font-size", function(d) {
if (normal_status) {
percentage_normal = (normal_status * 100) / total;
if (Number.isInteger(percentage_normal)) {
percentage_normal = percentage_normal.toFixed(0);
return radius / 3 + "px";
} else {
percentage_normal = percentage_normal.toFixed(1);
return radius / 3.5 + "px";
}
}
})
.text(function(d) {
if (normal_status) {
return percentage_normal + "%";
} else {
return "0%";
}
});
g.on("mouseover", function(obj) {
//console.log(obj);
var percentage;
svg
.select("text.text-tooltip")
// This is to paint the text of the corresponding color.
/* .attr("fill", function(d) {
return color(obj.data.key);
})*/
.attr("fill", textColor)
.style("font-size", function(d) {
percentage = (d[obj.data.key] * 100) / total;
if (Number.isInteger(percentage)) {
percentage = percentage.toFixed(0);
return radius / 3 + "px";
} else {
percentage = percentage.toFixed(1);
return radius / 3.5 + "px";
}
})
.text(percentage + "%");
});
g.on("mouseout", function(obj) {
svg.select("text.text-tooltip").text(function(d) {
if (normal_status) {
return percentage_normal + "%";
} else {
return "0%";
}
});
// .attr("fill", "#82b92e");
});
} else {
g.data(pie(d3.entries(data)))
.exit()
.remove();
g.select("path")
.transition()
.duration(200)
.attrTween("d", function(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
});
g.select("text").attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")";
});
svg.select("text.text-tooltip").datum(data);
}
return object;
};
// Getter and setter methods
object.data = function(value) {
if (!arguments.length) return data;
data = value;
return object;
};
object.donutbody = function(value) {
if (!arguments.length) return donutbody;
donutbody = value;
return object;
};
object.width = function(value) {
if (!arguments.length) return width;
width = value;
radius = Math.min(width, height) / 2;
return object;
};
object.height = function(value) {
if (!arguments.length) return height;
height = value;
radius = Math.min(width, height) / 2;
return object;
};
return object;
}

View File

@ -222,6 +222,7 @@ if (check_acl($config['id_user'], 0, 'ER')) {
$event_filter .= ' AND utimestamp > (UNIX_TIMESTAMP(NOW()) - '.($config['event_view_hr'] * SECONDS_1HOUR).')';
}
hd('aaaaaaaaaaaaaaa');
$events = events_print_event_table($event_filter, 10, '100%', true, false, true);
ui_toggle(
$events,
@ -240,13 +241,15 @@ if ($is_admin) {
include $config['homedir'].'/godmode/servers/servers.build_table.php';
}
$out = '<table cellpadding=0 cellspacing=0 class="databox pies mrgn_top_15px" width=100%><tr><td>';
$out .= '<fieldset class="padding-0 databox tactical_set" id="total_event_graph">
<legend>'.__('Event graph').'</legend>'.html_print_image('images/spinner.gif', true, ['id' => 'spinner_total_event_graph']).'</fieldset>';
$out .= '</td><td>';
$out .= '<fieldset class="padding-0 databox tactical_set" id="graphic_event_group">
<legend>'.__('Event graph by agent').'</legend>'.html_print_image('images/spinner.gif', true, ['id' => 'spinner_graphic_event_group']).'</fieldset>';
$out .= '</td></tr></table>';
$out = '<table cellpadding=0 cellspacing=0 class="databox pies mrgn_top_15px" width=100%><tr><td style="width:50%;">';
$out .= '<fieldset class="padding-0 databox tactical_set" id="total_event_graph">';
$out .= '<legend>'.__('Event graph').'</legend>';
$out .= html_print_image('images/spinner.gif', true, ['id' => 'spinner_total_event_graph']);
$out .= '</fieldset>';
$out .= '</td><td style="width:50%;">';
$out .= '<fieldset class="padding-0 databox tactical_set" id="graphic_event_group">
<legend>'.__('Event graph by agent').'</legend>'.html_print_image('images/spinner.gif', true, ['id' => 'spinner_graphic_event_group']).'</fieldset>';
$out .= '</td></tr></table>';
ui_toggle(

View File

@ -307,34 +307,6 @@ if ($view_graph) {
return;
}
if ($graph_return) {
echo "<table id='graph-container' class='databox filters' cellpadding='0' cellspacing='0' width='100%'>";
echo '<tr><td>';
if (!is_ajax()) {
echo '<div id="spinner_loading" class="loading invisible" style="display:flex;flex-direction:column-reverse;justify-content:center;align-items:center">';
echo html_print_image('images/spinner.gif', true, ['width' => '20px']);
echo __('Loading').'&hellip;';
echo '</div>';
}
if ($stacked == CUSTOM_GRAPH_VBARS) {
echo '<div class="w100p height_600px">';
echo '<div id="div-container" class="w100p height_600px">';
} else {
echo '<div id="div-container">';
}
echo $graph_return;
echo '</div>';
if ($stacked == CUSTOM_GRAPH_VBARS) {
echo '</div>';
}
echo '</td></tr></table>';
} else {
ui_print_info_message([ 'no_close' => true, 'message' => __('No data.') ]);
}
if ($stacked == CUSTOM_GRAPH_BULLET_CHART_THRESHOLD) {
$stacked = 4;
}
@ -403,6 +375,31 @@ if ($view_graph) {
echo '</table>';
echo '</form>';
if ($graph_return) {
echo "<table id='graph-container' class='databox filters' cellpadding='0' cellspacing='0' style='height:100%;width:100%;overflow:hidden;'>";
echo '<tr><td>';
if (!is_ajax()) {
echo '<div id="spinner_loading" class="loading invisible" style="display:flex;flex-direction:column-reverse;justify-content:center;align-items:center">';
echo html_print_image('images/spinner.gif', true, ['width' => '20px']);
echo __('Loading').'&hellip;';
echo '</div>';
}
echo '<div id="div-container" class="w100p" style="height:100%;">';
echo '<div style="position: relative; display: flex; flex-direction:row; justify-content: center; align-items: center; align-content: center; width:100%; height:50vh;">';
echo '<div style="flex: 0 0 auto; width:99%; height:100%;">';
echo $graph_return;
echo '</div>';
echo '</div>';
echo '</div>';
echo '</td></tr></table>';
} else {
ui_print_info_message([ 'no_close' => true, 'message' => __('No data.') ]);
}
/*
We must add javascript here. Otherwise, the date picker won't
work if the date is not correct because php is returning.

View File

@ -209,7 +209,7 @@ $table_source_row['table'] = html_print_table($table_source_data, true);
unset($table_source_data);
if (empty($table_source_graph_data)) {
$table_source_graph = graph_nodata_image();
$table_source_graph = graph_nodata_image([]);
} else {
$table_source_graph = pie_graph(
$table_source_graph_data,
@ -266,7 +266,7 @@ $table_oid_row['table'] = html_print_table($table_oid_data, true);
unset($table_oid_data);
if (empty($table_oid_graph_data)) {
$table_oid_graph = graph_nodata_image();
$table_oid_graph = graph_nodata_image([]);
} else {
$table_oid_graph = pie_graph(
$table_oid_graph_data,

View File

@ -3,8 +3,21 @@
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
exit(1);
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';

View File

@ -0,0 +1,23 @@
The MIT License (MIT)
Copyright (c) 2017-2020 Soufiane Ghzal <sghzal@gmail.com>
Copyright (c) 2020-2022 Graham Campbell <hello@gjcampbell.co.uk>
Copyright (c) 2020-2022 Enrico Dias <enricodias@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,55 @@
{
"name": "chrome-php/chrome",
"description": "Instrument headless chrome/chromium instances from PHP",
"keywords": ["chrome", "chromium", "crawl", "browser", "headless", "screenshot", "pdf", "puppeteer"],
"license": "MIT",
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Enrico Dias",
"email": "enrico@enricodias.com",
"homepage": "https://github.com/enricodias"
}
],
"require": {
"php": "^7.3 || ^8.0",
"chrome-php/wrench": "^1.3",
"evenement/evenement": "^3.0.1",
"monolog/monolog": "^1.27.1 || ^2.8 || ^3.2",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"symfony/filesystem": "^4.4 || ^5.0 || ^6.0",
"symfony/polyfill-mbstring": "^1.26",
"symfony/process": "^4.4 || ^5.0 || ^6.0"
},
"require-dev":{
"bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^9.5.23",
"symfony/var-dumper": "^4.4 || ^5.0 || ^6.0"
},
"autoload":{
"psr-4" : {
"HeadlessChromium\\": "src/"
}
},
"autoload-dev":{
"psr-4" : {
"HeadlessChromium\\Test\\": "tests/"
}
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true
},
"preferred-install": "dist"
},
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium;
class AutoDiscover
{
/**
* @var callable(): string
*/
private $osFamily;
/**
* @param (callable(): string)|null $osFamily
*/
public function __construct(?callable $osFamily = null)
{
$this->osFamily = $osFamily ?? function (): string {
return \PHP_OS_FAMILY;
};
}
public function guessChromeBinaryPath(): string
{
if (\array_key_exists('CHROME_PATH', $_SERVER)) {
return $_SERVER['CHROME_PATH'];
}
switch (($this->osFamily)()) {
case 'Darwin':
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
case 'Windows':
return self::getFromRegistry() ?? '%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe';
default:
return null === self::shellExec('command -v google-chrome') ? 'chrome' : 'google-chrome';
}
}
private static function getFromRegistry(): ?string
{
$registryKey = self::shellExec(
'reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe" /ve'
);
if (null === $registryKey) {
return null;
}
\preg_match('/.:(?!.*:).*/', $registryKey, $matches);
return $matches[0] ?? null;
}
private static function shellExec(string $command): ?string
{
try {
$result = @\shell_exec($command);
return \is_string($result) ? $result : null;
} catch (\Throwable $e) {
return null;
}
}
}

View File

@ -0,0 +1,254 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium;
use HeadlessChromium\Communication\Connection;
use HeadlessChromium\Communication\Message;
use HeadlessChromium\Communication\Target;
use HeadlessChromium\Exception\CommunicationException;
use HeadlessChromium\Exception\CommunicationException\ResponseHasError;
use HeadlessChromium\Exception\NoResponseAvailable;
use HeadlessChromium\Exception\OperationTimedOut;
class Browser
{
/**
* @var Connection
*/
protected $connection;
/**
* @var array<string,Target>
*/
protected $targets = [];
/**
* @var array<string,Page>
*/
protected $pages = [];
/**
* A preScript to be automatically added on every new pages.
*
* @var string|null
*/
protected $pagePreScript;
public function __construct(Connection $connection)
{
$this->connection = $connection;
// listen for target created
$this->connection->on(Connection::EVENT_TARGET_CREATED, function (array $params): void {
// create and store the target
$this->targets[$params['targetInfo']['targetId']] = new Target($params['targetInfo'], $this->connection);
});
// listen for target info changed
$this->connection->on(Connection::EVENT_TARGET_INFO_CHANGED, function (array $params): void {
// get target by id
$target = $this->getTarget($params['targetInfo']['targetId']);
if ($target) {
$target->targetInfoChanged($params['targetInfo']);
}
});
// listen for target destroyed
$this->connection->on(Connection::EVENT_TARGET_DESTROYED, function (array $params): void {
// get target by id
$target = $this->getTarget($params['targetId']);
if ($target) {
// remove the page
unset($this->pages[$params['targetId']]);
// remove the target
unset($this->targets[$params['targetId']]);
$target->destroy();
$this->connection
->getLogger()
->debug('✘ target('.$params['targetId'].') was destroyed and unreferenced.');
}
});
// enable target discovery
$connection->sendMessageSync(new Message('Target.setDiscoverTargets', ['discover' => true]));
}
/**
* @return Connection
*/
public function getConnection(): Connection
{
return $this->connection;
}
/**
* Set a preScript to be added on every new pages.
* Use null to disable it.
*
* @param string|null $script
*/
public function setPagePreScript(string $script = null): void
{
$this->pagePreScript = $script;
}
/**
* Closes the browser.
*
* @throws \Exception
*/
public function close(): void
{
$this->sendCloseMessage();
}
/**
* Send close message to the browser.
*
* @throws OperationTimedOut
*/
final public function sendCloseMessage(): void
{
$r = $this->connection->sendMessageSync(new Message('Browser.close'));
if (!$r->isSuccessful()) {
// log
$this->connection->getLogger()->debug('process: ✗ could not close gracefully');
throw new \Exception('cannot close, Browser.close not supported');
}
}
/**
* Creates a new page.
*
* @throws NoResponseAvailable
* @throws CommunicationException
* @throws OperationTimedOut
*
* @return Page
*/
public function createPage(): Page
{
// page url
$params = ['url' => 'about:blank'];
// create page and get target id
$response = $this->connection->sendMessageSync(new Message('Target.createTarget', $params));
$targetId = $response['result']['targetId'];
// todo handle error
$target = $this->getTarget($targetId);
if (!$target) {
throw new \RuntimeException('Target could not be created for page.');
}
$page = $this->getPage($targetId);
return $page;
}
/**
* @param string $targetId
*
* @return Target|null
*/
public function getTarget($targetId)
{
// make sure target was created (via Target.targetCreated event)
if (!\array_key_exists($targetId, $this->targets)) {
return null;
}
return $this->targets[$targetId];
}
/**
* @return Target[]
*/
public function getTargets()
{
return \array_values($this->targets);
}
/**
* @param string $targetId
*
* @throws CommunicationException
*
* @return Page|null
*/
public function getPage($targetId)
{
if (\array_key_exists($targetId, $this->pages)) {
return $this->pages[$targetId];
}
$target = $this->getTarget($targetId);
if ('page' !== $target->getTargetInfo('type')) {
return null;
}
// get initial frame tree
$frameTreeResponse = $target->getSession()->sendMessageSync(new Message('Page.getFrameTree'));
// make sure frame tree was found
if (!$frameTreeResponse->isSuccessful()) {
throw new ResponseHasError('Cannot read frame tree. Please, consider upgrading chrome version.');
}
// create page
$page = new Page($target, $frameTreeResponse['result']['frameTree']);
// Page.enable
$page->getSession()->sendMessageSync(new Message('Page.enable'));
// Network.enable
$page->getSession()->sendMessageSync(new Message('Network.enable'));
// Runtime.enable
$page->getSession()->sendMessageSync(new Message('Runtime.enable'));
// Page.setLifecycleEventsEnabled
$page->getSession()->sendMessageSync(new Message('Page.setLifecycleEventsEnabled', ['enabled' => true]));
// set up http headers
$headers = $this->connection->getConnectionHttpHeaders();
if (\count($headers) > 0) {
$page->setExtraHTTPHeaders($headers);
}
// add prescript
if ($this->pagePreScript) {
$page->addPreScript($this->pagePreScript);
}
$this->pages[$targetId] = $page;
return $page;
}
/**
* @return Page[]
*/
public function getPages()
{
$ids = \array_keys($this->targets);
$pages = \array_filter(\array_map([$this, 'getPage'], $ids));
return \array_values($pages);
}
}

View File

@ -0,0 +1,467 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Browser;
use HeadlessChromium\Browser;
use HeadlessChromium\Communication\Connection;
use HeadlessChromium\Exception\OperationTimedOut;
use HeadlessChromium\Utils;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\Process;
use Wrench\Exception\SocketException;
/**
* A browser process starter. Don't use directly, use BrowserFactory instead.
*/
class BrowserProcess implements LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* chrome instance's user data data.
*
* @var string
*/
protected $userDataDir;
/**
* @var Process
*/
protected $process;
/**
* True if the user data dir is temporary and should be deleted on process closes.
*
* @var bool
*/
protected $userDataDirIsTemp;
/**
* @var Connection
*/
protected $connection;
/**
* @var ProcessAwareBrowser
*/
protected $browser;
/**
* @var bool
*/
protected $wasKilled = false;
/**
* @var bool
*/
protected $wasStarted = false;
/**
* @var string
*/
protected $wsUri;
/**
* BrowserProcess constructor.
*
* @param LoggerInterface|null $logger
*/
public function __construct(LoggerInterface $logger = null)
{
// set or create logger
$this->setLogger($logger ?? new NullLogger());
}
/**
* Starts the browser.
*
* @param string $binary
* @param array $options
*/
public function start($binary, $options): void
{
if ($this->wasStarted) {
// cannot start twice because once started this class contains the necessary data to cleanup the browser.
// starting in again would result in replacing those data.
throw new \RuntimeException('This process was already started');
}
$this->wasStarted = true;
// log
$this->logger->debug('process: initializing');
// user data dir
if (!\array_key_exists('userDataDir', $options) || !$options['userDataDir']) {
// if no data dir specified create it
$options['userDataDir'] = $this->createTempDir();
// set user data dir to get removed on close
$this->userDataDirIsTemp = true;
}
$this->userDataDir = $options['userDataDir'];
// log
$this->logger->debug('process: using directory: '.$options['userDataDir']);
// get args for command line
$args = $this->getArgsFromOptions($binary, $options);
// setup chrome process
if (!\array_key_exists('keepAlive', $options) || !$options['keepAlive']) {
$process = new Process($args, null, $options['envVariables'] ?? null);
} else {
$process = new ProcessKeepAlive($args, null, $options['envVariables'] ?? null);
}
$this->process = $process;
// log
$this->logger->debug('process: starting process: '.$process->getCommandLine());
// and start
$process->start();
// wait for start and retrieve ws uri
$startupTimeout = $options['startupTimeout'] ?? 30;
$this->wsUri = $this->waitForStartup($process, $startupTimeout * 1000 * 1000);
// log
$this->logger->debug('process: connecting using '.$this->wsUri);
// connect to browser
$connection = new Connection($this->wsUri, $this->logger, $options['sendSyncDefaultTimeout'] ?? 5000);
$connection->connect();
// connection delay
if (\array_key_exists('connectionDelay', $options)) {
$connection->setConnectionDelay($options['connectionDelay']);
}
// connection headers
if (\array_key_exists('headers', $options)) {
$connection->setConnectionHttpHeaders($options['headers']);
}
// set connection to allow killing chrome
$this->connection = $connection;
// create browser instance
$this->browser = new ProcessAwareBrowser($connection, $this);
}
/**
* @return ProcessAwareBrowser
*/
public function getBrowser()
{
return $this->browser;
}
/**
* @return string
*/
public function getSocketUri()
{
return $this->wsUri;
}
/**
* Kills the process and clean temporary files.
*
* @throws OperationTimedOut
*/
public function kill(): void
{
// log
$this->logger->debug('process: killing chrome');
if ($this->wasKilled) {
// log
$this->logger->debug('process: chrome already killed, ignoring');
return;
}
$this->wasKilled = true;
if (isset($this->process)) {
// close gracefully if connection exists
if (isset($this->connection)) {
// if socket connect try graceful close
if ($this->connection->isConnected()) {
// first try to close with Browser.close
// if Browser.close is not implemented, try to kill by closing all pages
try {
// log
$this->logger->debug('process: trying to close chrome gracefully');
$this->browser->sendCloseMessage();
} catch (\Exception $e) {
// log
$this->logger->debug('process: closing chrome gracefully - compatibility');
// close all pages if connected
try {
$this->connection->isConnected() && Utils::closeAllPage($this->connection);
} catch (OperationTimedOut $e) {
// log
$this->logger->debug('process: failed to close all pages');
}
}
// disconnect socket
try {
$this->connection->disconnect();
} catch (SocketException $e) {
// Socket might be already disconnected
}
// log
$this->logger->debug('process: waiting for process to close');
// wait for process to close
$generator = function (Process $process) {
while ($process->isRunning()) {
yield 2 * 1000; // wait for 2ms
}
};
$timeout = 8 * 1000 * 1000; // 8 seconds
try {
Utils::tryWithTimeout($timeout, $generator($this->process));
} catch (OperationTimedOut $e) {
// log
$this->logger->debug('process: process didn\'t close by itself');
}
}
}
// stop process if running
if ($this->process->isRunning()) {
// log
$this->logger->debug('process: stopping process');
// stop process
$exitCode = $this->process->stop();
// log
$this->logger->debug('process: process stopped with exit code '.$exitCode);
}
}
// remove data dir
if ($this->userDataDirIsTemp && $this->userDataDir) {
try {
// log
$this->logger->debug('process: cleaning temporary resources:'.$this->userDataDir);
// cleaning
$fs = new Filesystem();
$fs->remove($this->userDataDir);
} catch (\Exception $e) {
// log
$this->logger->debug('process: ✗ could not clean temporary resources');
}
}
}
/**
* Get args for creating chrome's startup command.
*
* @param array $options
*
* @return array
*/
private function getArgsFromOptions($binary, array $options)
{
// command line args to add to start chrome (inspired by puppeteer configs)
// see https://peter.sh/experiments/chromium-command-line-switches/
$args = [
$binary,
// auto debug port
'--remote-debugging-port=0',
// disable undesired features
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-client-side-phishing-detection',
'--disable-hang-monitor',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-sync',
'--disable-translate',
'--disable-features=ChromeWhatsNewUI',
'--metrics-recording-only',
'--no-first-run',
'--safebrowsing-disable-auto-update',
// automation mode
'--enable-automation',
// password settings
'--password-store=basic',
'--use-mock-keychain', // osX only
];
// enable headless mode
if (!\array_key_exists('headless', $options) || $options['headless']) {
$args[] = '--headless';
$args[] = '--disable-gpu';
$args[] = '--font-render-hinting=none';
$args[] = '--hide-scrollbars';
$args[] = '--mute-audio';
}
// disable loading of images (currently can't be done via devtools, only CLI)
if (\array_key_exists('enableImages', $options) && (false === $options['enableImages'])) {
$args[] = '--blink-settings=imagesEnabled=false';
}
// window's size
if (\array_key_exists('windowSize', $options) && $options['windowSize']) {
if (
!\is_array($options['windowSize']) ||
2 !== \count($options['windowSize']) ||
!\is_numeric($options['windowSize'][0]) ||
!\is_numeric($options['windowSize'][1])
) {
throw new \InvalidArgumentException('Option "windowSize" must be an array of dimensions (eg: [1000, 1200])');
}
$args[] = '--window-size='.\implode(',', $options['windowSize']);
}
// sandbox mode - useful if you want to use chrome headless inside docker
if (\array_key_exists('noSandbox', $options) && $options['noSandbox']) {
$args[] = '--no-sandbox';
}
// user agent
if (\array_key_exists('userAgent', $options)) {
$args[] = '--user-agent='.$options['userAgent'];
}
// ignore certificate errors
if (\array_key_exists('ignoreCertificateErrors', $options) && $options['ignoreCertificateErrors']) {
$args[] = '--ignore-certificate-errors';
}
// proxy server
if (\array_key_exists('proxyServer', $options)) {
$args[] = '--proxy-server='.$options['proxyServer'];
}
if (\array_key_exists('noProxyServer', $options) && $options['noProxyServer']) {
$args[] = '--no-proxy-server';
}
if (\array_key_exists('proxyBypassList', $options)) {
$args[] = '--proxy-bypass-list='.$options['proxyBypassList'];
}
// add custom flags
if (\array_key_exists('customFlags', $options) && \is_array($options['customFlags'])) {
$args = \array_merge($args, $options['customFlags']);
}
// add user data dir to args
$args[] = '--user-data-dir='.$options['userDataDir'];
return $args;
}
/**
* Wait for chrome to startup (given a process) and return the ws uri to connect to.
*
* @param Process $process
* @param int $timeout
*
* @return mixed
*/
private function waitForStartup(Process $process, int $timeout)
{
// log
$this->logger->debug('process: waiting for '.$timeout / 1000000 .' seconds for startup');
try {
$generator = function (Process $process) {
while (true) {
if (!$process->isRunning()) {
// log
$this->logger->debug('process: ✗ chrome process stopped');
// exception
$message = 'Chrome process stopped before startup completed.';
$error = \trim($process->getErrorOutput());
if (!empty($error)) {
$message .= ' Additional info: '.$error;
}
throw new \RuntimeException($message);
}
$output = \trim($process->getIncrementalErrorOutput());
if ($output) {
// log
$this->logger->debug('process: chrome output:'.$output);
$outputs = \explode(\PHP_EOL, $output);
foreach ($outputs as $output) {
$output = \trim($output);
// ignore empty line
if (empty($output)) {
continue;
}
// find socket uri
if (\preg_match('/DevTools listening on (ws:\/\/.*)/', $output, $matches)) {
// log
$this->logger->debug('process: ✓ accepted output');
return $matches[1];
} else {
// log
$this->logger->debug('process: ignoring output:'.\trim($output));
}
}
}
// wait for 10ms
yield 10 * 1000;
}
};
return Utils::tryWithTimeout($timeout, $generator($process));
} catch (OperationTimedOut $e) {
throw new \RuntimeException('Cannot start browser', 0, $e);
}
}
/**
* Creates a temp directory for the app.
*
* @return string path to the new temp directory
*/
private function createTempDir()
{
$tmpFile = \tempnam(\sys_get_temp_dir(), 'chromium-php-');
\unlink($tmpFile);
\mkdir($tmpFile);
return $tmpFile;
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Browser;
use HeadlessChromium\Browser;
use HeadlessChromium\Communication\Connection;
class ProcessAwareBrowser extends Browser
{
/**
* @var BrowserProcess
*/
protected $browserProcess;
public function __construct(Connection $connection, BrowserProcess $browserProcess)
{
parent::__construct($connection);
$this->browserProcess = $browserProcess;
}
/**
* {@inheritdoc}
*/
public function close(): void
{
$this->browserProcess->kill();
}
/**
* @return string
*/
public function getSocketUri()
{
return $this->browserProcess->getSocketUri();
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Browser;
use Symfony\Component\Process\Process;
class ProcessKeepAlive extends Process
{
public function __destruct()
{
// Do nothing because we are in mode keep alive, default behavior is to kill the process
}
}

View File

@ -0,0 +1,201 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium;
use HeadlessChromium\Browser\BrowserProcess;
use HeadlessChromium\Browser\ProcessAwareBrowser;
use HeadlessChromium\Communication\Connection;
use HeadlessChromium\Exception\BrowserConnectionFailed;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Process\Process;
use Wrench\Exception\HandshakeException;
class BrowserFactory
{
protected $chromeBinary;
/**
* Options for browser creation.
*
* - connectionDelay: Delay to apply between each operation for debugging purposes (default: none)
* - customFlags: An array of flags to pass to the command line.
* - debugLogger: A string (e.g "php://stdout"), or resource, or PSR-3 logger instance to print debug messages (default: none)
* - enableImages: Toggles loading of images (default: true)
* - envVariables: An array of environment variables to pass to the process (example DISPLAY variable)
* - headers: An array of custom HTTP headers
* - headless: Enable or disable headless mode (default: true)
* - ignoreCertificateErrors: Set chrome to ignore ssl errors
* - keepAlive: Set to `true` to keep alive the chrome instance when the script terminates (default: false)
* - noSandbox: Enable no sandbox mode, useful to run in a docker container (default: false)
* - proxyServer: Proxy server to use. ex: `127.0.0.1:8080` (default: none)
* - sendSyncDefaultTimeout: Default timeout (ms) for sending sync messages (default 5000 ms)
* - startupTimeout: Maximum time in seconds to wait for chrome to start (default: 30 sec)
* - userAgent: User agent to use for the whole browser
* - userDataDir: Chrome user data dir (default: a new empty dir is generated temporarily)
* - windowSize: Size of the window. ex: `[1920, 1080]` (default: none)
*/
protected $options = [];
public function __construct(string $chromeBinary = null)
{
$this->chromeBinary = $chromeBinary ?? (new AutoDiscover())->guessChromeBinaryPath();
}
/**
* Start a chrome process and allows to interact with it.
*
* @see BrowserFactory::$options
*
* @param array|null $options overwrite options for browser creation
*
* @return ProcessAwareBrowser a Browser instance to interact with the new chrome process
*/
public function createBrowser(?array $options = null): ProcessAwareBrowser
{
$options = $options ?? $this->options;
// create logger from options
$logger = self::createLogger($options);
// create browser process
$browserProcess = new BrowserProcess($logger);
// instruct the runtime to kill chrome and clean temp files on exit
if (!\array_key_exists('keepAlive', $options) || !$options['keepAlive']) {
\register_shutdown_function([$browserProcess, 'kill']);
}
// start the browser and connect to it
$browserProcess->start($this->chromeBinary, $options);
return $browserProcess->getBrowser();
}
public function addHeader(string $name, string $value): void
{
$this->options['headers'][$name] = $value;
}
/**
* @param array<string, string> $headers
*/
public function addHeaders(array $headers): void
{
foreach ($headers as $name => $value) {
$this->addHeader($name, $value);
}
}
/**
* Connects to an existing browser using it's web socket uri.
*
* usage:
*
* ```
* $browserFactory = new BrowserFactory();
* $browser = $browserFactory->createBrowser();
*
* $uri = $browser->getSocketUri();
*
* $existingBrowser = BrowserFactory::connectToBrowser($uri);
* ```
*
* @param string $uri
* @param array $options options when creating the connection to the browser:
* - connectionDelay: amount of time in seconds to slows down connection for debugging purposes (default: none)
* - debugLogger: resource string ("php://stdout"), resource or psr-3 logger instance (default: none)
* - sendSyncDefaultTimeout: maximum time in ms to wait for synchronous messages to send (default 5000 ms)
*
* @throws BrowserConnectionFailed
*
* @return Browser
*/
public static function connectToBrowser(string $uri, array $options = []): Browser
{
$logger = self::createLogger($options);
if ($logger) {
$logger->debug('Browser Factory: connecting using '.$uri);
}
// connect to browser
$connection = new Connection($uri, $logger, $options['sendSyncDefaultTimeout'] ?? 5000);
// try to connect
try {
$connection->connect();
} catch (HandshakeException $e) {
throw new BrowserConnectionFailed('Invalid socket uri', 0, $e);
}
// make sure it is connected
if (!$connection->isConnected()) {
throw new BrowserConnectionFailed('Cannot connect to the browser, make sure it was not closed');
}
// connection delay
if (\array_key_exists('connectionDelay', $options)) {
$connection->setConnectionDelay($options['connectionDelay']);
}
return new Browser($connection);
}
/**
* Set default options to be used in all browser instances.
*
* @see BrowserFactory::$options
*/
public function setOptions(array $options): void
{
$this->options = $options;
}
/**
* Add or overwrite options to the default options list.
*
* @see BrowserFactory::$options
*/
public function addOptions(array $options): void
{
$this->options = \array_merge($this->options, $options);
}
public function getOptions(): array
{
return $this->options;
}
/**
* Create a logger instance from given options.
*/
private static function createLogger(array $options): LoggerInterface
{
$logger = $options['debugLogger'] ?? null;
if ($logger instanceof LoggerInterface) {
return $logger;
}
if (\is_string($logger) || \is_resource($logger)) {
$log = new Logger('chrome');
$log->pushHandler(new StreamHandler($logger));
return $log;
}
return new NullLogger();
}
}

View File

@ -0,0 +1,119 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium;
class Clip
{
protected $x;
protected $y;
protected $height;
protected $width;
protected $scale;
/**
* Clip constructor.
*
* @param int $x
* @param int $y
* @param int $height
* @param int $width
* @param float $scale
*/
public function __construct($x, $y, $width, $height, $scale = 1.0)
{
$this->x = $x;
$this->y = $y;
$this->height = $height;
$this->width = $width;
$this->scale = $scale;
}
/**
* @return mixed
*/
public function getX()
{
return $this->x;
}
/**
* @return mixed
*/
public function getY()
{
return $this->y;
}
/**
* @return mixed
*/
public function getHeight()
{
return $this->height;
}
/**
* @return mixed
*/
public function getWidth()
{
return $this->width;
}
/**
* @return mixed
*/
public function getScale()
{
return $this->scale;
}
/**
* @param mixed $x
*/
public function setX($x): void
{
$this->x = $x;
}
/**
* @param mixed $y
*/
public function setY($y): void
{
$this->y = $y;
}
/**
* @param mixed $height
*/
public function setHeight($height): void
{
$this->height = $height;
}
/**
* @param mixed $width
*/
public function setWidth($width): void
{
$this->width = $width;
}
/**
* @param mixed $scale
*/
public function setScale($scale): void
{
$this->scale = $scale;
}
}

View File

@ -0,0 +1,458 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Communication;
use Evenement\EventEmitter;
use HeadlessChromium\Communication\Socket\SocketInterface;
use HeadlessChromium\Communication\Socket\Wrench;
use HeadlessChromium\Exception\CommunicationException;
use HeadlessChromium\Exception\CommunicationException\CannotReadResponse;
use HeadlessChromium\Exception\CommunicationException\InvalidResponse;
use HeadlessChromium\Exception\OperationTimedOut;
use HeadlessChromium\Exception\TargetDestroyed;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Wrench\Client as WrenchBaseClient;
class Connection extends EventEmitter implements LoggerAwareInterface
{
use LoggerAwareTrait;
public const EVENT_TARGET_CREATED = 'method:Target.targetCreated';
public const EVENT_TARGET_INFO_CHANGED = 'method:Target.targetInfoChanged';
public const EVENT_TARGET_DESTROYED = 'method:Target.targetDestroyed';
/**
* When strict mode is enabled communication error will result in exceptions.
*
* @var bool
*/
protected $strict = true;
/**
* time in ms to wait between each message to be sent
* That helps to see what is happening when debugging.
*
* @var int
*/
protected $delay;
/**
* time in ms when the previous message was sent. Used to know how long to wait for before send next message
* (only when $delay is set).
*
* @var int
*/
private $lastMessageSentTime;
/**
* @var SocketInterface
*/
protected $wsClient;
/**
* List of response sent from the remote host and that are waiting to be read.
*
* @var array
*/
protected $responseBuffer = [];
/**
* Default timeout for send sync in ms.
*
* @var int
*/
protected $sendSyncDefaultTimeout;
/**
* @var Session[]
*/
protected $sessions = [];
/**
* @var array array of data received and waiting to be read
*/
protected $receivedData = [];
/**
* @var array<string, string>
*/
protected $httpHeaders = [];
/**
* CommunicationChannel constructor.
*
* @param SocketInterface|string $socketClient
* @param int|null $sendSyncDefaultTimeout
*/
public function __construct($socketClient, LoggerInterface $logger = null, int $sendSyncDefaultTimeout = null)
{
// set or create logger
$this->setLogger($logger ?? new NullLogger());
// set timeout
$this->sendSyncDefaultTimeout = $sendSyncDefaultTimeout ?? 5000;
// create socket client
if (\is_string($socketClient)) {
$socketClient = new Wrench(new WrenchBaseClient($socketClient, 'http://127.0.0.1'), $this->logger);
} elseif (!\is_object($socketClient) && !$socketClient instanceof SocketInterface) {
throw new \InvalidArgumentException('$socketClient param should be either a SockInterface instance or a web socket uri string');
}
$this->wsClient = $socketClient;
}
/**
* @return LoggerInterface
*/
public function getLogger(): LoggerInterface
{
return $this->logger;
}
/**
* Set the delay to apply everytime before data are sent.
*
* @param int $delay
*/
public function setConnectionDelay(int $delay): void
{
$this->delay = $delay;
}
/**
* @param array<string, string> $headers
*
* @return void
*/
public function setConnectionHttpHeaders(array $headers): void
{
$this->httpHeaders = $headers;
}
/**
* @return array<string, string>
*/
public function getConnectionHttpHeaders(): array
{
return $this->httpHeaders;
}
/**
* Gets the default timeout used when sending a message synchronously.
*
* @return int
*/
public function getSendSyncDefaultTimeout(): int
{
return $this->sendSyncDefaultTimeout;
}
/**
* @return bool
*/
public function isStrict(): bool
{
return $this->strict;
}
/**
* @param bool $strict
*/
public function setStrict(bool $strict): void
{
$this->strict = $strict;
}
/**
* Connects to the server.
*
* @return bool Whether a new connection was made
*/
public function connect()
{
return $this->wsClient->connect();
}
/**
* Disconnects the underlying socket, and marks the client as disconnected.
*
* @return bool
*/
public function disconnect()
{
return $this->wsClient->disconnect();
}
/**
* Returns whether the client is currently connected.
*
* @return bool true if connected
*/
public function isConnected()
{
return $this->wsClient->isConnected();
}
/**
* Wait before sending next message.
*/
private function waitForDelay(): void
{
if ($this->lastMessageSentTime) {
$currentTime = (int) (\hrtime(true) / 1000 / 1000);
// if not enough time was spent until last message was sent, wait
if ($this->lastMessageSentTime + $this->delay > $currentTime) {
$timeToWait = ($this->lastMessageSentTime + $this->delay) - $currentTime;
\usleep($timeToWait * 1000);
}
}
$this->lastMessageSentTime = (int) (\hrtime(true) / 1000 / 1000);
}
/**
* Sends the given message and returns a response reader.
*
* @param Message $message
*
* @throws CommunicationException
*
* @return ResponseReader
*/
public function sendMessage(Message $message): ResponseReader
{
// if delay enabled wait before sending message
if ($this->delay > 0) {
$this->waitForDelay();
}
$sent = $this->wsClient->sendData((string) $message);
if (!$sent) {
$message = 'Message could not be sent.';
if (!$this->isConnected()) {
$message .= ' Reason: the connection is closed.';
} else {
$message .= ' Reason: unknown.';
}
throw new CommunicationException($message);
}
return new ResponseReader($message, $this);
}
/**
* @param Message $message
* @param int|null $timeout
*
* @throws OperationTimedOut
*
* @return Response
*/
public function sendMessageSync(Message $message, int $timeout = null): Response
{
$responseReader = $this->sendMessage($message);
$response = $responseReader->waitForResponse($timeout ?? $this->sendSyncDefaultTimeout);
return $response;
}
/**
* Create a session for the given target id.
*
* @param string $targetId
* @param ?string $sessionId
*
* @return Session
*/
public function createSession($targetId, $sessionId = null): Session
{
if (null === $sessionId) {
$response = $this->sendMessageSync(
new Message('Target.attachToTarget', ['targetId' => $targetId, 'flatten' => true])
);
if (empty($response['result'])) {
throw new TargetDestroyed('The target was destroyed.');
}
$sessionId = $response['result']['sessionId'];
}
$session = new Session($targetId, $sessionId, $this);
$this->sessions[$sessionId] = $session;
$session->on('destroyed', function () use ($sessionId): void {
$this->logger->debug('✘ session('.$sessionId.') was destroyed and unreferenced.');
unset($this->sessions[$sessionId]);
});
return $session;
}
/**
* Receive and stack data from the socket.
*/
private function receiveData(): void
{
$this->receivedData = \array_merge($this->receivedData, $this->wsClient->receiveData());
}
/**
* Read data from CRI and store messages.
*
* @throws CannotReadResponse
* @throws InvalidResponse
*
* @return bool true if data were received
*/
public function readData()
{
$hasData = false;
while ($this->readLine()) {
$hasData = true;
}
return $hasData;
}
public function readLine()
{
// if buffer empty, then read from input
if (empty($this->receivedData)) {
$this->receiveData();
}
// dispatch first line of buffer
$datum = \array_shift($this->receivedData);
if ($datum) {
return $this->dispatchMessage($datum);
}
return false;
}
/**
* Dispatches the message and either stores the response or emits an event.
*
* @throws InvalidResponse
*
* @return bool
*
* @internal
*/
private function dispatchMessage(string $message, Session $session = null)
{
// responses come as json string
$response = \json_decode($message, true);
// if json not valid throw exception
$jsonError = \json_last_error();
if (\JSON_ERROR_NONE !== $jsonError) {
if ($this->isStrict()) {
throw new CannotReadResponse(\sprintf('Response from chrome remote interface is not a valid json response. JSON error: %s', $jsonError));
}
return false;
}
// response must be array
if (!\is_array($response)) {
if ($this->isStrict()) {
throw new CannotReadResponse('Response from chrome remote interface was not a valid array');
}
return false;
}
// id is required to identify the response
if (!isset($response['id'])) {
if (isset($response['method'])) {
if ('Target.receivedMessageFromTarget' == $response['method']) {
$session = $this->sessions[$response['params']['sessionId']];
return $this->dispatchMessage($response['params']['message'], $session);
} else {
if (!$session && isset($response['sessionId'])) {
$session = $this->sessions[$response['sessionId']] ?? null;
}
if ($session) {
$this->logger->debug(
'session('.$session->getSessionId().'): ⇶ dispatching method:'.$response['method']
);
$session->emit('method:'.$response['method'], [$response['params']]);
} else {
$this->logger->debug('connection: ⇶ dispatching method:'.$response['method']);
$this->emit('method:'.$response['method'], [$response['params']]);
}
}
return false;
}
if ($this->isStrict()) {
throw new InvalidResponse('Response from chrome remote interface did not provide a valid message id');
}
return false;
}
// store response
$this->responseBuffer[$response['id']] = $response;
return true;
}
/**
* True if a response for the given id exists.
*
* @param string $id
*
* @return bool
*/
public function hasResponseForId($id)
{
return \array_key_exists($id, $this->responseBuffer);
}
/**
* @param string $id
*
* @return array|null
*/
public function getResponseForId($id)
{
if (\array_key_exists($id, $this->responseBuffer)) {
$data = $this->responseBuffer[$id];
unset($this->responseBuffer[$id]);
return $data;
}
return null;
}
/**
* @param string $sessionId
*
* @return bool
*/
public function isSessionDestroyed($sessionId)
{
return !isset($this->sessions[$sessionId]);
}
}

View File

@ -0,0 +1,112 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Communication;
class Message
{
/**
* global message id auto incremented for each message sent.
*
* @var int
*/
private static $messageId = 0;
/**
* @var int
*/
protected $id;
/**
* @var string
*/
protected $method;
/**
* @var array
*/
protected $params;
/**
* @var ?string
*/
protected $sessionId;
/**
* get the last generated message id.
*
* @return int
*/
public static function getLastMessageId()
{
return self::$messageId;
}
/**
* @param string $method
* @param array $params
*/
public function __construct(string $method, array $params = [], ?string $sessionId = null)
{
$this->id = ++self::$messageId;
$this->method = $method;
$this->params = $params;
$this->sessionId = $sessionId;
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return string
*/
public function getMethod(): string
{
return $this->method;
}
/**
* @return array
*/
public function getParams(): array
{
return $this->params;
}
public function __toString(): string
{
$message = [
'id' => $this->getId(),
'method' => $this->getMethod(),
'params' => (object) $this->getParams(),
];
if (null !== $this->sessionId) {
$message['sessionId'] = $this->sessionId;
}
return \json_encode($message);
}
public function getSessionId(): ?string
{
return $this->sessionId;
}
public function setSessionId(string $sessionId): void
{
$this->sessionId = $sessionId;
}
}

View File

@ -0,0 +1,134 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Communication;
class Response implements \ArrayAccess
{
protected $message;
protected $data;
/**
* Response constructor.
*/
public function __construct(array $data, Message $message)
{
$this->data = $data;
$this->message = $message;
}
/**
* True if the response is error free.
*
* @return bool
*/
public function isSuccessful()
{
return !\array_key_exists('error', $this->data);
}
/**
* Get the error message if set.
*
* @return string|null
*/
public function getErrorMessage(bool $extended = true)
{
$message = [];
if ($extended && isset($this->data['error']['code'])) {
$message[] = $this->data['error']['code'];
}
if (isset($this->data['error']['message'])) {
$message[] = $this->data['error']['message'];
}
if ($extended && isset($this->data['error']['data']) && \is_string($this->data['error']['data'])) {
$message[] = $this->data['error']['data'];
}
return \implode(' - ', $message);
}
/**
* Get the error code if set.
*
* @return string|null
*/
public function getErrorCode()
{
return $this->data['error']['code'] ?? null;
}
/**
* @param string $name
*
* @return mixed
*/
public function getResultData($name)
{
return $this->data['result'][$name] ?? null;
}
/**
* @return Message
*/
public function getMessage(): Message
{
return $this->message;
}
/**
* The data returned by chrome dev tools.
*
* @return array
*/
public function getData(): array
{
return $this->data;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return \array_key_exists($offset, $this->data);
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->data[$offset];
}
/**
* {@inheritdoc}
*/
public function offsetSet($offset, $value): void
{
throw new \Exception('Responses are immutable');
}
/**
* {@inheritdoc}
*/
public function offsetUnset($offset): void
{
throw new \Exception('Responses are immutable');
}
}

View File

@ -0,0 +1,188 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Communication;
use HeadlessChromium\Exception\NoResponseAvailable;
use HeadlessChromium\Exception\OperationTimedOut;
use HeadlessChromium\Utils;
class ResponseReader
{
/**
* @var Message
*/
protected $message;
/**
* @var Connection
*/
protected $connection;
/**
* @var Response|null
*/
protected $response = null;
/**
* Response constructor.
*
* @param Message $message
* @param Connection $connection
*/
public function __construct(Message $message, Connection $connection)
{
$this->message = $message;
$this->connection = $connection;
}
/**
* True if a response is available.
*
* @return bool
*/
public function hasResponse()
{
return null !== $this->response;
}
/**
* the message to get a response for.
*
* @return Message
*/
public function getMessage(): Message
{
return $this->message;
}
/**
* The connection to check messages for.
*
* @return Connection
*/
public function getConnection(): Connection
{
return $this->connection;
}
/**
* Get the response.
*
* Note: response will always be missing until checkForResponse is called
* and the response is available in the buffer
*
* @throws NoResponseAvailable
*
* @return Response
*/
public function getResponse(): Response
{
if (!$this->response) {
throw new NoResponseAvailable('Response is not available. Try to use the method waitForResponse instead.');
}
return $this->response;
}
/**
* Wait for a response.
*
* @param int $timeout time to wait for a response (milliseconds)
*
* @throws NoResponseAvailable
* @throws OperationTimedOut
*
* @return Response
*/
public function waitForResponse(int $timeout = null): Response
{
if ($this->hasResponse()) {
return $this->getResponse();
}
// default 2000ms
$timeout = $timeout ?? 2000;
return Utils::tryWithTimeout($timeout * 1000, $this->waitForResponseGenerator());
}
/**
* To be used in waitForResponse method.
*
* @throws NoResponseAvailable
*
* @return \Generator|Response
*
* @internal
*/
private function waitForResponseGenerator()
{
while (true) {
// 50 microseconds between each iteration
$tryDelay = 50;
// read available response
$hasResponse = $this->checkForResponse();
// if found return it
if ($hasResponse) {
return $this->getResponse();
}
// wait before next check
yield $tryDelay;
}
}
/**
* Check in the connection if a response exists for the message and store it if the response exists.
*
* @return bool
*/
public function checkForResponse()
{
// if response is already read, ignore
if ($this->hasResponse()) {
return true;
}
$id = $this->message->getId();
// if response exists store it
if ($this->connection->hasResponseForId($id)) {
$this->response = new Response($this->connection->getResponseForId($id), $this->message);
return true;
}
// read data
while (!$this->connection->hasResponseForId($id)) {
if (!$this->connection->readLine()) {
break;
}
}
// if response store it
if ($this->connection->hasResponseForId($id)) {
$this->response = new Response($this->connection->getResponseForId($id), $this->message);
return true;
}
// check if the session was destroyed in the mean time
if (null !== $this->message->getSessionId() && $this->connection->isSessionDestroyed($this->message->getSessionId())) {
throw new \HeadlessChromium\Exception\TargetDestroyed('The session is destroyed.');
}
return false;
}
}

View File

@ -0,0 +1,141 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Communication;
use Evenement\EventEmitter;
use HeadlessChromium\Exception\CommunicationException;
use HeadlessChromium\Exception\NoResponseAvailable;
use HeadlessChromium\Exception\TargetDestroyed;
class Session extends EventEmitter
{
/**
* @var string
*/
protected $sessionId;
/**
* @var string
*/
protected $targetId;
/**
* @var Connection|null
*/
protected $connection;
/**
* @var bool
*/
protected $destroyed = false;
/**
* Session constructor.
*
* @param string $targetId
* @param string $sessionId
* @param Connection $connection
*/
public function __construct(string $targetId, string $sessionId, Connection $connection)
{
$this->sessionId = $sessionId;
$this->targetId = $targetId;
$this->connection = $connection;
}
/**
* @param Message $message
*
* @throws CommunicationException
*
* @return ResponseReader
*/
public function sendMessage(Message $message): ResponseReader
{
if ($this->destroyed) {
throw new TargetDestroyed('The session was destroyed.');
}
if (null === $message->getSessionId()) {
$message->setSessionId($this->getSessionId());
}
$topResponse = $this->getConnection()->sendMessage($message);
return $topResponse;
}
/**
* @param Message $message
* @param int $timeout
*
* @throws NoResponseAvailable
* @throws CommunicationException
*
* @return Response
*/
public function sendMessageSync(Message $message, int $timeout = null): Response
{
$responseReader = $this->sendMessage($message);
$response = $responseReader->waitForResponse($timeout ?? $this->getConnection()->getSendSyncDefaultTimeout());
if (!$response) {
throw new NoResponseAvailable('No response was sent in the given timeout');
}
return $response;
}
/**
* @return string
*/
public function getSessionId()
{
return $this->sessionId;
}
/**
* @return string
*/
public function getTargetId()
{
return $this->targetId;
}
/**
* @return Connection
*/
public function getConnection()
{
if ($this->destroyed) {
throw new TargetDestroyed('The session was destroyed.');
}
return $this->connection;
}
/**
* Marks the session as destroyed.
*
* @internal
*/
public function destroy(): void
{
if ($this->destroyed) {
throw new TargetDestroyed('The session was already destroyed.');
}
$this->emit('destroyed');
$this->connection = null;
$this->destroyed = true;
$this->removeAllListeners();
}
}

View File

@ -0,0 +1,127 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Communication\Socket;
/**
* A mock adapter for unit tests.
*/
class MockSocket implements SocketInterface
{
protected $sentData = [];
protected $receivedData = [];
protected $receivedDataForNextMessage = [];
protected $isConnected = false;
protected $shouldConnect = true;
/**
* {@inheritdoc}
*/
public function sendData($data)
{
if (!$this->isConnected()) {
return false;
}
$this->sentData[] = $data;
if (!empty($this->receivedDataForNextMessage)) {
$data = \json_decode($data, true);
if ($data['id']) {
$next = \array_shift($this->receivedDataForNextMessage);
$next = \json_decode($next, true);
$next['id'] = $data['id'];
$this->receivedData[] = \json_encode($next);
if (isset($data['method']) && 'Target.sendMessageToTarget' == $data['method']) {
--$next['id'];
$this->receivedData[] = \json_encode($next);
}
}
}
return true;
}
/**
* resets the data stored with sendData.
*/
public function flushData(): void
{
$this->sentData = [];
}
/**
* gets the data stored with sendData.
*/
public function getSentData()
{
return $this->sentData;
}
/**
* {@inheritdoc}
*/
public function receiveData(): array
{
$data = $this->receivedData;
$this->receivedData = [];
return $data;
}
/**
* Add data to be returned with receiveData.
*
* @param bool $forNextMessage true to set the response id automatically
* for next message (can stack for multiple messages
*/
public function addReceivedData($data, $forNextMessage = false): void
{
if ($forNextMessage) {
$this->receivedDataForNextMessage[] = $data;
} else {
$this->receivedData[] = $data;
}
}
/**
* {@inheritdoc}
*/
public function connect()
{
$this->isConnected = $this->shouldConnect;
return $this->isConnected;
}
/**
* {@inheritdoc}
*/
public function isConnected()
{
return $this->isConnected;
}
/**
* {@inheritdoc}
*/
public function disconnect($reason = 1000)
{
$this->isConnected = false;
return true;
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Communication\Socket;
/**
* A simplified interface to wrap a socket client.
*/
interface SocketInterface
{
/**
* Sends data to the socket.
*
* @return bool whether the data were sent
*/
public function sendData($data);
/**
* Receives data sent by the server.
*
* @return array Payload received since the last call to receive()
*/
public function receiveData(): array;
/**
* Connect to the server.
*
* @return bool Whether a new connection was made
*/
public function connect();
/**
* Whether the client is currently connected.
*
* @return bool
*/
public function isConnected();
/**
* Disconnects the underlying socket, and marks the client as disconnected.
*
* @param int $reason see http://tools.ietf.org/html/rfc6455#section-7.4
*
* @return bool
*/
public function disconnect($reason = 1000);
}

View File

@ -0,0 +1,140 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Communication\Socket;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Wrench\Client as WrenchClient;
use Wrench\Payload\Payload;
class Wrench implements SocketInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* An auto incremented counter to uniquely identify each socket instance.
*
* @var int
*/
private static $socketIdCounter = 0;
/**
* @var WrenchClient
*/
protected $client;
/**
* Id of this socket generated from self::$socketIdCounter.
*
* @var int
*/
protected $socketId = 0;
/**
* @param WrenchClient $client
*/
public function __construct(WrenchClient $client, LoggerInterface $logger = null)
{
$this->client = $client;
$this->setLogger($logger ?? new NullLogger());
$this->socketId = ++self::$socketIdCounter;
}
/**
* {@inheritdoc}
*/
public function sendData($data)
{
// log
$this->logger->debug('socket('.$this->socketId.'): → sending data:'.$data);
// send data
return $this->client->sendData($data);
}
/**
* {@inheritdoc}
*/
public function receiveData(): array
{
$playloads = $this->client->receive();
$data = [];
if ($playloads) {
foreach ($playloads as $playload) {
/** @var Payload */
$dataString = $playload->getPayload();
$data[] = $dataString;
// log
$this->logger->debug('socket('.$this->socketId.'): ← receiving data:'.$dataString);
}
}
return $data;
}
/**
* {@inheritdoc}
*/
public function connect()
{
// log
$this->logger->debug('socket('.$this->socketId.'): connecting');
$connected = $this->client->connect();
if ($connected) {
// log
$this->logger->debug('socket('.$this->socketId.'): ✓ connected');
} else {
// log
$this->logger->debug('socket('.$this->socketId.'): ✗ could not connect');
}
return $connected;
}
/**
* {@inheritdoc}
*/
public function isConnected()
{
return $this->client->isConnected();
}
/**
* {@inheritdoc}
*/
public function disconnect($reason = 1000)
{
// log
$this->logger->debug('socket('.$this->socketId.'): disconnecting');
$disconnected = $this->client->disconnect($reason);
if ($disconnected) {
// log
$this->logger->debug('socket('.$this->socketId.'): ✓ disconnected');
} else {
// log
$this->logger->debug('socket('.$this->socketId.'): ✗ could not disconnect');
}
return $disconnected;
}
}

View File

@ -0,0 +1,116 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Communication;
use HeadlessChromium\Exception\TargetDestroyed;
class Target
{
/**
* @var array
*/
protected $targetInfo;
/**
* @var Session|null
*/
protected $session;
/**
* @var Connection
*/
private $connection;
/**
* @var bool
*/
protected $destroyed = false;
/**
* Target constructor.
*/
public function __construct(array $targetInfo, Connection $connection)
{
$this->targetInfo = $targetInfo;
$this->connection = $connection;
}
/**
* @param ?string $sessionId
*
* @return Session
*/
public function getSession(?string $sessionId = null): Session
{
if ($this->destroyed) {
throw new TargetDestroyed('The target was destroyed.');
}
// if not already done, create a session for the target
if (!$this->session) {
$this->session = $this->connection->createSession($this->getTargetInfo('targetId'), $sessionId);
}
return $this->session;
}
/**
* Marks the target as destroyed.
*
* @internal
*/
public function destroy(): void
{
if ($this->destroyed) {
throw new TargetDestroyed('The target was already destroyed.');
}
if ($this->session) {
$this->session->destroy();
$this->session = null;
}
$this->destroyed = true;
}
/**
* @return bool
*/
public function isDestroyed(): bool
{
return $this->destroyed;
}
/**
* Get target info value by it's name or null if it does not exist.
*
* @param string $infoName
*
* @return mixed
*/
public function getTargetInfo($infoName)
{
return $this->targetInfo[$infoName] ?? null;
}
/**
* To be called when Target.targetInfoChanged is triggered.
*
* @param array $targetInfo
*
* @internal
*/
public function targetInfoChanged($targetInfo): void
{
$this->targetInfo = $targetInfo;
}
}

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Cookies;
class Cookie implements \ArrayAccess
{
/**
* @var array
*/
protected $data;
/**
* Cookie constructor.
*/
public function __construct(array $data)
{
if (isset($data['expires']) && \is_string($data['expires']) && !\is_numeric($data['expires'])) {
$data['expires'] = \strtotime($data['expires']);
}
$this->data = $data;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->offsetGet('value');
}
/**
* @return mixed
*/
public function getName()
{
return $this->offsetGet('name');
}
/**
* @return mixed
*/
public function getDomain()
{
return $this->offsetGet('domain');
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return \array_key_exists($offset, $this->data);
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->data[$offset] ?? null;
}
/**
* {@inheritdoc}
*/
public function offsetSet($offset, $value): void
{
throw new \RuntimeException('Cannot set cookie values');
}
/**
* {@inheritdoc}
*/
public function offsetUnset($offset): void
{
throw new \RuntimeException('Cannot unset cookie values');
}
/**
* @param string $name
* @param string $value
* @param array $params
*
* @return Cookie
*/
public static function create($name, $value, array $params = [])
{
$params['name'] = $name;
$params['value'] = $value;
return new self($params);
}
}

View File

@ -0,0 +1,132 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Cookies;
class CookiesCollection implements \IteratorAggregate, \Countable
{
/**
* @var Cookie[]
*/
protected $cookies = [];
/**
* CookiesCollection constructor.
*/
public function __construct(array $cookies = null)
{
if ($cookies) {
foreach ($cookies as $cookie) {
if (\is_array($cookie)) {
$cookie = new Cookie($cookie);
}
$this->addCookie($cookie);
}
}
}
/**
* Adds a cookie.
*/
public function addCookie(Cookie $cookie): void
{
$this->cookies[] = $cookie;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->cookies);
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function count()
{
return \count($this->cookies);
}
/**
* Get the cookie at the given index.
*
* @param int $i
*
* @return Cookie
*/
public function getAt($i): Cookie
{
if (!isset($this->cookies[$i])) {
throw new \RuntimeException(\sprintf('No cookie at index %s', $i));
}
return $this->cookies[$i];
}
/**
* Find cookies with matching values.
*
* usage:
*
* ```
* // find cookies having name == 'foo'
* $newCookies = $cookies->filterBy('name', 'foo');
*
* // find cookies having domain == 'example.com'
* $newCookies = $cookies->filterBy('domain', 'example.com');
* ```
*
* @param string $param
* @param string $value
*
* @return CookiesCollection
*/
public function filterBy(string $param, string $value)
{
return new self(\array_filter($this->cookies, function (Cookie $cookie) use ($param, $value) {
return $cookie[$param] == $value;
}));
}
/**
* Find first cookies with matching value.
*
* usage:
*
* ```
* // find first cookie having name == 'foo'
* $cookie = $cookies->findOneBy('name', 'foo');
*
* if ($cookie) {
* // do something
* }
* ```
*
* @param string $param
* @param string $value
*
* @return Cookie|null
*/
public function findOneBy(string $param, string $value)
{
foreach ($this->cookies as $cookie) {
if ($cookie[$param] == $value) {
return $cookie;
}
}
return null;
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace HeadlessChromium\Dom;
use HeadlessChromium\Communication\Message;
use HeadlessChromium\Page;
class Dom extends Node
{
public function __construct(Page $page)
{
$message = new Message('DOM.getDocument');
$stream = $page->getSession()->sendMessage($message);
$response = $stream->waitForResponse(1000);
$rootNodeId = $response->getResultData('root')['nodeId'];
parent::__construct($page, $rootNodeId);
}
public function search(string $selector): array
{
$message = new Message('DOM.performSearch', [
'query' => $selector,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
$searchId = $response->getResultData('searchId');
$count = $response->getResultData('resultCount');
if (0 === $count) {
return [];
}
$message = new Message('DOM.getSearchResults', [
'searchId' => $searchId,
'fromIndex' => 0,
'toIndex' => $count,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
$nodes = [];
$nodeIds = $response->getResultData('nodeIds');
foreach ($nodeIds as $nodeId) {
$nodes[] = new Node($this->page, $nodeId);
}
return $nodes;
}
}

View File

@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace HeadlessChromium\Dom;
use HeadlessChromium\Communication\Message;
use HeadlessChromium\Communication\Response;
use HeadlessChromium\Exception\DomException;
use HeadlessChromium\Page;
class Node
{
/**
* @var Page
*/
protected $page;
/**
* @var int
*/
protected $nodeId;
public function __construct(Page $page, int $nodeId)
{
$this->page = $page;
$this->nodeId = $nodeId;
}
public function getAttributes(): NodeAttributes
{
$message = new Message('DOM.getAttributes', [
'nodeId' => $this->nodeId,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
$attributes = $response->getResultData('attributes');
return new NodeAttributes($attributes);
}
public function setAttributeValue(string $name, string $value): void
{
$message = new Message('DOM.setAttributeValue', [
'nodeId' => $this->nodeId,
'name' => $name,
'value' => $value,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
}
public function querySelector(string $selector): ?self
{
$message = new Message('DOM.querySelector', [
'nodeId' => $this->nodeId,
'selector' => $selector,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
$nodeId = $response->getResultData('nodeId');
if (null !== $nodeId && 0 !== $nodeId) {
return new self($this->page, $nodeId);
}
return null;
}
public function querySelectorAll(string $selector): array
{
$message = new Message('DOM.querySelectorAll', [
'nodeId' => $this->nodeId,
'selector' => $selector,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
$nodes = [];
$nodeIds = $response->getResultData('nodeIds');
foreach ($nodeIds as $nodeId) {
$nodes[] = new self($this->page, $nodeId);
}
return $nodes;
}
public function focus(): void
{
$message = new Message('DOM.focus', [
'nodeId' => $this->nodeId,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
}
public function getAttribute(string $name): ?string
{
return $this->getAttributes()->get($name);
}
public function getPosition(): ?NodePosition
{
$message = new Message('DOM.getBoxModel', [
'nodeId' => $this->nodeId,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
$points = $response->getResultData('model')['content'];
if (null !== $points) {
return new NodePosition($points);
} else {
return null;
}
}
public function hasPosition(): bool
{
return null !== $this->getPosition();
}
public function getHTML(): string
{
$message = new Message('DOM.getOuterHTML', [
'nodeId' => $this->nodeId,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
return $response->getResultData('outerHTML');
}
public function getText(): string
{
return \strip_tags($this->getHTML());
}
public function scrollIntoView(): void
{
$message = new Message('DOM.scrollIntoViewIfNeeded', [
'nodeId' => $this->nodeId,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
}
/**
* @throws DomException
*/
public function click(): void
{
if (false === $this->hasPosition()) {
throw new DomException('Failed to click element without position');
}
$this->scrollIntoView();
$position = $this->getPosition();
$this->page->mouse()
->move($position->getCenterX(), $position->getCenterY())
->click();
}
public function sendKeys(string $text): void
{
$this->scrollIntoView();
$this->focus();
$this->page->keyboard()
->typeText($text);
}
public function sendFile(string $filePath): void
{
$this->sendFiles([$filePath]);
}
public function sendFiles(array $filePaths): void
{
$message = new Message('DOM.setFileInputFiles', [
'files' => $filePaths,
'nodeId' => $this->nodeId,
]);
$response = $this->page->getSession()->sendMessageSync($message);
$this->assertNotError($response);
}
/**
* @throws DomException
*/
public function assertNotError(Response $response): void
{
if (!$response->isSuccessful()) {
throw new DOMException($response->getErrorMessage());
}
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace HeadlessChromium\Dom;
class NodeAttributes
{
/**
* @var array
*/
private $attributes = [];
public function __construct(array $attrs)
{
for ($i = 0; $i <= \count($attrs) - 2; $i += 2) {
$this->attributes[$attrs[$i]] = $attrs[$i + 1];
}
}
public function toArray(): array
{
return $this->attributes;
}
public function has(string $name): bool
{
return isset($this->attributes[$name]);
}
public function get(string $name): ?string
{
return $this->attributes[$name] ?? null;
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace HeadlessChromium\Dom;
class NodePosition
{
/**
* @var float
*/
private $x;
/**
* @var float
*/
private $y;
/**
* @var float
*/
private $width;
/**
* @var float
*/
private $height;
public function __construct(array $points)
{
$leftTopX = $points[0];
$leftTopY = $points[1];
$rightTopX = $points[2];
$rightTopY = $points[3];
$rightBottomX = $points[4];
$rightBottomY = $points[5];
$leftBottomX = $points[6];
$leftBottomY = $points[7];
$this->x = $leftTopX;
$this->y = $leftTopY;
$this->height = $leftBottomY - $leftTopY;
$this->width = $rightBottomX - $leftBottomX;
}
public function getX(): int
{
return (int) $this->x;
}
public function getY(): int
{
return (int) $this->y;
}
public function getWidth(): int
{
return (int) $this->width;
}
public function getHeight(): int
{
return (int) $this->height;
}
public function getCenterX(): int
{
return (int) ($this->x + ($this->width / 2));
}
public function getCenterY(): int
{
return (int) ($this->y + ($this->height / 2));
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace HeadlessChromium\Dom\Selector;
/**
* @see https://developer.mozilla.org/docs/Web/API/Document/querySelector
*/
final class CssSelector implements Selector
{
/** @var string */
private $expression;
public function __construct(string $expression)
{
$this->expression = $expression;
}
public function expressionCount(): string
{
return \sprintf('document.querySelectorAll("%s").length', $this->expression);
}
public function expressionFindOne(int $position): string
{
return \sprintf('document.querySelectorAll("%s")[%d]', $this->expression, $position - 1);
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace HeadlessChromium\Dom\Selector;
interface Selector
{
public function expressionCount(): string;
public function expressionFindOne(int $position): string;
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace HeadlessChromium\Dom\Selector;
/**
* @see https://developer.mozilla.org/en-US/docs/Web/XPath/Introduction_to_using_XPath_in_JavaScript
*/
final class XPathSelector implements Selector
{
/** @var string */
private $expression;
public function __construct(string $expression)
{
$this->expression = $expression;
}
public function expressionCount(): string
{
return \sprintf(
'document.evaluate("%s", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength',
\addslashes($this->expression)
);
}
public function expressionFindOne(int $position): string
{
return \sprintf(
'document.evaluate("%s[%d]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue',
\addslashes($this->expression),
$position
);
}
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class BrowserConnectionFailed extends \Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class CommunicationException extends \Exception
{
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception\CommunicationException;
use HeadlessChromium\Exception\CommunicationException;
class CannotReadResponse extends CommunicationException
{
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception\CommunicationException;
use HeadlessChromium\Exception\CommunicationException;
class InvalidResponse extends CommunicationException
{
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception\CommunicationException;
use HeadlessChromium\Exception\CommunicationException;
class ResponseHasError extends CommunicationException
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace HeadlessChromium\Exception;
use Exception;
class DomException extends Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class ElementNotFoundException extends \Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class EvaluationFailed extends \Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class FilesystemException extends \Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class InvalidTimezoneId extends \Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class JavascriptException extends \Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class NavigationExpired extends \Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class NoResponseAvailable extends \Exception
{
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class OperationTimedOut extends \Exception
{
public static function createFromTimeout(int $timeoutMicroSec): self
{
return new self(\sprintf('Operation timed out after %s.', self::getTimeoutPhrase($timeoutMicroSec)));
}
private static function getTimeoutPhrase(int $timeoutMicroSec): string
{
if ($timeoutMicroSec > 1000 * 1000) {
return \sprintf('%ds', (int) ($timeoutMicroSec / (1000 * 1000)));
}
if ($timeoutMicroSec > 1000) {
return \sprintf('%dms', (int) ($timeoutMicroSec / 1000));
}
return \sprintf('%dμs', (int) ($timeoutMicroSec));
}
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class PdfFailed extends \Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class ScreenshotFailed extends \Exception
{
}

View File

@ -0,0 +1,16 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Exception;
class TargetDestroyed extends \RuntimeException
{
}

View File

@ -0,0 +1,110 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium;
class Frame
{
public const LIFECYCLE_INIT = 'init';
/**
* @var array
*/
protected $frameData;
/**
* @var array
*/
protected $lifeCycleEvents = [];
/**
* @var string
*/
protected $latestLoaderId;
/**
* @var string
*/
protected $frameId;
/**
* @var int
*/
protected $executionContextId;
/**
* Frame constructor.
*
* @param array $frameData
*/
public function __construct(array $frameData)
{
$this->frameData = $frameData;
$this->latestLoaderId = $frameData['loaderId'];
$this->frameId = $frameData['id'];
}
/**
* @internal
*/
public function onLifecycleEvent(array $params): void
{
if (self::LIFECYCLE_INIT === $params['name']) {
$this->lifeCycleEvents = [];
$this->latestLoaderId = $params['loaderId'];
$this->frameId = $params['frameId'];
}
$this->lifeCycleEvents[$params['name']] = $params['timestamp'];
}
/**
* @return int
*/
public function getExecutionContextId(): int
{
return $this->executionContextId;
}
/**
* @param int $executionContextId
*/
public function setExecutionContextId(int $executionContextId): void
{
$this->executionContextId = $executionContextId;
}
/**
* @return string
*/
public function getFrameId(): string
{
return $this->frameId;
}
/**
* @return string
*/
public function getLatestLoaderId(): string
{
return $this->latestLoaderId;
}
/**
* Gets the life cycle events of the frame with the time they occurred at.
*
* @return array
*/
public function getLifeCycle(): array
{
return $this->lifeCycleEvents;
}
}

View File

@ -0,0 +1,106 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium;
class FrameManager
{
/**
* @var Page
*/
protected $page;
/**
* @var Frame[]
*/
protected $frames = [];
/**
* @var Frame
*/
protected $mainFrame;
/**
* FrameManager constructor.
*/
public function __construct(Page $page, array $frameTree)
{
$this->page = $page;
if (isset($frameTree['frame'])) {
// TODO parse children frames
$this->frames[$frameTree['frame']['id']] = new Frame($frameTree['frame']);
// associate main frame
$this->mainFrame = $this->frames[$frameTree['frame']['id']];
}
// TODO listen for frame events
// update frame on init
$this->page->getSession()->on('method:Page.lifecycleEvent', function (array $params): void {
if (isset($this->frames[$params['frameId']])) {
$frame = $this->frames[$params['frameId']];
$frame->onLifecycleEvent($params);
}
});
// attach context id to frame
$this->page->getSession()->on('method:Runtime.executionContextCreated', function (array $params): void {
if (isset($params['context']['auxData']['frameId']) && $params['context']['auxData']['isDefault']) {
if ($this->hasFrame($params['context']['auxData']['frameId'])) {
$frame = $this->getFrame($params['context']['auxData']['frameId']);
$frame->setExecutionContextId($params['context']['id']);
}
}
});
// TODO maybe implement Runtime.executionContextDestroyed and Runtime.executionContextsCleared
}
/**
* Checks if the given frame exists.
*
* @param string $frameId
*
* @return bool
*/
public function hasFrame($frameId): bool
{
return \array_key_exists($frameId, $this->frames);
}
/**
* Get a frame given its id.
*
* @param string $frameId
*
* @return Frame
*/
public function getFrame($frameId): Frame
{
if (!isset($this->frames[$frameId])) {
throw new \RuntimeException(\sprintf('No such frame "%s"', $frameId));
}
return $this->frames[$frameId];
}
/**
* Gets the main frame.
*
* @return Frame
*/
public function getMainFrame(): Frame
{
return $this->mainFrame;
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Input;
/**
* Holds key constants and their respective bit values.
*
* @see https://chromedevtools.github.io/devtools-protocol/1-2/Input/
*/
abstract class Key
{
public const ALT = 1;
public const CONTROL = 2;
public const META = 4;
public const SHIFT = 8;
public const COMMAND = self::META;
}

View File

@ -0,0 +1,236 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Input;
use HeadlessChromium\Communication\Message;
use HeadlessChromium\Page;
class Keyboard
{
use KeyboardKeys;
/**
* @var Page
*/
protected $page;
/**
* @var int
*/
protected $sleep = 0;
/**
* @param Page $page
*/
public function __construct(Page $page)
{
$this->page = $page;
}
/**
* Type a text string, char by char, without applying modifiers.
*
* @param string $text text string to be typed
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*
* @return $this
*/
public function typeText(string $text)
{
$this->page->assertNotClosed();
$length = \mb_strlen($text);
for ($i = 0; $i < $length; ++$i) {
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchKeyEvent', [
'type' => 'char',
'modifiers' => $this->getModifiers(),
'text' => \mb_substr($text, $i, 1),
]));
\usleep($this->sleep);
}
return $this;
}
/**
* Type a raw key using the rawKeyDown event, without sending any codes or modifiers.
*
* Example:
*
* ```php
* $page->keyboard()->typeRawKey('Tab');
* ```
*
* @param string $key single raw key to be typed
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*
* @return $this
*/
public function typeRawKey(string $key): self
{
$this->page->assertNotClosed();
$this->onKeyPress($key);
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchKeyEvent', [
'type' => 'rawKeyDown',
'key' => $key,
]));
\usleep($this->sleep);
$this->release($key);
return $this;
}
/**
* Press and release a single key.
*
* Example:
*
* ```php
* $page->keyboard()->type('a');
* ```
*
* @param string $key single key to be typed
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*
* @return $this
*/
public function type(string $key): self
{
return $this->press($key)->release($key);
}
/**
* Press a single key with key codes and modifiers.
*
* A key can be pressed multiple times sequentially. This is what happens
* in a real browser when the user presses and holds down hown the key.
*
* Example:
*
* ```php
* $page->keyboard()->press('Control')->press('c'); // press ctrl + c
* ```
*
* @param string $key single key to be pressed
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*
* @return $this
*/
public function press(string $key): self
{
$this->page->assertNotClosed();
$this->onKeyPress($key);
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchKeyEvent', [
'type' => 'keyDown',
'modifiers' => $this->getModifiers(),
'text' => $key,
'key' => $this->getCurrentKey(),
'windowsVirtualKeyCode' => $this->getKeyCode(),
]));
\usleep($this->sleep);
return $this;
}
/**
* Release a single key.
*
* A key is released only once, even if it was pressed multiple times.
* If no key is given, all pressed keys will be released.
*
* Example:
*
* ```php
* $page->keyboard()->release('Control'); // release Control
* $page->keyboard()->release(); // release all
* ```
*
* @param string $key (optional) single key to be released
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*
* @return $this
*/
public function release(string $key = null): self
{
$this->page->assertNotClosed();
if (null === $key) {
$this->releaseAll();
return $this;
}
$this->onKeyRelease($key);
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchKeyEvent', [
'type' => 'keyUp',
'key' => $this->getCurrentKey(),
]));
\usleep($this->sleep);
return $this;
}
/**
* Release all pressed keys.
*
* @return self
*/
private function releaseAll(): self
{
foreach ($this->pressedKeys as $key => $value) {
if (true === $value) {
$this->release($key);
}
}
return $this;
}
/**
* Set the time interval between key strokes in milliseconds.
*
* @param int $milliseconds
*
* @return $this
*/
public function setKeyInterval(int $milliseconds)
{
if ($milliseconds < 0) {
$milliseconds = 0;
}
$this->sleep = $milliseconds * 1000;
return $this;
}
}

View File

@ -0,0 +1,223 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Input;
/**
* Translates typed keys to their respective codes.
*
* @see https://chromedevtools.github.io/devtools-protocol/1-2/Input/
*/
trait KeyboardKeys
{
/**
* Array of currently pressed keys (keyDown events).
*
* The elements of this array should be unique. A real keyboard can create several keyDown events
* by holding down a key, but only one keyUp event will be sent when the key is released.
*/
protected $pressedKeys = [];
/**
* Current key as a sanitized string.
*
* Single letters like "v" must be in uppercase, otherwise key combinations like ctrl + v won't work.
*/
protected $currentKey = '';
/**
* Bit field representing pressed modifier keys.
*/
protected $modifiers = 0;
/**
* Aliases for modifier keys, in lowercase.
*/
protected $keyAliases = [
Key::ALT => [
'alt',
'altgr',
'alt gr',
],
Key::CONTROL => [
'control',
'ctrl',
'ctr',
],
Key::META => [
'meta',
'command',
'cmd',
],
Key::SHIFT => [
'shift',
],
];
/**
* Register a pressed key and apply modifiers.
*
* @param string $key pressed key
*
* @return void
*/
protected function onKeyPress(string $key): void
{
$this->setCurrentKey($key);
if (true === $this->isKeyPressed()) {
return;
}
$this->pressedKeys[$this->currentKey] = true;
$this->toggleModifierFromKey();
}
/**
* Register a released key and remove modifiers.
*
* @param string $key released key
*
* @return void
*/
protected function onKeyRelease(string $key): void
{
$this->setCurrentKey($key);
if (false === $this->isKeyPressed()) {
return;
}
unset($this->pressedKeys[$this->currentKey]);
$this->toggleModifierFromKey();
}
/**
* Check the current key against the list of aliases.
* If it match, try to add or remove its bits to the modifier.
*
* @see self::$keyAliases
* @see self::$modifiers
*
* @return void
*/
protected function toggleModifierFromKey(): void
{
$key = \strtolower($this->currentKey);
foreach ($this->keyAliases as $modifier => $aliases) {
if (true === \in_array($key, $aliases)) {
$this->toggleModifier($modifier);
break;
}
}
}
/**
* Perform bit operations to add or remove bits from the modifier.
*
* Examples:
*
* 0001
* | 0100
* = 0101
*
* 0101
* & 0100
* = 0100
*
* 0101
* & 0010
* = 0000
*
* @see self::$modifiers
*
* @return void
*/
protected function toggleModifier(int $bit): void
{
if (($this->modifiers & $bit) === $bit) {
$this->modifiers &= ~$bit;
return;
}
$this->modifiers |= $bit;
}
/**
* Check if the current key was pressed and not released yet.
*
* @return bool true if they key is listed as pressed
*/
protected function isKeyPressed(): bool
{
return \array_key_exists($this->currentKey, $this->pressedKeys);
}
/**
* Return the current key code.
*
* @return int the key code
*/
public function getKeyCode(): int
{
return \ord($this->currentKey);
}
/**
* Return the current bit modifier.
* The browser expects to receive this value as int.
*
* @return int current bit modifier
*/
public function getModifiers(): int
{
return $this->modifiers;
}
/**
* Return the current key being processed.
*
* @return string the current key
*/
public function getCurrentKey(): string
{
return $this->currentKey;
}
/**
* Return the list of unique pressed keys that were not released yet.
*
* @return array list of pressed keys
*/
public function getPressedKeys(): array
{
return $this->pressedKeys;
}
/**
* Set a key as the current key.
*
* Single character keys must be in uppercase, otherwhie things like ctrl + v won't work.
* Triming the string will also prevent future mistakes during normal usage.
*
* @param string $key key to be set as current
*
* @return void
*/
protected function setCurrentKey(string $key): void
{
$this->currentKey = \ucfirst(\trim($key));
}
}

View File

@ -0,0 +1,392 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\Input;
use HeadlessChromium\Communication\Message;
use HeadlessChromium\Dom\Selector\CssSelector;
use HeadlessChromium\Dom\Selector\Selector;
use HeadlessChromium\Exception\ElementNotFoundException;
use HeadlessChromium\Exception\JavascriptException;
use HeadlessChromium\Page;
use HeadlessChromium\Utils;
class Mouse
{
public const BUTTON_LEFT = 'left';
public const BUTTON_NONE = 'none';
public const BUTTON_RIGHT = 'right';
public const BUTTON_MIDDLE = 'middle';
/**
* @var Page
*/
protected $page;
protected $x = 0;
protected $y = 0;
protected $button = self::BUTTON_NONE;
/**
* @param Page $page
*/
public function __construct(Page $page)
{
$this->page = $page;
}
/**
* @param int $x
* @param int $y
* @param array|null $options
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*
* @return $this
*/
public function move(int $x, int $y, array $options = null)
{
$this->page->assertNotClosed();
// get origin of the move
$originX = $this->x;
$originY = $this->y;
// set new position after move
$this->x = $x;
$this->y = $y;
// number of steps to achieve the move
$steps = $options['steps'] ?? 1;
if ($steps <= 0) {
throw new \InvalidArgumentException('options "steps" for mouse move must be a positive integer');
}
// move
for ($i = 1; $i <= $steps; ++$i) {
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchMouseEvent', [
'x' => $originX + ($this->x - $originX) * ($i / $steps),
'y' => $originY + ($this->y - $originY) * ($i / $steps),
'type' => 'mouseMoved',
]));
}
return $this;
}
/**
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*/
public function press(array $options = null)
{
$this->page->assertNotClosed();
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchMouseEvent', [
'x' => $this->x,
'y' => $this->y,
'type' => 'mousePressed',
'button' => $options['button'] ?? self::BUTTON_LEFT,
'clickCount' => 1,
]));
return $this;
}
/**
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*/
public function release(array $options = null)
{
$this->page->assertNotClosed();
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchMouseEvent', [
'x' => $this->x,
'y' => $this->y,
'type' => 'mouseReleased',
'button' => $options['button'] ?? self::BUTTON_LEFT,
'clickCount' => 1,
]));
return $this;
}
/**
* @param array|null $options
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*/
public function click(array $options = null)
{
$this->press($options);
$this->release($options);
return $this;
}
/**
* Scroll up using the mouse wheel.
*
* @param int $distance Distance in pixels
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*
* @return $this
*/
public function scrollUp(int $distance)
{
return $this->scroll((-1 * \abs($distance)));
}
/**
* Scroll down using the mouse wheel.
*
* @param int $distance Distance in pixels
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*
* @return $this
*/
public function scrollDown(int $distance)
{
return $this->scroll(\abs($distance));
}
/**
* Scroll a positive or negative distance using the mouseWheel event type.
*
* @param int $distanceY Distance in pixels for the Y axis
* @param int $distanceX (optional) Distance in pixels for the X axis
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return $this
*/
private function scroll(int $distanceY, int $distanceX = 0): self
{
$this->page->assertNotClosed();
$scrollableArea = $this->page->getLayoutMetrics()->getCssContentSize();
$visibleArea = $this->page->getLayoutMetrics()->getCssVisualViewport();
$maximumX = $scrollableArea['width'] - $visibleArea['clientWidth'];
$maximumY = $scrollableArea['height'] - $visibleArea['clientHeight'];
$distanceX = $this->getMaximumDistance($distanceX, $visibleArea['pageX'], $maximumX);
$distanceY = $this->getMaximumDistance($distanceY, $visibleArea['pageY'], $maximumY);
$targetX = $visibleArea['pageX'] + $distanceX;
$targetY = $visibleArea['pageY'] + $distanceY;
// make sure the mouse is on the screen
$this->move($this->x, $this->y);
// scroll
$this->page->getSession()->sendMessageSync(new Message('Input.dispatchMouseEvent', [
'type' => 'mouseWheel',
'x' => $this->x,
'y' => $this->y,
'deltaX' => $distanceX,
'deltaY' => $distanceY,
]));
// wait until the scroll is done
Utils::tryWithTimeout(30000 * 1000, $this->waitForScroll($targetX, $targetY));
// set new position after move
$this->x += $distanceX;
$this->y += $distanceY;
return $this;
}
/**
* Scroll in both X and Y axis until the given boundaries fit in the screen.
*
* This method currently scrolls only to right and bottom. If the desired element is outside the visible screen
* to the left or top, thie method will not work. Its visibility will stay private until it works for both cases.
*
* @param int $right The element right boundary
* @param int $bottom The element bottom boundary
*
* @return $this
*/
private function scrollToBoundary(int $right, int $bottom): self
{
$visibleArea = $this->page->getLayoutMetrics()->getCssLayoutViewport();
$distanceX = $distanceY = 0;
if ($right > $visibleArea['clientWidth']) {
$distanceX = $right - $visibleArea['clientWidth'];
}
if ($bottom > $visibleArea['clientHeight']) {
$distanceY = $bottom - $visibleArea['clientHeight'];
}
return $this->scroll($distanceY, $distanceX);
}
/**
* Find an element and move the mouse to a random position over it.
*
* The search could result in several elements. The $position param can be used to select a specific element.
* The given position can only be between 1 and the maximum number or elements. It will be adjusted to the
* minimum and maximum values if needed.
*
* Example:
* $page->mouse()->find('#a'):
* $page->mouse()->find('.a', 2);
*
* @see https://developer.mozilla.org/docs/Web/API/Document/querySelector
*
* @param string $selectors selectors to use with document.querySelector
* @param int $position (optional) which element of the result set should be used
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\ElementNotFoundException
*
* @return $this
*/
public function find(string $selectors, int $position = 1): self
{
$this->findElement(new CssSelector($selectors), $position);
return $this;
}
/**
* Find an element and move the mouse to a random position over it.
*
* The search could result in several elements. The $position param can be used to select a specific element.
* The given position can only be between 1 and the maximum number or elements. It will be adjusted to the
* minimum and maximum values if needed.
*
* Example:
* $page->mouse()->findElement(new CssSelector('#a')):
* $page->mouse()->findElement(new CssSelector('.a'), 2);
* $page->mouse()->findElement(new XPathSelector('//*[@id="a"]'), 2);
*
* @param Selector $selector selector to use
* @param int $position (optional) which element of the result set should be used
*
* @throws \HeadlessChromium\Exception\CommunicationException
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\ElementNotFoundException
*
* @return $this
*/
public function findElement(Selector $selector, int $position = 1): self
{
$this->page->assertNotClosed();
try {
$element = Utils::getElementPositionFromPage($this->page, $selector, $position);
} catch (JavascriptException $exception) {
throw new ElementNotFoundException('The search for "'.$selector->expressionCount().'" returned no result.');
}
if (false === \array_key_exists('x', $element)) {
throw new ElementNotFoundException('The search for "'.$selector->expressionFindOne($position).'" returned an element with no position.');
}
$rightBoundary = \floor($element['right']);
$bottomBoundary = \floor($element['bottom']);
$this->scrollToBoundary($rightBoundary, $bottomBoundary);
$visibleArea = $this->page->getLayoutMetrics()->getLayoutViewport();
$offsetX = $visibleArea['pageX'];
$offsetY = $visibleArea['pageY'];
$minX = $element['left'] - $offsetX;
$minY = $element['top'] - $offsetY;
$positionX = \floor($minX + (($rightBoundary - $offsetX) - $minX) / 2);
$positionY = \ceil($minY + (($bottomBoundary - $offsetY) - $minY) / 2);
$this->move($positionX, $positionY);
return $this;
}
/**
* Get the maximum distance to scroll a page.
*
* @param int $distance Distance to scroll, positive or negative
* @param int $current Current position
* @param int $maximum Maximum possible distance
*
* @return int allowed distance to scroll
*/
private function getMaximumDistance(int $distance, int $current, int $maximum): int
{
$result = $current + $distance;
if ($result < 0) {
return $distance + \abs($result);
}
if ($result > $maximum) {
return $maximum - $current;
}
return $distance;
}
/**
* Wait for the browser to process the scroll command.
*
* Return the number of microseconds to wait before trying again or true in case of success.
*
* @see \HeadlessChromium\Utils::tryWithTimeout
*
* @param int $targetX
* @param int $targetY
*
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return bool|\Generator
*/
private function waitForScroll(int $targetX, int $targetY)
{
while (true) {
$visibleArea = $this->page->getLayoutMetrics()->getCssVisualViewport();
if ($visibleArea['pageX'] === $targetX && $visibleArea['pageY'] === $targetY) {
return true;
}
yield 1000;
}
}
/**
* Get the current mouse position.
*
* @return array [x, y]
*/
public function getPosition(): array
{
return [
'x' => $this->x,
'y' => $this->y,
];
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\PageUtils;
use HeadlessChromium\Communication\ResponseReader;
use HeadlessChromium\Exception\FilesystemException;
use HeadlessChromium\Exception\ScreenshotFailed;
abstract class AbstractBinaryInput
{
/**
* @var ResponseReader
*/
protected $responseReader;
/**
* @param ResponseReader $responseReader
*/
public function __construct(ResponseReader $responseReader)
{
$this->responseReader = $responseReader;
}
/**
* @return ResponseReader
*/
public function getResponseReader(): ResponseReader
{
return $this->responseReader;
}
/**
* Get base64 representation of the file.
*
* @return mixed
*/
public function getBase64(int $timeout = null)
{
$response = $this->responseReader->waitForResponse($timeout);
if (!$response->isSuccessful()) {
throw $this->getException($response->getErrorMessage());
}
return $response->getResultData('data');
}
/**
* Save data to the given file.
*
* @param string $path
*
* @throws FilesystemException
* @throws ScreenshotFailed
*/
public function saveToFile(string $path, int $timeout = 5000): void
{
$response = $this->responseReader->waitForResponse($timeout);
if (!$response->isSuccessful()) {
throw $this->getException($response->getErrorMessage());
}
// create directory
$dir = \dirname($path);
if (!\file_exists($dir)) {
if (!\mkdir($dir, 0777, true)) {
throw new FilesystemException(\sprintf('Could not create the directory %s.', $dir));
}
}
// save
if (\file_exists($path)) {
if (!\is_writable($path)) {
throw new FilesystemException(\sprintf('The file %s is not writable.', $path));
}
} else {
if (!\touch($path)) {
throw new FilesystemException(\sprintf('The file %s could not be created.', $path));
}
}
$file = \fopen($path, 'w');
\stream_filter_append($file, 'convert.base64-decode');
\fwrite($file, $response->getResultData('data'));
\fclose($file);
}
/**
* @internal
*
* @return \Exception
*/
abstract protected function getException(string $message): \Exception;
}

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\PageUtils;
use HeadlessChromium\Cookies\CookiesCollection;
/**
* @internal
*/
class CookiesGetter extends ResponseWaiter
{
/**
* Gets the cookies collection.
*
* @throws \HeadlessChromium\Exception\NoResponseAvailable
*
* @return CookiesCollection
*/
public function getCookies()
{
return new CookiesCollection(
$this->responseReader->getResponse()->getResultData('cookies')
);
}
}

View File

@ -0,0 +1,131 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\PageUtils;
use HeadlessChromium\Communication\Response;
use HeadlessChromium\Communication\ResponseReader;
use HeadlessChromium\Exception\EvaluationFailed;
use HeadlessChromium\Exception\JavascriptException;
use HeadlessChromium\Page;
/**
* Used to read data from page evaluation response.
*
* @internal
*/
class PageEvaluation
{
/**
* @var ResponseReader
*/
protected $responseReader;
/**
* @var Response
*/
protected $response;
/**
* @var string
*/
protected $pageLoaderId;
/**
* @var Page
*/
protected $page;
/**
* PageEvaluation constructor.
*
* @param ResponseReader $responseReader\
* @param string $pageLoaderId
*
* @internal
*/
public function __construct(ResponseReader $responseReader, $pageLoaderId, Page $page)
{
$this->responseReader = $responseReader;
$this->pageLoaderId = $pageLoaderId;
$this->page = $page;
}
/**
* If the script requested a page reload this method will help to wait for the page to be fully reloaded.
*/
public function waitForPageReload($eventName = Page::LOAD, $timeout = 30000): void
{
$this->page->waitForReload($eventName, $timeout, $this->pageLoaderId);
}
/**
* Wait for the script to evaluate and to return a valid response.
*
* @param int|null $timeout
*/
public function waitForResponse(int $timeout = null)
{
$this->response = $this->responseReader->waitForResponse($timeout);
if (!$this->response->isSuccessful()) {
throw new EvaluationFailed(\sprintf('Could not evaluate the script in the page. Message: "%s"', $this->response->getErrorMessage(true)));
}
$result = $this->response->getResultData('result');
$resultSubType = $result['subtype'] ?? null;
if ('error' == $resultSubType) {
// TODO dump javascript trace
throw new JavascriptException('Error during javascript evaluation: '.$result['description']);
}
return $this;
}
/**
* Gets the value produced when the script evaluated in the page.
*
* @param int|null $timeout
*
* @throws EvaluationFailed
* @throws JavascriptException
*
* @return mixed
*/
public function getReturnValue(int $timeout = null)
{
if (!$this->response) {
$this->waitForResponse($timeout);
}
return $this->response->getResultData('result')['value'] ?? null;
}
/**
* Gets the return type of the response from the page.
*
* @param int|null $timeout
*
* @throws EvaluationFailed
*
* @return mixed
*/
public function getReturnType(int $timeout = null)
{
if (!$this->response) {
$this->waitForResponse($timeout);
}
return $this->response->getResultData('result')['type'] ?? null;
}
}

View File

@ -0,0 +1,128 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\PageUtils;
use HeadlessChromium\Exception\CommunicationException;
/**
* Used to read layout metrics of the page.
*
* @internal
*/
class PageLayoutMetrics extends ResponseWaiter
{
/**
* Returns raw page metrics data.
*
* @throws CommunicationException\ResponseHasError
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return array
*/
public function getMetrics(): array
{
$response = $this->awaitResponse();
return $response->getData()['results'];
}
/**
* Returns size of scrollable area.
*
* @throws CommunicationException\ResponseHasError
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return array
*/
public function getContentSize(): array
{
return $this->getResultData('contentSize');
}
/**
* Returns metrics relating to the layout viewport.
*
* @throws CommunicationException\ResponseHasError
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return array
*/
public function getLayoutViewport(): array
{
return $this->getResultData('layoutViewport');
}
/**
* Returns metrics relating to the visual viewport.
*
* @throws CommunicationException\ResponseHasError
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return array
*/
public function getVisualViewport()
{
return $this->getResultData('visualViewport');
}
/**
* Returns real size of scrollable area.
*
* @throws CommunicationException\ResponseHasError
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return array
*/
public function getCssContentSize(): array
{
return $this->getResultData('cssContentSize') ?? $this->getContentSize();
}
/**
* Returns real metrics relating to the layout viewport.
*
* @throws CommunicationException\ResponseHasError
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return array
*/
public function getCssLayoutViewport(): array
{
return $this->getResultData('cssLayoutViewport') ?? $this->getLayoutViewport();
}
/**
* Returns real metrics relating to the visual viewport.
*
* @throws CommunicationException\ResponseHasError
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return array
*/
public function getCssVisualViewport()
{
return $this->getResultData('cssVisualViewport') ?? $this->getVisualViewport();
}
/** @param 'layoutViewport'|'visualViewport'|'contentSize'|'cssLayoutViewport'|'cssVisualViewport'|'cssContentSize' $key */
private function getResultData(string $key): array
{
return $this->awaitResponse()->getResultData($key);
}
}

View File

@ -0,0 +1,196 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\PageUtils;
use HeadlessChromium\Communication\Message;
use HeadlessChromium\Communication\ResponseReader;
use HeadlessChromium\Exception;
use HeadlessChromium\Exception\CommunicationException\ResponseHasError;
use HeadlessChromium\Exception\NavigationExpired;
use HeadlessChromium\Frame;
use HeadlessChromium\Page;
use HeadlessChromium\Utils;
/**
* A class that is aimed to be used withing the method Page::navigate.
*
* @internal
*/
class PageNavigation
{
/**
* @var Frame
*/
protected $frame;
/**
* @var string
*/
protected $previousLoaderId;
/**
* @var string
*/
protected $currentLoaderId;
/**
* @var ResponseReader
*/
protected $navigateResponseReader;
/**
* @var string
*/
protected $url;
/**
* @var Page
*/
protected $page;
/**
* @var bool
*/
protected $strict;
/**
* PageNavigation constructor.
*
* @param Page $page
* @param string $url
* @param bool $strict by default this method will wait for the page to load even if a new navigation occurs
* (ie: a new loader replaced the initial navigation). Passing $string to true will make the navigation to fail
* if a new loader is generated
*
* @throws Exception\CommunicationException
* @throws Exception\CommunicationException\CannotReadResponse
* @throws Exception\CommunicationException\InvalidResponse
*/
public function __construct(Page $page, string $url, bool $strict = false)
{
// make sure latest loaderId was pulled
$page->getSession()->getConnection()->readData();
// get previous loaderId for the navigation watcher
$this->previousLoaderId = $page->getFrameManager()->getMainFrame()->getLatestLoaderId();
// send navigation message
$this->navigateResponseReader = $page->getSession()->sendMessage(
new Message('Page.navigate', ['url' => $url])
);
$this->page = $page;
$this->frame = $page->getFrameManager()->getMainFrame();
$this->url = $url;
$this->strict = $strict;
}
/**
* Wait until the page loads.
*
* Usage:
*
* ```php
* $navigation = $page->navigate('http://example.com');
* try {
* // wait max 30 seconds for dom content to load
* $navigation->waitForNavigation(Page::DOM_CONTENT_LOADED, 30000);
* } catch (OperationTimedOut $e) {
* // too long to load
* } catch (NavigationExpired $e) {
* // an other page loaded since this navigation was initiated
* }
* ```
*
* @param string $eventName
* @param int $timeout time in ms to wait for the navigation to complete. Default 30000 (30 seconds)
*
* @throws Exception\CommunicationException\CannotReadResponse
* @throws Exception\CommunicationException\InvalidResponse
* @throws Exception\NoResponseAvailable
* @throws Exception\OperationTimedOut
* @throws NavigationExpired
* @throws ResponseHasError
*
* @return mixed
*/
public function waitForNavigation($eventName = Page::LOAD, int $timeout = null)
{
if (null === $timeout) {
$timeout = 30000;
}
return Utils::tryWithTimeout($timeout * 1000, $this->navigationComplete($eventName));
}
/**
* To be used with @see Utils::tryWithTimeout.
*
* @param string $eventName
*
* @throws Exception\CommunicationException\CannotReadResponse
* @throws Exception\CommunicationException\InvalidResponse
* @throws Exception\NoResponseAvailable
* @throws NavigationExpired
* @throws ResponseHasError
*
* @return bool|\Generator
*/
private function navigationComplete($eventName)
{
$delay = 500;
while (true) {
// read the response only if it was not read already
if (!$this->navigateResponseReader->hasResponse()) {
$this->navigateResponseReader->checkForResponse();
if ($this->navigateResponseReader->hasResponse()) {
$response = $this->navigateResponseReader->getResponse();
if (!$response->isSuccessful()) {
throw new ResponseHasError(\sprintf('Cannot load page for url: "%s". Reason: %s', $this->url, $response->getErrorMessage()));
}
$this->currentLoaderId = $response->getResultData('loaderId');
} else {
yield $delay;
}
}
// make sure that the current loader is the good one
if ($this->frame->getLatestLoaderId() === $this->currentLoaderId) {
// check that lifecycle event exists
if ($this->page->hasLifecycleEvent($eventName)) {
return true;
// or else just wait for the new event to trigger
} else {
yield $delay;
}
// else if frame has still the previous loader, wait for the new one
} elseif ($this->frame->getLatestLoaderId() == $this->previousLoaderId) {
yield $delay;
// else if a new loader is present that means that a new navigation started
} else {
// if strict then throw or else replace the old navigation with the new one
if ($this->strict) {
throw new NavigationExpired('The page has navigated to an other page and this navigation expired');
} else {
$this->currentLoaderId = $this->frame->getLatestLoaderId();
}
}
$this->page->getSession()->getConnection()->readData();
}
}
}

View File

@ -0,0 +1,133 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\PageUtils;
use HeadlessChromium\Communication\Message;
use HeadlessChromium\Exception\PdfFailed;
use HeadlessChromium\Page;
class PagePdf extends AbstractBinaryInput
{
private const TYPE_NUMERIC = 1;
private const TYPE_STRING = 2;
private const TYPE_BOOLEAN = 3;
/**
* @see https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
*/
private const OPTIONS = [
'landscape' => self::TYPE_BOOLEAN,
'printBackground' => self::TYPE_BOOLEAN,
'displayHeaderFooter' => self::TYPE_BOOLEAN,
'headerTemplate' => self::TYPE_STRING,
'footerTemplate' => self::TYPE_STRING,
'paperWidth' => self::TYPE_NUMERIC,
'paperHeight' => self::TYPE_NUMERIC,
'marginTop' => self::TYPE_NUMERIC,
'marginBottom' => self::TYPE_NUMERIC,
'marginLeft' => self::TYPE_NUMERIC,
'marginRight' => self::TYPE_NUMERIC,
'pageRanges' => self::TYPE_STRING,
'ignoreInvalidPageRanges' => self::TYPE_BOOLEAN,
'preferCSSPageSize' => self::TYPE_BOOLEAN,
'scale' => self::TYPE_NUMERIC,
];
/**
* @var Page
*/
private $page;
private $options = [];
/**
* @throws \HeadlessChromium\Exception\CommunicationException
*/
public function __construct(Page $page, array $options = [])
{
$this->page = $page;
$this->setOptions($options)->print();
}
/**
* @throws \HeadlessChromium\Exception\CommunicationException
*/
public function print(): self
{
$responseReader = $this->page->getSession()->sendMessage(new Message('Page.printToPDF', $this->options));
parent::__construct($responseReader);
return $this;
}
/**
* @throws \InvalidArgumentException
*/
public function setOptions(array $options): self
{
\array_map([$this, 'validateOption'], \array_keys($options), $options);
$this->options = $options;
return $this;
}
/**
* @param string $name
* @param string|int|float|bool $value
*
* @throws \InvalidArgumentException
*/
private function validateOption(string $name, $value): bool
{
if (false === \in_array($name, \array_keys(self::OPTIONS))) {
throw new \InvalidArgumentException("Unknown option '{$name}' for print to pdf.");
}
switch (self::OPTIONS[$name]) {
case self::TYPE_NUMERIC:
\is_numeric($value) || $this->invalidArgument("Invalid option '{$name}' for print to pdf. Must be numeric.");
break;
case self::TYPE_STRING:
\is_string($value) || $this->invalidArgument("Invalid option '{$name}' for print to pdf. Must be string.");
break;
case self::TYPE_BOOLEAN:
\is_bool($value) || $this->invalidArgument("Invalid option '{$name}' for print to pdf. Must be boolean.");
break;
}
return true;
}
/**
* {@inheritdoc}
*
* @internal
*/
protected function getException(string $message): \Exception
{
return new PdfFailed(
\sprintf('Cannot make a PDF. Reason : %s', $message)
);
}
/**
* Wrapper to throw exception in expression when running in php 7.
*
* @throws \InvalidArgumentException
*/
private function invalidArgument(string $message): void
{
throw new \InvalidArgumentException($message);
}
}

View File

@ -0,0 +1,29 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\PageUtils;
use HeadlessChromium\Exception\ScreenshotFailed;
class PageScreenshot extends AbstractBinaryInput
{
/**
* {@inheritdoc}
*
* @internal
*/
protected function getException(string $message): \Exception
{
return new ScreenshotFailed(
\sprintf('Cannot make a screenshot. Reason : %s', $message)
);
}
}

View File

@ -0,0 +1,87 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium\PageUtils;
use HeadlessChromium\Communication\Response;
use HeadlessChromium\Communication\ResponseReader;
use HeadlessChromium\Exception\CommunicationException\ResponseHasError;
class ResponseWaiter
{
/**
* @var ResponseReader
*/
protected $responseReader;
/**
* @var Response
*/
protected $response;
/**
* @param ResponseReader $responseReader
*/
public function __construct(ResponseReader $responseReader)
{
$this->responseReader = $responseReader;
}
/**
* Chainable wait for response.
*
* @param int $time
*
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
* @throws \HeadlessChromium\Exception\CommunicationException\ResponseHasError
*
* @return $this
*/
public function await(int $time = null)
{
$this->response = $this->responseReader->waitForResponse($time);
if (!$this->response->isSuccessful()) {
throw new ResponseHasError($this->response->getErrorMessage(true));
}
return $this;
}
/**
* Waits for response and return it.
*
* @param int|null $time
*
* @throws ResponseHasError
* @throws \HeadlessChromium\Exception\NoResponseAvailable
* @throws \HeadlessChromium\Exception\OperationTimedOut
*
* @return Response
*/
protected function awaitResponse(int $time = null): Response
{
if (!$this->response) {
$this->await($time);
}
return $this->response;
}
/**
* @return ResponseReader
*/
public function getResponseReader(): ResponseReader
{
return $this->responseReader;
}
}

View File

@ -0,0 +1,117 @@
<?php
/*
* This file is part of Chrome PHP.
*
* (c) Soufiane Ghzal <sghzal@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HeadlessChromium;
use HeadlessChromium\Communication\Connection;
use HeadlessChromium\Communication\Message;
use HeadlessChromium\Dom\Selector\Selector;
use HeadlessChromium\Exception\CommunicationException;
use HeadlessChromium\Exception\JavascriptException;
use HeadlessChromium\Exception\OperationTimedOut;
class Utils
{
/**
* Iterates on the given generator until the generator returns a value or the given timeout is reached.
*
* When the generator yields a value, this value is the time in microseconds to wait before trying again.
*
* Example waiting for a process to complete:
*
* ```php
* // wait for process to close
* $generator = function (Process $process) {
* while ($process->isRunning()) {
* yield 2 * 1000; // wait for 2ms
* }
* };
*
* $timeout = 8 * 1000 * 1000; // 8 seconds
*
* try {
* Utils::tryWithTimeout($timeout, $generator($this->process));
* } catch (OperationTimedOut $e) {
* // log
* $this->logger->debug('process: process didn\'t close by itself');
* }
* ```
*
* @param int $timeoutMicroSec
* @param \Generator $generator
* @param callable|null $onTimeout
*
* @throws OperationTimedOut
*
* @return mixed
*/
public static function tryWithTimeout(int $timeoutMicroSec, \Generator $generator, callable $onTimeout = null)
{
$waitUntilMicroSec = \hrtime(true) / 1000 + $timeoutMicroSec;
foreach ($generator as $v) {
// if timeout reached or if time+delay exceed timeout stop the execution
if (\hrtime(true) / 1000 + (int) $v >= $waitUntilMicroSec) {
if (null !== $onTimeout) {
// if callback was set execute it
return $onTimeout();
}
throw OperationTimedOut::createFromTimeout($timeoutMicroSec);
}
\usleep((int) $v);
}
return $generator->getReturn();
}
/**
* Closes all pages for the given connection.
*
* @param Connection $connection
*/
public static function closeAllPage(Connection $connection): void
{
// get targets
$targetsResponse = $connection->sendMessageSync(new Message('Target.getTargets'));
if ($targetsResponse->isSuccessful()) {
foreach ($targetsResponse['result']['targetInfos'] as $target) {
if ('page' === $target['type']) {
$connection->sendMessageSync(
new Message('Target.closeTarget', ['targetId' => $target['targetId']])
);
}
}
}
}
/**
* @throws CommunicationException
* @throws Exception\EvaluationFailed
* @throws JavascriptException
*
* @return mixed
*/
public static function getElementPositionFromPage(Page $page, Selector $selector, int $position = 1)
{
$elementCount = $page
->evaluate(\sprintf('JSON.parse(JSON.stringify(%s));', $selector->expressionCount()))
->getReturnValue();
$position = \max(1, $position);
$position = \min($position, $elementCount);
return $page
->evaluate(\sprintf('JSON.parse(JSON.stringify(%s.getBoundingClientRect()));', $selector->expressionFindOne($position)))
->getReturnValue();
}
}

View File

@ -0,0 +1,26 @@
The MIT License (MIT)
Copyright (c) 2010-2011 Chris Boden <cboden@gmail.com>
Copyright (c) 2010-2011 Nico Kaiser <kaiser@boerse-go.de>
Copyright (c) 2011-2012 Simon Samtleben <web@lemmingzshadow.net>
Copyright (c) 2012-2017 Dominic Scheirlinck <dominic@varspool.com>
Copyright (c) 2017-2020 Zane Chua <zane.jocelyn@gmail.com>
Copyright (c) 2021-2022 Graham Campbell <hello@gjcampbell.co.uk>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,7 @@
install:
composer update
composer bin all update
test:
vendor/bin/phpunit
vendor/bin/phpstan analyze

View File

@ -0,0 +1,48 @@
{
"name": "chrome-php/wrench",
"description": "A simple PHP WebSocket implementation",
"keywords": ["websocket", "websockets", "hybi"],
"license": "MIT",
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"require": {
"php": "^7.3 || ^8.0",
"ext-sockets": "*",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"symfony/polyfill-php80": "^1.26"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^9.5.23"
},
"autoload": {
"psr-4": {
"Wrench\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Wrench\\": "tests/"
}
},
"conflict": {
"wrench/wrench": "*"
},
"config": {
"preferred-install": "dist",
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
},
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Wrench\Application;
use Wrench\Connection;
interface BinaryDataHandlerInterface
{
/**
* Handle binary data received from a client.
*/
public function onBinaryData(string $binaryData, Connection $connection): void;
}

View File

@ -0,0 +1,12 @@
<?php
namespace Wrench\Application;
use Wrench\Connection;
interface ConnectionHandlerInterface
{
public function onConnect(Connection $connection): void;
public function onDisconnect(Connection $connection): void;
}

View File

@ -0,0 +1,13 @@
<?php
namespace Wrench\Application;
use Wrench\Connection;
interface DataHandlerInterface
{
/**
* Handle data received from a client.
*/
public function onData(string $data, Connection $connection): void;
}

View File

@ -0,0 +1,11 @@
<?php
namespace Wrench\Application;
interface UpdateHandlerInterface
{
/**
* Handle an update tick.
*/
public function onUpdate(): void;
}

View File

@ -0,0 +1,60 @@
<?php
namespace Wrench;
use Wrench\Listener\OriginPolicy;
use Wrench\Listener\RateLimiter;
class BasicServer extends Server
{
protected $rateLimiter;
protected $originPolicy;
public function __construct(string $uri, array $options = [])
{
parent::__construct($uri, $options);
$this->configureRateLimiter();
$this->configureOriginPolicy();
}
protected function configureRateLimiter(): void
{
$class = $this->options['rate_limiter_class'];
$this->rateLimiter = new $class($this->options['rate_limiter_options']);
$this->rateLimiter->listen($this);
}
/**
* Configures the origin policy.
*/
protected function configureOriginPolicy(): void
{
$class = $this->options['origin_policy_class'];
$this->originPolicy = new $class($this->options['allowed_origins']);
if ($this->options['check_origin']) {
$this->originPolicy->listen($this);
}
}
/**
* @see Wrench.Server::configure()
*/
protected function configure(array $options): void
{
$options = \array_merge([
'check_origin' => true,
'allowed_origins' => [],
'origin_policy_class' => OriginPolicy::class,
'rate_limiter_class' => RateLimiter::class,
'rate_limiter_options' => [
'connections' => 200, // Total
'connections_per_ip' => 5, // At once
'requests_per_minute' => 200, // Per connection
],
], $options);
parent::configure($options);
}
}

View File

@ -0,0 +1,289 @@
<?php
namespace Wrench;
use InvalidArgumentException;
use Wrench\Exception\FrameException;
use Wrench\Exception\HandshakeException;
use Wrench\Exception\SocketException;
use Wrench\Payload\Payload;
use Wrench\Payload\PayloadHandler;
use Wrench\Protocol\Protocol;
use Wrench\Socket\ClientSocket;
use Wrench\Util\Configurable;
/**
* Client class.
*
* Represents a websocket client
*/
class Client extends Configurable
{
/**
* @var int bytes
*/
public const MAX_HANDSHAKE_RESPONSE = 1500;
/**
* @var string
*/
protected $uri;
/**
* @var string
*/
protected $origin;
/**
* @var ClientSocket|null
*/
protected $socket;
/**
* Request headers.
*
* @var array
*/
protected $headers = [];
/**
* Whether the client is connected.
*
* @var bool
*/
protected $connected = false;
/**
* @var PayloadHandler|null
*/
protected $payloadHandler = null;
/**
* Complete received payloads.
*
* @var array<Payload>
*/
protected $received = [];
/**
* @param string $origin The origin to include in the handshake (required
* in later versions of the protocol)
* @param array $options (optional) Array of options
* - socket => AbstractSocket instance (otherwise created)
* - protocol => Protocol
*/
public function __construct(string $uri, string $origin, array $options = [])
{
parent::__construct($options);
if (!$uri) {
throw new InvalidArgumentException('No URI specified');
}
$this->uri = $uri;
if (!$origin) {
throw new InvalidArgumentException('No origin specified');
}
$this->origin = $origin;
$this->protocol->validateUri($this->uri);
$this->protocol->validateOriginUri($this->origin);
$this->configureSocket();
$this->configurePayloadHandler();
}
/**
* Configures the client socket.
*/
protected function configureSocket(): void
{
$class = $this->options['socket_class'];
$options = $this->options['socket_options'];
$this->socket = new $class($this->uri, $options);
}
/**
* Configures the payload handler.
*/
protected function configurePayloadHandler(): void
{
$this->payloadHandler = new PayloadHandler([$this, 'onData'], $this->options);
}
/**
* Payload receiver
* Public because called from our PayloadHandler. Don't call us, we'll call
* you (via the on_data_callback option).
*
* @param Payload $payload
*/
public function onData(Payload $payload): void
{
$this->received[] = $payload;
if (($callback = $this->options['on_data_callback'])) {
\call_user_func($callback, $payload);
}
}
/**
* Adds a request header to be included in the initial handshake.
*
* For example, to include a Cookie header.
*
* @return void
*/
public function addRequestHeader(string $name, string $value): void
{
$this->headers[$name] = $value;
}
/**
* Sends data to the socket.
*
* @param int $type See Protocol::TYPE_*
*
* @return bool Success
*/
public function sendData(string $data, int $type = Protocol::TYPE_TEXT, bool $masked = true): bool
{
if (!$this->isConnected()) {
return false;
}
$payload = $this->protocol->getPayload();
$payload->encode(
$data,
$type,
$masked
);
return $payload->sendToSocket($this->socket);
}
/**
* Returns whether the client is currently connected
* Also checks the state of the underlying socket.
*
* @return bool
*/
public function isConnected()
{
if (false === $this->connected) {
return false;
}
// Check if the socket is still connected
if (false === $this->socket->isConnected()) {
$this->connected = false;
return false;
}
return true;
}
/**
* Receives data sent by the server.
*
* @return array<Payload> Payload received since the last call to receive()
*/
public function receive(): ?array
{
if (!$this->isConnected()) {
return null;
}
$data = $this->socket->receive();
if (!$data) {
return [];
}
$this->payloadHandler->handle($data);
$received = $this->received;
$this->received = [];
return $received;
}
/**
* Connect to the server.
*
* @throws HandshakeException
* @throws SocketException
*
* @return bool Whether a new connection was made
*/
public function connect(): bool
{
if ($this->isConnected()) {
return false;
}
try {
$this->socket->connect();
} catch (\Exception $ex) {
return false;
}
$key = $this->protocol->generateKey();
$handshake = $this->protocol->getRequestHandshake(
$this->uri,
$key,
$this->origin,
$this->headers
);
$this->socket->send($handshake);
$response = $this->socket->receive(self::MAX_HANDSHAKE_RESPONSE);
return $this->connected =
$this->protocol->validateResponseHandshake($response, $key);
}
/**
* Disconnects the underlying socket, and marks the client as disconnected.
*
* @param int $reason Reason for disconnecting. See Protocol::CLOSE_
*
* @throws SocketException
* @throws FrameException
*/
public function disconnect(int $reason = Protocol::CLOSE_NORMAL): bool
{
if (false === $this->connected) {
return false;
}
$payload = $this->protocol->getClosePayload($reason);
if ($this->socket) {
if (!$payload->sendToSocket($this->socket)) {
throw new FrameException('Unexpected exception when sending Close frame.');
}
// The client SHOULD wait for the server to close the connection
$this->socket->receive();
$this->socket->disconnect();
}
$this->connected = false;
return true;
}
/**
* Configure options.
*/
protected function configure(array $options): void
{
$options = \array_merge([
'socket_class' => ClientSocket::class,
'on_data_callback' => null,
'socket_options' => [],
], $options);
parent::configure($options);
}
}

View File

@ -0,0 +1,489 @@
<?php
namespace Wrench;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use RuntimeException;
use Throwable;
use Wrench\Application\BinaryDataHandlerInterface;
use Wrench\Application\ConnectionHandlerInterface;
use Wrench\Application\DataHandlerInterface;
use Wrench\Application\UpdateHandlerInterface;
use Wrench\Exception\BadRequestException;
use Wrench\Exception\CloseException;
use Wrench\Exception\ConnectionException;
use Wrench\Exception\Exception as WrenchException;
use Wrench\Exception\HandshakeException;
use Wrench\Payload\Payload;
use Wrench\Payload\PayloadHandler;
use Wrench\Protocol\Protocol;
use Wrench\Socket\ServerClientSocket;
use Wrench\Util\Configurable;
/**
* Represents a client connection on the server side.
*
* i.e. the `Server` manages a bunch of `Connection`s
*/
class Connection extends Configurable implements LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* @var ConnectionManager
*/
protected $manager;
/**
* Wraps the client connection resource.
*
* @var ServerClientSocket
*/
protected $socket;
/**
* Whether the connection has successfully handshaken.
*
* @var bool
*/
protected $handshaked = false;
/**
* The application this connection belongs to.
*
* @var DataHandlerInterface|ConnectionHandlerInterface|UpdateHandlerInterface|null
*/
protected $application = null;
/**
* The IP address of the client.
*
* @var string
*/
protected $ip;
/**
* The port of the client.
*
* @var int
*/
protected $port;
/**
* The array of headers included with the original request (like Cookie for example)
* The headers specific to the web sockets handshaking have been stripped out.
*
* @var array
*/
protected $headers = null;
/**
* The array of query parameters included in the original request
* The array is in the format 'key' => 'value'.
*
* @var array
*/
protected $queryParams = null;
/**
* Connection ID.
*
* @var string|null
*/
protected $id = null;
/**
* @var PayloadHandler
*/
protected $payloadHandler;
public function __construct(
ConnectionManager $manager,
ServerClientSocket $socket,
array $options = []
) {
$this->manager = $manager;
$this->socket = $socket;
$this->logger = new NullLogger();
parent::__construct($options);
$this->configureClientInformation();
$this->configurePayloadHandler();
}
/**
* @throws RuntimeException
*/
protected function configureClientInformation(): void
{
$this->ip = $this->socket->getIp();
$this->port = $this->socket->getPort();
$this->generateClientId();
}
/**
* Configures the client ID.
*
* We hash the client ID to prevent leakage of information if another client
* happens to get a hold of an ID. The secret *must* be lengthy, and must
* be kept secret for this to work: otherwise it's trivial to search the space
* of possible IP addresses/ports (well, if not trivial, at least very fast).
*/
protected function generateClientId(): void
{
$this->id = \bin2hex(\random_bytes(32));
}
protected function configurePayloadHandler(): void
{
$this->payloadHandler = new PayloadHandler(
[$this, 'handlePayload'],
$this->options
);
}
/**
* Gets the connection manager of this connection.
*
* @return \Wrench\ConnectionManager
*/
public function getConnectionManager(): ConnectionManager
{
return $this->manager;
}
/**
* Handle a complete payload received from the client.
*
* Public because called from our PayloadHandler
*
* @param Payload $payload
*
* @throws ConnectionException
*/
public function handlePayload(Payload $payload): void
{
$app = $this->getClientApplication();
$this->logger->debug('Handling payload: '.$payload->getPayload());
switch ($type = $payload->getType()) {
case Protocol::TYPE_TEXT:
if ($app instanceof DataHandlerInterface) {
$app->onData((string) $payload, $this);
}
return;
case Protocol::TYPE_BINARY:
if ($app instanceof BinaryDataHandlerInterface) {
$app->onBinaryData((string) $payload, $this);
} else {
$this->close(1003);
}
break;
case Protocol::TYPE_PING:
$this->logger->notice('Ping received');
$this->send($payload->getPayload(), Protocol::TYPE_PONG);
$this->logger->debug('Pong!');
break;
/**
* A Pong frame MAY be sent unsolicited. This serves as a
* unidirectional heartbeat. A response to an unsolicited Pong
* frame is not expected.
*/
case Protocol::TYPE_PONG:
$this->logger->info('Received unsolicited pong');
break;
case Protocol::TYPE_CLOSE:
$this->logger->notice('Close frame received');
$this->close();
$this->logger->debug('Disconnected');
break;
default:
throw new ConnectionException('Unhandled payload type');
}
}
/**
* Gets the client application.
*
* @return BinaryDataHandlerInterface|ConnectionHandlerInterface|DataHandlerInterface|UpdateHandlerInterface|false
*/
public function getClientApplication()
{
return $this->application ?? false;
}
/**
* Closes the connection according to the WebSocket protocol.
*
* If an endpoint receives a Close frame and that endpoint did not
* previously send a Close frame, the endpoint MUST send a Close frame
* in response. It SHOULD do so as soon as is practical. An endpoint
* MAY delay sending a close frame until its current message is sent
* (for instance, if the majority of a fragmented message is already
* sent, an endpoint MAY send the remaining fragments before sending a
* Close frame). However, there is no guarantee that the endpoint which
* has already sent a Close frame will continue to process data.
* After both sending and receiving a close message, an endpoint
* considers the WebSocket connection closed, and MUST close the
* underlying TCP connection. The server MUST close the underlying TCP
* connection immediately; the client SHOULD wait for the server to
* close the connection but MAY close the connection at any time after
* sending and receiving a close message, e.g. if it has not received a
* TCP close from the server in a reasonable time period.
*
* @param int $code
* @param string $reason The human readable reason the connection was closed
*
* @return bool
*/
public function close(int $code = Protocol::CLOSE_NORMAL, string $reason = null): bool
{
try {
if (!$this->handshaked) {
$response = $this->protocol->getResponseError($code);
$this->socket->send($response);
} else {
$response = $this->protocol->getClosePayload($code, false);
$response->sendToSocket($this->socket);
}
} catch (Throwable $e) {
$this->logger->warning('Unable to send close message');
}
if ($this->application instanceof ConnectionHandlerInterface) {
$this->application->onDisconnect($this);
}
$this->socket->disconnect();
$this->manager->removeConnection($this);
return true;
}
/**
* Sends the payload to the connection.
*
* @param mixed $data
* @param int $type
*
* @return bool
*/
public function send($data, int $type = Protocol::TYPE_TEXT): bool
{
if (!$this->handshaked) {
throw new HandshakeException('Connection is not handshaked');
}
$payload = $this->protocol->getPayload();
if (!\is_scalar($data) && !$data instanceof Payload) {
$data = \json_encode($data);
}
// Servers don't send masked payloads
$payload->encode($data, $type, false);
if (!$payload->sendToSocket($this->socket)) {
$this->logger->warning('Could not send payload to client');
throw new ConnectionException('Could not send data to connection: '.$this->socket->getLastError());
}
return true;
}
/**
* Processes data on the socket.
*
* @throws CloseException
*/
public function process(): void
{
$data = $this->socket->receive();
if ('' === $data) {
throw new CloseException('Error reading data from socket: '.$this->socket->getLastError());
}
$this->onData($data);
}
/**
* Data receiver.
*
* Called by the connection manager when the connection has received data
*
* @param string $data
*/
public function onData($data): void
{
if ($this->handshaked) {
$this->handle($data);
} else {
$this->handshake($data);
}
}
/**
* Performs a websocket handshake.
*
* @throws BadRequestException
* @throws HandshakeException
* @throws WrenchException
*/
public function handshake(string $data): void
{
try {
[$path, $origin, $key, $extensions, $protocol, $headers, $params]
= $this->protocol->validateRequestHandshake($data);
$this->headers = $headers;
$this->queryParams = $params;
$this->application = $this->manager->getApplicationForPath($path);
if (!$this->application) {
throw new BadRequestException('Invalid application');
}
$this->manager->getServer()->notify(
Server::EVENT_HANDSHAKE_REQUEST,
[$this, $path, $origin, $key, $extensions]
);
$response = $this->protocol->getResponseHandshake($key);
if (!$this->socket->isConnected()) {
throw new HandshakeException('Socket is not connected');
}
if (null === $this->socket->send($response)) {
throw new HandshakeException('Could not send handshake response');
}
$this->handshaked = true;
$this->logger->info(\sprintf(
'Handshake successful: %s:%d (%s) connected to %s',
$this->getIp(),
$this->getPort(),
$this->getId(),
$path
));
$this->manager->getServer()->notify(
Server::EVENT_HANDSHAKE_SUCCESSFUL,
[$this]
);
if ($this->application instanceof ConnectionHandlerInterface) {
$this->application->onConnect($this);
}
} catch (WrenchException $e) {
$this->logger->error('Handshake failed: {exception}', [
'exception' => $e,
]);
$this->close(Protocol::CLOSE_PROTOCOL_ERROR, (string) $e);
throw $e;
}
}
/**
* Gets the IP address of the connection.
*
* @return string Usually dotted quad notation
*/
public function getIp(): string
{
return $this->ip;
}
/**
* Gets the port of the connection.
*
* @return int
*/
public function getPort(): int
{
return $this->port;
}
/**
* Gets the connection ID.
*
* @return string
*/
public function getId(): string
{
return $this->id;
}
/**
* Handle data received from the client.
*
* The data passed in may belong to several different frames across one or
* more protocols. It may not even contain a single complete frame. This method
* manages slotting the data into separate payload objects.
*
* @todo An endpoint MUST be capable of handling control frames in the
* middle of a fragmented message.
*
* @param string $data
*
* @return void
*/
public function handle($data): void
{
$this->payloadHandler->handle($data);
}
/**
* Gets the non-web-sockets headers included with the original request.
*
* @return array
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* Gets the query parameters included with the original request.
*
* @return array
*/
public function getQueryParams(): array
{
return $this->queryParams;
}
/**
* Gets the socket object.
*
* @return Socket\ServerClientSocket
*/
public function getSocket(): ServerClientSocket
{
return $this->socket;
}
/**
* @see \Wrench\Util.Configurable::configure()
*/
protected function configure(array $options): void
{
$options = \array_merge([
'connection_id_secret' => 'asu5gj656h64Da(0crt8pud%^WAYWW$u76dwb',
'connection_id_algo' => 'sha512',
], $options);
parent::configure($options);
}
}

View File

@ -0,0 +1,323 @@
<?php
namespace Wrench;
use Countable;
use Exception;
use InvalidArgumentException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Wrench\Application\BinaryDataHandlerInterface;
use Wrench\Application\ConnectionHandlerInterface;
use Wrench\Application\DataHandlerInterface;
use Wrench\Application\UpdateHandlerInterface;
use Wrench\Exception\CloseException;
use Wrench\Exception\ConnectionException;
use Wrench\Exception\Exception as WrenchException;
use Wrench\Protocol\Protocol;
use Wrench\Socket\ServerClientSocket;
use Wrench\Socket\ServerSocket;
use Wrench\Util\Configurable;
class ConnectionManager extends Configurable implements Countable, LoggerAwareInterface
{
use LoggerAwareTrait;
public const TIMEOUT_SELECT = 0;
public const TIMEOUT_SELECT_MICROSEC = 200000;
/**
* @var Server
*/
protected $server;
/**
* Master socket.
*
* @var ServerSocket
*/
protected $socket;
/**
* An array of client connections.
*
* @var array<int, Connection>
*/
protected $connections = [];
/**
* An array of raw socket resources, corresponding to connections, roughly.
*
* @var array<int, resource>
*/
protected $resources = [];
public function __construct(Server $server, array $options = [])
{
$this->logger = new NullLogger();
$this->server = $server;
parent::__construct($options);
}
/**
* @see Countable::count()
*/
public function count(): int
{
return \count($this->connections);
}
/**
* Gets the application associated with the given path.
*
* @return BinaryDataHandlerInterface|ConnectionHandlerInterface|DataHandlerInterface|UpdateHandlerInterface|null
*/
public function getApplicationForPath(string $path): ?object
{
$path = \ltrim($path, '/');
return $this->server->getApplication($path);
}
/**
* Listens on the main socket.
*
* @throws ConnectionException
*/
public function listen(): void
{
$this->socket->listen();
$this->resources[$this->socket->getResourceId()] = $this->socket->getResource();
}
/**
* Select and process an array of resources.
*/
public function selectAndProcess(): void
{
$read = $this->resources;
$unused_write = null;
$unused_exception = null;
\stream_select(
$read,
$unused_write,
$unused_exception,
$this->options['timeout_select'],
$this->options['timeout_select_microsec']
);
foreach ($read as $socket) {
if ($socket == $this->socket->getResource()) {
$this->processMasterSocket();
} else {
$this->processClientSocket($socket);
}
}
}
/**
* Process events on the master socket ($this->socket).
*/
protected function processMasterSocket(): void
{
$new = null;
try {
$new = $this->socket->accept();
} catch (Exception $e) {
$this->logger->error('Socket error: {exception}', [
'exception' => $e,
]);
return;
}
$connection = $this->createConnection($new);
$this->server->notify(Server::EVENT_SOCKET_CONNECT, [$new, $connection]);
}
/**
* Creates a connection from a socket resource
* The create connection object is based on the options passed into the
* constructor ('connection_class', 'connection_options'). This connection
* instance and its associated socket resource are then stored in the
* manager.
*
* @param resource $resource A socket resource
*/
protected function createConnection($resource): Connection
{
$socket_class = $this->options['socket_client_class'];
$socket_options = $this->options['socket_client_options'];
$connection_class = $this->options['connection_class'];
$connection_options = $this->options['connection_options'];
$socket = new $socket_class($resource, $socket_options);
$connection = new $connection_class($this, $socket, $connection_options);
if ($connection instanceof LoggerAwareInterface) {
$connection->setLogger($this->logger);
}
$id = $this->resourceId($resource);
$this->resources[$id] = $resource;
$this->connections[$id] = $connection;
return $connection;
}
/**
* This server makes an explicit assumption: PHP resource types may be cast
* to a integer. Furthermore, we assume this is bijective. Both seem to be
* true in most circumstances, but may not be guaranteed.
* This method (and $this->getResourceId()) exist to make this assumption
* explicit.
*
* This is needed on the connection manager as well as on resources.
*
* @param resource $resource
*/
protected function resourceId($resource): int
{
if (\is_resource($resource)) {
return \get_resource_id($resource);
}
return \spl_object_id($resource);
}
/**
* Process events on a client socket.
*
* @param resource $socket
*/
protected function processClientSocket($socket): void
{
$connection = $this->getConnectionForClientSocket($socket);
if (!$connection) {
$this->logger->warning('No connection for client socket');
return;
}
try {
$this->server->notify(Server::EVENT_CLIENT_DATA, [$socket, $connection]);
$connection->process();
} catch (CloseException $e) {
$this->logger->notice('Client connection closed: {exception}', [
'exception' => $e,
]);
$connection->close(Protocol::CLOSE_UNEXPECTED, $e->getMessage());
} catch (WrenchException $e) {
$this->logger->warning('Error on client socket: {exception}', [
'exception' => $e,
]);
$connection->close(Protocol::CLOSE_UNEXPECTED);
} catch (InvalidArgumentException $e) {
$this->logger->warning('Wrong input arguments: {exception}', [
'exception' => $e,
]);
$connection->close(Protocol::CLOSE_UNEXPECTED);
}
}
/**
* Returns the Connection associated with the specified socket resource.
*
* @param resource $socket
*/
protected function getConnectionForClientSocket($socket): ?Connection
{
return $this->connections[$this->resourceId($socket)] ?? null;
}
/**
* Gets the connection manager's listening URI.
*/
public function getUri(): string
{
return $this->server->getUri();
}
/**
* @return Server
*/
public function getServer(): Server
{
return $this->server;
}
/**
* Removes a connection.
*/
public function removeConnection(Connection $connection): void
{
$socket = $connection->getSocket();
$index = $socket->getResourceId() ?? \array_search($connection, $this->connections);
if (false === $index) {
$this->logger->warning('Could not remove connection: not found');
}
unset($this->connections[$index]);
unset($this->resources[$index]);
$this->server->notify(
Server::EVENT_SOCKET_DISCONNECT,
[$connection->getSocket(), $connection]
);
}
/**
* @param array $options
* Options include:
* - timeout_select => int, seconds, default 0
* - timeout_select_microsec => int, microseconds (NB: not milli), default: 200000
*/
protected function configure(array $options): void
{
$options = \array_merge([
'socket_master_class' => ServerSocket::class,
'socket_master_options' => [],
'socket_client_class' => ServerClientSocket::class,
'socket_client_options' => [],
'connection_class' => Connection::class,
'connection_options' => [],
'timeout_select' => self::TIMEOUT_SELECT,
'timeout_select_microsec' => self::TIMEOUT_SELECT_MICROSEC,
], $options);
parent::configure($options);
$this->configureMasterSocket();
}
/**
* Configures the main server socket.
*/
protected function configureMasterSocket(): void
{
$class = $this->options['socket_master_class'];
$options = $this->options['socket_master_options'];
$this->socket = new $class($this->server->getUri(), $options);
}
/**
* Gets all resources.
*
* @return array<int, resource>
*/
protected function getAllResources(): array
{
return \array_merge($this->resources, [
$this->socket->getResourceId() => $this->socket->getResource(),
]);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Wrench\Exception;
use Throwable;
use Wrench\Protocol\Protocol;
class BadRequestException extends HandshakeException
{
public function __construct(string $message = '', int $code = null, Throwable $previous = null)
{
parent::__construct($message, $code ?? Protocol::HTTP_BAD_REQUEST, $previous);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Wrench\Exception;
use Throwable;
use Wrench\Exception\Exception as WrenchException;
use Wrench\Protocol\Protocol;
/**
* Close connection exception.
*/
class CloseException extends WrenchException
{
public function __construct(string $message = '', int $code = null, Throwable $previous = null)
{
parent::__construct($message, $code ?? Protocol::CLOSE_UNEXPECTED, $previous);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Wrench\Exception;
class ConnectionException extends Exception
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace Wrench\Exception;
class Exception extends \Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace Wrench\Exception;
use Wrench\Exception\Exception as WrenchException;
class FrameException extends WrenchException
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace Wrench\Exception;
use Throwable;
use Wrench\Exception\Exception as WrenchException;
use Wrench\Protocol\Protocol;
class HandshakeException extends WrenchException
{
public function __construct(string $message = '', int $code = null, Throwable $previous = null)
{
parent::__construct($message, $code ?? Protocol::HTTP_SERVER_ERROR, $previous);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Wrench\Exception;
use Throwable;
use Wrench\Protocol\Protocol;
/**
* Invalid origin exception.
*/
class InvalidOriginException extends HandshakeException
{
public function __construct(string $message = '', int $code = null, Throwable $previous = null)
{
parent::__construct($message, $code ?? Protocol::HTTP_FORBIDDEN, $previous);
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Wrench\Exception;
use Wrench\Exception\Exception as WrenchException;
class PayloadException extends WrenchException
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace Wrench\Exception;
use Wrench\Exception\Exception as WrenchException;
class SocketException extends WrenchException
{
}

View File

@ -0,0 +1,172 @@
<?php
namespace Wrench\Frame;
use Wrench\Exception\FrameException;
use Wrench\Protocol\Protocol;
/**
* Represents a WebSocket frame.
*/
abstract class Frame
{
/**
* The frame data length.
*
* @var int|null
*/
protected $length = null;
/**
* The type of this payload.
*
* @var int|null
*/
protected $type = null;
/**
* The buffer.
*
* May not be a complete payload, because this frame may still be receiving
* data. See.
*
* @var string
*/
protected $buffer = '';
/**
* The enclosed frame payload.
*
* May not be a complete payload, because this frame might indicate a continuation
* frame. See isFinal() versus isComplete().
*
* @var string
*/
protected $payload = '';
/**
* Gets the length of the payload.
*
* @throws FrameException
*/
abstract public function getLength(): int;
/**
* Resets the frame and encodes the given data into it.
*
* @return static
*/
abstract public function encode(string $payload, int $type = Protocol::TYPE_TEXT, bool $masked = false): self;
/**
* Whether the frame is the final one in a continuation.
*/
abstract public function isFinal(): bool;
abstract public function getType(): int;
/**
* Receieves data into the frame.
*/
public function receiveData(string $data): void
{
$this->buffer .= $data;
}
/**
* Whether this frame is waiting for more data.
*/
public function isWaitingForData(): bool
{
return $this->getRemainingData() > 0;
}
/**
* Gets the remaining number of bytes before this frame will be complete.
*/
public function getRemainingData(): ?int
{
try {
return $this->getExpectedBufferLength() - $this->getBufferLength();
} catch (FrameException $e) {
return null;
}
}
/**
* Gets the expected length of the buffer once all the data has been
* receieved.
*
* @return int
*/
abstract protected function getExpectedBufferLength(): int;
/**
* Gets the expected length of the frame payload.
*
* @return int
*/
protected function getBufferLength(): int
{
return \strlen($this->buffer);
}
/**
* Gets the contents of the frame payload.
*
* The frame must be complete to call this method.
*
* @throws FrameException
*/
public function getFramePayload(): string
{
if (!$this->isComplete()) {
throw new FrameException('Cannot get payload: frame is not complete');
}
if (!$this->payload && $this->buffer) {
$this->decodeFramePayloadFromBuffer();
}
return $this->payload;
}
/**
* Whether the frame is complete.
*/
public function isComplete(): bool
{
if (!$this->buffer) {
return false;
}
try {
return $this->getBufferLength() >= $this->getExpectedBufferLength();
} catch (FrameException $e) {
return false;
}
}
/**
* Decodes a frame payload from the buffer.
*
* @return void
*/
abstract protected function decodeFramePayloadFromBuffer(): void;
/**
* Gets the binary contents of the frame buffer.
*
* This is the encoded value, receieved into the frame with receiveData().
*
* @throws FrameException
*/
public function getFrameBuffer(): string
{
if (!$this->buffer && $this->payload) {
throw new FrameException('Cannot get frame buffer');
}
return $this->buffer;
}
}

Some files were not shown because too many files have changed in this diff Show More