Initial commit
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
vendor/
|
||||||
|
composer.lock
|
||||||
|
.DS_Store
|
||||||
|
/node_modules
|
||||||
|
/vendor
|
||||||
|
.idea
|
||||||
|
/logs/
|
||||||
|
.idea/
|
||||||
|
/.idea
|
||||||
|
/.idea/*
|
||||||
|
/.vscode
|
||||||
|
/nbproject
|
||||||
|
/.vagrant
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
.phpunit.result.cache
|
||||||
|
Example2\.php
|
||||||
|
Example3\.php
|
||||||
|
Example4\.php
|
||||||
|
request\.xml
|
||||||
|
response\.xml
|
||||||
|
*.log
|
||||||
|
\.php_cs\.cache
|
||||||
|
.php-cs-fixer.cache
|
84
Example.php
Normal file
84
Example.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp;
|
||||||
|
|
||||||
|
class Example
|
||||||
|
{
|
||||||
|
protected static string $host = 'host.example.com';
|
||||||
|
protected static int $port = 5222;
|
||||||
|
protected static string $username = 'foo';
|
||||||
|
protected static string $password = 'bar';
|
||||||
|
|
||||||
|
public static function test()
|
||||||
|
{
|
||||||
|
$options = new Options();
|
||||||
|
|
||||||
|
$options
|
||||||
|
->setHost(self::$host)
|
||||||
|
->setPort(self::$port)
|
||||||
|
->setUsername(self::$username)
|
||||||
|
->setPassword(self::$password)
|
||||||
|
;
|
||||||
|
|
||||||
|
$client = new XmppClient($options);
|
||||||
|
$client->connect();
|
||||||
|
|
||||||
|
$client->iq->getRoster();
|
||||||
|
|
||||||
|
$client->message->send('Hello world', 'test@jabber.com');
|
||||||
|
|
||||||
|
// Uncomment if you want to manually enter raw XML (or call a function) and see a server response
|
||||||
|
// (new self)->sendRawXML($client);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$response = $client->getResponse();
|
||||||
|
$client->prettyPrint($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
$client->disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendRawXML(XmppClient $client)
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
$response = $client->getResponse();
|
||||||
|
$client->prettyPrint($response);
|
||||||
|
|
||||||
|
// if you provide a function name here, (i.e. getRoster ...arg)
|
||||||
|
// the function will be called instead of sending raw XML
|
||||||
|
$line = readline("\nEnter XML: ");
|
||||||
|
|
||||||
|
if ($line == 'exit') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsedLine = explode(' ', $line);
|
||||||
|
|
||||||
|
if (method_exists($client, $parsedLine[0])) {
|
||||||
|
if (count($parsedLine) < 2) {
|
||||||
|
$client->{$parsedLine[0]}();
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$client->{$parsedLine[0]}($parsedLine[1]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@simplexml_load_string($line)) {
|
||||||
|
$client->send($line);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo 'This is not a method nor a valid XML';
|
||||||
|
} while ($line != 'exit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
Example::test();
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Dupor Marko
|
||||||
|
|
||||||
|
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.
|
277
README.md
Normal file
277
README.md
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
# PHP client library for XMPP (Jabber) protocol
|
||||||
|
|
||||||
|
> This library is based on [xmppo/xmpp-php](https://github.com/xmppo/xmpp-php)
|
||||||
|
|
||||||
|
This is low level socket implementation for enabling PHP to
|
||||||
|
communicate with XMPP due to lack of such libraries online (at least ones I
|
||||||
|
could find that had decent documentation).
|
||||||
|
|
||||||
|
XMPP core documentation can be found [here](https://xmpp.org/rfcs/rfc6120.html).
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
You can see usage example in `Example.php` file by changing credentials to
|
||||||
|
point to your XMPP server and from project root run `php Example.php`.
|
||||||
|
|
||||||
|
# Library usage
|
||||||
|
|
||||||
|
## Initialization
|
||||||
|
|
||||||
|
In order to start using the library you first need to instantiate a new `Options`
|
||||||
|
class. Host, username and password are mandatory fields, while port number, if omitted,
|
||||||
|
will default to `5222` which is XMPP default.
|
||||||
|
|
||||||
|
Username can be either bare `JID` or in `JID/resource` form. If you are using a bare `JID`
|
||||||
|
the resource will be added automatically. You can override this by explicitly setting a
|
||||||
|
resource with `$client->iq->setResource()`. In the second case the username will be automatically
|
||||||
|
parsed to `username` and `resource` variables. In case of `JID/resource/xyz` format, everything
|
||||||
|
after second slash will be ignored. If both `JID/resource` is present as well as using the
|
||||||
|
`$client->iq->setResource()` method, which ever was defined last will take precedence.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$options = new Options();
|
||||||
|
|
||||||
|
$options
|
||||||
|
->setHost($host) // required
|
||||||
|
->setPort($port) // not required, defaults to 5222
|
||||||
|
->setUsername($username) // required
|
||||||
|
->setPassword($password); // required
|
||||||
|
```
|
||||||
|
|
||||||
|
`Options` object is required for establishing the connection and every other subsequent
|
||||||
|
request, so once set it should not be changed.
|
||||||
|
|
||||||
|
Once this is set you can instantiate a new `XmppClient` object and pass the `Options` object in.
|
||||||
|
|
||||||
|
## XMPP client class explanation
|
||||||
|
|
||||||
|
Since XMPP is all about 3 major stanzas, (**IQ, Message and Presence**), I've
|
||||||
|
created separate classes which are dependant on socket implementation so that you
|
||||||
|
can directly send XML by calling a stanza method.
|
||||||
|
|
||||||
|
This means that 3 stanzas have been made available from `XmppClient` class constructor
|
||||||
|
to be used like a chained method on client's concrete class.
|
||||||
|
|
||||||
|
Current logic thus is `$client->STANZA->METHOD()`. For example:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$client->iq->getRoster();
|
||||||
|
$client->message->send();
|
||||||
|
$client->presence->subscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connecting to the server
|
||||||
|
Beside being a stanza wrapper, `XmppClient` class offers a few public methods.
|
||||||
|
|
||||||
|
`$client->connect()` method does a few things:
|
||||||
|
1. Connects to the socket which was initialized in `XmppClient` constructor
|
||||||
|
2. Opens an XML stream to exchange with the XMPP server
|
||||||
|
3. Tries to authenticate with the server based on provided credentials
|
||||||
|
4. Starts the initial communication with the server, bare minimum to get you started
|
||||||
|
|
||||||
|
Current version supports `PLAIN` and `DIGEST-MD5` auth methods.
|
||||||
|
|
||||||
|
TLS is supported by default. If server has support for TLS, library will
|
||||||
|
automatically try to connect with TLS and make the connection secure.
|
||||||
|
|
||||||
|
If you'd like to explicitly disable this functionality, you can use `setUseTls(false)`
|
||||||
|
function on the `Options` instance so that TLS communication is disabled. Note
|
||||||
|
that this will function in environments where TLS is supported but not required.
|
||||||
|
If TLS is required, program will connect to it independently of the option you
|
||||||
|
set.
|
||||||
|
|
||||||
|
## Sending raw data
|
||||||
|
|
||||||
|
`send()` message is exposed as being public in `XmppClient` class, and its intention
|
||||||
|
is to send raw XML data to the server. For it to work correctly, XML which you send
|
||||||
|
has to be valid XML.
|
||||||
|
|
||||||
|
## Getting raw response
|
||||||
|
|
||||||
|
Server responses (or server side continuous XML session to be exact) can be retrieved with
|
||||||
|
`$client->getResponse()`. This should be used in an infinite loop or for more sustainable
|
||||||
|
solution in some WebSocket solution like [Ratchet](http://socketo.me/) if you'd like to
|
||||||
|
see continuous stream of everything coming from the server.
|
||||||
|
|
||||||
|
If you would like to see the output of the received response in the console you can call the
|
||||||
|
`$client->prettyPrint($response)` method.
|
||||||
|
|
||||||
|
## Receiving messages and other responses
|
||||||
|
|
||||||
|
In case you are not interested in complete response which comes from server, you may also use
|
||||||
|
`$client->message->receive()` (`$client->getMessages()` was removed because it was just a shorthand
|
||||||
|
method for this one) which will match message tags with regex and return array of matched messages.
|
||||||
|
In case you'd like to see the response in the terminal, you can do something like this:
|
||||||
|
|
||||||
|
```php
|
||||||
|
do {
|
||||||
|
$response = $client->message->receive();
|
||||||
|
if($response)
|
||||||
|
echo print_r($response);
|
||||||
|
} while (true);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Disconnect
|
||||||
|
Disconnect method sends closing XML to the server to end the currently open session and
|
||||||
|
closes the open socket.
|
||||||
|
|
||||||
|
# Stanza method breakdown
|
||||||
|
|
||||||
|
Remember from [here](#xmpp-client-class-explanation) -> `$client->STANZA->METHOD()`
|
||||||
|
|
||||||
|
## Message
|
||||||
|
|
||||||
|
`send()` - sending a message to someone. Takes 3 parameters of which the last one is
|
||||||
|
optional. First parameter is the actual message you'd like to send (body), second
|
||||||
|
one is recipient of the message and third one is type of message to be sent. This
|
||||||
|
defaults to `chat`.
|
||||||
|
|
||||||
|
You can find possible types in [this RFC document](https://xmpp.org/rfcs/rfc3921.html#stanzas)
|
||||||
|
|
||||||
|
`receive()` - covered in [this section](#receiving-messages-and-other-responses)
|
||||||
|
|
||||||
|
## IQ
|
||||||
|
|
||||||
|
`getRoster()` - takes no arguments and fetches current authenticated user roster.
|
||||||
|
|
||||||
|
`setGroup()` - puts a given user in group you provide. Method takes two arguments:
|
||||||
|
first one being the group name which you will attach to given user, and other
|
||||||
|
being JID of that user.
|
||||||
|
|
||||||
|
## Presence
|
||||||
|
|
||||||
|
`setPriority()` - sets priority for given resource. First argument is an integer
|
||||||
|
`-128 <> 127`. If no second argument is given, priority will be set for currently used resource.
|
||||||
|
Other resource can be provided as a second argument whereas the priority will be set for that
|
||||||
|
specific resource.
|
||||||
|
|
||||||
|
`subscribe()` - takes JID as an argument and asks that user for presence.
|
||||||
|
|
||||||
|
`acceptSubscription()` - takes JID as an argument and accepts presence from that user.
|
||||||
|
|
||||||
|
`declineSubscription()` - takes JID as an argument and declines presence from that user.
|
||||||
|
|
||||||
|
## Sessions
|
||||||
|
|
||||||
|
Sessions are currently being used only to differentiate logs if multiple connections
|
||||||
|
are being made.
|
||||||
|
|
||||||
|
`XmppClient` class takes in second optional parameter `$sessionId` to which you can
|
||||||
|
forward session ID from your system, or it will be assigned automatically.
|
||||||
|
|
||||||
|
You can disable sessions through `Options` object (`$options->setSessionManager(false)`),
|
||||||
|
as they can cause collision with already established sessions if being used inside
|
||||||
|
frameworks or similar. Needless to say if this is disabled, forwarding a second parameter
|
||||||
|
to `XmppClient` will not establish a new session.
|
||||||
|
|
||||||
|
# More options (not required)
|
||||||
|
|
||||||
|
`Options` object can take more options which may be chained but are not required. These are explained
|
||||||
|
and commented in the code directly in the `Options` class:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$options
|
||||||
|
->setProtocol($protocol) // defaults to TCP
|
||||||
|
->setResource($resource) // defaults to 'Pdahal_machine_' string + timestamp
|
||||||
|
->setLogger($logger) // logger instance (logging explained below)
|
||||||
|
->setAuthType($authType) // Takes on classes which implement Authenticable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Socket options
|
||||||
|
|
||||||
|
Most of the socket options are set by default so there is no need to tamper
|
||||||
|
with this class, however you can additionally change the timeout for the period
|
||||||
|
the socket will be alive when doing a `socket_read()`, and you can do that with
|
||||||
|
`$socket->setTimeout()`.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Upon new established session the library is creating a `xmpp.log` log file in `logs/` folder:
|
||||||
|
|
||||||
|
You can manually set logger when instantiating `Options` with `setLogger($logger)`. The method accepts
|
||||||
|
any object which implements `Loggable` interface so you can create your own implementation.
|
||||||
|
|
||||||
|
Fun fact: this used to be a `PSR-3` logger interface, but I decided it was an overkill for this
|
||||||
|
stage of development.
|
||||||
|
|
||||||
|
# Other
|
||||||
|
|
||||||
|
`Example.php` has a `sendRawXML()` method which can be helpful with debugging. Method works in a way
|
||||||
|
that you can provide hand-written XML and send it to the server. On the other hand you can
|
||||||
|
also trigger a method by providing method name instead of XML.
|
||||||
|
|
||||||
|
```
|
||||||
|
Enter XML: <xml>foo</xml> <-- will send XML
|
||||||
|
Enter XML: getRoster <-- will run getRoster() method
|
||||||
|
Enter XML: requestPresence x@x.com <-- will run with argument requestPresence(x@x.com)
|
||||||
|
```
|
||||||
|
|
||||||
|
Some valid XMPP XML will be declined (like sending `<presence/>`) because `simplexml_load_string()`
|
||||||
|
is not able to parse it as being a valid XML. In cases you need to do some custom stuff like
|
||||||
|
that and you are sure it is a XMPP valid XML, you can remove the parsing line and just let the
|
||||||
|
`send()` method do its magic.
|
||||||
|
|
||||||
|
**Be aware! Be very aware!** sending an invalid XML to the server
|
||||||
|
will probably invalidate currently open XML session and you will probably need to restart the
|
||||||
|
script. This is highly experimental and not maintained really. It is a poor orphan method whose
|
||||||
|
parents have abandoned it and left in a hurry. It may prove to be Harry Potter one day, but hey...
|
||||||
|
we all clearly doubt it. You may be a special snowflake but no one likes you. Onward to
|
||||||
|
orphanagemobil! Begone method (just kidding, I won't delete it)!
|
||||||
|
|
||||||
|
Who says readme's are boring.
|
||||||
|
|
||||||
|
# Dev documentation
|
||||||
|
|
||||||
|
For anyone willing to contribute, a quick breakdown of the structure:
|
||||||
|
|
||||||
|
---
|
||||||
|
- `Options.php` - everything that is variable about the library
|
||||||
|
- `Socket.php` - socket related implementation (connecting, reading, writing etc.)
|
||||||
|
- `XmppClient.php` - user friendly methods to interact with the library and stanza wrapper enabling users to call stanza methods through
|
||||||
|
instantiated class. This should contain as little logic as possible, turns out it's not so easy :)
|
||||||
|
---
|
||||||
|
|
||||||
|
- `AuthTypes` - contains methods to authenticate yourself to XMPP server. Besides concrete implementations there is also an
|
||||||
|
abstract class with minor logic to avoid duplication and interface having all the necessary methods should the need for new
|
||||||
|
auth type arise.
|
||||||
|
|
||||||
|
- `Buffers` - implementation of buffer (or should I say a simple array) which gets filled when socket is calling the `receive()`
|
||||||
|
method, and it flushes on any read, which happens when calling `getResponse()` method for example. A brief history of why: I had
|
||||||
|
issues when a non-recoverable error would be thrown. In this situation I had to do 2 things: try to reconnect, show the error
|
||||||
|
to the user. The thing is that `getResponse()` returns string, and in case of reconnection the program execution would continue
|
||||||
|
returning either nothing or returning error string after the server already connected for the second time, thus misinforming the
|
||||||
|
user of the error which occurred before reconnection. Buffer was born.
|
||||||
|
|
||||||
|
- `Exceptions` - this is more or less a standard. I am just overriding constructors so I can get my message in.
|
||||||
|
|
||||||
|
- `Loggers` - containing logic to store logs to the `logs/xmpp.log` file. The idea was to keep several log types inside (full, simple,
|
||||||
|
no logger), but I found the one made to be sufficient.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`Xml`
|
||||||
|
|
||||||
|
- `Xml.php` - a trait consisting of way too many regex matching. This should be reformatted.
|
||||||
|
- `Stanzas` - main logic for all stanza communication with the server. This used to be just plain XML, but I have decided to
|
||||||
|
forward a socket dependency inside so that when you call the method, you actually also send it to the server. That used to be
|
||||||
|
something like `$this->socket->send($this->iq->getRoster())` which is correct from the programming perspective, but for the
|
||||||
|
simplicity sake, I like the `$client->iq->getRoster()` more. I'm open to other suggestions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODO's
|
||||||
|
|
||||||
|
- **unit testing** - unfortunately I have been prolonging this for far too long, maybe there is a good soul out there who enjoys writing tests.
|
||||||
|
- **throttling** - when an unrecoverable error occurs (currently I am catching `<stream:error>` ones which break the stream)
|
||||||
|
reconnect is being made automatically. In case this is happening over and over again, program will try connecting indefinitely,
|
||||||
|
which is fine to any aspect except logs which will get clogged. I would like to throttle the connection so that it increases the
|
||||||
|
connection time each time it is unsuccessful. Problem here is that I can only catch the error when getting the response, and
|
||||||
|
response can be successful on the first XML exchange (for example when you send opening stream request), while breaking on the
|
||||||
|
second request. With this in mind my only idea was to implement throttling with timestamps or something.
|
||||||
|
- **sessions** - I presume this part is working correctly but should be tested from a framework
|
||||||
|
- **multiple connections** - I think this part works fine, but I am worried that triggering `getRoster()` while simultaneously
|
||||||
|
fetching a message may delete one server response. If you get in one batch both roster and message, it will be added to the buffer.
|
||||||
|
Calling back the response will get either roster or message, not both. And then buffer will be flushed. This is something that
|
||||||
|
needs thinking.
|
||||||
|
- **structure of XmppClient** - in order to enable the `$client->stanza->method` I need to instantiate all stanzas within the
|
||||||
|
class. I feel as this could be simplified.
|
38
composer.json
Normal file
38
composer.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "pdahal/php-xmpp",
|
||||||
|
"description": "PHP library for XMPP based on `xmppo/xmpp-php`",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"php",
|
||||||
|
"xmpp",
|
||||||
|
"jabber",
|
||||||
|
"library"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Prabin Dahal",
|
||||||
|
"email": "mr.prabin@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Marko Dupor",
|
||||||
|
"email": "marko.dupor@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "~11.0",
|
||||||
|
"phpmd/phpmd": "~2.15",
|
||||||
|
"squizlabs/php_codesniffer": "~3.10",
|
||||||
|
"friendsofphp/php-cs-fixer": "~3.64",
|
||||||
|
"rector/rector": "~1.2"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Pdahal\\Xmpp\\": "src/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
phpunit.xml
Normal file
12
phpunit.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<phpunit bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
convertErrorsToExceptions="true"
|
||||||
|
convertNoticesToExceptions="true"
|
||||||
|
convertWarningsToExceptions="true"
|
||||||
|
stopOnFailure="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit Tests">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
17
rector.php
Normal file
17
rector.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Rector\Config\RectorConfig;
|
||||||
|
use Rector\Renaming\Rector\Name\RenameClassRector;
|
||||||
|
|
||||||
|
return RectorConfig::configure()
|
||||||
|
->withPaths([
|
||||||
|
__DIR__.'/src',
|
||||||
|
__DIR__.'/tests',
|
||||||
|
])
|
||||||
|
->withPreparedSets(deadCode: true)
|
||||||
|
// uncomment to reach your current PHP version
|
||||||
|
->withPhpSets(php82: true)
|
||||||
|
->withTypeCoverageLevel(7)
|
||||||
|
;
|
15
src/AuthTypes/Authenticable.php
Normal file
15
src/AuthTypes/Authenticable.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\AuthTypes;
|
||||||
|
|
||||||
|
interface Authenticable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Based on auth type, return the right format of credentials to be sent to the server.
|
||||||
|
*/
|
||||||
|
public function encodedCredentials(): string;
|
||||||
|
|
||||||
|
public function getName(): string;
|
||||||
|
}
|
24
src/AuthTypes/Authentication.php
Normal file
24
src/AuthTypes/Authentication.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\AuthTypes;
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\Options;
|
||||||
|
use Pdahal\Xmpp\Xml\Xml;
|
||||||
|
|
||||||
|
abstract class Authentication implements Authenticable
|
||||||
|
{
|
||||||
|
use Xml;
|
||||||
|
|
||||||
|
protected string $name;
|
||||||
|
|
||||||
|
public function __construct(protected Options $options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
16
src/AuthTypes/DigestMD5.php
Normal file
16
src/AuthTypes/DigestMD5.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\AuthTypes;
|
||||||
|
|
||||||
|
class DigestMD5 extends Authentication
|
||||||
|
{
|
||||||
|
protected string $name = 'DIGEST-MD5';
|
||||||
|
|
||||||
|
public function encodedCredentials(): string
|
||||||
|
{
|
||||||
|
$credentials = "\x00{$this->options->getUsername()}\x00{$this->options->getPassword()}";
|
||||||
|
return self::quote(sha1($credentials));
|
||||||
|
}
|
||||||
|
}
|
16
src/AuthTypes/Plain.php
Normal file
16
src/AuthTypes/Plain.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\AuthTypes;
|
||||||
|
|
||||||
|
class Plain extends Authentication
|
||||||
|
{
|
||||||
|
protected string $name = 'PLAIN';
|
||||||
|
|
||||||
|
public function encodedCredentials(): string
|
||||||
|
{
|
||||||
|
$credentials = "\x00{$this->options->getUsername()}\x00{$this->options->getPassword()}";
|
||||||
|
return self::quote(base64_encode($credentials));
|
||||||
|
}
|
||||||
|
}
|
18
src/Buffers/Buffer.php
Normal file
18
src/Buffers/Buffer.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Buffers;
|
||||||
|
|
||||||
|
interface Buffer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Write to buffer (add to array of values)
|
||||||
|
*/
|
||||||
|
public function write(string $data): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read from buffer and delete the data
|
||||||
|
*/
|
||||||
|
public function read(): string;
|
||||||
|
}
|
35
src/Buffers/Response.php
Normal file
35
src/Buffers/Response.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Buffers;
|
||||||
|
|
||||||
|
class Response implements Buffer
|
||||||
|
{
|
||||||
|
protected array $response = [];
|
||||||
|
|
||||||
|
public function write(?string $data = null): void
|
||||||
|
{
|
||||||
|
if ($data) {
|
||||||
|
$this->response[] = $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(): string
|
||||||
|
{
|
||||||
|
$implodedResponse = $this->response != [] ? implode('', $this->response) : '';
|
||||||
|
$this->flush();
|
||||||
|
|
||||||
|
return $implodedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function flush(): void
|
||||||
|
{
|
||||||
|
$this->response = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentBufferData(): array
|
||||||
|
{
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
}
|
18
src/Exceptions/DeadSocket.php
Normal file
18
src/Exceptions/DeadSocket.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class DeadSocket extends Exception
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$errorCode = socket_last_error();
|
||||||
|
$errorMsg = socket_strerror($errorCode);
|
||||||
|
|
||||||
|
parent::__construct("Couldn't create socket: [$errorCode] $errorMsg");
|
||||||
|
}
|
||||||
|
}
|
15
src/Exceptions/StreamError.php
Normal file
15
src/Exceptions/StreamError.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class StreamError extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(string $streamErrorType)
|
||||||
|
{
|
||||||
|
parent::__construct("Unrecoverable stream error ({$streamErrorType}), trying to reconnect...");
|
||||||
|
}
|
||||||
|
}
|
33
src/Loggers/Loggable.php
Normal file
33
src/Loggers/Loggable.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Loggers;
|
||||||
|
|
||||||
|
interface Loggable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Standard log message
|
||||||
|
*/
|
||||||
|
public function log(string $message): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand method for logging with prepended "REQUEST" string
|
||||||
|
*/
|
||||||
|
public function logRequest(string $message): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand method for logging with prepended "RESPONSE" string
|
||||||
|
*/
|
||||||
|
public function logResponse(string $message): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand method for logging with prepended "ERROR" string
|
||||||
|
*/
|
||||||
|
public function error(string $message): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns relative path from given resource
|
||||||
|
*/
|
||||||
|
public function getFilePathFromResource(mixed $resource): string;
|
||||||
|
}
|
69
src/Loggers/Logger.php
Normal file
69
src/Loggers/Logger.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Loggers;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class Logger implements Loggable
|
||||||
|
{
|
||||||
|
public mixed $log;
|
||||||
|
|
||||||
|
public const LOG_FOLDER = "logs";
|
||||||
|
public const LOG_FILE = "xmpp.log";
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->createLogFile();
|
||||||
|
$this->log = fopen(self::LOG_FOLDER . '/' . self::LOG_FILE, 'a');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createLogFile(): void
|
||||||
|
{
|
||||||
|
if (!file_exists(self::LOG_FOLDER)) {
|
||||||
|
mkdir(self::LOG_FOLDER, 0777, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function log(string $message): void
|
||||||
|
{
|
||||||
|
$this->writeToLog($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logRequest(string $message): void
|
||||||
|
{
|
||||||
|
$this->writeToLog($message, "REQUEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logResponse(string $message): void
|
||||||
|
{
|
||||||
|
$this->writeToLog($message, "RESPONSE");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function error(string $message): void
|
||||||
|
{
|
||||||
|
$this->writeToLog($message, "ERROR");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function writeToLog(string $message, string $type = ''): void
|
||||||
|
{
|
||||||
|
$prefix = date("Y.m.d H:i:s") . " " . session_id() . ($type ? " {$type}::" : " ");
|
||||||
|
$this->writeToFile($this->log, $prefix . "$message\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function writeToFile(mixed $file, string $message): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
fwrite($file, $message);
|
||||||
|
} catch (Exception) {
|
||||||
|
// silent fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilePathFromResource(mixed $resource): string
|
||||||
|
{
|
||||||
|
$metaData = stream_get_meta_data($resource);
|
||||||
|
return $metaData["uri"];
|
||||||
|
}
|
||||||
|
}
|
259
src/Options.php
Normal file
259
src/Options.php
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp;
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\AuthTypes\Authenticable;
|
||||||
|
use Pdahal\Xmpp\AuthTypes\Plain;
|
||||||
|
use Pdahal\Xmpp\Loggers\Loggable;
|
||||||
|
use Pdahal\Xmpp\Loggers\Logger;
|
||||||
|
use Psr\Log\InvalidArgumentException;
|
||||||
|
|
||||||
|
class Options
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Hostname of XMPP server.
|
||||||
|
*/
|
||||||
|
protected string $host = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain Name of XMPP server.
|
||||||
|
*/
|
||||||
|
protected string $domain = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMPP server port. Usually 5222.
|
||||||
|
*/
|
||||||
|
protected int $port = 5222;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protocol used for socket connection, defaults to TCP.
|
||||||
|
*/
|
||||||
|
protected string $protocol = 'tcp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Username to authenticate on XMPP server.
|
||||||
|
*/
|
||||||
|
protected string $username = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password to authenticate on XMPP server.
|
||||||
|
*/
|
||||||
|
protected string $password = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XMPP resource.
|
||||||
|
*/
|
||||||
|
protected string $resource = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom logger interface.
|
||||||
|
*/
|
||||||
|
protected ?Loggable $logger = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use TLS if available.
|
||||||
|
*/
|
||||||
|
protected bool $useTls = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth type (Authentication/AuthTypes/).
|
||||||
|
*/
|
||||||
|
protected ?Authenticable $authType = null;
|
||||||
|
|
||||||
|
public function getHost(): string
|
||||||
|
{
|
||||||
|
if (!$this->host) {
|
||||||
|
$this->getLogger()->error(__METHOD__.'::'.__LINE__.
|
||||||
|
' No host found, please set the host variable');
|
||||||
|
|
||||||
|
throw new InvalidArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHost(string $host): Options
|
||||||
|
{
|
||||||
|
$this->host = trim($host);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDomain(string $domain): Options
|
||||||
|
{
|
||||||
|
$this->domain = $domain;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getDomain(): string
|
||||||
|
{
|
||||||
|
if ($this->domain == '') {
|
||||||
|
return $this->getHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPort(): int
|
||||||
|
{
|
||||||
|
return $this->port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPort(int $port): Options
|
||||||
|
{
|
||||||
|
$this->port = $port;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUsername(): string
|
||||||
|
{
|
||||||
|
if ($this->username == '') {
|
||||||
|
$this->getLogger()->error(__METHOD__.'::'.__LINE__.
|
||||||
|
' No username found, please set the username variable');
|
||||||
|
|
||||||
|
throw new InvalidArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to assign a resource if it exists. If bare JID is forwarded, this will default to your username.
|
||||||
|
*/
|
||||||
|
public function setUsername(string $username): Options
|
||||||
|
{
|
||||||
|
$usernameResource = explode('/', $username);
|
||||||
|
|
||||||
|
if (count($usernameResource) > 1) {
|
||||||
|
$this->setResource($usernameResource[1]);
|
||||||
|
$username = $usernameResource[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->username = trim($username);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function getPassword(): string
|
||||||
|
{
|
||||||
|
if ($this->password == '') {
|
||||||
|
$this->getLogger()->error(__METHOD__.'::'.__LINE__.
|
||||||
|
' No password found, please set the password variable');
|
||||||
|
|
||||||
|
throw new InvalidArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPassword(string $password): Options
|
||||||
|
{
|
||||||
|
$this->password = $password;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResource(): string
|
||||||
|
{
|
||||||
|
if ($this->resource == '') {
|
||||||
|
$this->resource = 'Pdahal_machine_'.time();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResource(string $resource): Options
|
||||||
|
{
|
||||||
|
$this->resource = trim($resource);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProtocol(): string
|
||||||
|
{
|
||||||
|
return $this->protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProtocol(string $protocol): Options
|
||||||
|
{
|
||||||
|
$this->protocol = $protocol;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fullSocketAddress(): string
|
||||||
|
{
|
||||||
|
$protocol = $this->getProtocol();
|
||||||
|
$host = $this->getHost();
|
||||||
|
$port = $this->getPort();
|
||||||
|
|
||||||
|
return "{$protocol}://{$host}:{$port}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fullJid(): string
|
||||||
|
{
|
||||||
|
$username = $this->getUsername();
|
||||||
|
$resource = $this->getResource();
|
||||||
|
$host = $this->getHost();
|
||||||
|
|
||||||
|
return "{$username}@{$host}/{$resource}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bareJid(): string
|
||||||
|
{
|
||||||
|
$username = $this->getUsername();
|
||||||
|
$host = $this->getHost();
|
||||||
|
|
||||||
|
return "{$username}@{$host}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLogger(Loggable $logger): void
|
||||||
|
{
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLogger(): Loggable
|
||||||
|
{
|
||||||
|
if ($this->logger === null) {
|
||||||
|
$this->logger = new Logger();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUseTls(bool $enable): void
|
||||||
|
{
|
||||||
|
$this->useTls = $enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function usingTls(): bool
|
||||||
|
{
|
||||||
|
return $this->useTls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuthType(): Authenticable
|
||||||
|
{
|
||||||
|
if ($this->authType === null) {
|
||||||
|
$this->setAuthType(new Plain($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->authType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAuthType(Authenticable $authType): Options
|
||||||
|
{
|
||||||
|
$this->authType = $authType;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
111
src/Socket.php
Normal file
111
src/Socket.php
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp;
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\Buffers\Response;
|
||||||
|
use Pdahal\Xmpp\Exceptions\DeadSocket;
|
||||||
|
|
||||||
|
class Socket
|
||||||
|
{
|
||||||
|
public mixed $connection;
|
||||||
|
protected Response $responseBuffer;
|
||||||
|
protected Options $options;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Period in microseconds for imposed timeout while doing socket_read().
|
||||||
|
*/
|
||||||
|
protected int $timeout = 150000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socket constructor.
|
||||||
|
*
|
||||||
|
* @throws DeadSocket
|
||||||
|
*/
|
||||||
|
public function __construct(Options $options)
|
||||||
|
{
|
||||||
|
$this->responseBuffer = new Response();
|
||||||
|
$this->connection = stream_socket_client($options->fullSocketAddress());
|
||||||
|
|
||||||
|
if (!$this->isAlive($this->connection)) {
|
||||||
|
throw new DeadSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
// stream_set_blocking($this->connection, true);
|
||||||
|
stream_set_timeout($this->connection, 0, $this->timeout);
|
||||||
|
$this->options = $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disconnect(): void
|
||||||
|
{
|
||||||
|
fclose($this->connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sending XML stanzas to open socket.
|
||||||
|
*/
|
||||||
|
public function send(string $xml)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
fwrite($this->connection, $xml);
|
||||||
|
$this->options->getLogger()->logRequest(__METHOD__.'::'.__LINE__." {$xml}");
|
||||||
|
// $this->checkSocketStatus();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->options->getLogger()->error(__METHOD__.'::'.__LINE__.' fwrite() failed '.$e->getMessage());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->receive();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function receive(): void
|
||||||
|
{
|
||||||
|
$response = '';
|
||||||
|
while ($out = fgets($this->connection)) {
|
||||||
|
$response .= $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->responseBuffer->write($response);
|
||||||
|
$this->options->getLogger()->logResponse(__METHOD__.'::'.__LINE__." {$response}");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isAlive($socket): bool
|
||||||
|
{
|
||||||
|
return $socket !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTimeout($timeout): void
|
||||||
|
{
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResponseBuffer(): Response
|
||||||
|
{
|
||||||
|
return $this->responseBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOptions(): Options
|
||||||
|
{
|
||||||
|
return $this->options;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkSocketStatus(): void
|
||||||
|
{
|
||||||
|
$status = socket_get_status($this->connection);
|
||||||
|
|
||||||
|
// echo print_r($status);
|
||||||
|
|
||||||
|
if ($status['eof']) {
|
||||||
|
$this->options->getLogger()->logResponse(
|
||||||
|
__METHOD__.'::'.__LINE__.
|
||||||
|
" ---Probably a broken pipe, restart connection\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/Xml/Stanzas/Auth.php
Normal file
51
src/Xml/Stanzas/Auth.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\AuthTypes\Authenticable;
|
||||||
|
|
||||||
|
class Auth extends Stanza
|
||||||
|
{
|
||||||
|
public function authenticate(): void
|
||||||
|
{
|
||||||
|
$response = $this->socket->getResponseBuffer()->read();
|
||||||
|
$options = $this->socket->getOptions();
|
||||||
|
|
||||||
|
$tlsSupported = self::isTlsSupported($response);
|
||||||
|
$tlsRequired = self::isTlsRequired($response);
|
||||||
|
|
||||||
|
if ($tlsSupported && ($tlsRequired || (!$tlsRequired && $options->usingTls()))) {
|
||||||
|
$this->startTls();
|
||||||
|
$this->socket->send(self::openXmlStream($options->getDomain()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = $this->generateAuthXml($options->getAuthType());
|
||||||
|
$this->socket->send($xml);
|
||||||
|
$this->socket->send(self::openXmlStream($options->getDomain()));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function startTls(): void
|
||||||
|
{
|
||||||
|
$this->socket->send("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");
|
||||||
|
$response = $this->socket->getResponseBuffer()->read();
|
||||||
|
|
||||||
|
if (!self::canProceed($response)) {
|
||||||
|
$this->socket->getOptions()->getLogger()->error(__METHOD__ . '::' . __LINE__ .
|
||||||
|
" TLS authentication failed. Trying to continue but will most likely fail.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_socket_enable_crypto($this->socket->connection, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateAuthXml(Authenticable $authType): string
|
||||||
|
{
|
||||||
|
$mechanism = $authType->getName();
|
||||||
|
$encodedCredentials = $authType->encodedCredentials();
|
||||||
|
$nameSpace = "urn:ietf:params:xml:ns:xmpp-sasl";
|
||||||
|
|
||||||
|
return "<auth xmlns='{$nameSpace}' mechanism='{$mechanism}'>{$encodedCredentials}</auth>";
|
||||||
|
}
|
||||||
|
}
|
98
src/Xml/Stanzas/Iq.php
Normal file
98
src/Xml/Stanzas/Iq.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||||
|
|
||||||
|
class Iq extends Stanza
|
||||||
|
{
|
||||||
|
public function getRoster(): void
|
||||||
|
{
|
||||||
|
$query = "<query xmlns='jabber:iq:roster'/>";
|
||||||
|
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addToRoster(string $name, string $forJid, string $from, ?string $groupName = null): void
|
||||||
|
{
|
||||||
|
$group = $groupName ? "<group>{$groupName}</group>" : null;
|
||||||
|
$item = "<item jid='{$forJid}' name='{$name}'>{$group}</item>";
|
||||||
|
$query = "<query xmlns='jabber:iq:roster'>{$item}</query>";
|
||||||
|
$xml = "<iq type='set' id='{$this->uniqueId()}' from='{$from}'>{$query}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeFromRoster(string $jid, string $myJid): void
|
||||||
|
{
|
||||||
|
$item = "<item jid='{$jid}' subscription='remove'/>";
|
||||||
|
$query = "<query xmlns='jabber:iq:roster'>{$item}</query>";
|
||||||
|
$xml = "<iq type='set' id='{$this->uniqueId()}' from='{$myJid}'>{$query}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setResource(string $name): void
|
||||||
|
{
|
||||||
|
if (!trim($name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resource = "<resource>{$name}</resource>";
|
||||||
|
$bind = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>{$resource}</bind>";
|
||||||
|
$xml = "<iq type='set' id='{$this->uniqueId()}'>{$bind}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setGroup(string $name, string $forJid): void
|
||||||
|
{
|
||||||
|
$group = "<group>{$name}</group>";
|
||||||
|
$item = "<item jid='{$forJid}'>{$group}</item>";
|
||||||
|
$query = "<query xmlns='jabber:iq:roster'>{$item}</query>";
|
||||||
|
$xml = "<iq type='set' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getServerVersion(): void
|
||||||
|
{
|
||||||
|
$query = "<query xmlns='jabber:iq:version'/>";
|
||||||
|
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getServerFeatures(): void
|
||||||
|
{
|
||||||
|
$query = "<query xmlns='http://jabber.org/protocol/disco#info'></query>";
|
||||||
|
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getServerTime(): void
|
||||||
|
{
|
||||||
|
$query = "<query xmlns='urn:xmpp:time'/>";
|
||||||
|
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFeatures(string $forJid): void
|
||||||
|
{
|
||||||
|
$query = "<query xmlns='http://jabber.org/protocol/disco#info'></query>";
|
||||||
|
$xml = "<iq type='get' to='{$forJid}'>{$query}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ping(): void
|
||||||
|
{
|
||||||
|
$query = "<query xmlns='urn:xmpp:ping'/>";
|
||||||
|
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
}
|
31
src/Xml/Stanzas/Message.php
Normal file
31
src/Xml/Stanzas/Message.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||||
|
|
||||||
|
class Message extends Stanza
|
||||||
|
{
|
||||||
|
public function send(string $body, string $to, string $type = "chat"): void
|
||||||
|
{
|
||||||
|
$xml = $this->generateMessageXml($body, $to, $type);
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function receive(): array
|
||||||
|
{
|
||||||
|
$this->socket->receive();
|
||||||
|
$rawResponse = $this->socket->getResponseBuffer()->read();
|
||||||
|
return self::parseTag($rawResponse, "message");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateMessageXml(string $body, string $to, string $type): string
|
||||||
|
{
|
||||||
|
$to = self::quote($to);
|
||||||
|
$body = self::quote($body);
|
||||||
|
|
||||||
|
$bodyXml = "<body>{$body}</body>";
|
||||||
|
|
||||||
|
return "<message to='{$to}' type='{$type}'>{$bodyXml}</message>";
|
||||||
|
}
|
||||||
|
}
|
67
src/Xml/Stanzas/Presence.php
Normal file
67
src/Xml/Stanzas/Presence.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||||
|
|
||||||
|
class Presence extends Stanza
|
||||||
|
{
|
||||||
|
public const PRIORITY_UPPER_BOUND = 127;
|
||||||
|
public const PRIORITY_LOWER_BOUND = -128;
|
||||||
|
|
||||||
|
public function subscribe(string $to): void
|
||||||
|
{
|
||||||
|
$this->setPresence($to, 'subscribe');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unsubscribe(string $from): void
|
||||||
|
{
|
||||||
|
$this->setPresence($from, 'unsubscribe');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function acceptSubscription(string $from): void
|
||||||
|
{
|
||||||
|
$this->setPresence($from, 'subscribed');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function declineSubscription(string $from): void
|
||||||
|
{
|
||||||
|
$this->setPresence($from, 'unsubscribed');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setPresence(string $to, string $type = "subscribe"): void
|
||||||
|
{
|
||||||
|
$xml = "<presence from='{$this->socket->getOptions()->bareJid()}' to='{$to}' type='{$type}'/>";
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set priority to current resource by default, or optional other resource tied to the
|
||||||
|
* current username
|
||||||
|
* @param string|null $forResource
|
||||||
|
*/
|
||||||
|
public function setPriority(int $value, string $forResource = null): void
|
||||||
|
{
|
||||||
|
$from = self::quote($this->socket->getOptions()->fullJid());
|
||||||
|
|
||||||
|
if ($forResource) {
|
||||||
|
$from = $this->socket->getOptions()->getUsername() . "/$forResource";
|
||||||
|
}
|
||||||
|
|
||||||
|
$priority = "<priority>{$this->limitPriority($value)}</priority>";
|
||||||
|
$xml = "<presence from='{$from}'>{$priority}</presence>";
|
||||||
|
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function limitPriority(int $value): int
|
||||||
|
{
|
||||||
|
if ($value > self::PRIORITY_UPPER_BOUND) {
|
||||||
|
return self::PRIORITY_UPPER_BOUND;
|
||||||
|
} elseif ($value < self::PRIORITY_LOWER_BOUND) {
|
||||||
|
return self::PRIORITY_LOWER_BOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
31
src/Xml/Stanzas/Stanza.php
Normal file
31
src/Xml/Stanzas/Stanza.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\Socket;
|
||||||
|
use Pdahal\Xmpp\Xml\Xml;
|
||||||
|
|
||||||
|
abstract class Stanza
|
||||||
|
{
|
||||||
|
use Xml;
|
||||||
|
|
||||||
|
public function __construct(protected Socket $socket)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function uniqueId(): string
|
||||||
|
{
|
||||||
|
return uniqid();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function readResponseFile(): string|false
|
||||||
|
{
|
||||||
|
$logger = $this->socket->getOptions()->getLogger();
|
||||||
|
$responseFilePath = $logger->getFilePathFromResource($logger->log);
|
||||||
|
$responseFile = fopen($responseFilePath, 'r');
|
||||||
|
|
||||||
|
return fread($responseFile, filesize($responseFilePath));
|
||||||
|
}
|
||||||
|
}
|
123
src/Xml/Xml.php
Normal file
123
src/Xml/Xml.php
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp\Xml;
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\Exceptions\StreamError;
|
||||||
|
|
||||||
|
trait Xml
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Opening tag for starting a XMPP stream exchange.
|
||||||
|
*/
|
||||||
|
public static function openXmlStream(string $host): string
|
||||||
|
{
|
||||||
|
$xmlOpen = "<?xml version='1.0' encoding='UTF-8'?>";
|
||||||
|
$to = "to='{$host}'";
|
||||||
|
$stream = "xmlns:stream='http://etherx.jabber.org/streams'";
|
||||||
|
$client = "xmlns='jabber:client'";
|
||||||
|
$version = "version='1.0'";
|
||||||
|
|
||||||
|
return "{$xmlOpen}<stream:stream {$to} {$stream} {$client} {$version}>";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closing tag for one XMPP stream session.
|
||||||
|
*/
|
||||||
|
public static function closeXmlStream(): string
|
||||||
|
{
|
||||||
|
return '</stream:stream>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function quote(string $input): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($input, ENT_XML1, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function parseTag(string $rawResponse, string $tag): array
|
||||||
|
{
|
||||||
|
preg_match_all("#(<{$tag}.*?>.*?<\\/{$tag}>)#si", $rawResponse, $matched);
|
||||||
|
|
||||||
|
return count($matched) <= 1 ? [] : array_map(fn ($match): false|\SimpleXMLElement => @simplexml_load_string($match), $matched[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function parseFeatures(string $xml): string
|
||||||
|
{
|
||||||
|
return self::matchInsideOfTag($xml, 'stream:features');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isTlsSupported(string $xml): bool
|
||||||
|
{
|
||||||
|
$matchTag = self::matchCompleteTag($xml, 'starttls');
|
||||||
|
|
||||||
|
return !empty($matchTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isTlsRequired(string $xml): bool
|
||||||
|
{
|
||||||
|
if (!self::isTlsSupported($xml)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tls = self::matchCompleteTag($xml, 'starttls');
|
||||||
|
preg_match('#required#', $tls, $match);
|
||||||
|
|
||||||
|
return count($match) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function matchCompleteTag(string $xml, string $tag): string
|
||||||
|
{
|
||||||
|
$matches = self::matchTag($xml, $tag);
|
||||||
|
|
||||||
|
return is_array($matches) && count($matches) > 0 ? $matches[0] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function matchInsideOfTag(string $xml, string $tag): string
|
||||||
|
{
|
||||||
|
$match = self::matchTag($xml, $tag);
|
||||||
|
|
||||||
|
return is_array($match) && count($match) > 1 ? $match[1] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function matchTag(string $xml, string $tag): array
|
||||||
|
{
|
||||||
|
$matches = null;
|
||||||
|
preg_match("#<{$tag}.*?>(.*)<\\/{$tag}>#", $xml, $matches);
|
||||||
|
|
||||||
|
return count($matches) < 1 ? [] : $matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canProceed($xml): bool
|
||||||
|
{
|
||||||
|
preg_match("#<proceed xmlns=[\\'|\"]urn:ietf:params:xml:ns:xmpp-tls[\\'|\"]\\/>#", (string) $xml, $match);
|
||||||
|
|
||||||
|
return count($match) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function supportedAuthMethods($xml): array
|
||||||
|
{
|
||||||
|
preg_match_all('#<mechanism>(.*?)<\\/mechanism>#', (string) $xml, $match);
|
||||||
|
|
||||||
|
return count($match) < 1 ? [] : $match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function roster(string $xml): array
|
||||||
|
{
|
||||||
|
preg_match_all("#<iq.*?type=[\\'|\"]result[\\'|\"]>(.*?)<\\/iq>#", $xml, $match);
|
||||||
|
|
||||||
|
return count($match) < 1 ? [] : $match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws StreamError
|
||||||
|
*/
|
||||||
|
public static function checkForUnrecoverableErrors(string $response): void
|
||||||
|
{
|
||||||
|
preg_match_all('#<stream:error>(<(.*?) (.*?)\\/>)<\\/stream:error>#', $response, $streamErrors);
|
||||||
|
|
||||||
|
if ((!empty($streamErrors[0])) && count($streamErrors[2]) > 0) {
|
||||||
|
throw new StreamError($streamErrors[2][0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
131
src/XmppClient.php
Normal file
131
src/XmppClient.php
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pdahal\Xmpp;
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\Exceptions\StreamError;
|
||||||
|
use Pdahal\Xmpp\Xml\Stanzas\Auth;
|
||||||
|
use Pdahal\Xmpp\Xml\Stanzas\Iq;
|
||||||
|
use Pdahal\Xmpp\Xml\Stanzas\Message;
|
||||||
|
use Pdahal\Xmpp\Xml\Stanzas\Presence;
|
||||||
|
use Pdahal\Xmpp\Xml\Xml;
|
||||||
|
|
||||||
|
class XmppClient
|
||||||
|
{
|
||||||
|
use Xml;
|
||||||
|
|
||||||
|
protected Socket $socket;
|
||||||
|
|
||||||
|
public Auth $auth;
|
||||||
|
|
||||||
|
public Iq $iq;
|
||||||
|
|
||||||
|
public Presence $presence;
|
||||||
|
|
||||||
|
public Message $message;
|
||||||
|
|
||||||
|
public function __construct(protected Options $options)
|
||||||
|
{
|
||||||
|
$this->initDependencies();
|
||||||
|
$this->initSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initDependencies(): void
|
||||||
|
{
|
||||||
|
$this->socket = $this->initSocket();
|
||||||
|
$this->initStanzas($this->socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function connect(): void
|
||||||
|
{
|
||||||
|
$this->openStream();
|
||||||
|
$this->auth->authenticate();
|
||||||
|
$this->iq->setResource($this->options->getResource());
|
||||||
|
$this->sendInitialPresenceStanza();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(string $xml): void
|
||||||
|
{
|
||||||
|
$this->socket->send($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResponse(): string
|
||||||
|
{
|
||||||
|
$this->socket->receive();
|
||||||
|
$response = $this->socket->getResponseBuffer()->read();
|
||||||
|
|
||||||
|
return $this->checkForErrors($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prettyPrint(string $response): void
|
||||||
|
{
|
||||||
|
if ($response) {
|
||||||
|
$separator = "\n-------------\n";
|
||||||
|
echo "{$separator} {$response} {$separator}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disconnect(): void
|
||||||
|
{
|
||||||
|
$this->socket->send(self::closeXmlStream());
|
||||||
|
$this->socket->disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function openStream(): void
|
||||||
|
{
|
||||||
|
$openStreamXml = self::openXmlStream($this->options->getDomain());
|
||||||
|
$this->socket->send($openStreamXml);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sendInitialPresenceStanza(): void
|
||||||
|
{
|
||||||
|
$this->socket->send('<presence/>');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initStanzas(Socket $socket): void
|
||||||
|
{
|
||||||
|
$this->auth = new Auth($socket);
|
||||||
|
$this->iq = new Iq($socket);
|
||||||
|
$this->presence = new Presence($socket);
|
||||||
|
$this->message = new Message($socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initSession(): void
|
||||||
|
{
|
||||||
|
if (!headers_sent($filename, $linenum)) {
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initSocket(): Socket
|
||||||
|
{
|
||||||
|
return new Socket($this->options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws StreamError
|
||||||
|
*/
|
||||||
|
protected function checkForErrors(string $response): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
self::checkForUnrecoverableErrors($response);
|
||||||
|
} catch (StreamError $e) {
|
||||||
|
$this->options->getLogger()->logResponse(__METHOD__.'::'.__LINE__." {$response}");
|
||||||
|
$this->options->getLogger()->error(__METHOD__.'::'.__LINE__.' '.$e->getMessage());
|
||||||
|
$this->reconnect();
|
||||||
|
$response = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function reconnect(): void
|
||||||
|
{
|
||||||
|
$this->disconnect();
|
||||||
|
$this->initDependencies();
|
||||||
|
$this->connect();
|
||||||
|
}
|
||||||
|
}
|
31
tests/AuthPlainTest.php
Normal file
31
tests/AuthPlainTest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\AuthTypes\Plain;
|
||||||
|
use Pdahal\Xmpp\Options;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class AuthPlainTest extends TestCase
|
||||||
|
{
|
||||||
|
public Plain $plainAuth;
|
||||||
|
|
||||||
|
public Options $optionsStub;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->optionsStub = $this->createMock(Options::class);
|
||||||
|
$this->optionsStub->method('getUsername')->willReturn('Foo');
|
||||||
|
$this->optionsStub->method('getPassword')->willReturn('Bar');
|
||||||
|
|
||||||
|
$this->plainAuth = new Plain($this->optionsStub);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIfCredentialsAreEncodedRight(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('AEZvbwBCYXI=', $this->plainAuth->encodedCredentials());
|
||||||
|
}
|
||||||
|
}
|
26
tests/DigestMD5Test.php
Normal file
26
tests/DigestMD5Test.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\AuthTypes\DigestMD5;
|
||||||
|
use Pdahal\Xmpp\Options;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class DigestMD5Test extends TestCase
|
||||||
|
{
|
||||||
|
public DigestMD5 $digestAuth;
|
||||||
|
|
||||||
|
public Options $optionsStub;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->optionsStub = $this->createMock(Options::class);
|
||||||
|
$this->optionsStub->method('getUsername')->willReturn('Foo');
|
||||||
|
$this->optionsStub->method('getPassword')->willReturn('Bar');
|
||||||
|
|
||||||
|
$this->digestAuth = new DigestMD5($this->optionsStub);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIfCredentialsAreEncodedRight(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals("80e25ae4140c338afbe621c1b3d7a9ec9480f731", $this->digestAuth->encodedCredentials());
|
||||||
|
}
|
||||||
|
}
|
124
tests/OptionsTest.php
Normal file
124
tests/OptionsTest.php
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\Options;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class OptionsTest extends TestCase
|
||||||
|
{
|
||||||
|
protected $host;
|
||||||
|
protected $port;
|
||||||
|
protected $username;
|
||||||
|
protected $password;
|
||||||
|
|
||||||
|
public Options $options;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->host = 'www.host.com';
|
||||||
|
$this->username = 'foo';
|
||||||
|
$this->password = 'bar';
|
||||||
|
$this->port = 5222;
|
||||||
|
$this->options = new Options();
|
||||||
|
|
||||||
|
$this->options
|
||||||
|
->setHost($this->host)
|
||||||
|
->setPort($this->port)
|
||||||
|
->setUsername($this->username)
|
||||||
|
->setPassword($this->password)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIfNoHostThrowsError(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->options->setHost('');
|
||||||
|
$this->options->getHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIfNoUsernameThrowsError(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->options->setUsername('');
|
||||||
|
$this->options->getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIfNoPasswordThrowsError(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->options->setPassword('');
|
||||||
|
$this->options->getPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIfHostGetsTrimmed(): void
|
||||||
|
{
|
||||||
|
$this->options->setHost(' host');
|
||||||
|
$this->assertEquals('host', $this->options->getHost());
|
||||||
|
|
||||||
|
$this->options->setHost('host ');
|
||||||
|
$this->assertEquals('host', $this->options->getHost());
|
||||||
|
|
||||||
|
$this->options->setHost(' host ');
|
||||||
|
$this->assertEquals('host', $this->options->getHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIfUsernameSplitResource(): void
|
||||||
|
{
|
||||||
|
$this->options->setUsername('user/resource');
|
||||||
|
$this->assertEquals('user', $this->options->getUsername());
|
||||||
|
$this->assertEquals('resource', $this->options->getResource());
|
||||||
|
|
||||||
|
$this->options->setUsername('user');
|
||||||
|
$this->assertEquals('user', $this->options->getUsername());
|
||||||
|
|
||||||
|
$this->options->setUsername('user/resource/resource2/resource3');
|
||||||
|
$this->assertEquals('user', $this->options->getUsername());
|
||||||
|
$this->assertEquals('resource', $this->options->getResource());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResourcePrecedence(): void
|
||||||
|
{
|
||||||
|
$this->options->setUsername('user/resource');
|
||||||
|
$this->options->setResource('resource2');
|
||||||
|
$this->assertEquals('user', $this->options->getUsername());
|
||||||
|
$this->assertEquals('resource2', $this->options->getResource());
|
||||||
|
|
||||||
|
$this->options->setResource('resource2');
|
||||||
|
$this->options->setUsername('user/resource');
|
||||||
|
$this->assertEquals('user', $this->options->getUsername());
|
||||||
|
$this->assertEquals('resource', $this->options->getResource());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIfResourceGetsTrimmed(): void
|
||||||
|
{
|
||||||
|
$this->options->setResource(' resource');
|
||||||
|
$this->assertEquals('resource', $this->options->getResource());
|
||||||
|
|
||||||
|
$this->options->setResource('resource ');
|
||||||
|
$this->assertEquals('resource', $this->options->getResource());
|
||||||
|
|
||||||
|
$this->options->setResource(' resource ');
|
||||||
|
$this->assertEquals('resource', $this->options->getResource());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFullSocketAddress(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('tcp://www.host.com:5222', $this->options->fullSocketAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFullJid(): void
|
||||||
|
{
|
||||||
|
$this->options->setResource('resource');
|
||||||
|
$this->assertEquals('foo@www.host.com/resource', $this->options->fullJid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBareJid(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('foo@www.host.com', $this->options->bareJid());
|
||||||
|
}
|
||||||
|
}
|
70
tests/ResponseBufferTest.php
Normal file
70
tests/ResponseBufferTest.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\Buffers\Response;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class ResponseBufferTest extends TestCase
|
||||||
|
{
|
||||||
|
public Response $buffer;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->buffer = new Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteString(): void
|
||||||
|
{
|
||||||
|
$this->buffer->write('test');
|
||||||
|
$this->assertEquals(['test'], $this->buffer->getCurrentBufferData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteNumber(): void
|
||||||
|
{
|
||||||
|
$this->buffer->write(123);
|
||||||
|
$this->assertEquals(['123'], $this->buffer->getCurrentBufferData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteEmpty(): void
|
||||||
|
{
|
||||||
|
$this->buffer->write('');
|
||||||
|
$this->assertEquals([], $this->buffer->getCurrentBufferData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteNull(): void
|
||||||
|
{
|
||||||
|
$this->buffer->write(null);
|
||||||
|
$this->assertEquals([], $this->buffer->getCurrentBufferData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRead(): void
|
||||||
|
{
|
||||||
|
$this->buffer->write('test');
|
||||||
|
$response = $this->buffer->read();
|
||||||
|
$this->assertEquals('test', $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReadNullInput(): void
|
||||||
|
{
|
||||||
|
$this->buffer->write(null);
|
||||||
|
$response = $this->buffer->read();
|
||||||
|
$this->assertEquals('', $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFlushWithInput(): void
|
||||||
|
{
|
||||||
|
$this->buffer->write('test');
|
||||||
|
$this->buffer->read();
|
||||||
|
$this->assertEquals([], $this->buffer->getCurrentBufferData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFlushWithoutInput(): void
|
||||||
|
{
|
||||||
|
$this->buffer->read();
|
||||||
|
$this->assertEquals([], $this->buffer->getCurrentBufferData());
|
||||||
|
}
|
||||||
|
}
|
16
tests/XmlTest.php
Normal file
16
tests/XmlTest.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pdahal\Xmpp\Xml\Xml;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class XmlTest extends TestCase
|
||||||
|
{
|
||||||
|
use Xml;
|
||||||
|
public $host = 'www.test.com';
|
||||||
|
|
||||||
|
public function testOpeningStream(): void
|
||||||
|
{
|
||||||
|
$expected = "<?xml version='1.0' encoding='UTF-8'?><stream:stream to='www.test.com' xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' version='1.0'>";
|
||||||
|
$this->assertEquals($expected, $this->openXmlStream($this->host));
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user