Как предотвратить повторную отправку формы при обновлении страницы (F5 / CTRL + R)


134

У меня есть простая форма, которая отправляет текст в мою таблицу SQL. Проблема в том, что после того, как пользователь отправит текст, он может обновить страницу, и данные будут отправлены снова без повторного заполнения формы. Я мог бы перенаправить пользователя на другую страницу после отправки текста, но я хочу, чтобы пользователи оставались на той же странице.

Я помню, как читал что-то о предоставлении каждому пользователю уникального идентификатора сеанса и сравнении его с другим значением, которое решило мою проблему, но я забыл, где он находится.



1
Почему вы не хотите перенаправлять пользователя на другую страницу?
Adam

@Adam: Потому что это лишнее, чтобы сделать еще один запрос к серверу, который, в свою очередь, снова получит некоторые данные из БД. Но это POST
Евгений Коньков

@EugenKonkov в шаблоне PRG вы просто перенаправляете на страницу, которая показывает сообщение об успехе. Дальнейшая загрузка из БД не требуется.
Adam

@Adam: Вы также можете отобразить всю запись, созданную с помощью POSTданных. В этом случае вам нужно SELECTэто из БД. Например, когда вы создаете счет-фактуру, вы будете перенаправлены на /invoices/53
Евгений Коньков

Ответы:


96

Используйте шаблон Post / Redirect / Get. http://en.wikipedia.org/wiki/Post/Redirect/Get

На своем веб-сайте я буду хранить сообщение в файле cookie или сеансе, перенаправлять после публикации, читать cookie / сеанс, а затем очищать значение этого сеанса или переменной cookie.


1
Это делает хром непригодным для разработки, где бизнесу требуется только сообщение!
Джон Питерс

26
Если вы используете шаблон PRG, то вы действительно уходите со страницы? Не был вопрос, как заставить все работать без перенаправления?
Adam

1
Мне кажется, что если перенаправить на ту же страницу, то $ _POST очищается. Насколько я понимаю, это был желаемый эффект. Во всяком случае, это был МОЙ желаемый эффект. Я думаю, что ответ был бы лучше, если бы он был ясным.
donutguy640

Работает хорошо, моя единственная рекомендация - убедиться, что вы включили строгое тестирование ошибок, чтобы вы обнаруживали ошибки локально во время разработки, иначе они могут остаться незамеченными.
Морис

Не отвечает на вопрос. Не уверен, почему это принятый ответ.
YungGun

125

Я также хотел бы отметить, что вы можете использовать подход javascript, window.history.replaceStateчтобы предотвратить повторную отправку при нажатии кнопки обновления и возврата.

<script>
    if ( window.history.replaceState ) {
        window.history.replaceState( null, null, window.location.href );
    }
</script>

Доказательство концепции здесь: https://dtbaker.net/files/prevent-post-resubmit.php

Я бы по-прежнему рекомендовал подход Post / Redirect / Get, но это новое JS-решение.


1
Спасибо, @dtbaker, это круто :)
Гуфран Хасан

1
Спасибо, это идеальная работа для меня, просто нужно было создать страницу 404, чтобы избежать недоразумений со стороны пользователя
tess hsu

Просто удивительно. Thankyou
user4906240

2
он не работает в сафари, он изменил href, но все еще сохраняет данные с почтовым запросом для отправки
Vo Thanh Tung

Это сработало для меня, спасибо! Почему вы рекомендуете подход PRG?
iJassar

16

Вам действительно следует использовать шаблон Post Redirect Get для обработки этого, но если вы каким-то образом оказались в положении, в котором PRG нежизнеспособен (например, сама форма находится во включении, предотвращая перенаправления), вы можете хешировать некоторые параметры запроса чтобы создать строку на основе содержимого, а затем проверить, что вы ее еще не отправили.

//create digest of the form submission:

    $messageIdent = md5($_POST['name'] . $_POST['email'] . $_POST['phone'] . $_POST['comment']);

//and check it against the stored value:

    $sessionMessageIdent = isset($_SESSION['messageIdent'])?$_SESSION['messageIdent']:'';

    if($messageIdent!=$sessionMessageIdent){//if its different:          
        //save the session var:
            $_SESSION['messageIdent'] = $messageIdent;
        //and...
            do_your_thang();
    } else {
        //you've sent this already!
    }

Спасибо, что поделились, но, похоже, это не работает. При обновлении страницы я действительно повторно отправляю форму. Может я чего-то упускаю?
aLearner 03

Обновление страницы приведет к повторной отправке всего, но мы выбираем только обработку данных отправки, если они отличаются от данных, отправленных в прошлый раз (которые мы храним в сеансе var 'messageIdent'). Вы обрабатываете отправку формы в рамках предложения if messageIdent are different (т.е. где находится do_your_thang ())?
Moob

Спасибо за ваш ответ. В итоге я просто реализовал шаблон Post / Redirect / Get, так что теперь я не помню, что именно меня сбивало с толку. Тем не менее, еще раз спасибо, что вернулись.
aLearner

Этот метод не предназначен для блокировки повторной отправки ... но для обнаружения повторной отправки, чтобы вы могли изменить свой код, чтобы не «делать» то, что вы бы сделали, если бы это была новая отправка. Другими словами: с помощью этого метода вы можете обнаружить «исходное» представление и разместить свои данные. Если это НЕ оригинал, не размещайте свои данные. Либо покажите предупреждение «попытка обмана», либо, возможно, покажите вместо этого «подтверждение».
TheSatinKnight

Чтобы это сработало, вы должны начать сеанс, добавив его session_start();в начало файла. w3schools.com/php/php_sessions.asp говорит : Примечание. Функция session_start () должна быть самой первой в вашем документе. Перед любыми HTML-тегами.
wkille

16

Я использую эту строку javascript, чтобы заблокировать всплывающее окно с запросом на повторную отправку формы при обновлении после отправки формы.

if ( window.history.replaceState ) {
  window.history.replaceState( null, null, window.location.href );
}

Просто поместите эту строку в нижний колонтитул вашего файла и увидите волшебство


Было бы неплохо иметь версию, которую нельзя отключить на стороне клиента, но она короткая, простая, быстрая ... и делает то, что нужно. Сохраняет историю, чтобы пользователь мог вернуться назад, не отправляя сообщение повторно.
Péter Vértényi

16

Вы можете предотвратить повторную отправку формы с помощью переменной сеанса.

Сначала вы должны установить rand()в текстовом поле и $_SESSION['rand']на странице формы:

<form action="" method="post">
  <?php
   $rand=rand();
   $_SESSION['rand']=$rand;
  ?>
 <input type="hidden" value="<?php echo $rand; ?>" name="randcheck" />
   Your Form's Other Field 
 <input type="submit" name="submitbtn" value="submit" />
</form>

После этого проверьте значение $_SESSION['rand']текстового поля $_POST['randcheck']следующим образом:

if(isset($_POST['submitbtn']) && $_POST['randcheck']==$_SESSION['rand'])
{
    // Your code here
}

3
мы можем использовать <input type="hidden" name="randcheck" id="randcheck" value="<?php echo microtime(); ?>" />вместо
Николай Бронский

Да, мы можем использовать microtime (), а также time () вместо rand (), любую функцию или переменную, которая дает другое значение, которое мы можем использовать. НО убедитесь, что вы установили это значение для переменной SESSION. здесь SESSION необходимо проверить с помощью поля randcheck и предотвратить повторную отправку формы.
Savoo

не следует ли очищать переменную сеанса после проверки совпадения переменных сеанса и сообщения?
DeNoise

1
@denoise Я считаю, что идея @Savoo заключается в том, что после $_POSTданных формы та же страница перезагружается, повторно устанавливая переменные сеанса и публикации. Также создание переменных должно происходить после проверки совпадения переменных. Поэтому unsetне нужен.
Hmerman6006,

13

Когда форма обрабатывается, вы перенаправляетесь на другую страницу:

... process complete....
header('Location: thankyou.php');

вы также можете перенаправить на ту же страницу.

если вы делаете что-то вроде комментариев и хотите, чтобы пользователь оставался на той же странице, вы можете использовать Ajax для обработки отправки формы


12

Я нашел следующий обходной путь. Вы можете избежать перенаправления после обработки POSTзапроса, манипулируя historyобъектом.

Итак, у вас есть HTML-форма:

<form method=POST action='/process.php'>
 <input type=submit value=OK>
</form>

Когда вы обрабатываете эту форму на своем сервере, вы вместо того, чтобы перенаправлять пользователя /the/result/page, настраивая Locationзаголовок следующим образом:

$cat process.php
<?php 
     process POST data here
     ... 
     header('Location: /the/result/page');
     exit();
?>

введите описание изображения здесь

После обработки POSTредактируемых данных вы визуализируете небольшой размер, <script>и результат/the/result/page

<?php 
     process POST data here
     render the <script>         // see below
     render `/the/result/page`   // OK
?>

<script>Вы должны оказывать:

<script>
    window.onload = function() {
        history.replaceState("", "", "/the/result/page");
    }
</script>

Результат:

введите описание изображения здесь

как вы можете видеть, данные формы POSTредактируются в process.phpскрипт.
Этот скрипт обрабатывает POSTданные и рендеринг /the/result/pageсразу с помощью:

  1. нет перенаправления
  2. нет POSTданных при обновлении страницы (F5)
  3. нет re POSTпри переходе на предыдущую / следующую страницу через историю браузера

UPD

В качестве другого решения я прошу команду Mozilla FireFox запросить функцию, чтобы пользователи могли настроить заголовок, который будет работать как заголовок и сделать шаблон устаревшим.NextPageLocationpost/redirect/get

Коротко. Когда сервер обрабатывает POSTданные формы успешно, он:

  1. NextPageЗаголовок установки вместоLocation
  2. Отображать результат обработки POSTданных формы, как он будет отображаться для GETзапроса в post/redirect/getшаблоне

Браузер, в свою очередь, увидит NextPageзаголовок:

  1. Отрегулировать window.locationсо NextPageзначением
  2. Когда пользователь обновляет страницу, браузер будет согласовывать GETзапрос NextPageвместо преобразования POSTданных.

Я думаю, это было бы здорово, если бы это было реализовано, не так ли? =)


11
  1. Используйте заголовок и перенаправьте страницу.

    header("Location:your_page.php"); Вы можете перенаправить на ту же или другую страницу.

  2. Отключите $ _POST после его вставки в базу данных.

    unset($_POST);


54
Отмена установки $ _POST вообще не влияет на повторную отправку формы, по крайней мере, не в Chrome.
Gavin

2
Он не будет работать. Когда пользователь возвращается на предыдущую страницу, он еще раз выполняет настройку переменных POST.
Sandhu

В чем причина использования unset? Пожалуйста, добавьте пояснения к своему ответу, чтобы другие могли извлечь из него уроки
Нико Хаасе,

7

Самый верный способ - добавить уникальный идентификатор в сообщение и кэшировать его в

<input type='hidden' name='post_id' value='".createPassword(64)."'>

Затем в вашем коде сделайте это:

if( ($_SESSION['post_id'] != $_POST['post_id']) )
{
    $_SESSION['post_id'] = $_POST['post_id'];
    //do post stuff
} else {
    //normal display
}

function createPassword($length)
{
    $chars = "abcdefghijkmnopqrstuvwxyz023456789";
    srand((double)microtime()*1000000);
    $i = 0;
    $pass = '' ;

    while ($i <= ($length - 1)) {
        $num = rand() % 33;
        $tmp = substr($chars, $num, 1);
        $pass = $pass . $tmp;
        $i++;
    }
    return $pass;
}

На самом деле я просто написал что-то похожее на это, но не стал создавать пароль, я просто перечислил свои формы (например: шаги 1-5), поэтому, если они равны, мы можем двигаться дальше, иначе не сохраняйте в БД и не отправляйте электронные письма. Но позвольте пользователю приземлиться, куда бы он ни пошел.
Фернандо Силва

1
Я думаю, что это лучшее решение, чем перезагрузка страницы, поскольку я показываю сообщение, когда публикация успешна, и перезагрузка страницы удалит сообщение. это работает для меня, вы также можете использовать uniqid
Хулио Попокатль

7

Этот метод мне подходит, и я думаю, что это самый простой способ выполнить эту работу.

Общая идея состоит в том, чтобы перенаправить пользователя на некоторые другие страницы после отправки формы, что остановит повторную отправку формы при обновлении страницы. Тем не менее, если вам нужно удерживать пользователя на той же странице после отправки формы, вы можете сделать это несколькими способами, но здесь я описываю метод javascript.

Метод Javascript

Этот метод довольно прост и блокирует всплывающее окно с запросом на повторную отправку формы при обновлении после отправки формы. Просто поместите эту строку кода javascript в нижний колонтитул вашего файла и увидите волшебство.

<script>
if ( window.history.replaceState ) {
  window.history.replaceState( null, null, window.location.href );
}
</script>


Это тоже помогает мне
Ankit

4

Javascript

Этот метод довольно прост и блокирует всплывающее окно с запросом на повторную отправку формы при обновлении после отправки формы. Просто поместите эту строку кода javascript в нижний колонтитул вашего файла и увидите волшебство.

<script>
    if ( window.history.replaceState ) {
        window.history.replaceState( null, null, window.location.href );
    }
</script>

3

Просто перенаправьте его на ту же страницу после использования данных формы, и он работает. Я пробовал.

header('location:yourpage.php');

4
Это просто повторение того же ответа, который дал кто-то несколько лет назад. (На самом деле двое других!)
Ник Райс

Если вы дублируете ответы других людей, вы должны хотя бы добавить некоторые пояснения к своему ответу, чтобы сделать его интересным,
Нико Хаасе,

3

Доработанная версия поста Моба. Создайте хэш POST, сохраните его как файл cookie сеанса и сравнивайте хеши в каждом сеансе.

// Optionally Disable browser caching on "Back"
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
header( 'Expires: Sun, 1 Jan 2000 12:00:00 GMT' );
header( 'Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT' );

$post_hash = md5( json_encode( $_POST ) );

if( session_start() )
{
    $post_resubmitted = isset( $_SESSION[ 'post_hash' ] ) && $_SESSION[ 'post_hash' ] == $post_hash;
    $_SESSION[ 'post_hash' ] = $post_hash;
    session_write_close();
}
else
{
    $post_resubmitted = false;
}

if ( $post_resubmitted ) {
  // POST was resubmitted
}
else
{
  // POST was submitted normally
}

2

По сути, вам нужно перенаправить с этой страницы, но это все еще может создать проблему, пока ваш интернет медленный (заголовок перенаправления со стороны сервера)

Пример базового сценария:

Дважды нажмите кнопку отправки

Способ решения

  • Сторона клиента

    • Отключить кнопку отправки, как только клиент нажмет на нее
    • Если вы используете JQuery: Jquery.one
    • Шаблон PRG
  • Сторона сервера

    • Использование дифференцированной метки времени / метки времени на основе хеширования при отправке запроса.
    • Используйте жетоны запросов. Когда основной загружается, назначьте временный токен запроса, который при повторении игнорируется.

2

Как предотвратить повторную отправку формы php без перенаправления. Если вы используете $ _SESSION (после session_start) и форму $ _POST, вы можете сделать что-то вроде этого:

if ( !empty($_SESSION['act']) && !empty($_POST['act']) && $_POST['act'] == $_SESSION['act'] ) {
  // do your stuff, save data into database, etc
}

В вашей html-форме поместите это:

<input type="hidden" id="act" name="act" value="<?php echo ( empty($_POST['act']) || $_POST['act']==2 )? 1 : 2; ?>">
<?php
if ( $_POST['act'] == $_SESSION['act'] ){
    if ( empty( $_SESSION['act'] ) || $_SESSION['act'] == 2 ){
        $_SESSION['act'] = 1;
    } else {
        $_SESSION['act'] = 2;
    }
}
?>

Таким образом, каждый раз, когда форма отправляется, создается новый акт, который сохраняется в сеансе и сравнивается с почтовым актом.

Ps: если вы используете форму Get, вы можете легко изменить все POST с помощью GET, и это тоже работает.


1

После вставки в базу данных вызовите метод unset () для очистки данных.

снята с охраны ($ _ POST);

Чтобы предотвратить вставку обновленных данных, выполните перенаправление страницы на ту же или другую страницу после вставки записи.

Заголовок ( 'Location:' $ _ SERVER [ 'PHP_SELF'].);


1
Пожалуйста, добавьте пояснение к своему ответу, чтобы другие могли извлечь из него уроки - зачем unsetнужен этот звонок? Что будет, если вы его пропустите?
Нико Хаасе,

0

Использование шаблона Post / Redirect / Get из ответа Keverw - хорошая идея. Однако вы не можете оставаться на своей странице (и я думаю, это было то, о чем вы просили?) Кроме того, иногда это может дать сбой :

Если веб-пользователь обновляется до завершения первоначальной отправки из-за задержки сервера, что приводит к дублированию запроса HTTP POST в определенных пользовательских агентах.

Другой вариант - сохранить в сеансе, если текст должен быть записан в вашу базу данных SQL следующим образом:

if($_SERVER['REQUEST_METHOD'] != 'POST')
{
  $_SESSION['writeSQL'] = true;
}
else
{
  if(isset($_SESSION['writeSQL']) && $_SESSION['writeSQL'])
  {
    $_SESSION['writeSQL'] = false;

    /* save $_POST values into SQL */
  }
}

0

Как говорили другие, невозможно отказаться от использования post / redirect / get. Но в то же время довольно легко делать то, что вы хотите, на стороне сервера.

На странице POST вы просто проверяете ввод пользователя, но не выполняете никаких действий, вместо этого вы копируете его в массив SESSION. Затем вы снова перенаправляете обратно на главную страницу отправки. Ваша основная страница отправки начинается с проверки, существует ли массив SESSION, который вы используете, и, если да, скопируйте его в локальный массив и отключите его. Оттуда вы можете действовать.

Таким образом, вы выполняете всю свою основную работу только один раз, достигая того, чего хотите.


0

После этого я искал решение, чтобы предотвратить повторную отправку в огромный проект. Код отлично работает с $ _GET и $ _POST, и я не могу изменить поведение элементов формы без риска непредвиденных ошибок. Итак, вот мой код:

<!-- language: lang-php -->
<?php

// Very top of your code:

// Start session:
session_start();

// If Post Form Data send and no File Upload
if ( empty( $_FILES ) && ! empty( $_POST ) ) {
    // Store Post Form Data in Session Variable
    $_SESSION["POST"] = $_POST;
    // Reload Page if there were no outputs
    if ( ! headers_sent() ) {
        // Build URL to reload with GET Parameters
        // Change https to http if your site has no ssl
        $location = "https://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
        // Reload Page
        header( "location: " . $location, true, 303 );
        // Stop any further progress
        die();
    }
}

// Rebuilt POST Form Data from Session Variable
if ( isset( $_SESSION["POST"] ) ) {
    $_POST = $_SESSION["POST"];
    // Tell PHP that POST is sent
    $_SERVER['REQUEST_METHOD'] = 'POST';
}

// Your code:
?><html>
    <head>
        <title>GET/POST Resubmit</title>
    </head>
    <body>

    <h1>Forms:</h1>
    <h2>GET Form:</h2>
    <form action="index.php" method="get">
        <input type="text" id="text_get" value="test text get" name="text_get"/>
        <input type="submit" value="submit">
    </form>
    <h2>POST Form:</h2>
    <form action="index.php" method="post">
        <input type="text" id="text_post" value="test text post" name="text_post"/>
        <input type="submit" value="submit">
    </form>
    <h2>POST Form with GET action:</h2>
    <form action="index.php?text_get2=getwithpost" method="post">
        <input type="text" id="text_post2" value="test text get post" name="text_post2"/>
        <input type="submit" value="submit">
    </form>
    <h2>File Upload Form:</h2>
    <form action="index.php" method="post" enctype="multipart/form-data">
        <input type="file" id="file" name="file">
        <input type="submit" value="submit">
    </form>

    <h1>Results:</h1>
    <h2>GET Form Result:</h2>
    <p>text_get: <?php echo $_GET["text_get"]; ?></p>
    <h2>POST Form Result:</h2>
    <p>text_post: <?php echo $_POST["text_post"]; ?></p>
    <h2>POST Form with GET Result:</h2>
    <p>text_get2: <?php echo $_GET["text_get2"]; ?></p>
    <p>text_post2: <?php echo $_POST["text_post2"]; ?></p>
    <h2>File Upload:</h2>
    <p>file:
    <pre><?php if ( ! empty( $_FILES ) ) {
            echo print_r( $_FILES, true );
        } ?></pre>
    </p>
    <p></p>
    </body>
    </html><?php
// Very Bottom of your code:
// Kill Post Form Data Session Variable, so User can reload the Page without sending post data twice
unset( $_SESSION["POST"] );

Это работает только для предотвращения повторной отправки $ _POST, а не $ _GET. Но это то поведение, которое мне нужно. Проблема с повторной отправкой не работает с загрузкой файлов!


0

Что мне подходит:

if ( !refreshed()) {
   //Your Submit Here
        if (isset( $_GET['refresh'])) {
            setcookie("refresh",$_GET['refresh'], time() + (86400 * 5), "/");
        }

    }    
}


function refreshed()
{
    if (isset($_GET['refresh'])) {
        $token = $_GET['refresh'];
        if (isset($_COOKIE['refresh'])) {
            if ($_COOKIE['refresh'] != $token) {
                return false;
            } else {
                return true;
            }
        } else {
            return false;
        }
    } else {
        return false;
    }
}  


function createToken($length) {
    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $charactersLength = strlen($characters);
    $randomString = '';
    for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[rand(0, $charactersLength - 1)];
    }
    return $randomString;
}

?>

И в твоей форме

 <form  action="?refresh=<?php echo createToken(3)?>">



 </form>

0

В этом form.phpпримере показано, как правильно использовать PRG (когда форма действительна или нет).

  • Он перенаправляет на ту же страницу, только когда форма действительна и действие было выполнено.
  • Перенаправление защищает форму от повторной отправки при обновлении страницы.
  • Он использует сеанс, чтобы не терять сообщения об успехе, которые вы хотите показать, когда форма действительна.
  • Есть две кнопки для тестирования: «Допустимая отправка», «Недействительная отправка». Попробуйте оба варианта и после этого обновите страницу.
<?php
session_start();

function doSelfRedirect()
{
  header('Location:'.$_SERVER['PHP_SELF']);
  exit;
}

function setFlashMessage($msg)
{
  $_SESSION['message'] = $msg;
}

function getFlashMessage()
{
  if (!empty($_SESSION['message'])) {
    $msg = $_SESSION['message'];
    unset($_SESSION['message']);
  } else {
    $msg = null;
  }

  return $msg;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  // Validation primitive example.
  if (empty($_POST['valid'])) {
    $formIsValid = false;
    setFlashMessage('Invalid form submit');
  } else {
    $formIsValid = true;
  }

  if ($formIsValid) {
    // Perform any actions here.
    // ...

    // Cool!
    setFlashMessage('Form is valid. Action performed.');

    // Prevent form resubmission.
    doSelfRedirect();
  }
}
?>
<h1>Hello form</h1>

<?php if ($msg = getFlashMessage()): ?>
  <div><?= $msg ?></div>
<?php endif; ?>

<form method="post">
  <input type="text" name="foo" value="bar"><br><br>
  <button type="submit" name="invalid" value="0">Invalid submit</button>
  <button type="submit" name="valid" value="1">Valid submit</button>
</form>

-2
if (($_SERVER['REQUEST_METHOD'] == 'POST') and (isset($_SESSION['uniq']))){
    if($everything_fine){
        unset($_SESSION['uniq']);
    }
}
else{
    $_SESSION['uniq'] = uniqid();
}

Пожалуйста, добавьте пояснения к своему ответу, чтобы другие могли извлечь из него уроки - откуда $everything_fineвзялось?
Нико Хаасе,

-3

Почему бы просто не использовать $_POST['submit']переменную как логический оператор, чтобы сохранить все, что находится в форме. Вы всегда можете перенаправлять на ту же страницу (в случае , если они освежат, и когда они попали go backв браузере, то отправить сообщение переменный не будет установлены больше. Просто убедитесь , что кнопка отправить имеет nameи idиз submit.


1
Взгляните на en.wikipedia.org/wiki/Post/Redirect/Get ! Это правильный подход
jan267 05
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.