2024年5月29日 星期三

2024 about time


前幾年趁 apple tv+ about time 特價台幣 90元收藏了。老實說,不曾再完整看過,夜深人靜時,還是會回想起片段的劇情,特別是人生的選擇,一旦做了選擇,正向的說,產生了新的變化,負面來說,已經無法退回到過去。

翻一下 blog ,原來七年前也曾 murmur 過,哈。

近幾年的變化,可能更加重偏心靈層面?似乎跟七年前不太一樣,唯一不變的是對時間的焦愁,只能不斷提醒自己,有些事該放下的。然後,開始重視人生資源的分配,六個罐子,雖然是被用在理財角度,其實最貴的是時間,包括自己周邊的親友和人事物。

像工作上碰到處理事情效率不佳的情況時,會退回一步想,是啊,大家都是混口飯吃的 XD 儘管資源重組(裁員)已成常態,但別把這當理由強推。許多管理書籍的重點也落在 SOP 這領域,重點是讓事情有進展,並且不依賴超強人才特質,實務上 overqualified 的人才也待不久的,卻也令人期待人才蛻變後,主管們多了這甜蜜的煩惱。

關於親友之間,反而被些大學好友不斷互相砥礪,持續增加自己的視野,但也能認清階級的差距。因資源不同而視野不同,煩惱也不同,絕對不會因此沒煩惱的。想起幾年跟總經理聊上幾句,我跟他分享同輩的在哪些美商陸商發展,他直接分享十多年前的別人挖角他的故事,翻翻手機內的聯絡人清單,某某某已經是該區域的 Top 50 富豪,當年跟著他的也 Top 100,但人生再來一次,是不是會放棄那個選擇?也著實是個有趣的議題。

回到自己的人生,越來越更重要的事需要珍惜,如何讓自己保有餘力去實踐,真的又是個 about time 的議題。每個人打從出生至今,每個階段拿到的手牌資源都不同,唯一不變的,仍是該好好的出牌。

2024年5月24日 星期五

Docker 開發筆記 - 使用 Dockerfile 透過 Ubuntu 18.04 建置 PHP7 + MySQL 5.7 + PHPMyAdmin 開發環境 @ macOS arm64

Ubuntu:18.04 + php7.2 + mysql5.7 + phpinfo();

此需求源自於舊網站的開發環境的建置,一般人不會碰到。

同事回報之前弄的 Docker 環境,因為從 Macbook Intel x86 架構,換到 Mac M2 ARM 環境後,出現問題。主因是網站使用很舊的 MySQL 5.6 環境,在 macos arm64 的 Docker 環境中,無法很便利的只用官方環境建置出來,預設只能直接升級到 MySQL 8 跑,接著還要面對 MySQL 5.6 跟 MySQL 5.7 以上的 SQL 語法變化。

在此的解法是放棄 MySQL 5.6 ,頂多弄出 MySQL 5.7 環境,並追蹤有哪些 SQL 語法需要修改,SQL語法方面,主要是面對 ONLY_FULL_GROUP_BY 模式的處理。此紀錄透過 Docker 弄個 MySQL 5.7 環境出來過度,且最終只需用 ubuntu:18.04 即可搞定大部分,以下是流水帳:

原先要用 Docker - Ubuntu 24.04 arm64 環境來使用,但追蹤後發現 Ubuntu 20.04, Ubuntu 22.04, Ubuntu 24.04 預設都只提供 MySQL 8 的環境,並且依照 MySQL APT Repository 的方式安裝 mysql-apt-config_0.8.30-1_all.deb 後,確認都沒 mysql-5.7 可選,而坊間其他教學文,主要是透過舊版 mysql-apt-config_0.8.12-1_all.deb 套件,在 Ubuntu 20.04 安裝了 mysql-5.7 ,但其過程是 mysql-apt-config 套件無法偵測出 Ubuntu 環境,透過人工選擇 Ubuntu 18.04 的環境裝到 mysql-5.7 而已,以下是個例子:

% docker run -it ubuntu:24.04 /bin/bash
root@2560222cf1ee:/# apt update && apt install -y wget lsb-release gnupg dialog
root@2560222cf1ee:/# wget https://dev.mysql.com/get/mysql-apt-config_0.8.30-1_all.deb
root@2560222cf1ee:/# dpkg -i ./mysql-apt-config_0.8.30-1_all.deb

MySQL APT Repo features MySQL Server along with a variety of MySQL components. You may select the appropriate product to choose the version that you
wish to receive.

Once you are satisfied with the configuration then select last option 'Ok' to save the configuration, then run 'apt-get update' to load package list.
Advanced users can always change the configurations later, depending on their own needs.

  1. MySQL Server & Cluster (Currently selected: mysql-8.4-lts)  3. MySQL Preview Packages (Currently selected: Disabled)
  2. MySQL Tools & Connectors (Currently selected: Enabled)      4. Ok

Which MySQL product do you wish to configure?
Which server version do you wish to receive?

來試試看裝舊版 mysql-apt-config_0.8.12-1_all.deb:

root@2560222cf1ee:/# dpkg -i ./mysql-apt-config_0.8.12-1_all.deb
root@bc33821f13de:/# apt update
W: GPG error: http://repo.mysql.com/apt/ubuntu cosmic InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY B7B3B788A8D3785C
E: The repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
root@bc33821f13de:/# apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).
Executing: /tmp/apt-key-gpghome.eheHF2erjp/gpg.1.sh --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
gpg: key B7B3B788A8D3785C: public key "MySQL Release Engineering <mysql-build@oss.oracle.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1
root@bc33821f13de:/# apt update
W: http://repo.mysql.com/apt/ubuntu/dists/cosmic/InRelease: Key is stored in legacy trusted.gpg keyring (/etc/apt/trusted.gpg), see the DEPRECATION section in apt-key(8) for details.
N: Skipping acquire of configured file 'mysql-tools/binary-arm64/Packages' as repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' doesn't support architecture 'arm64'
N: Skipping acquire of configured file 'mysql-5.7/binary-arm64/Packages' as repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' doesn't support architecture 'arm64'
N: Skipping acquire of configured file 'mysql-apt-config/binary-arm64/Packages' as repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' doesn't support architecture 'arm64'

最後就輕鬆改用 ubuntu:18.04 ,再把 phpmyadmin 擺好即可,收工。

連續動作:

# Use the official Ubuntu 18.04 as the base image
FROM ubuntu:18.04

# Set non-interactive mode for APT
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Taipei
ENV PHP_VERSION=7.2

# Update the package list and install necessary packages
RUN apt-get update && \
    apt-get install -y tzdata && \
    ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && dpkg-reconfigure --frontend noninteractive tzdata && \
    apt-get install -y \
    htop \
    jq \
    vim \
    curl \
    wget \
    telnet \
    php7.2-cli \
    php7.2-curl \
    php7.2-fpm \
    php7.2-ldap \
    php7.2-mysql \
    php7.2-zip \
    phpunit \
    mysql-server-5.7 \
    ssl-cert \
    nginx \
    openssh-server && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

EXPOSE 22 80 443 8080 8443 3306

# Set up MySQL
RUN service mysql start && \
    mysql -e "CREATE USER 'developer'@'%' IDENTIFIED BY 'developer'; GRANT ALL PRIVILEGES ON *.* TO 'developer'@'%'; CREATE DATABASE developer; FLUSH PRIVILEGES;"

# Set up PHP-FPM Short Open Tag
RUN test -f /etc/php/$PHP_VERSION/fpm/php.ini && \ 
    sed -i 's/short_open_tag = Off/short_open_tag = On/' /etc/php/$PHP_VERSION/fpm/php.ini && \
    sed -i 's/max_execution_time = 30/max_execution_time = 300/' /etc/php/$PHP_VERSION/fpm/php.ini && \
    sed -i 's/upload_max_filesize = 2M/upload_max_filesize = 100M/' /etc/php/$PHP_VERSION/fpm/php.ini && \
    sed -i 's/post_max_size = 8M/post_max_size = 100M/' /etc/php/$PHP_VERSION/fpm/php.ini 

# Set up phpmyadmin
RUN cd /tmp && \
    wget https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.tar.gz && \
    tar xvzf phpMyAdmin-latest-all-languages.tar.gz && \
    mv phpMyAdmin-*-all-languages /usr/share/phpmyadmin && \
    mkdir -p /usr/share/phpmyadmin/tmp && \
    chown -R www-data:www-data /usr/share/phpmyadmin && \
    chmod 777 /usr/share/phpmyadmin/tmp

# Copy the entrypoint script
COPY nginx-php7.2-fpm-phpmyadmin.conf /etc/nginx/sites-enabled/phpmyadmin
COPY nginx-php7.2-fpm-website.conf /etc/nginx/sites-enabled/default 
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh

# Set the entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

# Start services in the foreground
CMD ["bash"]

# Define a volume for mounting the local directory
VOLUME ["/var/www/my-website"]

運行:

% ls
Dockerfile nginx-php7.2-fpm-phpmyadmin.conf
README.md nginx-php7.2-fpm-website.conf
entrypoint.sh
% docker build -t test .
% mkdir /tmp/php
% echo "<?php phpinfo();" > /tmp/php/index.php
% cat /tmp/php/index.php 
<?php phpinfo();
% docker run -p 80:80 -p 443:443 -p 8080:8080 -p 8443:8443 -v /tmp/php:/var/www/my-website -t test
 * Starting MySQL database server mysqld                                                           No directory, logging in with HOME=/
                                                                                            [ OK ]
 * Starting OpenBSD Secure Shell server sshd                                                [ OK ] 
 * Starting nginx nginx                                                                     [ OK ] 
Fri May 24 21:42:43 CST 2024

如此,就可以用 http://localhost, https://localhost/ 看到 phpinfo() 的資料,而用 http://localhost:8080, https://localhost:8443/ 則會看到 phpmyadmin 網頁服務

要關閉時,記得找一下 container id:

% docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED              STATUS              PORTS                                                                                                        NAMES
a1631b90d924   test      "/usr/local/bin/entr…"   About a minute ago   Up About a minute   0.0.0.0:80->80/tcp, 22/tcp, 0.0.0.0:443->443/tcp, 0.0.0.0:8080->8080/tcp, 3306/tcp, 0.0.0.0:8443->8443/tcp   thirsty_booth
% docker stop a1631b90d924
a1631b90d924

 其他資訊:changyy/docker-study/ubuntu18.04-php7.2-mysql5.7-phpmyadmin

2024年4月11日 星期四

PHP 開發筆記 - 修正 osTicket v1.18.1 信件處理流程 (Mail Parser / Mail Fetch / MIMEDecode)


經歷一陣子的追蹤,發現 osTicket v1.18.1 的信件處理流程更新的不錯,但在一個關鍵的格式拼裝上出了錯誤,目前就發個 PR 讓團隊評估是否能修正了

這陣子研究 osTicket v1.18.1 時,信件處理分成兩個部分,一個是信件下載,另一個是信件解析。之前設法先把他信件架在過程中,埋了一個機制把信件存起來,接著去研究它的 MIMEDecode 流程,並猜測這邊要補強,實際上是他下載信件後,要組成 MIME 格式有問題,才使得信件解析有問題


        public function getRawEmail(int $i) {
-           return $this->getRawHeader($i) . $this->getRawContent($i);
+           return trim($this->getRawHeader($i)) . "\r\n\r\n" . $this->getRawContent($i);
        }

透過這次也研究了 osTicket v1.18 採用了 laminas-mail 來處理信件下載流程,整體上跑 api/cron.php 可以看到:

# php8.1 cron.php
@class.mail.php - Imap class: __construct
@Storage/Imap.php: __construct 
@Storage/Imap.php: selectFolder(INBOX) 
@Storage/Imap.php: countMessages()
@Storage/Imap.php: getRawHeader(1, , 0)
@Storage/Imap.php: getRawContent(1, )
@class.mail.php, Imap class: markAsSeen(1)
@Storage/Imap.php: setFlags(1, Array)
@class.mail.php, Imap class: expunge()
@Storage/Imap.php:close()

而在 class.mail.php 在 v1.16 是看不到的,可在 osTicket/osTicket/blob/1.16.x/include/class.mailfetch.php 觀看直接用 PHP imap_* 系列的函式做事,而在 v1.17 起多了 class.mail.php 檔案,在其檔案內可以看到 getRawEmail() 的實作分成 getRawHeader() + getRawContent() 

再往下追就會發現架構也變得漂亮。趁這次多了解了 osTicket ,而為了這次任務,解題思維:
  1. 設法用 Docker 建立 osTicket 測試環境
  2. 設法在 osTicket 處理信件流程中,取出一個 eml 檔案實驗
  3. 設法在 osTicket 架構下,建立匯入 eml 檔案作為重現 MIMEDecode 的測試
  4. 設法直接用 MIMEDecode 測試流程
  5. 追蹤 osTicket IMAP 信件下載流程
沒想到最後一步才發現問題的根源,若一開始先懷疑信件下載流程,應當可以秒解任務。後續再看送出的 PR 有沒緣被收了

2024年3月27日 星期三

PHP 開發筆記 - 使用 Docker 在 Ubuntu 22.04 和 PHP 8.1 架設 osTicket v1.18.1 環境,研究 Mail Parser 流程

接續上篇追蹤 osTicket 信件處理流程的筆記,這次用 Docker 包裝可運行和測試的獨立環境,主要採用 Ubuntu 22.04 與 PHP 8.1,並且規劃方式,建構出備份原始信件的架構,以及可反覆解析信件的流程。

首先是 Dockerfile ,主要設計從 GitHub.com 取出 v1.18.1.zip 並解壓縮 /var/www/osticket-v1.18.1,其餘是安裝相關套件和資料庫的帳號建立等等,最後再把 MySQL, PHP-FPM 和 nginx 運行起來,並預設把 nginx logs 寫到 stdout 觀看:

RUN wget https://github.com/osTicket/osTicket/releases/download/v1.18.1/osTicket-v1.18.1.zip -O /tmp/osticket.zip \
    && unzip /tmp/osticket.zip -d /var/www/ \
    && mv /var/www/upload /var/www/osticket-v1.18.1 \
    && cp /var/www/osticket-v1.18.1/include/ost-sampleconfig.php /var/www/osticket-v1.18.1/include/ost-config.php \
    && chown -R www-data:www-data /var/www/osticket-v1.18.1 \
    && rm /tmp/osticket.zip

...

RUN service mysql start && \
    mysql -e "CREATE USER 'developer'@'%' IDENTIFIED BY '12345678';" && \
    mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'developer'@'%' WITH GRANT OPTION;" && \
    mysql -e "CREATE DATABASE osticket_dev CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" && \
    mysql -e "GRANT ALL PRIVILEGES ON osticket_dev.* TO 'developer'@'%';" && \
    mysql -e "FLUSH PRIVILEGES;" 

...

CMD service mysql start && service php8.1-fpm start && service nginx start && tail -f /var/log/nginx/access.log /var/log/nginx/error.log;

接下來更動 include/class.mailfetch.php 檔案,讓他下載信件時,可以存一份在 /tmp 方便後續使用:

@file_put_contents("/tmp/debug-mail.$i", $this->mbox->getRawEmail($i));

最後,弄一隻 api/cron-dev.php 檔案,可以指定 RawMail 的格式路徑,從指定位置讀進來解析信件,如此靠 api/cron-dev.php 就可以輕鬆不斷實驗 MIMEDecode 流程:

# php api/cron-dev.php 
[INFO] Input: /tmp/debug-mail
[INFO] Input File not found

# php api/cron-dev.php /tmp/debug-mail.1
[INFO] Input: /tmp/debug-mail.1
Ticket Object
(
    [ht] => Array
        (
            [ticket_id] => 2
            [ticket_pid] => 
...
            [lastupdate] => 2024-03-27 21:35:52
            [created] => 2024-03-27 21:35:52
            [updated] => 2024-03-27 21:35:52
            [topic] => 
            [staff] => 
            [user] => User Object
                (
                    [ht] => Array
                        (
                            [id] => 2
                            [org_id] => 0
                            [default_email_id] => 2
                            [status] => 0
                            [name] => UserName
                            [created] => 2024-03-27 21:35:52
                            [updated] => 2024-03-27 21:35:52
                            [default_email] => UserEmailModel Object
                                (
                                    [ht] => Array
                                        (
                                            [id] => 2
                                            [user_id] => 2
                                            [flags] => 0
                                            [address] => user@example.com
...

PHP 開發筆記 - 追蹤 osTicket v1.18.1 在 PHP8 環境處理信件的流程

公司用 osTicket 系統幾年了,最近把環境升級到 PHP8 和最新版 osTicket v1.18.1 後,同事回報踩到信件內容的 cid 圖片沒正常處理,就先追蹤一下信件處理流程,主要先追到 MIMEDecode 即可。

從官網文件 POP3/IMAP Settings Guide 得知,信件處理的觸發有從 crontab 和 api 的管道,目前就從 crontab 來追:

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.6 LTS
Release:        20.04
Codename:       focal

$ php -v
PHP 8.2.17 (cli) (built: Mar 16 2024 08:41:44) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.17, Copyright (c) Zend Technologies
    with Zend OPcache v8.2.17, Copyright (c), by Zend Technologies

$ php api/cron.php

接著開始看 api/cron.php 程式碼,大概追到 include/class.cron.php 時,就可以看到 osTicket\Mail\Fetcher::run(); 和 include/class.email.php 檔案了,從 include/class.mailfetch.php 可以看到:

```php
 78     function processMessage(int $i, array $defaults = []) {
 79         try {
 80             // Please note that the returned object could be anything from
 81             // ticket, task to thread entry or a boolean.
 82             // Don't let TicketApi call fool you!
 83             return $this->getTicketsApi()->processEmail(
 84                     $this->mbox->getRawEmail($i), $defaults);
 85         } catch (\TicketDenied $ex) {
 86             // If a ticket is denied we're going to report it as processed
 87             // so it can be moved out of the Fetch Folder or Deleted based
 88             // on the MailBox settings.
 89             return true;
 90         } catch (\EmailParseError $ex) {
 91             // Upstream we try to create a ticket on email parse error - if
 92             // it fails then that means we have invalid headers.
 93             // For Debug purposes log the parse error + headers as a warning
 94             $this->logWarning(sprintf("%s\n\n%s",
 95                         $ex->getMessage(),
 96                         $this->mbox->getRawHeader($i)));
 97         }
 98         return false;
 99     }
```

接著在 processEmail 前埋一個 @file_put_contents('/tmp/debug-mail', print_r($this->mbox->getRawEmail($i), true)); ,如此處理信件時,原始格式就會被記錄在 /tmp/debug-mail 裡,下一刻就能再重現信件格式的處理流程

小改後面:

$ sudo cp api/cron.php  api/cron-debug.php
$ tail -n 5 api/cron-debug.php 
//LocalCronApiController::call();
//
$obj = new \TicketApiController('cli');
print_r($obj->processEmail(file_get_contents('/tmp/debug-mail'), []));
?>

如此運行時就可以看資訊:

$ php api/cron-debug.php 
Ticket Object
(
    [ht] => Array
        (
            [ticket_id] => #
            [ticket_pid] => 
            [number] => #####
...

接著要來研究信件處理流程,就來到了 include/api.tickets.php 檔案:

```php
    function processEmail($data=false, array $defaults = []) {

        try {
            if (!$data)
                $data = $this->getEmailRequest();
            elseif (!is_array($data))
                $data = $this->parseEmail($data);
            print_r($data);
        } catch (Exception $ex)  {
            throw new EmailParseError($ex->getMessage());
        }
...
```

繼續追 include/class.api.php 檔案:

```php
238     function parseRequest($stream, $format, $validate=true) {
239         $parser = null;
240         switch(strtolower($format)) {
241             case 'xml':
242                 if (!function_exists('xml_parser_create'))
243                     return $this->exerr(501, __('XML extension not supported'));
244                 $parser = new ApiXmlDataParser();
245                 break;
246             case 'json':
247                 $parser = new ApiJsonDataParser();
248                 break;
249             case 'email': 
250                 $parser = new ApiEmailDataParser();
251                 break;
252             default:
253                 return $this->exerr(415, __('Unsupported data format'));
254         }
255         
256         if (!($data = $parser->parse($stream)) || !is_array($data)) {
257             $this->exerr(400, $parser->lastError());
258         }
259         
260         //Validate structure of the request.
261         if ($validate && $data)
262             $this->validate($data, $format, false);
263         
264         return $data;
265     }
266     
267     function parseEmail($content) {
268         return $this->parseRequest($content, 'email', false);
269     }
```

繼續追 include/class.mailparse.php 檔案,主要是追蹤 EmailDataParser -> Mail_Parse -> Mail_mimeDecode ,最後就是 include/pear/Mail/mimeDecode.php 的處理 MIMEDecode 的實作了。

剩下的工作就是研究它解析原始信件的流程,看看是 MIMEDecode 是否少了遞迴解,還是有什麼限制了: