--- /dev/null
+<?php
+
+namespace wcf\data\unfurl\url;
+
+use wcf\data\DatabaseObject;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\CryptoUtil;
+use wcf\util\Url;
+
+/**
+ * Represents an unfurl url object in the database.
+ *
+ * @author Joshua Ruesweg
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Unfurl\Url
+ * @since 5.4
+ *
+ * @property-read string $url
+ * @property-read string $urlHash
+ * @property-read string $title
+ * @property-read string $description
+ * @property-read string $imageHash
+ * @property-read string $imageUrl
+ * @property-read string $imageType
+ */
+class UnfurlUrl extends DatabaseObject
+{
+ public const IMAGE_SQUARED = "SQUARED";
+ public const IMAGE_COVER = "COVER";
+ public const IMAGE_NO_IMAGE = "NOIMAGE";
+
+ public const STATUS_PENDING = "PENDING";
+ public const STATUS_SUCCESSFUL = "SUCCESSFUL";
+ public const STATUS_REJECTED = "REJECTED";
+
+ /**
+ * Renders the unfurl url card and returns the template.
+ *
+ * @return string
+ */
+ public function render() : string
+ {
+ return WCF::getTPL()->fetch('unfurlUrl', 'wcf', [
+ 'object' => $this,
+ ]);
+ }
+
+ /**
+ * Returns the hostname of the url.
+ *
+ * @return string
+ */
+ public function getHost() : string
+ {
+ $url = Url::parse($this->url);
+
+ return $url['host'];
+ }
+
+ /**
+ * Returns the image url for the url.
+ *
+ * @throws \wcf\system\exception\SystemException
+ */
+ public function getImageUrl() : ?string
+ {
+ if (!empty($this->imageHash)) {
+ return WCF::getPath() . 'images/unfurlUrl/' . \substr($this->imageHash, 0, 2) . '/' . $this->imageHash;
+ } elseif (!empty($this->imageUrl)) {
+ if (MODULE_IMAGE_PROXY) {
+ $key = CryptoUtil::createSignedString($this->imageUrl);
+
+ return LinkHandler::getInstance()->getLink('ImageProxy', [
+ 'key' => $key,
+ ]);
+ } elseif (IMAGE_ALLOW_EXTERNAL_SOURCE) {
+ return $this->imageUrl;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the unfurl url object for a given url.
+ *
+ * @throws \InvalidArgumentException If the given URL is invalid.
+ */
+ public static function getByUrl(string $url) : UnfurlUrl
+ {
+ if (!Url::is($url)) {
+ throw new \InvalidArgumentException("Given URL is not a valid URL.");
+ }
+
+ $sql = "SELECT unfurl_url.*
+ FROM wcf" . WCF_N . "_unfurl_url unfurl_url
+ WHERE unfurl_url.urlHash = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([\sha1($url)]);
+ $row = $statement->fetchArray();
+ if (!$row) {
+ $row = [];
+ }
+
+ return new self(null, $row);
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\data\unfurl\url;
+
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\system\background\BackgroundQueueHandler;
+use wcf\system\background\job\UnfurlURLJob;
+
+/**
+ * Contains all dbo actions for unfurl url objects.
+ *
+ * @author Joshua Ruesweg
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Unfurl\Url
+ * @since 5.4
+ *
+ * @method UnfurlUrlEditor[] getObjects()
+ * @method UnfurlUrlEditor getSingleObject()
+ */
+class UnfurlUrlAction extends AbstractDatabaseObjectAction
+{
+ /**
+ * @inheritDoc
+ */
+ public function create()
+ {
+ /** @var UnfurlUrl $object */
+ $object = parent::create();
+
+ // @TODO enqueue job
+
+ return $object;
+ }
+
+ /**
+ * Returns the unfurl url object to a given url.
+ *
+ * @return UnfurlUrl
+ */
+ public function findOrCreate()
+ {
+ $object = UnfurlUrl::getByUrl($this->parameters['data']['url']);
+
+ if (!$object->urlID) {
+ $returnValues = (new self([], 'create', [
+ 'data' => [
+ 'url' => $this->parameters['data']['url'],
+ 'urlHash' => \sha1($this->parameters['data']['url']),
+ ],
+ ]))->executeAction();
+
+ return $returnValues['returnValues'];
+ }
+
+ return $object;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\data\unfurl\url;
+
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provide functions to edit an unfurl url.
+ *
+ * @author Joshua Ruesweg
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Unfurl\Url
+ * @since 5.4
+ *
+ * @method UnfurlUrl getDecoratedObject()
+ * @mixin UnfurlUrl
+ */
+class UnfurlUrlEditor extends DatabaseObjectEditor
+{
+ /**
+ * @inheritDoc
+ */
+ public static $baseClass = UnfurlUrl::class;
+}
--- /dev/null
+<?php
+
+namespace wcf\data\unfurl\url;
+
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of unfurled urls.
+ *
+ * @author Joshua Ruesweg
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Unfurl\Url
+ * @since 5.4
+ *
+ * @method UnfurlUrl current()
+ * @method UnfurlUrl[] getObjects()
+ * @method UnfurlUrl|null search($objectID)
+ * @property UnfurlUrl[] $objects
+ */
+class UnfurlUrlList extends DatabaseObjectList
+{
+}
KEY(categoryID)
);
+DROP TABLE IF EXISTS wcf1_unfurl_url;
+CREATE TABLE wcf1_unfurl_url (
+ urlID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ url TEXT NOT NULL,
+ urlHash VARCHAR(40) NOT NULL UNIQUE KEY,
+ title VARCHAR(255) NOT NULL DEFAULT '',
+ description TEXT,
+ imageUrl TEXT,
+ imageType ENUM('SQUARED', 'COVER', 'NOIMAGE') NOT NULL DEFAULT 'NOIMAGE',
+ imageHash VARCHAR(40) NOT NULL DEFAULT '',
+ status ENUM('PENDING', 'SUCCESSFUL', 'REJECTED') DEFAULT 'PENDING'
+);
+
DROP TABLE IF EXISTS wcf1_user;
CREATE TABLE wcf1_user (
userID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,