MM-11272 Add OpenGraph and image dimension metadata to posts (#9313)
* Move OpenGraph code into its own file * Move OpenGraph image proxying to app layer * Move test file code out of api4 package * MM-11272 Add OpenGraph and image dimension metadata to posts
Этот коммит содержится в:
@@ -4,7 +4,6 @@
|
|||||||
package api4
|
package api4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -746,22 +745,6 @@ func CheckInternalErrorStatus(t *testing.T, resp *model.Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readTestFile(name string) ([]byte, error) {
|
|
||||||
path, _ := utils.FindDir("tests")
|
|
||||||
file, err := os.Open(filepath.Join(path, name))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
data := &bytes.Buffer{}
|
|
||||||
if _, err := io.Copy(data, file); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return data.Bytes(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
|
// Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
|
||||||
// If signV2 input is false, function always returns signature v4.
|
// If signV2 input is false, function always returns signature v4.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ package api4
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/utils/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetBrandImage(t *testing.T) {
|
func TestGetBrandImage(t *testing.T) {
|
||||||
@@ -29,7 +31,7 @@ func TestUploadBrandImage(t *testing.T) {
|
|||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
Client := th.Client
|
Client := th.Client
|
||||||
|
|
||||||
data, err := readTestFile("test.png")
|
data, err := testutils.ReadTestFile("test.png")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/store"
|
"github.com/mattermost/mattermost-server/store"
|
||||||
|
"github.com/mattermost/mattermost-server/utils/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUploadFileAsMultipart(t *testing.T) {
|
func TestUploadFileAsMultipart(t *testing.T) {
|
||||||
@@ -25,7 +26,7 @@ func TestUploadFileAsMultipart(t *testing.T) {
|
|||||||
var uploadInfo *model.FileInfo
|
var uploadInfo *model.FileInfo
|
||||||
var data []byte
|
var data []byte
|
||||||
var err error
|
var err error
|
||||||
if data, err = readTestFile("test.png"); err != nil {
|
if data, err = testutils.ReadTestFile("test.png"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if fileResp, resp := Client.UploadFile(data, channel.Id, "test.png"); resp.Error != nil {
|
} else if fileResp, resp := Client.UploadFile(data, channel.Id, "test.png"); resp.Error != nil {
|
||||||
t.Fatal(resp.Error)
|
t.Fatal(resp.Error)
|
||||||
@@ -138,7 +139,7 @@ func TestUploadFileAsRequestBody(t *testing.T) {
|
|||||||
var uploadInfo *model.FileInfo
|
var uploadInfo *model.FileInfo
|
||||||
var data []byte
|
var data []byte
|
||||||
var err error
|
var err error
|
||||||
if data, err = readTestFile("test.png"); err != nil {
|
if data, err = testutils.ReadTestFile("test.png"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if fileResp, resp := Client.UploadFileAsRequestBody(data, channel.Id, "test.png"); resp.Error != nil {
|
} else if fileResp, resp := Client.UploadFileAsRequestBody(data, channel.Id, "test.png"); resp.Error != nil {
|
||||||
t.Fatal(resp.Error)
|
t.Fatal(resp.Error)
|
||||||
@@ -263,7 +264,7 @@ func TestGetFile(t *testing.T) {
|
|||||||
fileId := ""
|
fileId := ""
|
||||||
var sent []byte
|
var sent []byte
|
||||||
var err error
|
var err error
|
||||||
if sent, err = readTestFile("test.png"); err != nil {
|
if sent, err = testutils.ReadTestFile("test.png"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png")
|
fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png")
|
||||||
@@ -378,7 +379,7 @@ func TestGetFileThumbnail(t *testing.T) {
|
|||||||
fileId := ""
|
fileId := ""
|
||||||
var sent []byte
|
var sent []byte
|
||||||
var err error
|
var err error
|
||||||
if sent, err = readTestFile("test.png"); err != nil {
|
if sent, err = testutils.ReadTestFile("test.png"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png")
|
fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png")
|
||||||
@@ -437,7 +438,7 @@ func TestGetFileLink(t *testing.T) {
|
|||||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = model.NewId() })
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = model.NewId() })
|
||||||
|
|
||||||
fileId := ""
|
fileId := ""
|
||||||
if data, err := readTestFile("test.png"); err != nil {
|
if data, err := testutils.ReadTestFile("test.png"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
fileResp, resp := Client.UploadFile(data, channel.Id, "test.png")
|
fileResp, resp := Client.UploadFile(data, channel.Id, "test.png")
|
||||||
@@ -506,7 +507,7 @@ func TestGetFilePreview(t *testing.T) {
|
|||||||
fileId := ""
|
fileId := ""
|
||||||
var sent []byte
|
var sent []byte
|
||||||
var err error
|
var err error
|
||||||
if sent, err = readTestFile("test.png"); err != nil {
|
if sent, err = testutils.ReadTestFile("test.png"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png")
|
fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png")
|
||||||
@@ -559,7 +560,7 @@ func TestGetFileInfo(t *testing.T) {
|
|||||||
fileId := ""
|
fileId := ""
|
||||||
var sent []byte
|
var sent []byte
|
||||||
var err error
|
var err error
|
||||||
if sent, err = readTestFile("test.png"); err != nil {
|
if sent, err = testutils.ReadTestFile("test.png"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png")
|
fileResp, resp := Client.UploadFile(sent, channel.Id, "test.png")
|
||||||
@@ -632,7 +633,7 @@ func TestGetPublicFile(t *testing.T) {
|
|||||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = GenerateTestId() })
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = GenerateTestId() })
|
||||||
|
|
||||||
fileId := ""
|
fileId := ""
|
||||||
if data, err := readTestFile("test.png"); err != nil {
|
if data, err := testutils.ReadTestFile("test.png"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
fileResp, resp := Client.UploadFile(data, channel.Id, "test.png")
|
fileResp, resp := Client.UploadFile(data, channel.Id, "test.png")
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package api4
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/dyatlov/go-opengraph/opengraph"
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/utils"
|
"github.com/mattermost/mattermost-server/utils"
|
||||||
)
|
)
|
||||||
@@ -27,22 +26,6 @@ func (api *API) InitOpenGraph() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenGraphDataWithProxyAddedToImageURLs(ogdata *opengraph.OpenGraph, toProxyURL func(string) string) *opengraph.OpenGraph {
|
|
||||||
for _, image := range ogdata.Images {
|
|
||||||
var url string
|
|
||||||
if image.SecureURL != "" {
|
|
||||||
url = image.SecureURL
|
|
||||||
} else {
|
|
||||||
url = image.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
image.URL = ""
|
|
||||||
image.SecureURL = toProxyURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ogdata
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOpenGraphMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getOpenGraphMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
if !*c.App.Config().ServiceSettings.EnableLinkPreviews {
|
if !*c.App.Config().ServiceSettings.EnableLinkPreviews {
|
||||||
c.Err = model.NewAppError("getOpenGraphMetadata", "api.post.link_preview_disabled.app_error", nil, "", http.StatusNotImplemented)
|
c.Err = model.NewAppError("getOpenGraphMetadata", "api.post.link_preview_disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||||
@@ -65,12 +48,6 @@ func getOpenGraphMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
og := c.App.GetOpenGraphMetadata(url)
|
og := c.App.GetOpenGraphMetadata(url)
|
||||||
|
|
||||||
// If image proxy enabled modify open graph data to feed though proxy
|
|
||||||
if toProxyURL := c.App.ImageProxyAdder(); toProxyURL != nil {
|
|
||||||
og = OpenGraphDataWithProxyAddedToImageURLs(og, toProxyURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
ogJSON, err := og.ToJSON()
|
ogJSON, err := og.ToJSON()
|
||||||
openGraphDataCache.AddWithExpiresInSecs(props["url"], ogJSON, 3600) // Cache would expire after 1 hour
|
openGraphDataCache.AddWithExpiresInSecs(props["url"], ogJSON, 3600) // Cache would expire after 1 hour
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/services/mailservice"
|
"github.com/mattermost/mattermost-server/services/mailservice"
|
||||||
"github.com/mattermost/mattermost-server/utils"
|
"github.com/mattermost/mattermost-server/utils"
|
||||||
|
"github.com/mattermost/mattermost-server/utils/testutils"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -1813,7 +1814,7 @@ func TestImportTeam(t *testing.T) {
|
|||||||
t.Run("ImportTeam", func(t *testing.T) {
|
t.Run("ImportTeam", func(t *testing.T) {
|
||||||
var data []byte
|
var data []byte
|
||||||
var err error
|
var err error
|
||||||
data, err = readTestFile("Fake_Team_Import.zip")
|
data, err = testutils.ReadTestFile("Fake_Team_Import.zip")
|
||||||
if err != nil && len(data) == 0 {
|
if err != nil && len(data) == 0 {
|
||||||
t.Fatal("Error while reading the test file.")
|
t.Fatal("Error while reading the test file.")
|
||||||
}
|
}
|
||||||
@@ -1874,7 +1875,7 @@ func TestImportTeam(t *testing.T) {
|
|||||||
t.Run("WrongPermission", func(t *testing.T) {
|
t.Run("WrongPermission", func(t *testing.T) {
|
||||||
var data []byte
|
var data []byte
|
||||||
var err error
|
var err error
|
||||||
data, err = readTestFile("Fake_Team_Import.zip")
|
data, err = testutils.ReadTestFile("Fake_Team_Import.zip")
|
||||||
if err != nil && len(data) == 0 {
|
if err != nil && len(data) == 0 {
|
||||||
t.Fatal("Error while reading the test file.")
|
t.Fatal("Error while reading the test file.")
|
||||||
}
|
}
|
||||||
@@ -2029,7 +2030,7 @@ func TestSetTeamIcon(t *testing.T) {
|
|||||||
Client := th.Client
|
Client := th.Client
|
||||||
team := th.BasicTeam
|
team := th.BasicTeam
|
||||||
|
|
||||||
data, err := readTestFile("test.png")
|
data, err := testutils.ReadTestFile("test.png")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -2109,7 +2110,7 @@ func TestRemoveTeamIcon(t *testing.T) {
|
|||||||
team := th.BasicTeam
|
team := th.BasicTeam
|
||||||
|
|
||||||
th.LoginTeamAdmin()
|
th.LoginTeamAdmin()
|
||||||
data, _ := readTestFile("test.png")
|
data, _ := testutils.ReadTestFile("test.png")
|
||||||
Client.SetTeamIcon(team.Id, data)
|
Client.SetTeamIcon(team.Id, data)
|
||||||
|
|
||||||
_, resp := Client.RemoveTeamIcon(team.Id)
|
_, resp := Client.RemoveTeamIcon(team.Id)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/app"
|
"github.com/mattermost/mattermost-server/app"
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/store"
|
"github.com/mattermost/mattermost-server/store"
|
||||||
|
"github.com/mattermost/mattermost-server/utils/testutils"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -2265,7 +2266,7 @@ func TestSetProfileImage(t *testing.T) {
|
|||||||
Client := th.Client
|
Client := th.Client
|
||||||
user := th.BasicUser
|
user := th.BasicUser
|
||||||
|
|
||||||
data, err := readTestFile("test.png")
|
data, err := testutils.ReadTestFile("test.png")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,8 @@ func New(options ...Option) (outApp *App, outErr error) {
|
|||||||
handlers: make(map[string]webSocketHandler),
|
handlers: make(map[string]webSocketHandler),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.InitPostMetadata()
|
||||||
|
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
app/opengraph.go
Обычный файл
116
app/opengraph.go
Обычный файл
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/dyatlov/go-opengraph/opengraph"
|
||||||
|
"github.com/mattermost/mattermost-server/mlog"
|
||||||
|
"golang.org/x/net/html/charset"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) GetOpenGraphMetadata(requestURL string) *opengraph.OpenGraph {
|
||||||
|
res, err := a.HTTPClient(false).Get(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Error("GetOpenGraphMetadata request failed", mlog.String("requestURL", requestURL), mlog.Any("err", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer consumeAndClose(res)
|
||||||
|
|
||||||
|
return a.ParseOpenGraphMetadata(requestURL, res.Body, res.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ParseOpenGraphMetadata(requestURL string, body io.Reader, contentType string) *opengraph.OpenGraph {
|
||||||
|
og := opengraph.NewOpenGraph()
|
||||||
|
|
||||||
|
body = forceHTMLEncodingToUTF8(body, contentType)
|
||||||
|
|
||||||
|
if err := og.ProcessHTML(body); err != nil {
|
||||||
|
mlog.Error("ParseOpenGraphMetadata processing failed", mlog.String("requestURL", requestURL), mlog.Any("err", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
makeOpenGraphURLsAbsolute(og, requestURL)
|
||||||
|
|
||||||
|
// If image proxy enabled modify open graph data to feed though proxy
|
||||||
|
if toProxyURL := a.ImageProxyAdder(); toProxyURL != nil {
|
||||||
|
og = OpenGraphDataWithProxyAddedToImageURLs(og, toProxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The URL should be the link the user provided in their message, not a redirected one.
|
||||||
|
if og.URL != "" {
|
||||||
|
og.URL = requestURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return og
|
||||||
|
}
|
||||||
|
|
||||||
|
func forceHTMLEncodingToUTF8(body io.Reader, contentType string) io.Reader {
|
||||||
|
r, err := charset.NewReader(body, contentType)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Error("forceHTMLEncodingToUTF8 failed to convert", mlog.String("contentType", contentType), mlog.Any("err", err))
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeOpenGraphURLsAbsolute(og *opengraph.OpenGraph, requestURL string) {
|
||||||
|
parsedRequestURL, err := url.Parse(requestURL)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Warn("makeOpenGraphURLsAbsolute failed to parse url", mlog.String("requestURL", requestURL), mlog.Any("err", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
makeURLAbsolute := func(resultURL string) string {
|
||||||
|
if resultURL == "" {
|
||||||
|
return resultURL
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedResultURL, err := url.Parse(resultURL)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Warn("makeOpenGraphURLsAbsolute failed to parse result", mlog.String("requestURL", requestURL), mlog.Any("err", err))
|
||||||
|
return resultURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedResultURL.IsAbs() {
|
||||||
|
return resultURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedRequestURL.ResolveReference(parsedResultURL).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
og.URL = makeURLAbsolute(og.URL)
|
||||||
|
|
||||||
|
for _, image := range og.Images {
|
||||||
|
image.URL = makeURLAbsolute(image.URL)
|
||||||
|
image.SecureURL = makeURLAbsolute(image.SecureURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, audio := range og.Audios {
|
||||||
|
audio.URL = makeURLAbsolute(audio.URL)
|
||||||
|
audio.SecureURL = makeURLAbsolute(audio.SecureURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, video := range og.Videos {
|
||||||
|
video.URL = makeURLAbsolute(video.URL)
|
||||||
|
video.SecureURL = makeURLAbsolute(video.SecureURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenGraphDataWithProxyAddedToImageURLs(ogdata *opengraph.OpenGraph, toProxyURL func(string) string) *opengraph.OpenGraph {
|
||||||
|
for _, image := range ogdata.Images {
|
||||||
|
var url string
|
||||||
|
if image.SecureURL != "" {
|
||||||
|
url = image.SecureURL
|
||||||
|
} else {
|
||||||
|
url = image.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
image.URL = ""
|
||||||
|
image.SecureURL = toProxyURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ogdata
|
||||||
|
}
|
||||||
129
app/opengraph_test.go
Обычный файл
129
app/opengraph_test.go
Обычный файл
@@ -0,0 +1,129 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dyatlov/go-opengraph/opengraph"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkForceHTMLEncodingToUTF8(b *testing.B) {
|
||||||
|
HTML := `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta property="og:url" content="https://example.com/apps/mattermost">
|
||||||
|
<meta property="og:image" content="https://images.example.com/image.png">
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
ContentType := "text/html; utf-8"
|
||||||
|
|
||||||
|
b.Run("with converting", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
r := forceHTMLEncodingToUTF8(strings.NewReader(HTML), ContentType)
|
||||||
|
|
||||||
|
og := opengraph.NewOpenGraph()
|
||||||
|
og.ProcessHTML(r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("without converting", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
og := opengraph.NewOpenGraph()
|
||||||
|
og.ProcessHTML(strings.NewReader(HTML))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeOpenGraphURLsAbsolute(t *testing.T) {
|
||||||
|
for name, tc := range map[string]struct {
|
||||||
|
HTML string
|
||||||
|
RequestURL string
|
||||||
|
URL string
|
||||||
|
ImageURL string
|
||||||
|
}{
|
||||||
|
"absolute URLs": {
|
||||||
|
HTML: `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta property="og:url" content="https://example.com/apps/mattermost">
|
||||||
|
<meta property="og:image" content="https://images.example.com/image.png">
|
||||||
|
</head>
|
||||||
|
</html>`,
|
||||||
|
RequestURL: "https://example.com",
|
||||||
|
URL: "https://example.com/apps/mattermost",
|
||||||
|
ImageURL: "https://images.example.com/image.png",
|
||||||
|
},
|
||||||
|
"URLs starting with /": {
|
||||||
|
HTML: `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta property="og:url" content="/apps/mattermost">
|
||||||
|
<meta property="og:image" content="/image.png">
|
||||||
|
</head>
|
||||||
|
</html>`,
|
||||||
|
RequestURL: "http://example.com",
|
||||||
|
URL: "http://example.com/apps/mattermost",
|
||||||
|
ImageURL: "http://example.com/image.png",
|
||||||
|
},
|
||||||
|
"HTTPS URLs starting with /": {
|
||||||
|
HTML: `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta property="og:url" content="/apps/mattermost">
|
||||||
|
<meta property="og:image" content="/image.png">
|
||||||
|
</head>
|
||||||
|
</html>`,
|
||||||
|
RequestURL: "https://example.com",
|
||||||
|
URL: "https://example.com/apps/mattermost",
|
||||||
|
ImageURL: "https://example.com/image.png",
|
||||||
|
},
|
||||||
|
"missing image URL": {
|
||||||
|
HTML: `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta property="og:url" content="/apps/mattermost">
|
||||||
|
</head>
|
||||||
|
</html>`,
|
||||||
|
RequestURL: "http://example.com",
|
||||||
|
URL: "http://example.com/apps/mattermost",
|
||||||
|
ImageURL: "",
|
||||||
|
},
|
||||||
|
"relative URLs": {
|
||||||
|
HTML: `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta property="og:url" content="index.html">
|
||||||
|
<meta property="og:image" content="../resources/image.png">
|
||||||
|
</head>
|
||||||
|
</html>`,
|
||||||
|
RequestURL: "http://example.com/content/index.html",
|
||||||
|
URL: "http://example.com/content/index.html",
|
||||||
|
ImageURL: "http://example.com/resources/image.png",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
og := opengraph.NewOpenGraph()
|
||||||
|
if err := og.ProcessHTML(strings.NewReader(tc.HTML)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeOpenGraphURLsAbsolute(og, tc.RequestURL)
|
||||||
|
|
||||||
|
if og.URL != tc.URL {
|
||||||
|
t.Fatalf("incorrect url, expected %v, got %v", tc.URL, og.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(og.Images) > 0 {
|
||||||
|
if og.Images[0].URL != tc.ImageURL {
|
||||||
|
t.Fatalf("incorrect image url, expected %v, got %v", tc.ImageURL, og.Images[0].URL)
|
||||||
|
}
|
||||||
|
} else if tc.ImageURL != "" {
|
||||||
|
t.Fatalf("missing image url, expected %v, got nothing", tc.ImageURL)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
83
app/post.go
83
app/post.go
@@ -9,15 +9,11 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dyatlov/go-opengraph/opengraph"
|
|
||||||
"golang.org/x/net/html/charset"
|
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/mlog"
|
"github.com/mattermost/mattermost-server/mlog"
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/plugin"
|
"github.com/mattermost/mattermost-server/plugin"
|
||||||
@@ -798,85 +794,6 @@ func (a *App) GetFileInfosForPost(postId string, readFromMaster bool) ([]*model.
|
|||||||
return infos, nil
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetOpenGraphMetadata(requestURL string) *opengraph.OpenGraph {
|
|
||||||
og := opengraph.NewOpenGraph()
|
|
||||||
|
|
||||||
res, err := a.HTTPService.MakeClient(false).Get(requestURL)
|
|
||||||
if err != nil {
|
|
||||||
mlog.Error(fmt.Sprintf("GetOpenGraphMetadata request failed for url=%v with err=%v", requestURL, err.Error()))
|
|
||||||
return og
|
|
||||||
}
|
|
||||||
defer consumeAndClose(res)
|
|
||||||
|
|
||||||
contentType := res.Header.Get("Content-Type")
|
|
||||||
body := forceHTMLEncodingToUTF8(res.Body, contentType)
|
|
||||||
|
|
||||||
if err := og.ProcessHTML(body); err != nil {
|
|
||||||
mlog.Error(fmt.Sprintf("GetOpenGraphMetadata processing failed for url=%v with err=%v", requestURL, err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
makeOpenGraphURLsAbsolute(og, requestURL)
|
|
||||||
|
|
||||||
// The URL should be the link the user provided in their message, not a redirected one.
|
|
||||||
if og.URL != "" {
|
|
||||||
og.URL = requestURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return og
|
|
||||||
}
|
|
||||||
|
|
||||||
func forceHTMLEncodingToUTF8(body io.Reader, contentType string) io.Reader {
|
|
||||||
r, err := charset.NewReader(body, contentType)
|
|
||||||
if err != nil {
|
|
||||||
mlog.Error(fmt.Sprintf("forceHTMLEncodingToUTF8 failed to convert for contentType=%v with err=%v", contentType, err.Error()))
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeOpenGraphURLsAbsolute(og *opengraph.OpenGraph, requestURL string) {
|
|
||||||
parsedRequestURL, err := url.Parse(requestURL)
|
|
||||||
if err != nil {
|
|
||||||
mlog.Warn(fmt.Sprintf("makeOpenGraphURLsAbsolute failed to parse url=%v", requestURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
makeURLAbsolute := func(resultURL string) string {
|
|
||||||
if resultURL == "" {
|
|
||||||
return resultURL
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedResultURL, err := url.Parse(resultURL)
|
|
||||||
if err != nil {
|
|
||||||
mlog.Warn(fmt.Sprintf("makeOpenGraphURLsAbsolute failed to parse result url=%v", resultURL))
|
|
||||||
return resultURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedResultURL.IsAbs() {
|
|
||||||
return resultURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedRequestURL.ResolveReference(parsedResultURL).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
og.URL = makeURLAbsolute(og.URL)
|
|
||||||
|
|
||||||
for _, image := range og.Images {
|
|
||||||
image.URL = makeURLAbsolute(image.URL)
|
|
||||||
image.SecureURL = makeURLAbsolute(image.SecureURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, audio := range og.Audios {
|
|
||||||
audio.URL = makeURLAbsolute(audio.URL)
|
|
||||||
audio.SecureURL = makeURLAbsolute(audio.SecureURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, video := range og.Videos {
|
|
||||||
video.URL = makeURLAbsolute(video.URL)
|
|
||||||
video.SecureURL = makeURLAbsolute(video.SecureURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) DoPostAction(postId, actionId, userId, selectedOption string) *model.AppError {
|
func (a *App) DoPostAction(postId, actionId, userId, selectedOption string) *model.AppError {
|
||||||
pchan := a.Srv.Store.Post().GetSingle(postId)
|
pchan := a.Srv.Store.Post().GetSingle(postId)
|
||||||
cchan := a.Srv.Store.Channel().GetForPost(postId)
|
cchan := a.Srv.Store.Channel().GetForPost(postId)
|
||||||
|
|||||||
@@ -4,12 +4,32 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dyatlov/go-opengraph/opengraph"
|
"github.com/dyatlov/go-opengraph/opengraph"
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
|
"github.com/mattermost/mattermost-server/utils"
|
||||||
|
"github.com/mattermost/mattermost-server/utils/markdown"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const LINK_CACHE_SIZE = 10000
|
||||||
|
const LINK_CACHE_DURATION = 3600
|
||||||
|
|
||||||
|
var linkCache = utils.NewLru(LINK_CACHE_SIZE)
|
||||||
|
|
||||||
|
func (a *App) InitPostMetadata() {
|
||||||
|
// Dump any cached links if the proxy settings have changed so image URLs can be updated
|
||||||
|
a.AddConfigListener(func(before, after *model.Config) {
|
||||||
|
if (before.ServiceSettings.ImageProxyType != after.ServiceSettings.ImageProxyType) ||
|
||||||
|
(before.ServiceSettings.ImageProxyURL != after.ServiceSettings.ImageProxyType) {
|
||||||
|
linkCache.Purge()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) PreparePostListForClient(originalList *model.PostList) (*model.PostList, *model.AppError) {
|
func (a *App) PreparePostListForClient(originalList *model.PostList) (*model.PostList, *model.AppError) {
|
||||||
list := &model.PostList{
|
list := &model.PostList{
|
||||||
Posts: make(map[string]*model.Post),
|
Posts: make(map[string]*model.Post),
|
||||||
@@ -31,15 +51,15 @@ func (a *App) PreparePostListForClient(originalList *model.PostList) (*model.Pos
|
|||||||
func (a *App) PreparePostForClient(originalPost *model.Post) (*model.Post, *model.AppError) {
|
func (a *App) PreparePostForClient(originalPost *model.Post) (*model.Post, *model.AppError) {
|
||||||
post := originalPost.Clone()
|
post := originalPost.Clone()
|
||||||
|
|
||||||
var err *model.AppError
|
|
||||||
|
|
||||||
needReactionCounts := post.ReactionCounts == nil
|
needReactionCounts := post.ReactionCounts == nil
|
||||||
needEmojis := post.Emojis == nil
|
needEmojis := post.Emojis == nil
|
||||||
needImageDimensions := post.ImageDimensions == nil
|
|
||||||
needOpenGraphData := post.OpenGraphData == nil
|
needOpenGraphData := post.OpenGraphData == nil
|
||||||
|
needImageDimensions := post.ImageDimensions == nil
|
||||||
|
|
||||||
|
// Get reactions to post
|
||||||
var reactions []*model.Reaction
|
var reactions []*model.Reaction
|
||||||
if needReactionCounts || needEmojis {
|
if needReactionCounts || needEmojis {
|
||||||
|
var err *model.AppError
|
||||||
reactions, err = a.GetReactionsForPost(post.Id)
|
reactions, err = a.GetReactionsForPost(post.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return post, err
|
return post, err
|
||||||
@@ -50,15 +70,7 @@ func (a *App) PreparePostForClient(originalPost *model.Post) (*model.Post, *mode
|
|||||||
post.ReactionCounts = model.CountReactions(reactions)
|
post.ReactionCounts = model.CountReactions(reactions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if post.FileInfos == nil {
|
// Get emojis for post
|
||||||
fileInfos, err := a.GetFileInfosForPost(post.Id, false)
|
|
||||||
if err != nil {
|
|
||||||
return post, err
|
|
||||||
}
|
|
||||||
|
|
||||||
post.FileInfos = fileInfos
|
|
||||||
}
|
|
||||||
|
|
||||||
if needEmojis {
|
if needEmojis {
|
||||||
emojis, err := a.getCustomEmojisForPost(post.Message, reactions)
|
emojis, err := a.getCustomEmojisForPost(post.Message, reactions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -68,23 +80,79 @@ func (a *App) PreparePostForClient(originalPost *model.Post) (*model.Post, *mode
|
|||||||
post.Emojis = emojis
|
post.Emojis = emojis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get files for post
|
||||||
|
if post.FileInfos == nil {
|
||||||
|
fileInfos, err := a.GetFileInfosForPost(post.Id, false)
|
||||||
|
if err != nil {
|
||||||
|
return post, err
|
||||||
|
}
|
||||||
|
|
||||||
|
post.FileInfos = fileInfos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy image links in post
|
||||||
post = a.PostWithProxyAddedToImageURLs(post)
|
post = a.PostWithProxyAddedToImageURLs(post)
|
||||||
|
|
||||||
if needImageDimensions || needOpenGraphData {
|
// Get OpenGraph and image metadata
|
||||||
if needImageDimensions {
|
if needOpenGraphData || needImageDimensions {
|
||||||
post.ImageDimensions = []*model.PostImageDimensions{}
|
err := a.preparePostWithOpenGraphAndImageMetadata(post, needOpenGraphData, needImageDimensions)
|
||||||
|
if err != nil {
|
||||||
|
return post, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if needOpenGraphData {
|
|
||||||
post.OpenGraphData = []*opengraph.OpenGraph{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return post, nil
|
return post, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) preparePostWithOpenGraphAndImageMetadata(post *model.Post, needOpenGraphData, needImageDimensions bool) *model.AppError {
|
||||||
|
var appError *model.AppError
|
||||||
|
|
||||||
|
if needOpenGraphData {
|
||||||
|
post.OpenGraphData = []*opengraph.OpenGraph{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if needImageDimensions {
|
||||||
|
post.ImageDimensions = []*model.PostImageDimensions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
firstLink, images := getFirstLinkAndImages(post.Message)
|
||||||
|
|
||||||
|
// Look at the first link to see if it's a web page or an image
|
||||||
|
if firstLink != "" {
|
||||||
|
og, dimensions, err := a.getLinkMetadata(firstLink, true)
|
||||||
|
if err != nil {
|
||||||
|
// Keep going so that one bad link doesn't prevent other image dimensions from being sent to the client
|
||||||
|
appError = model.NewAppError("PreparePostForClient", "app.post.metadata.link.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if needOpenGraphData {
|
||||||
|
post.OpenGraphData = append(post.OpenGraphData, og)
|
||||||
|
}
|
||||||
|
|
||||||
|
if needImageDimensions {
|
||||||
|
post.ImageDimensions = append(post.ImageDimensions, dimensions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if needImageDimensions {
|
||||||
|
// And dimensions for other images
|
||||||
|
for _, image := range images {
|
||||||
|
_, dimensions, err := a.getLinkMetadata(image, true)
|
||||||
|
if err != nil {
|
||||||
|
// Keep going so that one bad link doesn't prevent other image dimensions from being sent to the client
|
||||||
|
appError = model.NewAppError("PreparePostForClient", "app.post.metadata.link.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if dimensions != nil {
|
||||||
|
post.ImageDimensions = append(post.ImageDimensions, dimensions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return appError
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) getCustomEmojisForPost(message string, reactions []*model.Reaction) ([]*model.Emoji, *model.AppError) {
|
func (a *App) getCustomEmojisForPost(message string, reactions []*model.Reaction) ([]*model.Emoji, *model.AppError) {
|
||||||
if !*a.Config().ServiceSettings.EnableCustomEmoji {
|
if !*a.Config().ServiceSettings.EnableCustomEmoji {
|
||||||
// Only custom emoji are returned
|
// Only custom emoji are returned
|
||||||
@@ -109,3 +177,129 @@ func (a *App) getCustomEmojisForPost(message string, reactions []*model.Reaction
|
|||||||
|
|
||||||
return a.GetMultipleEmojiByName(names)
|
return a.GetMultipleEmojiByName(names)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a string, returns the first autolinked URL in the string as well as an array of all Markdown
|
||||||
|
// images of the form . Note that this does not return Markdown links of the
|
||||||
|
// form [text](url).
|
||||||
|
func getFirstLinkAndImages(str string) (string, []string) {
|
||||||
|
firstLink := ""
|
||||||
|
images := []string{}
|
||||||
|
|
||||||
|
markdown.Inspect(str, func(blockOrInline interface{}) bool {
|
||||||
|
switch v := blockOrInline.(type) {
|
||||||
|
case *markdown.Autolink:
|
||||||
|
if firstLink == "" {
|
||||||
|
firstLink = v.Destination()
|
||||||
|
}
|
||||||
|
case *markdown.InlineImage:
|
||||||
|
images = append(images, v.Destination())
|
||||||
|
case *markdown.ReferenceImage:
|
||||||
|
images = append(images, v.ReferenceDefinition.Destination())
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(images) > 1 {
|
||||||
|
images = model.RemoveDuplicateStrings(images)
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstLink, images
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) getLinkMetadata(requestURL string, useCache bool) (*opengraph.OpenGraph, *model.PostImageDimensions, error) {
|
||||||
|
// Check cache
|
||||||
|
if useCache {
|
||||||
|
og, dimensions, ok := getLinkMetadataFromCache(requestURL)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return og, dimensions, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make request for a web page or an image
|
||||||
|
request, err := http.NewRequest("GET", requestURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Add("Accept", "text/html, image/*")
|
||||||
|
|
||||||
|
res, err := a.HTTPClient(false).Do(request) // TODO figure out a way to mock out the client for testing
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer consumeAndClose(res)
|
||||||
|
|
||||||
|
// Parse the data
|
||||||
|
og, dimensions, err := a.parseLinkMetadata(requestURL, res.Body, res.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
// Write back to cache
|
||||||
|
if useCache {
|
||||||
|
cacheLinkMetadata(requestURL, og, dimensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return og, dimensions, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLinkMetadataFromCache(requestURL string) (*opengraph.OpenGraph, *model.PostImageDimensions, bool) {
|
||||||
|
cached, ok := linkCache.Get(requestURL)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := cached.(type) {
|
||||||
|
case *opengraph.OpenGraph:
|
||||||
|
return v, nil, true
|
||||||
|
case *model.PostImageDimensions:
|
||||||
|
return nil, v, true
|
||||||
|
default:
|
||||||
|
return nil, nil, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheLinkMetadata(requestURL string, og *opengraph.OpenGraph, dimensions *model.PostImageDimensions) {
|
||||||
|
var val interface{}
|
||||||
|
if og != nil {
|
||||||
|
val = og
|
||||||
|
} else if dimensions != nil {
|
||||||
|
val = dimensions
|
||||||
|
}
|
||||||
|
|
||||||
|
linkCache.AddWithExpiresInSecs(requestURL, val, LINK_CACHE_DURATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) parseLinkMetadata(requestURL string, body io.Reader, contentType string) (*opengraph.OpenGraph, *model.PostImageDimensions, error) {
|
||||||
|
if strings.HasPrefix(contentType, "image") {
|
||||||
|
dimensions, err := parseImageDimensions(requestURL, body)
|
||||||
|
return nil, dimensions, err
|
||||||
|
} else if strings.HasPrefix(contentType, "text/html") {
|
||||||
|
og := a.ParseOpenGraphMetadata(requestURL, body, contentType)
|
||||||
|
|
||||||
|
// The OpenGraph library and Go HTML library don't error for malformed input, so check that at least
|
||||||
|
// one of these required fields exists before returning the OpenGraph data
|
||||||
|
if og.Title != "" || og.Type != "" || og.URL != "" {
|
||||||
|
return og, nil, nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not an image or web page with OpenGraph information
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseImageDimensions(requestURL string, body io.Reader) (*model.PostImageDimensions, error) {
|
||||||
|
config, _, err := image.DecodeConfig(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dimensions := &model.PostImageDimensions{
|
||||||
|
URL: requestURL,
|
||||||
|
Width: config.Width,
|
||||||
|
Height: config.Height,
|
||||||
|
}
|
||||||
|
|
||||||
|
return dimensions, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,16 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyatlov/go-opengraph/opengraph"
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
|
"github.com/mattermost/mattermost-server/utils/testutils"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -167,8 +172,55 @@ func TestPreparePostForClient(t *testing.T) {
|
|||||||
assert.ElementsMatch(t, []*model.Emoji{emoji1, emoji2, emoji3}, clientPost.Emojis, "should've populated post.Emojis")
|
assert.ElementsMatch(t, []*model.Emoji{emoji1, emoji2, emoji3}, clientPost.Emojis, "should've populated post.Emojis")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("markdown image dimensions", func(t *testing.T) {
|
||||||
|
th := setup()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
post, err := th.App.CreatePost(&model.Post{
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
Message: "This is  and ",
|
||||||
|
}, th.BasicChannel, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
clientPost, err := th.App.PreparePostForClient(post)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, clientPost.ImageDimensions, 2)
|
||||||
|
assert.Equal(t, &model.PostImageDimensions{
|
||||||
|
URL: "https://github.com/hmhealey/test-files/raw/master/logoVertical.png",
|
||||||
|
Width: 1068,
|
||||||
|
Height: 552,
|
||||||
|
}, clientPost.ImageDimensions[0])
|
||||||
|
assert.Equal(t, &model.PostImageDimensions{
|
||||||
|
URL: "https://github.com/hmhealey/test-files/raw/master/icon.png",
|
||||||
|
Width: 501,
|
||||||
|
Height: 501,
|
||||||
|
}, clientPost.ImageDimensions[1])
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("linked image dimensions", func(t *testing.T) {
|
t.Run("linked image dimensions", func(t *testing.T) {
|
||||||
// TODO
|
th := setup()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
post, err := th.App.CreatePost(&model.Post{
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
Message: `This is our logo: https://github.com/hmhealey/test-files/raw/master/logoVertical.png
|
||||||
|
And this is our icon: https://github.com/hmhealey/test-files/raw/master/icon.png`,
|
||||||
|
}, th.BasicChannel, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
clientPost, err := th.App.PreparePostForClient(post)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// Reminder that only the first link gets dimensions
|
||||||
|
assert.Len(t, clientPost.ImageDimensions, 1)
|
||||||
|
assert.Equal(t, &model.PostImageDimensions{
|
||||||
|
URL: "https://github.com/hmhealey/test-files/raw/master/logoVertical.png",
|
||||||
|
Width: 1068,
|
||||||
|
Height: 552,
|
||||||
|
}, clientPost.ImageDimensions[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("proxy linked images", func(t *testing.T) {
|
t.Run("proxy linked images", func(t *testing.T) {
|
||||||
@@ -179,7 +231,32 @@ func TestPreparePostForClient(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("opengraph", func(t *testing.T) {
|
t.Run("opengraph", func(t *testing.T) {
|
||||||
// TODO
|
th := setup()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
post, err := th.App.CreatePost(&model.Post{
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
Message: `This is our web page: https://github.com/hmhealey/test-files`,
|
||||||
|
}, th.BasicChannel, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
clientPost, err := th.App.PreparePostForClient(post)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, clientPost.OpenGraphData, 1)
|
||||||
|
assert.Equal(t, &opengraph.OpenGraph{
|
||||||
|
Description: "Contribute to hmhealey/test-files development by creating an account on GitHub.",
|
||||||
|
SiteName: "GitHub",
|
||||||
|
Title: "hmhealey/test-files",
|
||||||
|
Type: "object",
|
||||||
|
URL: "https://github.com/hmhealey/test-files",
|
||||||
|
Images: []*opengraph.Image{
|
||||||
|
{
|
||||||
|
URL: "https://avatars1.githubusercontent.com/u/3277310?s=400&v=4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, clientPost.OpenGraphData[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("opengraph image dimensions", func(t *testing.T) {
|
t.Run("opengraph image dimensions", func(t *testing.T) {
|
||||||
@@ -187,7 +264,10 @@ func TestPreparePostForClient(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("proxy opengraph images", func(t *testing.T) {
|
t.Run("proxy opengraph images", func(t *testing.T) {
|
||||||
// TODO
|
th := setup()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
testProxyOpenGraphImage(t, th, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +293,10 @@ func TestPreparePostForClientWithImageProxy(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("proxy opengraph images", func(t *testing.T) {
|
t.Run("proxy opengraph images", func(t *testing.T) {
|
||||||
// TODO
|
th := setup()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
testProxyOpenGraphImage(t, th, true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +316,9 @@ func testProxyLinkedImage(t *testing.T, th *TestHelper, shouldProxy bool) {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
clientPost, err := th.App.PreparePostForClient(post)
|
clientPost, err := th.App.PreparePostForClient(post)
|
||||||
require.Nil(t, err)
|
if err != nil && err.Id != "app.post.metadata.link.app_error" {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if shouldProxy {
|
if shouldProxy {
|
||||||
assert.Equal(t, post.Message, fmt.Sprintf(postTemplate, imageURL), "should not have mutated original post")
|
assert.Equal(t, post.Message, fmt.Sprintf(postTemplate, imageURL), "should not have mutated original post")
|
||||||
@@ -243,6 +328,35 @@ func testProxyLinkedImage(t *testing.T, th *TestHelper, shouldProxy bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testProxyOpenGraphImage(t *testing.T, th *TestHelper, shouldProxy bool) {
|
||||||
|
post, err := th.App.CreatePost(&model.Post{
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
Message: `This is our web page: https://github.com/hmhealey/test-files`,
|
||||||
|
}, th.BasicChannel, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
clientPost, err := th.App.PreparePostForClient(post)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
image := &opengraph.Image{}
|
||||||
|
if shouldProxy {
|
||||||
|
image.SecureURL = "https://127.0.0.1/b2ef6ef4890a0107aa80ba33b3011fd51f668303/68747470733a2f2f61766174617273312e67697468756275736572636f6e74656e742e636f6d2f752f333237373331303f733d34303026763d34"
|
||||||
|
} else {
|
||||||
|
image.URL = "https://avatars1.githubusercontent.com/u/3277310?s=400&v=4"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, clientPost.OpenGraphData, 1)
|
||||||
|
assert.Equal(t, &opengraph.OpenGraph{
|
||||||
|
Description: "Contribute to hmhealey/test-files development by creating an account on GitHub.",
|
||||||
|
SiteName: "GitHub",
|
||||||
|
Title: "hmhealey/test-files",
|
||||||
|
Type: "object",
|
||||||
|
URL: "https://github.com/hmhealey/test-files",
|
||||||
|
Images: []*opengraph.Image{image},
|
||||||
|
}, clientPost.OpenGraphData[0])
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetCustomEmojisForPost_Message(t *testing.T) {
|
func TestGetCustomEmojisForPost_Message(t *testing.T) {
|
||||||
th := Setup().InitBasic()
|
th := Setup().InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
@@ -354,3 +468,187 @@ func TestGetCustomEmojisForPost(t *testing.T) {
|
|||||||
assert.Nil(t, err, "failed to get emojis for post")
|
assert.Nil(t, err, "failed to get emojis for post")
|
||||||
assert.ElementsMatch(t, emojis, []*model.Emoji{emoji1, emoji2}, "received incorrect emojis")
|
assert.ElementsMatch(t, emojis, []*model.Emoji{emoji1, emoji2}, "received incorrect emojis")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetFirstLinkAndImages(t *testing.T) {
|
||||||
|
for name, testCase := range map[string]struct {
|
||||||
|
Input string
|
||||||
|
ExpectedFirstLink string
|
||||||
|
ExpectedImages []string
|
||||||
|
}{
|
||||||
|
"no links or images": {
|
||||||
|
Input: "this is a string",
|
||||||
|
ExpectedFirstLink: "",
|
||||||
|
ExpectedImages: []string{},
|
||||||
|
},
|
||||||
|
"http link": {
|
||||||
|
Input: "this is a http://example.com",
|
||||||
|
ExpectedFirstLink: "http://example.com",
|
||||||
|
ExpectedImages: []string{},
|
||||||
|
},
|
||||||
|
"www link": {
|
||||||
|
Input: "this is a www.example.com",
|
||||||
|
ExpectedFirstLink: "http://www.example.com",
|
||||||
|
ExpectedImages: []string{},
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
Input: "this is a ",
|
||||||
|
ExpectedFirstLink: "",
|
||||||
|
ExpectedImages: []string{"http://example.com/logo"},
|
||||||
|
},
|
||||||
|
"multiple images": {
|
||||||
|
Input: "this is a  and  and ",
|
||||||
|
ExpectedFirstLink: "",
|
||||||
|
ExpectedImages: []string{"http://example.com/logo", "http://example.com/logo2", "http://example.com/logo3"},
|
||||||
|
},
|
||||||
|
"multiple images with duplicate": {
|
||||||
|
Input: "this is a  and  and ",
|
||||||
|
ExpectedFirstLink: "",
|
||||||
|
ExpectedImages: []string{"http://example.com/logo", "http://example.com/logo2"},
|
||||||
|
},
|
||||||
|
"reference image": {
|
||||||
|
Input: `this is a ![our logo][logo]
|
||||||
|
|
||||||
|
[logo]: http://example.com/logo`,
|
||||||
|
ExpectedFirstLink: "",
|
||||||
|
ExpectedImages: []string{"http://example.com/logo"},
|
||||||
|
},
|
||||||
|
"image and link": {
|
||||||
|
Input: "this is a https://example.com and ",
|
||||||
|
ExpectedFirstLink: "https://example.com",
|
||||||
|
ExpectedImages: []string{"https://example.com/logo"},
|
||||||
|
},
|
||||||
|
"markdown links (not returned)": {
|
||||||
|
Input: `this is a [our page](http://example.com) and [another page][]
|
||||||
|
|
||||||
|
[another page]: http://www.exaple.com/another_page`,
|
||||||
|
ExpectedFirstLink: "",
|
||||||
|
ExpectedImages: []string{},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
firstLink, images := getFirstLinkAndImages(testCase.Input)
|
||||||
|
|
||||||
|
assert.Equal(t, firstLink, testCase.ExpectedFirstLink)
|
||||||
|
assert.Equal(t, images, testCase.ExpectedImages)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseLinkMetadata(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
imageURL := "http://example.com/test.png"
|
||||||
|
file, err := testutils.ReadTestFile("test.png")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
ogURL := "https://example.com/hello"
|
||||||
|
html := `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta property="og:title" content="Hello, World!">
|
||||||
|
<meta property="og:type" content="object">
|
||||||
|
<meta property="og:url" content="` + ogURL + `">
|
||||||
|
</head>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
makeImageReader := func() io.Reader {
|
||||||
|
return bytes.NewReader(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeOpenGraphReader := func() io.Reader {
|
||||||
|
return strings.NewReader(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("image", func(t *testing.T) {
|
||||||
|
og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeImageReader(), "image/png")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, og)
|
||||||
|
assert.Equal(t, &model.PostImageDimensions{
|
||||||
|
URL: imageURL,
|
||||||
|
Width: 408,
|
||||||
|
Height: 336,
|
||||||
|
}, dimensions)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("malformed image", func(t *testing.T) {
|
||||||
|
og, dimensions, err := th.App.parseLinkMetadata(imageURL, makeOpenGraphReader(), "image/png")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, og)
|
||||||
|
assert.Nil(t, dimensions)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("opengraph", func(t *testing.T) {
|
||||||
|
og, dimensions, err := th.App.parseLinkMetadata(ogURL, makeOpenGraphReader(), "text/html; charset=utf-8")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.NotNil(t, og)
|
||||||
|
assert.Equal(t, og.Title, "Hello, World!")
|
||||||
|
assert.Equal(t, og.Type, "object")
|
||||||
|
assert.Equal(t, og.URL, ogURL)
|
||||||
|
assert.Nil(t, dimensions)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("malformed opengraph", func(t *testing.T) {
|
||||||
|
og, dimensions, err := th.App.parseLinkMetadata(ogURL, makeImageReader(), "text/html; charset=utf-8")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, og)
|
||||||
|
assert.Nil(t, dimensions)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("neither", func(t *testing.T) {
|
||||||
|
og, dimensions, err := th.App.parseLinkMetadata("http://example.com/test.wad", strings.NewReader("garbage"), "application/x-doom")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, og)
|
||||||
|
assert.Nil(t, dimensions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseImageDimensions(t *testing.T) {
|
||||||
|
for name, testCase := range map[string]struct {
|
||||||
|
FileName string
|
||||||
|
URL string
|
||||||
|
ExpectedWidth int
|
||||||
|
ExpectedHeight int
|
||||||
|
ExpectError bool
|
||||||
|
}{
|
||||||
|
"png": {
|
||||||
|
FileName: "test.png",
|
||||||
|
URL: "https://example.com/test.png",
|
||||||
|
ExpectedWidth: 408,
|
||||||
|
ExpectedHeight: 336,
|
||||||
|
},
|
||||||
|
"animated gif": {
|
||||||
|
FileName: "testgif.gif",
|
||||||
|
URL: "http://example.com/test.gif?foo=bar",
|
||||||
|
ExpectedWidth: 118,
|
||||||
|
ExpectedHeight: 118,
|
||||||
|
},
|
||||||
|
"not an image": {
|
||||||
|
FileName: "README.md",
|
||||||
|
URL: "https://example.com/test.png",
|
||||||
|
ExpectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
file, err := testutils.ReadTestFile(testCase.FileName)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
dimensions, err := parseImageDimensions(testCase.URL, bytes.NewReader(file))
|
||||||
|
if testCase.ExpectError {
|
||||||
|
require.NotNil(t, err)
|
||||||
|
} else {
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.NotNil(t, dimensions)
|
||||||
|
require.Equal(t, testCase.URL, dimensions.URL)
|
||||||
|
require.Equal(t, testCase.ExpectedWidth, dimensions.Width)
|
||||||
|
require.Equal(t, testCase.ExpectedHeight, dimensions.Height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
120
app/post_test.go
120
app/post_test.go
@@ -7,12 +7,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dyatlov/go-opengraph/opengraph"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@@ -483,124 +481,6 @@ func TestImageProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkForceHTMLEncodingToUTF8(b *testing.B) {
|
|
||||||
HTML := `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta property="og:url" content="https://example.com/apps/mattermost">
|
|
||||||
<meta property="og:image" content="https://images.example.com/image.png">
|
|
||||||
</head>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
ContentType := "text/html; utf-8"
|
|
||||||
|
|
||||||
b.Run("with converting", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
r := forceHTMLEncodingToUTF8(strings.NewReader(HTML), ContentType)
|
|
||||||
|
|
||||||
og := opengraph.NewOpenGraph()
|
|
||||||
og.ProcessHTML(r)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("without converting", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
og := opengraph.NewOpenGraph()
|
|
||||||
og.ProcessHTML(strings.NewReader(HTML))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeOpenGraphURLsAbsolute(t *testing.T) {
|
|
||||||
for name, tc := range map[string]struct {
|
|
||||||
HTML string
|
|
||||||
RequestURL string
|
|
||||||
URL string
|
|
||||||
ImageURL string
|
|
||||||
}{
|
|
||||||
"absolute URLs": {
|
|
||||||
HTML: `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta property="og:url" content="https://example.com/apps/mattermost">
|
|
||||||
<meta property="og:image" content="https://images.example.com/image.png">
|
|
||||||
</head>
|
|
||||||
</html>`,
|
|
||||||
RequestURL: "https://example.com",
|
|
||||||
URL: "https://example.com/apps/mattermost",
|
|
||||||
ImageURL: "https://images.example.com/image.png",
|
|
||||||
},
|
|
||||||
"URLs starting with /": {
|
|
||||||
HTML: `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta property="og:url" content="/apps/mattermost">
|
|
||||||
<meta property="og:image" content="/image.png">
|
|
||||||
</head>
|
|
||||||
</html>`,
|
|
||||||
RequestURL: "http://example.com",
|
|
||||||
URL: "http://example.com/apps/mattermost",
|
|
||||||
ImageURL: "http://example.com/image.png",
|
|
||||||
},
|
|
||||||
"HTTPS URLs starting with /": {
|
|
||||||
HTML: `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta property="og:url" content="/apps/mattermost">
|
|
||||||
<meta property="og:image" content="/image.png">
|
|
||||||
</head>
|
|
||||||
</html>`,
|
|
||||||
RequestURL: "https://example.com",
|
|
||||||
URL: "https://example.com/apps/mattermost",
|
|
||||||
ImageURL: "https://example.com/image.png",
|
|
||||||
},
|
|
||||||
"missing image URL": {
|
|
||||||
HTML: `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta property="og:url" content="/apps/mattermost">
|
|
||||||
</head>
|
|
||||||
</html>`,
|
|
||||||
RequestURL: "http://example.com",
|
|
||||||
URL: "http://example.com/apps/mattermost",
|
|
||||||
ImageURL: "",
|
|
||||||
},
|
|
||||||
"relative URLs": {
|
|
||||||
HTML: `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta property="og:url" content="index.html">
|
|
||||||
<meta property="og:image" content="../resources/image.png">
|
|
||||||
</head>
|
|
||||||
</html>`,
|
|
||||||
RequestURL: "http://example.com/content/index.html",
|
|
||||||
URL: "http://example.com/content/index.html",
|
|
||||||
ImageURL: "http://example.com/resources/image.png",
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
og := opengraph.NewOpenGraph()
|
|
||||||
if err := og.ProcessHTML(strings.NewReader(tc.HTML)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
makeOpenGraphURLsAbsolute(og, tc.RequestURL)
|
|
||||||
|
|
||||||
if og.URL != tc.URL {
|
|
||||||
t.Fatalf("incorrect url, expected %v, got %v", tc.URL, og.URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(og.Images) > 0 {
|
|
||||||
if og.Images[0].URL != tc.ImageURL {
|
|
||||||
t.Fatalf("incorrect image url, expected %v, got %v", tc.ImageURL, og.Images[0].URL)
|
|
||||||
}
|
|
||||||
} else if tc.ImageURL != "" {
|
|
||||||
t.Fatalf("missing image url, expected %v, got nothing", tc.ImageURL)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMaxPostSize(t *testing.T) {
|
func TestMaxPostSize(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -3146,6 +3146,10 @@
|
|||||||
"id": "app.plugin.upload_disabled.app_error",
|
"id": "app.plugin.upload_disabled.app_error",
|
||||||
"translation": "Plugins and/or plugin uploads have been disabled."
|
"translation": "Plugins and/or plugin uploads have been disabled."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.post.metadata.link.app_error",
|
||||||
|
"translation": "Failed to get metadata for a link in a post."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.role.check_roles_exist.role_not_found",
|
"id": "app.role.check_roles_exist.role_not_found",
|
||||||
"translation": "The provided role does not exist"
|
"translation": "The provided role does not exist"
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ type Post struct {
|
|||||||
|
|
||||||
type PostImageDimensions struct {
|
type PostImageDimensions struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Width int64 `json:"width"`
|
Width int `json:"width"`
|
||||||
Height int64 `json:"height"`
|
Height int `json:"height"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostEphemeral struct {
|
type PostEphemeral struct {
|
||||||
|
|||||||
29
utils/testutils/testutils.go
Обычный файл
29
utils/testutils/testutils.go
Обычный файл
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package testutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReadTestFile(name string) ([]byte, error) {
|
||||||
|
path, _ := utils.FindDir("tests")
|
||||||
|
file, err := os.Open(filepath.Join(path, name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data := &bytes.Buffer{}
|
||||||
|
if _, err := io.Copy(data, file); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return data.Bytes(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
Ссылка в новой задаче
Block a user