diff --git a/CHANGELOG.md b/CHANGELOG.md index 72f86f31..cee04504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * FIXED: Allow pasting a password for decrypting a paste (#1620) * FIXED: Allow copying the shortened link after using a URL shortener (#1624) * ADDED: Auto shorten URLs with config option `shortenbydefault` (#1627) +* ADDED: Added `shortenviashlink` endpoint with an `shlink` configuration section ## 2.0.0 (2025-07-28) * ADDED: Error logging in database and filesystem backend (#1554) diff --git a/cfg/conf.sample.php b/cfg/conf.sample.php index 8883716f..8fb5288b 100644 --- a/cfg/conf.sample.php +++ b/cfg/conf.sample.php @@ -274,6 +274,17 @@ dir = PATH "data" ;version = "latest" ;bucket = "my-bucket" +;[shlink] +; - Shlink requires you to make a post call with a generated API key. +; use this section to setup the API key and URL. In order to use this section, +; "urlshortener" needs to point to the base URL of your PrivateBin +; instance with "?shortenviashlink&link=" appended. For example: +; urlshortener = "${basepath}?shortenviashlink&link=" +; This URL will in turn call YOURLS on the server side, using the URL from +; "apiurl" and the API Key from the "apikey" parameters below. +; apiurl = "https://shlink.example.com/rest/v3/short-urls" +; apikey = "your_api_key" + ;[yourls] ; When using YOURLS as a "urlshortener" config item: ; - By default, "urlshortener" will point to the YOURLS API URL, with or without diff --git a/lib/Configuration.php b/lib/Configuration.php index e6bb89cc..b139903e 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -107,6 +107,10 @@ class Configuration 'signature' => '', 'apiurl' => '', ), + 'shlink' => array( + 'apikey' => '', + 'apiurl' => '', + ), // update this array when adding/changing/removing js files 'sri' => array( 'js/base-x-5.0.1.js' => 'sha512-FmhlnjIxQyxkkxQmzf0l6IRGsGbgyCdgqPxypFsEtHMF1naRqaLLo6mcyN5rEaT16nKx1PeJ4g7+07D6gnk/Tg==', diff --git a/lib/Controller.php b/lib/Controller.php index 5b2ddb94..104985b0 100644 --- a/lib/Controller.php +++ b/lib/Controller.php @@ -151,6 +151,9 @@ class Controller case 'yourlsproxy': $this->_yourlsproxy($this->_request->getParam('link')); break; + case 'shlinkproxy': + $this->_shlinkproxy($this->_request->getParam('link')); + break; } $this->_setCacheHeaders(); @@ -454,6 +457,11 @@ class Controller $page->draw('yourlsproxy'); return; } + if ($this->_request->getOperation() === 'shlinkproxy') { + $page->assign('SHORTURL', $this->_status); + $page->draw('shlinkproxy'); + return; + } $page->assign('BASEPATH', I18n::_($this->_conf->getKey('basepath'))); $page->assign('STATUS', I18n::_($this->_status)); $page->assign('ISDELETED', I18n::_(json_encode($this->_is_deleted))); @@ -543,6 +551,22 @@ class Controller } } + /** + * proxies link to SHLINK, updates status or error with response + * + * @access private + * @param string $link + */ + private function _shlinkproxy($link) + { + $shlink = new ShlinkProxy($this->_conf, $link); + if ($shlink->isError()) { + $this->_error = $shlink->getError(); + } else { + $this->_status = $shlink->getUrl(); + } + } + /** * prepares JSON encoded status message * diff --git a/lib/Request.php b/lib/Request.php index c01f91c6..24a083be 100644 --- a/lib/Request.php +++ b/lib/Request.php @@ -124,6 +124,7 @@ class Request 'link' => FILTER_SANITIZE_URL, 'pasteid' => FILTER_SANITIZE_SPECIAL_CHARS, 'shortenviayourls' => FILTER_SANITIZE_SPECIAL_CHARS, + 'shortenviashlink' => FILTER_SANITIZE_SPECIAL_CHARS, ), false); } if ( @@ -149,6 +150,9 @@ class Request if (str_contains($this->getRequestUri(), '/shortenviayourls') || array_key_exists('shortenviayourls', $this->_params)) { $this->_operation = 'yourlsproxy'; } + if (str_contains($this->getRequestUri(), '/shortenviashlink') || array_key_exists('shortenviashlink', $this->_params)) { + $this->_operation = 'shlinkproxy'; + } } } diff --git a/lib/ShlinkProxy.php b/lib/ShlinkProxy.php new file mode 100644 index 00000000..38c311c1 --- /dev/null +++ b/lib/ShlinkProxy.php @@ -0,0 +1,140 @@ +getKey('basepath') . '?')) { + $this->_error = 'Trying to shorten a URL that isn\'t pointing at our instance.'; + return; + } + + $shlink_api_url = $conf->getKey('apiurl', 'shlink'); + $shlink_api_key = $conf->getKey('apikey', 'shlink'); + + if (empty($shlink_api_url) || empty($shlink_api_key)) { + $this->_error = 'Error calling Shlink. Probably a configuration issue, like wrong or missing "apiurl" or "apikey".'; + return; + } + + $data = file_get_contents( + $shlink_api_url, false, stream_context_create( + array( + 'http' => array( + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n" . + "X-Api-Key: " . $shlink_api_key . "\r\n", + 'content' => json_encode( + array( + 'longUrl' => $link, + ) + ), + ), + ) + ) + ); + if ($data === false) { + $http_response_header = $http_response_header ?? []; + $statusCode = ''; + if (!empty($http_response_header) && preg_match('/HTTP\/\d+\.\d+\s+(\d+)/', $http_response_header[0], $matches)) { + $statusCode = $matches[1]; + } + $this->_error = 'Error calling shlink. HTTP request failed for URL ' . $shlink_api_url . '. Status code: ' . $statusCode; + error_log('Error calling shlink: HTTP request failed for URL ' . $shlink_api_url . '. Status code: ' . $statusCode); + return; + } + try { + $data = Json::decode($data); + } catch (Exception $e) { + $this->_error = 'Error calling shlink. Probably a configuration issue, like wrong or missing "apiurl" or "apikey".'; + error_log('Error calling shlink: ' . $e->getMessage()); + return; + } + + if ( + !is_null($data) && + // array_key_exists('statusCode', $data) && + // $data['statusCode'] == 200 && + array_key_exists('shortUrl', $data) + ) { + $this->_url = $data['shortUrl']; + } else { + $this->_error = 'Error parsing shlink response.'; + } + } + + /** + * Returns the (untranslated) error message + * + * @access public + * @return string + */ + public function getError() + { + return $this->_error; + } + + /** + * Returns the shortened URL + * + * @access public + * @return string + */ + public function getUrl() + { + return $this->_url; + } + + /** + * Returns true if any error has occurred + * + * @access public + * @return bool + */ + public function isError() + { + return !empty($this->_error); + } +} diff --git a/tpl/shlinkproxy.php b/tpl/shlinkproxy.php new file mode 100644 index 00000000..bbd454e9 --- /dev/null +++ b/tpl/shlinkproxy.php @@ -0,0 +1,27 @@ + +> +
+ + + + +%s (Hit Ctrl+c to copy)', $SHORTURL, $SHORTURL); ?>
+ +