Category: Developer

งานพัฒนาระบบ, เขียนโปรแกรม

  • WordPress Content Security Policy (2)

    WordPress Content Security Policy (2)

    ครั้งก่อนนำเสนอปลั๊กอิน CSP-ANTS&STP ซึ่งปัจจุบันได้เปลี่ยนชื่อเป็น CSP Friendly Security แต่ถึงจะมีการปรับปรุงแล้วก็อาจเกิด Header Overflow ได้ในอนาคต ดูผลลัพธ์ของ CSP ที่ได้เป็นดังภาพ

    ซึ่งหลังจากคุยกับ Gemini ก็ได้รับคำแนะนำให้เปลี่ยน เนื่องจากมีโอกาสเกิด Header Overflow ได้ (ถึงแม้จะมีขนาดใหญ่มาก 8KB ถึง 16KB) ก็เลยให้เขียนใหม่ปรับปรุงจากรุ่นเดิมของ CSP Friendly Security โดยใช้ z.ai และ gemini.google.com ปรับปรุง

    สิ่งที่ต้องมี

    1. Woody snippets
    2. สร้าง new php snippets ใส่โค้ดต่อไปนี้แล้วเปลี่ยนเป็น run everywhere แล้วกดบันทึกให้เรียบร้อย
    CSP {PHP}
    add_action('template_redirect', 'csp_controller');
    
    function csp_controller() {
        // ตรวจสอบว่าเป็นหน้า Admin หรือหน้าเป้าหมายหรือไม่
        $target_pages = array('cd-key');
        if (is_admin() || is_page($target_pages)) {
            // ส่ง Simple Header แล้วจบการทำงานทันที (ไม่เปิด Buffer)
            if (!headers_sent()) {
                header("Content-Security-Policy: upgrade-insecure-requests");
            }
            return;
        }
    
        if (function_exists('litespeed_autoload')) {
            add_filter('litespeed_buffer_after', 'process_csp_buffer', 0);
        } else {
            ob_start('process_csp_buffer');
        }
    }
    
    function process_csp_buffer($content) {
        // ตรวจสอบว่าเป็นหน้า Admin หรือหน้าเป้าหมายหรือไม่
        $target_pages = array('cd-key');
        if (is_admin() || is_page($target_pages)) {
            return $content;
        }
    
        // สร้าง Nonce
        $nonce = base64_encode(random_bytes(16));
    
        // แทรก Nonce (ใช้รหัส Hex เพื่อหลบ Editor บั๊ก)
        $pattern = '#<(script|style)(?![^>]*\bnonce=)(?![^>]*\btype=[\x22\x27]application/(ld\+json|json)[\x22\x27])([^>]*)>#i';
        $content = preg_replace($pattern, "<$1 nonce='" . $nonce . "'$3>", $content);
    
        $sha256_csp = get_event_hashes($content);
        $uris = get_allowed_domains($content);
        $uri_string = implode(' ', $uris);
    
        $script_src = "script-src https: 'strict-dynamic' 'nonce-" . $nonce . "'";
        if (!empty($sha256_csp)) {
            $script_src .= " " . $sha256_csp;
        }
    
        // สร้าง Full CSP Header
        $csp_header = sprintf(
            "Content-Security-Policy: base-uri 'self' %s data:; object-src 'none'; %s; frame-ancestors 'none';",
            $uri_string,
            $script_src
        );
    
        if (!headers_sent()) {
            header($csp_header);
        }
    
        return $content;
    }
    
    // --- ฟังก์ชัน Helper (คงเดิม) ---
    
    function get_event_hashes($output) {
        $sha256 = array();
        if (preg_match_all('#\s(onload|onclick)\s*=\s*([\x22\x27])(.+?)\2#is', $output, $matches)) {
            foreach ($matches[3] as $event_code) {
                if (!empty($event_code)) {
                    $sha256[] = base64_encode(hash('sha256', $event_code, true));
                }
            }
        }
        if (class_exists('autoptimizeConfig')) {
            $sha256[] = base64_encode(hash('sha256', "this.onload=null;this.media='all';", true));
        }
        if (empty($sha256)) return '';
        $unique_hashes = array_unique($sha256);
        $formatted_hashes = array();
        foreach ($unique_hashes as $h) {
            $formatted_hashes[] = "'sha256-" . $h . "'";
        }
        return "'unsafe-hashes' " . implode(' ', $formatted_hashes);
    }
    
    function get_allowed_domains($string) {
        $result = array();
        $domains = array(
            'https://secure.gravatar.com/avatar/',
            'https://fonts.googleapis.com/',
            'https://maxcdn.bootstrapcdn.com/',
            'https://cdn.jsdelivr.net/'
        );
        foreach ($domains as $domain) {
            if (strpos($string, $domain) !== false) {
                $result[] = $domain;
            }
        }
        return $result;
    }
    1. เมื่อไปทดสอบที่ https://securityheaders.com/
    2. ก็จะได้ CSP ใหม่ที่สั้นลง
    content-security-policybase-uri ‘self’ data:; object-src ‘none’; script-src https: ‘strict-dynamic’ ‘nonce-sdfadsdfasdfadsfadsfa==’; frame-ancestors ‘none’;
    1. ก็ยังมี recomendation ที่ต้องปรับปรุงอีกนิดหน่อยค่อยๆ ทำไป
    2. จบ… ขอให้สนุก
  • บันทึกการติดตั้งเพื่อใช้งานบริการระบบยืนยันตัวตน PSU Passport (Authentik)

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

    ต่อไปนี้จะแสดงตัวอย่างการ implement ระบบเพื่อติดตั้งใช้งาน PSU Passport (Authentik) ด้วยภาษา PHP

    (more…)
  • การกำหนดค่าพื้นฐานความปลอดภัยสำหรับ IIS และ WordPress บน Windows Server

    เพื่อให้ Web Server ของเราปลอดภัยจากการถูกโจมตี บทความนี้จะเป็นการแนะนำการกำหนดค่าต่างๆของ web server ที่ให้บริการ ซึ่งทำงานด้วยบน Windows Server และ มีการติดตั้ง IIS, PHP, MySql, ASP.Net และ WordPress

    • การกำหนดส่วนของ Windows Server อ้างอิงคำแนะนำจาก Quays SSL Labs ให้ได้ระดับ A ขึ้นไป
      1. ใช้ใบรับรองจาก CA ที่น่าเชื่อถือ และ ใช้การ RSA 2048 bits (SHA256withRSA) ขึ้นไป
      2. การกำหนด Cipher Suites ที่ปลอดภัย ซึ่งจะมีเครื่องมือที่ช่วยในการกำหนดดังนี้
        • IIS Crypto เป็นโปรแกรมฟรีไม่ต้องติดตั้งสำหรับช่วยจัดการกำหนด protocols, ciphers, hashes and key exchange algorithms บน Windows Server โดยกำหนดพื้นฐานดังนี้
          1. เมนู Schannel
          1.1 Protocols เลือกกำหนดใช้งาน TLS 1.2 และ/หรือ TLS 1.3 เท่านั้น
          1.2 Cipher เลือกกำหนดเป็น AES
          1.3 Hashes เลือก SHA 256 ขึ้นไป
          1.4 Key Exchanges สามารถเลือกได้ทั้ง Diffie-Hellman, PKCS และ ECDH
          2. เมนู Cipher Suites สามารถกำหนด Cipher Suites ที่ปลอดภัยในปัจจุบัน ซึ่งค้นหาได้จากเว็บ https://www.tenable.com/plugins/nessus/156899
      3. เป็นส่วนของการกำหนดใน IIS
        • การกำหนดสำหรับ Security Headers ให้ได้ระดับ A+ อ้างอิงคำแนะนำจากเว็บ https://securityheaders.com/
        • การจัดการ Http Response Header โดยกำหนดค่าดังนี้
          1. X-Frame-Options เป็นการกำหนดเพื่อหลีกเลี่ยงจากการถูกโจมตีด้วย Clickjacking
            ตัวอย่างการกำหนดค่าเป็น SAMEORIGIN
          2. X-XSS-Protection เป็นการป้องกันการโหลดสคริปต์ข้ามไซต์
            ตัวอย่างการกำหนดเป็น 1; mode=block
          3. X-Content-Type-Options เป็นการป้องการโจมตีเนื้อหาประเภท MINE (Multipurpose Internet Mail Extensions) ซึ่งเป็นรูปแบบที่ใช้ระบุประเภทของข้อมูลที่ถูกส่งผ่านเครือข่าย หรือเก็บที่เครื่องอุปกรณ์ มันช่วยบอกให้ระบบรับรู้ว่าไฟล์เป็นประเภทไหนและวิธีการจัดการข้อมูลนั้น ที่อาจถูกใช้ในการโจมตีเพื่อหลอกลวงระบบหรือละเว้นมาตรฐานการตรวจสอบปลอดภัย เช่น application/pdf, image/jpeg, text/html
            ตัวอย่างการกำหนดค่าเป็น nosniff
          4. Referrer-Policy เป็นการควบคุมการส่งผ่านส่วนอ้างอิง เช่น ป้องกันส่วน HTTPS ไม่ให้กลับไป HTTP ที่ไม่ปลอดภัย
            ตัวอย่างการกำหนดค่าเป็น no-referrer-when-downgrade
          5. Strict-Transport-Security เป็นการช่วยให้การเข้าเว็บไซต์ด้วย HTTPS เท่านั้น
            ตัวอย่างการกำหนดค่าเป็น max-age=31536000; includeSubDomains; preload
          6. Content-Security-Policy เป็นการระบุที่มาของเนื้อหาที่ได้รับอนุญาตให้โหลดบนเว็บไซต์ เช่น JavaScript เพื่อป้องกันการโจมตีแบบ Cross-Site Scripting (XSS)
            ตัวอย่างการกำหนดค่าเป็น upgrade-insecure-requests
          7. Permissions-Policy เป็นการควบคุมการเปิดใช้งานเช่น กล้อง หรือ ไมโครโฟน หรือ ฟีเจอร์อื่น ๆ
            ตัวอย่างการกำหนดค่า เช่น geolocation=(), camera=(), microphone=()
        • การปกปิดเวอร์ชันไม่แสดงในส่วนของ Header สามารถกำหนดเพิ่มเติมดังนี้
          1. เมนู IIS Manager –> Configuration Editor
            • Section: system.webServer/security/requestFiltering กำหนด removeServerHeader เป็น True เพื่อไม่ให้แสดง เวอร์ชันของ server
            • Section: system.web/httpRuntime กำหนด enableVersionHeader เป็น False เพื่อไม่ให้แสดงเวอร์ชันของ IIS หรือ ASP.Net
          2. กำหนด expose_php = Off ใน php.ini เพื่อไม่ให้แสดงเวอร์ชันของ php
          3. ลบ X-Powered-By ออกจาก HTTP Response Headers
      4. กำหนด IP Address ส่วนของ Remote Address ใน Windows Defender Firewall with Advance Security – Inbound Rules เพื่อควบคุมการเข้าถึงจากคอมพิวเตอร์ที่ได้รับอนุญาตเท่านั้น
      5. ปิด port ที่ไม่ได้ใช้งาน
    • การกำหนดส่วนของเว็บ
      1. การเรียกใช้งานไรบรารีจากภายนอกเว็บไซต์ เช่น เดิม จะมีการเรียกใช้โดยอ้างอิงแบบ
        src=”https://code.jquery.com/jquery-3.7.1.min.js” ซึ่งจะไม่มีการตรวจสอบความน่าเชื่อถือ
        เพื่อสร้างความเชื่อมั่นว่าไรบรารีที่ใช้งานจะไม่มีการเปลี่ยนแปลง จึงมีการเพิ่มส่วนการตรวจสอบ integrity และ crossorigin ซึ่งสามารถเลือกใช้งาน Code Integration ได้จากเว็บ https://releases.jquery.com/jquery/ หรือ https://cdnjs.com/libraries ดังตัวอย่างนี้
        • src=”https://code.jquery.com/jquery-3.7.1.min.js” integrity=”sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=” crossorigin=”anonymous” หรือ
        • src=”https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js” integrity=”sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==” crossorigin=”anonymous” referrerpolicy=”no-referrer”
      2. การป้องกันการเรียกดู user data ผ่าน REST API ใน WordPress กรณีนี้ควรติดตั้งส่วนเสริมไม่ให้สามารถเรียกใช้งานผ่าน REST API โดยไม่มีการยืนยันตัวตนก่อน เช่น Disable WP REST API
    • หลังจาก กำหนดค่าเรียบร้อยแล้ว สามารถทดสอบได้ที่ https://www.ssllabs.com/ssltest/analyze.html คลิกเลือก Do not show the results on the boards ก่อนสแกน ด้วยครับ
    • Windows Server 2019 รองรับ TLS 1.2
    • Windows Server 2022 รองรับ TLS 1.2 และ TLS 1.3
      หมายเหตุ ทั้งนี้ Windows Server 2022 เพิ่มการรองรับ TLS 1.3 อย่างไรก็ตาม หากเปิดใช้งานทั้ง TLS 1.2 และ 1.3 Site Scanner จะส่งผลให้ได้เกรด A เท่านั้น เนื่องจากปัจจุบัน Windows Server ไม่รองรับการป้องกันการโจมตีแบบดาวน์เกรด หากไคลเอนต์ร้องขอ TLS 1.3 Windows จะยังคงอนุญาตให้ปรับไปใช้ TLS 1.2 ได้ และนั่นคือสาเหตุที่ Site Scanner รายงานเกรด A แทนที่จะเป็น A+
  • วิธีการติดตั้ง SDK Sentry บน Platforms .NET (Asp.Net)

    Step 1 ลง Sentry.AspNet

    ผ่าน NuGet Package Manager โดยใช้คำสั่ง

    Install-Package Sentry.AspNet -Version 3.20.1

    Step 2 Create project ผ่าน Sentry

    เลือก Server เป็น ASP.NET และตั้งชื่อ aspnet-e-admission จากนั้น ก็กดปุ่ม Create Project

    เมื่อกดสร้างเรียบร้อยแล้ว จะเห็น Project ชื่อ aspnet-e-admission ตามรูป

    เมื่อคลิกดูรายละเอียด จะแสดงดังภาพ

    Step 3 Project Settings>Client Keys

    นำค่า DSN ที่อยู่ในส่วน Client Keys ไปใช้งานต่อในส่วนของตั้งค่า Web.config

    ศึกษาวิธีการตั้งค่า SDK Sentry บน Platforms .NET

    Step 1 ตั้งค่า Web.config

    เพิ่ม key “SentryDsn” และค่า value ได้มาจาก Project Settings>Client Keys

    Step 2 ตั้งค่า Global.asax.cs

    2.1 เรียก library sentry ที่ต้องใช้งาน

    2.2 ตั้งค่า Application_Start

    2.3 ตั้งค่า Application_BeginRequest

    2.4 ตั้งค่า Application_EndRequest

    2.5 ตั้งค่า Application_Error

    2.6 ตั้งค่า Application_End

    Step 3 ทดสอบรันเวบ และเปิด projects ใน Sentry

    เมื่อทดสอบรันเวบ ระบบรับสมัครนักศึกษาออนไลน์ (PSU Admission) จะเห็น Latest Releases ถ้าเลขเวอร์ชันตรงกับที่รัน แสดงว่าติดตั้งสำเร็จ

    หวังว่า km จะมีประโยชน์ไม่มากก็น้อยนะคะ

    ที่มา https://docs.sentry.io/platforms/dotnet/guides/aspnet/

  • Mail ขอนัดประชุมต้องประกอบด้วยอะไรบ้าง

    ช่วงนี้เป็นช่วงที่ต้อง Mail ขอนัดประชุมเพื่อขอข้อมูลเพื่อจัดทำ Data Lake จากหลายๆหน่วยงานเป็นจำนวนมาก เลยลองตั้งคำถามว่า “mail ขอนัดประชุมควรจะประกอบด้วยหัวข้ออะไรบ้าง” ถึงจะครบถ้วน เหมาะสม สือสารตรงจุด ผู้รับ Mail อยากจะตอบรับ อยากประชุมกับเรา ผลการค้นหาและประมาณผลด้วยตัวเองออกมาประมาณนี้ครับ

    Subject Mail
    Subject Mail ต้องชัดเจนและกระชับ ช่วยให้ผู้เห็น Subject Mail แล้วพอจะรับรู้ได้ว่าเนื้อ Mail จะเกี่ยวกับเรื่องอะไร
    เช่น
    ขอนัดประชุมเรื่อง….
    ขอเชิญประชุมเรื่อง…
    ขอนำเสนอผลงานเรื่อง…
    ขอนัดปรึกษาเรื่อง…
    ขอชี้แจ้งเรื่อง…
    เริ่มเนื้อ Mail ด้วยการทักทายที่สุภาพและเหมาะสมกับความสัมพันธ์ ตำแหน่งหน้าที่ วัยวุฒิ
    ทักทายด้วยตำแหน่ง สำหรับผู้ใหญ่ ผู้บริหาร
    เช่น เรียน ่ทานผู้อำนวยการ../่ท่านคณบดี
    ทักทายด้วยสวัสดี สำหรับทั่วไปและคนที่รู้จักกันมาก่อน
    เกริ่นนำ ที่มาที่ของการขอนัดประชุม วัตถุประสงค์ของการขอประชุม
    ถึงจะมีการพูดคุยกันมาก่อนแล้วก็ตาม เกริ่นนำ และบอกวัตถุประสงค์ก็ต้องมีอย่างชัดเจน อย่างคิดว่าเคยรับรู้หรือเคยคุยมาแล้วไม่ต้องบอก เพราะบางครั้งก็มีลืมกันบ้าง หรือทางผู้รับ mail อาจจะ forword mail ต่อไปให้ท่านอื่นๆที่ไม่ได้รับรู้เรื่องที่ขอนัดประชุมมาก่อน
    วาระหรือหัวข้อที่จะประชุมพร้อมเวลาที่จะใช้ในการประชุม
    อธิบายวาระการประชุมเพื่อสังเขป
    และเวลาที่จะใช้ในการประชุม เช่น 1 ช.ม. 2 ช.ม. เพื่อถ้าเป็นการประชุมกับผู้บริหารทางผู้รับ Mail จะได้กำหนดเวลาได้ชัดเจน
    ถ้าการประชุมครั้งนี้เกินขึ้นหรือสำเร็จ ผู้เข้าประชุมจะได้รับอะไรที่เป็นประโยชน์กับตัวบุคคลหรือหน่วยงานบ้าง
    หัวข้อนี้ต้องให้ความสำคัญที่สุดเพราะเป็นหัวข้อที่ทำให้ผู้รับ Mail เห็นถึงประโยชน์ที่จะได้รับเมื่อมีการประชุมครั้งนี้เกิดขึ้น
    เตรียมตัวหรือเตรียมข้อมูลในการประชุมสำหรับผู้รับ Mail
    แจ้งคำขอให้มีการเตรียมตัวก่อนการประชุม หรือสิ่งที่ต้องเตรียมก่อนการประชุม
    เพื่อความพร้อมในการประชุม
    วัน เวลาและสถานที่ (Online/Onsite ) ขอนัดประชุม
    รูปแบบการประชุม Online หรือ Onsite
    วันและเวลา จะดีมากถ้ามีช่วงวันและเวลาให้ทางผู้รับ mail เลือกเพื่อช่วยการติดสินใจ
    ข้อความแสดงความขอบคุณสำหรับเวลาและการพิจารณาของผู้เข้าประชุม
    “ขอขอบคุณในการพิจารณาวันและเวลานัดหมายประชุมในครั้งนี้”
    “ขอขอบคุณในการเสียสละเวลาพิจารณาวันและเวลานัดหมายประชุมในครั้งนี้”
    ข้อมูลเพื่อให้ผู้รับ Mail ติดต่อกลับ
    เพื่อให้ผู้รับ Mail ติดต่อได้ง่ายและตามช่องทางที่ทางผู้รับ Mail สะดวก
    เช่น
    “กรุณาแจ้งกลับภายในวันที่ [วันที่] เวลา [เวลา]
    คุณ/ท่าน สามารถติดต่อกลับได้ที่
    [ชื่อ นามสกุล]
    [หมายเลขโทรศัพท์]
    หรือ
    [ที่อยู่อีเมล]”

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

    ขอบคุณทุกท่านที่แวะมาอ่านนะครับ

  • เทคนิคการเลื่อนสลับแถว (Drag & Drop) ใน Table ด้วย JavaScript

    บล็อกนี้น่าจะเป็น EP สุดท้ายของซีรี่ส์ Drag & Drop row ใน HTML Table แล้ว จากบล็อกที่ผ่านๆ มา ผู้เขียนได้ใช้เทคนิคต่างๆ โดยพึ่งพา framework ที่ใช้ในการพัฒนาโปรแกรม เช่น ASP.NET, Blazor หรือ jQuery แต่สำหรับบล็อกนี้เราจะเขียนโดยใช้ JavaScript ล้วนๆ บวกกับ CSS จาก Bootstrap อีกนิดหน่อยเพื่อความสวยงามของ Table และเพื่อไม่ให้เป็นการเสียเวลา เรามาลุยกันเลยครับ

    1. เริ่มต้นด้วยการเตรียมไฟล์ HTML ดังโค้ดด้านล่าง

    <!DOCTYPE html>
    <html lang="en">
    	<head>
    		<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    		<title>Drag & Drop Table Row</title>
    		
    		<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
    	</head>
    	<body style="padding:10px">
    		
    		<table style="width:90%;" class="table table-striped">
    			<thead>
    				<tr >
    					<th>ลำดับ</th>
    					<th>จังหวัด</th>
    				</tr>
    			</thead>
    			<tbody id="provinceList">
    			</tbody>
    		</table>
    		
    	</body>
    </html>

    2. เพิ่ม code JavaScript สำหรับเพิ่มข้อมูลลงใน Table ใน event window.onload โดยเราจะเพิ่มแท็ก <tr> ใน element tbody ที่มี id=”provinceList” และระบุ class=”prv-item” สำหรับใช้ในการอ้างถึงจากโค้ด JavaScript

    <script>
    
    window.onload = async () => {
    
    	const provinces = ['สงขลา','ปัตตานี','ยะลา','นราธิวาส','สตูล'];
    	for (let i = 0; i < provinces.length; i++) {
    		provinceList.innerHTML += "<tr class='prv-item' data-value=" + i + "><td> " + (i + 1) + "</td><td>" + provinces[i] + "</td></tr>";
    	};
    
    };
    
    </script>

    ซึ่งจะได้ผลลัพธ์หน้าจอดังรูป และเป้าหมายของเราก็คือสามารถคลิกเลือกจังหวัด จากนั้นลากและวางไปยังตำแหน่งที่ต้องการได้

    3. และเพิ่มโค้ด JavaScript สำหรับผูก event ต่างๆ เข้ากับแต่ละ <tr> ที่เพิ่มเข้าไป ใน event window.onload เพื่อให้สามารถลากและวางได้

    let listPrvItems = document.querySelectorAll(".prv-item");
    listPrvItems.forEach((element) => {
    	element.draggable = true;
    	element.addEventListener("dragstart", dragStart, false);
    	element.addEventListener("dragover", dragOver, false);
    	element.addEventListener("drop", drop, false);
    });

    4. เพิ่มโค้ด JavaScript ส่วนควบคุม event ต่างๆ คือ เมื่อเริ่มกดลาก เมื่อกดวาง และฟังก์ชันสำหรับหาตำแหน่งของแถว (<tr>) ที่เริ่มกดลาก ดังนี้

    //หาลำดับของ tr จาก value ที่ส่งมา ทำการวนลูปเทียบกับ data-value ใน tr ของตารางทั้งหมด
    const getPosition = (value) => {
    	let elementIndex;
    	let listPrvItems = document.querySelectorAll(".prv-item");
    	listPrvItems.forEach((element, index) => {
    		let elementValue = element.getAttribute("data-value");
    		if (value == elementValue) {
    			elementIndex = index;
    		}
    		});
    	return elementIndex;
    };
    
    //เก็บตำแหน่ง x,y และ element ที่เริ่มคลิกลาก
    function dragStart(e) {
    	initialX = e.clientX;
    	initialY = e.clientY;
    	//Set current Element
    	currentElement = e.target;
    }
    
    function dragOver(e) {
    	e.preventDefault();
    }
    
    //เหตุการณ์วาง
    const drop = (e) => {
    	e.preventDefault();
    
        // เก็บตำแหน่ง x, y ที่วาง
    	let newX = e.clientX;
    	let newY = e.clientY;
    
    	//Set targetElement(where we drop the picked element).It is based on mouse position
        // เก็บ element ที่จะถูกวางแทนที่
    	let targetElement = document.elementFromPoint(newX, newY);
    
        // ตรวจสอบว่า element ที่ถูกวาง เป็น TD หรือไม่ ถ้าใช่ จะเปลี่ยนให้เป็น TR ที่เป็น parent ของ TD นั้น
        // เนื่องจากการลากวาง จะเป็นการลากวางและแทนที่ทั้งแถว ซึ่งก็คือ TR
    	if(targetElement.nodeName == "TD")
    	{
    		targetElement = targetElement.parentNode;
    	}
    
        // เก็บ value จาก element ต้นทางและปลายทาง
    	let currentValue = currentElement.getAttribute("data-value");
    	let targetValue = targetElement.getAttribute("data-value");
    
    	//หาตำแหน่ง TR จาก value
    	let [currentPosition, targetPosition] = [
    		getPosition(currentValue),
    		getPosition(targetValue),
    	];
    	initialX = newX;
    	initialY = newY;
    
    	try {
    
    	if (currentPosition < targetPosition) {
          
          // ถ้าแถวที่เริ่มลาก น้อยกว่าแถวที่จะวาง จะทำการแทรกในตำแหน่งด้านล่างของแถวที่จะวาง
    	  targetElement.insertAdjacentElement("afterend", currentElement);
    	} else {
    
          // ถ้าแถวที่เริ่มลาก มากกว่าแถวที่จะวาง จะทำการแทรกในตำแหน่งด้านบนของแถวที่จะวาง
    	  targetElement.insertAdjacentElement("beforebegin", currentElement);
    	}
    	} catch (err) {}
    }; 

    4. เมื่อทดสอบการทำงาน รายชื่อจังหวัดในตารางจะสามารถคลิก ลาก และวางไปยังตำแหน่งที่ต้องการได้

    5. และเมื่อ drop ลงไปยังตำแหน่งที่ต้องการ จังหวัดที่ลากมาก็จะแทรกเข้าไปยังตำแหน่งที่ drop

    จากซีรี่ส์ Drag & Drop Row ใน Table อันยาวนานนี้ ผู้เขียนหวังเป็นอย่างยิ่งว่าจะมีประโยชน์กับผู้อ่านไม่มากก็น้อยที่จะนำไปประยุกต์ใช้กับงานของตัวเอง แล้วเจอกันใหม่โอกาสหน้า เมื่อผู้เขียนจะมีเหตุให้ต้องเข้ามาอีก สวัสดีครับ


    แหล่งข้อมูลอ้างอิง

  • เขียน JavaScript RegExp กับข้อมูล sensitive ให้แสดง 4 ตัวอักษรสุดท้าย

    เขียน JavaScript กับข้อมูลที่ sensitive โดยใช้ RegExp เพื่อแสดงบางส่วน เนื่องจากทางผู้เขียนได้รับ requirement ให้ปรับปรุงข้อมูลที่อ่อนไหว ให้แสดงบางส่วน อย่างเช่น เลขบัตรประชาชน

    โดยก่อนอื่นจะอธิบายความหมายสัญลักษณ์ RegExp ตัวอย่างเช่น

    สัญลักษณ์ความหมาย
    .หาตัวอักษรเดียวและยกเว้น newline หรือ line terminator
    เช่น คำว่า “Thailand Finland” หากใช้ pattern /./ จะได้ผลลัพธ์ “T”
    n{X}นับจำนวนอักษรทั้งหมดตามจำนวน X ตัวอักษร
    เช่น คำว่า “Thailand Finland” หากใช้ pattern /.{4}/ จะได้ผลลัพธ์ “Thai”
    ?=nหาตัวอักษรทั้งหมดทีมี n
    เช่น คำว่า “Thailand Finland” หากใช้ pattern /.(?=land)/ จะได้ผลลัพธ์ “i”
    gหาตัวอักษรที่ตรงทั้งหมดและจะหยุดเมื่อเจอผลลัพธ์แรก
    เช่น คำว่า “Thailand Finland” หากใช้ pattern /.(?=land)/g จะได้ผลลัพธ์ “i,n”

    จากตัวอย่างคำว่า “Thailand Finland” หากใช้ pattern /.(?=.{4})/g จะได้ผลลัพธ์ “T,h,a,i,l,a,n,d, ,F,i,n”

    จากตัวอย่างคำว่า “1234567890123” หากใช้ pattern /.(?=.{4})/g จะได้ผลลัพธ์ “1,2,3,4,5,6,7,8,9”

    จากนั้นใช้ function replace()

    Note: To replace all matches, use a regular expression with a /g flag (global match)

    จากรูป แสดงการเขียน JavaScript แสดงบางส่วนโดยที่แสดง 4 ตัวอักษรสุดท้าย ส่วนอักษรอื่นเปลี่ยนเป็น x
    ผลลัพธ์จากการใช้งานจริง

    หวังว่า km จะมีประโยชน์ไม่มากก็น้อยนะคะ

    ที่มา https://www.w3schools.com/jsref/jsref_obj_regexp.asp

  • เทคนิคการขยับแถวขึ้นลงใน Table ด้วยวิธี Drag & Drop ใน Blazor

    สำหรับ blog นี้ของผู้เขียน ถือว่าเป็นซีรี่ส์ที่ต่อเนื่องมาจาก https://sysadmin.psu.ac.th/2021/05/25/ขยับแถว-row-ขึ้น-ลง-ใน-asp-net-gridview-ด้วย-j/ และ https://sysadmin.psu.ac.th/2021/05/27/ขยับแถว-row-ขึ้น-ลง-ใน-asp-net-gridview-ด้วย-c/ ก็คือผู้เขียนจะนำเสนอวิธีการขยับแถวอีกวิธีหนึ่ง ที่น่าจะเฟรนด์ลี่ต่อผู้ใช้มากกว่า นั่นก็การ Drag & Drop ก็คือผู้ใช้งานสามารถเลือกคลิก ลาก และวาง เพื่อจัดลำดับได้ตามความต้องการ ซึ่งเมื่อผู้เขียนมาทำงานบน Blazor พบว่าการ implement เรื่องนี้สามารถทำได้ง่ายมาก โดยไม่ต้องพี่งพา javascript แต่อย่างใด เรามาดูกันเลยครับ

    1. เพิ่มโค้ด HTML ในส่วนของการแสดงผลข้อมูลในรูปแบบ Table

    @if (provinces != null)
    {
        <table class="table table-striped" >
            <thead>
                <tr >
                    <th>ลำดับ</th>
                    <th>จังหวัด</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var pr in provinces)
                {
                    <tr role="button" class="cursor-pointer"  >
                        <td>@pr.ID</td>
                        <td>@pr.Name</td>
                    </tr>
                }
            </tbody>
        </table>
    }

    2. เพิ่มโค้ด C# ในส่วนที่ควบคุมการทำงานและจำลองข้อมูล

    private List<Province> provinces = new List<Province>();
    
    protected override void OnInitialized()
    {
    
    	provinces.AddRange(new List<Province> {
    		new Province(1,"สงขลา"),
    		new Province(2,"ปัตตานี"),
    		new Province(3,"ยะลา"),
    		new Province(4,"นราธิวาส"),
    		new Province(5,"สตูล")
    	});
    
    }
    
    public class Province
    {
    	public Int32 ID;
    	public string Name;
    	public Province(int id, string name)
    	{
    		ID = id;
    		Name = name;
    	}
    }

    ซึ่งจะได้ผลลัพธ์หน้าจอดังรูป และเป้าหมายของเราก็คือสามารถคลิกเลือกจังหวัด จากนั้นลากและวางไปยังตำแหน่งที่ต้องการได้

    3. จากนั้นเพิ่มโค้ดส่วนที่ควบคุมการ Drag และ Drop ทั้งใน HTML และ C#

    @if (provinces != null)
    {
        <table class="table table-striped" ondragover="event.preventDefault();">
            <thead>
                <tr >
                    <th>ลำดับ</th>
                    <th>จังหวัด</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var pr in provinces)
                {
                    <tr role="button" class="cursor-pointer" draggable="true"
                @ondrop="@(()=> Drop(pr))" @ondrag="@(e => StartDrag(pr))" >
                        <td>@pr.ID</td>
                        <td>@pr.Name</td>
                    </tr>
                }
            </tbody>
        </table>
    }
    • ondragover=”event.preventDefault();” เป็นการขัดขวางการทำงานปกติของการ drag บน object เพื่อให้สามารถ drop ได้
    • @ondrop=”@(()=> Drop(pr))” เรียกฟังก์ชัน Drop พร้อมกับส่ง pr (จังหวัด) เมื่อมีเหตุการณ์ drop เกิดขึ้น
    • @ondrag=”@(e => StartDrag(pr))” เรียกฟังก์ชัน StartDrage พร้อมกับส่ง pr เมื่อมีเหตุการณ์ drag เกิดขึ้น
    /// <summary>
    /// เก็บตำแหน่งของจังหวัดที่คลิกเลือก เมื่อเริ่ม drag
    /// </summary>
    /// <param name="province"></param>
    private void StartDrag(Province province)
    {
    	currentIndex = GetIndex(province);
    }
    
    /// <summary>
    /// หาตำแหน่งของจังหวัดที่อยู่จาก list
    /// </summary>
    /// <param name="province"></param>
    /// <returns></returns>
    int GetIndex(Province province)
    {
    	return provinces.FindIndex(p => p.ID == province.ID);
    }
    
    /// <summary>
    /// เมื่อ Drop จังหวัดที่ drag มาลงในตำแหน่งที่ต้องการ
    /// </summary>
    /// <param name="province"></param>
    void Drop(Province province)
    {
    	if (province != null)
    	{
    		// หาตำแหน่งของจังหวัดที่ถูก drop
    		var index = GetIndex(province);
    
    		// หาจังหวัดที่ถูก drag มา จาก index ที่เก็บไว้ตั้งแต่เริ่ม drag
    		var current = provinces[currentIndex];
    
    		// ลบจังหวัดที่ถูก drag มา ออกจาก list
    		provinces.RemoveAt(currentIndex);
    
    		// แทรกจังหวัดที่ถูก drag มา ลงในตำแหน่งที่ drop
    		provinces.Insert(index, current);
    
    		StateHasChanged();
    	}
    }

    4. เมื่อทดสอบการทำงาน รายชื่อจังหวัดในตารางจะสามารถคลิก ลาก และวางไปยังตำแหน่งที่ต้องการได้

    5. และเมื่อ drop ลงไปยังตำแหน่งที่ต้องการ จังหวัดที่ลากมาก็จะแทรกเข้าไปยังตำแหน่งที่ drop

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

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


    แหล่งข้อมูลอ้างอิง

  • เทคนิคการดึงข้อมูล Youtube Video ผ่าน Youtube API

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

    โจทย์ที่ว่าก็คือ จะต้องดึงข้อมูลรายละเอียดต่างๆ ของ Youtube จาก URL ที่ระบุ ไม่ว่าจะเป็น Thumbmail, Title, Description ซึ่งจากการไปศึกษา API ที่ Youtube ได้จัดเตรียมไว้ให้ ก็พบว่าสามารถใช้งานได้อย่างไม่ยุ่งยากแต่อย่างใด เพียงแต่เราจะต้องมี API Key ที่ต้องใช้ในการเข้าถึงข้อมูลต่างๆ ของ Youtube ดังนั้นเริ่มต้นเราจะไปดูวิธีการให้ได้มาซึ่ง API Key ก่อน

    1. เข้าไปที่ https://console.cloud.google.com/cloud-resource-manager และ login ด้วย google account ให้เรียบร้อย จากนั้นทำการสร้าง Project ใหม่

    2. ป้อนชื่อโปรเจ็ค ในที่นี้สมมติเป็น Sample Project กด CREATE

    3. จากนั้นให้ทำการ Enable API เพื่อให้โปรเจ็คที่เราสร้างสามารถใช้งานได้ โดยให้ไปที่เมนูแฮมเบอร์เกอร์ -> APIs & Services -> Enabled APIs & services

    4. กด + ENABLE APIS AND SERVISES

    5. ค้น Youtube Data API

    6. เลือก YouTube Data API v3 และกด Enable

    7. กด Enable

    8. กลับไปที่ Enable APIS And Services และเลือก YouTube Data API v3

    9. เลือก CREATE CREDENTIALS

    10. เลือก Public data และกด NEXT

    11. จะได้ API Key ที่จะนำไปใช้ในการเขียนโปรแกรมเพื่อดึงข้อมูลจาก Youtube API ให้สำเนาเก็บไว้ใช้งานในขั้นตอนต่อไป

    เมื่อเราได้ API Key มาแล้ว เราก็จะมาถึงขั้นตอนในการเขียนโปรแกรมกันต่อ เนื่องจากในตอนนี้ผู้เขียนพัฒนาโปรเจ็คต่างๆ ด้วย Blazor ซึ่งเป็นเฟรมเวิร์คปัจจุบันของ ASP.NET ตัวอย่างโค้ดผู้เขียนก็จะขอเขียนด้วย Blazor ซึ่งในส่วนของการติดต่อ API ก็น่าจะพอเป็นแนวทางให้กับการพัฒนาด้วยเฟรมเวิร์คอื่นหรือภาษาอื่นได้ และเพื่อไม่ให้บล็อกนี้ยาวเกินไป ผู้เขียนขอข้ามขั้นตอนการสร้างโปรเจ็คไปเลย

    1. เริ่มต้นเพื่อให้โปรเจ็คของเราสามารถใช้งาน Youtube API ได้ เราจะต้องติดตั้ง Library ที่จำเป็นก่อน โดยเราจะติดตั้งผ่านเครื่องมือที่เรียกว่า Nuget โดยคลิกขวาที่โปรเจ็ค เลือก Manage Nuget Packages…

    2. ค้น Youtube เลือก Google.Apis.Youtube.v3 และ Install

    3. และเพื่อให้ page ที่เราจะเขียนโปรแกรมติดต่อกับ Youtube API สามารถใช้งาน Library ดังกล่าวได้ เราจะต้องเพิ่ม using Library นั้นๆ เข้ามาก่อน

    @using Google.Apis.Services
    @using Google.Apis.YouTube.v3
    @using Google.Apis.YouTube.v3.Data
    

    4. สร้าง YoutubeService เพื่อใช้ในการติดต่อ API และเราจะกำหนด API Key ใน object นี้ ดังโค้ดตัวอย่าง

    var youtubeService = new YouTubeService(new BaseClientService.Initializer()
    {
        ApiKey = "XXxxXxXxXXX0xxxxXXxxxXxXXxxxxXXxX0XX0x0",
        ApplicationName = this.GetType().ToString()
    });

    5. สร้าง object ListRequest และกำหนดข้อมูลที่ต้องการเป็น snippet ผ่านเมทธอด List ซึ่ง snippet จะเป็น object ที่บรรจุข้อมูลต่างๆ ของวิดีโอยูทูปนั้นๆ

    VideosResource.ListRequest req = youtubeService.Videos.List("snippet");

    6. กำหนดค่าแปรวิดีโอที่เราต้องการดึงข้อมูล ซึ่งจะเป็น parameter ที่อยู่หลังตัวแปร v เช่น https://www.youtube.com/watch?v=4VTx7oIzv_8 ค่าที่เราต้องการคือ 4VTx7oIzv_8

    req.Id = "4VTx7oIzv_8";

    7. จากนั้นทำการดึงข้อมูลจาก Youtube โดยเรียกเมทธอด Execute และดึงข้อมูลที่ต้องการจาก property ต่างๆ ที่อยู่ใน object Snippet

    VideoListResponse resp = req.Execute();
    if (resp.Items.Count > 0)
    {
    	title = resp.Items.ElementAt(0).Snippet.Title;
    	description = resp.Items.ElementAt(0).Snippet.Description;
        ThumbnailDetails tmb = resp.Items.ElementAt(0).Snippet.Thumbnails;
        thumbnail = tmb.Default__.Url;
    }

    8. และโด้ด HTML ส่วนแสดงผล

    <p><b>URL : </b> @url</p>
    <p><b>Title : </b> @title</p>
    <p><b>Description : </b> @title</p>
    <p><b>Thumbnail : </b> </p>
    <img src="@thumbnail" />

    9. จะได้ผลลัพธ์ดังตัวอย่าง

    10. นอกจากนี้ผู้อ่านสามารถดึงข้อมูลอื่นๆ ที่เกี่ยวข้องกับ video นั้นๆ ได้ผ่าน object Snippet รวมทั้งมี object ย่อยๆ อีกมากมายอย่างเช่น Thumbnails เป็นต้น หรือผู้อ่านจะทดสอบการทำงานได้ง่ายๆ ผ่าน query string โดยให้แทนที่ API Key ด้วยคีย์ของท่านเอง และ Video ID ด้วย ID ของ Video ที่ต้องการได้เลย ดังตัวอย่าง

    https://www.googleapis.com/youtube/v3/videos?part=snippet&id={video IDs}&key={API key}

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


    แหล่งข้อมูลอ้างอิง