[PHP]Ajax CORS 錯誤

有時在串接Ajax會看到下列錯誤

Access to XMLHttpRequest at 'https://url/test.php' from origin 'http://url' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

錯誤訊息主要是說ajax要連線的網址被瀏覽器的同源政策阻擋(CORS policy)
簡單來說就是ajax連線網址跟現在正在瀏覽網頁的網址不一樣
(就算是http跟https也視同不一樣)
所以ajax連線的程式就要設定Header開放跨網域連線


if($_SERVER['HTTP_ORIGIN'] == "http://URL_A") {
    header('Access-Control-Allow-Origin: http://URL_A');
}else if($_SERVER['HTTP_ORIGIN'] == "https://URL_A") {
    header('Access-Control-Allow-Origin: https://URL_A');
}else if($_SERVER['HTTP_ORIGIN'] == "http://URL_B") {
    header('Access-Control-Allow-Origin: http://URL_B');
}else {
	echo "no auth";
	exit;
}

如果要開放讓所有網域連線的話,設為 * 就可以了

header("Access-Control-Allow-Origin: *");

[PHP]取得用戶IP

主要有2個方式可以取得用戶的IP,分別為 getenv()$_SERVER
不過 getenv() 不支援 IIS 的 ISAPI

getenv():

function get_client_ip() { 
    $client_ip = '';
    if (getenv('HTTP_CLIENT_IP'))
        $client_ip = getenv('HTTP_CLIENT_IP');
    else if(getenv('HTTP_X_FORWARDED_FOR'))
        $client_ip = getenv('HTTP_X_FORWARDED_FOR');
    else if(getenv('HTTP_X_FORWARDED'))
        $client_ip = getenv('HTTP_X_FORWARDED');
    else if(getenv('HTTP_X_CLUSTER_CLIENT_IP'))
        $client_ip = getenv('HTTP_X_CLUSTER_CLIENT_IP');
    else if(getenv('HTTP_FORWARDED_FOR'))
        $client_ip = getenv('HTTP_FORWARDED_FOR');
    else if(getenv('HTTP_FORWARDED'))
       $client_ip = getenv('HTTP_FORWARDED');
    else if(getenv('REMOTE_ADDR'))
        $client_ip = getenv('REMOTE_ADDR');
    else if(getenv('HTTP_VIA'))
        $client_ip = getenv('HTTP_VIA');
    else
        $client_ip = '無法取得資訊';
    return $client_ip;
}

$_SERVER:

function get_client_ip() {
    $client_ip = '';
    if (isset($_SERVER['HTTP_CLIENT_IP']))
        $client_ip = $_SERVER['HTTP_CLIENT_IP'];
    else if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
        $client_ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    else if(isset($_SERVER['HTTP_X_FORWARDED']))
        $client_ip = $_SERVER['HTTP_X_FORWARDED'];
    else if(isset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']))
        $client_ip = $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
    else if(isset($_SERVER['HTTP_FORWARDED_FOR']))
        $client_ip = $_SERVER['HTTP_FORWARDED_FOR'];
    else if(isset($_SERVER['HTTP_FORWARDED']))
        $client_ip = $_SERVER['HTTP_FORWARDED'];
    else if(isset($_SERVER['REMOTE_ADDR']))
        $client_ip = $_SERVER['REMOTE_ADDR'];
    else if(isset($_SERVER['HTTP_VIA']))
        $client_ip = $_SERVER['HTTP_VIA'];
    else
        $client_ip = '無法取得資訊';
    return $client_ip;
}

不同的狀況IP會出現在不同的地方
可以從這邊去測試從哪可以抓到IP:http://www.gocar.idv.tw/tools/whatismyip.php

mysqli_insert_id 取得最後寫入資料的ID

我們在寫入資料時常常會需要剛 Insert 資料的 ID 來與其他的 Table 做關聯
ID 必須要是一個 AUTO_INCREMENT 欄位,不然會取不到資料
id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,

以下為程式範例:

$sql = "INSERT INTO table_name (column1, column2, column3)
        VALUES ('value1', 'value2', 'value3')";

if (mysqli_query($conn, $sql)) {
    $last_id = mysqli_insert_id($conn);
    echo '剛寫入的資料ID: ' . $last_id;
} else {
    echo "Error: " . $sql . "<br>" . mysqli_error($conn);
}

我則是習慣改寫一個 Insert Function,在寫入時直接回傳 ID
也加了一個 $is_return 的參數來判斷是否要回傳ID
在不需要回傳ID的大量寫入時可以使用

$servername = "localhost";
$username = "username";
$password = "password";

// 建立連線
$conn = mysqli_connect($servername, $username, $password);

// 檢查連線若錯誤顯示訊息
if (!$conn) {
    die("連線失敗: " . mysqli_connect_error()); 
}
$sql = "INSERT INTO table_name (column1, column2, column3)
        VALUES ('value1', 'value2', 'value3')";

$last_id = insert_data($conn,$sql,'Y');
echo '剛寫入的資料ID: '.$last_id;

function insert_data($conn,$sql,$is_return=''){
    if (mysqli_query($conn, $sql)) {
        if($is_return) return mysqli_insert_id($conn);
    } else {
        echo "Error: " . $sql . "<br>" . mysqli_error($conn);
    }
}

PHP Insert MySQL 寫入資料

MySQL有2種寫入方式

SQL標準語法:
INSERT INTO table_name (column1, column2, column3,...)
VALUES (value1, value2, value3,...);

MYSQL專用語法:
INSERT INTO table_name SET
       column1 = value1,
       column2 = value2,
       column3 = value3;
主要差別在標準語法在其他DB也能使用,並且可以一行SQL指令批次寫入多筆資料
INSERT INTO table_name (a,b,c) VALUES
       (1,2,3),(4,5,6),(7,8,9);

而 SET 的語法則是只能用在MySQL上,個人比較習慣用這種
優點是好閱讀跟維護,不容易把資料塞錯格
而且在做UPDATE時可以直接共用

PHP 物件寫法 ( MySQLi Object-Oriented ):

$sql = "INSERT INTO table_name (column1, column2, column3)
VALUES ('value1', 'value2', 'value3')";

if ($conn->query($sql) === TRUE) {
    echo "寫入成功";
} else {
    echo "寫入失敗: " . $sql . "<br>" . $conn->error;
}

PHP 程序式寫法( MySQLi Procedural ):

$sql = "INSERT INTO table_name (column1, column2, column3)
VALUES ('value1', 'value2', 'value3')";

if (mysqli_query($conn, $sql)) {
    echo "寫入成功";
} else {
    echo "寫入失敗: " . $sql . "<br>" . mysqli_error($conn);
}

PHPMailer & 透過Gmail發信

PHPMailer 是一套功能強大的的Mail套件
詳細資料可查看PHPMailer GitHub: https://github.com/PHPMailer/PHPMailer

下載整包程式碼
wget https://github.com/PHPMailer/PHPMailer/archive/master.zip
解壓縮
unzip master.zip

而直接複製GitHub的Sample Code最常見的錯誤就是找不到 autoload.php

Warning: require(vendor/autoload.php): failed to open stream: No such file or directory 
Fatal error: require(): Failed opening required 'vendor/autoload.php' (include_path='.:/usr/share/php') 

因為GitHub裡的Sample code是走Composer路線的,Composer是PHP管理套件的工具
而 autoload.php 要跑過 Composer 才會出現
Composer & autoload.php 的關係,後續有機會再另開文章解說
會出錯多半是沒 Composer 直接拿 Composer 的語法下去用,所以 GitHub官網有這段說明

Alternatively, if you're not using Composer, copy the contents of the PHPMailer folder into one of the include_path directories specified in your PHP configuration and load each class file manually:
<?php 
    use PHPMailer\PHPMailer\PHPMailer; 
    use PHPMailer\PHPMailer\Exception; 

    require 'path/to/PHPMailer/src/Exception.php'; 
    require 'path/to/PHPMailer/src/PHPMailer.php'; 
    require 'path/to/PHPMailer/src/SMTP.php';
If you're not using the SMTP class explicitly (you're probably not), you don't need a use line for the SMTP class.

所以這邊修改一下不用 Composer 做法就可以直上了
如果不是用Google的SMTP,也可以視情況修改參數

<?php
//移除Composer語法
// Import PHPMailer classes into the global namespace
 // These must be at the top of your script, not inside a function
 use PHPMailer\PHPMailer\PHPMailer;
 use PHPMailer\PHPMailer\SMTP;
 use PHPMailer\PHPMailer\Exception;
 // Load Composer's autoloader
 require 'vendor/autoload.php';
//新增
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

//設定檔案路徑
require 'path/to/PHPMailer/src/Exception.php';
require 'path/to/PHPMailer/src/PHPMailer.php';
require 'path/to/PHPMailer/src/SMTP.php';

//建立物件                                                                
$mail = new PHPMailer(true);

try {
    //Server settings
    //$mail->SMTPDebug = SMTP::DEBUG_SERVER;  // Enable verbose debug output
    $mail->SMTPDebug = 0; // DEBUG訊息
    $mail->isSMTP(); // 使用SMTP
    $mail->Host = 'smtp.gmail.com'; // SMTP server 位址
    $mail->SMTPAuth = true;  // 開啟SMTP驗證
    $mail->Username = 'Gmail帳號'; // SMTP 帳號
    $mail->Password = 'Gmail密碼(*註1)'; // SMTP 密碼
    //$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // Enable TLS encryption; `PHPMailer::ENCRYPTION_SMTPS` also accepted
    $mail->SMTPSecure = "ssl"; // Gmail要透過SSL連線
    $mail->Port       = 465; // SMTP TCP port 

    //設定收件人資料
    $mail->setFrom('from@example.com', 'Mailer'); // 寄件人(透過Gmail發送會顯示Gmail帳號為寄件者)
    $mail->addAddress('apple@example.com', 'Apple User'); // 收件人會顯示 Apple User<apple@example.com>(*註2)
    // $mail->addAddress('banana@example.com'); // 名字非必填
    $mail->addReplyTo('info@example.com', 'Information'); //回信的收件人
    $mail->addCC('cc@example.com'); //副本
    $mail->addBCC('bcc@example.com'); //密件副本

    // 附件
    $mail->addAttachment('/var/tmp/file.tar.gz'); // 附件 (*註3) 
    // $mail->addAttachment('/tmp/image.jpg', 'new.jpg'); // 插入附件可更改檔名

    // 信件內容
    $mail->isHTML(true); // 設定為HTML格式
    $mail->Subject = 'Here is the subject'; // 信件標題
    $mail->Body    = 'This is the HTML message body <B>in bold!</B>'; // 信件內容
    $mail->AltBody = 'This is the body in plain text for non-HTML mail clients'; // 對方若不支援HTML的信件內容

    $mail->send();
    echo 'Message has been sent';
} catch (Exception $e) {
    echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}
?>
*註1
若要讓透過Gmail發信,Gmail帳號也需要設定
如果未開啟兩步驟驗證,需要開啟低安全性應用程式存取權:
https://support.google.com/accounts/answer/6010255?hl=zh-Hant
若已經開啟兩步驟驗證,則要設定應用程式密碼(建議):
https://support.google.com/accounts/answer/185833?hl=zh-Hant
如果有使用G Suite,還可以使用relay(有限發送數量)
https://support.google.com/a/answer/2956491?hl=zh-Hant
*註2
若要發送給多人:
$mailto = array(
             'user1@example.com' => 'user1',
             'user2@example.com' => 'user2',
             'user3@example.com' => 'user3',
);
if(is_array($mailto)){
    foreach ($mailto as $email => $name) {
        $mail->AddAddress("$email","$name");
    }
}else{
    $mail->AddAddress("$mailto");
}
另外的做法就是直接疊上去
 $mail->addAddress('user1@example.com', 'user1'); 
 $mail->addAddress('user2@example.com', 'user2'); 
 $mail->addAddress('user3@example.com', 'user3'); 
*註3
附件如果要插入多個檔案:
$files = array($file1,$file2,$file3,$file4,$file5);
$mail->AddAttachment("$files");

PHP 發生錯誤Mail回報

系統會因為安全性考量遇到錯誤時不顯示錯誤訊息
透過此 function 可以把系統錯誤訊息回傳給開發者

程式碼:

function die_send_email($body) {
    $mailto      = 'nobody@example.com'; //收信人
    $subject = '錯誤回報'; //信件標題
    $headers = 'From: webmaster@example.com' . "\r\n" .
               'Reply-To: webmaster@example.com' . "\r\n" .
               'X-Mailer: PHP/' . phpversion();
    $message = '錯誤訊息:'."\r\n".$body; //訊息內容
    mail($mailto, $subject, $message, $headers); //發送Mail
    die('<h1>很抱歉,發生錯誤囉!</h1><p>我們已把你遇到的問題回報,請稍後再試</p>'); 
}
?>

範例:

$servername = "localhost";
$username = "username";
$password = "password";

// 建立連線
$conn = mysqli_connect($servername, $username, $password);

// 檢查連線若錯誤回傳訊息
if (!$conn) {
    die_send_email(mysqli_connect_error()); // 將die更換成上述function
}

php.ini 記得要設定 SMTP

[mail function]
; For Win32 only.
; http://php.net/smtp
SMTP = localhost
; http://php.net/smtp-port
smtp_port = 25

另外也可以透過 PHPMailer + Gmail 的方式發送 Mail : http://www.gocar.idv.tw/archives/72

PHP 連接 MySQL 資料庫

物件 ( MySQLi Object-Oriented ):

<?php
$servername = "localhost";
$username = "username";
$password = "password";

// 建立連線
$conn = new mysqli($servername, $username, $password);

// 檢查連線若錯誤顯示訊息
if ($conn->connect_error) {
    die("連線失敗: " . $conn->connect_error);
}
echo "連線成功";
?>

程序式( MySQLi Procedural ):

<?php
$servername = "localhost";
$username = "username";
$password = "password";

// 建立連線
$conn = mysqli_connect($servername, $username, $password);

// 檢查連線若錯誤顯示訊息
if (!$conn) {
    die("連線失敗: " . mysqli_connect_error()); 
}
echo "連線成功";
?>
一般系統出錯時不建議將錯誤訊息直接顯示,建議透過Mail的方式將錯誤訊息回報給工程師
錯誤Mail回報:http://www.gocar.idv.tw/archives/40