finish the oembed conversion

pull/6688/head
Martin Schoeler 9 years ago
parent 1e443da137
commit 1cfdba4d3a
  1. 21
      packages/rocketchat-oembed/client/baseWidget.coffee
  2. 28
      packages/rocketchat-oembed/client/baseWidget.js
  3. 7
      packages/rocketchat-oembed/client/oembedAudioWidget.coffee
  4. 10
      packages/rocketchat-oembed/client/oembedAudioWidget.js
  5. 7
      packages/rocketchat-oembed/client/oembedFrameWidget.coffee
  6. 10
      packages/rocketchat-oembed/client/oembedFrameWidget.js
  7. 17
      packages/rocketchat-oembed/client/oembedImageWidget.coffee
  8. 22
      packages/rocketchat-oembed/client/oembedImageWidget.js
  9. 17
      packages/rocketchat-oembed/client/oembedSandstormGrain.coffee
  10. 25
      packages/rocketchat-oembed/client/oembedSandstormGrain.js
  11. 57
      packages/rocketchat-oembed/client/oembedUrlWidget.coffee
  12. 67
      packages/rocketchat-oembed/client/oembedUrlWidget.js
  13. 22
      packages/rocketchat-oembed/client/oembedVideoWidget.coffee
  14. 35
      packages/rocketchat-oembed/client/oembedVideoWidget.js
  15. 7
      packages/rocketchat-oembed/client/oembedYoutubeWidget.coffee
  16. 10
      packages/rocketchat-oembed/client/oembedYoutubeWidget.js
  17. 21
      packages/rocketchat-oembed/package.js
  18. 30
      packages/rocketchat-oembed/server/models/OEmbedCache.coffee
  19. 38
      packages/rocketchat-oembed/server/models/OEmbedCache.js
  20. 84
      packages/rocketchat-oembed/server/providers.coffee
  21. 120
      packages/rocketchat-oembed/server/providers.js
  22. 256
      packages/rocketchat-oembed/server/server.coffee
  23. 39
      packages/rocketchat-oembed/server/server.js

@ -1,21 +0,0 @@
Template.oembedBaseWidget.helpers
template: ->
if this._overrideTemplate
return this._overrideTemplate
if this.headers?.contentType?.match(/image\/.*/)?
return 'oembedImageWidget'
if this.headers?.contentType?.match(/audio\/.*/)?
return 'oembedAudioWidget'
if this.headers?.contentType?.match(/video\/.*/)? or this.meta?.twitterPlayerStreamContentType?.match(/video\/.*/)?
return 'oembedVideoWidget'
if this.meta?.oembedHtml?
return 'oembedFrameWidget'
if this.meta?.sandstorm?.grain?
return 'oembedSandstormGrain'
return 'oembedUrlWidget'

@ -0,0 +1,28 @@
Template.oembedBaseWidget.helpers({
template() {
let contentType;
if (this.headers) {
contentType = this.headers.contentType;
}
if (this._overrideTemplate) {
return this._overrideTemplate;
}
if (this.headers && contentType && contentType.match(/image\/.*/)) {
return 'oembedImageWidget';
}
if (this.headers && contentType && contentType.match(/audio\/.*/)) {
return 'oembedAudioWidget';
}
if ((this.headers && contentType && contentType.match(/video\/.*/)) || (this.meta && this.meta.twitterPlayerStreamContentType && this.meta.twitterPlayerStreamContentType.match(/video\/.*/))) {
return 'oembedVideoWidget';
}
if (this.meta && this.meta.oembedHtml) {
return 'oembedFrameWidget';
}
if (this.meta && this.meta.sandstorm && this.meta.sandstorm.grain) {
return 'oembedSandstormGrain';
}
return 'oembedUrlWidget';
}
});

@ -1,7 +0,0 @@
Template.oembedAudioWidget.helpers
collapsed: ->
if this.collapsed?
return this.collapsed
else
return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true

@ -0,0 +1,10 @@
Template.oembedAudioWidget.helpers({
collapsed() {
const user = Meteor.user();
if (this.collapsed) {
return this.collapsed;
} else {
return (user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault);
}
}
});

@ -1,7 +0,0 @@
Template.oembedFrameWidget.helpers
collapsed: ->
if this.collapsed?
return this.collapsed
else
return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true

@ -0,0 +1,10 @@
Template.oembedFrameWidget.helpers({
collapsed() {
const user = Meteor.user();
if (this.collapsed) {
return this.collapsed;
} else {
return (user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault) === true;
}
}
});

@ -1,17 +0,0 @@
Template.oembedImageWidget.helpers
loadImage: ->
if Meteor.user()?.settings?.preferences?.autoImageLoad is false and this.downloadImages? is not true
return false
if Meteor.Device.isPhone() and Meteor.user()?.settings?.preferences?.saveMobileBandwidth and this.downloadImages? is not true
return false
return true
collapsed: ->
if this.collapsed?
return this.collapsed
else
return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true

@ -0,0 +1,22 @@
Template.oembedImageWidget.helpers({
loadImage() {
const user = Meteor.user();
if (user && user.settings && user.settings.preferences && user.settings.preferences.autoImageLoad === false && this.downloadImages) {
return false;
}
if (Meteor.Device.isPhone() && user() && user.settings && user.settings.preferences && user.settings.preferences.saveMobileBandwidth && this.downloadImages) {
return false;
}
return true;
},
collapsed() {
const user = Meteor.user();
if (this.collapsed != null) {
return this.collapsed;
} else {
return (user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault) === true;
}
}
});

@ -1,17 +0,0 @@
Template.oembedSandstormGrain.helpers
token: ->
return @meta.sandstorm.grain.token
appTitle: ->
return @meta.sandstorm.grain.appTitle.defaultText
grainTitle: ->
return @meta.sandstorm.grain.grainTitle
appIconUrl: ->
return @meta.sandstorm.grain.appIconUrl
descriptor: ->
return @meta.sandstorm.grain.descriptor
window.sandstormOembed = (e) ->
e = e or window.event
src = e.target or e.srcElement
token = src.getAttribute "data-token"
descriptor = src.getAttribute "data-descriptor"
Meteor.call "sandstormOffer", token, descriptor

@ -0,0 +1,25 @@
Template.oembedSandstormGrain.helpers({
token() {
return this.meta.sandstorm.grain.token;
},
appTitle() {
return this.meta.sandstorm.grain.appTitle.defaultText;
},
grainTitle() {
return this.meta.sandstorm.grain.grainTitle;
},
appIconUrl() {
return this.meta.sandstorm.grain.appIconUrl;
},
descriptor() {
return this.meta.sandstorm.grain.descriptor;
}
});
window.sandstormOembed = function(e) {
e = e || window.event;
const src = e.target || e.srcElement;
const token = src.getAttribute('data-token');
const descriptor = src.getAttribute('data-descriptor');
return Meteor.call('sandstormOffer', token, descriptor);
};

@ -1,57 +0,0 @@
getTitle = (self) ->
if not self.meta?
return
return self.meta.ogTitle or self.meta.twitterTitle or self.meta.title or self.meta.pageTitle
getDescription = (self) ->
if not self.meta?
return
description = self.meta.ogDescription or self.meta.twitterDescription or self.meta.description
if not description?
return
return _.unescape description.replace /(^[\s]*)|([\s]*$)/g, ''
Template.oembedUrlWidget.helpers
description: ->
description = getDescription this
return Blaze._escape(description) if _.isString description
title: ->
title = getTitle this
return Blaze._escape(title) if _.isString title
target: ->
if not this.parsedUrl?.host || !document?.location?.host || this.parsedUrl.host isnt document.location.host
return '_blank'
image: ->
if not this.meta?
return
decodedOgImage = @meta.ogImage?.replace?(/&/g, '&')
url = this.meta.msapplicationTileImage or decodedOgImage or this.meta.twitterImage
if not url?
return
if url.indexOf('//') is 0
url = "#{this.parsedUrl.protocol}#{url}"
else if url.indexOf('/') is 0 and this.parsedUrl?.host?
url = "#{this.parsedUrl.protocol}//#{this.parsedUrl.host}#{url}"
return url
show: ->
return getDescription(this)? or getTitle(this)?
collapsed: ->
if this.collapsed?
return this.collapsed
else
return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true

@ -0,0 +1,67 @@
const getTitle = function(self) {
if (self.meta == null) {
return;
}
return self.meta.ogTitle || self.meta.twitterTitle || self.meta.title || self.meta.pageTitle;
};
const getDescription = function(self) {
if (self.meta == null) {
return;
}
const description = self.meta.ogDescription || self.meta.twitterDescription || self.meta.description;
if (description == null) {
return;
}
return _.unescape(description.replace(/(^[“\s]*)|([”\s]*$)/g, ''));
};
Template.oembedUrlWidget.helpers({
description() {
const description = getDescription(this);
if (_.isString(description)) {
return Blaze._escape(description);
}
},
title() {
const title = getTitle(this);
if (_.isString(title)) {
return Blaze._escape(title);
}
},
target() {
if (!(this.parsedUrl && this.parsedUrl.host) || !(document && document.location && document.location.host) || (this.parsedUrl && this.parsedUrl.host !== document.location.host)) {
return '_blank';
}
},
image() {
if (this.meta == null) {
return;
}
let decodedOgImage;
if (this.meta.ogImage && this.meta.ogImage.replace) {
decodedOgImage = this.meta.ogImage.replace(/&/g, '&');
}
let url = this.meta.msapplicationTileImage || decodedOgImage || this.meta.twitterImage;
if (url == null) {
return;
}
if (url.indexOf('//') === 0) {
url = `${ this.parsedUrl.protocol }${ url }`;
} else if (url.indexOf('/') === 0 && (this.parsedUrl && this.parsedUrl.host)) {
url = `${ this.parsedUrl.protocol }//${ this.parsedUrl.host }${ url }`;
}
return url;
},
show() {
return (getDescription(this) != null) || (getTitle(this) != null);
},
collapsed() {
const user = Meteor.user();
if (this.collapsed != null) {
return this.collapsed;
} else {
return (user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault) === true;
}
}
});

@ -1,22 +0,0 @@
getTitle = (self) ->
if not self.meta?
return
return self.meta.ogTitle or self.meta.twitterTitle or self.meta.title or self.meta.pageTitle
Template.oembedVideoWidget.helpers
url: ->
return @meta?.twitterPlayerStream or @url
contentType: ->
return @meta?.twitterPlayerStreamContentType or @headers?.contentType
title: ->
return getTitle @
collapsed: ->
if this.collapsed?
return this.collapsed
else
return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true

@ -0,0 +1,35 @@
const getTitle = function(self) {
if (self.meta == null) {
return;
}
return self.meta.ogTitle || self.meta.twitterTitle || self.meta.title || self.meta.pageTitle;
};
Template.oembedVideoWidget.helpers({
url() {
if (this.meta && this.meta.twitterPlayerStream) {
return this.meta.twitterPlayerStream;
} else if (this.url) {
return this.url;
}
},
contentType() {
if (this.meta && this.meta.twitterPlayerStreamContentType) {
return this.meta.twitterPlayerStreamContentType;
} else if (this.headers && this.headers.contentType) {
return this.headers.contentType;
}
},
title() {
return getTitle(this);
},
collapsed() {
const user = Meteor.user();
if (this.collapsed) {
return this.collapsed;
} else {
return (user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault) === true;
}
}
});

@ -1,7 +0,0 @@
Template.oembedYoutubeWidget.helpers
collapsed: ->
if this.collapsed?
return this.collapsed
else
return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true

@ -0,0 +1,10 @@
Template.oembedYoutubeWidget.helpers({
collapsed() {
const user = Meteor.user();
if (this.collapsed) {
return this.collapsed;
} else {
return (user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault) === true;
}
}
});

@ -17,40 +17,39 @@ Package.onUse(function(api) {
'http',
'templating',
'ecmascript',
'coffeescript',
'underscore',
'konecty:change-case',
'rocketchat:lib'
]);
api.addFiles('client/baseWidget.html', 'client');
api.addFiles('client/baseWidget.coffee', 'client');
api.addFiles('client/baseWidget.js', 'client');
api.addFiles('client/oembedImageWidget.html', 'client');
api.addFiles('client/oembedImageWidget.coffee', 'client');
api.addFiles('client/oembedImageWidget.js', 'client');
api.addFiles('client/oembedAudioWidget.html', 'client');
api.addFiles('client/oembedAudioWidget.coffee', 'client');
api.addFiles('client/oembedAudioWidget.js', 'client');
api.addFiles('client/oembedVideoWidget.html', 'client');
api.addFiles('client/oembedVideoWidget.coffee', 'client');
api.addFiles('client/oembedVideoWidget.js', 'client');
api.addFiles('client/oembedYoutubeWidget.html', 'client');
api.addFiles('client/oembedYoutubeWidget.coffee', 'client');
api.addFiles('client/oembedYoutubeWidget.js', 'client');
api.addFiles('client/oembedUrlWidget.html', 'client');
api.addFiles('client/oembedUrlWidget.coffee', 'client');
api.addFiles('client/oembedUrlWidget.js', 'client');
api.addFiles('client/oembedFrameWidget.html', 'client');
api.addFiles('client/oembedFrameWidget.coffee', 'client');
api.addFiles('client/oembedFrameWidget.js', 'client');
api.addFiles('client/oembedSandstormGrain.html', 'client');
api.addFiles('client/oembedSandstormGrain.coffee', 'client');
api.addFiles('client/oembedSandstormGrain.js', 'client');
api.addFiles('server/server.js', 'server');
api.addFiles('server/providers.coffee', 'server');
api.addFiles('server/providers.js', 'server');
api.addFiles('server/jumpToMessage.js', 'server');
api.addFiles('server/models/OEmbedCache.coffee', 'server');
api.addFiles('server/models/OEmbedCache.js', 'server');
api.export('OEmbed', 'server');
});

@ -1,30 +0,0 @@
RocketChat.models.OEmbedCache = new class extends RocketChat.models._Base
constructor: ->
super('oembed_cache')
@tryEnsureIndex { 'updatedAt': 1 }
# FIND ONE
findOneById: (_id, options) ->
query =
_id: _id
return @findOne query, options
# INSERT
createWithIdAndData: (_id, data) ->
record =
_id: _id
data: data
updatedAt: new Date
record._id = @insert record
return record
# REMOVE
removeAfterDate: (date) ->
query =
updatedAt:
$lte: date
@remove query

@ -0,0 +1,38 @@
RocketChat.models.OEmbedCache = new class extends RocketChat.models._Base {
constructor() {
super('oembed_cache');
this.tryEnsureIndex({ 'updatedAt': 1 });
}
//FIND ONE
findOneById(_id, options) {
const query = {
_id
};
return this.findOne(query, options);
}
//INSERT
createWithIdAndData(_id, data) {
const record = {
_id,
data,
updatedAt: new Date
};
record._id = this.insert(record);
return record;
}
//REMOVE
removeAfterDate(date) {
const query = {
updatedAt: {
$lte: date
}
};
return this.remove(query);
}
};

@ -1,84 +0,0 @@
URL = Npm.require('url')
QueryString = Npm.require('querystring')
class Providers
providers: []
@getConsumerUrl: (provider, url) ->
urlObj = URL.parse provider.endPoint, true
urlObj.query['url'] = url
delete urlObj.search
return URL.format urlObj
registerProvider: (provider) ->
this.providers.push(provider)
getProviders: () ->
return this.providers
getProviderForUrl: (url) ->
return _.find this.providers, (provider) ->
candidate = _.find provider.urls, (re) ->
return re.test url
return candidate?
providers = new Providers()
providers.registerProvider
urls: [new RegExp('https?://soundcloud.com/\\S+')]
endPoint: 'https://soundcloud.com/oembed?format=json&maxheight=150'
providers.registerProvider
urls: [new RegExp('https?://vimeo.com/[^/]+'), new RegExp('https?://vimeo.com/channels/[^/]+/[^/]+'), new RegExp('https://vimeo.com/groups/[^/]+/videos/[^/]+')]
endPoint: 'https://vimeo.com/api/oembed.json?maxheight=200'
providers.registerProvider
urls: [new RegExp('https?://www.youtube.com/\\S+'), new RegExp('https?://youtu.be/\\S+')]
endPoint: 'https://www.youtube.com/oembed?maxheight=200'
providers.registerProvider
urls: [new RegExp('https?://www.rdio.com/\\S+'), new RegExp('https?://rd.io/\\S+')]
endPoint: 'https://www.rdio.com/api/oembed/?format=json&maxheight=150'
providers.registerProvider
urls: [new RegExp('https?://www.slideshare.net/[^/]+/[^/]+')]
endPoint: 'https://www.slideshare.net/api/oembed/2?format=json&maxheight=200'
providers.registerProvider
urls: [new RegExp('https?://www.dailymotion.com/video/\\S+')]
endPoint: 'https://www.dailymotion.com/services/oembed?maxheight=200'
RocketChat.oembed = {}
RocketChat.oembed.providers = providers
RocketChat.callbacks.add 'oembed:beforeGetUrlContent', (data) ->
if data.parsedUrl?
url = URL.format data.parsedUrl
provider = providers.getProviderForUrl url
if provider?
consumerUrl = Providers.getConsumerUrl provider, url
consumerUrl = URL.parse consumerUrl, true
_.extend data.parsedUrl, consumerUrl
data.urlObj.port = consumerUrl.port
data.urlObj.hostname = consumerUrl.hostname
data.urlObj.pathname = consumerUrl.pathname
data.urlObj.query = consumerUrl.query
delete data.urlObj.search
delete data.urlObj.host
return data
, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-before'
RocketChat.callbacks.add 'oembed:afterParseContent', (data) ->
if data.parsedUrl?.query?
queryString = data.parsedUrl.query
if _.isString data.parsedUrl.query
queryString = QueryString.parse data.parsedUrl.query
if queryString.url?
url = queryString.url
provider = providers.getProviderForUrl url
if provider?
if data.content?.body?
try
metas = JSON.parse data.content.body;
_.each metas, (value, key) ->
if _.isString value
data.meta[changeCase.camelCase('oembed_' + key)] = value
data.meta['oembedUrl'] = url
return data
, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-after'

@ -0,0 +1,120 @@
/*globals changeCase */
const URL = Npm.require('url');
const QueryString = Npm.require('querystring');
class Providers {
constructor() {
this.providers = [];
}
static getConsumerUrl(provider, url) {
const urlObj = URL.parse(provider.endPoint, true);
urlObj.query['url'] = url;
delete urlObj.search;
return URL.format(urlObj);
}
registerProvider(provider) {
return this.providers.push(provider);
}
getProviders() {
return this.providers;
}
getProviderForUrl(url) {
return _.find(this.providers, function(provider) {
const candidate = _.find(provider.urls, function(re) {
return re.test(url);
});
return candidate != null;
});
}
}
const providers = new Providers();
providers.registerProvider({
urls: [new RegExp('https?://soundcloud.com/\\S+')],
endPoint: 'https://soundcloud.com/oembed?format=json&maxheight=150'
});
providers.registerProvider({
urls: [new RegExp('https?://vimeo.com/[^/]+'), new RegExp('https?://vimeo.com/channels/[^/]+/[^/]+'), new RegExp('https://vimeo.com/groups/[^/]+/videos/[^/]+')],
endPoint: 'https://vimeo.com/api/oembed.json?maxheight=200'
});
providers.registerProvider({
urls: [new RegExp('https?://www.youtube.com/\\S+'), new RegExp('https?://youtu.be/\\S+')],
endPoint: 'https://www.youtube.com/oembed?maxheight=200'
});
providers.registerProvider({
urls: [new RegExp('https?://www.rdio.com/\\S+'), new RegExp('https?://rd.io/\\S+')],
endPoint: 'https://www.rdio.com/api/oembed/?format=json&maxheight=150'
});
providers.registerProvider({
urls: [new RegExp('https?://www.slideshare.net/[^/]+/[^/]+')],
endPoint: 'https://www.slideshare.net/api/oembed/2?format=json&maxheight=200'
});
providers.registerProvider({
urls: [new RegExp('https?://www.dailymotion.com/video/\\S+')],
endPoint: 'https://www.dailymotion.com/services/oembed?maxheight=200'
});
RocketChat.oembed = {};
RocketChat.oembed.providers = providers;
RocketChat.callbacks.add('oembed:beforeGetUrlContent', function(data) {
if (data.parsedUrl != null) {
const url = URL.format(data.parsedUrl);
const provider = providers.getProviderForUrl(url);
if (provider != null) {
let consumerUrl = Providers.getConsumerUrl(provider, url);
consumerUrl = URL.parse(consumerUrl, true);
_.extend(data.parsedUrl, consumerUrl);
data.urlObj.port = consumerUrl.port;
data.urlObj.hostname = consumerUrl.hostname;
data.urlObj.pathname = consumerUrl.pathname;
data.urlObj.query = consumerUrl.query;
delete data.urlObj.search;
delete data.urlObj.host;
}
}
return data;
}, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-before');
RocketChat.callbacks.add('oembed:afterParseContent', function(data) {
if (data.parsedUrl && data.parsedUrl.query) {
let queryString = data.parsedUrl.query;
if (_.isString(data.parsedUrl.query)) {
queryString = QueryString.parse(data.parsedUrl.query);
}
if (queryString.url != null) {
const url = queryString.url;
const provider = providers.getProviderForUrl(url);
if (provider != null) {
if (data.content && data.content.body) {
try {
const metas = JSON.parse(data.content.body);
_.each(metas, function(value, key) {
if (_.isString(value)) {
return data.meta[changeCase.camelCase(`oembed_${ key }`)] = value;
}
});
data.meta['oembedUrl'] = url;
} catch (error) {
console.log(error);
}
}
}
}
}
return data;
}, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-after');

@ -1,256 +0,0 @@
URL = Npm.require('url')
querystring = Npm.require('querystring')
request = HTTPInternals.NpmModules.request.module
iconv = Npm.require('iconv-lite')
ipRangeCheck = Npm.require('ip-range-check')
he = Npm.require('he')
jschardet = Npm.require('jschardet')
OEmbed = {}
# Detect encoding
# Priority:
# Detected == HTTP Header > Detected == HTML meta > HTTP Header > HTML meta > Detected > Default (utf-8)
# See also: https://www.w3.org/International/questions/qa-html-encoding-declarations.en#quickanswer
getCharset = (contentType, body) ->
contentType = contentType || ''
binary = body.toString('binary')
detected = jschardet.detect(binary)
if detected.confidence > 0.8
detectedCharset = detected.encoding.toLowerCase()
m1 = contentType.match(/charset=([\w\-]+)/i)
if m1
httpHeaderCharset = m1[1].toLowerCase()
m2 = binary.match(/<meta\b[^>]*charset=["']?([\w\-]+)/i)
if m2
htmlMetaCharset = m2[1].toLowerCase()
if detectedCharset
if detectedCharset == httpHeaderCharset
result = httpHeaderCharset
else if detectedCharset == htmlMetaCharset
result = htmlMetaCharset
unless result
result = httpHeaderCharset || htmlMetaCharset || detectedCharset
return result || 'utf-8'
toUtf8 = (contentType, body) ->
return iconv.decode(body, getCharset(contentType, body))
getUrlContent = (urlObj, redirectCount = 5, callback) ->
if _.isString(urlObj)
urlObj = URL.parse urlObj
parsedUrl = _.pick urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']
ignoredHosts = RocketChat.settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') or []
if parsedUrl.hostname in ignoredHosts or ipRangeCheck(parsedUrl.hostname, ignoredHosts)
return callback()
safePorts = RocketChat.settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') or []
if parsedUrl.port and safePorts.length > 0 and parsedUrl.port not in safePorts
return callback()
data = RocketChat.callbacks.run 'oembed:beforeGetUrlContent',
urlObj: urlObj
parsedUrl: parsedUrl
if data.attachments?
return callback null, data
url = URL.format data.urlObj
opts =
url: url
strictSSL: !RocketChat.settings.get 'Allow_Invalid_SelfSigned_Certs'
gzip: true
maxRedirects: redirectCount
headers:
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36'
headers = null
statusCode = null
error = null
chunks = []
chunksTotalLength = 0
stream = request opts
stream.on 'response', (response) ->
statusCode = response.statusCode
headers = response.headers
if response.statusCode isnt 200
return stream.abort()
stream.on 'data', (chunk) ->
chunks.push chunk
chunksTotalLength += chunk.length
if chunksTotalLength > 250000
stream.abort()
stream.on 'end', Meteor.bindEnvironment ->
if error?
return callback null, {
error: error
parsedUrl: parsedUrl
}
buffer = Buffer.concat(chunks)
callback null, {
headers: headers
body: toUtf8(headers['content-type'], buffer)
parsedUrl: parsedUrl
statusCode: statusCode
}
stream.on 'error', (err) ->
error = err
OEmbed.getUrlMeta = (url, withFragment) ->
getUrlContentSync = Meteor.wrapAsync getUrlContent
urlObj = URL.parse url
if withFragment?
queryStringObj = querystring.parse urlObj.query
queryStringObj._escaped_fragment_ = ''
urlObj.query = querystring.stringify queryStringObj
path = urlObj.pathname
if urlObj.query?
path += '?' + urlObj.query
urlObj.path = path
content = getUrlContentSync urlObj, 5
if !content
return
if content.attachments?
return content
metas = undefined
if content?.body?
metas = {}
content.body.replace /<title[^>]*>([^<]*)<\/title>/gmi, (meta, title) ->
metas.pageTitle ?= he.unescape title
content.body.replace /<meta[^>]*(?:name|property)=[']([^']*)['][^>]*\scontent=[']([^']*)['][^>]*>/gmi, (meta, name, value) ->
metas[changeCase.camelCase(name)] ?= he.unescape value
content.body.replace /<meta[^>]*(?:name|property)=["]([^"]*)["][^>]*\scontent=["]([^"]*)["][^>]*>/gmi, (meta, name, value) ->
metas[changeCase.camelCase(name)] ?= he.unescape value
content.body.replace /<meta[^>]*\scontent=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gmi, (meta, value, name) ->
metas[changeCase.camelCase(name)] ?= he.unescape value
content.body.replace /<meta[^>]*\scontent=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gmi, (meta, value, name) ->
metas[changeCase.camelCase(name)] ?= he.unescape value
if metas.fragment is '!' and not withFragment?
return OEmbed.getUrlMeta url, true
headers = undefined
if content?.headers?
headers = {}
for header, value of content.headers
headers[changeCase.camelCase(header)] = value
if content?.statusCode isnt 200
return data
data = RocketChat.callbacks.run 'oembed:afterParseContent',
meta: metas
headers: headers
parsedUrl: content.parsedUrl
content: content
return data
OEmbed.getUrlMetaWithCache = (url, withFragment) ->
cache = RocketChat.models.OEmbedCache.findOneById url
if cache?
return cache.data
data = OEmbed.getUrlMeta url, withFragment
if data?
try
RocketChat.models.OEmbedCache.createWithIdAndData url, data
catch e
console.error 'OEmbed duplicated record', url
return data
return
getRelevantHeaders = (headersObj) ->
headers = {}
for key, value of headersObj
if key.toLowerCase() in ['contenttype', 'contentlength'] and value?.trim() isnt ''
headers[key] = value
if Object.keys(headers).length > 0
return headers
return
getRelevantMetaTags = (metaObj) ->
tags = {}
for key, value of metaObj
if /^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) and value?.trim() isnt ''
tags[key] = value
if Object.keys(tags).length > 0
return tags
return
OEmbed.rocketUrlParser = (message) ->
if Array.isArray message.urls
attachments = []
changed = false
message.urls.forEach (item) ->
if item.ignoreParse is true then return
if item.url.startsWith "grain://"
changed = true
item.meta =
sandstorm:
grain: item.sandstormViewInfo
return
if not /^https?:\/\//i.test item.url then return
data = OEmbed.getUrlMetaWithCache item.url
if data?
if data.attachments
attachments = _.union attachments, data.attachments
else
if data.meta?
item.meta = getRelevantMetaTags data.meta
if data.headers?
item.headers = getRelevantHeaders data.headers
item.parsedUrl = data.parsedUrl
changed = true
if attachments.length
RocketChat.models.Messages.setMessageAttachments message._id, attachments
if changed is true
RocketChat.models.Messages.setUrlsById message._id, message.urls
return message
RocketChat.settings.get 'API_Embed', (key, value) ->
if value
RocketChat.callbacks.add 'afterSaveMessage', OEmbed.rocketUrlParser, RocketChat.callbacks.priority.LOW, 'API_Embed'
else
RocketChat.callbacks.remove 'afterSaveMessage', 'API_Embed'

@ -1,6 +1,4 @@
/*globals HTTPInternals, changeCase */
const indexOf = [].indexOf || function(item) { for (let i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) { return i; } } return -1; };
const URL = Npm.require('url');
const querystring = Npm.require('querystring');
@ -17,6 +15,10 @@ const jschardet = Npm.require('jschardet');
const OEmbed = {};
// Detect encoding
// Priority:
// Detected == HTTP Header > Detected == HTML meta > HTTP Header > HTML meta > Detected > Default (utf-8)
// See also: https://www.w3.org/International/questions/qa-html-encoding-declarations.en#quickanswer
const getCharset = function(contentType, body) {
let detectedCharset;
let httpHeaderCharset;
@ -56,8 +58,6 @@ const toUtf8 = function(contentType, body) {
};
const getUrlContent = function(urlObj, redirectCount, callback) {
let ref;
let ref1;
if (redirectCount == null) {
redirectCount = 5;
}
@ -67,13 +67,15 @@ const getUrlContent = function(urlObj, redirectCount, callback) {
const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']);
const ignoredHosts = RocketChat.settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || [];
if ((ref = parsedUrl.hostname, indexOf.call(ignoredHosts, ref) >= 0) || ipRangeCheck(parsedUrl.hostname, ignoredHosts)) {
if (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts)) {
return callback();
}
const safePorts = RocketChat.settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || [];
if (parsedUrl.port && safePorts.length > 0 && (ref1 = parsedUrl.port, indexOf.call(safePorts, ref1) < 0)) {
if (parsedUrl.port && safePorts.length > 0 && (!safePorts.includes(parsedUrl.port))) {
return callback();
}
const data = RocketChat.callbacks.run('oembed:beforeGetUrlContent', {
urlObj,
parsedUrl
@ -182,11 +184,10 @@ OEmbed.getUrlMeta = function(url, withFragment) {
if (content && content.headers) {
headers = {};
const ref = content.headers;
for (const header in ref) {
const value = ref[header];
headers[changeCase.camelCase(header)] = value;
}
const headerObj = content.headers;
Object.keys(headerObj).forEach((header) => {
headers[changeCase.camelCase(header)] = headerObj[header];
});
}
if (content && content.statusCode !== 200) {
return;
@ -218,13 +219,14 @@ OEmbed.getUrlMetaWithCache = function(url, withFragment) {
const getRelevantHeaders = function(headersObj) {
const headers = {};
for (const key in headersObj) {
Object.keys(headersObj).forEach((key) => {
const value = headersObj[key];
let ref;
if (((ref = key.toLowerCase()) === 'contenttype' || ref === 'contentlength') && (value != null ? value.trim() : void 0) !== '') {
const lowerCaseKey = key.toLowerCase();
if ((lowerCaseKey === 'contenttype' || lowerCaseKey === 'contentlength') && (value && value.trim() !== '')) {
headers[key] = value;
}
}
});
if (Object.keys(headers).length > 0) {
return headers;
}
@ -232,12 +234,13 @@ const getRelevantHeaders = function(headersObj) {
const getRelevantMetaTags = function(metaObj) {
const tags = {};
for (const key in metaObj) {
Object.keys(metaObj).forEach((key) => {
const value = metaObj[key];
if (/^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) && (value != null ? value.trim() : void 0) !== '') {
if (/^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) && (value && value.trim() !== '')) {
tags[key] = value;
}
}
});
if (Object.keys(tags).length > 0) {
return tags;
}

Loading…
Cancel
Save