Skip to content

Instantly share code, notes, and snippets.

@acusti
Last active May 3, 2022 03:39
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save acusti/99ed86f19e37c6a23546 to your computer and use it in GitHub Desktop.
Save acusti/99ed86f19e37c6a23546 to your computer and use it in GitHub Desktop.
WordPress install script using WP-CLI: supports Multisite, MAMP, local php.ini overrides, and theme and plugin scaffolding, and will generate optimized .gitignore and .htaccess files.
#!/bin/bash
# Install WordPress
# =================
# Based on many sources, but originally based on:
# @ http://premium.wpmudev.org/blog/set-up-wordpress-like-a-pro/
# See also: http://wprealm.com/blog/wordpress-wp-cli-kung-fu-made-simple/
# BEGIN Configuration
# -------------------
IS_INITIAL_INSTALL="true"
# Note: if DBNAME, DBUSER, DBPASS are environment vars, next 3 vars are ignored
WP_DBNAME="database_name"
WP_DBUSER="database_name"
WP_DBPASS="http://passwordsgenerator.net"
WP_DBPREFIX="change_me_"
URL_DEFAULT="http://foo.dev"
TITLE="Your Next Great Site"
PLUGIN_NAME="Core Functionality"
PLUGIN_SLUG="core-functionality"
THEME_NAME="Name of Theme"
THEME_SLUG="themeslug"
THEME_AUTHOR_NAME="You"
THEME_AUTHOR_URI="http://www.yoursite.com/wp"
ADMINUSER="NOT_admin"
ADMINPASS="http://passwordsgenerator.net"
ADMINEMAIL="you@yoursite.com"
PLUGINS="jetpack simple-page-ordering lightbulb-save-and-close wordpress-seo"
SITE_DIR="www"
# -----------------
# END Configuration
# Validate parameters
# -------------------
# WP_DBUSER cannot be greater than 16 characters
if [ ${#WP_DBUSER} -gt 16 ] ; then
echo -e "\033[31mDB user \033[33m$WP_DBUSER\033[31m cannot be longer than 16 characters\033[0m"
exit 1
fi
# WP_DBPREFIX should be all lowercase to avoid issues
# http://dev.mysql.com/doc/refman/5.7/en/identifier-case-sensitivity.html
if [[ $WP_DBPREFIX =~ ^(.*[A-Z].*)$ ]] ; then
echo -e "\033[31mWP_DBPREFIX \033[33m$WP_DBPREFIX\033[31m should be all lowercase\033[0m"
exit 1
fi
# Script usage
# ------------
if [[ ("$1" = "--help" || "$1" = "-h") ]] ; then
echo "usage: $0 <db-root-user> <db-root-password> [db-host]"
echo " $0 --uninstall <db-root-user> <db-root-password> [db-host]"
exit 1
fi
# Setup DB config
# ---------------
DBROOTUSER=${1-root}
DBROOTPASS=${2-false}
DBHOST=${3-localhost}
# In uninstall, parameters are off-by-one
IS_UNINSTALL=false
if [ "$1" = "--uninstall" ] ; then
IS_UNINSTALL=true
DBROOTUSER=${2-root}
DBROOTPASS=${3-false}
DBHOST=${4-localhost}
fi
DBHOSTPARTS=(${DBHOST//\:/ })
DBHOSTNAME=${DBHOSTPARTS}
IS_DB_ENV_VARS=false
# Check for DB environment variables
if [ ${#DBNAME} -gt 0 ] && [ ${#DBUSER} -gt 0 ] && [ ${#DBPASS} -gt 0 ] ; then
IS_DB_ENV_VARS=true
WP_DBNAME=$DBNAME
WP_DBUSER=$DBUSER
WP_DBPASS=$DBPASS
fi
# Prepare environment
# -------------------
# Check for MAMP
PATH_ORIGINAL=$PATH
if [ -d /Applications/MAMP/bin/php/ ] ; then
# Use MAMP version of PHP http://stackoverflow.com/a/29990624/333625
PHP_VERSION=`ls /Applications/MAMP/bin/php/ | sort -n | tail -1`
export PATH=/Applications/MAMP/bin/php/${PHP_VERSION}/bin:$PATH
# Export MAMP MySQL executables as functions (to avoid exporting MAMP's Library/bin)
mysql() {
/Applications/MAMP/Library/bin/mysql "$@"
}
mysqladmin() {
/Applications/MAMP/Library/bin/mysqladmin "$@"
}
export -f mysql
export -f mysqladmin
fi
# Set up MySQL command
# --------------------
MYSQL_PREAMBLE="Running mysql command"
MYSQL_COMMAND="mysql --user=$DBROOTUSER "
if [ "$DBROOTPASS" = false ] ; then
MYSQL_PREAMBLE+=". Enter the password for \033[36m$DBROOTUSER\033[0m at prompt."
MYSQL_COMMAND+="-p "
else
MYSQL_COMMAND+="--password=$DBROOTPASS "
fi
# Uninstall
# ---------
if [ "$IS_UNINSTALL" = true ] ; then
echo -e "\033[1;33mWARNING:\033[0m You are about to completely remove your WordPress site. This means:"
echo -e "Emptying \033[36m"`pwd`"/$SITE_DIR\033[0m"
echo -e "And dropping the DB \033[36m$WP_DBNAME\033[0m"
echo -e "And dropping the MySQL user \033[36m$WP_DBUSER\033[0m"
read -p "Are you sure you wish to proceed? (y/N) " is_uninstall_confirmed
if [[ $is_uninstall_confirmed =~ ^([Yy][Ee][Ss]|[Yy])$ ]] ; then
echo "Removing DB and user..."
echo -e $MYSQL_PREAMBLE
`$MYSQL_COMMAND --execute="DROP DATABASE IF EXISTS $WP_DBNAME; GRANT USAGE ON *.* TO $WP_DBUSER@$DBHOSTNAME; DROP USER $WP_DBUSER@$DBHOSTNAME"`
if [ -d "./$SITE_DIR" ] ; then
rm -rf $SITE_DIR/wp-*
rm $SITE_DIR/.htaccess
rm $SITE_DIR/license.txt
rm $SITE_DIR/readme.html
rm $SITE_DIR/xmlrpc.php
> $SITE_DIR/index.php
else
echo "Note: directory $SITE_DIR does not exist"
fi
echo "Done."
else
echo "OK, exiting without doing anything."
fi
exit 1
fi
# Create DB and user
# ------------------
# DB setup based on:
# http://www.bluepiccadilly.com/2011/12/creating-mysql-database-and-user-command-line-and-bash-script-automate-process
create_db() {
# Assemble query to create database, create user, and grant all priviliges
local Q1="CREATE DATABASE IF NOT EXISTS $WP_DBNAME;"
local Q2="GRANT USAGE ON *.* TO $WP_DBUSER@$DBHOSTNAME IDENTIFIED BY '$WP_DBPASS';"
local Q3="GRANT ALL PRIVILEGES ON $WP_DBNAME.* TO $WP_DBUSER@$DBHOSTNAME;"
local Q4="FLUSH PRIVILEGES;"
local SQL="${Q1}${Q2}${Q3}${Q4}"
# Execute SQL query
echo -e $MYSQL_PREAMBLE
`$MYSQL_COMMAND --execute="$SQL"`
}
create_db
# Determine site url
# ------------------
read -p "Please enter the site URL (default is $URL_DEFAULT): " URL
if [ ${#URL} = 0 ] ; then
URL=$URL_DEFAULT
fi
# Create .gitignore
# -----------------
cat > .gitignore <<EOF
# Based on https://gist.github.com/salcode/9940509
# ignore everything in the root except the "wp-content" directory.
/$SITE_DIR/*
!/$SITE_DIR/wp-content/
# ignore everything in the "wp-content" directory, except:
# "mu-plugins" directory
# "plugins" directory
# "themes" directory
$SITE_DIR/wp-content/*
!$SITE_DIR/wp-content/mu-plugins/
!$SITE_DIR/wp-content/plugins/
!$SITE_DIR/wp-content/themes/
!$SITE_DIR/wp-content/uploads/
$SITE_DIR/wp-content/uploads/wp-migrate-db/
# ignore everything in the "plugins" directory,
# except the plugins you specify
$SITE_DIR/wp-content/plugins/*
!$SITE_DIR/wp-content/plugins/$PLUGIN_SLUG/
# ignore everything in the "themes" directory,
# except the themes you specify
$SITE_DIR/wp-content/themes/*
!$SITE_DIR/wp-content/themes/$THEME_SLUG/
# ignore all files that start with ~
~*
# ignore OS generated files
ehthumbs.db
Thumbs.db
.DS_Store
# ignore Editor files
*.sublime-project
*.sublime-workspace
*.komodoproject
# ignore sass-cache
.sass-cache
# ignore log files and databases
*.log
*.sql
*.sqlite
# ignore compiled files
*.com
*.class
*.dll
*.exe
*.o
*.so
# ignore packaged files
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# ignore node/grunt and bower dependency directories
node_modules/
bower_components/
# ignore built theme css and css.map files
$SITE_DIR/wp-content/themes/$THEME_SLUG/style.css
$SITE_DIR/wp-content/themes/$THEME_SLUG/style.css.map
EOF
# Create site directory or
# Preserve existing wp-content
# ----------------------------
SITE_DIR_TEMP=""
if [ -d "./$SITE_DIR" ] ; then
if [ -d "./$SITE_DIR/wp-content" ] ; then
SITE_DIR_TEMP="__$SITE_DIR-temp_"
mkdir $SITE_DIR_TEMP
mv "./$SITE_DIR/wp-content/" $SITE_DIR_TEMP
fi
else
mkdir $SITE_DIR
fi
cd $SITE_DIR
# Regular or Multisite
# --------------------
IS_MULTISITE=false
SITE_TYPE="WordPress"
read -p "Install WordPress multisite? (y/N) " IS_MULTISITE_RESPONSE
if [[ $IS_MULTISITE_RESPONSE =~ ^([Yy][Ee][Ss]|[Yy])$ ]] ; then
IS_MULTISITE=true
SITE_TYPE+=" Multisite"
fi
# Create .htaccess
# ----------------
IS_SECURE_HTACCESS=false
read -p "Add production-level security rules to .htaccess? (y/N) " IS_SECURE_HTACCESS_RESPONSE
if [[ $IS_SECURE_HTACCESS_RESPONSE =~ ^([Yy][Ee][Ss]|[Yy])$ ]] ; then
IS_SECURE_HTACCESS=true
fi
# Parse site url for path (if none found, path should be "/")
URL_NO_PROTOCOL=${URL#*//}
URL_PATH=""
if [[ $URL_NO_PROTOCOL =~ ^(.*/.*)$ ]] ; then
URL_PATH="/${URL_NO_PROTOCOL#*/}"
fi
URL_PATH+="/"
# Block localhost only if site url hostname isn't localhost
URL_HOSTNAME=${URL_NO_PROTOCOL%%[:/]*}
BLOCK_LOCALHOST="RewriteCond %{QUERY_STRING} (localhost) [NC,OR]"
if [ "$URL_HOSTNAME" = localhost ] ; then
BLOCK_LOCALHOST=""
fi
touch .htaccess
# Use >> to append to .htaccess
if [ "$IS_SECURE_HTACCESS" = true ] ; then
cat >> .htaccess <<EOF
# 5G BLACKLIST/FIREWALL (2013) + 2014 Micro Blacklist
# @ http://perishablepress.com/5g-blacklist-2013/
# @ http://perishablepress.com/2014-micro-blacklist/
# Check for latest here: http://perishablepress.com/category/htaccess/
# 5G:[QUERY STRINGS]
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase ${URL_PATH}
RewriteCond %{QUERY_STRING} (\"|%22).*(<|>|%3) [NC,OR]
RewriteCond %{QUERY_STRING} (javascript:).*(\;) [NC,OR]
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3) [NC,OR]
RewriteCond %{QUERY_STRING} (\\\|\.\./|\`|=\'$|=%27$) [NC,OR]
RewriteCond %{QUERY_STRING} (\;|\'|\"|%22).*(union|select|insert|drop|update|md5|benchmark|or|and|if) [NC,OR]
RewriteCond %{QUERY_STRING} (base64_encode|mosconfig) [NC,OR]
${BLOCK_LOCALHOST}
RewriteCond %{QUERY_STRING} (boot\.ini|echo.*kae|etc/passwd) [NC,OR]
RewriteCond %{QUERY_STRING} (GLOBALS|REQUEST)(=|\[|%) [NC]
RewriteRule .* - [F]
</IfModule>
# 5G:[USER AGENTS]
<IfModule mod_setenvif.c>
# SetEnvIfNoCase User-Agent ^$ keep_out
SetEnvIfNoCase User-Agent (binlar|casper|cmsworldmap|comodo|diavol|dotbot|feedfinder|flicky|jakarta|kmccrew|nutch|planetwork|purebot|pycurl|skygrid|sucker|turnit|vikspider|zmeu) keep_out
<limit GET POST PUT>
Order Allow,Deny
Allow from all
Deny from env=keep_out
</limit>
</IfModule>
# 5G:[REQUEST STRINGS]
<IfModule mod_alias.c>
RedirectMatch 403 (https?|ftp|php)\://
RedirectMatch 403 /(https?|ima|ucp)/
RedirectMatch 403 /(Permanent|Better)$
RedirectMatch 403 (\=\\\\\'|\=\\\%27|/\\\\\'/?|\)\.css\()\$
RedirectMatch 403 (\,|\)\+|/\,/|\{0\}|\(/\(|\.\.\.|\+\+\+|\||\\\\\"\\\\\")
RedirectMatch 403 \.(cgi|asp|aspx|cfg|dll|exe|jsp|mdb|sql|ini|rar)\$
RedirectMatch 403 /(contac|fpw|install|pingserver|register)\.php\$
RedirectMatch 403 (base64|crossdomain|localhost|wwwroot|e107\_)
RedirectMatch 403 (eval\(|\_vti\_|\(null\)|echo.*kae|config\.xml)
RedirectMatch 403 \.well\-known/host\-meta
RedirectMatch 403 /function\.array\-rand
RedirectMatch 403 \)\;\\$\(this\)\.html\(
RedirectMatch 403 proc/self/environ
RedirectMatch 403 msnbot\.htm\)\.\_
RedirectMatch 403 /ref\.outcontrol
RedirectMatch 403 com\_cropimage
RedirectMatch 403 indonesia\.htm
RedirectMatch 403 \{\\\$itemURL\}
RedirectMatch 403 function\(\)
RedirectMatch 403 labels\.rdf
RedirectMatch 403 /playing.php
RedirectMatch 403 muieblackcat
</IfModule>
# 5G:[REQUEST METHOD]
<ifModule mod_rewrite.c>
RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
RewriteRule .* - [F]
</IfModule>
# 2014 Micro Blacklist
<IfModule mod_setenvif.c>
Order Allow,Deny
Allow from all
Deny from 123.151.39.
Deny from 77.172.210.
Deny from 174.94.131.
Deny from 89.238.137.59
Deny from 212.90.148.101
Deny from 91.207.61.129
Deny from 202.46.52.120
Deny from 128.73.60.194
Deny from 68.108.17.141
Deny from 27.54.93.178
Deny from 194.9.94.213
Deny from 122.166.169.127
Deny from 96.9.163.49
Deny from 54.229.73.40
Deny from 203.109.158.201
Deny from 46.105.113.8
Deny from 183.60.244.
Deny from 54.232.102.193
Deny from 195.157.124.186
Deny from 118.39.113.219
Deny from 27.255.56.87
Deny from 69.161.138.1
Deny from 192.96.204.42
Deny from 178.63.52.200
Deny from 27.252.92.103
Deny from 37.59.65.58
Deny from 186.202.126.94
Deny from 186.213.72.146
Deny from 186.219.44.6
</IfModule>
<IfModule mod_rewrite.c>
RewriteCond %{HTTP_HOST} (.*)\.crimea\.com [NC,OR]
RewriteCond %{HTTP_HOST} s368\.loopia\.se [NC,OR]
RewriteCond %{HTTP_HOST} kanagawa\.ocn [NC,OR]
RewriteCond %{HTTP_HOST} g00g1e [NC,OR]
RewriteCond %{HTTP_USER_AGENT} (g00g1e|seekerspider|siclab|spam|sqlmap) [NC]
RewriteRule .* - [F,L]
</IfModule>
<IfModule mod_alias.c>
RedirectMatch 403 router\.php
RedirectMatch 403 /\)\.html\(
</IfModule>
<IfModule mod_rewrite.c>
RewriteCond %{QUERY_STRING} http\:\/\/www\.google\.com\/humans\.txt\? [NC]
RewriteRule .* - [F,L]
</IfModule>
EOF
fi
cat >> .htaccess <<EOF
# BEGIN Enable gzip compression
SetOutputFilter DEFLATE
AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml application/x-javascript application/x-httpd-php
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)\\\$ no-gzip
Header append Vary User-Agent env=!dont-vary
# END Enable gzip compression
# BEGIN $SITE_TYPE
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase ${URL_PATH}
RewriteRule ^index\.php\$ - [L]
EOF
if [ "$IS_MULTISITE" = true ] ; then
cat >> .htaccess <<EOF
# Add a trailing slash to /wp-admin
RewriteRule ^([_0-9a-zA-Z-]+)?/wp-admin\$ \$1/wp-admin/ [R=301,L]
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) \$2 [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)\$ \$2 [L]
EOF
else
cat >> .htaccess <<EOF
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase ${URL_PATH}
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
EOF
fi
cat >> .htaccess <<EOF
RewriteRule . ${URL_PATH}index.php [L]
</IfModule>
# END $SITE_TYPE
EOF
# Install WordPress
# -----------------
# Fetch WP-CLI
# Latest release:
# curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
# Nightly:
curl https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar -o wp-cli.phar
php -c ../ wp-cli.phar core download
# BEGIN wp-config
php -c ../ wp-cli.phar core config --dbname="$WP_DBNAME" --dbuser="$WP_DBUSER" --dbpass="$WP_DBPASS" --dbhost="$DBHOST" --dbprefix="$WP_DBPREFIX"
# If using environment variables, insert them into wp-config.php
if [ "$IS_DB_ENV_VARS" = true ] ; then
perl -pi -e "s/define\('DB_NAME',.+/define('DB_NAME', getenv('DBNAME'));/" wp-config.php
perl -pi -e "s/define\('DB_USER',.+/define('DB_USER', getenv('DBUSER'));/" wp-config.php
perl -pi -e "s/define\('DB_PASSWORD',.+/define('DB_PASSWORD', getenv('DBPASS'));/" wp-config.php
fi
# Insert logic for (better) environment-specific site urls
perl -pi -e "s|<\?php|<?php\n\n// ** Set WP site url ** //\n/** Determine url of current environment */\nif ( ! array_key_exists( 'SHELL', \\\$_SERVER ) ) {\n\t\\\$s = ! empty( \\\$_SERVER['HTTPS'] ) && \\\$_SERVER['HTTPS'] == 'on' ? 's' : '';\n\t\\\$port = \\\$_SERVER['SERVER_PORT'] == '80' ? '' : \":\\\${_SERVER['SERVER_PORT']}\";\n\t\\\$path_end = strrpos( \\\$_SERVER['SCRIPT_NAME'], '/' );\n\t\\\$path = substr( \\\$path, 0, \\\$path_end );\n\t\\\$url = \"http\\\$s://\\\${_SERVER['HTTP_HOST']}\\\$port\\\$path\";\n\tdefine( 'WP_HOME', \\\$url );\n\tdefine( 'WP_SITEURL', \\\$url );\n\tunset( \\\$s, \\\$port, \\\$path_end, \\\$path, \\\$url );\n}|" wp-config.php
# END wp-config
INSTALL_COMMAND="install"
INSTALL_EXTRAS=""
ACTIVATE_OPTION="--activate"
if [ "$IS_MULTISITE" = true ] ; then
INSTALL_COMMAND="multisite-install"
INSTALL_EXTRAS="--base=$URL_PATH"
ACTIVATE_OPTION+="-network"
fi
php -c ../ wp-cli.phar core $INSTALL_COMMAND --url="$URL" --title="$TITLE" --admin_user="$ADMINUSER" --admin_password="$ADMINPASS" --admin_email="$ADMINEMAIL" $INSTALL_EXTRAS
# Install Repo Plugins
php -c ../ wp-cli.phar plugin install $PLUGINS $ACTIVATE_OPTION
# Restore existing wp-content
if [ ${#SITE_DIR_TEMP} -gt 0 ] ; then
# Make sure invisible files get copied
shopt -s dotglob
cp -R ../$SITE_DIR_TEMP/* ../$SITE_DIR/
# Cleanup
rm -r ../$SITE_DIR_TEMP
fi
# Scaffolding with Underscores starter theme and starter plugin
if [ "$IS_INITIAL_INSTALL" = true ] ; then
php -c ../ wp-cli.phar scaffold _s $THEME_SLUG --theme_name="$THEME_NAME" --author="$THEME_AUTHOR_NAME" --author_uri="$THEME_AUTHOR_URI" --sassify --activate
php -c ../ wp-cli.phar scaffold plugin $PLUGIN_SLUG --plugin_name="$PLUGIN_NAME" --skip-tests --activate
else
php -c ../ wp-cli.phar theme activate $THEME_SLUG
php -c ../ wp-cli.phar plugin activate $PLUGIN_SLUG
fi
# Set rewrite (permalink) rules
php -c ../ wp-cli.phar rewrite structure '/%postname%/'
php -c ../ wp-cli.phar rewrite flush
# Misc Cleanup
php -c ../ wp-cli.phar post delete 1
php -c ../ wp-cli.phar comment delete 1
# php wp-cli.phar plugin delete hello-dolly
# Cleanup WP-CLI executable
rm wp-cli.phar
# Restore shell
# -------------
export PATH=$PATH_ORIGINAL
# Move back to parent directory
cd ../
# Commit Inital Version
# ---------------------
if [ "$IS_INITIAL_INSTALL" = true ] ; then
read -p "Commit inital version of site? (y/N) " do_commit
if [[ $do_commit =~ ^([Yy][Ee][Ss]|[Yy])$ ]] ; then
git add .
git commit -m "Initial commit of WP site (via $0)"
fi
fi
# Success Message
# ---------------
echo -e "
\033[32mYour new $SITE_TYPE site is installed and ready to use at:
\033[36m$URL
\033[0m🌱"
@acusti
Copy link
Author

acusti commented Mar 30, 2015

Usage

  1. Make a copy of this script in the parent directory of your web root (the parent of the directory into which you want to install WordPress)
  2. Configure the script by updating the variables in the Configuration section at the top of the file
  3. Install WordPress and any plugins you specified (will prompt for MySQL root password):
$ ./install.sh

The script will create your MySQL database and user as specified in the config (or do nothing if DB and DB user already exist), adapt to your current environment, set up .gitignore and .htaccess files, download WP-CLI, prompt you to indicate if you want to install WordPress Multisite, install WordPress, offer to commit the generated starter theme and plugin to git, and clean up after itself.

Uninstall

To uninstall your site (remove its files and drop its database and user), use the --uninstall option (will prompt for MySQL root password):

$ ./install.sh --uninstall

Notes

You may need to explicitly make the script executable: chmod +x install.sh

Dependencies

PHP / MySQL

Tested on CentOS and MAMP

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment