1<?php declare(strict_types=1); 2 3/* 4 * This file is part of the Monolog package. 5 * 6 * (c) Jordi Boggiano <j.boggiano@seld.be> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Monolog\Handler; 13 14use Monolog\Logger; 15use Monolog\Utils; 16use Psr\Log\LogLevel; 17 18/** 19 * Sends notifications through the pushover api to mobile phones 20 * 21 * @author Sebastian Göttschkes <sebastian.goettschkes@googlemail.com> 22 * @see https://www.pushover.net/api 23 * 24 * @phpstan-import-type FormattedRecord from AbstractProcessingHandler 25 * @phpstan-import-type Level from \Monolog\Logger 26 * @phpstan-import-type LevelName from \Monolog\Logger 27 */ 28class PushoverHandler extends SocketHandler 29{ 30 /** @var string */ 31 private $token; 32 /** @var array<int|string> */ 33 private $users; 34 /** @var string */ 35 private $title; 36 /** @var string|int|null */ 37 private $user = null; 38 /** @var int */ 39 private $retry; 40 /** @var int */ 41 private $expire; 42 43 /** @var int */ 44 private $highPriorityLevel; 45 /** @var int */ 46 private $emergencyLevel; 47 /** @var bool */ 48 private $useFormattedMessage = false; 49 50 /** 51 * All parameters that can be sent to Pushover 52 * @see https://pushover.net/api 53 * @var array<string, bool> 54 */ 55 private $parameterNames = [ 56 'token' => true, 57 'user' => true, 58 'message' => true, 59 'device' => true, 60 'title' => true, 61 'url' => true, 62 'url_title' => true, 63 'priority' => true, 64 'timestamp' => true, 65 'sound' => true, 66 'retry' => true, 67 'expire' => true, 68 'callback' => true, 69 ]; 70 71 /** 72 * Sounds the api supports by default 73 * @see https://pushover.net/api#sounds 74 * @var string[] 75 */ 76 private $sounds = [ 77 'pushover', 'bike', 'bugle', 'cashregister', 'classical', 'cosmic', 'falling', 'gamelan', 'incoming', 78 'intermission', 'magic', 'mechanical', 'pianobar', 'siren', 'spacealarm', 'tugboat', 'alien', 'climb', 79 'persistent', 'echo', 'updown', 'none', 80 ]; 81 82 /** 83 * @param string $token Pushover api token 84 * @param string|array $users Pushover user id or array of ids the message will be sent to 85 * @param string|null $title Title sent to the Pushover API 86 * @param bool $useSSL Whether to connect via SSL. Required when pushing messages to users that are not 87 * the pushover.net app owner. OpenSSL is required for this option. 88 * @param string|int $highPriorityLevel The minimum logging level at which this handler will start 89 * sending "high priority" requests to the Pushover API 90 * @param string|int $emergencyLevel The minimum logging level at which this handler will start 91 * sending "emergency" requests to the Pushover API 92 * @param int $retry The retry parameter specifies how often (in seconds) the Pushover servers will 93 * send the same notification to the user. 94 * @param int $expire The expire parameter specifies how many seconds your notification will continue 95 * to be retried for (every retry seconds). 96 * 97 * @phpstan-param string|array<int|string> $users 98 * @phpstan-param Level|LevelName|LogLevel::* $highPriorityLevel 99 * @phpstan-param Level|LevelName|LogLevel::* $emergencyLevel 100 */ 101 public function __construct( 102 string $token, 103 $users, 104 ?string $title = null, 105 $level = Logger::CRITICAL, 106 bool $bubble = true, 107 bool $useSSL = true, 108 $highPriorityLevel = Logger::CRITICAL, 109 $emergencyLevel = Logger::EMERGENCY, 110 int $retry = 30, 111 int $expire = 25200, 112 bool $persistent = false, 113 float $timeout = 0.0, 114 float $writingTimeout = 10.0, 115 ?float $connectionTimeout = null, 116 ?int $chunkSize = null 117 ) { 118 $connectionString = $useSSL ? 'ssl://api.pushover.net:443' : 'api.pushover.net:80'; 119 parent::__construct( 120 $connectionString, 121 $level, 122 $bubble, 123 $persistent, 124 $timeout, 125 $writingTimeout, 126 $connectionTimeout, 127 $chunkSize 128 ); 129 130 $this->token = $token; 131 $this->users = (array) $users; 132 $this->title = $title ?: (string) gethostname(); 133 $this->highPriorityLevel = Logger::toMonologLevel($highPriorityLevel); 134 $this->emergencyLevel = Logger::toMonologLevel($emergencyLevel); 135 $this->retry = $retry; 136 $this->expire = $expire; 137 } 138 139 protected function generateDataStream(array $record): string 140 { 141 $content = $this->buildContent($record); 142 143 return $this->buildHeader($content) . $content; 144 } 145 146 /** 147 * @phpstan-param FormattedRecord $record 148 */ 149 private function buildContent(array $record): string 150 { 151 // Pushover has a limit of 512 characters on title and message combined. 152 $maxMessageLength = 512 - strlen($this->title); 153 154 $message = ($this->useFormattedMessage) ? $record['formatted'] : $record['message']; 155 $message = Utils::substr($message, 0, $maxMessageLength); 156 157 $timestamp = $record['datetime']->getTimestamp(); 158 159 $dataArray = [ 160 'token' => $this->token, 161 'user' => $this->user, 162 'message' => $message, 163 'title' => $this->title, 164 'timestamp' => $timestamp, 165 ]; 166 167 if (isset($record['level']) && $record['level'] >= $this->emergencyLevel) { 168 $dataArray['priority'] = 2; 169 $dataArray['retry'] = $this->retry; 170 $dataArray['expire'] = $this->expire; 171 } elseif (isset($record['level']) && $record['level'] >= $this->highPriorityLevel) { 172 $dataArray['priority'] = 1; 173 } 174 175 // First determine the available parameters 176 $context = array_intersect_key($record['context'], $this->parameterNames); 177 $extra = array_intersect_key($record['extra'], $this->parameterNames); 178 179 // Least important info should be merged with subsequent info 180 $dataArray = array_merge($extra, $context, $dataArray); 181 182 // Only pass sounds that are supported by the API 183 if (isset($dataArray['sound']) && !in_array($dataArray['sound'], $this->sounds)) { 184 unset($dataArray['sound']); 185 } 186 187 return http_build_query($dataArray); 188 } 189 190 private function buildHeader(string $content): string 191 { 192 $header = "POST /1/messages.json HTTP/1.1\r\n"; 193 $header .= "Host: api.pushover.net\r\n"; 194 $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; 195 $header .= "Content-Length: " . strlen($content) . "\r\n"; 196 $header .= "\r\n"; 197 198 return $header; 199 } 200 201 protected function write(array $record): void 202 { 203 foreach ($this->users as $user) { 204 $this->user = $user; 205 206 parent::write($record); 207 $this->closeSocket(); 208 } 209 210 $this->user = null; 211 } 212 213 /** 214 * @param int|string $value 215 * 216 * @phpstan-param Level|LevelName|LogLevel::* $value 217 */ 218 public function setHighPriorityLevel($value): self 219 { 220 $this->highPriorityLevel = Logger::toMonologLevel($value); 221 222 return $this; 223 } 224 225 /** 226 * @param int|string $value 227 * 228 * @phpstan-param Level|LevelName|LogLevel::* $value 229 */ 230 public function setEmergencyLevel($value): self 231 { 232 $this->emergencyLevel = Logger::toMonologLevel($value); 233 234 return $this; 235 } 236 237 /** 238 * Use the formatted message? 239 */ 240 public function useFormattedMessage(bool $value): self 241 { 242 $this->useFormattedMessage = $value; 243 244 return $this; 245 } 246} 247