Cómo evitar el reenvío de formularios cuando se actualiza la página (F5 / CTRL + R)


132

Tengo un formulario simple que envía texto a mi tabla SQL. El problema es que después de que el usuario envía el texto, puede actualizar la página y los datos se envían nuevamente sin volver a llenar el formulario. Podría redirigir al usuario a otra página después de enviar el texto, pero quiero que los usuarios permanezcan en la misma página.

Recuerdo haber leído algo acerca de darle a cada usuario una identificación de sesión única y compararlo con otro valor que resolvió el problema que tengo, pero olvidé dónde está.



1
¿Por qué no desea redirigir al usuario a otra página?
Adam

@ Adam: Debido a que esto es excesivo para hacer otra solicitud al servidor, que a su vez recuperará algunos datos de DB nuevamente. Pero esto es una pérdida de recursos porque ya recuperamos todos los datos requeridos mientras procesamos la POSTsolicitud
Eugen Konkov

@EugenKonkov en el patrón PRG, simplemente redirigiría a una página que muestra un mensaje de éxito. No es necesario buscar más de DB.
Adam

@ Adam: También puede mostrar el registro completo que se crea mediante POSTdatos. En este caso, lo necesita SELECTdesde DB. Por ejemplo, cuando crea la factura, se le redirige a /invoices/53la pantalla que muestra la factura completa en lugar de solo 'éxito'
Eugen Konkov

Respuestas:


96

Use el patrón Publicar / Redirigir / Obtener. http://en.wikipedia.org/wiki/Post/Redirect/Get

Con mi sitio web, almacenaré un mensaje en una cookie o sesión, redirigiré después de la publicación, leeré la cookie / sesión y luego borraré el valor de esa sesión o variable de cookie.


1
¡Esto hace que Chrome no se pueda usar para el desarrollo donde las empresas solo requieren una publicación!
John Peters

26
Si usa el patrón PRG, ¿realmente abandona la página? ¿No era la pregunta cómo hacer que las cosas funcionen cuando no se redirige?
Adam

1
Me parece que si redirige a la misma página, se borra $ _POST. Según tengo entendido, ese fue el efecto deseado. Ese fue MI efecto deseado, de todos modos. Sin embargo, creo que la respuesta sería mejor si lo hiciera explícito.
donutguy640

Funciona bien, mi única recomendación es asegurarse de habilitar pruebas de error estrictas para que pueda detectar errores localmente durante el desarrollo, de lo contrario pueden pasar desapercibidos.
Maurice

No responde la pregunta. No estoy seguro de por qué es la respuesta aceptada.
YungGun

124

También me gustaría señalar que puede utilizar un enfoque de JavaScript, window.history.replaceStatepara evitar un reenvío al actualizar y el botón Atrás.

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

Prueba de concepto aquí: https://dtbaker.net/files/prevent-post-resubmit.php

Todavía recomendaría un enfoque Post / Redirect / Get, pero esta es una nueva solución JS.


1
Gracias, @dtbaker, es increíble :)
Gufran Hasan

1
TKS, este es el trabajo perfecto para mí, que sólo tuvo que crear la página 404 malentendido usuario a evitar
Tess Hsu

Simplemente asombroso. Gracias
user4906240

2
no funciona en safari, cambió href pero aún conserva los datos con la solicitud posterior para enviar
Vo Thanh Tung

Esto funcionó para mí, gracias! ¿Por qué recomienda el enfoque PRG?
iJassar

16

Realmente debería usar un patrón de obtención de redireccionamiento posterior para manejar esto, pero si de alguna manera terminó en una posición en la que PRG no es viable (por ejemplo, el formulario en sí está incluido, evitando redireccionamientos), puede modificar algunos de los parámetros de solicitud hacer una cadena basada en el contenido y luego verificar que aún no la haya enviado.

//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!
    }

Gracias por compartir, pero esto no parece funcionar. Actualizar la página de hecho vuelve a enviar el formulario. Tal vez me estoy perdiendo algo?
aLearner

Al actualizar la página, se volverá a enviar todo, pero elegimos solo procesar los datos de envío si son diferentes de los datos enviados la última vez (que almacenamos en la sesión var 'messageIdent'). ¿Está procesando el envío de su formulario dentro de la cláusula 'if messageIdents are different' (es decir, donde está 'do_your_thang ()')?
Moob

Gracias por su respuesta. Terminé simplemente implementando el patrón Post / Redirect / Get, por lo que ahora no recuerdo qué me estaba tropezando exactamente. Gracias de nuevo por dar la vuelta, sin embargo.
aLearner

Este método no está destinado a bloquear el reenvío ... sino a Detectar el reenvío para que pueda alterar su código para no "hacer" lo que haría si fuera un envío nuevo. En otras palabras: con este método puede detectar el envío "original" y colocar sus datos. Si NO es original, no coloque sus datos. Muestra una advertencia de "intento de engaño" o quizás muestra una "confirmación" en su lugar.
TheSatinKnight

Para que esto funcione, debe haber iniciado una sesión agregando session_start();al principio del archivo. w3schools.com/php/php_sessions.asp dice Nota: La función session_start () debe ser lo primero en su documento. Antes de cualquier etiqueta HTML.
wkille

16

Utilizo esta línea de JavaScript para bloquear la ventana emergente que solicita el reenvío del formulario al actualizar una vez que se envía el formulario.

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

Simplemente coloque esta línea al pie de página de su archivo y vea la magia


Sería una buena versión de esto que no se puede deshabilitar desde el lado del cliente, pero es corta, fácil, rápida ... y hace lo que debe hacer. Mantiene el historial para que el usuario pueda navegar hacia atrás, sin reenviar la publicación.
Péter Vértényi

16

Puede evitar el reenvío de formularios mediante una variable de sesión.

Primero debe configurar rand()en un cuadro de texto y $_SESSION['rand']en la página del formulario:

<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>

Después de eso, verifique $_SESSION['rand']con el $_POST['randcheck']valor del cuadro de texto como este:

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

3
podemos usar <input type="hidden" name="randcheck" id="randcheck" value="<?php echo microtime(); ?>" />en su lugar
Nikolay Bronskiy

Sí, podemos usar microtime () así como time () también en lugar de rand (), cualquiera que sea la función o variable que dé un valor diferente, podemos usarlo. PERO asegúrese de establecer ese valor en la variable SESIÓN. aquí la SESIÓN es imprescindible para verificar con el campo randcheck y para evitar volver a enviar el formulario.
Savoo

¿No debería borrarse la variable de sesión después de verificar si la sesión y los vars posteriores coinciden?
reemplazar el

1
@denoise Creo que la idea de @Savoo es que después $_POSTde los datos del formulario, la misma página se vuelve a cargar para restablecer la sesión y publicar las variables. Además, la creación de variables debe realizarse después de verificar si las variables coinciden. Por unsetlo tanto no es necesario.
HalfMens

13

Cuando se procesa el formulario, redirige a otra página:

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

También puede redirigir a la misma página.

si está haciendo algo como comentarios y desea que el usuario permanezca en la misma página, puede usar Ajax para manejar el envío del formulario


12

Encontré la próxima solución. Puede escapar de la redirección después de procesar la POSTsolicitud manipulandohistory objeto.

Entonces tienes el formulario HTML:

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

Cuando procesa este formulario en su servidor, en lugar de redirigir al usuario /the/result/pageconfigurando el Locationencabezado de esta manera:

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

ingrese la descripción de la imagen aquí

Después de procesar los POSTdatos ed, se vuelve pequeño <script>y el resultado/the/result/page

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

El <script>que debes renderizar:

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

El resultado es:

ingrese la descripción de la imagen aquí

como puede ver, los datos del formulario se POSTeditan al process.phpscript.
Este script procesa POSTdatos y renderiza /the/result/pagea la vez con:

  1. sin redireccionamiento
  2. no hay POSTdatos cuando actualiza la página (F5)
  3. no re POSTcuando navega a la página anterior / siguiente a través del historial del navegador

UPD

Como otra solución, solicito que la función solicite al equipo de Mozilla FireFox que permita a los usuarios configurar el NextPageencabezado que funcionará como Locationencabezado y crearápost/redirect/get obsoleto el patrón.

En breve. Cuando el servidor procesa los POSTdatos de forma satisfactoriamente:

  1. Configurar NextPageencabezado en lugar deLocation
  2. Representa el resultado del procesamiento de los POSTdatos del formulario tal como se representaría para la GETsolicitud en el post/redirect/getpatrón

El navegador a su vez cuando ve el NextPageencabezado:

  1. Ajustar window.locationconNextPage valor
  2. Cuando el usuario actualice la página, el navegador negociará la GETsolicitud en NextPagelugar de volver a POSTformar los datos.

Creo que esto sería excelente si se implementa, ¿no? =)


11
  1. Use el encabezado y redirija la página.

    header("Location:your_page.php"); Puede redirigir a la misma página o a una página diferente.

  2. Desarme $ _POST después de insertarlo en la base de datos.

    unset($_POST);


54
Desarmar $ _POST no afecta en absoluto el reenvío de formularios, al menos no en Chrome.
Gavin

2
No funcionará. Cuando el usuario vuelve a la página anterior, realiza la configuración de las variables POST una vez más.
Sandhu

¿Cuál es la razón para usar unset? Agregue alguna explicación a su respuesta para que otros puedan aprender de ella
Nico Haase

7

Una forma bastante segura es implementar una identificación única en la publicación y almacenarla en caché

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

Luego, en su código, haga esto:

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;
}

En realidad, acabo de escribir algo similar a esto, pero no tuve problemas para crear una contraseña, simplemente enumeré mis formularios (por ejemplo: pasos 1-5), así que si son iguales, estamos bien para seguir adelante, de lo contrario, no guarde en db ni envíe correos electrónicos. Pero deje que el usuario aterrice donde lo lleve.
Fernando Silva

1
Creo que esta es una mejor solución que volver a cargar la página, ya que muestro un mensaje cuando la publicación se realiza correctamente y la recarga de la página eliminará el mensaje. esto funciona bien para mí, también puedes usar un uniqid
Julio Popócatl

6

Este método me funciona bien, y creo que este es el más simple para hacer este trabajo.

La idea general es redirigir al usuario a otras páginas después del envío del formulario, lo que detendría el reenvío del formulario al actualizar la página. Aún así, si necesita mantener al usuario en la misma página después de enviar el formulario, puede hacerlo de varias maneras, pero aquí estoy describiendo el método de JavaScript.

Método Javascript

Este método es bastante fácil y bloquea la ventana emergente que solicita el reenvío del formulario al actualizar una vez que se envía el formulario. Simplemente coloque esta línea de código javascript en el pie de página de su archivo y vea la magia.

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


Esto también me ayuda
Ankit

4

Javascript

Este método es bastante fácil y bloquea la ventana emergente que solicita el reenvío del formulario al actualizar una vez que se envía el formulario. Simplemente coloque esta línea de código javascript en el pie de página de su archivo y vea la magia.

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

3

Simplemente rediríjalo a la misma página después de hacer uso de los datos del formulario, y funciona. Lo he intentado

header('location:yourpage.php');

44
Esto es solo repetir la misma respuesta que alguien más dio años antes. (De hecho, ¡otros dos!)
Nick Rice

Si duplica las respuestas de otras personas, al menos debería agregar alguna explicación a su respuesta para que sea interesante
Nico Haase

3

Una versión refinada de la publicación de Moob. Cree un hash de la POST, guárdelo como una cookie de sesión y compare los hash en cada sesión.

// 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

Básicamente, debe redirigir fuera de esa página, pero aún puede causar un problema mientras su Internet se ralentiza (encabezado de redireccionamiento desde el servidor)

Ejemplo de escenario básico:

Haga clic en el botón Enviar dos veces

Forma de resolver

  • Lado del cliente

    • Deshabilite el botón Enviar una vez que el cliente haga clic en él
    • Si usa Jquery: Jquery.one
    • Patrón PRG
  • Lado del servidor

    • Uso de marca de tiempo / marca de tiempo de hash basada en diferencia cuando se envió la solicitud.
    • Use tokens de solicitud. Cuando el principal se carga, asigne un token de solicitud temporal que si se repite se ignora.

2

Cómo evitar el reenvío de formularios PHP sin redireccionar. Si está utilizando $ _SESSION (después de session_start) y un formulario $ _POST, puede hacer algo como esto:

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

En su formulario html ponga esto:

<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;
    }
}
?>

Por lo tanto, cada vez que se envía el formulario, se genera un nuevo acto, se almacena en sesión y se compara con el acto posterior.

Ps: si está utilizando un formulario Get, puede cambiar fácilmente todas las POST con GET y también funciona.


1

Después de insertarlo en la base de datos, llame al método unset () para borrar los datos.

sin establecer ($ _ POST);

Para evitar la inserción de datos de actualización, realice una redirección de página a la misma página o a una página diferente después de insertar el registro.

header ('Ubicación:'. $ _ SERVER ['PHP_SELF']);


1
Agregue alguna explicación a su respuesta para que otros puedan aprender de ella. ¿Por qué se unsetnecesita esa llamada? ¿Qué pasaría si te lo saltaras?
Nico Haase

0

Usar la opción Publicar / Redirigir / Obtener patrón de Keverw es una buena idea. Sin embargo, no puede permanecer en su página (y creo que esto era lo que estaba pidiendo). Además, a veces puede fallar :

Si un usuario web se actualiza antes de que se complete el envío inicial debido al retraso del servidor, lo que resulta en una solicitud HTTP POST duplicada en ciertos agentes de usuario.

Otra opción sería almacenar en una sesión si el texto se debe escribir en su base de datos SQL de esta manera:

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

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

0

Como otros han dicho, no es posible dejar de usar post / redirect / get. Pero al mismo tiempo, es bastante fácil hacer lo que quieres hacer del lado del servidor.

En su página POST, simplemente valida la entrada del usuario, pero no actúa sobre ella, sino que la copia en una matriz SESSION. Luego redirige nuevamente a la página principal de envío. Su página principal de envío comienza verificando si la matriz SESSION que está utilizando existe, y si es así, cópiela en una matriz local y desarmela. Desde allí puedes actuar en consecuencia.

De esta manera, solo haces todo tu trabajo principal una vez, logrando lo que quieres hacer.


0

Luego busqué una solución para evitar el reenvío en un gran proyecto. El código funciona altamente con $ _GET y $ _POST y no puedo cambiar el comportamiento de los elementos de formulario sin el riesgo de errores imprevistos. Entonces, aquí está mi código:

<!-- 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"] );

Solo funciona para evitar el reenvío de $ _POST, no $ _GET. Pero este es el comportamiento que necesito. ¡El problema de reenvío no funciona con la carga de archivos!


0

Lo que funciona para mí es:

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;
}

?>

Y en tu forma

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



 </form>

0

Este form.phpejemplo muestra cómo usar PRG correcto (cuando el formulario es válido o no).

  • Redirige a la misma página, solo cuando el formulario es válido y se realizó una acción.
  • La redirección protege el formulario para que no se vuelva a enviar al actualizar la página.
  • Utiliza la sesión para no perder los mensajes de éxito que desea mostrar cuando el formulario es válido.
  • Hay dos botones para probar: "Envío válido", "Envío no válido". Pruebe ambos y actualice la página después de eso.
<?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();
}

Agregue alguna explicación a su respuesta para que otros puedan aprender de ella. ¿De dónde $everything_fineviene?
Nico Haase

-3

¿Por qué no simplemente usar la $_POST['submit']variable como una declaración lógica para guardar lo que esté en el formulario? Siempre puede redirigir a la misma página (en caso de que se actualicen, y cuando lleguen go backal navegador, la variable de publicación de envío ya no se establecerá. Solo asegúrese de que su botón de envío tenga una namey idde submit.


1
Echa un vistazo a en.wikipedia.org/wiki/Post/Redirect/Get ! Este es el enfoque correcto
enero 267
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.