Merge branch '2.2-maintenance'

This commit is contained in:
Adrian Moennich 2019-08-20 15:04:22 +02:00
commit 25d262a71f
15 changed files with 619 additions and 0 deletions

4
ursh/MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
graft indico_ursh/static
graft indico_ursh/translations
global-exclude *.pyc __pycache__ .keep

View File

@ -0,0 +1,13 @@
# This file is part of the Indico plugins.
# Copyright (C) 2002 - 2019 CERN
#
# The Indico plugins are free software; you can redistribute
# them and/or modify them under the terms of the MIT License;
# see the LICENSE file for more details.
from __future__ import unicode_literals
from indico.util.i18n import make_bound_gettext
_ = make_bound_gettext('ursh')

View File

@ -0,0 +1,19 @@
# This file is part of the Indico plugins.
# Copyright (C) 2002 - 2019 CERN
#
# The Indico plugins are free software; you can redistribute
# them and/or modify them under the terms of the MIT License;
# see the LICENSE file for more details.
from __future__ import unicode_literals
from indico.core.plugins import IndicoPluginBlueprint
from indico_ursh.controllers import RHCustomShortURLPage, RHGetShortURL, RHShortURLPage
blueprint = IndicoPluginBlueprint('ursh', 'indico_ursh')
blueprint.add_url_rule('/ursh', 'get_short_url', RHGetShortURL, methods=('POST',))
blueprint.add_url_rule('/url-shortener', 'shorten_url', RHShortURLPage)
blueprint.add_url_rule('/event/<confId>/manage/short-url', 'shorten_event_url', RHCustomShortURLPage,
methods=('GET', 'POST'))

View File

@ -0,0 +1,150 @@
// This file is part of the Indico plugins.
// Copyright (C) 2002 - 2019 CERN
//
// The Indico plugins are free software; you can redistribute
// them and/or modify them under the terms of the MIT License;
// see the LICENSE file for more details.
/* global $T:false */
import {handleAxiosError, indicoAxios} from 'indico/utils/axios';
const $t = $T.domain('ursh');
function _showTip(element, msg, hideEvent = 'unfocus') {
element.qtip({
content: {
text: msg,
},
hide: {
event: hideEvent,
fixed: true,
delay: 700,
},
show: {
event: false,
ready: true,
},
});
}
async function _makeUrshRequest(originalURL) {
const urshEndpoint = '/ursh';
let response;
try {
response = await indicoAxios.post(urshEndpoint, {
original_url: originalURL,
});
} catch (error) {
handleAxiosError(error);
return;
}
return response.data.url;
}
function _validateAndFormatURL(url) {
if (!url) {
throw Error($t.gettext('Please fill in a URL to shorten'));
}
// if protocol is missing, prepend it
if (url.startsWith(location.hostname)) {
url = `${location.protocol}//${url}`;
}
// regular expression, because I.E. does not like the URL class
// provides minimal validation, leaving the serious job to the server
const re = RegExp(`^([\\d\\w]+:)//([^/ .]+(?:\\.[^/ .]+)*)(/.*)?$`);
const urlTokens = url.match(re);
if (!urlTokens) {
throw Error($t.gettext('This does not look like a valid URL'));
}
// extract tokens
const hostname = urlTokens[2];
const path = urlTokens[3] ? urlTokens[3] : '/';
const protocol = location.protocol; // patch protocol to match server
if (hostname !== location.hostname) {
throw Error($t.gettext('Invalid host: only Indico URLs are allowed'));
}
return `${protocol}//${hostname}${path}`;
}
function _getUrshInput(input) {
const inputURL = input.val().trim();
input.val(inputURL);
try {
const formattedURL = _validateAndFormatURL(inputURL);
input.val(formattedURL);
return formattedURL;
} catch (err) {
_showTip(input, err.message);
input.focus().select();
return null;
}
}
async function _handleUrshPageInput(evt) {
evt.preventDefault();
const input = $('#ursh-shorten-input');
const originalURL = _getUrshInput(input);
if (originalURL) {
const result = await _makeUrshRequest(originalURL);
if (result) {
const outputElement = $('#ursh-shorten-output');
$('#ursh-shorten-response-form').slideDown();
outputElement.val(result);
outputElement.select();
} else {
_showTip(input, $t.gettext('This does not look like a valid URL'));
input.focus().select();
}
}
}
async function _handleUrshClick(evt) {
evt.preventDefault();
const originalURL = evt.target.dataset.originalUrl;
const result = await _makeUrshRequest(originalURL);
$(evt.target).copyURLTooltip(result, 'unfocus');
}
function _validateUrshCustomShortcut(shortcut) {
return shortcut.length >= 5;
}
$(document)
.on('click', '#ursh-shorten-button', _handleUrshPageInput)
.on('keydown', '#ursh-shorten-input', evt => {
if (evt.key === 'Enter') {
_handleUrshPageInput(evt);
}
})
.on('click', '.ursh-get', _handleUrshClick)
.on('input', '#ursh-custom-shortcut-input', evt => {
const value = $(evt.target).val();
$('#ursh-custom-shortcut-submit-button').prop('disabled', !_validateUrshCustomShortcut(value));
})
.on('mouseenter', '#ursh-custom-shortcut-submit-button', evt => {
if (evt.target.disabled) {
_showTip(
$(evt.target),
$t.gettext('Please check that the shortcut is correct'),
'mouseleave'
);
}
});
$(document).ready(() => {
// keep dropdown menu open when clicking on an entry
$('.ursh-dropdown')
.next('ul')
.find('li a')
.on('menu_select', () => true);
});

View File

@ -0,0 +1,103 @@
# This file is part of the Indico plugins.
# Copyright (C) 2002 - 2019 CERN
#
# The Indico plugins are free software; you can redistribute
# them and/or modify them under the terms of the MIT License;
# see the LICENSE file for more details.
from __future__ import unicode_literals
import posixpath
from flask import jsonify, request, session
from flask_pluginengine import render_plugin_template
from werkzeug.exceptions import BadRequest
from werkzeug.urls import url_parse
from indico.core.config import config
from indico.modules.events.management.controllers import RHManageEventBase
from indico.web.rh import RH
from indico.web.util import jsonify_template
from indico_ursh import _
from indico_ursh.util import register_shortcut, request_short_url, strip_end
from indico_ursh.views import WPShortenURLPage
CUSTOM_SHORTCUT_ALPHABET = frozenset('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-')
class RHGetShortURL(RH):
"""Make a request to the URL shortening service"""
@staticmethod
def _resolve_full_url(original_url):
if url_parse(original_url).host:
return original_url
original_url = original_url.lstrip('/')
return posixpath.join(config.BASE_URL, original_url)
@staticmethod
def _check_host(full_url):
if url_parse(full_url).host != url_parse(config.BASE_URL).host:
raise BadRequest('Invalid host for URL shortening service')
def _process(self):
original_url = request.json.get('original_url')
full_url = self._resolve_full_url(original_url)
self._check_host(full_url)
short_url = request_short_url(full_url)
return jsonify(url=short_url)
class RHShortURLPage(RH):
"""Provide a simple page, where users can submit a URL to be shortened"""
def _process(self):
return WPShortenURLPage.render_template('ursh_shortener_page.html')
class RHCustomShortURLPage(RHManageEventBase):
"""Provide a simple page, where users can submit a URL to be shortened"""
def _make_absolute_url(self, url):
return posixpath.join(config.BASE_URL, url[1:]) if url.startswith('/') else url
def _get_error_msg(self, result):
if result['status'] == 409:
return _('Shortcut already exists')
elif result['status'] == 400:
return _('Malformed shortcut')
return result['error'].get('description')
def _process_args(self):
from indico_ursh.plugin import UrshPlugin
super(RHCustomShortURLPage, self)._process_args()
api_host = url_parse(UrshPlugin.settings.get('api_host'))
self.ursh_host = strip_end(api_host.to_url(), api_host.path[1:]).rstrip('/') + '/'
def _process_GET(self):
original_url = self._make_absolute_url(request.args['original_url'])
return WPShortenURLPage.render_template('ursh_custom_shortener_page.html',
event=self.event,
ursh_host=self.ursh_host,
original_url=original_url,
submitted=False)
def _process_POST(self):
original_url = self._make_absolute_url(request.args['original_url'])
shortcut = request.form['shortcut'].strip()
if not (set(shortcut) <= CUSTOM_SHORTCUT_ALPHABET):
raise BadRequest('Invalid shortcut')
result = register_shortcut(original_url, shortcut, session.user)
if result.get('error'):
kwargs = {'success': False, 'msg': self._get_error_msg(result)}
else:
kwargs = {'success': True, 'shorturl': result['short_url']}
return jsonify_template('ursh_custom_shortener_page.html', render_plugin_template,
event=self.event, ursh_host=self.ursh_host, shortcut=shortcut,
original_url=original_url, submitted=True, **kwargs)

View File

@ -0,0 +1,58 @@
# This file is part of the Indico plugins.
# Copyright (C) 2002 - 2019 CERN
#
# The Indico plugins are free software; you can redistribute
# them and/or modify them under the terms of the MIT License;
# see the LICENSE file for more details.
from __future__ import unicode_literals
from flask_pluginengine import render_plugin_template
from wtforms.fields.core import StringField
from wtforms.fields.html5 import URLField
from wtforms.validators import DataRequired
from indico.core.plugins import IndicoPlugin
from indico.web.forms.base import IndicoForm
from indico.web.views import WPBase
from indico_ursh import _
from indico_ursh.blueprint import blueprint
class SettingsForm(IndicoForm):
api_key = StringField(_('API key'), [DataRequired()],
description=_('The API key to access the <tt>ursh</tt> service'))
api_host = URLField(_('API host'), [DataRequired()],
description=_('The <tt>ursh</tt> API host, providing the interface to generate short URLs'))
class UrshPlugin(IndicoPlugin):
"""URL Shortener
Provides a URL shortening service for indico assets, events, etc.
"""
configurable = True
settings_form = SettingsForm
default_settings = {
'api_key': '',
'api_host': '',
}
def init(self):
super(UrshPlugin, self).init()
self.template_hook('url-shortener', self._inject_ursh_link)
self.template_hook('page-footer', self._inject_ursh_footer)
self.inject_bundle('main.js', WPBase)
def get_blueprints(self):
return blueprint
def _inject_ursh_link(self, target=None, event=None, dropdown=False, element_class='', text='', **kwargs):
if self.settings.get('api_key') and self.settings.get('api_host') and (target or event):
return render_plugin_template('ursh_link.html', target=target, event=event,
dropdown=dropdown, element_class=element_class, text=text, **kwargs)
def _inject_ursh_footer(self, **kwargs):
return render_plugin_template('ursh_footer.html')

View File

@ -0,0 +1,57 @@
{% extends 'layout/base.html' %}
{% block page_class %}fixed-width-standalone-text-page{% endblock %}
{% block title -%}
{% trans %}Create custom shortcut{% endtrans %}
{%- endblock %}
{% block content -%}
{% if submitted and not success %}
<div class="error-message-box">
<div class="message-box-content">
<span class="icon"></span>
<div class="message-text">{% trans %}Custom URL creation failed:{% endtrans %} {{ msg }}</div>
</div>
</div>
{% elif submitted and success %}
<div class="success-message-box">
<div class="message-box-content">
<span class="icon"></span>
<div class="message-text">{% trans %}Custom URL created successfully{% endtrans %}</div>
</div>
</div>
{% endif %}
<p>{% trans %}Creating a custom shortcut for the following URL:{% endtrans %}</p>
<div class="i-has-action">
<input type="url" style="font-family:monospace;" value="{{ original_url }}" disabled>
</div>
<p>{% trans %}Please enter the desired shortcut below (at least 5 chars).{% endtrans %}</p>
<form method="POST">
<div class="i-has-action">
<button type="button" class="i-button" style="font-family:monospace;" disabled>
{{- ursh_host -}}
</button>
<input id="ursh-custom-shortcut-input" name="shortcut" type="text" style="font-family:monospace;"
spellcheck="false" autocomplete="off" autocapitalize="off"
pattern="^[0-9a-zA-Z-]{5,}$"
{% if shortcut %}value="{{ shortcut }}"{% endif %}
placeholder="{% trans %}Enter a shortcut...{% endtrans %}">
<button id="ursh-custom-shortcut-submit-button" type="submit" class="i-button" disabled>
{% trans -%}Create{%- endtrans %}
</button>
</div>
</form>
<script>
$(document).ready(() => {
$('#ursh-custom-shortcut-input').focus().select();
});
</script>
{% if submitted and success and shorturl %}
<script>
$(document).ready(() => {
$('#ursh-custom-shortcut-input').copyURLTooltip({{ shorturl | tojson }}, 'unfocus');
});
</script>
{% endif %}
{%- endblock %}

View File

@ -0,0 +1,23 @@
<a href="" class="arrow js-dropdown ursh-dropdown {{ element_class }}" data-toggle="dropdown"
title="{% trans %}Short URL options{% endtrans %}"></a>
<ul class="i-dropdown">
<li>
<a class="ursh-get"
data-original-url="{{ target }}">
{{ trans }}Get short URL{{ end_trans }}
</a>
</li>
{%- if allow_create %}
<li>
<a href="{{ url_for('plugin_ursh.shorten_url_custom') }}"
class="ursh-create" data-original-url="{{ target }}"
data-title="{{ _('Create short URL') }}"
data-href="{{ url_for('plugin_ursh.shorten_url_custom') }}"
data-params='{"original_url": "{{ target }}"}'
data-ajax-dialog
data-hide-page-header>
{{ trans }}Create custom short URL{{ end_trans }}
</a>
</li>
{% endif -%}
</ul>

View File

@ -0,0 +1,8 @@
<a href="{{ url_for('plugin_ursh.shorten_url') }}"
data-title="{{ _("URL Shortener") }}"
data-href="{{ url_for('plugin_ursh.shorten_url', url=request.url) }}"
data-ajax-dialog
data-hide-page-header
data-close-button>
{% trans %}URL Shortener{% endtrans %}
</a>

View File

@ -0,0 +1,32 @@
{% set event_manager = event and event.can_manage(session.user) %}
{% set target = event.url if event else target %}
{% if dropdown or event_manager %}
<a href="#" class="arrow js-dropdown ursh-dropdown {{ classes }}" data-toggle="dropdown"
title="{% trans %}Short URL options{% endtrans %}"></a>
<ul class="i-dropdown">
<li>
<a class="ursh-get" data-original-url="{{ target }}">
{{ trans }}Get short URL{{ end_trans }}
</a>
</li>
{%- if event_manager %}
<li>
<a href="{{ url_for('plugin_ursh.shorten_event_url', event) }}"
class="ursh-create" data-original-url="{{ target }}"
data-title="{{ _('Create short URL') }}"
data-href="{{ url_for('plugin_ursh.shorten_event_url', event) }}"
data-params='{"original_url": "{{ target }}"}'
data-ajax-dialog
data-hide-page-header>
{{ trans }}Create custom short URL{{ end_trans }}
</a>
</li>
{% endif -%}
</ul>
{% else %}
<a class="ursh-get {{ classes }}"
title="{% trans %}Obtain short URL{% endtrans %}"
data-original-url="{{ target }}">
{{- text -}}
</a>
{% endif %}

View File

@ -0,0 +1,36 @@
{% extends 'layout/base.html' %}
{% block page_class %}fixed-width-standalone-text-page{% endblock %}
{% block title -%}
{% trans %}URL Shortener{% endtrans %}
{%- endblock %}
{% block content -%}
<p>
{% trans -%}
In this page, you can generate short links for any Indico URL.
Just enter the URL below and click "Shorten".
{%- endtrans %}
</p>
<div id="ursh-shorten-request-form" class="i-has-action">
<input id="ursh-shorten-input" name="ursh-shorten-original-url" type="url" style="font-family:monospace;"
spellcheck="false" autocomplete="off" autocapitalize="off"
value="{{ request.args.url }}"
placeholder="{% trans %}Enter an Indico URL to shorten...{% endtrans %}">
<button id="ursh-shorten-button" type="button" class="i-button">{% trans %}Shorten{% endtrans %}</button>
</div>
<div id="ursh-shorten-response-form" class="i-has-action" style="margin-top:1em;display:none">
<input id="ursh-shorten-output" name="ursh-shorten-short-url" type="url" style="font-family:monospace;"
spellcheck="false" autocomplete="off" autocapitalize="off" readonly>
<button type="button" class="i-button icon-clipboard js-copy-to-clipboard"
title="{% trans %}Copy to clipboard{% endtrans %}"
data-clipboard-target="#ursh-shorten-output"></button>
</div>
<p>
{% trans -%}
<strong>Tip:</strong> You can also generate short links by using the quick-links in various
places within Indico! Just look for the <span class="icon-link"></span> icon.
{%- endtrans %}
</p>
{%- endblock %}

61
ursh/indico_ursh/util.py Normal file
View File

@ -0,0 +1,61 @@
# This file is part of the Indico plugins.
# Copyright (C) 2002 - 2019 CERN
#
# The Indico plugins are free software; you can redistribute
# them and/or modify them under the terms of the MIT License;
# see the LICENSE file for more details.
from __future__ import unicode_literals
import json
import posixpath
import requests
from werkzeug.exceptions import ServiceUnavailable
def _get_settings():
from indico_ursh.plugin import UrshPlugin
api_key = UrshPlugin.settings.get('api_key')
api_host = UrshPlugin.settings.get('api_host')
if not api_key or not api_host:
raise ServiceUnavailable('Not configured')
return api_key, api_host
def request_short_url(original_url):
from indico_ursh.plugin import UrshPlugin
api_key, api_host = _get_settings()
headers = {'Authorization': 'Bearer {api_key}'.format(api_key=api_key), 'Content-Type': 'application/json'}
url = posixpath.join(api_host, 'api/urls/')
response = requests.post(url, data=json.dumps({'url': original_url, 'allow_reuse': True}), headers=headers)
response.raise_for_status()
data = response.json()
UrshPlugin.logger.info('Shortcut created: %s -> %s', data['shortcut'], original_url)
return data['short_url']
def register_shortcut(original_url, shortcut, user):
from indico_ursh.plugin import UrshPlugin
api_key, api_host = _get_settings()
headers = {'Authorization': 'Bearer {api_key}'.format(api_key=api_key), 'Content-Type': 'application/json'}
url = posixpath.join(api_host, 'api/urls', shortcut)
data = {'url': original_url, 'allow_reuse': True, 'meta': {'indico.user': user.id}}
response = requests.put(url, data=json.dumps(data), headers=headers)
if not (400 <= response.status_code < 500):
response.raise_for_status()
data = response.json()
if not data.get('error'):
UrshPlugin.logger.info('Shortcut created: %s -> %s', data['shortcut'], original_url)
return data
def strip_end(text, suffix):
if not text.endswith(suffix):
return text
return text[:len(text) - len(suffix)]

16
ursh/indico_ursh/views.py Normal file
View File

@ -0,0 +1,16 @@
# This file is part of the Indico plugins.
# Copyright (C) 2002 - 2019 CERN
#
# The Indico plugins are free software; you can redistribute
# them and/or modify them under the terms of the MIT License;
# see the LICENSE file for more details.
from __future__ import unicode_literals
from indico.core.plugins import WPJinjaMixinPlugin
from indico.web.views import WPDecorated
class WPShortenURLPage(WPJinjaMixinPlugin, WPDecorated):
def _get_body(self, params):
return self._get_page_content(params)

34
ursh/setup.py Normal file
View File

@ -0,0 +1,34 @@
# This file is part of the Indico plugins.
# Copyright (C) 2002 - 2019 CERN
#
# The Indico plugins are free software; you can redistribute
# them and/or modify them under the terms of the MIT License;
# see the LICENSE file for more details.
from __future__ import unicode_literals
from setuptools import find_packages, setup
setup(
name='indico-plugin-ursh',
version='2.2',
description='URL shortening service for Indico',
url='https://github.com/indico/indico-plugins',
license='MIT',
author='Indico Team',
author_email='indico-team@cern.ch',
packages=find_packages(),
zip_safe=False,
include_package_data=True,
install_requires=[
'indico>=2.2.dev0',
],
classifiers=[
'Environment :: Plugins',
'Environment :: Web Environment',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Programming Language :: Python :: 2.7'
],
entry_points={'indico.plugins': {'ursh = indico_ursh.plugin:UrshPlugin'}}
)

View File

@ -0,0 +1,5 @@
{
"entry": {
"main": "./index.js"
}
}