diff --git a/README.md b/README.md index 6990231..022c8f1 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Method | Description `time($timestamp)` | Accepts either a `Carbon` object or a UNIX timestamp. `url($url[, $title])` | Accepts a string value for a [supplementary url](https://pushover.net/api#urls) and an optional string value for the title of the url. `sound($sound)` | Accepts a string value for the [notification sound](https://pushover.net/api#sounds). +`image($image)` | Accepts a string value for the image location (either full or relative server path or a URL). If there is any error with the file (too big, not an image) it will silently send the message without the image attachment. `priority($priority[, $retryTimeout, $expireAfter])` | Accepts an integer value for the priority and, when the priority is set to emergency, also an integer value for the retry timeout and expiry time (in seconds). Priority values are available as constants | `PushoverMessage::LOWEST_PRIORITY`, `PushoverMessage::LOW_PRIORITY`, `PushoverMessage::NORMAL_PRIORITY` and `PushoverMessage::EMERGENCY_PRIORITY`. `lowestPriority()` | Sets the priority to the lowest priority. `lowPriority()` | Sets the priority to low. diff --git a/composer.json b/composer.json index cd29e82..04b2fd0 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ } ], "require": { + "ext-exif": "*", "php": ">=7.3", "guzzlehttp/guzzle": "^7.0.1", "illuminate/notifications": "^8.0", diff --git a/src/Pushover.php b/src/Pushover.php index 04e3439..f00a51e 100644 --- a/src/Pushover.php +++ b/src/Pushover.php @@ -10,6 +10,13 @@ class Pushover { + /** + * Maximum size of the image attachment in bytes accepted by the API (https://pushover.net/api#attachments). + * + * @var int + */ + protected const IMAGE_SIZE_LIMIT = 2621440; + /** * Location of the Pushover API. * @@ -54,9 +61,29 @@ public function __construct(HttpClient $http, $token) public function send($params) { try { - return $this->http->post($this->pushoverApiUrl, [ - 'form_params' => $this->paramsWithToken($params), - ]); + $multipart = []; + + foreach ($this->paramsWithToken($params) as $name => $contents) { + if ($name !== 'image') { + $multipart[] = [ + 'name' => $name, + 'contents' => $contents, + ]; + } else { + $image = $this->getImageData($contents); + + if ($image) { + $multipart[] = $image; + } + } + } + + return $this->http->post( + $this->pushoverApiUrl, + [ + 'multipart' => $multipart, + ] + ); } catch (RequestException $exception) { if ($exception->getResponse()) { throw CouldNotSendNotification::serviceRespondedWithAnError($exception->getResponse()); @@ -79,4 +106,64 @@ protected function paramsWithToken($params) 'token' => $this->token, ], $params); } + + /** + * Build the multipart array information for the attached image. + * + * If there is any error (problem with reading the file, file size exceeds the limit, the file is not an image), + * silently returns null and sends the message without image attachment. + * + * @param $file + * @return array|null + */ + private function getImageData($file): ?array + { + try { + // check if $file is not too big + if (is_file($file) && is_readable($file)) { + // directly check server file size + if (filesize($file) > self::IMAGE_SIZE_LIMIT) { + return null; + } + + $fileSizeChecked = true; + } else { + // check "Content-Length" header even before downloading the file + $response = $this->http->request('GET', $file, ['stream' => true]); + $contentLength = $response->getHeader('Content-Length')[0] ?? null; + + if (isset($contentLength) && $contentLength > self::IMAGE_SIZE_LIMIT) { + return null; + } + + // some servers may not return the "Content-Length" header + $fileSizeChecked = (bool) $contentLength; + } + + // check if $file is an image + $imageType = exif_imagetype($file); + if ($imageType === false) { + return null; + } + $contentType = image_type_to_mime_type($imageType); + + $contents = file_get_contents($file); + // if not checked before, finally check the file size after reading it + if (! $fileSizeChecked && strlen($contents) > self::IMAGE_SIZE_LIMIT) { + return null; + } + } catch (Exception $exception) { + return null; + } + + return [ + // name of the field holding the image must be 'attachment' (https://pushover.net/api#attachments) + 'name' => 'attachment', + 'contents' => $contents, + 'filename' => basename($file), + 'headers' => [ + 'Content-Type' => $contentType, + ], + ]; + } } diff --git a/src/PushoverMessage.php b/src/PushoverMessage.php index fcecd3b..f179e03 100644 --- a/src/PushoverMessage.php +++ b/src/PushoverMessage.php @@ -81,6 +81,13 @@ class PushoverMessage */ public $sound; + /** + * The (optional) image to be attached to the message. + * + * @var string + */ + public $image; + /** * Message formats. */ @@ -222,6 +229,19 @@ public function sound($sound) return $this; } + /** + * Set the image for attaching to the Pushover message. Either full or relative server path or a URL. + * + * @param string $image + * @return $this + */ + public function image($image) + { + $this->image = $image; + + return $this; + } + /** * Set the priority of the Pushover message. * Retry and expire are mandatory when setting the priority to emergency. @@ -310,6 +330,7 @@ public function toArray() 'url' => $this->url, 'url_title' => $this->urlTitle, 'sound' => $this->sound, + 'image' => $this->image, 'retry' => $this->retry, 'expire' => $this->expire, 'html' => $this->format === static::FORMAT_HTML,