tohokuaikiのチラシの裏

技術的ネタとか。

WordPressのXMLRPCで自動投稿をPHPスクリプトから行う

なんか、昔に死ぬほどやったけど忘れてしまった…自分でAPIのメソッドを作ったりしたんだけどな。

そもそも時代も変わったので生のPHPのXMLRPCとか触りたくないのでライブラリを使う。よさげなのがあった。

とりあえずパッケージインストール

composer.pharは使えるとして…

./composer.phar init
./composer.phar require hieu-le/wordpress-xmlrpc-client
./composer.phar require monolog/monolog

使い方

APIドキュメントがあったんだけど、これがもう全然正しくなくて…ってかこれ、ソースのコメントから作ってるんだろうけど引数が全然違うしね。
http://letrunghieu.github.io/wordpress-xmlrpc-client/api/index.html

ということで、ソースコードWordPressAPIレファレンス見て頑張った。
https://codex.wordpress.org/XML-RPC_WordPress_API

で、import.phpとかで

<?php
require_once 'vendor/autoload.php';
$endpoint = 'http://your-wordpress-site/xmlrpc.php';
$username = 'your_name';
$password = 'your_password'; 
$client = new \HieuLe\WordpressXmlrpcClient\WordpressClient();
$client->setCredentials($endpoint, $username, $password);
$wpLog = new Monolog\Logger('wp-xmlrpc');
$client->onError(function($error, $event) use ($wpLog){
    $wpLog->error($error, $event);
});

ここまでは共通

新しい投稿

<?php
$client->newPost('新しいタイトル ',  "内容をここに書く",
                 array(
                     'post_type' => 'post',
                     'post_status' => 'publish',
                     'terms_names' => array(
                         'category' => array('お知らせ','カテゴリー2'),
                         ),
                     'post_status' => 'private',
                     'post_author' => 1,
                     'post_date' => date('c', time() - 86400 * 8), //
                     'comment_status' => 'closed',
                     'ping_status' => 'closed',
                     ));

こんな感じ。

カテゴリーの設定が難しかったけど、WordPressのclass-wp-xmlrpc-server.phpを覗いて理解した。

                     'terms_names' => array(
                         'category' => array('お知らせ','カテゴリ―2'),
                         'post_tag' => array('タグ1', 'タグ2'),
                         'post_format' => // WordPressのpost_format使ったことないのでよくわからない。
                         ),

という感じ。IDを使いたかったら、terms_namesの代わりにtermsで。

うーん、これは楽だ。

画像のアップロード

よーし、画像のアップロードも…

<?php
$upfile = '/home/itoh/photos/1270.jpg';
$client->uploadFile('写真.jpg',
                    mime_content_type($upfile),
                    file_get_contents($upfile),
                    true,
                    80
                    );

最初、ファイル名に拡張子の.jpgを付けずに「写真」とだけしてたらアップロードできずになんでだーとなってた。

と、なんか、上書きできないなーって思ったら…
https://codex.wordpress.org/XML-RPC_WordPress_API/Media#wp.uploadFile

bool overwrite: Optional. Has no effect (see 17604).

ん~~と、リンク先チケットを見ると…私の4年前の書き込みが…直ってないのか…使われないんだろうな…

しかし、これキャプションを付けられないものかな…
…とそこで気付いた。WordPressは写真も1つのPOSTとして保存しているのである。

post_content => 説明の欄  
post_excerpt => キャプションの欄  
_wp_attachment_image_alt => ALTテキスト  

となっている。

ちなみにパーマリンクになる日付は、所属している投稿の時刻である。意味付けとしてはこれを写真の時刻とすればいい。

Apacheのmod_cacheが効かなかったので色々と調べた件

WordPressの負荷軽減にmod_cacheを使おうと思った。

で、こんな感じ

# a2enmod cache_disk
# systemctl start apache-htcacheclean
# systemctl restart apache2

で、/etc/apache2/site-availables/vhosts.confに

CacheRoot /tmp/apache/cache
CacheIgnoreCacheControl On
CacheIgnoreHeaders Set-Cookie
CacheEnable disk /
CacheDefaultExpire 300

を加える。/tmp/apache/cacheは、Apacheユーザーにするか777に。

ところが、効かない…

キャッシュされる感じが全くない。/tmp/apache/cacheにもファイルができない。

ん~~~~~~と、ログ見たりapacheの mod_cache の設定を復習した - うまいぼうぶろぐ見たりして気付いた。

Cacheが有効になるのは以下の条件がある。 http://httpd.apache.org/docs/2.4/caching.html

What Can be Cached?
The full definition of which responses can be cached by an HTTP cache is defined in RFC2616 Section 13.4 Response Cacheability, and can be summed up as follows:

  • Caching must be enabled for this URL. See the CacheEnable and CacheDisable directives.
  • If the response has an HTTP status code other than 200, 203, 300, 301 or 410 it must also specify an "Expires" or "Cache-Control" header.
  • The request must be a HTTP GET request.
  • If the response contains an "Authorization:" header, it must also contain an "s-maxage", "must-revalidate" or "public" option in the "Cache-Control:" header, or it won't be cached.
  • If the URL included a query string (e.g. from a HTML form GET method) it will not be cached unless the response specifies an explicit expiration by including an "Expires:" header or the max-age or s-maxage directive of the "Cache-Control:" header, as per RFC2616 sections 13.9 and 13.2.1.
  • If the response has a status of 200 (OK), the response must also include at least one of the "Etag", "Last-Modified" or the "Expires" headers, or the max-age or s-maxage directive of the "Cache-Control:" header, unless the CacheIgnoreNoLastMod directive has been used to require otherwise.
  • If the response includes the "private" option in a "Cache-Control:" header, it will not be stored unless the CacheStorePrivate has been used to require otherwise.
  • Likewise, if the response includes the "no-store" option in a "Cache-Control:" header, it will not be stored unless the CacheStoreNoStore has been used.
  • A response will not be stored if it includes a "Vary:" header containing the match-all "*".
  • CacheEnableとCacheDisable の指定
  • HTTPステータスが200/203/300/410以外なら"Expires" or "Cache-Control"で指定する
  • GETメソッドであること
  • Authorizationヘッダーが含まれる場合(Basic認証など)、Cache-Controlヘッダーにs-maxage/must-revalidate/publicが入っていること
  • Queryパラメータがある場合は、Expiresヘッダがで指定するかCache-Controlヘッダでs-maxageかmax-ageが入っていること
  • "Etag"か"Last-Modified"か"Expires"が入っていること。または、 max-age or s-maxageが入っているCache-Controlヘッダがあること。ただし、CacheIgnoreNoLastMod を使っている場合は除く
  • Cache-Controlでprivateを指定している場合、CacheStorePrivate オプションが有効になっていること。
  • Cache-Controlでno-storeを指定している場合、CacheStoreNoStore オプションが有効になっていること。
  • Vary: *があるとキャッシュされない

キャッシュされなかった理由は

Basic認証のページを見ていたから。

正確には、

  1. Basic認証のページを開く
  2. httpd.confでBasic認証を解除
  3. Apache再起動
  4. ブラウザでアクセス

としてたんだけど、ブラウザはBasic認証の情報を自動送信するのでブラウザを再起動させなければRequestHeaderにAuthorizationがそのまま記憶されて送られてしまうんだな。ハマった。

上記には、「 If the response contains an "Authorization:" header,」ってあるけど、これ「If the request」の間違いじゃないかな?文中には

Requests with an "Authorization" header (for example, HTTP Basic Authentication) are neither cacheable nor served from the cache when mod_cache is running in this phase.

ともあって、Basic認証を掛けている場合はキャッシュされない模様。

なんか、全部同じページになるんだけど…

なんだこれは…違うURLにアクセスしても全部同じコンテンツになっとる…

これか。

RewriteRule . /index.php [L]

RewriteRule . /index.php?cache_key=%{REQUEST_URI} [QSA,L]

にする。

しかし、なんかまだchromeでアクセスしたときにDeveloper Toolsを開けてるとキャッシュを取りに行き、そうでないとキャッシュ使わないとか挙動が不穏…

ということで、とりあえず現状で

.htaccess

RewriteRule . /index.php?cache_key=%{REQUEST_URI} [QSA,L]
#RewriteRule . /index.php [L]

テーマのfunctions.php

<?php
/* mod_cacheのための */
unset($_GET['cache_key']);
unset($_REQUEST['cache_key']);

// キャッシュ制御のためのLast-Modifiedヘッダ
add_action("template_redirect", function (){
    if( (is_home() || is_singular()) && !$_GET) {
        $span = 300;
        header('Expires: '. date('r', time() + $span));
        header('Cache-Control: public, max-age='.$span.', s-maxage='.$span);
        header(sprintf("Last-Modified: %s", get_the_modified_time("r")) );
    }
});

httpd.conf

    <IfModule mod_cache.c>
    <IfModule mod_cache_disk.c>
          CacheEnable disk /
          CacheRoot /tmp/var/apache2/cache
          CacheIgnoreHeaders Set-Cookie
          CacheIgnoreCacheControl On
          CacheIgnoreNoLastMod On
          CacheMaxExpire 600
          CacheDetailHeader On
    </IfModule>
    </IfModule>

うーん、CacheIgnoreCacheControl とかCacheIgnoreNoLastMod あたりはOffにしたいなぁ…とは思うけど、ブラウザ側の挙動に依存させたくないのでこれでいいか。

あと、CacheDetailHeader On これは良い。HTTPヘッダにキャッシュを使ったかどうかが分かる。そもそもサーバー側のキャッシュポリシーはCache-Controlヘッダで相手にはわかっているのでこれがユーザーに漏れたところで実害は無いと思う。

Debian10でApache2.4+PHP-fpmを動かす

PHP5.6を使いたい。参考にしたサイト

aptでパッケージ先を追加

普通にapt installできないので、https://packages.sury.org/php/ をパッケージ先に追加する。

GPG認証するパッケージを入れて、キーを追加して、設定ファイルを作る。

# apt install ca-certificates apt-transport-https 
# wget -q https://packages.sury.org/php/apt.gpg -O- | apt-key add -
# echo "deb https://packages.sury.org/php/ stretch main" | tee /etc/apt/sources.list.d/php.list

で、パッケージ情報を追加

# apt update
# apt upgrade

適当にPHP5.6周りをインストール

dpkg -l |grep php5.6 で出てきたのから適当に必要そうなのをインストール

#  apt install -y php5.6-mysql php5.6-mbstring php5.6-xml php5.6-zip php5.6-cli

PHP5.6がApacheからモジュールとして実行されないようにphp5.6はインストールしない。

次にphp5.6-fpmをインストール。さっきのと同時でもいいのだけど。

# apt install -y php5.6-fpm

次にPHPモジュールも入れる。PHPモジュールはFastCGIApache SAPI も同じである。当たり前か。

# apt install -y php5.6 php5.6-mysql php5.6-mbstring php5.6-xml php5.6-zip php5.6-cli

VirtualHostごとに実行するPHPを変更する。

php-fpm5.6がserviceとして起動しているのを確認。

# systemctl status php5.6-fpm.service

/etc/php/5.6/fpm/pool.d/www.conf を見ると、

listen = /run/php/php5.6-fpm.sock

Unixソケットで接続するようになっているので、sites-available/hogehoge.conf で

<VirtualHost *:80>
        <Directory /path/to/document/root>
            require all granted
        </Directory>
        <FilesMatch \.(html|php)$>
            SetHandler "proxy:unix:/run/php/php5.6-fpm.sock|fcgi://localhost"

としておく。HTMLとPHP拡張子で動く。

require all granted は、いつものように「あー、AH01797: client denied by server configuration」で403を何度も繰り返すため…何度設定しても1度はミスる。

Apacheのモジュールを適宜IN/OUTしておく

これやり忘れてて「SetHandlerのproxyが効いてくれない・・・」とハマりました。

 a2dismod mpm_prefork
 a2enmod mpm_event
 a2enmod proxy_fcgi

.htaccessphp_value, php_flagの書き方を変える。

php_value auto_prepend_file /path/to/document/root/auto_prepend.php
php_flag error_log Off

とあるのを

SetEnv PHP_VALUE "auto_prepend_file=/path/to/document/root/auto_prepend.php"
SetEnv PHP_FLAG "error_log=Off"

とかする。

またAccess Denied

ってエラーがindex.htmlにアクセスすると出る。

 AH01071: Got error 'Access to the script '/var/www/htdocs/index.html' has been denied (see security.limit_extensions)\n'

/etc/php/5.6/fpm/pool.d/www.conf で

security.limit_extensions = .php .html

として、/etc/init.d/php5.6-fpm restart で動いた。

で、またハマる

VirtualHostにして、他のサイトでinclude_pathやauto_prepend_fileを設定すると、他のサイトでもこの指定が残る。

んー、なんやねんこれは。

結局、

<VirtualHost>
SetEnv PHP_VALUE "auto_prepend_file=/path/to/document/root/auto_prepend.php"
</VirtualHost>

<VirtualHost>
SetEnv PHP_VALUE "auto_prepend_file="
</VirtualHost>

みたいにして逐一書き直す羽目になった…なんやねんこれは…

ミライコンパスを使ってる学校のイベント情報をRSSで取得する

全国の私立中高校で説明会などの予約に使われているミライコンパスというのがあるんだけど、これを学校をまとめてRSS化するようにした。

Nodejsで作ってる。

$ node -v
v10.16.0

コード

これを

$ node mirai-compass-rss-index.js

として、見たいのが

学校 イベントURL
女子美術大学付属中学校(東京都) https://mirai-compass.net/usr/joshibij/event/evtIndex.jsf
成立学園中学校(東京都) https://mirai-compass.net/usr/seiritzj/event/evtIndex.jsf
かえつ有明中学校(東京都) https://mirai-compass.net/usr/kariakej/event/evtIndex.jsf
国士舘高等学校(東京都) https://mirai-compass.net/usr/kokushih/event/evtIndex.jsf
常総学院中学校(茨城県 https://mirai-compass.net/usr/josogj/event/evtIndex.jsf
淑徳与野中学校(埼玉県) https://mirai-compass.net/usr/shukuynj/event/evtIndex.jsf

だったら、
http://localhost:3000/?school=seiritzj&school=kariakej&school=kokushih&school=josogj&school=shukuynj
というURLにアクセスする。

最初、どっかのサーバーにおいてサービス提供するかなと思ったけど、「サービス落ちてて説明会予約できなかった!(怒)」とか言われるとヤダなーとか思いつつ作った。

Laravel-AdminLTEを使ってみたメモ

AdminLTEを使いたかったのでこちらを使った。以前は違うの使ってた

インストールとかは簡単なので略

@extends('adminlte::master')

adminlte::は、ServiceProviderで付け加えてるっぽい。

vendor/jeroennoten/laravel-adminlte/src/ServiceProvider.php
<?php
    private function loadViews()
    {
        $viewsPath = $this->packagePath('resources/views');

        $this->loadViewsFrom($viewsPath, 'adminlte');

のadminlteがこのパッケージ(jeroennoten/Laravel-AdminLTE)のresources/viewsを使うPrefixになるみたい。

リンク先とかFormのAction先の変更

あとは、URLとかrouteを変えたら

{{ url(config('adminlte.dashboard_url', 'home')) }}

とかあるんだけど、これは固定なのでポチポチと変更をしていく。んだけど、これって

{{ route('user.login') }}

とかのRouteのname使った方が良いんじゃないかな?