Merge pull request #59 from nextcloud/push-notification
Allow devices to register for push notifications
This commit is contained in:
Коммит
af90c15598
|
@ -4,6 +4,7 @@
|
|||
<create>true</create>
|
||||
<overwrite>false</overwrite>
|
||||
<charset>utf8</charset>
|
||||
|
||||
<table>
|
||||
<name>*dbprefix*notifications</name>
|
||||
<declaration>
|
||||
|
@ -118,4 +119,64 @@
|
|||
</index>
|
||||
</declaration>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<name>*dbprefix*notifications_pushtokens</name>
|
||||
<declaration>
|
||||
<field>
|
||||
<name>uid</name>
|
||||
<type>text</type>
|
||||
<notnull>true</notnull>
|
||||
<length>64</length>
|
||||
</field>
|
||||
<field>
|
||||
<name>token</name>
|
||||
<type>integer</type>
|
||||
<default>0</default>
|
||||
<notnull>true</notnull>
|
||||
<length>4</length>
|
||||
</field>
|
||||
<field>
|
||||
<name>deviceidentifier</name>
|
||||
<type>text</type>
|
||||
<notnull>true</notnull>
|
||||
<length>128</length>
|
||||
</field>
|
||||
<field>
|
||||
<name>devicepublickey</name>
|
||||
<type>text</type>
|
||||
<notnull>true</notnull>
|
||||
<length>512</length>
|
||||
</field>
|
||||
<field>
|
||||
<name>devicepublickeyhash</name>
|
||||
<type>text</type>
|
||||
<notnull>true</notnull>
|
||||
<length>128</length>
|
||||
</field>
|
||||
<field>
|
||||
<name>pushtokenhash</name>
|
||||
<type>text</type>
|
||||
<notnull>true</notnull>
|
||||
<length>128</length>
|
||||
</field>
|
||||
<field>
|
||||
<name>proxyserver</name>
|
||||
<type>text</type>
|
||||
<notnull>true</notnull>
|
||||
<length>256</length>
|
||||
</field>
|
||||
|
||||
<index>
|
||||
<name>oc_notifpushtoken</name>
|
||||
<unique>true</unique>
|
||||
<field>
|
||||
<name>uid</name>
|
||||
</field>
|
||||
<field>
|
||||
<name>token</name>
|
||||
</field>
|
||||
</index>
|
||||
</declaration>
|
||||
</table>
|
||||
</database>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<licence>AGPL</licence>
|
||||
<author>Joas Schilling</author>
|
||||
<version>1.2.0</version>
|
||||
<version>2.0.0</version>
|
||||
|
||||
<types>
|
||||
<logging/>
|
||||
|
|
|
@ -24,5 +24,7 @@ return [
|
|||
['name' => 'Endpoint#listNotifications', 'url' => '/api/{apiVersion}/notifications', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)']],
|
||||
['name' => 'Endpoint#getNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']],
|
||||
['name' => 'Endpoint#deleteNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']],
|
||||
['name' => 'Push#registerDevice', 'url' => '/api/{apiVersion}/push', 'verb' => 'POST', 'requirements' => ['apiVersion' => 'v2']],
|
||||
['name' => 'Push#removeDevice', 'url' => '/api/{apiVersion}/push', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => 'v2']],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
# Push notifications as a Nextcloud client device
|
||||
|
||||
|
||||
|
||||
## Checking the capabilities of the Nextcloud server
|
||||
|
||||
In order to find out if notifications support push on the server you can run a request against the capabilities endpoint: `/ocs/v2.php/cloud/capabilities`
|
||||
|
||||
```
|
||||
{
|
||||
"ocs": {
|
||||
...
|
||||
"data": {
|
||||
...
|
||||
"capabilities": {
|
||||
...
|
||||
"notifications": {
|
||||
"push": [
|
||||
...
|
||||
"devices"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Subscribing at the Nextcloud server
|
||||
|
||||
1. **Only on first registration on the server** The device generates a `rsa2048` key pair (`devicePrivateKey` and `devicePublicKey`).
|
||||
|
||||
2. The device generates the `PushToken` for *Apple Push Notification Service* (iOS) or *Firebase Cloud Messaging* (Android)
|
||||
|
||||
3. The device generates a `sha512` hash of the `PushToken` (`PushTokenHash`)
|
||||
|
||||
4. The device then sends the `devicePublicKey`, `PushTokenHash` and `proxyServerUrl` to the Nextcloud server:
|
||||
|
||||
```
|
||||
POST /ocs/v2.php/apps/notifications/api/v2/push
|
||||
|
||||
{
|
||||
"pushTokenHash": "{{PushTokenHash}}",
|
||||
"devicePublicKey": "{{devicePublicKey}}",
|
||||
"proxyServer": "{{proxyServerUrl}}"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Response
|
||||
|
||||
The server replies with the following status codes:
|
||||
|
||||
| Status code | Meaning |
|
||||
| ----------- | ---------------------------------------- |
|
||||
| 200 | No further action by the device required |
|
||||
| 201 | Push token was created/updated and **needs to be sent to the `Proxy`** |
|
||||
| 400 | Invalid device public key; device does not use a token to authenticate; the push token hash is invalid formatted; the proxy server URL is invalid; |
|
||||
| 401 | Device is not logged in |
|
||||
|
||||
|
||||
|
||||
#### Body in case of success
|
||||
|
||||
In case of `200` and `201` the reply has more information in the body:
|
||||
|
||||
| Key | Type | |
|
||||
| ---------------- | ------------ | ---------------------------------------- |
|
||||
| publicKey | string (512) | rsa2048 public key of the user account on the instance |
|
||||
| deviceIdentifier | string (128) | unique identifier encrypted with the users private key |
|
||||
| signature | string (512) | base64 encoded signature of the deviceIdentifier |
|
||||
|
||||
|
||||
|
||||
#### Body in case of an error
|
||||
|
||||
In case of `400` the following `message` can appear in the body:
|
||||
|
||||
| Error | Description |
|
||||
| ------------------------ | ---------------------------------------- |
|
||||
| `INVALID_PUSHTOKEN_HASH` | The hash of the push token was not a valid `sha512` hash. |
|
||||
| `INVALID_SESSION_TOKEN` | The authentication token of the request could not be identified. Check whether a password was used to login. |
|
||||
| `INVALID_DEVICE_KEY` | The device key does not match the one registered to the provided session token. |
|
||||
| `INVALID_PROXY_SERVER` | The proxy server was not a valid https URL. |
|
||||
|
||||
|
||||
|
||||
## Unsubcribing at the Nextcloud server
|
||||
|
||||
When an account is removed from a device, the device should unregister on the server. Otherwise the server sends unnecessary push notifications and might be blocked because of spam.
|
||||
|
||||
|
||||
|
||||
The device should then send a `DELETE` request to the Nextcloud server:
|
||||
|
||||
```
|
||||
DELETE /ocs/v2.php/apps/notifications/api/v2/push
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Response
|
||||
|
||||
The server replies with the following status codes:
|
||||
|
||||
| Status code | Meaning |
|
||||
| ----------- | ---------------------------------------- |
|
||||
| 200 | Push token was not registered on the server |
|
||||
| 202 | Push token was deleted and **needs to be deleted from the `Proxy`** |
|
||||
| 400 | Device does not use a token to authenticate |
|
||||
| 401 | Device is not logged in |
|
||||
|
||||
|
||||
|
||||
#### Body in case of an error
|
||||
|
||||
In case of `400` the following `message` can appear in the body:
|
||||
|
||||
| Error | Description |
|
||||
| ----------------------- | ---------------------------------------- |
|
||||
| `INVALID_SESSION_TOKEN` | The authentication token of the request could not be identified. |
|
||||
|
||||
|
||||
|
||||
## Subscribing at the Push Proxy
|
||||
|
||||
The device sends the`PushToken` as well as the `deviceIdentifier`, `signature` and the user´s `publicKey` (from the server´s response) to the Push Proxy:
|
||||
|
||||
```
|
||||
POST /devices
|
||||
|
||||
{
|
||||
"pushToken": "{{PushToken}}",
|
||||
"deviceIdentifier": "{{deviceIdentifier}}",
|
||||
"deviceIdentifierSignature": "{{signature}}",
|
||||
"userPublicKey": "{{userPublicKey}}"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Response
|
||||
|
||||
The server replies with the following status codes:
|
||||
|
||||
| Status code | Meaning |
|
||||
| ----------- | ---------------------------------------- |
|
||||
| 200 | Push token was written to the databse |
|
||||
| 400 | Push token, public key or device identifier is malformed, the signature does not match |
|
||||
| 403 | Device is not allowed to write the push token of the device identifier |
|
||||
| 409 | In case of a conflict the device can retry with the additional field `cloudId` with the value `{{userid}}@{{serverurl}}` which allows the proxy to verify the public key and device identifier belongs to the given user on the instance |
|
||||
|
||||
|
||||
|
||||
## Unsubscribing at the Push Proxy
|
||||
|
||||
The device sends the `deviceIdentifier`, `deviceIdentifierSignature` and the user´s `publicKey` (from the server´s response) to the Push Proxy:
|
||||
|
||||
```
|
||||
DELETE /devices
|
||||
|
||||
{
|
||||
"deviceIdentifier": "{{deviceIdentifier}}",
|
||||
"deviceIdentifierSignature": "{{signature}}",
|
||||
"userPublicKey": "{{userPublicKey}}"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Response
|
||||
|
||||
The server replies with the following status codes:
|
||||
|
||||
| Status code | Meaning |
|
||||
| ----------- | ---------------------------------------- |
|
||||
| 200 | Push token was deleted from the database |
|
||||
| 400 | Public key or device identifier is malformed |
|
||||
| 403 | Device identifier and device public key didn't match or could not be found |
|
||||
|
||||
|
||||
|
||||
## Pushed notifications
|
||||
|
||||
The pushed notifications is defined by the [Firebase Cloud Messaging HTTP Protocol](https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream). The sample content of a Nextcloud push notification looks like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"to" : "APA91bHun4MxP5egoKMwt2KZFBaFUH-1RYqx...",
|
||||
"notification" : {
|
||||
"body" : "NEW_NOTIFICATION",
|
||||
"body_loc_key" : "NEW_NOTIFICATION",
|
||||
"title" : "NEW_NOTIFICATION",
|
||||
"title_loc_key" : "NEW_NOTIFICATION"
|
||||
},
|
||||
"data" : {
|
||||
"subject" : "*Encrypted subject*",
|
||||
"signature" : "*Signature*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Attribute | Meaning |
|
||||
| ----------- | ---------------------------------------- |
|
||||
| `subject` | The subject is encrypted with the device´s *public key*. |
|
||||
| `signature` | The signature is a sha512 signature over the encrypted subject using the user´s private key. |
|
||||
|
||||
### Verification
|
||||
So a device should verify the signature using the user´s public key.
|
||||
If the signature is okay, the subject can be decrypted using the device´s private key.
|
10
lib/App.php
10
lib/App.php
|
@ -28,9 +28,12 @@ use OCP\Notification\INotification;
|
|||
class App implements IApp {
|
||||
/** @var Handler */
|
||||
protected $handler;
|
||||
/** @var Push */
|
||||
protected $push;
|
||||
|
||||
public function __construct(Handler $handler) {
|
||||
public function __construct(Handler $handler, Push $push) {
|
||||
$this->handler = $handler;
|
||||
$this->push = $push;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,7 +42,10 @@ class App implements IApp {
|
|||
* @since 8.2.0
|
||||
*/
|
||||
public function notify(INotification $notification) {
|
||||
$this->handler->add($notification);
|
||||
$notificationId = $this->handler->add($notification);
|
||||
|
||||
$notificationToPush = $this->handler->getById($notificationId, $notification->getUser());
|
||||
$this->push->pushToDevice($notificationToPush);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,9 +21,11 @@
|
|||
|
||||
namespace OCA\Notifications\AppInfo;
|
||||
|
||||
use OC\Authentication\Token\IProvider;
|
||||
use OCA\Notifications\App;
|
||||
use OCA\Notifications\Capabilities;
|
||||
use OCA\Notifications\Controller\EndpointController;
|
||||
use OCP\AppFramework\IAppContainer;
|
||||
use OCP\Util;
|
||||
|
||||
class Application extends \OCP\AppFramework\App {
|
||||
|
@ -31,8 +33,12 @@ class Application extends \OCP\AppFramework\App {
|
|||
parent::__construct('notifications');
|
||||
$container = $this->getContainer();
|
||||
|
||||
$container->registerAlias('EndpointController', EndpointController::class);
|
||||
$container->registerCapability(Capabilities::class);
|
||||
|
||||
// FIXME this is for automatic DI because it is not in DIContainer
|
||||
$container->registerService(IProvider::class, function(IAppContainer $c) {
|
||||
return $c->getServer()->query(IProvider::class);
|
||||
});
|
||||
}
|
||||
|
||||
public function register() {
|
||||
|
|
|
@ -45,6 +45,9 @@ class Capabilities implements ICapability {
|
|||
'icons',
|
||||
'rich-strings',
|
||||
],
|
||||
'push' => [
|
||||
'devices',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,242 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Notifications\Controller;
|
||||
|
||||
use OC\Authentication\Exceptions\InvalidTokenException;
|
||||
use OC\Authentication\Token\IProvider;
|
||||
use OC\Authentication\Token\IToken;
|
||||
use OC\Security\IdentityProof\Manager;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IRequest;
|
||||
use OCP\ISession;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class PushController extends OCSController {
|
||||
|
||||
/** @var IDBConnection */
|
||||
private $db;
|
||||
|
||||
/** @var ISession */
|
||||
private $session;
|
||||
|
||||
/** @var IUserSession */
|
||||
private $userSession;
|
||||
|
||||
/** @var IProvider */
|
||||
private $tokenProvider;
|
||||
|
||||
/** @var Manager */
|
||||
private $identityProof;
|
||||
|
||||
/**
|
||||
* @param string $appName
|
||||
* @param IRequest $request
|
||||
* @param IDBConnection $db
|
||||
* @param ISession $session
|
||||
* @param IUserSession $userSession
|
||||
* @param IProvider $tokenProvider
|
||||
* @param Manager $identityProof
|
||||
*/
|
||||
public function __construct($appName, IRequest $request, IDBConnection $db, ISession $session, IUserSession $userSession, IProvider $tokenProvider, Manager $identityProof) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->db = $db;
|
||||
$this->session = $session;
|
||||
$this->userSession = $userSession;
|
||||
$this->tokenProvider = $tokenProvider;
|
||||
$this->identityProof = $identityProof;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @param string $pushTokenHash
|
||||
* @param string $devicePublicKey
|
||||
* @param string $proxyServer
|
||||
* @return DataResponse
|
||||
*/
|
||||
public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) {
|
||||
$user = $this->userSession->getUser();
|
||||
if (!$user instanceof IUser) {
|
||||
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (!preg_match('/^([a-f0-9]{128})$/', $pushTokenHash)) {
|
||||
return new DataResponse(['message' => 'INVALID_PUSHTOKEN_HASH'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (
|
||||
((strlen($devicePublicKey) !== 450 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----') !== 425) &&
|
||||
(strlen($devicePublicKey) !== 451 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----' . "\n") !== 425)) ||
|
||||
strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----' . "\n") !== 0) {
|
||||
return new DataResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (
|
||||
!filter_var($proxyServer, FILTER_VALIDATE_URL) ||
|
||||
strlen($proxyServer) > 256 ||
|
||||
!preg_match('/^(https\:\/\/|http\:\/\/localhost(\:[0-9]{0,5})?\/)/', $proxyServer)
|
||||
) {
|
||||
return new DataResponse(['message' => 'INVALID_PROXY_SERVER'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$tokenId = $this->session->get('token-id');
|
||||
try {
|
||||
$token = $this->tokenProvider->getTokenById($tokenId);
|
||||
} catch (InvalidTokenException $e) {
|
||||
return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$key = $this->identityProof->getKey($user);
|
||||
|
||||
$deviceIdentifier = json_encode([$user->getCloudId(), $token->getId()]);
|
||||
openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512);
|
||||
$deviceIdentifier = base64_encode(hash('sha512', $deviceIdentifier, true));
|
||||
|
||||
$created = $this->savePushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer);
|
||||
|
||||
return new DataResponse([
|
||||
'publicKey' => $key->getPublic(),
|
||||
'deviceIdentifier' => $deviceIdentifier,
|
||||
'signature' => base64_encode($signature),
|
||||
], $created ? Http::STATUS_CREATED : Http::STATUS_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @return DataResponse
|
||||
*/
|
||||
public function removeDevice() {
|
||||
$user = $this->userSession->getUser();
|
||||
if (!$user instanceof IUser) {
|
||||
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$tokenId = $this->session->get('token-id');
|
||||
try {
|
||||
$token = $this->tokenProvider->getTokenById($tokenId);
|
||||
} catch (InvalidTokenException $e) {
|
||||
return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if ($this->deletePushToken($user, $token)) {
|
||||
return new DataResponse([], Http::STATUS_ACCEPTED);
|
||||
}
|
||||
|
||||
return new DataResponse([], Http::STATUS_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IUser $user
|
||||
* @param IToken $token
|
||||
* @param string $deviceIdentifier
|
||||
* @param string $devicePublicKey
|
||||
* @param string $pushTokenHash
|
||||
* @param string $proxyServer
|
||||
* @return bool If the hash was new to the database
|
||||
*/
|
||||
protected function savePushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer) {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select('*')
|
||||
->from('notifications_pushtokens')
|
||||
->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
|
||||
->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId())));
|
||||
$result = $query->execute();
|
||||
$row = $result->fetch();
|
||||
$result->closeCursor();
|
||||
|
||||
if (!$row) {
|
||||
return $this->insertPushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer);
|
||||
}
|
||||
|
||||
return $this->updatePushToken($user, $token, $devicePublicKey, $pushTokenHash, $proxyServer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IUser $user
|
||||
* @param IToken $token
|
||||
* @param string $deviceIdentifier
|
||||
* @param string $devicePublicKey
|
||||
* @param string $pushTokenHash
|
||||
* @param string $proxyServer
|
||||
* @return bool If the entry was created
|
||||
*/
|
||||
protected function insertPushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer) {
|
||||
$devicePublicKeyHash = hash('sha512', $devicePublicKey);
|
||||
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->insert('notifications_pushtokens')
|
||||
->values([
|
||||
'uid' => $query->createNamedParameter($user->getUID()),
|
||||
'token' => $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT),
|
||||
'deviceidentifier' => $query->createNamedParameter($deviceIdentifier),
|
||||
'devicepublickey' => $query->createNamedParameter($devicePublicKey),
|
||||
'devicepublickeyhash' => $query->createNamedParameter($devicePublicKeyHash),
|
||||
'pushtokenhash' => $query->createNamedParameter($pushTokenHash),
|
||||
'proxyserver' => $query->createNamedParameter($proxyServer),
|
||||
]);
|
||||
return $query->execute() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IUser $user
|
||||
* @param IToken $token
|
||||
* @param string $devicePublicKey
|
||||
* @param string $pushTokenHash
|
||||
* @param string $proxyServer
|
||||
* @return bool If the entry was updated
|
||||
*/
|
||||
protected function updatePushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash, $proxyServer) {
|
||||
$devicePublicKeyHash = hash('sha512', $devicePublicKey);
|
||||
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->update('notifications_pushtokens')
|
||||
->set('devicepublickey', $query->createNamedParameter($devicePublicKey))
|
||||
->set('devicepublickeyhash', $query->createNamedParameter($devicePublicKeyHash))
|
||||
->set('pushtokenhash', $query->createNamedParameter($pushTokenHash))
|
||||
->set('proxyserver', $query->createNamedParameter($proxyServer))
|
||||
->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
|
||||
->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT)));
|
||||
|
||||
return $query->execute() !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IUser $user
|
||||
* @param IToken $token
|
||||
* @return bool If the entry was deleted
|
||||
*/
|
||||
protected function deletePushToken(IUser $user, IToken $token) {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->delete('notifications_pushtokens')
|
||||
->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
|
||||
->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT)));
|
||||
|
||||
return $query->execute() !== 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Notifications;
|
||||
|
||||
|
||||
use OC\Authentication\Exceptions\InvalidTokenException;
|
||||
use OC\Authentication\Token\IProvider;
|
||||
use OC\Security\IdentityProof\Key;
|
||||
use OC\Security\IdentityProof\Manager;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\ILogger;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Notification\IManager as INotificationManager;
|
||||
use OCP\Notification\INotification;
|
||||
|
||||
class Push {
|
||||
/** @var IDBConnection */
|
||||
protected $db;
|
||||
/** @var INotificationManager */
|
||||
protected $notificationManager;
|
||||
/** @var IConfig */
|
||||
protected $config;
|
||||
/** @var IProvider */
|
||||
protected $tokenProvider;
|
||||
/** @var Manager */
|
||||
private $keyManager;
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
/** @var IClientService */
|
||||
protected $clientService;
|
||||
/** @var ILogger */
|
||||
protected $log;
|
||||
|
||||
public function __construct(IDBConnection $connection, INotificationManager $notificationManager, IConfig $config, IProvider $tokenProvider, Manager $keyManager, IUserManager $userManager, IClientService $clientService, ILogger $log) {
|
||||
$this->db = $connection;
|
||||
$this->notificationManager = $notificationManager;
|
||||
$this->config = $config;
|
||||
$this->tokenProvider = $tokenProvider;
|
||||
$this->keyManager = $keyManager;
|
||||
$this->userManager = $userManager;
|
||||
$this->clientService = $clientService;
|
||||
$this->log = $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param INotification $notification
|
||||
*/
|
||||
public function pushToDevice(INotification $notification) {
|
||||
$user = $this->userManager->get($notification->getUser());
|
||||
if (!($user instanceof IUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$devices = $this->getDevicesForUser($notification->getUser());
|
||||
if (empty($devices)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$language = $this->config->getUserValue($notification->getUser(), 'core', 'lang', 'en');
|
||||
try {
|
||||
$notification = $this->notificationManager->prepare($notification, $language);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userKey = $this->keyManager->getKey($user);
|
||||
|
||||
$pushNotifications = [];
|
||||
foreach ($devices as $device) {
|
||||
try {
|
||||
$payload = json_encode($this->encryptAndSign($userKey, $device, $notification));
|
||||
|
||||
$proxyServer = rtrim($device['proxyserver'], '/');
|
||||
if (!isset($pushNotifications[$proxyServer])) {
|
||||
$pushNotifications[$proxyServer] = [];
|
||||
}
|
||||
$pushNotifications[$proxyServer][] = $payload;
|
||||
} catch (InvalidTokenException $e) {
|
||||
// Token does not exist anymore, should drop the push device entry
|
||||
$this->deletePushToken($device['token']);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Failed to encrypt message for device: public key is invalid
|
||||
$this->deletePushToken($device['token']);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($pushNotifications)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$client = $this->clientService->newClient();
|
||||
foreach ($pushNotifications as $proxyServer => $notifications) {
|
||||
try {
|
||||
$response = $client->post($proxyServer . '/notifications', [
|
||||
'body' => [
|
||||
'notifications' => $notifications,
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->log->logException($e, [
|
||||
'app' => 'notifications',
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$status = $response->getStatusCode();
|
||||
if ($status !== Http::STATUS_OK && $status !== Http::STATUS_SERVICE_UNAVAILABLE) {
|
||||
$body = $response->getBody();
|
||||
$this->log->error('Could not send notification to push server [{url}]: {error}',[
|
||||
'error' => is_string($body) ? $body : 'no reason given',
|
||||
'url' => $proxyServer,
|
||||
'app' => 'notifications',
|
||||
]);
|
||||
} else if ($status === Http::STATUS_SERVICE_UNAVAILABLE && $this->config->getSystemValue('debug', false)) {
|
||||
$body = $response->getBody();
|
||||
$this->log->debug('Could not send notification to push server [{url}]: {error}',[
|
||||
'error' => is_string($body) ? $body : 'no reason given',
|
||||
'url' => $proxyServer,
|
||||
'app' => 'notifications',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Key $userKey
|
||||
* @param array $device
|
||||
* @param INotification $notification
|
||||
* @return array
|
||||
* @throws InvalidTokenException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function encryptAndSign(Key $userKey, array $device, INotification $notification) {
|
||||
// Check if the token is still valid...
|
||||
$this->tokenProvider->getTokenById($device['token']);
|
||||
|
||||
$data = [
|
||||
'app' => $notification->getApp(),
|
||||
'subject' => $notification->getParsedSubject(),
|
||||
];
|
||||
|
||||
if (!openssl_public_encrypt(json_encode($data), $encryptedSubject, $device['devicepublickey'], OPENSSL_PKCS1_PADDING)) {
|
||||
$this->log->error(openssl_error_string(), ['app' => 'notifications']);
|
||||
throw new \InvalidArgumentException('Failed to encrypt message for device');
|
||||
}
|
||||
|
||||
openssl_sign($encryptedSubject, $signature, $userKey->getPrivate(), OPENSSL_ALGO_SHA512);
|
||||
$base64EncryptedSubject = base64_encode(hash('sha512', $encryptedSubject, true));
|
||||
$base64Signature = base64_encode($signature);
|
||||
|
||||
return [
|
||||
'deviceIdentifier' => $device['deviceidentifier'],
|
||||
'pushTokenHash' => $device['pushtokenhash'],
|
||||
'subject' => $base64EncryptedSubject,
|
||||
'signature' => $base64Signature,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uid
|
||||
* @return array[]
|
||||
*/
|
||||
protected function getDevicesForUser($uid) {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select('*')
|
||||
->from('notifications_pushtokens')
|
||||
->where($query->expr()->eq('uid', $query->createNamedParameter($uid)));
|
||||
|
||||
$result = $query->execute();
|
||||
$devices = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
|
||||
return $devices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $tokenId
|
||||
* @return bool
|
||||
*/
|
||||
protected function deletePushToken($tokenId) {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->delete('notifications_pushtokens')
|
||||
->where($query->expr()->eq('token', $query->createNamedParameter($tokenId, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
return $query->execute() !== 0;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
namespace OCA\Notifications\Tests\Unit\AppInfo;
|
||||
|
||||
use OC\User\Session;
|
||||
use OCA\Notifications\App;
|
||||
use OCA\Notifications\Tests\Unit\TestCase;
|
||||
use OCP\IRequest;
|
||||
|
@ -46,14 +47,9 @@ class AppTest extends TestCase {
|
|||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->manager = $this->getMockBuilder(IManager::class)
|
||||
->getMock();
|
||||
|
||||
$this->request = $this->getMockBuilder(IRequest::class)
|
||||
->getMock();
|
||||
|
||||
$this->session = $this->getMockBuilder(IUserSession::class)
|
||||
->getMock();
|
||||
$this->manager = $this->createMock(IManager::class);
|
||||
$this->request = $this->createMock(IRequest::class);
|
||||
$this->session = $this->createMock(Session::class);
|
||||
|
||||
$this->overwriteService('NotificationManager', $this->manager);
|
||||
$this->overwriteService('Request', $this->request);
|
||||
|
|
|
@ -26,7 +26,9 @@ use OCA\Notifications\App;
|
|||
use OCA\Notifications\AppInfo\Application;
|
||||
use OCA\Notifications\Capabilities;
|
||||
use OCA\Notifications\Controller\EndpointController;
|
||||
use OCA\Notifications\Controller\PushController;
|
||||
use OCA\Notifications\Handler;
|
||||
use OCA\Notifications\Push;
|
||||
use OCA\Notifications\Tests\Unit\TestCase;
|
||||
use OCP\AppFramework\IAppContainer;
|
||||
use OCP\AppFramework\OCSController;
|
||||
|
@ -63,12 +65,13 @@ class ApplicationTest extends TestCase {
|
|||
array(App::class, IApp::class),
|
||||
array(Capabilities::class),
|
||||
array(Handler::class),
|
||||
array(Push::class),
|
||||
|
||||
// Controller/
|
||||
array('EndpointController', EndpointController::class),
|
||||
array('EndpointController', OCSController::class),
|
||||
array(EndpointController::class),
|
||||
array(EndpointController::class, OCSController::class),
|
||||
array(PushController::class),
|
||||
array(PushController::class, OCSController::class),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,6 @@ class RoutesTest extends TestCase {
|
|||
$this->assertCount(1, $routes);
|
||||
$this->assertArrayHasKey('ocs', $routes);
|
||||
$this->assertInternalType('array', $routes['ocs']);
|
||||
$this->assertGreaterThanOrEqual(3, sizeof($routes['ocs']));
|
||||
$this->assertCount(5, $routes['ocs']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,12 +25,14 @@ namespace OCA\Notifications\Tests\Unit;
|
|||
|
||||
use OCA\Notifications\App;
|
||||
use OCA\Notifications\Handler;
|
||||
use OCA\Notifications\Push;
|
||||
use OCP\Notification\INotification;
|
||||
|
||||
class AppTest extends TestCase {
|
||||
/** @var Handler|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $handler;
|
||||
|
||||
/** @var Push|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $push;
|
||||
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $notification;
|
||||
|
||||
|
@ -40,34 +42,68 @@ class AppTest extends TestCase {
|
|||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->handler = $this->getMockBuilder(Handler::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$this->notification = $this->getMockBuilder(INotification::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$this->handler = $this->createMock(Handler::class);
|
||||
$this->push = $this->createMock(Push::class);
|
||||
$this->notification = $this->createMock(INotification::class);
|
||||
|
||||
$this->app = new App(
|
||||
$this->handler
|
||||
$this->handler,
|
||||
$this->push
|
||||
);
|
||||
}
|
||||
|
||||
public function testNotify() {
|
||||
public function dataNotify() {
|
||||
return [
|
||||
[23, 'user1'],
|
||||
[42, 'user2'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataNotify
|
||||
*
|
||||
* @param int $id
|
||||
* @param string $user
|
||||
*/
|
||||
public function testNotify($id, $user) {
|
||||
$this->notification->expects($this->once())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
|
||||
$this->handler->expects($this->once())
|
||||
->method('add')
|
||||
->with($this->notification)
|
||||
->willReturn($id);
|
||||
$this->handler->expects($this->once())
|
||||
->method('getById')
|
||||
->with($id, $user)
|
||||
->willReturn($this->notification);
|
||||
$this->push->expects($this->once())
|
||||
->method('pushToDevice')
|
||||
->with($this->notification);
|
||||
|
||||
$this->app->notify($this->notification);
|
||||
}
|
||||
|
||||
public function testGetCount() {
|
||||
public function dataGetCount() {
|
||||
return [
|
||||
[23],
|
||||
[42],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataGetCount
|
||||
*
|
||||
* @param int $count
|
||||
*/
|
||||
public function testGetCount($count) {
|
||||
$this->handler->expects($this->once())
|
||||
->method('count')
|
||||
->with($this->notification)
|
||||
->willReturn(42);
|
||||
->willReturn($count);
|
||||
|
||||
$this->assertSame(42, $this->app->getCount($this->notification));
|
||||
$this->assertSame($count, $this->app->getCount($this->notification));
|
||||
}
|
||||
|
||||
public function testMarkProcessed() {
|
||||
|
|
|
@ -38,6 +38,9 @@ class CapabilitiesTest extends TestCase {
|
|||
'icons',
|
||||
'rich-strings',
|
||||
],
|
||||
'push' => [
|
||||
'devices',
|
||||
],
|
||||
],
|
||||
], $capabilities->getCapabilities());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,508 @@
|
|||
<?php
|
||||
/**
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
||||
*
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Notifications\Tests\Unit\Controller;
|
||||
|
||||
use OC\Authentication\Exceptions\InvalidTokenException;
|
||||
use OC\Authentication\Token\IProvider;
|
||||
use OC\Authentication\Token\IToken;
|
||||
use OC\Security\IdentityProof\Key;
|
||||
use OC\Security\IdentityProof\Manager;
|
||||
use OCA\Notifications\Controller\PushController;
|
||||
use OCA\Notifications\Tests\Unit\TestCase;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IRequest;
|
||||
use OCP\ISession;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class PushControllerTest extends TestCase {
|
||||
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $request;
|
||||
/** @var IDBConnection|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $db;
|
||||
/** @var ISession|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $session;
|
||||
/** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $userSession;
|
||||
/** @var IProvider|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $tokenProvider;
|
||||
/** @var Manager|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $identityProof;
|
||||
|
||||
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $user;
|
||||
/** @var PushController */
|
||||
protected $controller;
|
||||
|
||||
protected $devicePublicKey = '-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Or1KumSDfk8dT0MuCW9
|
||||
WS5wkVOpNsbz2OIJFBYrBvu6joC2iQo9StONMaXoTQj5Ucak9UBtC60PHyTkIDFb
|
||||
HOpCST5onmIAtZdqHN/3ABOBeHVU/notdRIl/menGM64jiqGWvE06F1+yZ8GGcGQ
|
||||
8RKzabqMd2K1iUohXP625uzTABVaiwz3u8nGEwui5R6Pf5Fy6DccuqdUMtJIfW21
|
||||
Z4Tj48Tw+pR+fUrGpa1Wg+wiwlg7ISK8Symml1Rd6hSRXK2t8Opm/kjH9ZX8oVwn
|
||||
RSO1ehjzRpTY+gdw/5gvwMZI0XmrIanZmZHwePRR4HC6FLPrL2OQG3gWikDIPyTS
|
||||
hQIDAQAB
|
||||
-----END PUBLIC KEY-----';
|
||||
|
||||
protected $userPrivateKey = '-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDPR0uV6e1cNSoy
|
||||
vsITBvGyYpOIn9vI7zpEhk7FGGwdOTd2dxxJ2ikegRJ6Fr2Ojce15K3zfiasXPen
|
||||
TAQuFEXecGoP9WY+DS5X1LfCpj9EeAOBfVGKeQDst5z/GoXeU+YqWbayJTp6vFRj
|
||||
7o5X6QDCCXy25Kt4snNDWTHPlMc44BLjZ6w+Wj0D2ySlz1dGpunc0vwYN/uEyjr9
|
||||
ztmiN82TZtZHgzN43DJSv7tLufsZgGsWnVlytXmsi4QuCAKcm92X2ZtIXkn5niMW
|
||||
DxJJepqFx7pC3ILXMZKYolAtt91VvLiGQjzURhq7HA4QdqvFyKXp0uLN2rKZjqQ0
|
||||
2nUzC34XAgMBAAECggEAFrL/Ew7IIKXt1hrP1BeZlmh3MaoX/pw8LE7tB2aSSG0A
|
||||
pueKYIgUorON23LsFVVvfnrpldXF1HBl6ptHhehQcnirFM5SAQ+eeJ3h9d4Q5aWi
|
||||
9KZNrLVtpX7CIam86UkU1qR2fnHXQqOnNj5ktjndDGLPlpPaN2CLgN+etdXcL10g
|
||||
G5fltrFnTzYgkYap/eNkY+ivA+0xqc1l3jP2i5PHihv1adcoiOuam36GARM9C51X
|
||||
fyWvMtxMvkRAZsdTATtRcQsEoJuQ3Rvseei38forkQdRn9p61UW8VT6Wa/+DWebO
|
||||
Ll4OAv1RH4H2V6nrYY2ILJNnPzP8V4hjP9OGEAUQ8QKBgQDssSBUmb8Ztt6SsHNr
|
||||
fgnbJBGAYizB1oAr6W1kLTQCq+BYirSYWMcJ/rakx+VCPmZ1fbbGYjPX5yVUsskx
|
||||
jQ/GUT7D8lMIQNZiI9CqWR0+fJpVJ/zxwrPT2jqu8lEJxq2i/WB0nRHCgosGBTmw
|
||||
UqhRGLkE5Ds14Q0zePZbdpAAyQKBgQDgL+yftcJEam8c3ipkrv02aT7vghoB0pAg
|
||||
JNSSwhXED1CTboccY4daOfTYdt/PnkVmndENrUGMRyEbAY0DDK6hclG6/gE3fwn4
|
||||
mL33IIzQ9BCoXxr3tcS0r4iQjbGKorUNJW1OwmkqyMZ4POF9BSkLXpTTcJaM5WxU
|
||||
8JU9PmLX3wKBgFNpuLMX27j8MUQQ2xwuttp7w48zCgLlzRWsldiP9ZxbZhzOBQcL
|
||||
glmLYmJ/79OAmisduqP/R7X2x7kpqK3FwKFrUGtNouVttB+x73+ZGC1FTD5mcUXi
|
||||
D+3BIp002EpRsi+Wi7+M+w1JZCUjAkmZV6f8xndq11MNlNFm96sUBXvBAoGAJ9hc
|
||||
tgYYARDprrfN0RdI6eLKzMbS2IAUHaJuJadZNv+B0rJSUTlfVSn32oFGRiBbNWHX
|
||||
RhcFD2mU+LfN2DzozMkEvbdnf/WUUBrVqJagcILwcvx0TpJ/451PKGIGrB0/EJcW
|
||||
Vmk3R+NnYvdvHElOgjbNPMdF+sTL/EzGOZxc9QECgYBNY4LAAKqrw47p+lcRi31O
|
||||
X4fhdGWAIFyiUliPDkxzEl8857FbT5c6qhdes3Gyc9tSF1wh0X7lpCDquWXYLP1V
|
||||
9WNvdon+YMRi9BKpO0SlE07lwFANBpz+wJkhONVJBMzvKbxEnMRPRJ4lWa0VAAGE
|
||||
j2ZL3j2Nwefj3HrR/AkeFA==
|
||||
-----END PRIVATE KEY-----
|
||||
';
|
||||
|
||||
protected $userPublicKey = '-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz0dLlentXDUqMr7CEwbx
|
||||
smKTiJ/byO86RIZOxRhsHTk3dnccSdopHoESeha9jo3HteSt834mrFz3p0wELhRF
|
||||
3nBqD/VmPg0uV9S3wqY/RHgDgX1RinkA7Lec/xqF3lPmKlm2siU6erxUY+6OV+kA
|
||||
wgl8tuSreLJzQ1kxz5THOOAS42esPlo9A9skpc9XRqbp3NL8GDf7hMo6/c7ZojfN
|
||||
k2bWR4MzeNwyUr+7S7n7GYBrFp1ZcrV5rIuELggCnJvdl9mbSF5J+Z4jFg8SSXqa
|
||||
hce6QtyC1zGSmKJQLbfdVby4hkI81EYauxwOEHarxcil6dLizdqymY6kNNp1Mwt+
|
||||
FwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
';
|
||||
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->request = $this->createMock(IRequest::class);
|
||||
$this->db = $this->createMock(IDBConnection::class);
|
||||
$this->session = $this->createMock(ISession::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->tokenProvider = $this->createMock(IProvider::class);
|
||||
$this->identityProof = $this->createMock(Manager::class);
|
||||
}
|
||||
|
||||
protected function getController(array $methods = []) {
|
||||
if (empty($methods)) {
|
||||
return new PushController(
|
||||
'notifications',
|
||||
$this->request,
|
||||
$this->db,
|
||||
$this->session,
|
||||
$this->userSession,
|
||||
$this->tokenProvider,
|
||||
$this->identityProof
|
||||
);
|
||||
}
|
||||
|
||||
return $this->getMockBuilder(PushController::class)
|
||||
->setConstructorArgs([
|
||||
'notifications',
|
||||
$this->request,
|
||||
$this->db,
|
||||
$this->session,
|
||||
$this->userSession,
|
||||
$this->tokenProvider,
|
||||
$this->identityProof,
|
||||
])
|
||||
->setMethods($methods)
|
||||
->getMock();
|
||||
}
|
||||
|
||||
public function dataRegisterDevice() {
|
||||
return [
|
||||
'not authenticated' => [
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
[],
|
||||
Http::STATUS_UNAUTHORIZED
|
||||
],
|
||||
'too short token hash' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e47',
|
||||
'',
|
||||
'',
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_PUSHTOKEN_HASH'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'too long token hash' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e4722',
|
||||
'',
|
||||
'',
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_PUSHTOKEN_HASH'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'invalid char in token hash' => [
|
||||
'rb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
'',
|
||||
'',
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_PUSHTOKEN_HASH'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'device key invalid start' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
substr($this->devicePublicKey, 1),
|
||||
'',
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_DEVICE_KEY'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'device key invalid end' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
substr($this->devicePublicKey, 0, -1),
|
||||
'',
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_DEVICE_KEY'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'device key too much end' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
$this->devicePublicKey . "\n\n",
|
||||
'',
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_DEVICE_KEY'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'device key without trailing new line' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
$this->devicePublicKey,
|
||||
'',
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_PROXY_SERVER'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'device key with trailing new line' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
$this->devicePublicKey . "\n",
|
||||
'',
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_PROXY_SERVER'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'invalid push proxy' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
$this->devicePublicKey,
|
||||
'localhost',
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_PROXY_SERVER'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'using localhost' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
$this->devicePublicKey,
|
||||
'http://localhost/',
|
||||
true,
|
||||
23,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_SESSION_TOKEN'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'using localhost with port' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
$this->devicePublicKey,
|
||||
'http://localhost:8088/',
|
||||
true,
|
||||
23,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_SESSION_TOKEN'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'using production' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
$this->devicePublicKey,
|
||||
'https://push-notifications.nextcloud.com/',
|
||||
true,
|
||||
23,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_SESSION_TOKEN'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'created or updated' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
$this->devicePublicKey,
|
||||
'https://push-notifications.nextcloud.com/',
|
||||
true,
|
||||
23,
|
||||
true,
|
||||
true,
|
||||
[
|
||||
'publicKey' => $this->userPublicKey,
|
||||
'deviceIdentifier' => 'XUCEZ1EHvTUcVhIvrQQQ1XcP0ZD2BFdFqw4EYbOhBfiEgXgirurR4x/ve4GSSyfivvbQOdOkZUM+g4m+tSb0Ew==',
|
||||
'signature' => 'LRhbXO71WYX9qqDbQX7C+87YaaFfWoT/vG0DlaXdBz6+lhyOA0dw/1Ggz3fd7RerCQ0MfgnnTyxO+cSeRpUaPdA2yPjfoiPpfYA5SOJQGF3comS/HYna3fHiFDbOoM3BJOnjvqiSZdxA/ICdyl2mEEC5wO7AZ4OZKBTa5XfL7eSCXZLEv1YldqcLOStbXrI7voDQocTMJxoQZI/j8BVcf2i3D6F454aXIFDrYYzC2PQY+CKJoXZW0m0RMWaTM2B8tBmFFwrmaGLDqcjjpd33TsTtsV5DB7WimffLBPpOuGV4Z1Kiagp/mxpPLz2NImNV79mDX9gY3ZppCZTwChP5qQ==',
|
||||
],
|
||||
Http::STATUS_CREATED,
|
||||
],
|
||||
'not updated' => [
|
||||
'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
|
||||
$this->devicePublicKey,
|
||||
'https://push-notifications.nextcloud.com/',
|
||||
true,
|
||||
42,
|
||||
true,
|
||||
false,
|
||||
[
|
||||
'publicKey' => $this->userPublicKey,
|
||||
'deviceIdentifier' => 'x9vSImcGjhzR9BfZ/XbbUqqCCNC4bHKsX7vkQWNZRd1/MiY+OuF02fx8K08My0RpkNnwj/rQ/gVSU1oEdFwkww==',
|
||||
'signature' => 'J9AcdJt5youJmMnBhS+Cc9ytArynIKtCRoNf/m0oOFO/e0hWHqs1NRdQBe81qzYIjf0+bj0Q97X9Xv1rnVJesPkQUbGaa4nAPt+viGSfvzTptjX4LKgqm8B3UkduBA262IcaWgM5P84gUqelkQIC1nIqq/MJTuC6oQ5lUwIV1a92ZurDjhwH4b3f7/ZLTTOTRD0DWN9W/yOyF1qECivgePR3eu+mkcBzXVU/TDZDJic9G7xhqcTnWV6qk+aKyzdNo1tu5W7mF+v5vF6rrGZrq55vPLWAHApTD7P+NFV01BnaCuN7/qGJNVs7m7EH03jpOw7y3jqNMmcmonYrJSMVqg==',
|
||||
],
|
||||
Http::STATUS_OK,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataRegisterDevice
|
||||
*
|
||||
* @param string $pushTokenHash
|
||||
* @param string $devicePublicKey
|
||||
* @param string $proxyServer
|
||||
* @param bool $userIsValid
|
||||
* @param int $tokenId
|
||||
* @param bool $tokenIsValid
|
||||
* @param bool $deviceCreated
|
||||
* @param array $payload
|
||||
* @param int $status
|
||||
*/
|
||||
public function testRegisterDevice($pushTokenHash, $devicePublicKey, $proxyServer, $userIsValid, $tokenId, $tokenIsValid, $deviceCreated, $payload, $status) {
|
||||
$controller = $this->getController([
|
||||
'savePushToken',
|
||||
]);
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
if ($userIsValid) {
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
} else {
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
->willReturn(null);
|
||||
}
|
||||
|
||||
$this->session->expects($tokenId > 0 ? $this->once() : $this->never())
|
||||
->method('get')
|
||||
->with('token-id')
|
||||
->willReturn($tokenId);
|
||||
|
||||
if ($tokenIsValid) {
|
||||
$token = $this->createMock(IToken::class);
|
||||
$token->expects($this->once())
|
||||
->method('getId')
|
||||
->willReturn($tokenId);
|
||||
$this->tokenProvider->expects($this->any())
|
||||
->method('getTokenById')
|
||||
->with($tokenId)
|
||||
->willReturn($token);
|
||||
|
||||
$key = $this->createMock(Key::class);
|
||||
$key->expects($this->once())
|
||||
->method('getPrivate')
|
||||
->willReturn($this->userPrivateKey);
|
||||
$key->expects($this->once())
|
||||
->method('getPublic')
|
||||
->willReturn($this->userPublicKey);
|
||||
|
||||
$this->identityProof->expects($this->once())
|
||||
->method('getKey')
|
||||
->with($user)
|
||||
->willReturn($key);
|
||||
|
||||
$controller->expects($this->once())
|
||||
->method('savePushToken')
|
||||
->with($user, $token, $this->anything(), $devicePublicKey, $pushTokenHash, $proxyServer)
|
||||
->willReturn($deviceCreated);
|
||||
} else {
|
||||
$controller->expects($this->never())
|
||||
->method('savePushToken');
|
||||
|
||||
$this->tokenProvider->expects($this->any())
|
||||
->method('getTokenById')
|
||||
->with($tokenId)
|
||||
->willThrowException(new InvalidTokenException());
|
||||
}
|
||||
|
||||
$response = $controller->registerDevice($pushTokenHash, $devicePublicKey, $proxyServer);
|
||||
$this->assertInstanceOf(DataResponse::class, $response);
|
||||
$this->assertSame($status, $response->getStatus());
|
||||
$this->assertSame($payload, $response->getData());
|
||||
}
|
||||
|
||||
public function dataRemoveDevice() {
|
||||
return [
|
||||
'not authenticated' => [
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
[],
|
||||
Http::STATUS_UNAUTHORIZED
|
||||
],
|
||||
'invalid token' => [
|
||||
true,
|
||||
23,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_SESSION_TOKEN'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'using production' => [
|
||||
true,
|
||||
23,
|
||||
false,
|
||||
null,
|
||||
['message' => 'INVALID_SESSION_TOKEN'],
|
||||
Http::STATUS_BAD_REQUEST,
|
||||
],
|
||||
'created or updated' => [
|
||||
true,
|
||||
23,
|
||||
true,
|
||||
true,
|
||||
[],
|
||||
Http::STATUS_ACCEPTED,
|
||||
],
|
||||
'not updated' => [
|
||||
true,
|
||||
42,
|
||||
true,
|
||||
false,
|
||||
[],
|
||||
Http::STATUS_OK,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @dataProvider dataRemoveDevice
|
||||
*
|
||||
* @param bool $userIsValid
|
||||
* @param int $tokenId
|
||||
* @param bool $tokenIsValid
|
||||
* @param bool $deviceDeleted
|
||||
* @param array $payload
|
||||
* @param int $status
|
||||
*/
|
||||
public function testRemoveDevice($userIsValid, $tokenId, $tokenIsValid, $deviceDeleted, $payload, $status) {
|
||||
$controller = $this->getController([
|
||||
'deletePushToken',
|
||||
]);
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
if ($userIsValid) {
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
->willReturn($user);
|
||||
} else {
|
||||
$this->userSession->expects($this->any())
|
||||
->method('getUser')
|
||||
->willReturn(null);
|
||||
}
|
||||
|
||||
$this->session->expects($tokenId > 0 ? $this->once() : $this->never())
|
||||
->method('get')
|
||||
->with('token-id')
|
||||
->willReturn($tokenId);
|
||||
|
||||
if ($tokenIsValid) {
|
||||
$token = $this->createMock(IToken::class);
|
||||
$this->tokenProvider->expects($this->any())
|
||||
->method('getTokenById')
|
||||
->with($tokenId)
|
||||
->willReturn($token);
|
||||
|
||||
$controller->expects($this->once())
|
||||
->method('deletePushToken')
|
||||
->with($user, $token)
|
||||
->willReturn($deviceDeleted);
|
||||
} else {
|
||||
$controller->expects($this->never())
|
||||
->method('deletePushToken');
|
||||
|
||||
$this->tokenProvider->expects($this->any())
|
||||
->method('getTokenById')
|
||||
->with($tokenId)
|
||||
->willThrowException(new InvalidTokenException());
|
||||
}
|
||||
|
||||
$response = $controller->removeDevice();
|
||||
$this->assertInstanceOf(DataResponse::class, $response);
|
||||
$this->assertSame($status, $response->getStatus());
|
||||
$this->assertSame($payload, $response->getData());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,487 @@
|
|||
<?php
|
||||
/**
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
||||
*
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Notifications\Tests\Unit;
|
||||
|
||||
|
||||
use OC\Authentication\Exceptions\InvalidTokenException;
|
||||
use OC\Authentication\Token\IProvider;
|
||||
use OC\Security\IdentityProof\Key;
|
||||
use OC\Security\IdentityProof\Manager;
|
||||
use OCA\Notifications\Push;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\Http\Client\IClient;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\ILogger;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Notification\IManager as INotificationManager;
|
||||
use OCP\Notification\INotification;
|
||||
|
||||
/**
|
||||
* Class PushTest
|
||||
*
|
||||
* @package OCA\Notifications\Tests\Unit
|
||||
* @group DB
|
||||
*/
|
||||
class PushTest extends TestCase {
|
||||
/** @var IDBConnection */
|
||||
protected $db;
|
||||
/** @var INotificationManager|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $notificationManager;
|
||||
/** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $config;
|
||||
/** @var IProvider|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $tokenProvider;
|
||||
/** @var Manager|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $keyManager;
|
||||
/** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $userManager;
|
||||
/** @var IClientService|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $clientService;
|
||||
/** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $logger;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->db = \OC::$server->getDatabaseConnection();
|
||||
$this->notificationManager = $this->createMock(INotificationManager::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->tokenProvider = $this->createMock(IProvider::class);
|
||||
$this->keyManager = $this->createMock(Manager::class);
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->clientService = $this->createMock(IClientService::class);
|
||||
$this->logger = $this->createMock(ILogger::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $methods
|
||||
* @return Push|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected function getPush(array $methods = []) {
|
||||
if (!empty($methods)) {
|
||||
return $this->getMockBuilder(Push::class)
|
||||
->setConstructorArgs([
|
||||
$this->db,
|
||||
$this->notificationManager,
|
||||
$this->config,
|
||||
$this->tokenProvider,
|
||||
$this->keyManager,
|
||||
$this->userManager,
|
||||
$this->clientService,
|
||||
$this->logger,
|
||||
])
|
||||
->setMethods($methods)
|
||||
->getMock();
|
||||
}
|
||||
|
||||
return new Push(
|
||||
$this->db,
|
||||
$this->notificationManager,
|
||||
$this->config,
|
||||
$this->tokenProvider,
|
||||
$this->keyManager,
|
||||
$this->userManager,
|
||||
$this->clientService,
|
||||
$this->logger
|
||||
);
|
||||
}
|
||||
|
||||
public function testPushToDeviceInvalidUser() {
|
||||
$push = $this->getPush();
|
||||
$this->keyManager->expects($this->never())
|
||||
->method('getKey');
|
||||
$this->clientService->expects($this->never())
|
||||
->method('newClient');
|
||||
|
||||
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
|
||||
$notification = $this->createMock(INotification::class);
|
||||
$notification->expects($this->once())
|
||||
->method('getUser')
|
||||
->willReturn('invalid');
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('get')
|
||||
->with('invalid')
|
||||
->willReturn(null);
|
||||
|
||||
$push->pushToDevice($notification);
|
||||
}
|
||||
|
||||
public function testPushToDeviceNoDevices() {
|
||||
$push = $this->getPush(['getDevicesForUser']);
|
||||
$this->keyManager->expects($this->never())
|
||||
->method('getKey');
|
||||
$this->clientService->expects($this->never())
|
||||
->method('newClient');
|
||||
|
||||
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
|
||||
$notification = $this->createMock(INotification::class);
|
||||
$notification->expects($this->exactly(2))
|
||||
->method('getUser')
|
||||
->willReturn('valid');
|
||||
|
||||
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
|
||||
$user = $this->createMock(IUser::class);
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid')
|
||||
->willReturn($user);
|
||||
|
||||
$push->expects($this->once())
|
||||
->method('getDevicesForUser')
|
||||
->willReturn([]);
|
||||
|
||||
$push->pushToDevice($notification);
|
||||
}
|
||||
|
||||
public function testPushToDeviceNotPrepared() {
|
||||
$push = $this->getPush(['getDevicesForUser']);
|
||||
$this->keyManager->expects($this->never())
|
||||
->method('getKey');
|
||||
$this->clientService->expects($this->never())
|
||||
->method('newClient');
|
||||
|
||||
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
|
||||
$notification = $this->createMock(INotification::class);
|
||||
$notification->expects($this->exactly(3))
|
||||
->method('getUser')
|
||||
->willReturn('valid');
|
||||
|
||||
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
|
||||
$user = $this->createMock(IUser::class);
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid')
|
||||
->willReturn($user);
|
||||
|
||||
$push->expects($this->once())
|
||||
->method('getDevicesForUser')
|
||||
->willReturn([[
|
||||
'proxyserver' => 'proxyserver1',
|
||||
'token' => 'token1',
|
||||
]]);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('valid', 'core', 'lang', 'en')
|
||||
->willReturn('de');
|
||||
|
||||
$this->notificationManager->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($notification, 'de')
|
||||
->willThrowException(new \InvalidArgumentException());
|
||||
|
||||
$push->pushToDevice($notification);
|
||||
}
|
||||
|
||||
public function testPushToDeviceInvalidToken() {
|
||||
$push = $this->getPush(['getDevicesForUser', 'encryptAndSign', 'deletePushToken']);
|
||||
$this->clientService->expects($this->never())
|
||||
->method('newClient');
|
||||
|
||||
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
|
||||
$notification = $this->createMock(INotification::class);
|
||||
$notification->expects($this->exactly(3))
|
||||
->method('getUser')
|
||||
->willReturn('valid');
|
||||
|
||||
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
|
||||
$user = $this->createMock(IUser::class);
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid')
|
||||
->willReturn($user);
|
||||
|
||||
$push->expects($this->once())
|
||||
->method('getDevicesForUser')
|
||||
->willReturn([[
|
||||
'proxyserver' => 'proxyserver1',
|
||||
'token' => 23,
|
||||
]]);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('valid', 'core', 'lang', 'en')
|
||||
->willReturn('ru');
|
||||
|
||||
$this->notificationManager->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($notification, 'ru')
|
||||
->willReturnArgument(0);
|
||||
|
||||
|
||||
/** @var Key|\PHPUnit_Framework_MockObject_MockObject $key */
|
||||
$key = $this->createMock(Key::class);
|
||||
|
||||
$this->keyManager->expects($this->once())
|
||||
->method('getKey')
|
||||
->with($user)
|
||||
->willReturn($key);
|
||||
|
||||
$push->expects($this->once())
|
||||
->method('encryptAndSign')
|
||||
->willThrowException(new InvalidTokenException());
|
||||
|
||||
$push->expects($this->once())
|
||||
->method('deletePushToken')
|
||||
->with(23);
|
||||
|
||||
$push->pushToDevice($notification);
|
||||
}
|
||||
|
||||
public function testPushToDeviceEncryptionError() {
|
||||
$push = $this->getPush(['getDevicesForUser', 'encryptAndSign', 'deletePushToken']);
|
||||
$this->clientService->expects($this->never())
|
||||
->method('newClient');
|
||||
|
||||
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
|
||||
$notification = $this->createMock(INotification::class);
|
||||
$notification->expects($this->exactly(3))
|
||||
->method('getUser')
|
||||
->willReturn('valid');
|
||||
|
||||
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
|
||||
$user = $this->createMock(IUser::class);
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid')
|
||||
->willReturn($user);
|
||||
|
||||
$push->expects($this->once())
|
||||
->method('getDevicesForUser')
|
||||
->willReturn([[
|
||||
'proxyserver' => 'proxyserver1',
|
||||
'token' => 23,
|
||||
]]);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('valid', 'core', 'lang', 'en')
|
||||
->willReturn('ru');
|
||||
|
||||
$this->notificationManager->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($notification, 'ru')
|
||||
->willReturnArgument(0);
|
||||
|
||||
|
||||
/** @var Key|\PHPUnit_Framework_MockObject_MockObject $key */
|
||||
$key = $this->createMock(Key::class);
|
||||
|
||||
$this->keyManager->expects($this->once())
|
||||
->method('getKey')
|
||||
->with($user)
|
||||
->willReturn($key);
|
||||
|
||||
$push->expects($this->once())
|
||||
->method('encryptAndSign')
|
||||
->willThrowException(new \InvalidArgumentException());
|
||||
|
||||
$push->expects($this->once())
|
||||
->method('deletePushToken')
|
||||
->with(23);
|
||||
|
||||
$push->pushToDevice($notification);
|
||||
}
|
||||
|
||||
public function dataPushToDeviceSending() {
|
||||
return [
|
||||
[true],
|
||||
[false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataPushToDeviceSending
|
||||
* @param bool $isDebug
|
||||
*/
|
||||
public function testPushToDeviceSending($isDebug) {
|
||||
$push = $this->getPush(['getDevicesForUser', 'encryptAndSign', 'deletePushToken']);
|
||||
|
||||
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
|
||||
$notification = $this->createMock(INotification::class);
|
||||
$notification->expects($this->exactly(3))
|
||||
->method('getUser')
|
||||
->willReturn('valid');
|
||||
|
||||
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
|
||||
$user = $this->createMock(IUser::class);
|
||||
|
||||
$this->userManager->expects($this->once())
|
||||
->method('get')
|
||||
->with('valid')
|
||||
->willReturn($user);
|
||||
|
||||
$push->expects($this->once())
|
||||
->method('getDevicesForUser')
|
||||
->willReturn([
|
||||
[
|
||||
'proxyserver' => 'proxyserver1',
|
||||
'token' => 16,
|
||||
],
|
||||
[
|
||||
'proxyserver' => 'proxyserver1/',
|
||||
'token' => 23,
|
||||
],
|
||||
[
|
||||
'proxyserver' => 'badrequest',
|
||||
'token' => 42,
|
||||
],
|
||||
[
|
||||
'proxyserver' => 'unavailable',
|
||||
'token' => 48,
|
||||
],
|
||||
[
|
||||
'proxyserver' => 'ok',
|
||||
'token' => 64,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with('valid', 'core', 'lang', 'en')
|
||||
->willReturn('ru');
|
||||
|
||||
$this->notificationManager->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($notification, 'ru')
|
||||
->willReturnArgument(0);
|
||||
|
||||
/** @var Key|\PHPUnit_Framework_MockObject_MockObject $key */
|
||||
$key = $this->createMock(Key::class);
|
||||
|
||||
$this->keyManager->expects($this->once())
|
||||
->method('getKey')
|
||||
->with($user)
|
||||
->willReturn($key);
|
||||
|
||||
$push->expects($this->exactly(5))
|
||||
->method('encryptAndSign')
|
||||
->willReturn(['Payload']);
|
||||
|
||||
$push->expects($this->never())
|
||||
->method('deletePushToken');
|
||||
|
||||
/** @var IClient|\PHPUnit_Framework_MockObject_MockObject $client */
|
||||
$client = $this->createMock(IClient::class);
|
||||
|
||||
$this->clientService->expects($this->once())
|
||||
->method('newClient')
|
||||
->willReturn($client);
|
||||
|
||||
$e = new \Exception();
|
||||
$client->expects($this->at(0))
|
||||
->method('post')
|
||||
->with('proxyserver1/notifications', [
|
||||
'body' => [
|
||||
'notifications' => ['["Payload"]', '["Payload"]'],
|
||||
],
|
||||
])
|
||||
->willThrowException($e);
|
||||
|
||||
$this->logger->expects($this->at(0))
|
||||
->method('logException')
|
||||
->with($e, [
|
||||
'app' => 'notifications',
|
||||
]);
|
||||
|
||||
/** @var IResponse|\PHPUnit_Framework_MockObject_MockObject $response1 */
|
||||
$response1 = $this->createMock(IResponse::class);
|
||||
$response1->expects($this->once())
|
||||
->method('getStatusCode')
|
||||
->willReturn(Http::STATUS_BAD_REQUEST);
|
||||
$response1->expects($this->once())
|
||||
->method('getBody')
|
||||
->willReturn(null);
|
||||
$client->expects($this->at(1))
|
||||
->method('post')
|
||||
->with('badrequest/notifications', [
|
||||
'body' => [
|
||||
'notifications' => ['["Payload"]'],
|
||||
],
|
||||
])
|
||||
->willReturn($response1);
|
||||
|
||||
$this->logger->expects($this->at(1))
|
||||
->method('error')
|
||||
->with('Could not send notification to push server [{url}]: {error}', [
|
||||
'error' => 'no reason given',
|
||||
'url' => 'badrequest',
|
||||
'app' => 'notifications',
|
||||
]);
|
||||
|
||||
/** @var IResponse|\PHPUnit_Framework_MockObject_MockObject $response1 */
|
||||
$response2 = $this->createMock(IResponse::class);
|
||||
$response2->expects($this->once())
|
||||
->method('getStatusCode')
|
||||
->willReturn(Http::STATUS_SERVICE_UNAVAILABLE);
|
||||
$response2->expects($isDebug ? $this->once() : $this->never())
|
||||
->method('getBody')
|
||||
->willReturn('Maintenance');
|
||||
$client->expects($this->at(2))
|
||||
->method('post')
|
||||
->with('unavailable/notifications', [
|
||||
'body' => [
|
||||
'notifications' => ['["Payload"]'],
|
||||
],
|
||||
])
|
||||
->willReturn($response2);
|
||||
|
||||
$this->config->expects($this->once())
|
||||
->method('getSystemValue')
|
||||
->with('debug', false)
|
||||
->willReturn($isDebug);
|
||||
|
||||
$this->logger->expects($isDebug ? $this->at(2) : $this->never())
|
||||
->method('debug')
|
||||
->with('Could not send notification to push server [{url}]: {error}', [
|
||||
'error' => 'Maintenance',
|
||||
'url' => 'unavailable',
|
||||
'app' => 'notifications',
|
||||
]);
|
||||
|
||||
/** @var IResponse|\PHPUnit_Framework_MockObject_MockObject $response1 */
|
||||
$response3 = $this->createMock(IResponse::class);
|
||||
$response3->expects($this->once())
|
||||
->method('getStatusCode')
|
||||
->willReturn(Http::STATUS_OK);
|
||||
$client->expects($this->at(3))
|
||||
->method('post')
|
||||
->with('ok/notifications', [
|
||||
'body' => [
|
||||
'notifications' => ['["Payload"]'],
|
||||
],
|
||||
])
|
||||
->willReturn($response3);
|
||||
|
||||
$push->pushToDevice($notification);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче