oEmbed for Facebook, Instagram and Twitter (#5510)

* oEmbed for FB, Inst and Twitter

- Support of oEmbed for Twitter, Facebook and Instagram
- Redesigned of oEmbed settings pages
- Added dynamic endpoint parameters inputs

* Migration and tests

- Migration: keep oEmbed providers already defined by users
- Adapted unit tests

* Access token required

- If an Access Token param is used in the Endpoint URL and is empty, we indicate that the configuration is incomplete
- Adapted Unit Tests
This commit is contained in:
s-tyshchenko 2022-01-31 18:32:00 +02:00 committed by GitHub
parent 9b7ec58bbf
commit d1a387137c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 5822 additions and 43 deletions

View File

@ -8,3 +8,4 @@
- Enh #5490: Display confirmation message before display embedded content
- Enh #5258: Display who invited the user on the Approval page
- Enh #4890: Allow to define actions in a controller which should not be intercepted by other actions
- Enh #5510: oEmbed support for other social networks, redesign of oEmbed settings pages

View File

@ -40,9 +40,9 @@ class OembedFetchEvent extends Event
private function getProviderUrl()
{
foreach ($this->providers as $url => $endpoint) {
if (strpos($this->url, $url) !== false) {
return str_replace("%url%", urlencode($this->url), $endpoint);
foreach ($this->providers as $providerName => $provider) {
if (preg_match($provider['pattern'], $this->url)) {
return str_replace("%url%", urlencode($this->url), $provider['endpoint']);
}
}
return '';

View File

@ -0,0 +1,97 @@
<?php
use yii\db\Migration;
use yii\helpers\Json;
/**
* Class m220121_193617_oembed_setting_update
*/
class m220121_193617_oembed_setting_update extends Migration
{
/**
* {@inheritdoc}
*/
public function up()
{
$oembedProviders = [
'Facebook Video' => [
'pattern' => '/facebook\.com\/(.*)(video)/',
'endpoint' => 'https://graph.facebook.com/v12.0/oembed_video?url=%url%&access_token='
],
'Facebook Post' => [
'pattern' => '/facebook\.com\/(.*)(post|activity|photo|permalink|media|question|note)/',
'endpoint' => 'https://graph.facebook.com/v12.0/oembed_post?url=%url%&access_token='
],
'Facebook Page' => [
'pattern' => '/^(https\:\/\/)*(www\.)*facebook\.com\/((?!video|post|activity|photo|permalink|media|question|note).)*$/',
'endpoint' => 'https://graph.facebook.com/v12.0/oembed_post?url=%url%&access_token='
],
'Instagram' => [
'pattern' => '/instagram\.com/',
'endpoint' => 'https://graph.facebook.com/v12.0/instagram_oembed?url=%url%&access_token='
],
'Twitter' => [
'pattern' => '/twitter\.com/',
'endpoint' => 'https://publish.twitter.com/oembed?url=%url%&maxwidth=450'
],
'YouTube' => [
'pattern' => '/youtube\.com|youtu.be/',
'endpoint' => 'https://www.youtube.com/oembed?scheme=https&url=%url%&format=json&maxwidth=450'
],
'Soundcloud' => [
'pattern' => '/soundcloud\.com/',
'endpoint' => 'https://soundcloud.com/oembed?url=%url%&format=json&maxwidth=450'
],
'Vimeo' => [
'pattern' => '/vimeo\.com/',
'endpoint' => 'https://vimeo.com/api/oembed.json?scheme=https&url=%url%&format=json&maxwidth=450'
],
'SlideShare' => [
'pattern' => '/slideshare\.net/',
'endpoint' => 'https://www.slideshare.net/api/oembed/2?url=%url%&format=json&maxwidth=450'
]
];
foreach (\humhub\models\UrlOembed::getProviders() as $providerUrl => $providerEndpoint)
{
$providerExists = false;
foreach ($oembedProviders as $provider) {
if(preg_match($provider['pattern'], $providerUrl)) {
$providerExists = true;
}
}
if(!$providerExists) {
$oembedProviders[$providerUrl] = [
'pattern' => '/' . str_replace('.', '\.', $providerUrl) . '/',
'endpoint' => $providerEndpoint
];
}
}
$this->update('setting', ['value' => Json::encode($oembedProviders)], ['name' => 'oembedProviders', 'module_id' => 'base']);
Yii::$app->settings->set('oembedProviders', Json::encode($oembedProviders));
}
/**
* {@inheritdoc}
*/
public function down()
{
$oembedProvidersJson = Json::encode([
'twitter.com' => 'https://publish.twitter.com/oembed?url=%url%&maxwidth=450',
'instagram.com' => 'https://graph.facebook.com/v12.0/instagram_oembed?url=%url%&access_token=',
'vimeo.com' => 'https://vimeo.com/api/oembed.json?scheme=https&url=%url%&format=json&maxwidth=450',
'youtube.com' => 'https://www.youtube.com/oembed?scheme=https&url=%url%&format=json&maxwidth=450',
'youtu.be' => 'https://www.youtube.com/oembed?scheme=https&url=%url%&format=json&maxwidth=450',
'soundcloud.com' => 'https://soundcloud.com/oembed?url=%url%&format=json&maxwidth=450',
'slideshare.net' => 'https://www.slideshare.net/api/oembed/2?url=%url%&format=json&maxwidth=450',
]);
$this->update('setting', ['value' => $oembedProvidersJson], ['name' => 'oembedProviders', 'module_id' => 'base']);
Yii::$app->settings->set('oembedProviders', $oembedProvidersJson);
}
}

View File

@ -117,9 +117,9 @@ class UrlOembed extends ActiveRecord
*/
public function getProviderUrl()
{
foreach (static::getProviders() as $providerBaseUrl => $providerAPI) {
if (strpos($this->url, $providerBaseUrl) !== false) {
return str_replace("%url%", urlencode($this->url), $providerAPI);
foreach (static::getProviders() as $provider) {
if (preg_match($provider['pattern'], $this->url)) {
return str_replace("%url%", urlencode($this->url), $provider['endpoint']);
}
}
return null;
@ -360,9 +360,9 @@ class UrlOembed extends ActiveRecord
*/
public static function getProviderByUrl($url)
{
foreach (static::getProviders() as $providerBaseUrl => $providerAPI) {
if (strpos($url, $providerBaseUrl) !== false) {
return $providerBaseUrl;
foreach (static::getProviders() as $provider) {
if (preg_match($provider['pattern'], $url)) {
return $provider['endpoint'];
}
}

View File

@ -28,6 +28,7 @@ use humhub\models\UrlOembed;
use humhub\modules\admin\components\Controller;
use humhub\modules\admin\models\Log;
use humhub\modules\notification\models\forms\NotificationSettings;
use yii\base\BaseObject;
/**
* SettingController
@ -358,19 +359,23 @@ class SettingController extends Controller
{
$form = new OEmbedProviderForm;
$prefix = Yii::$app->request->get('prefix');
$name = Yii::$app->request->get('name');
$providers = UrlOembed::getProviders();
if (isset($providers[$prefix])) {
$form->prefix = $prefix;
$form->endpoint = $providers[$prefix];
if (isset($providers[$name])) {
$form->name = $name;
$form->endpoint = $providers[$name]['endpoint'];
$form->pattern = $providers[$name]['pattern'];
}
if ($form->load(Yii::$app->request->post()) && $form->validate()) {
if ($prefix && isset($providers[$prefix])) {
unset($providers[$prefix]);
if ($name && isset($providers[$name])) {
unset($providers[$name]);
}
$providers[$form->prefix] = $form->endpoint;
$providers[$form->name] = [
'endpoint' => $form->endpoint,
'pattern' => $form->pattern
];
UrlOembed::setProviders($providers);
return $this->redirect(
@ -382,7 +387,7 @@ class SettingController extends Controller
return $this->render('oembed_edit',
[
'model' => $form,
'prefix' => $prefix
'name' => $name
]);
}
@ -392,11 +397,11 @@ class SettingController extends Controller
public function actionOembedDelete()
{
$this->forcePostRequest();
$prefix = Yii::$app->request->get('prefix');
$name = Yii::$app->request->get('name');
$providers = UrlOembed::getProviders();
if (isset($providers[$prefix])) {
unset($providers[$prefix]);
if (isset($providers[$name])) {
unset($providers[$name]);
UrlOembed::setProviders($providers);
}
return $this->redirect([

View File

@ -11,8 +11,10 @@ use Yii;
class OEmbedProviderForm extends \yii\base\Model
{
public $prefix;
public $name;
public $endpoint;
public $pattern;
public $access_token;
/**
* Declares the validation rules.
@ -20,9 +22,13 @@ class OEmbedProviderForm extends \yii\base\Model
public function rules()
{
return [
['prefix', 'safe'],
[['prefix', 'endpoint'], 'required'],
[['name', 'pattern', 'endpoint'], 'string'],
[['name', 'pattern', 'endpoint'], 'required'],
['endpoint', 'url'],
['access_token', 'required', 'when' => function($model) {
parse_str($model->endpoint, $query);
return isset($query['access_token']);
}]
];
}
@ -34,8 +40,10 @@ class OEmbedProviderForm extends \yii\base\Model
public function attributeLabels()
{
return [
'prefix' => Yii::t('AdminModule.settings', 'Url Prefix'),
'name' => Yii::t('AdminModule.settings', 'Provider Name'),
'endpoint' => Yii::t('AdminModule.settings', 'Endpoint Url'),
'pattern' => Yii::t('AdminModule.settings', 'Url Pattern'),
'access_token' => Yii::t('AdminModule.settings', 'Access Token'),
];
}

View File

@ -5,9 +5,15 @@ use humhub\modules\ui\form\widgets\ActiveForm;
use humhub\widgets\Button;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\web\View;
/* @var array $providers */
/* @var OEmbedSettingsForm $settings */
$this->registerJs(<<<JS
$('[data-toggle="tooltip"]').tooltip();
JS, View::POS_READY);
?>
<?php $this->beginContent('@admin/views/setting/_advancedLayout.php') ?>
@ -19,20 +25,42 @@ use yii\helpers\Url;
<h4><?= Yii::t('AdminModule.settings', 'Enabled OEmbed providers'); ?></h4>
<?php if (count($providers) != 0): ?>
<ul>
<?php foreach ($providers as $providerUrl => $providerOEmbedAPI) : ?>
<li><?= Html::a($providerUrl, Url::to(['oembed-edit', 'prefix' => $providerUrl]), ['data-method' => 'POST']); ?></li>
<div id="oembed-providers">
<?php foreach ($providers as $providerName => $provider) : ?>
<div class="oembed-provider-container col-xs-6 col-md-3">
<div class="oembed-provider">
<div class="oembed-provider-name">
<span>
<?= Html::encode($providerName) ?>
</span>
<?php parse_str($provider['endpoint'], $query); ?>
<?php if (isset($query['access_token']) && empty($query['access_token'])): ?>
<span class="label label-danger label-error"
data-toggle="tooltip" data-placement="right"
title="<?= Yii::t('AdminModule.settings', 'Access token is not provided yet.') ?>">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i>
</span>
<?php endif; ?>
</div>
<?= Html::a(Yii::t('base', 'Edit'), Url::to(['oembed-edit', 'name' => $providerName]), ['data-method' => 'POST', 'class' => 'btn btn-xs btn-link']); ?>
</div>
</div>
<?php endforeach; ?>
</ul>
</div>
<?php else: ?>
<p><strong><?= Yii::t('AdminModule.settings', 'Currently no provider active!'); ?></strong></p>
<?php endif; ?>
<hr>
<?php $form = ActiveForm::begin() ?>
<?= $form->field($settings, 'requestConfirmation')->checkbox() ?>
<?= $form->field($settings, 'requestConfirmation')->checkbox() ?>
<?= Button::primary(Yii::t('AdminModule.settings', 'Save'))->submit() ?>
<?= Button::primary(Yii::t('AdminModule.settings', 'Save'))->submit() ?>
<?php ActiveForm::end(); ?>

View File

@ -1,11 +1,72 @@
<?php
use humhub\modules\admin\models\forms\OEmbedProviderForm;
use humhub\modules\ui\form\widgets\ActiveForm;
use humhub\widgets\Button;
use yii\helpers\Html;
use yii\helpers\Url;
use humhub\modules\ui\form\widgets\ActiveForm;
use yii\web\View;
/* @var $name string */
/** @var OEmbedProviderForm $model */
parse_str($model->endpoint, $query);
$this->registerJs(<<<JS
function initEndpointInputs() {
var url = new URL($('#oembedproviderform-endpoint').val());
var formGroup;
$('#endpoint-parameters').html('');
for (var key of url.searchParams.keys()) {
if (key !== 'url' && key !== 'access_token') {
var value = url.searchParams.get(key);
var label = key[0].toUpperCase() + key.substring(1)
.replace(/_([a-z])/, function (m, w) {
return w.toUpperCase();
}).replace(/-([a-z])/, function (m, w) {
return w.toUpperCase();
}).replace(/([A-Z])/, " $1");
var inputId = 'oembedproviderform-' + key;
formGroup = '<div class="form-group col-xs-12 col-sm-6">' +
'<label for="' + inputId + '" class="control-label" type="text">' + label + '</label>' +
'<input id="' + inputId + '" value="' + (!value.match(/\%\w+\%/) ? value : "") + '" type="text" class="form-control endpoint-param" data-param-name="' + key + '">' +
'</div>';
$('#endpoint-parameters').append(formGroup);
}
}
$('input[data-param-name]').on('input change', composeEndpoint);
}
function composeEndpoint() {
var endpointInput = $('#oembedproviderform-endpoint');
var url = new URL(endpointInput.val());
$('.endpoint-param').each(function (index) {
url.searchParams.set($(this).attr('data-param-name'), $(this).val());
});
endpointInput.val(url.toString());
}
$('#oembedproviderform-endpoint').on('input change', function () {
initEndpointInputs();
});
$('#oembed-provider-form').on('submit', function () {
composeEndpoint();
});
JS, View::POS_END);
$this->registerJs(<<<JS
initEndpointInputs();
JS, View::POS_LOAD);
/* @var $prefix string */
?>
<?php $this->beginContent('@admin/views/setting/_advancedLayout.php') ?>
@ -14,7 +75,7 @@ use humhub\modules\ui\form\widgets\ActiveForm;
<?= Button::back(Url::to(['setting/oembed']), Yii::t('AdminModule.settings', 'Back to overview')) ?>
<h4 class="pull-left">
<?php
if ($prefix == "") {
if (empty($name)) {
echo Yii::t('AdminModule.settings', 'Add OEmbed provider');
} else {
echo Yii::t('AdminModule.settings', 'Edit OEmbed provider');
@ -25,21 +86,30 @@ use humhub\modules\ui\form\widgets\ActiveForm;
<br>
<?php $form = ActiveForm::begin(['id' => 'authentication-settings-form', 'acknowledge' => true]); ?>
<?php $form = ActiveForm::begin(['id' => 'oembed-provider-form', 'acknowledge' => true]); ?>
<?= $form->errorSummary($model); ?>
<?= $form->field($model, 'prefix')->textInput(['class' => 'form-control']); ?>
<p class="help-block"><?= Yii::t('AdminModule.settings', 'Url Prefix without http:// or https:// (e.g. youtube.com)'); ?></p>
<?= $form->field($model, 'name')->textInput(['class' => 'form-control']); ?>
<?= $form->field($model, 'pattern')->textInput(['class' => 'form-control']); ?>
<p class="help-block"><?= Yii::t('AdminModule.settings', 'Regular expression by which the link match will be checked.'); ?></p>
<?= $form->field($model, 'endpoint')->textInput(['class' => 'form-control']); ?>
<p class="help-block"><?= Yii::t('AdminModule.settings', 'Use %url% as placeholder for URL. Format needs to be JSON. (e.g. http://www.youtube.com/oembed?url=%url%&format=json)'); ?></p>
<?php if(isset($query['access_token'])): ?>
<?= $form->field($model, 'access_token')->textInput(['class' => 'form-control endpoint-param', 'data-param-name' => 'access_token', 'value' => $query['access_token']]) ?>
<?php endif; ?>
<div id="endpoint-parameters"></div>
<?= Html::submitButton(Yii::t('AdminModule.settings', 'Save'), ['class' => 'btn btn-primary', 'data-ui-loader' => ""]); ?>
<?php ActiveForm::end(); ?>
<?php if ($prefix != ""): ?>
<?= Html::a(Yii::t('AdminModule.settings', 'Delete'), Url::to(['oembed-delete', 'prefix' => $prefix]), ['class' => 'btn btn-danger pull-right', 'data-method' => 'POST']); ?>
<?php if (!empty($name)): ?>
<?= Html::a(Yii::t('AdminModule.settings', 'Delete'), Url::to(['oembed-delete', 'name' => $name]), ['class' => 'btn btn-danger pull-right', 'data-method' => 'POST']); ?>
<?php endif; ?>
<?php $this->endContent(); ?>

View File

@ -25,7 +25,44 @@ return [
['name' => 'colorInfo', 'value' => '#6fdbe8', 'module_id' => 'base'],
['name' => 'colorSuccess', 'value' => '#97d271', 'module_id' => 'base'],
['name' => 'colorDanger', 'value' => '#ff8989', 'module_id' => 'base'],
['name' => 'oembedProviders', 'value' => '{"vimeo.com":"http:\/\/vimeo.com\/api\/oembed.json?scheme=https&url=%url%&format=json&maxwidth=450","youtube.com":"http:\/\/www.youtube.com\/oembed?scheme=https&url=%url%&format=json&maxwidth=450","youtu.be":"http:\/\/www.youtube.com\/oembed?scheme=https&url=%url%&format=json&maxwidth=450","soundcloud.com":"https:\/\/soundcloud.com\/oembed?url=%url%&format=json&maxwidth=450","slideshare.net":"https:\/\/www.slideshare.net\/api\/oembed\/2?url=%url%&format=json&maxwidth=450"}', 'module_id' => 'base'],
['name' => 'oembedProviders', 'value' => json_encode([
'Facebook Video' => [
'pattern' => '/facebook\.com\/(.*)(video)/',
'endpoint' => 'https://graph.facebook.com/v12.0/oembed_video?url=%url%&access_token='
],
'Facebook Post' => [
'pattern' => '/facebook\.com\/(.*)(post|activity|photo|permalink|media|question|note)/',
'endpoint' => 'https://graph.facebook.com/v12.0/oembed_post?url=%url%&access_token='
],
'Facebook Page' => [
'pattern' => '/^(https\:\/\/)*(www\.)*facebook\.com\/((?!video|post|activity|photo|permalink|media|question|note).)*$/',
'endpoint' => 'https://graph.facebook.com/v12.0/oembed_post?url=%url%&access_token='
],
'Instagram' => [
'pattern' => '/instagram\.com/',
'endpoint' => 'https://graph.facebook.com/v12.0/instagram_oembed?url=%url%&access_token='
],
'Twitter' => [
'pattern' => '/twitter\.com/',
'endpoint' => 'https://publish.twitter.com/oembed?url=%url%&maxwidth=450'
],
'YouTube' => [
'pattern' => '/youtube\.com|youtu.be/',
'endpoint' => 'https://www.youtube.com/oembed?scheme=https&url=%url%&format=json&maxwidth=450'
],
'Soundcloud' => [
'pattern' => '/soundcloud\.com/',
'endpoint' => 'https://soundcloud.com/oembed?url=%url%&format=json&maxwidth=450'
],
'Vimeo' => [
'pattern' => '/vimeo\.com/',
'endpoint' => 'https://vimeo.com/api/oembed.json?scheme=https&url=%url%&format=json&maxwidth=450'
],
'SlideShare' => [
'pattern' => '/slideshare\.net/',
'endpoint' => 'https://www.slideshare.net/api/oembed/2?url=%url%&format=json&maxwidth=450'
]
]), 'module_id' => 'base'],
['name' => 'defaultLanguage', 'value' => 'en-US', 'module_id' => 'base'],
['name' => 'maintenanceMode', 'value' => '0', 'module_id' => 'base'],
['name' => 'enableProfilePermissions', 'value' => '1', 'module_id' => 'user'],

View File

@ -12,7 +12,12 @@ class UrlOembedTest extends HumHubDbTestCase
{
parent::_before();
UrlOembedMock::setClient(new UrlOembedClientMock());
UrlOembedMock::setProviders(['test.de' => UrlOembedMock::TEST_PROVIDER_URL_PREFIX.'%url%']);
UrlOembedMock::setProviders([
'Test.de' => [
'pattern' => '/test\.de/',
'endpoint' => UrlOembedMock::TEST_PROVIDER_URL_PREFIX.'%url%'
]
]);
UrlOembedMock::flush();
}
@ -124,4 +129,4 @@ class UrlOembedTest extends HumHubDbTestCase
}
}
}

View File

@ -49,4 +49,48 @@
top: -8px;
}
}
}
}
#oembed-providers {
width: 100%;
display: flex;
flex-direction: row;
justify-content: left;
align-items: center;
flex-wrap: wrap;
margin: 0 -0.5rem;
.oembed-provider-container {
padding: 0;
.oembed-provider {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0.75rem;
margin: 0 0.5rem 0.5rem 0.5rem;
.oembed-provider-name {
display: flex;
justify-content: center;
align-items: center;
.label.label-error {
margin-left: 2px;
}
}
}
}
}
#endpoint-parameters {
display: flex;
flex-direction: row;
justify-content: left;
align-items: center;
flex-wrap: wrap;
margin: 0 -15px;
}

File diff suppressed because one or more lines are too long