Registro de lista de correo electrónico PHP
Vea mi publicación original: aquí .
Tengo una página html y cuatro archivos php que permiten a los usuarios registrarse para obtener una lista de correo electrónico. Uno de los scripts php es un cronjob que elimina las filas no verificadas de más de 24 horas, y no se incluye a continuación por el bien de la duración de la publicación. Utilizo PDO para mis declaraciones preparadas. Todo ha sido probado en vivo y es completamente funcional según mi leal saber y entender. Todos y cada uno de los comentarios son bienvenidos. He incluido algunas preguntas debajo de los fragmentos. :)
email.html --- Los usuarios se registran aquí
<form action="signup.php" method="POST" autocomplete="off">
<input type="text" autocomplete="off" placeholder="Email address" name="email" required>
<br/>
<input type="submit" autocomplete="off" value="Subscribe">
</form>
signup.php --- Filtra y envía la entrada del usuario a la base de datos
<?php
//1---DATABASE CONNECTION---
$dbHost = "HOST"; $dbName = "DATABASE";
$dbUser = "USER"; $dbPassword = "PASSWORD";
$port = "PORT"; $charset = 'utf8mb4';
$options = [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, \PDO::ATTR_EMULATE_PREPARES => false, ]; $dsn = "mysql:host=$dbHost;dbname=$dbName;charset=$charset;port=$port";
try {
$pdo = new \PDO($dsn, $dbUser, $dbPassword, $options); } catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
//1---END---
//2---Add to table: IPv4 ADDRESS, EMAIL, DATETIME, and ACODE---
//prevent direct url access of .php from users, routes to starting page
if(($_SERVER['REQUEST_METHOD'] == 'POST') == NULL) { header("Location: email.html"); exit (0); } //trim spaces on ends of user email input $Temail = trim($_POST['email']); //(on mobile, auto-complete often leaves a space at the end) //allow international characters if(preg_match("/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$^/", $Temail)) { //prevents invalid email addresses header("Location: invalid.html"); exit (0); } //Check Email Domain MX Record $email_host = strtolower(substr(strrchr($Temail, "@"), 1)); if (!checkdnsrr($email_host, "MX")) {
header("Location: invalid.html");
exit (0);
}
//Prevent users from inputting a specific domain...like mine
$notallowed = [ 'mydomain.com', ]; if (!in_array($email_host, $notallowed) == NULL) { header("Location: notallowed.html"); exit (0); } //checks database to make sure the email is not a duplicate $stmt1 = $pdo->prepare("SELECT email FROM emailTable WHERE email = ?"); $stmt1->execute([$Temail]); if($stmt1->fetch()) { //prevents adding a duplicate email
header("Location: duplicate.html");
exit (0);
}
//send verification email using seperate php file
include_once 'vEmail.php';
//check to see if email could be put together
if(include_once 'vEmail' == NULL) {
header("Location: failure.html");
exit (0);
}
//set date and time
date_default_timezone_set('America/Los_Angeles');
$dateTime = date('Ymd-His', strtotime('NOW')); // ('Ymd-His' format and LA timezone are preferred) //variable to store ipv4 address $euserIP4 = $_SERVER['REMOTE_ADDR']; //add all data to the database $stmt2 = $pdo->prepare("INSERT INTO emailTable (IP4, datetime, email, acode) VALUES (:IP4, :datetime, :email, :acode)"); $stmt2->execute(['IP4' => $euserIP4, 'datetime' => $dateTime, 'email' => $Temail, 'acode' => $Acode]);
header("Location: success.html");
exit (0);
//2---END---
?>
vEmail.php ---include_onceen signup.php, envía un correo electrónico de verificación
<?php
//generate verification code
$Acode = bin2hex(random_bytes(30)); //send verification email w/ code $emailbody =
"<html>
<body>
<table>
<tr>
<td>
<button><a href='https://www.MYDOMAIN.com/status/verify.php?acode=$Acode'>VERIFY</a></button> </td> </tr> </table> </body> </html>"; $headers = "Reply-To: MY NAME <[email protected]>\r\n";
$headers .= "Return-Path: MY NAME <[email protected]>\r\n"; $headers .= "From: MY NAME <[email protected]>\r\n";
$headers .= "MIME-Version: 1.0\r\n"; $headers .= "Content-type: text/html; charset=UTF-8\r\n";
$headers .= "X-Priority: 3\r\n"; //send email mail($Temail, 'Confirm Your Email Subscription', $emailbody, $headers, '-f ' . '[email protected]');
?>
verify.php --- Adjunto al enlace que se envió en el correo electrónico de verificación
<?php
//1---DATABASE CONNECTION---
$vHost = ""; $vName = "";
$vUser = ""; $vPassword = "";
$vPort = ""; $vCharset = "";
$vOptions = [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, \PDO::ATTR_EMULATE_PREPARES => false, ]; $vdsn = "mysql:host=$vHost;dbname=$vName;charset=$vCharset;port=$vPort";
try {
$vpdo = new \PDO($vdsn, $vUser, $vPassword, $vOptions); } catch (\PDOException $ve) {
throw new \PDOException($ve->getMessage(), (int)$ve->getCode());
}
//1---END---
//2---VERIFICATION LINK---
//set timezone
date_default_timezone_set('America/Los_Angeles');
//prevent direct url access of .php from users, routes to starting page
if(isset($_GET['acode']) == NULL) { header("Location: email.html"); exit (0); } //set verification code variable $vAcode = $_GET['acode']; //check if row still exists $vStmt1 = $vpdo->prepare("SELECT verified, acode FROM emailTable WHERE acode = '$vAcode' LIMIT 1");
$vStmt1->execute(); if($vStmt1->rowCount() == NULL) {
//EXPIRED
header("Location: expired1.html");
exit (0);
}
//check if row is verified ('verified' set to 0)
$vStmt2 = $vpdo->prepare("SELECT verified, acode FROM emailTable WHERE verified = 0 AND acode = '$vAcode' LIMIT 1"); $vStmt2->execute();
if($vStmt2->rowCount() == NULL) { //if 'verified' is set to 1 already header("Location: expired2.html"); exit (0); } //since 'verified' is set to 0,update verification status to 1 $vStmt3 = $vpdo->prepare("UPDATE emailTable SET verified = 1 WHERE acode = '$vAcode' LIMIT 1");
$vStmt3->execute(); //check if the 'verified' was updated correctly if($vStmt3->fetch()) {
header("Location: failure.html");
exit (0);
}
//SUCCESS
header("Location: verified.html");
exit (0);
//2---END---
?>
Preguntas y comentarios:
- He incluido conexiones de base de datos para cada archivo php, pero descubrí que algunos prefieren tener un archivo de configuración global para sus conexiones. ¿Porqué es eso? ¿Es más eficiente?
- En la publicación original, alguien mencionó que a la expresión regular que se encuentra en signup.php le falta la bandera Unicode. ¿Alguien podría explicar esto, porque no pude encontrar nada en él?
- Si bien el almacenamiento de IPv4 funciona bien, no he podido averiguar cómo almacenar correctamente IPv6 (hasta donde yo sé). Probé esto:
bin2hex(inet_pton($_SERVER['REMOTE_ADDR']));pero no pude averiguar si el resultado era correcto, porque no se parecía en nada a una dirección ipv6. Corrígeme si esto parece utilizable. - Estoy buscando en PHPMailer en lugar de usar la función nativa mail (). En el caso de los scripts anteriores, ¿se recomendaría esto o es más para el envío masivo de correos electrónicos?
Edición 1:
Me acabo de dar cuenta de que la validación de correo electrónico que se encuentra en signup.phppermite una dirección de correo electrónico con espacios (es decir: ohnothere are [email protected]). La versión anterior del script, que se encuentra en el enlace en la parte superior de la publicación, impedía este tipo de direcciones. ¿Alguna idea de por qué puede ser eso, o cómo prevenir eso? Me gustaría mantenerme alejado de filter_var (FILTER_VALIDATE_EMAIL) ya que cancela los caracteres internacionales. Seguiremos experimentando con él ...
Respuestas
Incluir un archivo de configuración global no se trata de eficiencia, solo se trata de centralizar los datos de configuración de la aplicación.
Suponga, por ejemplo, que está desarrollando una nueva función para su aplicación, desea utilizar una base de datos específicamente para este propósito, para no interferir con la base de datos de producción.
Si incluye la configuración de la base de datos en cada secuencia de comandos, tendrá que asegurarse de verificar (y con suerte volver a verificar) cada secuencia de comandos para asegurarse de que ninguna de sus acciones de desarrollo se propague a la base de datos de producción. (Incluso si estuviera seguro de que ninguno de sus cambios sería visible para su aplicación si se enviara a la base de datos de producción, el uso de una base de datos completamente diferente evita que su base de datos de producción y su aplicación tengan que manejar la carga tanto de los usuarios habituales como del equipo de desarrollo).
Por otro lado, podrías hacer esto. Tenga en cuenta que la definición de constantes con valores de matriz es válida a partir de PHP 7.0.0, aunque simplemente podría usar define( 'DEV_HOSTNAME', ... )definiciones de constantes de estilo ini , simplemente no es tan elegante.
define( 'CONFIGURATION', 'PRODUCTION' );
define( 'ENVIRONMENT', [
'DEVELOPMENT' => [
'hostname' => 'localhost',
'database' => 'devdb',
'username' => 'username',
'password' => 'password'
],
'PRODUCTION' => [
'hostname' => 'hostname',
'database' => 'proddb',
'username' => 'username',
'password' => 'password'
]
]);
Ahora todo lo que tiene que hacer para cambiar de entorno en su aplicación es modificar la CONFIGURATIONconstante, ya que su aplicación podría acceder a los valores de configuración usando algo como esto.
$hostname = ENVIRONMENT[CONFIGURATION]['hostname']; $database = ENVIRONMENT[CONFIGURATION]['database'];
$username = ENVIRONMENT[CONFIGURATION]['username']; $password = ENVIRONMENT[CONFIGURATION]['password'];
Lo que hacen algunas aplicaciones de producción es definir una Databaseclase abstracta cuyas clases derivadas implementan una interfaz que define cómo se debe acceder a ellas, por lo que podría cambiar entre, por ejemplo, MySQL y Postgres haciendo algo como lo anterior. Ésta es una de las razones por las que la DOP es tan útil; nos permite codificar en una interfaz, no en una implementación.
Para responder a su pregunta sobre la eficiencia, en realidad es menos eficiente incluir un archivo separado que contenga los datos de configuración.
Para incluir un archivo, el intérprete debe obtenerlo primero. Los archivos de configuración de la aplicación son (obviamente) específicos de la aplicación y, por lo tanto, se encuentran en algún lugar dentro o alrededor del directorio actual. Sin embargo, a menos que la ruta de inclusión sea absoluta o comience con un '.' o '..', PHP primero buscará el archivo en elinclude_path .
Suponiendo que el archivo se encuentre finalmente, leerlo desde donde esté almacenado requiere acceso al disco, que es realmente lento , aunque el impacto se puede mitigar mediante el uso de SSD o incluso tempfs, así como el almacenamiento en caché.
Peor aún, si usa la *_onceversión de requireo include, no solo incluirá el archivo (si y cuando finalmente se encuentre el archivo), el intérprete pasará por el problema adicional de verificar que el archivo aún no se haya incluido.
Puede pensar en la compensación entre la propagación ineficaz del cambio de configuración y la velocidad de ejecución como una compensación entre la eficiencia del tiempo de desarrollo y la eficiencia del tiempo de ejecución, y definitivamente vale la pena (juego de palabras).
Las aplicaciones más grandes tienden a optar por un enfoque de configuración más orientado a objetos al encapsular esencialmente estas variables globales en una clase única que supervisa la obtención y el establecimiento de parámetros de configuración. Probablemente verá que esto se conoce como el patrón de registro .
Sí, use un archivo de inclusión para su conexión de base de datos. Si cambia la contraseña de la base de datos o el servidor, tendrá que actualizar varias páginas y asegurarse de que todas coincidan. Eso es tedioso, e incluso si solo tiene 4 páginas hoy, podrían ser 20 o más en el futuro.
Evite las repeticiones innecesarias . La repetición es el enemigo de todo programador. También hay más cosas que repites en otras páginas, por ejemplo:
date_default_timezone_set('America/Los_Angeles');
Cuál es el propósito de esto ? De todos modos, debería tener algún tipo de archivo de configuración para centralizar la configuración. Y luego require_once debería hacer.
Registros DNS
Creo que el mérito de esto es discutible:
//Check Email Domain MX Record
$email_host = strtolower(substr(strrchr($Temail, "@"), 1));
if (!checkdnsrr($email_host, "MX")) {
header("Location: invalid.html");
exit (0);
}
Está comprobando si el dominio tiene al menos un registro MX. Es de suponer que desea frustrar envíos aleatorios de spammers o lo que sea. Pero en sí mismo eso prueba muy poco, y no demuestra que la presentación se haya realizado de buena fe. Podría usar la dirección de correo electrónico de cualquier persona, siempre que pase la prueba. Así que personalmente no veo mucho valor añadido en esto.
Además, pueden producirse errores transitorios de DNS. Esta función puede fallar de vez en cuando, incluso con nombres de dominio legítimos. Normalmente, el correo no se entrega directamente, sino que va a una cola local y en caso de fallas en la búsqueda de DNS o lo que sea, su MTA intentará enviar el correo a intervalos regulares.
Además, si no me equivoco, el RFC dice que en ausencia de un registro MX, el MTA puede usar un registro A como respaldo (tenga en cuenta que muchas zonas DNS están mal configuradas).
strtolower no es necesario, ya que los nombres de dominio no distinguen entre mayúsculas y minúsculas. Aunque es posible que desee normalizar la entrada y forzar toda la dirección de correo electrónico a minúsculas en caso de que contenga una combinación de caracteres en minúsculas y mayúsculas. Meramente por motivos estéticos.
Incluye
No hagas esto:
//send verification email using seperate php file
include_once 'vEmail.php';
Eso hace que el código sea aún más difícil de seguir y comprender. Recuerde, el código que es difícil de leer / entender tiende a tener más errores y ser menos seguro.
En su lugar, escriba una función dedicada que devuelva un valor booleano o cualquier cosa que desee. Pero puede tener una inclusión en la parte superior de su código para importar sus funciones, por supuesto. Podría ser algo como esto:
if (!send_verification_email($email, $verification_code)) {
// an error occured, do something
}
// proceed normally
Misc
Creo que tiene demasiadas páginas de redireccionamiento, solo considere esto:
- expired1.html
- expired2.html
- email.html
- invalid.html
- notallowed.html
- duplicate.html
- failure.html
- success.html
- verificado.html
Y, por supuesto, todos deben ser muy similares. Recomiendo encarecidamente que evolucionen sus prácticas de codificación: use un marco o al menos una solución de plantillas . Una página sería suficiente y pasa algunos parámetros como un mensaje personalizado para que la página se pueda representar de muchas formas diferentes. No necesita una página para cada escenario posible. Solo piense en el mantenimiento y la hinchazón.
Tenga en cuenta que también puede utilizar plantillas para sus correos electrónicos . Todo lo que necesita son archivos de texto con algunas etiquetas delimitadas, por ejemplo,% VERIFICATION_CODE%, luego simplemente reemplace esas etiquetas con los valores de sus variables.
IPv6
¿Cuál es su problema con IPv6? ¿Está almacenando el valor en un campo varchar? Luego asigne al menos 45 caracteres, pero no tiene nada de especial. Por supuesto, las direcciones IPv6 tienden a ser más largas y tienen un patrón marcadamente diferente. La pregunta es cuál es el propósito de almacenar esta información y cómo piensa utilizarla.