diff --git a/README.md b/README.md index f6592d0aa6f3e14d67c83609887712667971f108..e30109d41a629ff5ca95eb794d4e1a8701fd8c15 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,15 @@ Security optimization: ## Example ``` - \Drupal::service('web_push.manager')->sendNotification( + $subscriptionIds = [1, 2, 3]; // There can be any logic for obtaining subscription IDs. + \Drupal::service('web_push.manager')->sendNotification( 'Notification Title', 'Notification Body', 'noticiation-redirect-url', 'notification-icon-url', - \Drupal\web_push\Service\WebPushSender::URGENCY_HIGH + \Drupal\web_push\Service\WebPushSender::URGENCY_HIGH, + 'myTopic', // Only alphanumeric character, limited to 32 characters. + $subscriptionIds // Can be Null to send to every subscription. ); ``` diff --git a/composer.json b/composer.json index 3f3e12f5e744d18dfa30fb87fd4cf00e829c56ec..e67df5b7ff23385c267dd5a07c8eaed94bdd2cac 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "ext-mbstring": "*", "ext-curl": "*", "ext-openssl": "*", - "minishlink/web-push": "^8.0" + "minishlink/web-push": "^9.0" }, "require-dev": { "phpunit/phpunit": "5.*" diff --git a/src/Controller/WebPushController.php b/src/Controller/WebPushController.php index cee8a79349565c17f0b3e9f75a140baf33dc3418..785e3e0b6adee770bcb693469d309286565d2350 100644 --- a/src/Controller/WebPushController.php +++ b/src/Controller/WebPushController.php @@ -5,8 +5,6 @@ namespace Drupal\web_push\Controller; use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Extension\ModuleHandler; -use Drupal\Core\Url; -use Drupal\web_push\Form\SettingsForm; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -17,6 +15,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; class WebPushController extends ControllerBase { /** + * The module handler service. + * * @var \Drupal\Core\Extension\ModuleHandler */ protected $moduleHandler; @@ -40,12 +40,11 @@ class WebPushController extends ControllerBase { ); } - /** * Get the service worker javascript handler. * * @return \Drupal\Core\Cache\CacheableResponse - * The service worker content. + * The service worker content. */ public function serviceWorker() { // Get the actual module path. @@ -57,9 +56,7 @@ class WebPushController extends ControllerBase { 'Content-Type' => 'application/javascript', 'Service-Worker-Allowed' => '/', ]); - // TODO add cache metadata - // $response->addCacheableDependency($cacheable_metadata); - + // @todo add cache metadata return $response; } diff --git a/src/Entity/Subscription.php b/src/Entity/Subscription.php index 012486b69432622d433330f8426af7a9a96d965c..5d65101413fd81a8bf9bd7bb99567b4cdf0c9e53 100644 --- a/src/Entity/Subscription.php +++ b/src/Entity/Subscription.php @@ -2,9 +2,9 @@ namespace Drupal\web_push\Entity; -use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; /** * Defines the Notification subscription entity. @@ -134,5 +134,4 @@ class Subscription extends ContentEntityBase { return $fields; } - } diff --git a/src/Entity/SubscriptionListBuilder.php b/src/Entity/SubscriptionListBuilder.php index d5cc9d7bfdd72ebf4915868286778974b4de4195..621d53a50be6521639e9c32f5f5e1669949906b9 100644 --- a/src/Entity/SubscriptionListBuilder.php +++ b/src/Entity/SubscriptionListBuilder.php @@ -3,11 +3,11 @@ namespace Drupal\web_push\Entity; use Drupal\Core\Datetime\DateFormatterInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityListBuilder; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a list controller for web_push_subscription entity. @@ -51,7 +51,6 @@ class SubscriptionListBuilder extends EntityListBuilder { ); } - /** * {@inheritdoc} * @@ -78,4 +77,4 @@ class SubscriptionListBuilder extends EntityListBuilder { return $row + parent::buildRow($entity); } -} \ No newline at end of file +} diff --git a/src/Form/SecurityForm.php b/src/Form/SecurityForm.php index e1af22d5eadd998fd4383dd93a6b986280457f71..1318b5687f1a4e2577bd905b8c158e3a6f3c7f20 100644 --- a/src/Form/SecurityForm.php +++ b/src/Form/SecurityForm.php @@ -59,7 +59,7 @@ class SecurityForm extends ConfigFormBase { */ protected function getEditableConfigNames() { return [ - static::$configId + static::$configId, ]; } @@ -98,7 +98,7 @@ class SecurityForm extends ConfigFormBase { ]; $form['actions'] = [ - '#type' => 'actions' + '#type' => 'actions', ]; $form['actions']['save'] = [ '#type' => 'submit', diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index 4d128e812be93411a905eb64c57b3206897632d0..623a7f16a33448b1e4ea9d93f3989bb261ab7428 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -61,7 +61,7 @@ class SettingsForm extends ConfigFormBase { */ protected function getEditableConfigNames() { return [ - static::$configId + static::$configId, ]; } @@ -105,12 +105,14 @@ class SettingsForm extends ConfigFormBase { '#type' => 'textfield', '#title' => $this->t('Topic'), '#description' => $this->t( - 'Topics are strings that can be used to replace a pending messages with a new message + 'The string must contain only alphanumeric characters. + Topics are strings that can be used to replace a pending messages with a new message if they have matching topic names. This is useful in scenarios where multiple messages are sent while a device is offline, and you really only want a user to see the latest message when the device is turned on.' ), '#maxlength' => 32, + '#pattern' => '[a-zA-Z0-9]+', '#default_value' => $config->get('topic') ?: '', ]; $form['options']['batchSize'] = [ @@ -140,7 +142,7 @@ class SettingsForm extends ConfigFormBase { ]; $form['actions'] = [ - '#type' => 'actions' + '#type' => 'actions', ]; $form['actions']['save'] = [ '#type' => 'submit', diff --git a/src/Form/SubscriptionDeleteForm.php b/src/Form/SubscriptionDeleteForm.php index b3a6ea40514bf0d7a92bd687a19038e2cf2d22f0..7e2bb71b4a5316092f3810d7749883a5a3d6a196 100644 --- a/src/Form/SubscriptionDeleteForm.php +++ b/src/Form/SubscriptionDeleteForm.php @@ -1,10 +1,5 @@ <?php -/** - * @file - * Contains \Drupal\web_push\Form\SubscriptionDeleteForm. - */ - namespace Drupal\web_push\Form; use Drupal\Core\Entity\ContentEntityConfirmFormBase; @@ -22,7 +17,7 @@ class SubscriptionDeleteForm extends ContentEntityConfirmFormBase { * {@inheritdoc} */ public function getQuestion() { - return $this->t('Are you sure you want to delete entity %token?', array('%token' => $this->entity->getToken())); + return $this->t('Are you sure you want to delete entity %token?', ['%token' => $this->entity->getToken()]); } /** @@ -58,4 +53,5 @@ class SubscriptionDeleteForm extends ContentEntityConfirmFormBase { // Redirect to term list after delete. $form_state->setRedirect('entity.web_push_subscription.collection'); } -} \ No newline at end of file + +} diff --git a/src/Form/TestNotification.php b/src/Form/TestNotification.php index 67e0a1001375c00f1a8fa85c461bd5408cd948b2..3f8c2de55f5545dda3f06daf8e102ce809d33261 100644 --- a/src/Form/TestNotification.php +++ b/src/Form/TestNotification.php @@ -113,13 +113,6 @@ class TestNotification extends FormBase { return $form; } - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - parent::validateForm($form, $form_state); - } - /** * {@inheritdoc} */ @@ -133,10 +126,10 @@ class TestNotification extends FormBase { 'title' => $form_state->getValue('title'), 'body' => $form_state->getValue('body'), 'icon' => $form_state->getValue('icon'), - 'url' => $url + 'url' => $url, ], 'options' => [ - 'urgency' => '' + 'urgency' => '', ], ]; diff --git a/src/Form/VAPIDForm.php b/src/Form/VAPIDForm.php index 422b0b280973538b00b5f45e32d7cfebb93aae97..f21559d4b28b6a291dbb254a80dc8b6dcb3e9311 100644 --- a/src/Form/VAPIDForm.php +++ b/src/Form/VAPIDForm.php @@ -39,6 +39,8 @@ class VAPIDForm extends ConfigFormBase { * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The factory for configuration objects. + * @param \Drupal\web_push\Service\AuthenticationHelper $authentication_helper + * The authentication service helper. */ public function __construct( ConfigFactoryInterface $config_factory, @@ -71,7 +73,7 @@ class VAPIDForm extends ConfigFormBase { */ protected function getEditableConfigNames() { return [ - static::$configId + static::$configId, ]; } @@ -99,25 +101,24 @@ class VAPIDForm extends ConfigFormBase { '#type' => 'textfield', '#title' => $this->t('Public Key'), '#description' => $this->t('(recommended) uncompressed public key P-256 encoded in Base64-URL'), - '#default_value' => $config->get('publicKey') + '#default_value' => $config->get('publicKey'), ]; $form['VAPID']['privateKey'] = [ '#type' => 'textfield', '#title' => $this->t('Private Key'), '#description' => $this->t('(recommended) in fact the secret multiplier of the private key encoded in Base64-URL'), - '#default_value' => $config->get('privateKey') + '#default_value' => $config->get('privateKey'), ]; // No method to add PEM file, PEM file is not recommended. // @see https://github.com/web-push-libs/web-push-php#authentication-vapid. - $form['actions'] = [ - '#type' => 'actions' + '#type' => 'actions', ]; $form['actions']['generate'] = [ '#type' => 'submit', '#value' => $this->t('Generate and Save VAPID Keys'), - '#submit' => ['::generateVAPIDKeys'] + '#submit' => ['::generateVAPIDKeys'], ]; $form['actions']['save'] = [ '#type' => 'submit', @@ -130,12 +131,12 @@ class VAPIDForm extends ConfigFormBase { /** * Save the keys in config. * - * @param $publicKey + * @param string $publicKey * The public key to save. - * @param $privateKey + * @param string $privateKey * The private key to save. */ - private function saveKeys($publicKey, $privateKey): void { + private function saveKeys(string $publicKey, string $privateKey): void { $config = $this->config(static::$configId); $config ->set('publicKey', $publicKey) diff --git a/src/Plugin/Block/WebPushBlock.php b/src/Plugin/Block/WebPushBlock.php index dc60972d7de6af27baafcc6e996cddefd66670cf..e4651f0bfd8a90b7ee64e04959a7e039f17fa61f 100644 --- a/src/Plugin/Block/WebPushBlock.php +++ b/src/Plugin/Block/WebPushBlock.php @@ -79,7 +79,7 @@ class WebPushBlock extends BlockBase implements ContainerFactoryPluginInterface * {@inheritdoc} */ public function build() { - $build = [ + $build = [ '#theme' => 'web_push_subscription', '#attached' => ['library' => ['web_push/manual_subscrition']], ]; diff --git a/src/Plugin/QueueWorker/WebPushQueueWorker.php b/src/Plugin/QueueWorker/WebPushQueueWorker.php index 8c65eb27f89dd7bd3dfe8062027c0f9ccb687ac7..94fd03e1216aa4822d28c7792e6fe64d511163f4 100644 --- a/src/Plugin/QueueWorker/WebPushQueueWorker.php +++ b/src/Plugin/QueueWorker/WebPushQueueWorker.php @@ -64,4 +64,5 @@ class WebPushQueueWorker extends QueueWorkerBase implements ContainerFactoryPlug public function processItem($pushData) { $this->sender->sendNotification($pushData); } -} \ No newline at end of file + +} diff --git a/src/Plugin/rest/resource/SubscriptionResource.php b/src/Plugin/rest/resource/SubscriptionResource.php index 75ec549cd02dd7aba675183a33f56cd72b90a6d2..2438921ec8ae211946c65323ce92992db8ac9081 100644 --- a/src/Plugin/rest/resource/SubscriptionResource.php +++ b/src/Plugin/rest/resource/SubscriptionResource.php @@ -8,9 +8,9 @@ use Drupal\Core\Flood\FloodInterface; use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; use Drupal\web_push\Form\SecurityForm; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** * Provides a resource for Subscription. @@ -59,10 +59,8 @@ class SubscriptionResource extends ResourceBase { * The available serialization formats. * @param \Psr\Log\LoggerInterface $logger * A logger instance. - * @param \Drupal\Core\Session\AccountProxyInterface $current_user - * A current user instance. - * @param \Drupal\Core\Session\AccountProxyInterface $current_user - * A current user instance. + * @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager + * An entity type manager instance. * @param \Drupal\Core\Flood\FloodInterface $flood * The flood service. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory @@ -133,7 +131,7 @@ class SubscriptionResource extends ResourceBase { if (!empty($key) && !empty($token) && !empty($endpoint)) { $ids = $this->entityStorage->loadByProperties([ 'key' => $key, - 'token' => $token + 'token' => $token, ]); if (empty($ids)) { $subscription = $this->entityStorage->create([ @@ -149,6 +147,6 @@ class SubscriptionResource extends ResourceBase { } return new ResourceResponse('OK', 200); - } + } diff --git a/src/Service/AuthenticationHelper.php b/src/Service/AuthenticationHelper.php index de668676ff8e456fe925348bbdec4da12f31846d..5ce11edd14c598bd8d63178644164033e83f7ecc 100644 --- a/src/Service/AuthenticationHelper.php +++ b/src/Service/AuthenticationHelper.php @@ -8,6 +8,9 @@ use Drupal\web_push\Form\VAPIDForm; use Minishlink\WebPush\VAPID; use Psr\Log\LoggerInterface; +/** + * The athentication service helper. + */ class AuthenticationHelper { /** @@ -37,13 +40,13 @@ class AuthenticationHelper { $this->config = $config_factory->get(VAPIDForm::$configId); } - /** * Generate Keys. * * @return array + * The VAPID keys. */ - public function genrateKeys() { + public function genrateKeys(): array { try { return VAPID::createVapidKeys(); } @@ -57,14 +60,16 @@ class AuthenticationHelper { * Build the authentitcation for push. * * @return array + * The auth informations. */ - public function getAuth() { + public function getAuth(): array { return [ 'VAPID' => [ 'subject' => Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString(), 'publicKey' => $this->config->get('publicKey'), 'privateKey' => $this->config->get('privateKey'), - ] + ], ]; } + } diff --git a/src/Service/WebPushManager.php b/src/Service/WebPushManager.php index 1b8005b56cb9f04f7e7d9a4e9da29275819989cd..c4c64168576b9c743f1a5a3fbfc5ce7f92738034 100644 --- a/src/Service/WebPushManager.php +++ b/src/Service/WebPushManager.php @@ -3,8 +3,8 @@ namespace Drupal\web_push\Service; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Url; use Drupal\Core\Queue\QueueWorkerManagerInterface; +use Drupal\Core\Url; use Drupal\web_push\Form\SettingsForm; use Psr\Log\LoggerInterface; @@ -54,9 +54,6 @@ class WebPushManager { $this->queueWorkerManager = $queue_worker_manager; } - /** - * {@inheritdoc} - */ /** * Send the notification to the queue. * @@ -70,30 +67,35 @@ class WebPushManager { * The icon url of the notification. * @param string $urgency * The urgency of the notification. + * @param string $topic + * The topic of the notification. + * @param array|null $subscriptionIds + * The subscriptions ids or NULL for all. */ public function sendNotification( string $title, string $body, string $url = '', string $icon = '', - string $urgency = '' + string $urgency = '', + string $topic = '', + ?array $subscriptionIds = NULL ): void { if (empty($url)) { $url = Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString(); } - if (empty($icon)) { - // TODO get default icon. - } $pushData = [ 'content' => [ 'title' => $title, 'body' => $body, 'icon' => $icon, - 'url' => $url + 'url' => $url, ], 'options' => [ - 'urgency' => $urgency + 'urgency' => $urgency, + 'topic' => $topic, ], + 'subscriptionIds' => $subscriptionIds, ]; $worker = $this->queueWorkerManager->createInstance('web_push'); diff --git a/src/Service/WebPushSender.php b/src/Service/WebPushSender.php index 660dcae6980452eb64c7d44cedbb6ac5f4de755c..ca4295be7e8102c1e141d8cbb47b5c1be7306eb5 100644 --- a/src/Service/WebPushSender.php +++ b/src/Service/WebPushSender.php @@ -7,10 +7,9 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityTypeManager; use Drupal\web_push\Entity\Subscription; use Drupal\web_push\Form\SettingsForm; -use Drupal\web_push\Service\AuthenticationHelper; use Minishlink\WebPush\MessageSentReport; -use Minishlink\WebPush\WebPush; use Minishlink\WebPush\Subscription as WebPushSubscription; +use Minishlink\WebPush\WebPush; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -106,13 +105,13 @@ class WebPushSender { 'TTL' => $this->config->get('ttl'), 'urgency' => $this->config->get('urgency'), 'topic' => $this->config->get('topic'), - 'batchSize' => (int) $this->config->get('batchSize') + 'batchSize' => (int) $this->config->get('batchSize'), ]; try { $this->webPush = new WebPush($auth, $options); $this->webPush->setDefaultOptions($options); // @see https://github.com/web-push-libs/web-push-php#reusing-vapid-headers. - $this->webPush->setReuseVAPIDHeaders(true); + $this->webPush->setReuseVAPIDHeaders(TRUE); } catch (\ErrorException $e) { $this->logger->error($e->getMessage()); @@ -122,7 +121,7 @@ class WebPushSender { /** * Build the Subscription Notification to send. * - * @param Subscription $subscription + * @param \Drupal\web_push\Entity\Subscription $subscription * The Drupal subscription details. * @param array $pushData * The data to send. @@ -134,7 +133,7 @@ class WebPushSender { $webSubscription['subscription'] = WebPushSubscription::create([ 'endpoint' => $subscription->getEndpoint(), 'publicKey' => $subscription->getPublicKey(), - 'authToken' => $subscription->getToken() + 'authToken' => $subscription->getToken(), ]); // Check the URL to avoid redirection outside the site. $urlHost = parse_url($pushData['url'], PHP_URL_HOST); @@ -154,15 +153,19 @@ class WebPushSender { * * @param array $pushData * The data content. - * @param string $urgency + * @param array $optionsCustom * The notification urgency. + * @param array|null $subscriptionIds + * List of subscription Ids to load. */ - protected function prepareSubscriptions(array $pushData, string $urgency = ''): void { - $listSubscriptions = $this->entityStorage->loadMultiple(); + protected function prepareSubscriptions(array $pushData, array $optionsCustom = [], ?array $subscriptionIds = NULL): void { + $listSubscriptions = $this->entityStorage->loadMultiple($subscriptionIds); foreach ($listSubscriptions as $subscription) { $webSubscription = $this->buildSubscription($subscription, $pushData); $options = $this->webPush->getDefaultOptions(); - $options['urgency'] = $urgency ? $urgency : $options['urgency']; + + $options['urgency'] = array_key_exists('urgency', $optionsCustom) && $optionsCustom['urgency'] ? $optionsCustom['urgency'] : $options['urgency']; + $options['topic'] = array_key_exists('topic', $optionsCustom) && $optionsCustom['topic'] ? $optionsCustom['topic'] : $options['topic']; try { $this->webPush->queueNotification( $webSubscription['subscription'], @@ -179,7 +182,7 @@ class WebPushSender { /** * Manage action on result event. * - * @param MessageSentReport $report + * @param \Minishlink\WebPush\MessageSentReportMessageSentReport $report * The result of the notification send. */ protected function manageReport(MessageSentReport $report): void { @@ -193,9 +196,8 @@ class WebPushSender { } if ($report->isSubscriptionExpired()) { // Get the subscription to delete. - // TODO : optimize to avoid the load of one entity by one. $subscriptionList = $this->entityStorage->loadByProperties(['endpoint' => $endpoint]); - foreach($subscriptionList as $subscription) { + foreach ($subscriptionList as $subscription) { $subscription->delete(); } } @@ -209,7 +211,7 @@ class WebPushSender { */ public function sendNotification(array $pushData): void { $this->singletonWebPush(); - $this->prepareSubscriptions($pushData['content'], $pushData['options']['urgency']); + $this->prepareSubscriptions($pushData['content'], $pushData['options'], $pushData['subscriptionIds']); // Send the queued items by prepareSubscriptions(). // And check message for each notifications. diff --git a/web_push.info.yml b/web_push.info.yml index 11906ca09927982a48f13fa81f967197de0c11d8..c9f296d51a77f47b0246382b55b08cf9553a35f6 100644 --- a/web_push.info.yml +++ b/web_push.info.yml @@ -3,7 +3,7 @@ description: Sends web push notifications to users. package: Notification type: module -core_version_requirement: ^9.5 || ^10 +core_version_requirement: ^9.5 || ^10 || ^11 dependencies: - drupal:rest diff --git a/web_push.links.tasks.yml b/web_push.links.tasks.yml index 63f6cf50c693b4862a2b6b2e8f0df2a445cf7bee..169248ef42ae908cde0aa93916ac4aefe899d09d 100644 --- a/web_push.links.tasks.yml +++ b/web_push.links.tasks.yml @@ -2,4 +2,4 @@ entity.dictionary_term.delete_confirm: route_name: entity.web_push_subscription.delete_form base_route: entity.web_push_subscription.collection title: Delete - weight: 10 \ No newline at end of file + weight: 10 diff --git a/web_push.module b/web_push.module index e8c5c908d62c12611c7900253b82eda3b43ce06f..4d88990953661807ed38c2f9868ec278276159d7 100644 --- a/web_push.module +++ b/web_push.module @@ -1,5 +1,10 @@ <?php +/** + * @file + * Primary module hooks for Web Push module. + */ + use Drupal\Core\Url; use Drupal\web_push\Form\SettingsForm; use Drupal\web_push\Form\VAPIDForm;