diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 0633cfc5f54..a3bf2eed9dc 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -526,6 +526,10 @@ disable_from = false ; configuration options. sms = enabled +; Set this value to "database" to shorten links sent via email/SMS and +; store its path in the database (default "none"). +url_shortener = none + ; This section needs to be changed to match your database connection information [Database] ; Connection string format is [platform]://[username]:[password]@[host]:[port]/[db] diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index ee9e2200024..e2eb24506c6 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -45,6 +45,19 @@ ] ], ], + 'shortlink' => [ + 'type' => 'Zend\Router\Http\Segment', + 'options' => [ + 'route' => '/short/[:id]', + 'constraints' => [ + 'id' => '[a-zA-Z0-9]+', + ], + 'defaults' => [ + 'controller' => 'Shortlink', + 'action' => 'redirect', + ] + ], + ], 'legacy-alphabrowse-results' => [ 'type' => 'Zend\Router\Http\Literal', 'options' => [ @@ -162,6 +175,7 @@ 'VuFind\Controller\RelaisController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\SearchController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\ShibbolethLogoutNotificationController' => 'VuFind\Controller\AbstractBaseFactory', + 'VuFind\Controller\ShortlinkController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\SummonController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\SummonrecordController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\TagController' => 'VuFind\Controller\AbstractBaseFactory', @@ -257,6 +271,8 @@ 'search' => 'VuFind\Controller\SearchController', 'ShibbolethLogoutNotification' => 'VuFind\Controller\ShibbolethLogoutNotificationController', 'shibbolethlogoutnotification' => 'VuFind\Controller\ShibbolethLogoutNotificationController', + 'Shortlink' => 'VuFind\Controller\ShortlinkController', + 'shortlink' => 'VuFind\Controller\ShortlinkController', 'Summon' => 'VuFind\Controller\SummonController', 'summon' => 'VuFind\Controller\SummonController', 'SummonRecord' => 'VuFind\Controller\SummonrecordController', @@ -396,6 +412,8 @@ 'VuFind\SMS\SMSInterface' => 'VuFind\SMS\Factory', 'VuFind\Solr\Writer' => 'VuFind\Solr\WriterFactory', 'VuFind\Tags' => 'VuFind\TagsFactory', + 'VuFind\UrlShortener\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', + 'VuFind\UrlShortener\UrlShortenerInterface' => 'VuFind\UrlShortener\ServiceFactory', 'VuFind\Validator\Csrf' => 'VuFind\Validator\CsrfFactory', 'VuFindHttp\HttpService' => 'VuFind\Service\HttpServiceFactory', 'VuFindSearch\Service' => 'VuFind\Service\SearchServiceFactory', @@ -500,6 +518,7 @@ 'resource_tags' => ['id', 'resource_tags_id_seq'], 'search' => ['id', 'search_id_seq'], 'session' => ['id', 'session_id_seq'], + 'shortlinks' => ['id', 'shortlinks_id_seq'], 'tags' => ['id', 'tags_id_seq'], 'user' => ['id', 'user_id_seq'], 'user_card' => ['id', 'user_card_id_seq'], @@ -542,6 +561,7 @@ 'search_params' => [ /* See VuFind\Search\Params\PluginManager for defaults */ ], 'search_results' => [ /* See VuFind\Search\Results\PluginManager for defaults */ ], 'session' => [ /* see VuFind\Session\PluginManager for defaults */ ], + 'urlshortener' => [ /* see VuFind\UrlShortener\PluginManager for defaults */ ], ], ], // Authorization configuration: diff --git a/module/VuFind/sql/migrations/pgsql/6.0/001-add-shortlinks-table.sql b/module/VuFind/sql/migrations/pgsql/6.0/001-add-shortlinks-table.sql new file mode 100644 index 00000000000..275661e1cda --- /dev/null +++ b/module/VuFind/sql/migrations/pgsql/6.0/001-add-shortlinks-table.sql @@ -0,0 +1,10 @@ +-- +-- Table structure for table shortlinks +-- + +CREATE TABLE shortlinks ( +id SERIAL, +path text, +created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +PRIMARY KEY (id) +); diff --git a/module/VuFind/sql/mysql.sql b/module/VuFind/sql/mysql.sql index 289b1a71549..5bcf1df8a83 100644 --- a/module/VuFind/sql/mysql.sql +++ b/module/VuFind/sql/mysql.sql @@ -163,6 +163,20 @@ CREATE TABLE `external_session` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `shortlinks` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `shortlinks` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `path` MEDIUMTEXT NOT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `tags` -- diff --git a/module/VuFind/sql/pgsql.sql b/module/VuFind/sql/pgsql.sql index d0eb1ef33c4..e31d908e4e8 100644 --- a/module/VuFind/sql/pgsql.sql +++ b/module/VuFind/sql/pgsql.sql @@ -84,6 +84,21 @@ CREATE INDEX search_user_id_idx ON search (user_id); CREATE INDEX search_folder_id_idx ON search (folder_id); CREATE INDEX session_id_idx ON search (session_id); +-- -------------------------------------------------------- + +-- +-- Table structure for table shortlinks +-- + +DROP TABLE IF EXISTS "shortlinks"; + +CREATE TABLE shortlinks ( +id SERIAL, +path text, +created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +PRIMARY KEY (id) +); + -- -------------------------------------------------------- diff --git a/module/VuFind/src/VuFind/Controller/ShortlinkController.php b/module/VuFind/src/VuFind/Controller/ShortlinkController.php new file mode 100644 index 00000000000..8b8a33eab23 --- /dev/null +++ b/module/VuFind/src/VuFind/Controller/ShortlinkController.php @@ -0,0 +1,59 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +namespace VuFind\Controller; + +use VuFind\UrlShortener\UrlShortenerInterface; + +/** + * Short link controller + * + * @category VuFind + * @package Controller + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +class ShortlinkController extends AbstractBase +{ + /** + * Resolve full version of shortlink & redirect to target. + * + * @return mixed + */ + public function redirectAction() + { + if ($id = $this->params('id')) { + $resolver = $this->serviceLocator->get(UrlShortenerInterface::class); + if ($url = $resolver->resolve($id)) { + return $this->redirect()->toUrl($url); + } + } + + $this->getResponse()->setStatusCode(404); + } +} diff --git a/module/VuFind/src/VuFind/Db/Row/PluginManager.php b/module/VuFind/src/VuFind/Db/Row/PluginManager.php index 92e2138cc49..5b402dec18f 100644 --- a/module/VuFind/src/VuFind/Db/Row/PluginManager.php +++ b/module/VuFind/src/VuFind/Db/Row/PluginManager.php @@ -53,6 +53,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager 'resourcetags' => ResourceTags::class, 'search' => Search::class, 'session' => Session::class, + 'shortlinks' => Shortlinks::class, 'tags' => Tags::class, 'user' => User::class, 'usercard' => UserCard::class, @@ -75,6 +76,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager ResourceTags::class => RowGatewayFactory::class, Search::class => RowGatewayFactory::class, Session::class => RowGatewayFactory::class, + Shortlinks::class => RowGatewayFactory::class, Tags::class => RowGatewayFactory::class, User::class => UserFactory::class, UserCard::class => RowGatewayFactory::class, diff --git a/module/VuFind/src/VuFind/Db/Row/Shortlinks.php b/module/VuFind/src/VuFind/Db/Row/Shortlinks.php new file mode 100644 index 00000000000..36345f9876b --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Row/Shortlinks.php @@ -0,0 +1,50 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +namespace VuFind\Db\Row; + +/** + * Row Definition for shortlinks + * + * @category VuFind + * @package Db_Row + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +class Shortlinks extends RowGateway +{ + /** + * Constructor + * + * @param \Zend\Db\Adapter\Adapter $adapter Database adapter + */ + public function __construct($adapter) + { + parent::__construct('id', 'shortlinks', $adapter); + } +} diff --git a/module/VuFind/src/VuFind/Db/Table/PluginManager.php b/module/VuFind/src/VuFind/Db/Table/PluginManager.php index a93d859324c..e08e1e41276 100644 --- a/module/VuFind/src/VuFind/Db/Table/PluginManager.php +++ b/module/VuFind/src/VuFind/Db/Table/PluginManager.php @@ -53,6 +53,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager 'resourcetags' => ResourceTags::class, 'search' => Search::class, 'session' => Session::class, + 'shortlinks' => Shortlinks::class, 'tags' => Tags::class, 'user' => User::class, 'usercard' => UserCard::class, @@ -75,6 +76,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager ResourceTags::class => CaseSensitiveTagsFactory::class, Search::class => GatewayFactory::class, Session::class => GatewayFactory::class, + Shortlinks::class => GatewayFactory::class, Tags::class => CaseSensitiveTagsFactory::class, User::class => UserFactory::class, UserCard::class => GatewayFactory::class, diff --git a/module/VuFind/src/VuFind/Db/Table/Shortlinks.php b/module/VuFind/src/VuFind/Db/Table/Shortlinks.php new file mode 100644 index 00000000000..e241ad8a8ec --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Table/Shortlinks.php @@ -0,0 +1,58 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +namespace VuFind\Db\Table; + +use VuFind\Db\Row\RowGateway; +use Zend\Db\Adapter\Adapter; + +/** + * Table Definition for shortlinks + * + * @category VuFind + * @package Db_Table + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +class Shortlinks extends Gateway +{ + /** + * Constructor + * + * @param Adapter $adapter Database adapter + * @param PluginManager $tm Table manager + * @param array $cfg Zend Framework configuration + * @param RowGateway $rowObj Row prototype object (null for default) + * @param string $table Name of database table to interface with + */ + public function __construct(Adapter $adapter, PluginManager $tm, $cfg, + RowGateway $rowObj = null, $table = 'shortlinks' + ) { + parent::__construct($adapter, $tm, $cfg, $rowObj, $table); + } +} diff --git a/module/VuFind/src/VuFind/UrlShortener/Database.php b/module/VuFind/src/VuFind/UrlShortener/Database.php new file mode 100644 index 00000000000..81c09a93ead --- /dev/null +++ b/module/VuFind/src/VuFind/UrlShortener/Database.php @@ -0,0 +1,159 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\UrlShortener; + +use VuFind\Db\Table\Shortlinks as ShortlinksTable; + +/** + * Local database-driven URL shortener. + * + * @category VuFind + * @package UrlShortener + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class Database implements UrlShortenerInterface +{ + const BASE62_ALPHABET + = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + const BASE62_BASE = 62; + + /** + * Base URL of current VuFind site + * + * @var string + */ + protected $baseUrl; + + /** + * Table containing shortlinks + * + * @var ShortlinksTable + */ + protected $table; + + /** + * Constructor + * + * @param string $baseUrl Base URL of current VuFind site + * @param ShortlinksTable $table Shortlinks database table + */ + public function __construct(string $baseUrl, ShortlinksTable $table) + { + $this->baseUrl = $baseUrl; + $this->table = $table; + } + + /** + * Common base62 encoding function. + * Implemented here so we don't need additional PHP modules like bcmath. + * + * @param string $base10Number Number to encode + * + * @return string + * + * @throws \Exception + */ + protected function base62Encode($base10Number) + { + $binaryNumber = intval($base10Number); + if ($binaryNumber === 0) { + throw new \Exception('not a base10 number: "' . $base10Number . '"'); + } + + $base62Number = ''; + while ($binaryNumber != 0) { + $base62Number = self::BASE62_ALPHABET[$binaryNumber % self::BASE62_BASE] + . $base62Number; + $binaryNumber = intdiv($binaryNumber, self::BASE62_BASE); + } + + return ($base62Number == '') ? '0' : $base62Number; + } + + /** + * Common base62 decoding function. + * Implemented here so we don't need additional PHP modules like bcmath. + * + * @param string $base62Number Number to decode + * + * @return int + * + * @throws \Exception + */ + protected function base62Decode($base62Number) + { + $binaryNumber = 0; + for ($i = 0; $i < strlen($base62Number); ++$i) { + $digit = $base62Number[$i]; + $strpos = strpos(self::BASE62_ALPHABET, $digit); + if ($strpos === false) { + throw new \Exception('not a base62 digit: "' . $digit . '"'); + } + + $binaryNumber *= self::BASE62_BASE; + $binaryNumber += $strpos; + } + return $binaryNumber; + } + + /** + * Generate & store shortened URL in Database. + * + * @param string $url URL + * + * @return string + */ + public function shorten($url) + { + $path = str_replace($this->baseUrl, '', $url); + $this->table->insert(['path' => $path]); + $id = $this->table->getLastInsertValue(); + + $shortUrl = $this->baseUrl . '/short/' . $this->base62Encode($id); + return $shortUrl; + } + + /** + * Resolve URL from Database via id. + * + * @param string $id ID to resolve + * + * @return string + */ + public function resolve($id) + { + $results = $this->table->select(['id' => $this->base62Decode($id)]); + if ($results->count() !== 1) { + throw new \Exception('Shortlink could not be resolved: ' . $id); + } + + return $this->baseUrl . $results->current()['path']; + } +} diff --git a/module/VuFind/src/VuFind/UrlShortener/DatabaseFactory.php b/module/VuFind/src/VuFind/UrlShortener/DatabaseFactory.php new file mode 100644 index 00000000000..2790b129e76 --- /dev/null +++ b/module/VuFind/src/VuFind/UrlShortener/DatabaseFactory.php @@ -0,0 +1,65 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\UrlShortener; + +use Interop\Container\ContainerInterface; + +/** + * Factory for local database-driven URL shortener. + * + * @category VuFind + * @package UrlShortener + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class DatabaseFactory +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options passed to factory.'); + } + $router = $container->get('HttpRouter'); + $baseUrl = $container->get('ViewRenderer')->plugin('serverurl') + ->__invoke($router->assemble([], ['name' => 'home'])); + $table = $container->get(\VuFind\Db\Table\PluginManager::class) + ->get('shortlinks'); + return new $requestedName(rtrim($baseUrl, '/'), $table); + } +} diff --git a/module/VuFind/src/VuFind/UrlShortener/None.php b/module/VuFind/src/VuFind/UrlShortener/None.php new file mode 100644 index 00000000000..5651b7a8784 --- /dev/null +++ b/module/VuFind/src/VuFind/UrlShortener/None.php @@ -0,0 +1,66 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\UrlShortener; + +/** + * No-op URL shortener (default version, does nothing). + * + * @category VuFind + * @package UrlShortener + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class None implements UrlShortenerInterface +{ + /** + * Dummy to return original URL version. + * + * @param string $url URL + * + * @return string + */ + public function shorten($url) + { + return $url; + } + + /** + * Dummy implementation. Resolving is not necessary because initial URL + * has not been shortened. + * + * @param string $id ID to resolve + * + * @return string + * @throws Exception because this class is not meant to resolve shortlinks. + */ + public function resolve($id) + { + throw new \Exception('UrlShortener None is unable to resolve shortlinks.'); + } +} diff --git a/module/VuFind/src/VuFind/UrlShortener/PluginManager.php b/module/VuFind/src/VuFind/UrlShortener/PluginManager.php new file mode 100644 index 00000000000..8e8080988c9 --- /dev/null +++ b/module/VuFind/src/VuFind/UrlShortener/PluginManager.php @@ -0,0 +1,73 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\UrlShortener; + +use Zend\ServiceManager\Factory\InvokableFactory; + +/** + * URL shortener plugin manager. + * + * @category VuFind + * @package UrlShortener + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager +{ + /** + * Default plugin aliases. + * + * @var array + */ + protected $aliases = [ + 'none' => None::class, + 'database' => Database::class, + ]; + + /** + * Default plugin factories. + * + * @var array + */ + protected $factories = [ + None::class => InvokableFactory::class, + Database::class => DatabaseFactory::class, + ]; + + /** + * Return the name of the base class or interface that plug-ins must conform + * to. + * + * @return string + */ + protected function getExpectedInterface() + { + return UrlShortenerInterface::class; + } +} diff --git a/module/VuFind/src/VuFind/UrlShortener/ServiceFactory.php b/module/VuFind/src/VuFind/UrlShortener/ServiceFactory.php new file mode 100644 index 00000000000..286a405f249 --- /dev/null +++ b/module/VuFind/src/VuFind/UrlShortener/ServiceFactory.php @@ -0,0 +1,64 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\UrlShortener; + +use Interop\Container\ContainerInterface; + +/** + * Factory to construct the configured UrlShortener service. + * + * @category VuFind + * @package UrlShortener + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class ServiceFactory +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options passed to factory.'); + } + $config = $container->get(\VuFind\Config\PluginManager::class) + ->get('config'); + $service = empty($config->Mail->url_shortener) + ? 'none' : $config->Mail->url_shortener; + return $container->get(PluginManager::class)->get($service); + } +} diff --git a/module/VuFind/src/VuFind/UrlShortener/UrlShortenerInterface.php b/module/VuFind/src/VuFind/UrlShortener/UrlShortenerInterface.php new file mode 100644 index 00000000000..3f9d02e6635 --- /dev/null +++ b/module/VuFind/src/VuFind/UrlShortener/UrlShortenerInterface.php @@ -0,0 +1,58 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\UrlShortener; + +/** + * URL shortener interface. + * + * @category VuFind + * @package UrlShortener + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +interface UrlShortenerInterface +{ + /** + * Generate and return shortened version of a URL. + * + * @param string $url URL + * + * @return string + */ + public function shorten($url); + + /** + * Resolve a shortened URL by its id. + * + * @param string $id ID to resolve + * + * @return string + */ + public function resolve($id); +} diff --git a/module/VuFind/src/VuFind/View/Helper/Root/ShortenUrl.php b/module/VuFind/src/VuFind/View/Helper/Root/ShortenUrl.php new file mode 100644 index 00000000000..ea70d1be214 --- /dev/null +++ b/module/VuFind/src/VuFind/View/Helper/Root/ShortenUrl.php @@ -0,0 +1,71 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\View\Helper\Root; + +use VuFind\UrlShortener\UrlShortenerInterface; + +/** + * View helper for formatting dates and times + * + * @category VuFind + * @package View_Helpers + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class ShortenUrl extends \Zend\View\Helper\AbstractHelper +{ + /** + * URL shortener + * + * @var UrlShortenerInterface + */ + protected $shortener; + + /** + * Constructor + * + * @param UrlShortenerInterface $shortener URL shortener + */ + public function __construct(UrlShortenerInterface $shortener) + { + $this->shortener = $shortener; + } + + /** + * Shorten a URL + * + * @param string $url URL to shorten + * + * @return string + */ + public function __invoke($url) + { + return $this->shortener->shorten($url); + } +} diff --git a/module/VuFind/src/VuFind/View/Helper/Root/ShortenUrlFactory.php b/module/VuFind/src/VuFind/View/Helper/Root/ShortenUrlFactory.php new file mode 100644 index 00000000000..cbf78fc8e03 --- /dev/null +++ b/module/VuFind/src/VuFind/View/Helper/Root/ShortenUrlFactory.php @@ -0,0 +1,68 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\View\Helper\Root; + +use Interop\Container\ContainerInterface; +use Zend\ServiceManager\Factory\FactoryInterface; + +/** + * ShortenUrl helper factory. + * + * @category VuFind + * @package View_Helpers + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class ShortenUrlFactory implements FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options sent to factory.'); + } + return new $requestedName( + $container->get(\VuFind\UrlShortener\UrlShortenerInterface::class) + ); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/DatabaseTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/DatabaseTest.php new file mode 100644 index 00000000000..43ea7065d21 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/DatabaseTest.php @@ -0,0 +1,132 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +namespace VuFindTest\UrlShortener; + +use VuFind\UrlShortener\Database; + +/** + * "Database" URL shortener test. + * + * @category VuFind + * @package Tests + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +class DatabaseTest extends \PHPUnit\Framework\TestCase +{ + /** + * Get the object to test. + * + * @param object $table Database table object/mock + * + * @return Database + */ + public function getShortener($table) + { + return new Database('http://foo', $table); + } + + /** + * Get the mock table object. + * + * @param array $methods Methods to mock. + * + * @return object + */ + public function getMockTable($methods) + { + return $this->getMockBuilder(\VuFind\Db\Table\Shortlinks::class) + ->disableOriginalConstructor() + ->setMethods($methods) + ->getMock(); + } + + /** + * Test that the shortener works correctly under "happy path." + * + * @return void + */ + public function testShortener() + { + $table = $this->getMockTable(['insert', 'getLastInsertValue']); + $table->expects($this->once())->method('insert') + ->with($this->equalTo(['path' => '/bar'])); + $table->expects($this->once())->method('getLastInsertValue') + ->will($this->returnValue('10')); + $db = $this->getShortener($table); + $this->assertEquals('http://foo/short/A', $db->shorten('http://foo/bar')); + } + + /** + * Test that resolve is supported. + * + * @return void + */ + public function testResolution() + { + $table = $this->getMockTable(['select']); + $mockResults = $this->getMockBuilder(\Zend\Db\ResultSet::class) + ->setMethods(['count', 'current']) + ->disableOriginalConstructor() + ->getMock(); + $mockResults->expects($this->once())->method('count') + ->will($this->returnValue(1)); + $mockResults->expects($this->once())->method('current') + ->will($this->returnValue(['path' => '/bar'])); + $table->expects($this->once())->method('select') + ->with($this->equalTo(['id' => 10])) + ->will($this->returnValue($mockResults)); + $db = $this->getShortener($table); + $this->assertEquals('http://foo/bar', $db->resolve('A')); + } + + /** + * Test that resolve errors correctly when given bad input + * + * @return void + * + * @expectedException Exception + * @expectedExceptionMessage Shortlink could not be resolved: B + */ + public function testResolutionOfBadInput() + { + $table = $this->getMockTable(['select']); + $mockResults = $this->getMockBuilder(\Zend\Db\ResultSet::class) + ->setMethods(['count']) + ->disableOriginalConstructor() + ->getMock(); + $mockResults->expects($this->once())->method('count') + ->will($this->returnValue(0)); + $table->expects($this->once())->method('select') + ->with($this->equalTo(['id' => 11])) + ->will($this->returnValue($mockResults)); + $db = $this->getShortener($table); + $db->resolve('B'); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/NoneTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/NoneTest.php new file mode 100644 index 00000000000..1b978f255e3 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/UrlShortener/NoneTest.php @@ -0,0 +1,68 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +namespace VuFindTest\UrlShortener; + +use VuFind\UrlShortener\None; + +/** + * "None" URL shortener test. + * + * @category VuFind + * @package Tests + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +class NoneTest extends \PHPUnit\Framework\TestCase +{ + /** + * Test that the shortener does nothing. + * + * @return void + */ + public function testShortener() + { + $none = new None(); + $url = 'http://foo'; + $this->assertEquals($url, $none->shorten($url)); + } + + /** + * Test that resolve is not supported. + * + * @return void + * + * @expectedException Exception + * @expectedExceptionMessage UrlShortener None is unable to resolve shortlinks. + */ + public function testNoResolution() + { + $none = new None(); + $none->resolve('foo'); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/ShortenUrlTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/ShortenUrlTest.php new file mode 100644 index 00000000000..0c0a053754d --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/ShortenUrlTest.php @@ -0,0 +1,62 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +namespace VuFindTest\View\Helper\Root; + +use VuFind\UrlShortener\Database; +use VuFind\UrlShortener\UrlShortenerInterface; +use VuFind\View\Helper\Root\ShortenUrl; +use VuFind\View\Helper\Root\ShortenUrlFactory; + +/** + * ShortenUrl view helper Test Class + * + * @category VuFind + * @package Tests + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +class ShortenUrlTest extends \PHPUnit\Framework\TestCase +{ + /** + * Test that helper proxies to appropriate service. + * + * @return void + */ + public function testHelper() + { + $container = new \VuFindTest\Container\MockContainer($this); + $service = $container->createMock(Database::class, ['shorten']); + $service->expects($this->once())->method('shorten') + ->with($this->equalTo('foo'))->will($this->returnValue('bar')); + $container->set(UrlShortenerInterface::class, $service); + $factory = new ShortenUrlFactory(); + $helper = $factory($container, ShortenUrl::class); + $this->assertEquals('bar', $helper('foo')); + } +} diff --git a/themes/root/templates/Email/record-sms.phtml b/themes/root/templates/Email/record-sms.phtml index 268c4a5cd5e..71cb8a8a781 100644 --- a/themes/root/templates/Email/record-sms.phtml +++ b/themes/root/templates/Email/record-sms.phtml @@ -6,6 +6,9 @@ // we'll just show URL and title. This prioritization is important, // since text messages can be short, and we want the most important stuff // at the top! + $recordUrl = $this->shortenUrl( + $this->serverUrl($this->recordLink()->getUrl($this->driver)) + ); if ($this->driver->supportsAjaxStatus()) { $holdings = $this->driver->getRealTimeHoldings(); @@ -46,9 +49,9 @@ echo $this->translate('Location') . ': ' . trim($location) . "\n"; } echo $this->driver->getBreadcrumb() . "\n"; - echo $this->serverUrl($this->recordLink()->getUrl($this->driver)) . "\n"; + echo $recordUrl . "\n"; } else { - echo $this->serverUrl($this->recordLink()->getUrl($this->driver)) . "\n"; + echo $recordUrl . "\n"; echo $this->driver->getBreadcrumb() . "\n"; } ?> diff --git a/themes/root/templates/Email/share-link.phtml b/themes/root/templates/Email/share-link.phtml index bcfca31ed20..80839e3cf4b 100644 --- a/themes/root/templates/Email/share-link.phtml +++ b/themes/root/templates/Email/share-link.phtml @@ -7,7 +7,7 @@ - translate('email_link')?>: <msgUrl?>> + translate('email_link')?>: <shortenUrl($this->msgUrl)?>> ------------------------------------------------------------ diff --git a/themes/root/theme.config.php b/themes/root/theme.config.php index dbeb543746d..09708f831c0 100644 --- a/themes/root/theme.config.php +++ b/themes/root/theme.config.php @@ -53,6 +53,7 @@ 'VuFind\View\Helper\Root\SearchOptions' => 'VuFind\View\Helper\Root\SearchOptionsFactory', 'VuFind\View\Helper\Root\SearchParams' => 'VuFind\View\Helper\Root\SearchParamsFactory', 'VuFind\View\Helper\Root\SearchTabs' => 'VuFind\View\Helper\Root\SearchTabsFactory', + 'VuFind\View\Helper\Root\ShortenUrl' => 'VuFind\View\Helper\Root\ShortenUrlFactory', 'VuFind\View\Helper\Root\SortFacetList' => 'Zend\ServiceManager\Factory\InvokableFactory', 'VuFind\View\Helper\Root\Summaries' => 'VuFind\View\Helper\Root\ContentLoaderFactory', 'VuFind\View\Helper\Root\Summon' => 'Zend\ServiceManager\Factory\InvokableFactory', @@ -115,6 +116,7 @@ 'searchParams' => 'VuFind\View\Helper\Root\SearchParams', 'searchTabs' => 'VuFind\View\Helper\Root\SearchTabs', 'searchbox' => 'VuFind\View\Helper\Root\SearchBox', + 'shortenUrl' => 'VuFind\View\Helper\Root\ShortenUrl', 'sortFacetList' => 'VuFind\View\Helper\Root\SortFacetList', 'summaries' => 'VuFind\View\Helper\Root\Summaries', 'summon' => 'VuFind\View\Helper\Root\Summon',