Merge branch 'feature/improve-self-editing-vote' into 'develop'

Feature : improve self editing vote

Adding a possibility to the user to enter his email address and to receive the personalized url by email.

See merge request !110
This commit is contained in:
Olivier Perez 2016-05-03 21:16:05 +02:00
commit ab0748597a
20 changed files with 302 additions and 23 deletions

View File

@ -23,8 +23,6 @@ use Framadate\Services\MailService;
use Framadate\Services\NotificationService;
use Framadate\Services\SecurityService;
use Framadate\Message;
use Framadate\Utils;
use Framadate\Editable;
include_once __DIR__ . '/../app/inc/init.php';

View File

@ -0,0 +1,96 @@
<?php
/**
* This software is governed by the CeCILL-B license. If a copy of this license
* is not distributed with this file, you can obtain one at
* http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt
*
* Authors of STUdS (initial project): Guilhem BORGHESI (borghesi@unistra.fr) and Raphaël DROZ
* Authors of Framadate/OpenSondate: Framasoft (https://github.com/framasoft)
*
* =============================
*
* Ce logiciel est régi par la licence CeCILL-B. Si une copie de cette licence
* ne se trouve pas avec ce fichier vous pouvez l'obtenir sur
* http://www.cecill.info/licences/Licence_CeCILL-B_V1-fr.txt
*
* Auteurs de STUdS (projet initial) : Guilhem BORGHESI (borghesi@unistra.fr) et Raphaël DROZ
* Auteurs de Framadate/OpenSondage : Framasoft (https://github.com/framasoft)
*/
use Framadate\Services\SessionService;
use Framadate\Services\PollService;
use Framadate\Services\MailService;
use Framadate\Services\LogService;
use Framadate\Message;
use Framadate\Utils;
include_once __DIR__ . '/../app/inc/init.php';
$logService = new LogService();
$sessionService = new SessionService();
$mailService = new MailService($config['use_smtp']);
$pollService = new PollService($connect, $logService);
$result = false;
$message = null;
$poll = null;
$poll_id = null;
$email = null;
if (!empty($_POST['poll'])) {
$poll_id = filter_input(INPUT_POST, 'poll', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => POLL_REGEX]]);
$poll = $pollService->findById($poll_id);
}
$token = $sessionService->get("Common", SESSION_EDIT_LINK_TOKEN);
$token_form_value = empty($_POST['token']) ? null : $_POST['token'];
$editedVoteUniqueId = filter_input(INPUT_POST, 'editedVoteUniqueId', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => POLL_REGEX]]);
if (is_null($poll) || $config['use_smtp'] == false || is_null($token) || is_null($token_form_value)
|| !$token->check($token_form_value) || is_null($editedVoteUniqueId)) {
$message = new Message('error', __('Error', 'Something is going wrong...'));
}
if (is_null($message)) {
$email = $mailService->isValidEmail($_POST['email']);
if (is_null($email)) {
$message = new Message('error', __('EditLink', 'The email address is not correct.'));
}
}
if (is_null($message)) {
$time = $sessionService->get("Common", SESSION_EDIT_LINK_TIME);
if (!empty($time)) {
$remainingTime = TIME_EDIT_LINK_EMAIL - (time() - $time);
if ($remainingTime > 0) {
$message = new Message('error', __f('EditLink', 'Please wait %d seconds before we can send an email to you then try again.', $remainingTime));
}
}
}
if (is_null($message)) {
$url = Utils::getUrlSondage($poll_id, false, $editedVoteUniqueId);
$smarty->assign('poll', $poll);
$smarty->assign('poll_id', $poll_id);
$smarty->assign('editedVoteUniqueId', $editedVoteUniqueId);
$body = $smarty->fetch('mail/remember_edit_link.tpl');
$subject = '[' . NOMAPPLICATION . ']['.__('EditLink', 'REMINDER').'] '.__f('EditLink', 'Edit link for poll "%s"', $poll->title);
//$mailService->send($email, $subject, $body);
$sessionService->remove("Common", SESSION_EDIT_LINK_TOKEN);
$sessionService->set("Common", SESSION_EDIT_LINK_TIME, time());
$message = new Message('success', __('EditLink', 'Your reminder has been successfully sent!'));
$result = true;
}
$smarty->error_reporting = E_ALL & ~E_NOTICE;
$response = array('result' => $result, 'message' => $message);
echo json_encode($response);

View File

@ -23,11 +23,17 @@ class Message {
var $type;
var $message;
var $link;
var $linkTitle;
var $linkIcon;
var $includeTemplate;
function __construct($type, $message, $link=null) {
function __construct($type, $message, $link=null, $linkTitle=null, $linkIcon=null, $includeTemplate=null) {
$this->type = $type;
$this->message = $message;
$this->link = $link;
$this->linkTitle = $linkTitle;
$this->linkIcon = $linkIcon;
$this->includeTemplate = $includeTemplate;
}
}

View File

@ -3,12 +3,13 @@ namespace Framadate\Security;
class Token {
const DEFAULT_LENGTH = 64;
private $time;
private $value;
private $length;
private static $codeAlphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789';
function __construct($length = 64) {
function __construct($length = self::DEFAULT_LENGTH) {
$this->length = $length;
$this->time = time() + TOKEN_TIME;
$this->value = $this->generate();
@ -41,7 +42,7 @@ class Token {
* @param bool $crypto_strong If passed, tells if the token is "cryptographically strong" or not.
* @return string
*/
public static function getToken($length, &$crypto_strong = false) {
public static function getToken($length = self::DEFAULT_LENGTH, &$crypto_strong = false) {
if (function_exists('openssl_random_pseudo_bytes')) {
openssl_random_pseudo_bytes(1, $crypto_strong); // Fake use to see if the algorithm used was "cryptographically strong"
return self::getSecureToken($length);

View File

@ -44,6 +44,19 @@ class SessionService {
$_SESSION[$section][$key] = $value;
}
/**
* Remove a session value
*
* @param $section
* @param $key
*/
public function remove($section, $key) {
assert(!empty($key));
assert(!empty($section));
unset($_SESSION[$section][$key]);
}
private function initSectionIfNeeded($section) {
if (!isset($_SESSION[$section])) {
$_SESSION[$section] = array();

View File

@ -77,6 +77,9 @@ const PURGE_DELAY = 60;
// Max slots per poll
const MAX_SLOTS_PER_POLL = 366;
// Number of seconds before we allow to resend an "Remember Edit Link" email.
const TIME_EDIT_LINK_EMAIL = 60;
// Config
$config = [
/* general config */

View File

@ -30,5 +30,9 @@ const EDITABLE_CHOICE_REGEX = '/^[0-2]$/';
const BASE64_REGEX = '/^[A-Za-z0-9]+$/';
const MD5_REGEX = '/^[A-Fa-f0-9]{32}$/';
// Session constants
const SESSION_EDIT_LINK_TOKEN = 'EditLinkToken';
const SESSION_EDIT_LINK_TIME = "EditLinkMail";
// CSRF (300s = 5min)
const TOKEN_TIME = 300;

View File

@ -136,7 +136,7 @@
},
"Poll results": {
"Votes of the poll": "Stimmabgaben zur Umfrage",
"Edit the line:": "Zeile bearbeiten:",
"Edit the line: %s": "Zeile bearbeiten: %s",
"Remove the line:": "Zeile entfernen:",
"Vote no for": "Nein-Stimmen für",
"Vote yes for": "Ja-Stimmen für",
@ -179,6 +179,16 @@
"Update vote succeeded": "Die Wertung wurde geändert",
"Adding the vote succeeded": "Die Wertung wurde hinzugefügt"
},
"EditLink": {
"Send": "DE_Envoyer",
"If you don't want to lose your personalized link, we can send it to your email.": "DE_Afin de ne pas perdre ce lien d'édition de vote, nous pouvons vous l'envoyer par courriel.",
"The email address is not correct.": "DE_Courriel incorrect.",
"REMINDER": "DE_RAPPEL",
"Edit link for poll \"%s\"": "DE_Lien d'édition du sondage \"%s\"",
"Please wait %d seconds before we can send an email to you then try again.": "DE_Veuillez patienter encore %d seconds avant que nous puissions vous envoyer un email, puis réessayez.",
"Here is the link for editing your vote:": "DE_Voici le lien pour éditer votre vote :",
"Your reminder has been successfully sent!": "DE_Votre rappel a été envoyé avec succès !"
},
"adminstuds": {
"As poll administrator, you can change all the lines of this poll with this button": "Als Administrator der Umfrage können Sie alle Zeilen der Umfrage über diesen Button ändern",
"remove a column or a line with": "Zeile oder Spalte entfernen mit",

View File

@ -136,7 +136,7 @@
},
"Poll results": {
"Votes of the poll": "Votes",
"Edit the line:": "Edit line:",
"Edit the line: %s": "Edit line: %s",
"Remove the line:": "Remove line:",
"Vote no for": "Vote \"no\" for",
"Vote yes for": "Vote \"yes\" for",
@ -179,6 +179,16 @@
"Update vote succeeded": "Vote updated",
"Adding the vote succeeded": "Vote added"
},
"EditLink": {
"Send": "Send",
"If you don't want to lose your personalized link, we can send it to your email.": "If you don't want to lose your personalized link, we can send it to your email.",
"The email address is not correct.": "The email address is not correct.",
"REMINDER": "REMINDER",
"Edit link for poll \"%s\"": "Edit link for poll \"%s\"",
"Please wait %d seconds before we can send an email to you then try again.": "Please wait %d seconds before we can send an email to you then try again.",
"Here is the link for editing your vote:": "Here is the link for editing your vote:",
"Your reminder has been successfully sent!": "Your reminder has been successfully sent!"
},
"adminstuds": {
"As poll administrator, you can change all the lines of this poll with this button": "As poll administrator, you can change all the lines of this poll with this button",
"remove a column or a line with": "remove a column or a line with",

View File

@ -139,7 +139,7 @@
},
"Poll results": {
"Votes of the poll": "Votos de la encuesta",
"Edit the line:": "Modificar la fila:",
"Edit the line: %s": "Modificar la fila: %s",
"Remove the line:": "Borrar la fila:",
"Vote no for": "Votar « no » para",
"Vote yes for": "Votar « si » para",
@ -182,6 +182,16 @@
"Update vote succeeded": "Actualización exítosa de su voto",
"Adding the vote succeeded": "Voto guardado con exíto"
},
"EditLink": {
"Send": "ES_Envoyer",
"If you don't want to lose your personalized link, we can send it to your email.": "ES_Afin de ne pas perdre ce lien d'édition de vote, nous pouvons vous l'envoyer par courriel.",
"The email address is not correct.": "ES_Courriel incorrect.",
"REMINDER": "ES_RAPPEL",
"Edit link for poll \"%s\"": "ES_Lien d'édition du sondage \"%s\"",
"Please wait %d seconds before we can send an email to you then try again.": "ES_Veuillez patienter encore %d seconds avant que nous puissions vous envoyer un email, puis réessayez.",
"Here is the link for editing your vote:": "ES_Voici le lien pour éditer votre vote :",
"Your reminder has been successfully sent!": "ES_Votre rappel a été envoyé avec succès !"
},
"adminstuds": {
"As poll administrator, you can change all the lines of this poll with this button": "Como administrador, Usted puede cambiar todas las filas de la encuesta con este botón",
"remove a column or a line with": "borrar una columna o una fila con",

View File

@ -136,7 +136,7 @@
},
"Poll results": {
"Votes of the poll": "Votes du sondage",
"Edit the line:": "Modifier la ligne :",
"Edit the line: %s": "Modifier la ligne : %s",
"Remove the line:": "Supprimer la ligne :",
"Vote no for": "Voter « non » pour",
"Vote yes for": "Voter « oui » pour",
@ -179,6 +179,16 @@
"Update vote succeeded": "Mise à jour du vote réussi",
"Adding the vote succeeded": "Ajout du vote réussi"
},
"EditLink": {
"Send": "Envoyer",
"If you don't want to lose your personalized link, we can send it to your email.": "Afin de ne pas perdre ce lien d'édition de vote, nous pouvons vous l'envoyer par courriel.",
"The email address is not correct.": "Courriel incorrect.",
"REMINDER": "RAPPEL",
"Edit link for poll \"%s\"": "Lien d'édition du sondage \"%s\"",
"Please wait %d seconds before we can send an email to you then try again.": "Veuillez patienter encore %d secondes avant que nous puissions vous envoyer un email, puis réessayez.",
"Here is the link for editing your vote:": "Voici le lien pour éditer votre vote :",
"Your reminder has been successfully sent!": "Votre rappel a été envoyé avec succès !"
},
"adminstuds": {
"As poll administrator, you can change all the lines of this poll with this button": "En tant qu'administrateur, vous pouvez modifier toutes les lignes de ce sondage avec ce bouton",
"remove a column or a line with": "effacer une colonne ou une ligne avec",

View File

@ -136,7 +136,7 @@
},
"Poll results": {
"Votes of the poll": "Voti del sondaggio ",
"Edit the line:": "Modificare la riga :",
"Edit the line: %s": "Modificare la riga : %s",
"Remove the line:": "Eliminare la riga :",
"Vote no for": "Scegliere \"no\" per",
"Vote yes for": "Scegliere \"sì\" per",
@ -179,6 +179,16 @@
"Update vote succeeded": "Aggiornamento del voto di successo",
"Adding the vote succeeded": "L'aggiunta del voto ha avuto successo"
},
"EditLink": {
"Send": "IT_Envoyer",
"If you don't want to lose your personalized link, we can send it to your email.": "IT_Afin de ne pas perdre ce lien d'édition de vote, nous pouvons vous l'envoyer par courriel.",
"The email address is not correct.": "IT_Courriel incorrect.",
"REMINDER": "IT_RAPPEL",
"Edit link for poll \"%s\"": "IT_Lien d'édition du sondage \"%s\"",
"Please wait %d seconds before we can send an email to you then try again.": "IT_Veuillez patienter encore %d seconds avant que nous puissions vous envoyer un email, puis réessayez.",
"Here is the link for editing your vote:": "IT_Voici le lien pour éditer votre vote :",
"Your reminder has been successfully sent!": "IT_Votre rappel a été envoyé avec succès !"
},
"adminstuds": {
"As poll administrator, you can change all the lines of this poll with this button": "Essendo l'amministratore, potete modificare tutte le righe di questo sondaggio con questo pulsante",
"remove a column or a line with": " cancellare una colonna o una riga con ",

View File

@ -134,7 +134,7 @@
},
"Poll results": {
"Votes of the poll": "Vòtes del sondatge",
"Edit the line:": "Modificar la linha :",
"Edit the line: %s": "Modificar la linha : %s",
"Remove the line:": "Suprimir la linha :",
"Vote no for": "Votar « non » per",
"Vote yes for": "Votar « òc » per",
@ -170,6 +170,16 @@
"Update vote succeeded": "Mesa a jorn del vòte amb succès",
"Adding the vote succeeded": "Apondon del vòte capitat"
},
"EditLink": {
"Send": "OC_Envoyer",
"If you don't want to lose your personalized link, we can send it to your email.": "OC_Afin de ne pas perdre ce lien d'édition de vote, nous pouvons vous l'envoyer par courriel.",
"The email address is not correct.": "OC_Courriel incorrect.",
"REMINDER": "OC_RAPPEL",
"Edit link for poll \"%s\"": "OC_Lien d'édition du sondage \"%s\"",
"Please wait %d seconds before we can send an email to you then try again.": "OC_Veuillez patienter encore %d seconds avant que nous puissions vous envoyer un email, puis réessayez.",
"Here is the link for editing your vote:": "OC_Voici le lien pour éditer votre vote :",
"Your reminder has been successfully sent!": "OC_Votre rappel a été envoyé avec succès !"
},
"adminstuds": {
"As poll administrator, you can change all the lines of this poll with this button": "En qualitat d'administrator podètz modificar totas las linhas d'aqueste sondatge amb aqueste boton",
"remove a column or a line with": "escafar una colomna o una linha amb",

View File

@ -28,6 +28,7 @@ use Framadate\Services\SessionService;
use Framadate\Message;
use Framadate\Utils;
use Framadate\Editable;
use Framadate\Security\Token;
include_once __DIR__ . '/app/inc/init.php';
@ -139,9 +140,7 @@ if ($accessGranted) {
if ($result) {
if ($poll->editable == Editable::EDITABLE_BY_OWN) {
$editedVoteUniqueId = filter_input(INPUT_POST, 'edited_vote', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => POLL_REGEX]]);
$sessionService->set(USER_REMEMBER_VOTES_KEY, $poll_id, $editedVoteUniqueId);
$urlEditVote = Utils::getUrlSondage($poll_id, false, $editedVoteUniqueId);
$message = new Message('success', __('studs', 'Your vote has been registered successfully, but be careful: regarding this poll options, you need to keep this personal link to edit your own vote:'), $urlEditVote);
$message = getMessageForOwnVoteEditableVote($sessionService, $smarty, $editedVoteUniqueId, $config['use_smtp'], $poll_id, $name);
} else {
$message = new Message('success', __('studs', 'Update vote succeeded'));
}
@ -172,9 +171,7 @@ if ($accessGranted) {
if ($result) {
if ($poll->editable == Editable::EDITABLE_BY_OWN) {
$editedVoteUniqueId = $result->uniqId;
$sessionService->set(USER_REMEMBER_VOTES_KEY, $poll_id, $editedVoteUniqueId);
$urlEditVote = Utils::getUrlSondage($poll_id, false, $editedVoteUniqueId);
$message = new Message('success', __('studs', 'Your vote has been registered successfully, but be careful: regarding this poll options, you need to keep this personal link to edit your own vote:'), $urlEditVote);
$message = getMessageForOwnVoteEditableVote($sessionService, $smarty, $editedVoteUniqueId, $config['use_smtp'], $poll_id, $name);
} else {
$message = new Message('success', __('studs', 'Adding the vote succeeded'));
}
@ -191,6 +188,28 @@ if ($accessGranted) {
}
}
// Functions
function getMessageForOwnVoteEditableVote(SessionService &$sessionService, Smarty &$smarty, $editedVoteUniqueId, $canUseSMTP, $poll_id, $name) {
$sessionService->set(USER_REMEMBER_VOTES_KEY, $poll_id, $editedVoteUniqueId);
$urlEditVote = Utils::getUrlSondage($poll_id, false, $editedVoteUniqueId);
$message = new Message(
'success',
__('studs', 'Your vote has been registered successfully, but be careful: regarding this poll options, you need to keep this personal link to edit your own vote:'),
$urlEditVote,
__f('Poll results', 'Edit the line: %s', $name),
'glyphicon-pencil');
if ($canUseSMTP) {
$token = new Token();
$sessionService->set("Common", SESSION_EDIT_LINK_TOKEN, $token);
$smarty->assign('editedVoteUniqueId', $editedVoteUniqueId);
$smarty->assign('token', $token->getValue());
$smarty->assign('poll_id', $poll_id);
$message->includeTemplate = $smarty->fetch('part/form_remember_edit_link.tpl');
$smarty->clearAssign('token');
}
return $message;
}
// Retrieve data
if ($resultPubliclyVisible || $accessGranted) {
$slots = $pollService->allSlotsByPoll($poll);

View File

@ -2,7 +2,7 @@
{block name=main}
{if !empty($message)}
<div class="alert alert-dismissible alert-{$message->type|html}" role="alert">{$message->message|html}{if $message->link != null}<br/><a href="{$message->link}">{$message->link}</a>{/if}<button type="button" class="close" data-dismiss="alert" aria-label="{__('Generic', 'CLose')}"><span aria-hidden="true">&times;</span></button></div>
<div class="alert alert-dismissible alert-{$message->type|html}" role="alert">{$message->message|html}{if $message->link != null}<br/><a href="{$message->link}">{$message->link}</a>{/if}<button type="button" class="close" data-dismiss="alert" aria-label="{__('Generic', 'Close')}"><span aria-hidden="true">&times;</span></button></div>
{/if}
<form action="" method="post">
<div class="row">

View File

@ -0,0 +1,5 @@
<h1>{$poll->title|html|string_format:__('EditLink', 'Edit link for poll "%s"')}</h1>
<p>
{__('EditLink', 'Here is the link for editing your vote:')}
<a href="{poll_url id=$poll->admin_id vote_id=$editedVoteUniqueId}">{$poll->title|html}</a>
</p>

View File

@ -0,0 +1,57 @@
<div class="well">
<form action="action/send_edit_link_by_email_action.php" method="POST" class="form-inline" id="send_edit_link_form">
<p>{__('EditLink', "If you don't want to lose your personalized link, we can send it to your email.")}</p>
<input type="hidden" name="token" value="{$token}"/>
<input type="hidden" name="poll" value="{$poll_id}"/>
<input type="hidden" name="editedVoteUniqueId" value="{$editedVoteUniqueId}"/>
<div class="form-group">
<label for="email" class="control-label">{__('PollInfo', 'Email')}</label>
<input type="email" name="email" id="email" class="form-control" />
<input type="submit" id="send_edit_link_submit" value="{__('EditLink', 'Send')}" class="btn btn-success">
</div>
</form>
<div id="send_edit_link_alert"></div>
</div>
<script>
$(document).ready(function () {
var form = $('#send_edit_link_form');
form.submit(function(event) {
event.preventDefault();
if ($('#email').val()) {
//$('#send_edit_link_submit').attr("disabled", "disabled");
$.ajax({
type: 'POST',
url: form.attr('action'),
data: form.serialize(),
dataType: 'json',
success: function(data)
{
var newMessage;
if (data.result) {
$('#send_edit_link_form').remove();
newMessage = $('#genericUnclosableSuccessTemplate').clone();
} else {
newMessage = $('#genericErrorTemplate').clone();
}
newMessage
.find('.contents')
.text(data.message.message);
newMessage.removeClass('hidden');
$('#send_edit_link_alert')
.empty()
.append(newMessage);
},
complete: function() {
$('#add_comment').removeAttr("disabled");
}
});
}
return false;
});
});
</script>

View File

@ -128,7 +128,7 @@
}
<td class="hidden-print">
<a href="{if $admin}{poll_url id=$poll->admin_id vote_id=$vote->uniqId admin=true}{else}{poll_url id=$poll->id vote_id=$vote->uniqId}{/if}" class="btn btn-default btn-sm" title="{__('Poll results', 'Edit the line:')|html} {$vote->name|html}">
<a href="{if $admin}{poll_url id=$poll->admin_id vote_id=$vote->uniqId admin=true}{else}{poll_url id=$poll->id vote_id=$vote->uniqId}{/if}" class="btn btn-default btn-sm" title="{__f('Poll results', 'Edit the line: %s', $vote->name)|html}">
<i class="glyphicon glyphicon-pencil"></i><span class="sr-only">{__('Generic', 'Edit')}</span>
</a>
{if $admin}

View File

@ -177,7 +177,7 @@
)
}
<td class="hidden-print">
<a href="{if $admin}{poll_url id=$poll->admin_id vote_id=$vote->uniqId admin=true}{else}{poll_url id=$poll->id vote_id=$vote->uniqId}{/if}" class="btn btn-default btn-sm" title="{__('Poll results', 'Edit the line:')|escape} {$vote->name|html}">
<a href="{if $admin}{poll_url id=$poll->admin_id vote_id=$vote->uniqId admin=true}{else}{poll_url id=$poll->id vote_id=$vote->uniqId}{/if}" class="btn btn-default btn-sm" title="{__f('Poll results', 'Edit the line: %s', $vote->name)|html}">
<i class="glyphicon glyphicon-pencil"></i><span class="sr-only">{__('Generic', 'Edit')}</span>
</a>
{if $admin}

View File

@ -15,11 +15,28 @@
{* Messages *}
<div id="message-container">
{if !empty($message)}
<div class="alert alert-dismissible alert-{$message->type|html} hidden-print" role="alert">{$message->message|html}{if $message->link != null}<br/><a href="{$message->link}">{$message->link}</a>{/if}<button type="button" class="close" data-dismiss="alert" aria-label="{__('Generic', 'CLose')}"><span aria-hidden="true">&times;</span></button></div>
<div class="alert alert-dismissible alert-{$message->type|html} hidden-print" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="{__('Generic', 'Close')}"><span aria-hidden="true">&times;</span></button>
{$message->message|html}
{if $message->link != null}
<div class="input-group input-group-sm">
<span class="input-group-btn">
<a {if $message->linkTitle != null} title="{$message->linkTitle|escape}" {/if} class="btn btn-default btn-sm" href="{$message->link}">
{if $message->linkIcon != null}<i class="glyphicon glyphicon-pencil"></i>{if $message->linkTitle != null}<span class="sr-only">{$message->linkTitle|escape}</span>{/if}{/if}
</a>
</span>
<input type="text" aria-hidden="true" value="{$message->link}" class="form-control" readonly="readonly" >
</div>
{if $message->includeTemplate != null}
{$message->includeTemplate}
{/if}
{/if}
</div>
{/if}
</div>
<div id="nameErrorMessage" class="hidden alert alert-dismissible alert-danger hidden-print" role="alert">{__('Error', 'The name is invalid.')}<button type="button" class="close" data-dismiss="alert" aria-label="{__('Generic', 'CLose')}"><span aria-hidden="true">&times;</span></button></div>
<div id="genericErrorTemplate" class="hidden alert alert-dismissible alert-danger hidden-print" role="alert"><span class="contents"></span><button type="button" class="close" data-dismiss="alert" aria-label="{__('Generic', 'CLose')}"><span aria-hidden="true">&times;</span></button></div>
<div id="nameErrorMessage" class="hidden alert alert-dismissible alert-danger hidden-print" role="alert">{__('Error', 'The name is invalid.')}<button type="button" class="close" data-dismiss="alert" aria-label="{__('Generic', 'Close')}"><span aria-hidden="true">&times;</span></button></div>
<div id="genericErrorTemplate" class="hidden alert alert-dismissible alert-danger hidden-print" role="alert"><span class="contents"></span><button type="button" class="close" data-dismiss="alert" aria-label="{__('Generic', 'Close')}"><span aria-hidden="true">&times;</span></button></div>
<div id="genericUnclosableSuccessTemplate" class="hidden alert alert-success hidden-print" role="alert"><span class="contents"></span></div>
{if !$accessGranted && !$resultPubliclyVisible}