ลงทะเบียนรายชื่ออีเมล PHP

Oct 01 2020

ดูโพสต์เดิมของฉัน: ที่นี่

ฉันมีหน้า html หนึ่งหน้าและไฟล์ php สี่ไฟล์ที่อนุญาตให้ผู้ใช้สมัครรายชื่ออีเมล หนึ่งในสคริปต์ php คือ cronjob ที่ลบแถวที่ไม่ได้รับการยืนยันที่เก่ากว่า 24 ชั่วโมงและจะไม่รวมอยู่ด้านล่างเพื่อความยาวของโพสต์ ฉันใช้ PDO สำหรับงบที่เตรียมไว้ ทุกอย่างผ่านการทดสอบสดและใช้งานได้เต็มที่ตามความรู้ของฉัน ยินดีรับข้อเสนอแนะใด ๆ และทั้งหมด ฉันได้เขียนหัวข้อย่อยคำถามไว้ด้านล่างตัวอย่าง :)

email.html --- ผู้ใช้สมัครที่นี่

<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 - กรองและส่งข้อมูลผู้ใช้ไปยังฐานข้อมูล

<?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_onceใน signup.php ส่งอีเมลยืนยัน

<?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 --- แนบมากับลิงค์ที่ส่งมาในอีเมลยืนยัน

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

คำถามและความคิดเห็น:

  • ฉันได้รวมการเชื่อมต่อฐานข้อมูลสำหรับไฟล์ php แต่ละไฟล์แล้ว แต่ฉันพบว่าบางคนชอบที่จะมีไฟล์กำหนดค่าส่วนกลางสำหรับการเชื่อมต่อ ทำไมถึงเป็นเช่นนั้น? มีประสิทธิภาพมากขึ้นหรือไม่?
  • ในโพสต์ต้นฉบับมีคนกล่าวว่า regex ที่พบใน signup.php ไม่มีแฟล็ก Unicode มีใครช่วยอธิบายเรื่องนี้ได้ไหมเพราะฉันไม่พบอะไรเลย
  • แม้ว่าการจัดเก็บ IPv4 จะทำงานได้ดี แต่ฉันยังไม่สามารถหาวิธีจัดเก็บ IPv6 ได้อย่างถูกต้อง (เท่าที่ฉันรู้) ฉันได้ลองสิ่งนี้แล้วbin2hex(inet_pton($_SERVER['REMOTE_ADDR']));แต่ฉันคิดไม่ออกว่าผลลัพธ์นั้นถูกต้องหรือไม่เพราะมันดูเหมือนที่อยู่ ipv6 แก้ไขฉันถ้าสิ่งนี้ใช้งานได้
  • ฉันกำลังมองหา PHPMailer แทนที่จะใช้ฟังก์ชัน mail () ดั้งเดิม ในกรณีของสคริปต์ข้างต้นจะแนะนำให้ใช้หรือมากกว่านี้สำหรับการส่งอีเมลจำนวนมาก

แก้ไข 1:

ฉันเพิ่งรู้ว่าการตรวจสอบอีเมลที่พบในsignup.phpทำให้ที่อยู่อีเมลมีช่องว่าง (เช่น: ohnothere are [email protected]) เวอร์ชันก่อนหน้าของสคริปต์ซึ่งพบได้ที่ลิงก์ที่ด้านบนของโพสต์ป้องกันที่อยู่ประเภทนี้ มีความคิดว่าเหตุใดจึงเป็นเช่นนั้นหรือจะป้องกันได้อย่างไร ฉันต้องการอยู่ห่างจาก filter_var (FILTER_VALIDATE_EMAIL) เนื่องจากยกเลิกอักขระสากล จะทดลองดูต่อไป ...

คำตอบ

3 JoseFernandoLopezFernandez Oct 02 2020 at 15:17

การรวมไฟล์คอนฟิกูเรชันส่วนกลางไม่ได้เกี่ยวกับประสิทธิภาพ แต่เป็นการรวมข้อมูลคอนฟิกูเรชันแอปพลิเคชันไว้ที่ส่วนกลาง

ตัวอย่างเช่นสมมติว่าคุณกำลังพัฒนาคุณลักษณะใหม่สำหรับแอปพลิเคชันของคุณคุณต้องการใช้ฐานข้อมูลเพื่อจุดประสงค์นี้โดยเฉพาะเพื่อที่จะไม่รบกวนฐานข้อมูลการผลิต

หากคุณรวมการกำหนดค่าฐานข้อมูลไว้ในแต่ละสคริปต์คุณจะต้องตรวจสอบให้แน่ใจว่าคุณได้ตรวจสอบ (และหวังว่าจะตรวจสอบอีกครั้ง) ทุกสคริปต์เพื่อให้แน่ใจว่าการดำเนินการพัฒนาของคุณจะไม่ถูกเผยแพร่ไปยังฐานข้อมูลการผลิต (แม้ว่าคุณจะแน่ใจว่าแอปพลิเคชันของคุณจะไม่สามารถมองเห็นการเปลี่ยนแปลงใด ๆ ของคุณได้หากผลักดันไปยังฐานข้อมูลที่ใช้งานจริงการใช้ฐานข้อมูลที่แตกต่างกันโดยสิ้นเชิงจะช่วยประหยัดฐานข้อมูลการผลิตและแอปพลิเคชันของคุณไม่ให้ต้องจัดการกับภาระจากทั้งผู้ใช้ทั่วไปและทีมพัฒนา

ในทางกลับกันคุณสามารถทำได้ โปรดทราบว่าการกำหนดค่าคงที่ที่มีมูลค่าอาร์เรย์นั้นใช้ได้กับ PHP 7.0.0 แม้ว่าคุณจะสามารถใช้define( 'DEV_HOSTNAME', ... )คำจำกัดความคงที่สไตล์ini ได้ แต่ก็ไม่ได้เป็นเพียงแค่จินตนาการเท่านั้น

define( 'CONFIGURATION', 'PRODUCTION' );

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

ตอนนี้สิ่งที่คุณต้องทำเพื่อเปลี่ยนสภาพแวดล้อมในแอปพลิเคชันของคุณคือแก้ไขCONFIGURATIONค่าคงที่เนื่องจากแอปพลิเคชันของคุณสามารถเข้าถึงค่าการกำหนดค่าได้โดยใช้สิ่งนี้

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

สิ่งที่แอปพลิเคชันการผลิตบางตัวทำคือกำหนดDatabaseคลาสนามธรรมซึ่งคลาสที่ได้รับจะใช้อินเทอร์เฟซที่กำหนดวิธีการเข้าถึงดังนั้นคุณสามารถสลับระหว่างการพูด MySQL และ Postgres ได้โดยทำสิ่งต่างๆดังที่กล่าวมา นี่เป็นหนึ่งในเหตุผลที่ PDO มีประโยชน์มาก ช่วยให้เราสามารถเขียนโค้ดไปยังอินเทอร์เฟซไม่ใช่การใช้งาน

เพื่อตอบคำถามของคุณเกี่ยวกับประสิทธิภาพการรวมไฟล์แยกต่างหากที่มีข้อมูลการกำหนดค่ามีประสิทธิภาพน้อยกว่า

ในการรวมไฟล์ล่ามต้องได้รับไฟล์ก่อน ไฟล์คอนฟิกูเรชันของแอ็พพลิเคชัน (แน่นอน) เป็นแอพพลิเคชั่นเฉพาะดังนั้นจึงอยู่ที่ไหนสักแห่งในหรือรอบ ๆ ไดเร็กทอรีปัจจุบัน อย่างไรก็ตามเว้นแต่เส้นทางรวมเป็นค่าสัมบูรณ์หรือขึ้นต้นด้วย "." หรือ ".. " PHP จะค้นหาไฟล์ในไฟล์include_path .

สมมติว่าพบไฟล์จริงในที่สุดการอ่านจากที่ใดก็ตามที่จัดเก็บต้องใช้การเข้าถึงดิสก์ซึ่งช้ามากแม้ว่าผลกระทบจะบรรเทาลงได้โดยใช้ SSD หรือแม้กระทั่งการtempfsแคช

ยิ่งไปกว่านั้นหากคุณใช้*_onceเวอร์ชันอย่างใดอย่างหนึ่งrequireหรือincludeไม่เพียง แต่รวมไฟล์ไว้เท่านั้น (หากและเมื่อพบไฟล์ในที่สุด) ล่ามจะดำเนินการแก้ไขปัญหาเพิ่มเติมในการตรวจสอบว่าไฟล์ยังไม่ได้รวมอยู่ด้วย

คุณสามารถนึกถึงการแลกเปลี่ยนระหว่างการเผยแพร่การเปลี่ยนแปลงการกำหนดค่าที่ไม่มีประสิทธิภาพและความเร็วในการดำเนินการเป็นการแลกเปลี่ยนระหว่างประสิทธิภาพของเวลาในการพัฒนากับประสิทธิภาพของเวลาดำเนินการและมันก็คุ้มค่ากับเวลาของคุณ (ตั้งใจเล่นสำนวน)

แอปพลิเคชันขนาดใหญ่มีแนวโน้มที่จะเลือกใช้แนวทางเชิงวัตถุมากขึ้นในการกำหนดค่าโดยการห่อหุ้มตัวแปรส่วนกลางเหล่านี้ในคลาสซิงเกิลตันที่ดูแลการรับและการตั้งค่าพารามิเตอร์การกำหนดค่า คุณอาจจะเห็นนี้เรียกว่ารูปแบบ Registry

2 Anonymous Oct 02 2020 at 20:34

ใช่ใช้ไฟล์รวมสำหรับการเชื่อมต่อฐานข้อมูลของคุณ หากคุณเปลี่ยนรหัสผ่าน DB หรือเซิร์ฟเวอร์คุณจะต้องอัปเดตหลายหน้าและตรวจสอบให้แน่ใจว่าตรงกันทั้งหมด นั่นเป็นเรื่องที่น่าเบื่อและแม้ว่าคุณจะมีเพียง 4 หน้าในวันนี้ แต่อาจเป็น 20 หรือมากกว่านั้นในอนาคต

หลีกเลี่ยงไม่จำเป็นซ้ำ การทำซ้ำเป็นศัตรูของโปรแกรมเมอร์ทุกคน นอกจากนี้ยังมีอีกหลายสิ่งที่คุณทำซ้ำในหน้าอื่น ๆ เช่น:

date_default_timezone_set('America/Los_Angeles');

จุดประสงค์ของสิ่งนี้คืออะไร? อย่างไรก็ตามคุณควรมีไฟล์กำหนดค่าบางประเภทเพื่อรวมศูนย์การตั้งค่า แล้ว require_once ควรทำ

ระเบียน DNS

ฉันคิดว่าข้อดีของสิ่งนี้เป็นที่ถกเถียงกัน:

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

คุณกำลังตรวจสอบว่าโดเมนมีระเบียน MX อย่างน้อยหนึ่งรายการหรือไม่ สันนิษฐานว่าคุณต้องการขัดขวางการส่งแบบสุ่มโดยนักส่งสแปมหรืออะไรก็ตาม แต่ในตัวของมันเองที่พิสูจน์ได้น้อยมากและไม่ได้แสดงให้เห็นว่าการส่งนั้นทำโดยสุจริต ฉันสามารถใช้ที่อยู่อีเมลของใครก็ได้ตราบเท่าที่ผ่านการทดสอบ ดังนั้นฉันจึงไม่เห็นมูลค่าเพิ่มในส่วนนี้มากนัก

นอกจากนี้อาจเกิดข้อผิดพลาด DNS ชั่วคราว ฟังก์ชันนี้อาจล้มเหลวในบางครั้งแม้ว่าจะมีชื่อโดเมนที่ถูกต้องก็ตาม โดยปกติอีเมลจะไม่ถูกส่งโดยตรง แต่ไปที่คิวภายในและในกรณีที่การค้นหา DNS ล้มเหลวหรืออะไรก็ตามที่ MTA ของคุณจะพยายามส่งอีเมลตามช่วงเวลาปกติ

นอกจากนี้ถ้าฉันไม่ผิด RFC บอกว่าในกรณีที่ไม่มีระเบียน MX MTA สามารถใช้ระเบียน A เป็นทางเลือกได้ (โปรดทราบว่าโซน DNS จำนวนมากได้รับการกำหนดค่าไม่ดี)

ไม่จำเป็นต้องใช้ strtolower เนื่องจากชื่อโดเมนไม่คำนึงถึงขนาดตัวพิมพ์ แม้ว่าคุณอาจต้องการปรับอินพุตให้เป็นมาตรฐานและบังคับให้ที่อยู่อีเมลทั้งหมดเป็นตัวพิมพ์เล็กในกรณีที่มีการผสมระหว่างตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ ด้วยเหตุผลด้านเครื่องสำอางล้วนๆ

รวม

อย่าทำสิ่งนี้:

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

ซึ่งทำให้โค้ดติดตามและทำความเข้าใจได้ยากยิ่งขึ้น โปรดจำไว้ว่ารหัสที่อ่าน / เข้าใจยากมักจะมีปัญหามากกว่าและปลอดภัยน้อยกว่า

ให้เขียนฟังก์ชันเฉพาะที่ส่งคืนค่าบูลีนหรืออะไรก็ได้ที่คุณต้องการแทน แต่คุณสามารถรวมไว้ที่ด้านบนของโค้ดเพื่อนำเข้าฟังก์ชันของคุณได้อย่างแน่นอน อาจเป็นดังนี้:

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

อื่น ๆ

ฉันคิดว่าคุณมีหน้าเปลี่ยนเส้นทางมากเกินไปให้พิจารณาสิ่งนี้:

  • หมดอายุ 1.html
  • หมดอายุ 2.html
  • email.html
  • invalid.html
  • notallowed.html
  • duplicate.html
  • failure.html
  • success.html
  • Verified.html

และแน่นอนว่าพวกเขาทั้งหมดต้องคล้ายกันมาก ฉันขอแนะนำอย่างยิ่งให้พัฒนาแนวทางการเขียนโค้ดของคุณ - ใช้กรอบงานหรืออย่างน้อยที่สุดก็คือโซลูชันการสร้างเทมเพลต หน้าเดียวก็เพียงพอแล้วและคุณส่งพารามิเตอร์บางอย่างเช่นข้อความที่กำหนดเองเพื่อให้สามารถแสดงผลเพจได้หลายวิธี คุณไม่จำเป็นต้องมีหน้าสำหรับทุกสถานการณ์ที่เป็นไปได้ แค่คิดเกี่ยวกับการบำรุงรักษาและการขยายตัว

โปรดทราบว่าคุณสามารถใช้เทมเพลตสำหรับอีเมลของคุณได้เช่นกัน สิ่งที่คุณต้องมีคือไฟล์ข้อความที่มีแท็กที่มีตัวคั่นเช่น% VERIFICATION_CODE% จากนั้นคุณก็แทนที่แท็กเหล่านั้นด้วยค่าจากตัวแปรของคุณ

IPv6

ปัญหาของคุณกับ IPv6 คืออะไร? คุณเก็บค่าไว้ในช่อง varchar หรือไม่? จากนั้นจัดสรรอักขระอย่างน้อย45 ตัวแต่ไม่มีอะไรพิเศษเกี่ยวกับเรื่องนี้ แน่นอนว่าที่อยู่ IPv6 มักจะยาวกว่าและมีรูปแบบที่แตกต่างกันอย่างเห็นได้ชัด คำถามคือวัตถุประสงค์ของการจัดเก็บข้อมูลนี้คืออะไรและคุณตั้งใจจะใช้มันอย่างไร