Use own table for unfurl url images
authorjoshuaruesweg <ruesweg@woltlab.com>
Wed, 10 Mar 2021 13:07:59 +0000 (14:07 +0100)
committerjoshuaruesweg <ruesweg@woltlab.com>
Tue, 16 Mar 2021 15:19:15 +0000 (16:19 +0100)
wcfsetup/install/files/acp/database/update_com.woltlab.wcf_5.4.php
wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrl.class.php
wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php
wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlList.class.php
wcfsetup/install/files/lib/system/background/job/UnfurlUrlBackgroundJob.class.php
wcfsetup/setup/db/install.sql

index 17d7f931530cf415096981dfe3b5860dfb30d7fe..cc9c1a2d4d698ed35541ac7a2e6253bf19b02d3e 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * Makes non-critical database adjustments (i.e. everything that is not related
    * Makes non-critical database adjustments (i.e. everything that is not related
  * to sessions).
  *
  * @author  Tim Duesterhus
@@ -221,6 +221,27 @@ return [
             DefaultFalseBooleanDatabaseTableColumn::create('invertPermissions'),
         ]),
 
+    DatabaseTable::create('wcf1_unfurl_url_image')
+        ->columns([
+            ObjectIdDatabaseTableColumn::create('imageID'),
+            TextDatabaseTableColumn::create('imageUrl')
+                ->notNull(),
+            VarcharDatabaseTableColumn::create('imageHash')
+                ->notNull()
+                ->length(40),
+            NotNullInt10DatabaseTableColumn::create('width'),
+            NotNullInt10DatabaseTableColumn::create('height'),
+            VarcharDatabaseTableColumn::create('imageExtension')
+                ->length(4),
+        ])
+        ->indices([
+            DatabaseTablePrimaryIndex::create()
+                ->columns(['imageID']),
+            DatabaseTableIndex::create('imageHash')
+                ->type(DatabaseTableIndex::UNIQUE_TYPE)
+                ->columns(['imageHash']),
+        ]),
+
     DatabaseTable::create('wcf1_unfurl_url')
         ->columns([
             ObjectIdDatabaseTableColumn::create('urlID'),
@@ -232,14 +253,8 @@ return [
             NotNullVarchar255DatabaseTableColumn::create('title'),
             TextDatabaseTableColumn::create('description')
                 ->notNull(),
-            TextDatabaseTableColumn::create('imageUrl')
-                ->notNull()
-                ->defaultValue(''),
-            NotNullVarchar255DatabaseTableColumn::create('imageType')
-                ->defaultValue('NOIMAGE'),
-            VarcharDatabaseTableColumn::create('imageHash')
-                ->notNull()
-                ->length(45),
+            IntDatabaseTableColumn::create('imageID')
+                ->length(10),
             NotNullVarchar255DatabaseTableColumn::create('status')
                 ->defaultValue('PENDING'),
             NotNullInt10DatabaseTableColumn::create('lastFetch')
@@ -251,5 +266,12 @@ return [
             DatabaseTableIndex::create('urlHash')
                 ->type(DatabaseTableIndex::UNIQUE_TYPE)
                 ->columns(['urlHash']),
+        ])
+        ->foreignKeys([
+            DatabaseTableForeignKey::create()
+                ->columns(['imageID'])
+                ->referencedTable('wcf1_unfurl_url_image')
+                ->referencedColumns(['imageID'])
+                ->onDelete('SET NULL'),
         ]),
 ];
index aea01b6fef0a31a62d5b1f8e35d95f49b44a7752..4756f0017e18d92625e80a88ec6d038be063a536 100644 (file)
@@ -25,16 +25,19 @@ use wcf\util\Url;
  * @property-read string $description
  * @property-read string $imageHash
  * @property-read string $imageUrl
- * @property-read string $imageType
+ * @property-read string $imageExtension
+ * @property-read int $width
+ * @property-read int $height
  * @property-read int $lastFetch
+ * @property-read int $imageID
  */
 class UnfurlUrl extends DatabaseObject
 {
-    public const IMAGE_SQUARED = "SQUARED";
+    private const IMAGE_SQUARED = "SQUARED";
 
-    public const IMAGE_COVER = "COVER";
+    private const IMAGE_COVER = "COVER";
 
-    public const IMAGE_NO_IMAGE = "NOIMAGE";
+    private const IMAGE_NO_IMAGE = "NOIMAGE";
 
     public const STATUS_PENDING = "PENDING";
 
@@ -42,10 +45,36 @@ class UnfurlUrl extends DatabaseObject
 
     public const STATUS_REJECTED = "REJECTED";
 
+    public const IMAGE_DIR = "images/unfurlUrl/";
+
+    /**
+     * @inheritDoc
+     */
+    public function __construct($id, $row = null, ?DatabaseObject $object = null)
+    {
+        if ($id !== null) {
+            $sql = "SELECT      unfurl_url.*, unfurl_url_image.*
+                    FROM        wcf" . WCF_N . "_unfurl_url unfurl_url
+                    LEFT JOIN   wcf" . WCF_N . "_unfurl_url_image unfurl_url_image
+                    ON          unfurl_url_image.imageID = unfurl_url.imageID
+                    WHERE       unfurl_url.urlID = ?";
+            $statement = WCF::getDB()->prepareStatement($sql);
+            $statement->execute([$id]);
+            $row = $statement->fetchArray();
+
+            // enforce data type 'array'
+            if ($row === false) {
+                $row = [];
+            }
+        } elseif ($object !== null) {
+            $row = $object->data;
+        }
+
+        $this->handleData($row);
+    }
+
     /**
      * Renders the unfurl url card and returns the template.
-     *
-     * @return string
      */
     public function render(): string
     {
@@ -56,8 +85,6 @@ class UnfurlUrl extends DatabaseObject
 
     /**
      * Returns the hostname of the url.
-     *
-     * @return string
      */
     public function getHost(): string
     {
@@ -74,7 +101,10 @@ class UnfurlUrl extends DatabaseObject
     public function getImageUrl(): ?string
     {
         if (!empty($this->imageHash)) {
-            return WCF::getPath() . 'images/unfurlUrl/' . \substr($this->imageHash, 0, 2) . '/' . $this->imageHash;
+            $imageFolder = self::IMAGE_DIR . \substr($this->imageHash, 0, 2) . "/";
+            $imageName = $this->imageHash . '.' . $this->imageExtension;
+
+            return WCF::getPath() . $imageFolder . $imageName;
         } elseif (!empty($this->imageUrl)) {
             if (MODULE_IMAGE_PROXY) {
                 $key = CryptoUtil::createSignedString($this->imageUrl);
@@ -92,12 +122,25 @@ class UnfurlUrl extends DatabaseObject
 
     public function hasCoverImage(): bool
     {
-        return $this->imageType == self::IMAGE_COVER && !empty($this->getImageUrl());
+        return $this->getImageType() == self::IMAGE_COVER && !empty($this->getImageUrl());
     }
 
     public function hasSquaredImage(): bool
     {
-        return $this->imageType == self::IMAGE_SQUARED && !empty($this->getImageUrl());
+        return $this->getImageType() == self::IMAGE_SQUARED && !empty($this->getImageUrl());
+    }
+
+    private function getImageType(): string
+    {
+        if (!$this->imageID) {
+            return self::IMAGE_NO_IMAGE;
+        }
+
+        if ($this->width == $this->height) {
+            return self::IMAGE_SQUARED;
+        }
+
+        return self::IMAGE_COVER;
     }
 
     /**
@@ -111,8 +154,10 @@ class UnfurlUrl extends DatabaseObject
             throw new \InvalidArgumentException("Given URL is not a valid URL.");
         }
 
-        $sql = "SELECT      unfurl_url.*
+        $sql = "SELECT      unfurl_url.*, unfurl_url_image.*
                 FROM        wcf" . WCF_N . "_unfurl_url unfurl_url
+                LEFT JOIN   wcf" . WCF_N . "_unfurl_url_image unfurl_url_image
+                ON          unfurl_url_image.imageID = unfurl_url.imageID
                 WHERE       unfurl_url.urlHash = ?";
         $statement = WCF::getDB()->prepareStatement($sql);
         $statement->execute([\sha1($url)]);
index 9f808c3007bb19ba8c03e06999168a736e8b7b99..bb0169bc08f6e14951899e8142c941bca87572ff 100644 (file)
@@ -5,6 +5,7 @@ namespace wcf\data\unfurl\url;
 use wcf\data\AbstractDatabaseObjectAction;
 use wcf\system\background\BackgroundQueueHandler;
 use wcf\system\background\job\UnfurlUrlBackgroundJob;
+use wcf\system\WCF;
 
 /**
  * Contains all dbo actions for unfurl url objects.
@@ -25,6 +26,10 @@ class UnfurlUrlAction extends AbstractDatabaseObjectAction
      */
     public function create()
     {
+        if (isset($this->parameters['imageData']) && !empty($this->parameters['imageData'])) {
+            $this->parameters['data']['imageID'] = $this->saveImageData($this->parameters['imageData']);
+        }
+
         /** @var UnfurlUrl $object */
         $object = parent::create();
 
@@ -37,19 +42,53 @@ class UnfurlUrlAction extends AbstractDatabaseObjectAction
         return $object;
     }
 
+    /**
+     * @inheritDoc
+     */
+    public function update()
+    {
+        if (isset($this->parameters['imageData']) && !empty($this->parameters['imageData'])) {
+            $this->parameters['data']['imageID'] = $this->saveImageData($this->parameters['imageData']);
+        }
+
+        parent::update();
+    }
+
+    private function saveImageData(array $imageData): int
+    {
+        $keys = $values = '';
+        $statementParameters = [];
+        foreach ($imageData as $key => $value) {
+            if (!empty($keys)) {
+                $keys .= ',';
+                $values .= ',';
+            }
+
+            $keys .= $key;
+            $values .= '?';
+            $statementParameters[] = $value;
+        }
+
+        // save object
+        $sql = "INSERT INTO wcf" . WCF_N . "_unfurl_url_image
+                            (" . $keys . ")
+                VALUES      (" . $values . ")";
+        $statement = WCF::getDB()->prepareStatement($sql);
+        $statement->execute($statementParameters);
+
+        return WCF::getDB()->getInsertID("wcf" . WCF_N . "_unfurl_url_image", "imageID");
+    }
+
     /**
      * Returns the unfurl url object to a given url.
-     *
-     * @return UnfurlUrl
      */
-    public function findOrCreate()
+    public function findOrCreate(): UnfurlUrl
     {
         $object = UnfurlUrl::getByUrl($this->parameters['data']['url']);
 
         if (!$object) {
             $returnValues = (new self([], 'create', [
                 'data' => [
-                    'imageUrl' => '',
                     'url' => $this->parameters['data']['url'],
                     'urlHash' => \sha1($this->parameters['data']['url']),
                 ],
index d2e7b9901afd07a75094a00172b796d7b8c059dc..ce7e3823519231e6489681663a5cc89eaccf600c 100644 (file)
@@ -20,4 +20,19 @@ use wcf\data\DatabaseObjectList;
  */
 class UnfurlUrlList extends DatabaseObjectList
 {
+    /**
+     * @inheritDoc
+     */
+    public function __construct()
+    {
+        parent::__construct();
+
+        if (!empty($this->sqlSelects)) {
+            $this->sqlSelects .= ',';
+        }
+        $this->sqlSelects .= "unfurl_url_image.*";
+        $this->sqlJoins .= "
+            LEFT JOIN   wcf" . WCF_N . "_unfurl_url_image unfurl_url_image
+            ON          unfurl_url_image.imageID = unfurl_url.imageID";
+    }
 }
index c888b89d5c1465a6c1b0d3a396232c3402a222db..2e4e968586b015de0ff1dd09604d2f288bab22e2 100644 (file)
@@ -10,8 +10,10 @@ use wcf\system\message\unfurl\exception\DownloadFailed;
 use wcf\system\message\unfurl\exception\ParsingFailed;
 use wcf\system\message\unfurl\exception\UrlInaccessible;
 use wcf\system\message\unfurl\UnfurlResponse;
+use wcf\system\WCF;
 use wcf\util\FileUtil;
 use wcf\util\StringUtil;
+use wcf\util\Url;
 
 /**
  * Represents a background job to get information for an url.
@@ -79,42 +81,22 @@ final class UnfurlUrlBackgroundJob extends AbstractBackgroundJob
                 $description = StringUtil::truncate($unfurlResponse->getDescription());
             }
 
-            if ($unfurlResponse->getImageUrl()) {
-                try {
-                    $image = $this->downloadImage($unfurlResponse->getImage());
-                    $imageData = \getimagesizefromstring($image);
-                    if ($imageData !== false) {
-                        $imageType = $this->validateImage($imageData);
-                        if (!(MODULE_IMAGE_PROXY || IMAGE_ALLOW_EXTERNAL_SOURCE)) {
-                            $imageHash = $this->saveImage($imageData, $image);
-                        } else {
-                            $imageHash = "";
-                        }
-                    } else {
-                        $imageType = UnfurlUrl::IMAGE_NO_IMAGE;
-                    }
+            $imageData = [];
+            $imageID = null;
+            if ($unfurlResponse->getImageUrl() && Url::is($unfurlResponse->getImageUrl())) {
+                $imageID = self::getImageIdByUrl($unfurlResponse->getImageUrl());
 
-                    if ($imageType === UnfurlUrl::IMAGE_NO_IMAGE) {
-                        $imageUrl = $imageHash = "";
-                    } else {
-                        $imageUrl = $unfurlResponse->getImageUrl();
-                    }
-                } catch (UrlInaccessible | DownloadFailed $e) {
-                    $imageType = UnfurlUrl::IMAGE_NO_IMAGE;
-                    $imageUrl = $imageHash = "";
+                if ($imageID === null) {
+                    $imageData = $this->getImageData($unfurlResponse);
                 }
-            } else {
-                $imageType = UnfurlUrl::IMAGE_NO_IMAGE;
-                $imageUrl = $imageHash = "";
             }
 
             $this->save(
                 UnfurlUrl::STATUS_SUCCESSFUL,
                 $title,
                 $description,
-                $imageType,
-                $imageUrl,
-                $imageHash
+                $imageID,
+                $imageData
             );
         } catch (UrlInaccessible | ParsingFailed $e) {
             if (\ENABLE_DEBUG_MODE) {
@@ -125,6 +107,54 @@ final class UnfurlUrlBackgroundJob extends AbstractBackgroundJob
         }
     }
 
+    private function getImageData(UnfurlResponse $unfurlResponse): array
+    {
+        $imageSaveData = [];
+
+        if (empty($unfurlResponse->getImageUrl()) || !Url::is($unfurlResponse->getImageUrl())) {
+            throw new BadMethodCallException("Invalid image given.");
+        }
+
+        try {
+            $imageResponse = $unfurlResponse->getImage();
+            $image = $this->downloadImage($imageResponse);
+            $imageData = \getimagesizefromstring($image);
+
+            if ($imageData !== false) {
+                if ($this->validateImage($imageData)) {
+                    $imageSaveData['imageUrl'] = $unfurlResponse->getImageUrl();
+                    $imageSaveData['width'] = $imageData[0];
+                    $imageSaveData['height'] = $imageData[1];
+                    if (!(MODULE_IMAGE_PROXY || IMAGE_ALLOW_EXTERNAL_SOURCE)) {
+                        $imageSaveData['imageHash'] = $this->saveImage($imageData, $image);
+                        $imageSaveData['imageExtension'] = $this->getImageExtension($imageData);
+                    }
+                }
+            }
+        } catch (UrlInaccessible | DownloadFailed $e) {
+            $imageSaveData = [];
+        }
+
+        return $imageSaveData;
+    }
+
+    private static function getImageIdByUrl(string $url): ?int
+    {
+        $sql = "SELECT  imageID
+                FROM    wcf" . WCF_N . "_unfurl_url_image
+                WHERE   imageUrl = ?";
+        $statement = WCF::getDB()->prepareStatement($sql);
+        $statement->execute([$url]);
+
+        $imageID = $statement->fetchSingleColumn();
+
+        if ($imageID === false) {
+            return null;
+        }
+
+        return $imageID;
+    }
+
     private function downloadImage(Response $imageResponse): string
     {
         $image = "";
@@ -143,61 +173,65 @@ final class UnfurlUrlBackgroundJob extends AbstractBackgroundJob
         return $image;
     }
 
-    private function validateImage(array $imageData): string
+    private function validateImage(array $imageData): bool
     {
         $isSquared = $imageData[0] === $imageData[1];
         if (
             (!$isSquared && ($imageData[0] < 300 && $imageData[1] < 150))
             || \min($imageData[0], $imageData[1]) < 50
         ) {
-            return UnfurlUrl::IMAGE_NO_IMAGE;
-        } else {
-            if ($isSquared) {
-                return UnfurlUrl::IMAGE_SQUARED;
-            } else {
-                return UnfurlUrl::IMAGE_COVER;
-            }
+            return false;
+        }
+
+        if (!$this->getImageExtension($imageData)) {
+            return false;
         }
+
+        return true;
     }
 
     private function saveImage(array $imageData, string $image): string
     {
-        switch ($imageData[2]) {
-            case \IMAGETYPE_PNG:
-                $extension = 'png';
-                break;
-            case \IMAGETYPE_GIF:
-                $extension = 'gif';
-                break;
-            case \IMAGETYPE_JPEG:
-                $extension = 'jpg';
-                break;
-
-            default:
-                throw new DownloadFailed();
-        }
-
         $imageHash = \sha1($image);
 
         $path = WCF_DIR . 'images/unfurlUrl/' . \substr($imageHash, 0, 2);
         FileUtil::makePath($path);
 
+        $extension = $this->getImageExtension($imageData);
+
         $fileLocation = $path . '/' . $imageHash . '.' . $extension;
 
         \file_put_contents($fileLocation, $image);
 
         @\touch($fileLocation);
 
-        return $imageHash . '.' . $extension;
+        return $imageHash;
+    }
+
+    private function getImageExtension(array $imageData): ?string
+    {
+        switch ($imageData[2]) {
+            case \IMAGETYPE_PNG:
+                return 'png';
+                break;
+            case \IMAGETYPE_GIF:
+                return 'gif';
+                break;
+            case \IMAGETYPE_JPEG:
+                return 'jpg';
+                break;
+
+            default:
+                return null;
+        }
     }
 
     private function save(
         string $status,
         string $title = "",
         string $description = "",
-        string $imageType = UnfurlUrl::IMAGE_NO_IMAGE,
-        string $imageUrl = "",
-        string $imageHash = ""
+        ?int $imageID = null,
+        array $imageData = []
     ): void {
         switch ($status) {
             case UnfurlUrl::STATUS_PENDING:
@@ -209,14 +243,8 @@ final class UnfurlUrlBackgroundJob extends AbstractBackgroundJob
                 throw new BadMethodCallException("Invalid status '{$status}' given.");
         }
 
-        switch ($imageType) {
-            case UnfurlUrl::IMAGE_COVER:
-            case UnfurlUrl::IMAGE_NO_IMAGE:
-            case UnfurlUrl::IMAGE_SQUARED:
-                break;
-
-            default:
-                throw new BadMethodCallException("Invalid imageType '{$imageType}' given.");
+        if ($imageID !== null && !empty($imageData)) {
+            throw new BadMethodCallException("You cannot pass an imageID and imageData at the same time.");
         }
 
         $urlAction = new UnfurlUrlAction([$this->urlID], 'update', [
@@ -224,11 +252,10 @@ final class UnfurlUrlBackgroundJob extends AbstractBackgroundJob
                 'status' => $status,
                 'title' => $title,
                 'description' => $description,
-                'imageType' => $imageType,
-                'imageUrl' => $imageUrl,
-                'imageHash' => $imageHash,
+                'imageID' => $imageID,
                 'lastFetch' => TIME_NOW,
             ],
+            'imageData' => $imageData,
         ]);
         $urlAction->executeAction();
     }
index 5b68c62cdeade6117b95da1292559a703b33fb59..bf04c662c22f7de3d6357b48675aaa192508f3a4 100644 (file)
@@ -1450,14 +1450,26 @@ 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 (urlHash),
+       urlHash VARCHAR(40) NOT NULL,
        title VARCHAR(255) NOT NULL DEFAULT '',
        description TEXT,
-       imageUrl TEXT NOT NULL,
-       imageType VARCHAR(255) NOT NULL DEFAULT 'NOIMAGE',
-       imageHash VARCHAR(45) NOT NULL DEFAULT '',
+       imageID INT(10),
        status VARCHAR(255) NOT NULL DEFAULT 'PENDING',
-       lastFetch INT(10) NOT NULL DEFAULT 0
+       lastFetch INT(10) NOT NULL DEFAULT 0,
+
+       UNIQUE KEY urlHash (urlHash)
+);
+
+DROP TABLE IF EXISTS wcf1_unfurl_url_image;
+CREATE TABLE wcf1_unfurl_url_image (
+       imageID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       imageUrl TEXT NOT NULL,
+       imageHash VARCHAR(40) DEFAULT NULL,
+       width INT(10) NOT NULL,
+       height INT(10) NOT NULL,
+       imageExtension VARCHAR(4) DEFAULT NULL,
+
+       UNIQUE KEY imageHash (imageHash)
 );
 
 DROP TABLE IF EXISTS wcf1_user;
@@ -2181,6 +2193,8 @@ ALTER TABLE wcf1_tracked_visit ADD FOREIGN KEY (userID) REFERENCES wcf1_user (us
 ALTER TABLE wcf1_tracked_visit_type ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
 ALTER TABLE wcf1_tracked_visit_type ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;
 
+ALTER TABLE wcf1_unfurl_url ADD FOREIGN KEY (imageID) REFERENCES wcf1_unfurl_url_image (imageID) ON DELETE SET NULL;
+
 ALTER TABLE wcf1_user ADD FOREIGN KEY (avatarID) REFERENCES wcf1_user_avatar (avatarID) ON DELETE SET NULL;
 ALTER TABLE wcf1_user ADD FOREIGN KEY (rankID) REFERENCES wcf1_user_rank (rankID) ON DELETE SET NULL;
 ALTER TABLE wcf1_user ADD FOREIGN KEY (userOnlineGroupID) REFERENCES wcf1_user_group (groupID) ON DELETE SET NULL;