WebPush can be used to send notifications to endpoints which server delivers Web Push

Overview

WebPush

Web Push library for PHP

Build Status SensioLabsInsight

WebPush can be used to send notifications to endpoints which server delivers Web Push notifications as described in the Web Push protocol. As it is standardized, you don't have to worry about what server type it relies on.

Requirements

PHP 7.3+ and the following extensions:

  • gmp (optional but better for performance)
  • mbstring
  • curl
  • openssl

There is no support and maintenance for older PHP versions, however you are free to use the following compatible versions:

  • PHP 5.6 or HHVM: v1.x
  • PHP 7.0: v2.x
  • PHP 7.1: v3.x-v5.x
  • PHP 7.2: v6.x

Installation

Use composer to download and install the library and its dependencies.

composer require minishlink/web-push

Usage

Subscription::create([ // this is the structure for the working draft from october 2018 (https://www.w3.org/TR/2018/WD-push-api-20181026/) "endpoint" => "https://example.com/other/endpoint/of/another/vendor/abcdef...", "keys" => [ 'p256dh' => '(stringOf88Chars)', 'auth' => '(stringOf24Chars)' ], ]), 'payload' => '{"msg":"Hello World!"}', ], ]; $webPush = new WebPush(); // send multiple notifications with payload foreach ($notifications as $notification) { $webPush->queueNotification( $notification['subscription'], $notification['payload'] // optional (defaults null) ); } /** * Check sent results * @var MessageSentReport $report */ foreach ($webPush->flush() as $report) { $endpoint = $report->getRequest()->getUri()->__toString(); if ($report->isSuccess()) { echo "[v] Message sent successfully for subscription {$endpoint}."; } else { echo "[x] Message failed to sent for subscription {$endpoint}: {$report->getReason()}"; } } /** * send one notification and flush directly * @var MessageSentReport $report */ $report = $webPush->sendOneNotification( $notifications[0]['subscription'], $notifications[0]['payload'] // optional (defaults null) );">


use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

// array of notifications
$notifications = [
    [
        'subscription' => Subscription::create([
            'endpoint' => 'https://updates.push.services.mozilla.com/push/abc...', // Firefox 43+,
            'publicKey' => 'BPcMbnWQL5GOYX/5LKZXT6sLmHiMsJSiEvIFvfcDvX7IZ9qqtq68onpTPEYmyxSQNiH7UD/98AUcQ12kBoxz/0s=', // base 64 encoded, should be 88 chars
            'authToken' => 'CxVX6QsVToEGEcjfYPqXQw==', // base 64 encoded, should be 24 chars
        ]),
        'payload' => 'hello !',
    ], [
        'subscription' => Subscription::create([
            'endpoint' => 'https://fcm.googleapis.com/fcm/send/abcdef...', // Chrome
        ]),
        'payload' => null,
    ], [
        'subscription' => Subscription::create([
            'endpoint' => 'https://example.com/other/endpoint/of/another/vendor/abcdef...',
            'publicKey' => '(stringOf88Chars)',
            'authToken' => '(stringOf24Chars)',
            'contentEncoding' => 'aesgcm', // one of PushManager.supportedContentEncodings
        ]),
        'payload' => '{msg:"test"}',
    ], [
          'subscription' => Subscription::create([ // this is the structure for the working draft from october 2018 (https://www.w3.org/TR/2018/WD-push-api-20181026/) 
              "endpoint" => "https://example.com/other/endpoint/of/another/vendor/abcdef...",
              "keys" => [
                  'p256dh' => '(stringOf88Chars)',
                  'auth' => '(stringOf24Chars)'
              ],
          ]),
          'payload' => '{"msg":"Hello World!"}',
      ],
];

$webPush = new WebPush();

// send multiple notifications with payload
foreach ($notifications as $notification) {
    $webPush->queueNotification(
        $notification['subscription'],
        $notification['payload'] // optional (defaults null)
    );
}

/**
 * Check sent results
 * @var MessageSentReport $report
 */
foreach ($webPush->flush() as $report) {
    $endpoint = $report->getRequest()->getUri()->__toString();

    if ($report->isSuccess()) {
        echo "[v] Message sent successfully for subscription {$endpoint}.";
    } else {
        echo "[x] Message failed to sent for subscription {$endpoint}: {$report->getReason()}";
    }
}

/**
 * send one notification and flush directly
 * @var MessageSentReport $report
 */
$report = $webPush->sendOneNotification(
    $notifications[0]['subscription'],
    $notifications[0]['payload'] // optional (defaults null)
);

Full examples of Web Push implementations

Authentication (VAPID)

Browsers need to verify your identity. A standard called VAPID can authenticate you for all browsers. You'll need to create and provide a public and private key for your server. These keys must be safely stored and should not change.

You can specify your authentication details when instantiating WebPush. The keys can be passed directly (recommended), or you can load a PEM file or its content:



use Minishlink\WebPush\WebPush;

$endpoint = 'https://fcm.googleapis.com/fcm/send/abcdef...'; // Chrome

$auth = [
    'VAPID' => [
        'subject' => 'mailto:[email protected]', // can be a mailto: or your website address
        'publicKey' => '~88 chars', // (recommended) uncompressed public key P-256 encoded in Base64-URL
        'privateKey' => '~44 chars', // (recommended) in fact the secret multiplier of the private key encoded in Base64-URL
        'pemFile' => 'path/to/pem', // if you have a PEM file and can link to it on your filesystem
        'pem' => 'pemFileContent', // if you have a PEM file and want to hardcode its content
    ],
];

$webPush = new WebPush($auth);
$webPush->queueNotification(...);

In order to generate the uncompressed public and secret key, encoded in Base64, enter the following in your Linux bash:

$ openssl ecparam -genkey -name prime256v1 -out private_key.pem
$ openssl ec -in private_key.pem -pubout -outform DER|tail -c 65|base64|tr -d '=' |tr '/+' '_-' >> public_key.txt
$ openssl ec -in private_key.pem -outform DER|tail -c +8|head -c 32|base64|tr -d '=' |tr '/+' '_-' >> private_key.txt

If you can't access a Linux bash, you can print the output of the createVapidKeys function:

var_dump(VAPID::createVapidKeys()); // store the keys afterwards

On the client-side, don't forget to subscribe with the VAPID public key as the applicationServerKey: (urlBase64ToUint8Array source here)

serviceWorkerRegistration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
})

Reusing VAPID headers

VAPID headers make use of a JSON Web Token (JWT) to verify your identity. That token payload includes the protocol and hostname of the endpoint included in the subscription and an expiration timestamp (usually between 12-24h), and it's signed using your public and private key. Given that, two notifications sent to the same push service will use the same token, so you can reuse them for the same flush session to boost performance using:

$webPush->setReuseVAPIDHeaders(true);

Notifications and default options

Each notification can have a specific Time To Live, urgency, and topic. You can change the default options with setDefaultOptions() or in the constructor:

'new_event', // not defined by default, 'batchSize' => 200, // defaults to 1000 ]; // for every notifications $webPush = new WebPush([], $defaultOptions); $webPush->setDefaultOptions($defaultOptions); // or for one notification $webPush->sendOneNotification($subscription, $payload, ['TTL' => 5000]);">


use Minishlink\WebPush\WebPush;

$defaultOptions = [
    'TTL' => 300, // defaults to 4 weeks
    'urgency' => 'normal', // protocol defaults to "normal"
    'topic' => 'new_event', // not defined by default,
    'batchSize' => 200, // defaults to 1000
];

// for every notifications
$webPush = new WebPush([], $defaultOptions);
$webPush->setDefaultOptions($defaultOptions);

// or for one notification
$webPush->sendOneNotification($subscription, $payload, ['TTL' => 5000]);

TTL

Time To Live (TTL, in seconds) is how long a push message is retained by the push service (eg. Mozilla) in case the user browser is not yet accessible (eg. is not connected). You may want to use a very long time for important notifications. The default TTL is 4 weeks. However, if you send multiple nonessential notifications, set a TTL of 0: the push notification will be delivered only if the user is currently connected. For other cases, you should use a minimum of one day if your users have multiple time zones, and if they don't several hours will suffice.

urgency

Urgency can be either "very-low", "low", "normal", or "high". If the browser vendor has implemented this feature, it will save battery life on mobile devices (cf. protocol).

topic

Similar to the old collapse_key on legacy GCM servers, this string will make the vendor show to the user only the last notification of this topic (cf. protocol).

batchSize

If you send tens of thousands notifications at a time, you may get memory overflows due to how endpoints are called in Guzzle. In order to fix this, WebPush sends notifications in batches. The default size is 1000. Depending on your server configuration (memory), you may want to decrease this number. Do this while instanciating WebPush or calling setDefaultOptions. Or, if you want to customize this for a specific flush, give it as a parameter : $webPush->flush($batchSize).

Server errors

You can see what the browser vendor's server sends back in case it encountered an error (push subscription expiration, wrong parameters...).

getReason()}"; // also available (to get more info) /** @var \Psr\Http\Message\RequestInterface $requestToPushService */ $requestToPushService = $report->getRequest(); /** @var \Psr\Http\Message\ResponseInterface $responseOfPushService */ $responseOfPushService = $report->getResponse(); /** @var string $failReason */ $failReason = $report->getReason(); /** @var bool $isTheEndpointWrongOrExpired */ $isTheEndpointWrongOrExpired = $report->isSubscriptionExpired(); } }">


/** @var \Minishlink\WebPush\MessageSentReport $report */
foreach ($webPush->flush() as $report) {
    $endpoint = $report->getEndpoint();

    if ($report->isSuccess()) {
        echo "[v] Message sent successfully for subscription {$endpoint}.";
    } else {
        echo "[x] Message failed to sent for subscription {$endpoint}: {$report->getReason()}";
        
        // also available (to get more info)
        
        /** @var \Psr\Http\Message\RequestInterface $requestToPushService */
        $requestToPushService = $report->getRequest();
        
        /** @var \Psr\Http\Message\ResponseInterface $responseOfPushService */
        $responseOfPushService = $report->getResponse();
        
        /** @var string $failReason */
        $failReason = $report->getReason();
        
        /** @var bool $isTheEndpointWrongOrExpired */
        $isTheEndpointWrongOrExpired = $report->isSubscriptionExpired();
    }
}

PLEASE NOTE: You can only iterate once over the \Generator object.

Firefox errors are listed in the autopush documentation.

Payload length, security, and performance

Payloads are encrypted by the library. The maximum payload length is theoretically 4078 bytes (or ASCII characters). For compatibility reasons though, your payload should be less than 3052 bytes long.

The library pads the payload by default. This is more secure but it decreases performance for both your server and your user's device.

Why is it more secure?

When you encrypt a string of a certain length, the resulting string will always have the same length, no matter how many times you encrypt the initial string. This can make attackers guess the content of the payload. In order to circumvent this, this library adds some null padding to the initial payload, so that all the input of the encryption process will have the same length. This way, all the output of the encryption process will also have the same length and attackers won't be able to guess the content of your payload.

Why does it decrease performance?

Encrypting more bytes takes more runtime on your server, and also slows down the user's device with decryption. Moreover, sending and receiving the packet will take more time. It's also not very friendly with users who have limited data plans.

How can I disable or customize automatic padding?

You can customize automatic padding in order to better fit your needs.

Here are some ideas of settings:

  • (default) Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH (3052 bytes) for compatibility purposes with Firefox for Android
  • Encryption::MAX_PAYLOAD_LENGTH (4078 bytes) for maximum security
  • false for maximum performance
  • If you know your payloads will not exceed X bytes, then set it to X for the best balance between security and performance.


use Minishlink\WebPush\WebPush;

$webPush = new WebPush();
$webPush->setAutomaticPadding(false); // disable automatic padding
$webPush->setAutomaticPadding(512); // enable automatic padding to 512 bytes (you should make sure that your payload is less than 512 bytes, or else an attacker could guess the content)
$webPush->setAutomaticPadding(true); // enable automatic padding to default maximum compatibility length

Customizing the HTTP client

WebPush uses Guzzle. It will use the most appropriate client it finds, and most of the time it will be MultiCurl, which allows to send multiple notifications in parallel.

You can customize the default request options and timeout when instantiating WebPush:



use Minishlink\WebPush\WebPush;

$timeout = 20; // seconds
$clientOptions = [
    \GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => false,
]; // see \GuzzleHttp\RequestOptions
$webPush = new WebPush([], [], $timeout, $clientOptions);

Common questions

Is there any plugin/bundle/extension for my favorite PHP framework?

The following are available:

Feel free to add your own!

Is the API stable?

Not until the Push API spec is finished.

What about security?

Payload is encrypted according to the Message Encryption for Web Push standard, using the user public key and authentication secret that you can get by following the Web Push API specification.

Internally, WebPush uses the WebToken framework or OpenSSL to handle encryption keys generation and encryption.

How do I scale?

Here are some ideas:

  1. Upgrade to PHP 7.3
  2. Make sure MultiCurl is available on your server
  3. Find the right balance for your needs between security and performance (see above)
  4. Find the right batch size (set it in defaultOptions or as parameter to flush())

How to solve "SSL certificate problem: unable to get local issuer certificate"?

Your installation lacks some certificates.

  1. Download cacert.pem.
  2. Edit your php.ini: after [curl], type curl.cainfo = /path/to/cacert.pem.

You can also force using a client without peer verification.

How to solve "Bad key encryption key length" or "Unsupported key type"?

Disable mbstring.func_overload in your php.ini.

How to solve "Class 'Minishlink\WebPush\WebPush' not found"

Make sure to require Composer's autoloader.

require __DIR__ . '/path/to/vendor/autoload.php';

I lost my VAPID keys!

See issue #58.

I'm using Firebase push notifications, how do I use this library?

This library is not designed for Firebase push notifications. You can still use it for your web projects (for standard WebPush notifications), but you should forget any link to Firebase while using the library.

I need to send notifications to native apps. (eg. APNS for iOS)

WebPush is for web apps. You need something like RMSPushNotificationsBundle (Symfony).

This is PHP... I need Javascript!

This library was inspired by the Node.js marco-c/web-push library.

Contributing

See CONTRIBUTING.md.

License

MIT

Sponsors

Thanks to JetBrains for supporting the project through sponsoring some All Products Packs within their Free Open Source License program.

Comments
  • Payload support

    Payload support

    • (done) use phpecc/phpecc in order to generate the encryption key in prime256v1
    • when this PHP bug is fixed, replace phpecc by the PHP binding of OpenSSL (thanks @marco-c)
    • use OpenSSL with AES 128 GCM mode to encrypt the payload (need to wait for the resolution of this other PHP bug.)

    If you want to help: (thanks!)

    • vote for the PHP bugs in the above linked pages
    • or contribute to php/php-src in order to fix the bugs
    opened by Minishlink 31
  • [WIP] [BC] General project maintenance

    [WIP] [BC] General project maintenance

    I've reached out to @Minishlink hoping to be able to collaborate on a roadmap for the project and shoot some ideas that he may be willing to accept into the project but haven't heard back yet. Hopefully, this will come as a sign of good faith and jump-start that process.

    Sourcing projects similar in function to fit a need in another project, I stumbled across this library as it seems to be the defacto standard for web push with PHP. I also noticed it has some great proposed contributions from some awesome contributors and decided that rather than building my own library, why not contribute back to the community.

    For anyone interested, this pull request will be a WIP and any feedback on particular changes being added in or removed are entirely welcome.

    Changes in this pull request

    • 7173909 - Removing dependency management from code
    • dae1c89 - Implementing PSR-2 and opinionated code cleanup
    • e7de87c - Implementing PSR-4 autoloading for development
    • e88ba23 - Adding tests around & cleaning up notices in \Minishlink\WebPush\Notification
    • ed52704 - Reducing public IP and cleaning up test suite of \Minishlink\WebPush\MessageSentReport [BC]
    • 0ee2485 - Separating unit and integration test suites (also snuck a fix in that encompasses the changes in #234)
    • 8bf6f80 - Implementing abstraction of HTTP communications [BC]
    • 4bf92e9 - Implementing Options
    • 78bff65 - Continuation of abstraction of HTTP communications; decoupling from third party HTTP libraries [BC]
    • e7cc878 - Implement Auth, drop support for GCM in exchange for VAPID (SEE: validation) [BC]
    • afc2cc5 - Implement Headers object to make building, manipulating and iterating over header keys and values easier and cleaner [BC]
    • ea9646e -
      • Implement Payload to keep the string payload it's encryption components together [BC]
      • Implement Queue to handle queueing of notifications and promises
      • Implement Cache to handle caching vapid headers
      • Implement SubscriptionInterface and AuthorizationInterface to allow for easier integration of the library into consumer code
      • Removed factory methods on model objects for explicit factory classes [BC]

    Additional pull requests to be considered for version 6

    • #218 - [BC]
    • #227
    • #228

    Questions

    • We're not currently running the integration tests in CI but our travis.yml is partially configured to run the entirety of our test suite, integration tests included - do we want to look into running our integration tests in CI, or clean up the otherwise unneeded configuration?
    opened by ryancco 24
  • Payload data empty when getting push notification

    Payload data empty when getting push notification

    Hello there!,

    I am sending push notification payload data to GCM endpoint and I am getting notification to corresponding browser but my payload data is empty in service workers JS.

    Any ideas are appreciated, Thank you!

    question 
    opened by anandselva 22
  • Add support for VAPID

    Add support for VAPID

    See also: https://github.com/marco-c/web-push/blob/b9156f658fc028c3388dbed005ffa5e2012b55ac/index.js#L206 https://github.com/mozilla/wp-web-push/pull/283/files#diff-05b502d88da481dfcef81a7fdb4a6da4R60

    wp-web-push is using the same libraries you're using for the payload.

    enhancement 
    opened by marco-c 19
  • event.data in service-worker is always null

    event.data in service-worker is always null

    NOTE: Please test in a least two browsers (i.e. Chrome and Firefox). This helps with diagnosing problems quicker.

    Please confirm the following:

    • [x] I have read the README entirely
    • [ ] I have verified in the issues that my problem hasn't already been resolved

    Setup

    Please provide the following details, the more info you can provide the better.

    • Operating System: Linux
    • PHP Version: 7.2
    • web-push-php Version: 4.0.2

    Please check that you have installed and enabled these PHP extensions :

    • [x] gmp
    • [x] mbstring
    • [x] curl
    • [x] openssl

    Please select any browsers that you are experiencing problems with:

    • [x] Chrome
    • [ ] Firefox
    • [ ] Firefox for Mobile
    • [ ] Opera for Android
    • [ ] Samsung Internet Browser
    • [ ] Other

    Chrome 70

    Problem

    event.data in service-worker is always null

    Expected

    event.data may not be null if a payload is defined in notification

    Features Used

    • [x] VAPID Support
    • [x] GCM API Key
    • [x] Sending with Payload

    Example / Reproduce Case

    <?php
    
    namespace App\Service;
    
    use Minishlink\WebPush\WebPush;
    use Minishlink\WebPush\Subscription;
    
    class NotificationService
    {
        /** @var Subscription */
        private $subscription;
    
        /** @var WebPush */
        private $webPush;
    
        public function __construct()
        {
            $destination = json_decode('{"endpoint":"https://fcm.googleapis.com/fcm/send/eqQOWl44JuU:APA91bG9TpM9-WbVp65uO4VZtZD06AJYBOpfigYeXq_lW3Gq7bs8Nqp4zT5DeFKjPqgd4NycaHt0VBPg-oNoi1WJnVv4vylmNjIJ_lyYovC11ztk60AtkIUFPTG-n4i_iYAoFJzwyajN","expirationTime":null,"keys":{"p256dh":"BLzGWK-7eDl0LYyFVPADfBMFa0YDLG9xsUikGhH-MMSXtgALIkoOwK6rrIowHX3tOhMw1IqNKzc3EqLhdzHst9A","auth":"Het3CBpFFMC_kuQp5DPPIw"}}', true);
            $destination['contentEncoding'] = 'aesgcm';
    
            $auth = array(
                'VAPID' => array(
                    'subject' => 'My awesome app name',
                    'publicKey' => 'My public key',
                    'privateKey' => 'My private key'
                )
            );
    
            $this->subscription = Subscription::create($destination);
            $this->webPush = new WebPush($auth);
        }
    
        public function sendNotification(string $message)
        {
            $this->webPush->sendNotification(
                $this->subscription,
                $message
            );
            return $this->webPush->flush();
        }
    }
    

    Other

    When I use $notificationService->sendNotification('Hello world'), a notification is well received in the web app but event.data is always null

    opened by vcastro45 18
  • Sending with payload not working

    Sending with payload not working

    NOTE: Please test in a least two browsers (i.e. Chrome and Firefox). This helps with diagnosing problems quicker.

    Please confirm the following:

    • [X] I have read the README entirely
    • [X] I have verified in the issues that my problem hasn't already been resolved

    Setup

    Please provide the following details, the more info you can provide the better.

    • Operating System: Windows
    • PHP Version: 7.1.5
    • web-push-php Version: v3.0.0

    Please check that you have installed and enabled these PHP extensions :

    • [X] gmp
    • [X] mbstring
    • [X] curl
    • [X] openssl

    Please select any browsers that you are experiencing problems with:

    • [X] Chrome
    • [X] Firefox
    • [X] Firefox for Mobile
    • [X] Opera for Android
    • [X] Samsung Internet Browser
    • [X] Other

    Please specify the versions (i.e. Chrome Beta, Firefox Beta etc). Chrome 64

    Problem

    I am unable to send notifications with payloads. Each time the payload is not empty, the request is sent and sendNotification method returns true (when I dumped response, status code was 201), but no notification is delivered.

    Disabling the padding did not help.

    I am suspecting the encryption/signing not working correctly and I will be trying to debug it step by step against web-push for node.js, which works OK.

    Please explain what behaviour you are seeing.

    Expected

    Send notification with payload, that is delivered to the browser.

    Please explain what you expected to happen

    Features Used

    • [X] VAPID Support
    • [ ] GCM API Key
    • [X] Sending with Payload

    Example / Reproduce Case

    $subscription = '{"endpoint":"https://fcm.googleapis.com/fcm/send/fxfsxX_N7Is:APA91bFeXNv4zQ9xKl2k-rfmf5zweDJP3walARMXnQFa6pdx4_1OCldKjl1ruuKPavs67tK5_xfP3XMx2XRVRWTIrhIaS6_GIQvT4U9hmH3xDJBABVfNlBewnx0Zml6oFZKiEW9xmZ5-","expirationTime":null,"keys":{"p256dh":"BHs7fMO97vNgGePrFBCdtK4cwCirLiL56e34QqpoirN5_MC2-LgoTsJ5hV0vu6wn7oRpebgYelICX7bEvXbB7mI=","auth":"aZ5dQkxnt7yFsdV4dpZS3w=="}}';
    
    $subscription = json_decode($subscription, true);
    
    use Minishlink\WebPush\WebPush;
    
    $auth = [
        'VAPID' => [
            'subject' => 'mailto:[email protected]',
            'publicKey' => 'BDaIDoGypt1kzw1XkCItDuvH0iKb9NcsiXjltGt1rCOXy7vF2ER94UqNM-Ers0ey8FzK6bskzPpXKwwYo0LgCBA',
            'privateKey' => '-n1Xh2Zrpl3GeV50X5vBtSHFzwbDs5Ie0LMzkfsyjLs',
        ],
    ];
    
    $webPush = new WebPush($auth);
    
    $webPush->sendNotification(
        $subscription['endpoint'],
        'Hello data',
        $subscription['keys']['p256dh'],
        $subscription['keys']['auth'],
        true
    );
    

    Please provide a code sample that reproduces the issue. If there is a repository that reproduces the issue please put the link here.

    Other

    Please put any remaining notes here.

    opened by lukasradek 16
  • Call to undefined method GuzzleHttp\Exception\ConnectException::getResponse() in WebPush.php line 185

    Call to undefined method GuzzleHttp\Exception\ConnectException::getResponse() in WebPush.php line 185

    NOTE: Please test in a least two browsers (i.e. Chrome and Firefox). This helps with diagnosing problems quicker.

    Please confirm the following:

    • [x] I have read the README entirely
    • [x] I have verified in the issues that my problem hasn't already been resolved

    Setup

    Please provide the following details, the more info you can provide the better.

    • Operating System: Linux
    • PHP Version: 7.4
    • web-push-php Version: 6.0.2

    Please check that you have installed and enabled these PHP extensions :

    • [x] gmp
    • [x] mbstring
    • [x] curl
    • [x] openssl

    Please select any browsers that you are experiencing problems with:

    • [ ] Chrome
    • [ ] Firefox
    • [ ] Firefox for Mobile
    • [ ] Opera for Android
    • [ ] Samsung Internet Browser
    • [x] Other

    Please specify the versions (i.e. Chrome Beta, Firefox Beta etc).

    Problem

    Please explain what behaviour you are seeing.

    Exception

    Expected

    Please explain what you expected to happen

    No exception

    Features Used

    • [x] VAPID Support
    • [x] Sending with Payload

    Example / Reproduce Case

    Please provide a code sample that reproduces the issue. If there is a repository that reproduces the issue please put the link here.

    Other

    Please put any remaining notes here.

    It never happened before v6.0.2 but it could be because HTTP request never failed. Also as I can see - RequestException was expected but ConnectException received.

    opened by swayok 14
  • 401 Unauthorized: authorization header must be specified

    401 Unauthorized: authorization header must be specified

    • [x] I have read the README entirely
    • [x] I have verified in the issues that my problem hasn't already been resolved

    Setup

    • Operating System: Linux
    • PHP Version: 7.4
    • web-push-php Version: <6.0.0>

    Please check that you have installed and enabled these PHP extensions :

    • [x] gmp
    • [x] mbstring
    • [x] curl
    • [x] openssl

    Please select any browsers that you are experiencing problems with:

    • [x] CLI

    Features Used

    • [x] VAPID Support
    • [x] Sending with Payload

    Problem

    As of v6.0.0, I get a 401 Unauthorized when sending a push notification:

    Client error: POST https://fcm.googleapis.com/fcm/send/.... resulted in a 401 Unauthorized response: authorization header must be specified.

    I'm using minishlink/web-push-bundle and everything worked perfectly, but upgrading to v6 gives this problem. For Windows clients I get a curl timeout, for FCM I get this 401.

    Any idea what causes this problem?

    opened by stephanvierkant 14
  • Firefox Push Issue

    Firefox Push Issue

    Hey, I'm not sure if you've encountered this or aware of this issue.

    But it seems like I need to send 2 Notifications in order for Firefox to work properly, I am not sure why this happening or how to solve it.

    Chrome seems to work out of the box.

    Now, an really funny think I've noticed.

    If both Chrome and Firefox are open, the notification in Firefox comes just normal.

    What could be the cause of this ? Is it something you've noticed or maybe something goes wrong.

    I've tested this theory like 7 times

    opened by robertuniqid 14
  • Rebuild/ng

    Rebuild/ng

    This PR is a new implementation of the Web Push Notifications. It completely breaks the previous implementation. It aims to address most of concerns and bugs that have been reported here and that are namely:

    The library is slow

    This new version uses PSR-6 based caching system and allow VAPID header and payload encryption keys to be reused. The result is that it takes less than 1ms (~) to compute the Authorization header (JWT) and encrypt the payload (AES GCM).

    | benchmark | subject | set | revs | its | mem_peak | best | mean | mode | worst | stdev | rstdev | diff | |---------------|-----------------------------------------|-----|------|-----|-------------|-------------|-------------|-------------|-------------|---------|--------|-------| | AES128GCMPaddingBench | encodeWithoutPadding | 0 | 4096 | 1 | 6,567,624b | 844.651μs | 844.651μs | 844.651μs | 844.651μs | 0.000μs | 0.00% | 5.60x | | AES128GCMPaddingBench | encodeWithRecommendedPadding | 0 | 4096 | 1 | 7,555,112b | 894.495μs | 894.495μs | 894.495μs | 894.495μs | 0.000μs | 0.00% | 5.93x | | AES128GCMPaddingBench | encodeWithMaximumPadding | 0 | 4096 | 1 | 7,555,488b | 905.728μs | 905.728μs | 905.728μs | 905.728μs | 0.000μs | 0.00% | 6.00x | | WebTokenBench | sendNotificationWithoutPayload | 0 | 4096 | 1 | 22,605,992b | 351.297μs | 351.297μs | 351.297μs | 351.297μs | 0.000μs | 0.00% | 2.33x | | WebTokenBench | sendNotificationWithoutPayloadWithCache | 0 | 4096 | 1 | 22,625,784b | 159.154μs | 159.154μs | 159.154μs | 159.154μs | 0.000μs | 0.00% | 1.06x | | WebTokenBench | sendNotificationWithPayload | 0 | 4096 | 1 | 50,266,256b | 1,313.706μs | 1,313.706μs | 1,313.706μs | 1,313.706μs | 0.000μs | 0.00% | 8.71x | | WebTokenBench | sendNotificationWithPayloadWithCache | 0 | 4096 | 1 | 50,156,568b | 669.178μs | 669.178μs | 669.178μs | 669.178μs | 0.000μs | 0.00% | 4.44x | | LcobucciBench | sendNotificationWithoutPayload | 0 | 4096 | 1 | 35,674,072b | 335.454μs | 335.454μs | 335.454μs | 335.454μs | 0.000μs | 0.00% | 2.22x | | LcobucciBench | sendNotificationWithoutPayloadWithCache | 0 | 4096 | 1 | 22,374,880b | 150.849μs | 150.849μs | 150.849μs | 150.849μs | 0.000μs | 0.00% | 1.00x | | LcobucciBench | sendNotificationWithPayload | 0 | 4096 | 1 | 62,682,896b | 1,270.005μs | 1,270.005μs | 1,270.005μs | 1,270.005μs | 0.000μs | 0.00% | 8.42x | | LcobucciBench | sendNotificationWithPayloadWithCache | 0 | 4096 | 1 | 50,069,072b | 631.355μs | 631.355μs | 631.355μs | 631.355μs | 0.000μs | 0.00% | 4.19x | | AESGCMPaddingBench | encodeWithoutPadding | 0 | 4096 | 1 | 6,221,536b | 802.347μs | 802.347μs | 802.347μs | 802.347μs | 0.000μs | 0.00% | 5.32x | | AESGCMPaddingBench | encodeWithRecommendedPadding | 0 | 4096 | 1 | 7,556,272b | 855.086μs | 855.086μs | 855.086μs | 855.086μs | 0.000μs | 0.00% | 5.67x | | AESGCMPaddingBench | encodeWithMaximumPadding | 0 | 4096 | 1 | 7,560,344b | 877.718μs | 877.718μs | 877.718μs | 877.718μs | 0.000μs | 0.00% | 5.82x |

    Cannot Upgrade XXXXX To …

    This new version mostly relies on PSR interfaces:

    • PSR-3: logging
    • PSR-6: caching
    • PSR-7: HTTP Messages
    • PSR-17: HTTP Factories
    • PSR-18: HTTP Client

    You are now free to use Guzzle, HTTPlug, Symfony Client… Regarding the JWT computation, this library provides a web-token wrapper and now a lcobucci/jwt one. You are free to use one or the other. If it does not fit on your needs, you can easily use the JWT provider you want by installing it and implementing the correct interface.

    Mutation Testing

    It is important to test the library, but also important to test the tests. This new version now uses Github Actions to perform the following tests:

    • Coding Standards (as done in early versions)
    • Tests with Code Coverage (as done in early versions)
    • Quality (as done in early versions)
    • Benchmark (new, see above)
    • Security (new)
    • Mutation Testing (new)

    Remaining Tasks

    • [x] Documentation typos
    • [x] Add a section regarding the keys (how to create)
    opened by Spomky 13
  • Return results as a \Generator

    Return results as a \Generator

    As discussed in #183:

    • [x] provide a mapped request/response report when sending a bunch of messages
    • [x] utilize the \Generator object for better performance
    • [x] add some documentation on running the tests for making contributions easier
    • [x] update documentation on sending requests in bulk
    opened by t1gor 13
  • some subscribers get 403 Error

    some subscribers get 403 Error

    Library works well for 90% of records, send to chrome|firefox|edge but some subscribers get this errors: 403 Forbidden response: the key in the authorization header does not correspond to the sender ID used to subscribe this user`

    opened by lrnzfrr 0
  • Pass a custom variable to flush()

    Pass a custom variable to flush()

    We are using the following simple code to flush the Queue:

    foreach( $webPush->flush() as $report ){ .... }

    The problem is, we need our own variables (the UserID) inside the loop. Is there a way to pass custom variables into the loop? Maybe so that it is available in $report

    opened by fx1234 0
  • Get subscription from report (in case of error)

    Get subscription from report (in case of error)

    NOTE: Please test in a least two browsers (i.e. Chrome and Firefox). This helps with diagnosing problems quicker.

    Please confirm the following:

    • [x] I have read the README entirely
    • [x] I have verified in the issues that my problem hasn't already been resolved

    Setup

    Please provide the following details, the more info you can provide the better.

    • Operating System: Linux
    • PHP Version: 8.1.8
    • web-push-php Version: Good question, were to find it? (It's not in the source)

    Please check that you have installed and enabled these PHP extensions :

    • [x] gmp
    • [x] mbstring
    • [x] curl
    • [x] openssl

    Please select any browsers that you are experiencing problems with:

    • [ ] Chrome
    • [ ] Firefox
    • [ ] Firefox for Mobile
    • [ ] Opera for Android
    • [ ] Samsung Internet Browser
    • [x] Other

    Server side, general concept.

    Problem

    When iterating over reports after $webPush->flush() how to find which subscription the report belongs to? I'd like to clean any 410 Gone subscriptions.

    Expected

    I would expect something like $report->getSubscription() to be able to get the subscription from the report and handle errors.

    Features Used

    • [x] VAPID Support
    • [x] Sending with Payload

    Example / Reproduce Case

    /**
    * Check sent results
    * @var MessageSentReport $report
    */
    foreach ($webPush->flush() as $report) {
      $endpoint = $report->getRequest()->getUri()->__toString();
      
      if (!$report->isSuccess()) {
    
        // Here you want to handle 410 Gone errors by disabling the subscription (e.g. in the dbase)
    
        $sub = $report->getSubscription();
        handleExpiredSubscription($sub);
      }
    }
    

    Other

    Please put any remaining notes here.

    opened by MaartenW 0
  • Shared device question

    Shared device question

    As i understood, endpoint is done against browser. So what happens if 2 users use same browser ?

    user1 subscribe to notifications; then log out user2 use the same device (let say a desktop) and same browser and subscribe to notifications user2 will receive user1 notifications ! coming back user1 will receive user2 notifications... I know a solution could be to unregister service worker on user1 logout, but what about session expiration and / or not logged out user (just close pwa) ? And i imagine unregister sw could have related miss configuration (no more notification ?)

    I'm not sure to be clear, but did someone have this situation to take care ? Thanks

    opened by stefanKP50 2
  • event.data is always null

    event.data is always null

    PHP Version: 7.4.26 web-push-php Version: 7.0.0 Browser: Microsoft Edge v104.0.1271.2

    php code like this : $notification = [ 'subscription' => Subscription::create([ 'endpoint' => 'https://sg2p.notify.windows.com/...', // Edge, 'publicKey' => '...', // base 64 encoded, should be 88 chars 'authToken' => '...', // base 64 encoded, should be 24 chars ]), 'payload' => json_encode([ "title"=>"have news" ]), ]

    Problem

    The browser can receive notification, but the event.data is always null.

    opened by kkCode 2
  • cURL error 28: NSS: client certificate not found (nickname not specified)

    cURL error 28: NSS: client certificate not found (nickname not specified)

    • [x] I have read the README entirely
    • [x] I have verified in the issues that my problem hasn't already been resolved

    Setup

    • Operating System: CentOS 7
    • PHP Version: 8.1
    • web-push-php Version: 7.0.0

    Please check that you have installed and enabled these PHP extensions :

    • [x] gmp
    • [x] mbstring
    • [x] curl
    • [x] openssl

    Problem

    Occasionally, I'm seeing this error being reported when trying to send Push Notifications:

    cURL error 28: NSS: client certificate not found (nickname not specified) (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)

    only for endpoints starting with:

    https://****.notify.windows.com/w/?token=

    Expected

    Hopefully we wouldn't see an error. I'm not sure what is causing this.

    Features Used

    • [x] VAPID Support
    • [x] Sending with Payload
    opened by ghnp5 0
Releases(v7.0.0)
  • v7.0.0(Mar 29, 2022)

    What's Changed

    There is no breaking change apart PHP 7.2 being now unsupported. @marc1706 did an awesome job bringing back CI testing on the repo, it means pull requests are now easily mergeable. If you have ideas for the library, now is a great time to contribute! (please propose small iterative changes 🙂)

    • Dropped support for PHP 7.2 by @stephanvierkant in https://github.com/web-push-libs/web-push-php/pull/342
    • Use GitHub Actions for testing by @marc1706 in https://github.com/web-push-libs/web-push-php/pull/330 (huge thanks!)
    • Checks PHP version bebore requiring GMP by @ivanwitzke in https://github.com/web-push-libs/web-push-php/pull/346
    • chore: allow jwt-framework 3.0 by @joostdebruijn in https://github.com/web-push-libs/web-push-php/pull/350

    New Contributors

    • @stephanvierkant made their first contribution in https://github.com/web-push-libs/web-push-php/pull/342
    • @marc1706 made their first contribution in https://github.com/web-push-libs/web-push-php/pull/330
    • @ivanwitzke made their first contribution in https://github.com/web-push-libs/web-push-php/pull/346
    • @joostdebruijn made their first contribution in https://github.com/web-push-libs/web-push-php/pull/350

    Full Changelog: https://github.com/web-push-libs/web-push-php/compare/v6.0.7...v7.0.0

    Source code(tar.gz)
    Source code(zip)
  • v6.0.7(Nov 22, 2021)

    What's Changed

    • More precise typing on PHP 8.1 by @chrisdeeming in https://github.com/web-push-libs/web-push-php/pull/340

    Full Changelog: https://github.com/web-push-libs/web-push-php/compare/v6.0.6...v6.0.7

    Source code(tar.gz)
    Source code(zip)
  • v6.0.6(Nov 22, 2021)

    What's Changed

    • Add #[\ReturnTypeWillChange] attribute for PHP 8.x compatibility. by @chrisdeeming in https://github.com/web-push-libs/web-push-php/pull/339

    New Contributors

    • @chrisdeeming made their first contribution in https://github.com/web-push-libs/web-push-php/pull/339

    Full Changelog: https://github.com/web-push-libs/web-push-php/compare/v6.0.5...v6.0.6

    Source code(tar.gz)
    Source code(zip)
  • v6.0.5(Apr 8, 2021)

    • PHP 8 doesn't throw notices anymore for openssl_pkey_free (thx @smujaddid!)
    • Fix error when HTTP call errored with a ConnectException instead of a RequestException (thx @rupamjbordoloi!) (see root cause)
    Source code(tar.gz)
    Source code(zip)
  • v6.0.4(Apr 8, 2021)

  • v6.0.3(Nov 6, 2020)

  • v6.0.2(Sep 29, 2020)

  • v6.0.1(Sep 13, 2020)

    • Support Guzzle 7 (and thus Laravel 8) (thx @martijnb92!) : a specific PR to decouplate web-push-php from Guzzle and use psr/http-client is most welcome 🙏🏻
    • Hotfix for encryption (thx @Spomky!)
    Source code(tar.gz)
    Source code(zip)
  • v6.0.0(Aug 2, 2020)

    • [Breaking Change] New API : sendOneNotification and queueNotification (see Usage in README)
    • [Breaking Change] PHP 7.2+ is now required
    • Upgrade web-token dependency (thx @baer95!)
    • Remove deprecated GCM auth support (thx @BR0kEN-!)
    Source code(tar.gz)
    Source code(zip)
  • v5.2.5(Aug 2, 2020)

  • v5.2.4(Mar 23, 2019)

  • v5.2.3(Feb 25, 2019)

  • v5.2.2(Feb 23, 2019)

  • v5.2.1(Feb 23, 2019)

  • v5.2.0(Feb 23, 2019)

    • added ability to cache VAPID headers in a request (thx @javiermarinros!)
    • enhanced message reporting (thx @t1gor!)
    • made flush really async (thx @marcvdm!)
    • fixed an encryption error that would occur when using the new subscription structure with a contentEncoding (thx @soerenuhrbach!)
    • fixed an error that would occur if network request failed (thx @steffenweber!)
    • fixed some static errors
    Source code(tar.gz)
    Source code(zip)
  • v5.1.0(Nov 29, 2018)

  • v5.0.0(Nov 27, 2018)

    Breaking changes

    You now need to iterate over the results of flush in order to actually send the notifications.

    The way you handle the results of the flush() method should be changed. In v5 the flush always returns a Generator object. This means that you can still iterate over it, but you wouldn't be able to store it, at least not as-is. Using the example from the docs, the responses would now look different:

    PLEASE NOTE: \Generator is returned even if you only send one message.

    BEFORE:

    $res = [
        [ // first notification (failed)
            'success' => false,
            'endpoint' => $theEndpointToDeleteInYourDatabaseIfExpired,
            'message' => $responseMessage,
            'statusCode' => $responseStatusCode,
            'headers' => $responseHeaders,
            'content' => $responseContent, // you may have more infos here
            'expired' => $isTheEndpointWrongOrExpired,
        ],
        [ // second notification (succeeded)
            'success' => true,
        ],
        [ // third notification
            ...
        ], ...
    ];
    

    AFTER:

    var_dump($res); // \Generator
    
    /** \Minishlink\WebPush\MessageSentReport */
    foreach ($res as $result) {
        // you now have access to request & response objects
    
        /** @var \Psr\Http\Message\RequestInterface $request */
        $request = $result-> getRequest();
        /** @var \Psr\Http\Message\ResponseInterface $response */
        $response = $result->getResponse();
    
        if ($result->isSuccess()) {
            // process successful message sent
            $logger->log('Notification with payload %s successfully sent for endpoint %s.' [
                json_decode((string) $response->getBody()),
                $result->getEndpoint()
            ]);
        } else {
            // or a failed one - check expiration first
            if ($result->isSubscriptionExpired()) {
                // this is just an example code, not included in library!
                $db->markExpired($result->getEndpoint());
            } else {
                // process faulty message
                $logger->log('Notification failed: %s. Payload: %s, endpoint: %s' [
                    $result->getReason(),
                    json_decode((string) $response->getBody()),
                    $result->getEndpoint()
                ]);
            }
        }
    }
    
    Source code(tar.gz)
    Source code(zip)
  • v4.0.2(May 13, 2018)

  • v4.0.1(Apr 12, 2018)

  • v4.0.0(Mar 17, 2018)

    See migration commit in web-push-php-example

    • Support new aes128gcm content encoding (used notably in MS Edge)

    You get your content encoding in your client JS code, and store it in your database alongside the rest of the subscription:

    const contentEncoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0];
    
    • [BREAKING] You must use a new Subscription object in sendNotification

    Before:

    use Minishlink\WebPush\WebPush;
    
    $webPush->sendNotification(
        'endpoint',
        'payload', // optional (defaults null)
        'userPublicKey', // optional (defaults null)
        'userAuthToken' // optional (defaults null)
    );
    

    After:

    use Minishlink\WebPush\WebPush;
    use Minishlink\WebPush\Subscription;
    
    $subcription = Subscription::create([
        'endpoint' => 'endpoint',
        'publicKey' => 'public key', // optional
        'authToken' => 'authToken', // optional
        'contentEncoding' => 'aesgcm', // optional, one of PushManager.supportedContentEncodings
    ]);
    
    $webPush->sendNotification(
        $notification['subscription'],
        $notification['payload'] // optional (defaults null)
    );
    
    Source code(tar.gz)
    Source code(zip)
  • v3.0.2(Mar 17, 2018)

  • v3.0.1(Mar 16, 2018)

  • v3.0.0(Mar 16, 2018)

  • v2.0.1(Nov 22, 2017)

  • v2.0.0(Oct 12, 2017)

  • v1.4.3(Jun 28, 2017)

  • v1.4.2(May 13, 2017)

    • Endpoints that return a 400 HTTP response are no longer considered as expired (thx @marcovtwout) (see #101)
    • Travis testing is fixed (thx @gauntface)
    Source code(tar.gz)
    Source code(zip)
  • v1.4.1(Mar 13, 2017)

  • v1.4.0(Feb 11, 2017)

    • Use Guzzle instead of Buzz (~30% speed increase)
      • [BC Break] removed getBrowser() and setBrowser()
      • [BC Break] last argument of constructor takes Guzzle Client RequestOptions array instead of Buzz Client

    Feedbacks are welcome!

    Source code(tar.gz)
    Source code(zip)
  • v1.3.4(Nov 30, 2016)

Owner
null
This package makes it easy to send web push notifications with Laravel.

Web push notifications channel for Laravel This package makes it easy to send web push notifications with Laravel. Installation You can install the pa

Laravel Notification Channels 564 Jan 3, 2023
Send push notifications to apple devices (iPhone, iPad, iPod).

Apple Apn Push Send push notifications to apple devices (iPhone, iPad, iPod). Support authenticators: Certificate Json Web Token Supported protocols:

Vitaliy Zhuk 157 Dec 1, 2022
Larafirebase is a package thats offers you to send push notifications or custom messages via Firebase in Laravel.

Introduction Larafirebase is a package thats offers you to send push notifications or custom messages via Firebase in Laravel. Firebase Cloud Messagin

Kutia Software Company 264 Jan 7, 2023
Send Firebase push notifications with Laravel php framework.

FCM Notification Channel for Laravel Send Firebase push notifications with Laravel php framework. Installation You can install this package via compos

Ankur Kumar 23 Oct 31, 2022
Bundle around the WebPush library

MinishlinkWebPushBundle This bundle provides a simple integration of the WebPush library. Usage Web Push sends notifications to endpoints which server

Louis Lagrange 35 Dec 22, 2022
Standalone PHP library for easy devices notifications push.

NotificationPusher Standalone PHP library for easy devices message notifications push. Feel free to contribute! Thanks. Contributors Cédric Dugat (Aut

Cédric Dugat 1.2k Jan 3, 2023
Takes care of Apple push notifications (APNS) in your PHP projects.

Notificato Notificato takes care of push notifications in your PHP projects. Italian: notificato è: participio passato English: notified Why use Notif

Mathijs Kadijk 223 Sep 28, 2022
Push notifications Library for PHP

Push notifications Library for PHP Supported Protocols Protocol Supported Driver Options APNs (Token Based) ✓ APNs\Token APNs\Token\Option APNs (Certi

Norifumi SUNAOKA 3 Dec 14, 2022
Push Notifications using Laravel

laravel-push-notification Push Notifications using Laravel PushNotification::send(['deviceToken1', 'deviceToken2',..], 'Notification Message', 'Action

Webelight Solutions 26 Jul 22, 2022
Laravel package to enable sending push notifications to devices

Laravel Push Notification Package to enable sending push notifications to devices Installation Update your composer.json file to include this package

Davi Nunes 1.2k Sep 27, 2022
Takes care of Apple push notifications (APNS) in your PHP projects.

Notificato Notificato takes care of push notifications in your PHP projects. Italian: notificato è: participio passato English: notified Why use Notif

Mathijs Kadijk 223 Sep 28, 2022
Standalone PHP library for easy devices notifications push.

NotificationPusher Standalone PHP library for easy devices message notifications push. Feel free to contribute! Thanks. Contributors Cédric Dugat (Aut

Cédric Dugat 1.2k Jan 3, 2023
:computer: Send notifications to your desktop directly from your PHP script

About JoliNotif JoliNotif is a cross-platform PHP library to display desktop notifications. It works on Linux, Windows or MacOS. Requires PHP >= 7.2 (

JoliCode 1.2k Dec 29, 2022
Notifications in PHP (notify-send, growl, etc) like that.

#Nod Notifications in PHP (notify-send, growl, etc) like that. ##Examples Letting Nod figure out the best Adapter to use (not recommend ATM, only work

Filipe Dobreira 51 Mar 26, 2019
This package allows you to send notifications to Microsoft Teams.

Teams connector This package allows you to send notifications to Microsoft Teams. Installation You can install the package using the Composer package

skrepr 20 Oct 4, 2022
This package makes it easy to send notifications using RocketChat with Laravel 9.0+.

laravel-rocket-chat-notifications Introduction This package makes it easy to send notifications using RocketChat with Laravel 9.0+. Contents Installat

Team Nifty GmbH 25 Dec 1, 2022
Service that helps you to send notifications for a series of failed exceptions.

Laravel Failure Notifier This package helps you to track your exceptions and do what you want to do with them such as sending an SMS or and Email. You

Kamyar Gerami 7 Nov 26, 2022
It's Pimcore Bundle to send notifications to Google Chat, Slack or Email from admin panel inside Pimcore

Send notifications to Discord, Google Chat, Slack and more from Pimcore It's Pimcore Bundle to send notifications to Discord, Google Chat, Slack, Tele

LemonMind.com 6 Aug 31, 2022
ApnsPHP: Apple Push Notification & Feedback Provider

ApnsPHP: Apple Push Notification & Feedback Provider A full set of open source PHP classes to interact with the Apple Push Notification service for th

Immobiliare Labs 1.4k Nov 16, 2022