Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "nodebb"
}
3 changes: 3 additions & 0 deletions client/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "nodebb/public"
}
38 changes: 38 additions & 0 deletions client/admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';

define('admin/plugins/iframely', ['settings'], function (Settings) {
var ACP = {};

ACP.init = function () {
Settings.load('iframely', $('.iframely-settings'), function () {
function tagifyInput(selector) {
var input = $(selector).tagsinput({
confirmKeys: [13, 44],
trimValue: true,
});
if (input[0]) {
$(input[0].$input).addClass('form-control').parent().css('display', 'block');
}
}

tagifyInput('#blacklist');
});
$('#save').on('click', saveSettings);
};

function saveSettings() {
Settings.save('iframely', $('.iframely-settings'), function () {
app.alert({
type: 'success',
alert_id: 'iframely-saved',
title: 'Settings Saved',
message: 'Please reload your NodeBB to apply these settings',
clickfn: function () {
socket.emit('admin.reload');
},
});
});
}

return ACP;
});
36 changes: 36 additions & 0 deletions client/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

/* global iframely */

$(document).ready(function () {
$(window).on('action:ajaxify.end action:posts.loaded action:composer.preview', function () {
/**
* Iframely requires to call `iframely.load();` after widgets loaded to page.
* `action:ajaxify.end` - triggered when posts rendered on page.
* `action:posts.loaded` - triggered when you are on a topic page and a new post is sent to the client.
* `action:composer.preview` - triggered when new preview rendered in composer.
* In both cases Iframely need to initialize widgets.
*/
iframely.load();
});

$(window).on('action:composer.preview', function () {
/**
* This logic prevents widget flickering while editing post in composer.
* When user opens post editor, widget will be collapsed and `click to prevew` button will be shown.
* Click event on that button will show widget expanded.
* Click button rendered in template:
* `static/templates/partials/iframely-widget-wrapper.tpl`
*/
$('.iframely-container a[data-iframely-show-preview]').one('click', function (e) {
e.stopPropagation();
var $parent = $(this).parent();
var html = $parent.attr('data-html');
$parent.html(html);
return false;
});
});
});



File renamed without changes.
49 changes: 49 additions & 0 deletions lib/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

const undici = require('undici');

const { API_BASE } = require('./constants');
const logger = require('./logger');


const IframelyAPI = {};

IframelyAPI.query = async function (data, endpoint = '') {
if (!endpoint) {
logger.error('No API key or endpoint configured, skipping Iframely');
return null;
}

const custom_endpoint = /^https?:\/\//i.test(endpoint);
let iframelyAPI = custom_endpoint ? endpoint : `${API_BASE}&api_key=${endpoint}`;
iframelyAPI += `${iframelyAPI.indexOf('?') > -1 ? '&' : '?'}url=${encodeURIComponent(data.url)}`;
if (custom_endpoint) {
iframelyAPI += '&group=true';
}

try {
const response = await undici.request(iframelyAPI);
if (response.statusCode === 404) {
logger.verbose(`Not found: ${data.url}`);
return null;
}

const json = await response.body.json();
if (response.statusCode !== 200 || !json) {
logger.verbose(`Iframely responded with error: ${JSON.stringify(json)}. Url: ${data.url}. Api call: ${iframelyAPI}`);
return null;
}

if (!json.meta || !json.links) {
logger.error(`Invalid Iframely API response. Url: ${data.url}. Api call: ${iframelyAPI}. Body: ${JSON.stringify(json)}`);
return null;
}

return json;
} catch (err) {
logger.error(`Encountered error querying Iframely API: ${err.message}. Url: ${data.url}. Api call: ${iframelyAPI}`);
return null;
}
};

module.exports = IframelyAPI;
42 changes: 42 additions & 0 deletions lib/camo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const crypto = require('crypto');


/**
* @param {string} html
* @param {{ key: string, host: string }} { key: camoProxyKey, host: camoProxyHost }
* @returns {string}
*/
function wrapHtmlImages(html, { key, host }) {
if (html && key && host) {
return html.replace(/<img[^>]+src=["'][^'"]+["']/gi, (item) => {
const m = item.match(/(<img[^>]+src=["'])([^'"]+)(["'])/i);
const url = wrapImage(m[2]);
return m[1] + url + m[3];
});
}
return html;
}

/**
* @param {string} url
* @param {{ key: string, host: string }} { key: camoProxyKey, host: camoProxyHost }
* @returns {string}
*/
function wrapImage(url, { key, host }) {
if (url && key && host && url.indexOf(host) === -1) {
const hexDigest = crypto.createHmac('sha1', key).update(url).digest('hex');
const hexEncodedPath = Buffer.from(url).toString('hex');

return [
host.replace(/\/$/, ''), // Remove tail '/'
hexDigest,
hexEncodedPath,
].join('/');
}

return url;
}

module.exports = { wrapHtmlImages, wrapImage };
17 changes: 17 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

const url = require('url');
const { nconf } = require('./nodebb');


module.exports = {
HTML_REGEX: /(?:<p[^>]*>|<br\s*\/?>|^)<a.+?href="(.+?)".*?>(.*?)<\/a>(?:<br\s*\/?>|<\/p>)?/gm,

ONE_DAY_MS: 1000 * 60 * 60 * 24,
DEFAULT_CACHE_MAX_AGE_DAYS: 1,

FORUM_URL: url.parse(nconf.get('url')),
UPLOADS_URL: url.parse(url.resolve(nconf.get('url'), nconf.get('upload_url'))),

API_BASE: 'https://iframe.ly/api/iframely?origin=nodebb&align=left',
};
17 changes: 3 additions & 14 deletions lib/controllers.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
'use strict';

var Controllers = {};

Controllers.renderAdminPage = function (req, res, next) {
/*
Make sure the route matches your path to template exactly.

If your route was:
myforum.com/some/complex/route/
your template should be:
templates/some/complex/route.tpl
and you would render it like so:
res.render('some/complex/route');
*/
const Controllers = {};

Controllers.renderAdminPage = function (req, res) {
res.render('admin/plugins/iframely', {});
};

module.exports = Controllers;
module.exports = Controllers;
Loading