Explorar el Código

Added shlink integration

Karthik Kasturi hace 11 meses
padre
commit
0808052acf

+ 1 - 0
CHANGELOG.md

@@ -5,6 +5,7 @@
 * FIXED: Allow pasting a password for decrypting a paste (#1620)
 * FIXED: Allow pasting a password for decrypting a paste (#1620)
 * FIXED: Allow copying the shortened link after using a URL shortener (#1624)
 * FIXED: Allow copying the shortened link after using a URL shortener (#1624)
 * ADDED: Auto shorten URLs with config option `shortenbydefault` (#1627)
 * ADDED: Auto shorten URLs with config option `shortenbydefault` (#1627)
+* ADDED: Added `shortenviashlink` endpoint with an `shlink` configuration section 
 
 
 ## 2.0.0 (2025-07-28)
 ## 2.0.0 (2025-07-28)
 * ADDED: Error logging in database and filesystem backend (#1554)
 * ADDED: Error logging in database and filesystem backend (#1554)

+ 11 - 0
cfg/conf.sample.php

@@ -274,6 +274,17 @@ dir = PATH "data"
 ;version = "latest"
 ;version = "latest"
 ;bucket = "my-bucket"
 ;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]
 ;[yourls]
 ; When using YOURLS as a "urlshortener" config item:
 ; When using YOURLS as a "urlshortener" config item:
 ; - By default, "urlshortener" will point to the YOURLS API URL, with or without
 ; - By default, "urlshortener" will point to the YOURLS API URL, with or without

+ 4 - 0
lib/Configuration.php

@@ -107,6 +107,10 @@ class Configuration
             'signature' => '',
             'signature' => '',
             'apiurl'    => '',
             'apiurl'    => '',
         ),
         ),
+        'shlink' => array(
+            'apikey'    => '',
+            'apiurl'    => '',
+        ),
         // update this array when adding/changing/removing js files
         // update this array when adding/changing/removing js files
         'sri' => array(
         'sri' => array(
             'js/base-x-5.0.1.js'     => 'sha512-FmhlnjIxQyxkkxQmzf0l6IRGsGbgyCdgqPxypFsEtHMF1naRqaLLo6mcyN5rEaT16nKx1PeJ4g7+07D6gnk/Tg==',
             'js/base-x-5.0.1.js'     => 'sha512-FmhlnjIxQyxkkxQmzf0l6IRGsGbgyCdgqPxypFsEtHMF1naRqaLLo6mcyN5rEaT16nKx1PeJ4g7+07D6gnk/Tg==',

+ 24 - 0
lib/Controller.php

@@ -151,6 +151,9 @@ class Controller
             case 'yourlsproxy':
             case 'yourlsproxy':
                 $this->_yourlsproxy($this->_request->getParam('link'));
                 $this->_yourlsproxy($this->_request->getParam('link'));
                 break;
                 break;
+            case 'shlinkproxy':
+                $this->_shlinkproxy($this->_request->getParam('link'));
+                break;
         }
         }
 
 
         $this->_setCacheHeaders();
         $this->_setCacheHeaders();
@@ -454,6 +457,11 @@ class Controller
             $page->draw('yourlsproxy');
             $page->draw('yourlsproxy');
             return;
             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('BASEPATH', I18n::_($this->_conf->getKey('basepath')));
         $page->assign('STATUS', I18n::_($this->_status));
         $page->assign('STATUS', I18n::_($this->_status));
         $page->assign('ISDELETED', I18n::_(json_encode($this->_is_deleted)));
         $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
      * prepares JSON encoded status message
      *
      *

+ 4 - 0
lib/Request.php

@@ -124,6 +124,7 @@ class Request
                     'link'             => FILTER_SANITIZE_URL,
                     'link'             => FILTER_SANITIZE_URL,
                     'pasteid'          => FILTER_SANITIZE_SPECIAL_CHARS,
                     'pasteid'          => FILTER_SANITIZE_SPECIAL_CHARS,
                     'shortenviayourls' => FILTER_SANITIZE_SPECIAL_CHARS,
                     'shortenviayourls' => FILTER_SANITIZE_SPECIAL_CHARS,
+                    'shortenviashlink' => FILTER_SANITIZE_SPECIAL_CHARS,
                 ), false);
                 ), false);
         }
         }
         if (
         if (
@@ -149,6 +150,9 @@ class Request
             if (str_contains($this->getRequestUri(), '/shortenviayourls') || array_key_exists('shortenviayourls', $this->_params)) {
             if (str_contains($this->getRequestUri(), '/shortenviayourls') || array_key_exists('shortenviayourls', $this->_params)) {
                 $this->_operation = 'yourlsproxy';
                 $this->_operation = 'yourlsproxy';
             }
             }
+            if (str_contains($this->getRequestUri(), '/shortenviashlink') || array_key_exists('shortenviashlink', $this->_params)) {
+                $this->_operation = 'shlinkproxy';
+            }
         }
         }
     }
     }
 
 

+ 140 - 0
lib/ShlinkProxy.php

@@ -0,0 +1,140 @@
+<?php declare(strict_types=1);
+/**
+ * PrivateBin
+ *
+ * a zero-knowledge paste bin
+ *
+ * @link      https://github.com/PrivateBin/PrivateBin
+ * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
+ * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
+ */
+
+namespace PrivateBin;
+
+use Exception;
+
+/**
+ * ShlinkProxy
+ *
+ * Forwards a URL for shortening to shlink and stores the result.
+ */
+class ShlinkProxy
+{
+    /**
+     * error message
+     *
+     * @access private
+     * @var    string
+     */
+    private $_error = '';
+
+    /**
+     * shortened URL
+     *
+     * @access private
+     * @var    string
+     */
+    private $_url = '';
+
+    /**
+     * constructor
+     *
+     * initializes and runs PrivateBin
+     *
+     * @access public
+     * @param string $link
+     */
+    public function __construct(Configuration $conf, $link)
+    {
+        if (!str_starts_with($link, $conf->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);
+    }
+}

+ 27 - 0
tpl/shlinkproxy.php

@@ -0,0 +1,27 @@
+<?php declare(strict_types=1);
+use PrivateBin\I18n;
+?><!DOCTYPE html>
+<html lang="<?php echo I18n::getLanguage(); ?>"<?php echo I18n::isRtl() ? ' dir="rtl"' : ''; ?>>
+	<head>
+		<meta charset="utf-8" />
+		<meta http-equiv="Content-Security-Policy" content="<?php echo I18n::encode($CSPHEADER); ?>">
+		<meta name="robots" content="noindex" />
+		<meta name="google" content="notranslate">
+		<title><?php echo I18n::_($NAME); ?></title>
+	</head>
+	<body>
+<?php
+if (empty($ERROR)) :
+?>
+		<p><?php echo I18n::_('Your document is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span>', $SHORTURL, $SHORTURL); ?></p>
+<?php
+else:
+?>
+		<div id="errormessage">
+			<p><?php echo I18n::_('Could not create document: %s', $ERROR); ?></p>
+		</div>
+<?php
+endif;
+?>
+	</body>
+</html>

+ 2 - 2
tst/ViewTest.php

@@ -106,8 +106,8 @@ class ViewTest extends TestCase
                 $content,
                 $content,
                 $template . ': outputs error correctly'
                 $template . ': outputs error correctly'
             );
             );
-            if ($template === 'yourlsproxy') {
-                // yourlsproxy template only displays error message
+            if ($template === 'yourlsproxy' || $template === 'shlinkproxy') {
+                // yourlsproxy and shlinkproxy templates only display error message
                 continue;
                 continue;
             }
             }
             $this->assertMatchesRegularExpression(
             $this->assertMatchesRegularExpression(

+ 1 - 0
vendor/composer/autoload_classmap.php

@@ -90,6 +90,7 @@ return array(
     'PrivateBin\\View' => $baseDir . '/lib/View.php',
     'PrivateBin\\View' => $baseDir . '/lib/View.php',
     'PrivateBin\\Vizhash16x16' => $baseDir . '/lib/Vizhash16x16.php',
     'PrivateBin\\Vizhash16x16' => $baseDir . '/lib/Vizhash16x16.php',
     'PrivateBin\\YourlsProxy' => $baseDir . '/lib/YourlsProxy.php',
     'PrivateBin\\YourlsProxy' => $baseDir . '/lib/YourlsProxy.php',
+    'PrivateBin\\ShlinkProxy' => $baseDir . '/lib/ShlinkProxy.php',
     'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
     'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
     'Symfony\\Polyfill\\Php80\\Php80' => $vendorDir . '/symfony/polyfill-php80/Php80.php',
     'Symfony\\Polyfill\\Php80\\Php80' => $vendorDir . '/symfony/polyfill-php80/Php80.php',
     'Symfony\\Polyfill\\Php80\\PhpToken' => $vendorDir . '/symfony/polyfill-php80/PhpToken.php',
     'Symfony\\Polyfill\\Php80\\PhpToken' => $vendorDir . '/symfony/polyfill-php80/PhpToken.php',

+ 1 - 0
vendor/composer/autoload_static.php

@@ -138,6 +138,7 @@ class ComposerStaticInitDontChange
         'PrivateBin\\View' => __DIR__ . '/../..' . '/lib/View.php',
         'PrivateBin\\View' => __DIR__ . '/../..' . '/lib/View.php',
         'PrivateBin\\Vizhash16x16' => __DIR__ . '/../..' . '/lib/Vizhash16x16.php',
         'PrivateBin\\Vizhash16x16' => __DIR__ . '/../..' . '/lib/Vizhash16x16.php',
         'PrivateBin\\YourlsProxy' => __DIR__ . '/../..' . '/lib/YourlsProxy.php',
         'PrivateBin\\YourlsProxy' => __DIR__ . '/../..' . '/lib/YourlsProxy.php',
+        'PrivateBin\\ShlinkProxy' => __DIR__ . '/../..' . '/lib/ShlinkProxy.php',
         'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
         'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
         'Symfony\\Polyfill\\Php80\\Php80' => __DIR__ . '/..' . '/symfony/polyfill-php80/Php80.php',
         'Symfony\\Polyfill\\Php80\\Php80' => __DIR__ . '/..' . '/symfony/polyfill-php80/Php80.php',
         'Symfony\\Polyfill\\Php80\\PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/PhpToken.php',
         'Symfony\\Polyfill\\Php80\\PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/PhpToken.php',