Iscrizione elenco e-mail PHP

Oct 01 2020

Vedi il mio post originale: qui .

Ho una pagina html e quattro file php che consentono agli utenti di iscriversi a una mailing list. Uno degli script php è un cronjob che elimina le righe non verificate più vecchie di 24 ore e non è incluso di seguito per motivi di lunghezza del post. Uso PDO per le mie dichiarazioni preparate. Tutto è stato testato dal vivo ed è completamente funzionante al meglio delle mie conoscenze. Qualsiasi feedback è il benvenuto. Ho elencato alcune domande sotto gli snippet. :)

email.html --- Gli utenti si registrano qui

<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 e invia l'input dell'utente al database

<?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_oncein signup.php, invia e-mail di verifica

<?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]');
?>

verifica.php --- Allegato al collegamento che è stato inviato nell'e-mail di verifica

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

Domande e commenti:

  • Ho incluso connessioni al database per ogni file php, ma ho scoperto che alcuni preferiscono avere un file di configurazione globale per le loro connessioni. Perché? È più efficiente?
  • Nel post originale, qualcuno ha detto che all'espressione regolare trovata in signup.php manca il flag Unicode. Qualcuno potrebbe spiegarlo, perché non sono riuscito a trovare nulla su di esso.
  • Sebbene la memorizzazione di IPv4 funzioni bene, non sono stato in grado di capire come memorizzare correttamente IPv6 (per quanto ne so). Ho provato questo: bin2hex(inet_pton($_SERVER['REMOTE_ADDR']));ma non sono riuscito a capire se l'output fosse corretto, perché non assomigliava per niente a un indirizzo ipv6. Correggimi se sembra utilizzabile.
  • Sto esaminando PHPMailer piuttosto che utilizzare la funzione nativa mail (). Nel caso degli script precedenti, sarebbe consigliato o è più per l'invio di e-mail in blocco?

Modifica 1:

Mi sono appena reso conto che la convalida e-mail trovata in signup.phpconsente un indirizzo e-mail con spazi (ad esempio: ohnothere sono [email protected]). La versione precedente dello script, che si trova al link in cima al post, impediva questo tipo di indirizzi. Qualche idea sul perché potrebbe essere o su come prevenirlo? Vorrei stare lontano da filter_var (FILTER_VALIDATE_EMAIL) poiché annulla i caratteri internazionali. Continuerò a sperimentarlo ...

Risposte

3 JoseFernandoLopezFernandez Oct 02 2020 at 15:17

Includere un file di configurazione globale non riguarda l'efficienza, ma solo la centralizzazione dei dati di configurazione dell'applicazione.

Supponiamo, ad esempio, che tu stia sviluppando una nuova funzionalità per la tua applicazione, di voler utilizzare un database appositamente per questo scopo, in modo da non interferire con il database di produzione.

Se includi la configurazione del database su ogni script, dovrai assicurarti di controllare (e, si spera, ricontrollare) ogni script per assicurarti che nessuna delle tue azioni di sviluppo venga propagata al database di produzione. (Anche se fossi certo che nessuna delle tue modifiche sarebbe visibile alla tua applicazione se inviata al database di produzione, l'utilizzo di un database completamente diverso evita che il tuo database di produzione e l'applicazione debbano gestire il carico sia degli utenti regolari che del team di sviluppo.)

D'altra parte, potresti semplicemente farlo. Nota che la definizione di costanti con valori di matrice è valida a partire da PHP 7.0.0, anche se potresti semplicemente usare define( 'DEV_HOSTNAME', ... )definizioni di costanti in stile ini , semplicemente non è così elegante.

define( 'CONFIGURATION', 'PRODUCTION' );

define( 'ENVIRONMENT', [
    'DEVELOPMENT' => [
        'hostname' => 'localhost',
        'database' => 'devdb',
        'username' => 'username',
        'password' => 'password'
    ],
    'PRODUCTION' => [
        'hostname' => 'hostname',
        'database' => 'proddb',
        'username' => 'username',
        'password' => 'password'
    ]
]);

Ora tutto ciò che devi fare per cambiare ambiente nella tua applicazione è modificare la CONFIGURATIONcostante, poiché la tua applicazione potrebbe accedere ai valori di configurazione usando qualcosa di simile.

$hostname = ENVIRONMENT[CONFIGURATION]['hostname']; $database = ENVIRONMENT[CONFIGURATION]['database'];
$username = ENVIRONMENT[CONFIGURATION]['username']; $password = ENVIRONMENT[CONFIGURATION]['password'];

Ciò che fanno alcune applicazioni di produzione è definire una Databaseclasse astratta le cui classi derivate implementano un'interfaccia che definisce come devono essere accessibili, quindi è possibile passare da MySQL a Postgres facendo qualcosa come sopra. Questo è uno dei motivi per cui il DOP è così utile; ci consente di codificare per un'interfaccia, non per un'implementazione.

Per rispondere alla tua domanda sull'efficienza, in realtà è meno efficiente includere un file separato che contiene i dati di configurazione.

Per includere un file, l'interprete deve prima scaricarlo. I file di configurazione dell'applicazione sono (ovviamente) specifici dell'applicazione e quindi si trovano da qualche parte all'interno o intorno alla directory corrente. Tuttavia, a meno che il percorso di inclusione non sia assoluto o inizi con un "." o "..", PHP cercherà prima il file nel fileinclude_path .

Supponendo che il file venga effettivamente trovato alla fine, leggerlo da dove è archiviato richiede l'accesso al disco, che è molto lento , sebbene l'impatto possa essere mitigato utilizzando SSD o persino tempfs, oltre alla cache.

Peggio ancora, se si utilizza la *_onceversione di requireo include, non si limiterà a includere il file (se e quando il file verrà eventualmente trovato), l'interprete dovrà affrontare l'ulteriore problema di verificare che il file non sia già stato incluso.

Puoi pensare al compromesso tra una propagazione inefficiente delle modifiche alla configurazione e la velocità di esecuzione come un compromesso tra l'efficienza del tempo di sviluppo e l'efficienza del tempo di esecuzione, e ne vale sicuramente la pena (gioco di parole).

Le applicazioni più grandi tendono a optare per un approccio alla configurazione più orientato agli oggetti incapsulando essenzialmente queste variabili globali in una classe singleton che supervisiona l'acquisizione e l'impostazione dei parametri di configurazione. Probabilmente lo vedrai indicato come pattern di registro .

2 Anonymous Oct 02 2020 at 20:34

Sì, utilizza un file di inclusione per la tua connessione DB. Se modifichi la password del DB o il server, dovrai aggiornare diverse pagine e assicurarti che corrispondano tutte. Questo è noioso, e anche se hai solo 4 pagine oggi, potrebbero essere 20 o più in futuro.

Evita ripetizioni inutili . La ripetizione è il nemico di ogni programmatore. Ci sono anche più cose che ripeti in altre pagine, ad esempio:

date_default_timezone_set('America/Los_Angeles');

Qual è lo scopo di questo? Ad ogni modo, dovresti avere una sorta di file di configurazione per centralizzare le impostazioni. E poi require_once dovrebbe bastare.

Record DNS

Penso che il merito di questo sia discutibile:

//Check Email Domain MX Record
$email_host = strtolower(substr(strrchr($Temail, "@"), 1));
if (!checkdnsrr($email_host, "MX")) {
    header("Location: invalid.html");
    exit (0);
}

Stai verificando se il dominio ha almeno un record MX. Presumibilmente vuoi contrastare invii casuali da parte di spammer o altro. Ma di per sé ciò dimostra ben poco e non dimostra che la richiesta sia stata presentata in buona fede. Potrei usare l'indirizzo e-mail di chiunque, purché superi il test. Quindi personalmente non vedo molto valore aggiunto in questo.

Inoltre, potrebbero verificarsi errori DNS temporanei. Questa funzione potrebbe non funzionare di tanto in tanto, anche con nomi di dominio legittimi. Normalmente, la posta non viene consegnata direttamente ma va a una coda locale e in caso di errori di ricerca DNS o qualsiasi altra cosa il tuo MTA proverà a inviare la posta a intervalli regolari.

Inoltre, se non sbaglio, l'RFC dice che in assenza di un record MX, l'MTA può utilizzare un record A come fallback (tieni presente che molte zone DNS sono configurate male).

strtolower non è necessario, poiché i nomi di dominio non fanno distinzione tra maiuscole e minuscole. Anche se potresti voler normalizzare l'input e forzare l'intero indirizzo e-mail in minuscolo nel caso in cui contenga una combinazione di caratteri minuscoli e maiuscoli. Puramente per ragioni estetiche.

Include

Non farlo:

//send verification email using seperate php file
include_once 'vEmail.php';

Ciò rende il codice ancora più difficile da seguire e da capire. Ricorda, il codice difficile da leggere / comprendere tende ad essere più difettoso e meno sicuro.

Invece, scrivi una funzione dedicata che restituisca un valore booleano o qualsiasi cosa tu voglia. Ma puoi avere un'inclusione nella parte superiore del codice per importare le tue funzioni, naturalmente. Potrebbe essere qualcosa del genere:

if (!send_verification_email($email, $verification_code)) {
    // an error occured, do something
}
// proceed normally

Misc

Penso che tu abbia troppe pagine di reindirizzamento, considera questo:

  • expired1.html
  • expired2.html
  • email.html
  • invalid.html
  • notallowed.html
  • duplicate.html
  • failure.html
  • success.html
  • verificato.html

E ovviamente devono essere tutti molto simili. Consiglio vivamente di evolvere le tue pratiche di codifica: usa un framework o almeno una soluzione di template . Una pagina sarebbe sufficiente e si passano alcuni parametri come un messaggio personalizzato in modo che la pagina possa essere visualizzata in molti modi diversi. Non hai bisogno di una pagina per ogni possibile scenario. Pensa solo alla manutenzione e al gonfiore.

Nota che potresti usare modelli anche per le tue e- mail . Tutto ciò di cui hai bisogno sono file di testo con alcuni tag delimitati, ad esempio% VERIFICATION_CODE%, quindi sostituisci semplicemente quei tag con i valori delle tue variabili.

IPv6

Qual è il tuo problema con IPv6? Stai memorizzando il valore in un campo varchar? Quindi assegna almeno 45 caratteri ma non c'è niente di speciale. Ovviamente gli indirizzi IPv6 tendono ad essere più lunghi e hanno uno schema notevolmente diverso. La domanda è: qual è lo scopo della memorizzazione di queste informazioni e come intendi utilizzarle.