WordPressの設置を楽にする(インストールではない)

# きっかけ
サーバーのフルマネージドをやっていると、新規でWordPressの設置依頼がよくあります。
ちょっと特殊かもしれないですが、WordPressのインストール画面を出すまでの状態にするためのスクリプトを作成してみました。
基本的に特にサーバーに設定を加えなくても、(うちはCentOSがメインなので)どのサーバーでも動かせるようにPython2系で作成してみました。

# 懸念点と対策
これまでWordPressを新規で設置してお客様に提供するまでにいくつかトラブルがあったのでまとめました。

## パーミッション
Apache環境で一般ユーザーのFTPアカウントの場合、Webサーバーの実行ユーザー(Apache)とファイルのオーナー(FTPアカウント)が異なるため、ただWordPressファイルをアップロードしただけでは管理画面上のアップロードや更新についてパーミッションエラーになってしまいます。
通常その場合は、WordPressのアップロード後に `wp-content` 配下の `themes` `uploads` `plugins` の3ディレクトリのパーミッションを777にすることがあります。
=> スクリプトで自動設定することにしました。

## Salt忘れ
WordPress設置後にwp-config.php内にあるSaltの設定を書き換え忘れてしまうことがあります。
(無くても動くけどあったほうが良い。それとめんどくさい。)
=> スクリプトで自動設定することにしました。

## 引き渡しまでの間の乗っ取り
WordPress設置完了後お客様に提供するまでの間、インストール画面が出ている状態なのですが、この僅かの間に攻撃車によって勝手にインストールが進められて乗っ取られてしまうことがありました。
=> インストール画面は自動的にBasic認証をかけることにしました。
このBasic認証は必要のない(いらない)時のためにon/off出来るようにします。

## xmlrpc.phpへのアタック
外部投稿の仕組みを利用して、攻撃してくるパターンが多く、WordPressの乗っ取りやバックドアの配置等の攻撃を受けることが多いです。
=> xmlrpc.phpへのアクセスはデフォルト禁止にするようにしました。
お客様より利用したい旨の連絡があれば、IPアドレスによる制限解除等の措置を別途行うようにします。

## `uploads` ディレクトリへの不審なPHPファイルの設置
万が一WordPressが攻撃を受けた時に、 `wp-content/uploads` ディレクトリにPHPファイルが置かれてしまう事が多いです。(パーミッション777にするため)
=> `uploads` 以下は画像等メディアの置き場なので基本的にPHPは置きません。なので、ここ以下のPHPファイルへのアクセスは禁止にします。

# 設定
これらの懸念点を解消するようにスクリプトを作成しました。
WordPressをインストールする環境のあるサーバーに2つファイルを設置します。

```config.ini
# wp-setup config
[init]
# Wordpress setup directory.
path=/var/www/vhosts/test.com/public_html

# wordpress version
# version = latest => download wordpress latest version
# or version number
# version list => https://ja.wordpress.org/download/releases/
# default : latest
# ex) setup worespress for version 4.9.7
# version = 4.9.7
version = latest

# set owner and group for wordpress files and directories.
# unix user and unix group.
user = username
group = groupname

# Set Basic Auth
# true or false
# .htpasswd file is generated into an installation pass automatically.
basic_auth = false

[mysql]
dbname = database_name
user = user_name
password = user_password
```
configファイルです。
基本的に設定はこのファイルで行います。
`path=` WordPress設置先のディレクトリです。
`version =` 設置するWordPressのバージョンです。環境(要望)によっては最新でないものの場合もあるので、バージョンを選べるようにしました。 `latest` が最新です。
`user =` `group =` 実際のファイルのオーナーです。基本的にFTPユーザーになると思います。
`basic_auth =` true/falseで設定。Basic認証のon/offです。onの場合、インストール完了後にBasic認証のログイン情報が出力されます。また.htpasswdファイルも自動生成されます。
`[mysql]` 欄。DBへの接続情報です。

```wp-setup.py
#!/usr/bin/python
#-*- coding: utf-8 -*-

import sys
import os
import re
import pwd
import grp
import random
import string
import shutil
import urllib
import tarfile
from crypt import crypt
from ConfigParser import SafeConfigParser

configfile = 'config.ini'
tmpdir = '/tmp/'
salturl = 'https://api.wordpress.org/secret-key/1.1/salt/'
path = ''
basic_username = 'wpadmin'

def main():

    version = getParam("init", "version")
    path = getParam("init", "path")
    if getParam("init", "basic_auth") == "true":
        basic_auth = True
    else :
        basic_auth = False

    params = {
        'dbname'  : getParam("mysql", "dbname"),
        'user'    : getParam("mysql", "user"),
        'host'    : getParam("mysql", "host"),
        'password': getParam("mysql", "password")
    }

    if(not path.endswith("/")):
        path = path + "/"
    print "wordpress install to " + path

    getWP(version, path)

    salt = getSalt()
    replaceWPConfig(path, params, salt)

    pswd = setHtaccess(path, basic_auth)

    setPermission(path)

    print "A setup of wordpress has been completed."

    if basic_auth :
        print "--------"
        print "Basic auth info"
        print "username : " + basic_username
        print "password : " + pswd
        print "--------"

def getParam(section, param_name):
    parser = SafeConfigParser()
    parser.read(configfile)
    return parser.get(section, param_name)

def setHtaccess(path, basic_auth):
    pswd = ''

    if basic_auth :
        pswd = ''.join([random.choice(string.ascii_letters + string.digits) for i in range(12)])
        htpasswd = open(path + '.htpasswd', 'a')
        htpasswd.write(basic_username + ":" + crypt(pswd, 'generate_pswd'))
        htpasswd.close()

    f_output = open(path + '.htaccess', 'a')

    f_output.write('#Generate by wp-setup.py\n')
    f_output.write('\n# BEGIN WordPress\n')
    f_output.write('<Files ~ "wp-content/uploads/*\.php$">\n')
    f_output.write('   order deny,allow\n')
    f_output.write('   deny from all\n')
    f_output.write('</Files>\n')
    f_output.write('<IfModule mod_rewrite.c>\n')
    f_output.write('RewriteEngine On\n')
    f_output.write('RewriteBase /\n')
    f_output.write('RewriteRule ^xmlrpc\.php$ "http\:\/\/0\.0\.0\.0\/" [R=301,L]\n')
    f_output.write('RewriteRule ^index\.php$ - [L]\n')
    f_output.write('RewriteCond %{REQUEST_FILENAME} !-f\n')
    f_output.write('RewriteCond %{REQUEST_FILENAME} !-d\n')
    f_output.write('RewriteRule . /index.php [L]\n')
    f_output.write('</IfModule>\n')
    f_output.write('# END WordPress\n')

    if basic_auth :
        f_output.write('\n#Generate by wp-setup.py\n')
        f_output.write('AuthUserfile ' + path + '.htpasswd\n')
        f_output.write('AuthGroupfile /dev/null\n')
        f_output.write('AuthName "Please enter your ID and password"\n')
        f_output.write('AuthType Basic\n')
        f_output.write('require valid-user\n')

    f_output.close()

    return pswd

def setPermission(path):
    print "set owner and group"

    uid = pwd.getpwnam(getParam("init", "user")).pw_uid
    gid = grp.getgrnam(getParam("init", "group")).gr_gid

    for file in findAllFiles(path):
        os.chown(file, uid, gid)

    os.chmod(path + "wp-content/", 0777)
    os.chmod(path + "wp-content/plugins/", 0777)
    os.chmod(path + "wp-content/themes/", 0777)

def findAllFiles(dir):
    for root, dirs, files in os.walk(dir):
        #if root == dir:
        #    continue

        yield root
        for file in files:
            yield os.path.join(root, file)

def replaceWPConfig(path, params, salt):

    print "set wordpress config data"
    f_input = open(path + 'wp-config-sample.php')
    f_output = open(path + 'wp-config.php', 'w')

    for line in f_input:
        if re.match(r"define\('DB_NAME", line):
            line = "define('DB_NAME', '" + params["dbname"] + "');" + '\n'
        elif re.match(r"define\('DB_USER", line):
            line = "define('DB_USER', '" + params["user"] + "');" + '\n'
        elif re.match(r"define\('DB_PASSWORD", line):
            line = "define('DB_PASSWORD', '" + params["password"] + "');" + '\n'
        elif re.match(r"define\('DB_HOST", line):
            line = "define('DB_HOST', '" + params["host"] + "');" + '\n'
        elif re.match(r"define\('AUTH_KEY'", line):
            line = salt[0] + '\n'
        elif re.match(r"define\('SECURE_AUTH_KEY'", line):
            line = salt[1] + '\n'
        elif re.match(r"define\('LOGGED_IN_KEY'", line):
            line = salt[2] + '\n'
        elif re.match(r"define\('NONCE_KEY'", line):
            line = salt[3] + '\n'
        elif re.match(r"define\('AUTH_SALT'", line):
            line = salt[4] + '\n'
        elif re.match(r"define\('SECURE_AUTH_SALT'", line):
            line = salt[5] + '\n'
        elif re.match(r"define\('LOGGED_IN_SALT'", line):
            line = salt[6] + '\n'
        elif re.match(r"define\('NONCE_SALT'", line):
            line = salt[7] + '\n'

        f_output.write(line)

    f_input.close()
    f_output.close()

def getSalt():
    salt = urllib.urlopen(salturl).read().split("\n")
    return salt

def getWP(ver, path):

    if(not os.path.isdir(path)):
        print path + " is not directory or no such directory."
        sys.exit(1)

    url = "https://ja.wordpress.org/"

    regex = r'\d\.\d+.\d+'
    if re.match(regex, ver):
        filename = "wordpress-" + ver + "-ja.tar.gz"
    else :
        filename = "latest-ja.tar.gz"

    # download wordpress and untar
    print "downloading... " + url + filename
    urllib.urlretrieve(url + filename, tmpdir + filename)
    untar(tmpdir + filename)
    print "done."

    # move to setup directory
    srcdir = tmpdir + "wordpress/"
    files = os.listdir(srcdir)
    for fname in files:
        print "move from " + srcdir + fname + " to " + path
        shutil.move(srcdir + fname, path)

    print "delete tmp file " + tmpdir + filename
    os.remove(tmpdir + filename)

    print "delete tmp dir " + tmpdir + "wordpress/"
    os.removedirs(tmpdir + "wordpress/")

def untar(fname):
    if (fname.endswith("tar.gz")):
        print "untar " + fname
        tar = tarfile.open(fname)
        tar.extractall(tmpdir)
        tar.close()
    else:
        print "Not a tar.gz file: " + fname

if __name__ == '__main__':
    main()
```
スクリプト本体です。

# 実行
試しに `/var/www/html/` 以下、Basic認証ありWordPressを設置するとして、下記のようにconfig.iniを記載します。

```config.ini
# wp-setup config
[init]
# Wordpress setup directory.
path=/var/www/html

# wordpress version
# version = latest => download wordpress latest version
# or version number
# version list => https://ja.wordpress.org/download/releases/
# default : latest
# ex) setup worespress for version 4.9.7
# version = 4.9.7
version = latest

# set owner and group for wordpress files and directories.
# unix user and unix group.
user = root
group = root

# Set Basic Auth
# true or false
# .htpasswd file is generated into an installation pass automatically.
basic_auth = true

[mysql]
dbname = hoge
user = hoge
password = hogepass
host = localhost
```

実行します。

```
$ sudo ./wp-setup.py
wordpress install to /var/www/html/
downloading... https://ja.wordpress.org/latest-ja.tar.gz
untar /tmp/latest-ja.tar.gz
done.
move from /tmp/wordpress/wp-trackback.php to /var/www/html/
move from /tmp/wordpress/wp-cron.php to /var/www/html/
move from /tmp/wordpress/wp-comments-post.php to /var/www/html/
move from /tmp/wordpress/wp-blog-header.php to /var/www/html/
move from /tmp/wordpress/wp-config-sample.php to /var/www/html/
move from /tmp/wordpress/wp-includes to /var/www/html/
move from /tmp/wordpress/xmlrpc.php to /var/www/html/
move from /tmp/wordpress/index.php to /var/www/html/
move from /tmp/wordpress/wp-login.php to /var/www/html/
move from /tmp/wordpress/readme.html to /var/www/html/
move from /tmp/wordpress/wp-signup.php to /var/www/html/
move from /tmp/wordpress/wp-load.php to /var/www/html/
move from /tmp/wordpress/wp-content to /var/www/html/
move from /tmp/wordpress/wp-admin to /var/www/html/
move from /tmp/wordpress/wp-activate.php to /var/www/html/
move from /tmp/wordpress/wp-settings.php to /var/www/html/
move from /tmp/wordpress/license.txt to /var/www/html/
move from /tmp/wordpress/wp-mail.php to /var/www/html/
move from /tmp/wordpress/wp-links-opml.php to /var/www/html/
delete tmp file /tmp/latest-ja.tar.gz
delete tmp dir /tmp/wordpress/
set wordpress config data
set owner and group
A setup of wordpress has been completed.
--------
Basic auth info
username : wpadmin
password : jdpFPVotHpLv
--------
```

設置できました。Basic認証の情報も出力されているので、これでブラウザからアクセスしてみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/290607/e1ffc99b-f03f-11fb-11c4-c816ef71e5e5.png)
Basic認証の画面が出てきたので先程の情報を入力してみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/290607/a549d002-9cce-8d0d-b8c1-27b9641525c2.png)

インストール画面が出せました。

実際に設置されている `.htaccess` の中身は下記のようになります。
(実際には行中のコメントはありません)

```bash 
/var/www/html/.htaccess

# BEGIN WordPress
<Files ~ "wp-content/uploads/*\.php$">
   order deny,allow
   deny from all
</Files>
# ↑uploadsディレクトリのアクセスを禁止する。
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^xmlrpc\.php$ "http\:\/\/0\.0\.0\.0\/" [R=301,L]
# ↑xmlrpc.php へのアクセスは0.0.0.0にリダイレクトさせてアクセスさせません。
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
# ↑WordPressいつもの設定
</IfModule>
# END WordPress

#Generate by wp-setup.py
AuthUserfile /var/www/html/.htpasswd
AuthGroupfile /dev/null
AuthName "Please enter your ID and password"
AuthType Basic
require valid-user
# ↑Basic認証の設定。falseの場合はこの記載がありません。
```

1つのサーバーに複数のWordPressを置く場合は楽になりました。