WordPress Archives - Justin Silver https://www.justinsilver.com/tag/wordpress/ Technology, Travel, and Pictures Fri, 01 Mar 2019 17:55:51 +0000 en-US hourly 1 https://wordpress.org/?v=6.0.1 https://www.justinsilver.com/wp-content/uploads/2013/06/cropped-apple-touch-icon-160x160.png WordPress Archives - Justin Silver https://www.justinsilver.com/tag/wordpress/ 32 32 LEMP: CentOS 7, NGINX, PHP7, and Redis for WordPress https://www.justinsilver.com/technology/linux/lemp-centos-7-nginx-php7-redis-wordpress/?utm_source=rss&utm_medium=rss&utm_campaign=lemp-centos-7-nginx-php7-redis-wordpress https://www.justinsilver.com/technology/linux/lemp-centos-7-nginx-php7-redis-wordpress/#comments Fri, 23 Sep 2016 17:29:30 +0000 https://www.justinsilver.com/?p=4227 Scripts to setup a WordPress server on CentOS 7 with NGINX, PHP/PHP-FPM 7, Redis and more. Firewalld – allow only HTTP/S and SSH traffic Letsencrypt – free SSL certificates Yum Cron – keep system...

The post LEMP: CentOS 7, NGINX, PHP7, and Redis for WordPress appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

Scripts to setup a WordPress server on CentOS 7 with NGINX, PHP/PHP-FPM 7, Redis and more.

  • Firewalld – allow only HTTP/S and SSH traffic
  • Letsencrypt – free SSL certificates
  • Yum Cron – keep system up to date automatically
  • Redis – persistent, in-memory data store
  • PHP7 + PHP-FPM – PHP Next Generation with php-fpm for compatibility with Nginx
  • NGINX – event based web server
    • NGINX configurations – blacklisted referers, Cloudflare IP forwarding, MIME types, Security features, SSL, WordPress configuration

Firewalld

Enable the firewalld service and only allow http/s traffic to the server – in addition to the default of just ssh.

systemctl enable firewalld
service firewalld start
firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --reload

Letsencrypt

Use Letsencrypt for free SSL certificates.

yum -y install letsencrypt
openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

Yum Cron

#!/bin/bash

# make sure the YUM_CRON_EMAIL is set
if [[ -z $YUM_CRON_EMAIL ]]; then
  echo "You must specify an email using \$YUM_CRON_EMAIL";
else
  # install and enable, plus patch for bug fixing
  yum -y install yum-cron patch
  chkconfig yum-cron on

  # configure via sed replacements
  sed -i "s|^email_to = root|email_to = ${YUM_CRON_EMAIL}|" /etc/yum/yum-cron.conf
  sed -i 's|^update_messages = no|update_messages = yes|' /etc/yum/yum-cron.conf
  sed -i 's|^download_updates = no|download_updates = yes|' /etc/yum/yum-cron.conf
  sed -i 's|^apply_updates = no|apply_updates = yes|' /etc/yum/yum-cron.conf
  sed -i 's|^emit_via = stdio|emit_via = email|' /etc/yum/yum-cron.conf

  sed -i "s|^email_to = root|email_to = ${YUM_CRON_EMAIL}|" /etc/yum/yum-cron-hourly.conf
  sed -i 's|^update_cmd = default|update_cmd = security|' /etc/yum/yum-cron-hourly.conf
  sed -i 's|^update_messages = no|update_messages = yes|' /etc/yum/yum-cron-hourly.conf
  sed -i 's|^download_updates = no|download_updates = yes|' /etc/yum/yum-cron-hourly.conf
  sed -i 's|^apply_updates = no|apply_updates = yes|' /etc/yum/yum-cron-hourly.conf
  sed -i 's|^emit_via = stdio|emit_via = email|' /etc/yum/yum-cron-hourly.conf   

  egrep '^email_to|^update_messages|^download_updates|^apply_updates|^emit_via' /etc/yum/yum-cron.conf
  egrep '^email_to|^update_cmd|^update_messages|^download_updates|^apply_updates|^emit_via' /etc/yum/yum-cron-hourly.conf
  
  # fix bug in yum-cron nightly updates
  if [[ $(grep -q "# success, dependencies resolved" /usr/sbin/yum-cron) -ne 0 ]]; then
    patch /usr/sbin/yum-cron <<PATCHFILE
--- yum-cron.orig 2016-10-23 19:24:57.099859931 +0000
+++ yum-cron  2016-10-23 19:27:58.048784006 +0000
@@ -504,7 +504,13 @@
     except yum.Errors.RepoError, e:
       self.emitCheckFailed("%s" %(e,))
       sys.exit()
-        if res != 2:
+        if res == 0:
+            # success, empty transaction
+            sys.exit(0)
+        elif res == 2:
+            # success, dependencies resolved
+            pass
+        else:
       self.emitCheckFailed("Failed to build transaction: %s" %(str.join("\n", resmsg),))
       sys.exit(1)
PATCHFILE
  fi  
  # (re)start the yum-cron service
  (service yum-cron status > /dev/null && service yum-cron restart) || service yum-cron start
fi

Redis / EPEL

Redis is available via EPEL and provides a great in memory cache.

#!/bin/bash

# install the EPEL repo to access Redis
yum install -y epel-release
yum install -y redis

# fix redis background saves on low memory
sysctl vm.overcommit_memory=1 && cat <<SYSCTL_MEM > /etc/sysctl.d/88-vm.overcommit_memory.conf
vm.overcommit_memory = 1
SYSCTL_MEM

# increase max connections
sysctl -w net.core.somaxconn=65535 && cat <<SYSCTL_CONN > /etc/sysctl.d/88-net.core.somaxconn.conf
net.core.somaxconn = 65535
SYSCTL_CONN

sysctl -w fs.file-max=100000 && cat <<SYSCTL_FILEMAX > /etc/sysctl.d/88-fs.file-max.conf
fs.file-max = 100000
SYSCTL_FILEMAX

sed -i "s|^tcp-backlog [[:digit:]]\+|tcp-backlog 65535|" /etc/redis.conf

# enable redis service on reboot
systemctl enable redis

# start service
(service redis status > /dev/null && service redis restart) || service redis start
#!/bin/bash

# Create Service to disable THP
cat <<DISABLE_THP > /etc/systemd/system/disable-thp.service
[Unit]
Description=Disable Transparent Huge Pages (THP)

[Service]
Type=simple
ExecStart=/bin/sh -c "echo 'never' > /sys/kernel/mm/transparent_hugepage/enabled && echo 'never' > /sys/kernel/mm/transparent_hugepage/defrag"

[Install]
WantedBy=multi-user.target
DISABLE_THP

sudo systemctl daemon-reload
sudo systemctl start disable-thp
sudo systemctl enable disable-thp

PHP / PHP-FPM

Install PHP and PHP-FPM from the Remi Safe Repo. Some config files and binaries will need to be symlinked for compatibility.

# install the remi-safe.repo
cat <<REMISAFE > /etc/yum.repos.d/remi-safe.repo
# This repository is safe to use with RHEL/CentOS base repository
# it only provides additional packages for the PHP stack
# all dependencies are in base repository or in EPEL

[remi-safe]
name=Safe Remi's RPM repository for Enterprise Linux \$releasever - \$basearch
#baseurl=http://rpms.remirepo.net/enterprise/\$releasever/safe/\$basearch/
mirrorlist=http://rpms.remirepo.net/enterprise/\$releasever/safe/mirror
enabled=1
gpgcheck=1
gpgkey=http://rpms.remirepo.net/RPM-GPG-KEY-remi
REMISAFE

# install php7 and modules
yum install -y \
  php70 \
  php70-php-fpm \
  php70-php-gd \
  php70-php-json \
  php70-php-mbstring \
  php70-php-mysqlnd \
  php70-php-pdo \
  php70-php-pecl-apcu \
  php70-php-pecl-apcu-bc \
  php70-php-pecl-igbinary \
  php70-php-pecl-imagick \
  php70-php-pecl-redis \
  php70-php-xml

# start php-fpm at boot
systemctl enable php70-php-fpm

# link the systemd service to "php-fpm"
[[ -f /usr/lib/systemd/system/php-fpm.service ]] || ln -s /usr/lib/systemd/system/php70-php-fpm.service /usr/lib/systemd/system/php-fpm.service

# link the binaries
[[ -f /usr/bin/php ]] || ln -s `which php70` /usr/bin/php
[[ -f /usr/bin/php-cgi ]] || ln -s `which php70-cgi` /usr/bin/php-cgi
[[ -f /usr/bin/php-phar ]] || ln -s `which php70-phar` /usr/bin/php-phar

# link the php-fpm configs
[[ -f /etc/php-fpm.conf ]] || ln -s /etc/opt/remi/php70/php-fpm.conf /etc/php-fpm.conf
[[ -d /etc/php-fpm.d ]] || ln -s /etc/opt/remi/php70/php-fpm.d /etc/php-fpm.d

mkdir -p /var/log/php-fpm && chown -R nginx.nginx /var/log/php-fpm
mkdir -p /var/lib/php/session && mkdir -p /var/lib/php/wsdlcache && mkdir -p /var/lib/php/opcache
chown -R nginx.nginx /var/lib/php/*
sed -i -e 's/user = apache/user = nginx/' /etc/php-fpm.d/www.conf
sed -i -e 's/group = apache/group = nginx/' /etc/php-fpm.d/www.conf
sed -i -e 's|/var/opt/remi/php70/log/php-fpm/www-error.log|/var/log/php-fpm/www-error.log|' /etc/php-fpm.d/www.conf
sed -i -e 's|/var/opt/remi/php70/lib/php/session|/var/lib/php/session|' /etc/php-fpm.d/www.conf
sed -i -e 's|/var/opt/remi/php70/lib/php/wsdlcache|/var/lib/php/wsdlcache|' /etc/php-fpm.d/www.conf
sed -i -e 's|/var/opt/remi/php70/lib/php/opcache|/var/lib/php/opcache|' /etc/php-fpm.d/www.conf

NGINX

Install NGINX with HTTP2 support.

# install nginx repo
cat <<REPO > /etc/yum.repos.d/nginx.repo
[nginx]
name=nginx repo
# default repo
#baseurl=http://nginx.org/packages/centos/\$releasever/\$basearch/
# mainline "dev" repo for http2 support
baseurl=http://nginx.org/packages/mainline/centos/\$releasever/\$basearch/
gpgcheck=0
enabled=1
REPO
 
# install and enable nginx
yum install -y nginx
systemctl enable nginx
 
# enable httpd in selinux
semanage permissive -a httpd_t
 
# test your configuration and reload
nginx -t && service nginx start

Included via symlink in conf.d

Add a bunch of config files for Nginx.

# includes for nginx configurations
mkdir -p /etc/nginx/includes

# ssl settings for virtual hosts
cat <<BLACKLIST > /etc/nginx/includes/blacklist.conf
#  ██████╗ ██╗      █████╗  ██████╗██╗  ██╗██╗     ██╗███████╗████████╗
#  ██╔══██╗██║     ██╔══██╗██╔════╝██║ ██╔╝██║     ██║██╔════╝╚══██╔══╝
#  ██████╔╝██║     ███████║██║     █████╔╝ ██║     ██║███████╗   ██║   
#  ██╔══██╗██║     ██╔══██║██║     ██╔═██╗ ██║     ██║╚════██║   ██║   
#  ██████╔╝███████╗██║  ██║╚██████╗██║  ██╗███████╗██║███████║   ██║   
#  ╚═════╝ ╚══════╝╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝╚══════╝╚═╝╚══════╝   ╚═╝   
                                                                    
#-*- mode: nginx; mode: flyspell-prog; ispell-local-dictionary: "american" -*-
### This file implements a blacklist for certain user agents and
### referrers. It's a first line of defense. It must be included
### inside a http block.

## Add here all user agents that are to be blocked.
map \$http_user_agent \$bad_bot {
    default 0;
    ~*^Lynx 0; # Let Lynx go through
    libwww-perl                      1;
    ~*(?i)(httrack|htmlparser|libwww|JikeSpider|proximic|Sosospider|Baiduspider|msnbot|BBBike|WWWOFFLE|Widow|SuperHTTP|BlackWidow|HTTrack|Java|Pixray|CPython|Spinn3r|Abonti|MSIECrawler|Baiduspider|Yandex|Siteimprove|Aboundex|80legs|360Spider|^Java|Cogentbot|^Alexibot|^asterias|^attach|^BackDoorBot|^BackWeb|Bandit|^BatchFTP|^Bigfoot|^Black.Hole|^BlackWidow|^BlowFish|^BotALot|Buddy|^BuiltBotTough|^Bullseye|^BunnySlippers|^Cegbfeieh|^CheeseBot|^CherryPicker|^ChinaClaw|Collector|Copier|^CopyRightCheck|^cosmos|^Crescent|^Custo|^AIBOT) 1;
}

## Add here all referrers that are to blocked.
map \$http_referer \$bad_referer {
  default 0;
  ~*(?i)(adult|babes|click|diamond|forsale|girl|jewelry|love|nudit|organic|poker|porn|poweroversoftware|sex|teen|webcam|zippo|casino|replica|en.savefrom.net|7makemoneyonline.com|acunetix-referrer.com|adcash.com|bithack.ru|buttons-for-website.com|cenokos.ru|cenoval.ru|cityadspix.com|darodar.com|econom.co|edakgfvwql.ru|gobongo.info|iedit.ilovevitaly.com|ilovevitaly.com|ilovevitaly.co|ilovevitaly.info|ilovevitaly.org|ilovevitaly.ru|iskalko.ru|luxup.ru|make-money-online.7makemoneyonline.com|maps.ilovevitaly.com|myftpupload.com|savefrom.net|savetubevideo.com|screentoolkit.com|semalt.com|seoexperimenty.ru|shopping.ilovevitaly.ru|slftsdybbg.ru|socialseet.ru|srecorder.com|st3.cwl.yahoo.com|superiends.org|vodkoved.ru|websocial.me|ykecwqlixx.ru|yougetsignal.com|priceg.com|responsinator.com|o-o-6-o-o.ru|o-o-8-o-o.ru|12masterov.com|4webmasters.org|acads.net|adviceforum.info|affordablewebsitesandmobileapps.com|anal-acrobats.hol.es|anticrawler.org|bard-real.com.ua|best-seo-offer.com|best-seo-solution.com|bestwebsitesawards.com|billiard-classic.com.ua|blackhatworth.com|brakehawk.com|buttons-for-your-website.com|buy-cheap-online.info|buy-forum.ru|cardiosport.com.ua|ci.ua|customsua.com.ua|delfin-aqua.com.ua|dipstar.org|domination.ml|drupa.com|dvr.biz.ua|e-kwiaciarz.pl|este-line.com.ua|europages.com.ru|event-tracking.com|forum20.smailik.org|forum69.info|free-share-buttons.com|free-social-buttons.com|generalporn.org|get-free-traffic-now.com|ghazel.ru|googlsucks.com|guardlink.org|hulfingtonpost.com|humanorightswatch.org|ico.re|iloveitaly.ro|iloveitaly.ru|iminent.com|it-max.com.ua|kabbalah-red-bracelets.com|kambasoft.com|makemoneyonline.com|maridan.com.ua|masterseek.com|mebeldekor.com.ua|med-zdorovie.com.ua|mirobuvi.com.ua|ok.ru|onlywoman.org|o-o-6-o-o.com|palvira.com.ua|pornhub-forum.ga|pornhub-forum.uni.me|prodvigator.ua|ranksonic.info|ranksonic.org|rapidgator-porn.ga|resellerclub.com|sanjosestartups.com|search-error.com|sexyteens.hol.es|shop.xz618.com|simple-share-buttons.com|social-buttons.com|theguardlan.com|trion.od.ua|webmaster-traffic.com|websites-reviews.com|youporn-forum.ga|youporn-forum.uni.me|наркомания.лечениенаркомании.com|непереводимая.рф|floating-share-buttons.com|traffic2money.com|site7.free-floating-buttons.com|sexyali.com|get-free-social-traffic.com|site2.free-floating-buttons.com|success-seo.com|trafficmonetizer.org|chinese-amezon.com|free-social-buttons.com) 1;
}

## Add here all hosts that should be spared any referrer checking.
geo \$bad_referer {
  127.0.0.1 0;
  192.168.1.0/24 0;
  217.23.7.130 0;
  78.110.60.230 0;
  193.227.240.37 0;
  193.227.240.38 0;
}

map \$http_user_agent \$limit_bots {
  default 0;
  ~*(google|bing|yandex|msnbot) 1;
  ~*(AltaVista|Googlebot|Slurp|BlackWidow|Bot|ChinaClaw|Custo|DISCo|Download|Demon|eCatch|EirGrabber|EmailSiphon|EmailWolf|SuperHTTP|Surfbot|WebWhacker) 2;
  ~*(Express|WebPictures|ExtractorPro|EyeNetIE|FlashGet|GetRight|GetWeb!|Go!Zilla|Go-Ahead-Got-It|GrabNet|Grafula|HMView|Go!Zilla|Go-Ahead-Got-It) 2;
  ~*(rafula|HMView|HTTrack|Stripper|Sucker|Indy|InterGET|Ninja|JetCar|Spider|larbin|LeechFTP|Downloader|tool|Navroad|NearSite|NetAnts|tAkeOut|WWWOFFLE) 2;
  ~*(GrabNet|NetSpider|Vampire|NetZIP|Octopus|Offline|PageGrabber|Foto|pavuk|pcBrowser|RealDownload|ReGet|SiteSnagger|SmartDownload|SuperBot|WebSpider) 2;
  ~*(Teleport|VoidEYE|Collector|WebAuto|WebCopier|WebFetch|WebGo|WebLeacher|WebReaper|WebSauger|eXtractor|Quester|WebStripper|WebZIP|Wget|Widow|Zeus) 2;
  ~*(Twengabot|htmlparser|libwww|Python|perl|urllib|scan|Curl|email|PycURL|Pyth|PyQ|WebCollector|WebCopy|webcraw) 2;
}
BLACKLIST

cat <<CLOUDFLARECONF > /etc/nginx/includes/cloudflare.conf
#   ██████╗██╗      ██████╗ ██╗   ██╗██████╗ ███████╗██╗      █████╗ ██████╗ ███████╗
#  ██╔════╝██║     ██╔═══██╗██║   ██║██╔══██╗██╔════╝██║     ██╔══██╗██╔══██╗██╔════╝
#  ██║     ██║     ██║   ██║██║   ██║██║  ██║█████╗  ██║     ███████║██████╔╝█████╗  
#  ██║     ██║     ██║   ██║██║   ██║██║  ██║██╔══╝  ██║     ██╔══██║██╔══██╗██╔══╝  
#  ╚██████╗███████╗╚██████╔╝╚██████╔╝██████╔╝██║     ███████╗██║  ██║██║  ██║███████╗
#   ╚═════╝╚══════╝ ╚═════╝  ╚═════╝ ╚═════╝ ╚═╝     ╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝
                                                                              
set_real_ip_from 199.27.128.0/21;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20; 
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;

set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
CLOUDFLARECONF

cat <<MIMETYPESCONF > /etc/nginx/includes/mime.types.conf
#  ███╗   ███╗██╗███╗   ███╗███████╗    ████████╗██╗   ██╗██████╗ ███████╗███████╗
#  ████╗ ████║██║████╗ ████║██╔════╝    ╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝██╔════╝
#  ██╔████╔██║██║██╔████╔██║█████╗         ██║    ╚████╔╝ ██████╔╝█████╗  ███████╗
#  ██║╚██╔╝██║██║██║╚██╔╝██║██╔══╝         ██║     ╚██╔╝  ██╔═══╝ ██╔══╝  ╚════██║
#  ██║ ╚═╝ ██║██║██║ ╚═╝ ██║███████╗       ██║      ██║   ██║     ███████╗███████║
#  ╚═╝     ╚═╝╚═╝╚═╝     ╚═╝╚══════╝       ╚═╝      ╚═╝   ╚═╝     ╚══════╝╚══════╝
                                                                              
types {
  application/x-font-ttf ttf;
  font/opentype otf;
}
MIMETYPESCONF



# use a conf file to include our sites-enabled conf files
mkdir -p /etc/nginx/sites-available
mkdir -p /etc/nginx/sites-enabled
cat <<SITESENABLED > /etc/nginx/includes/sites-enabled.conf
#  ██╗    ██╗███████╗██████╗ ███████╗██╗████████╗███████╗███████╗
#  ██║    ██║██╔════╝██╔══██╗██╔════╝██║╚══██╔══╝██╔════╝██╔════╝
#  ██║ █╗ ██║█████╗  ██████╔╝███████╗██║   ██║   █████╗  ███████╗
#  ██║███╗██║██╔══╝  ██╔══██╗╚════██║██║   ██║   ██╔══╝  ╚════██║
#  ╚███╔███╔╝███████╗██████╔╝███████║██║   ██║   ███████╗███████║
#   ╚══╝╚══╝ ╚══════╝╚═════╝ ╚══════╝╚═╝   ╚═╝   ╚══════╝╚══════╝
                                                                                                                                                             
include /etc/nginx/sites-enabled/*.conf;
SITESENABLED

ln -s /etc/nginx/includes/blacklist.conf /etc/nginx/conf.d/_.blacklist.conf
ln -s /etc/nginx/includes/cloudflare.conf /etc/nginx/conf.d/_.cloudflare.conf
ln -s /etc/nginx/includes/mime.types.conf /etc/nginx/conf.d/_.mime.types.conf
ln -s /etc/nginx/includes/sites-enabled.conf /etc/nginx/conf.d/_.sites-enabled.conf

Included in server block

These Nginx include files are meant to be using inside virtual server blocks.

# ssl settings for virtual hosts
cat <<SSLCONF > /etc/nginx/includes/ssl.conf
#  ███████╗███████╗██╗     
#  ██╔════╝██╔════╝██║     
#  ███████╗███████╗██║     
#  ╚════██║╚════██║██║     
#  ███████║███████║███████╗
#  ╚══════╝╚══════╝╚══════╝
                        
# Use TLS (so don't use old version of SSL)
ssl_protocols TLSv3 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_dhparam /etc/ssl/certs/dhparam.pem; 
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
SSLCONF

# use a conf file to include our sites-enabled conf files
cat <<SECURITY > /etc/nginx/includes/security.conf
#  ███████╗███████╗ ██████╗██╗   ██╗██████╗ ██╗████████╗██╗   ██╗
#  ██╔════╝██╔════╝██╔════╝██║   ██║██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝
#  ███████╗█████╗  ██║     ██║   ██║██████╔╝██║   ██║    ╚████╔╝ 
#  ╚════██║██╔══╝  ██║     ██║   ██║██╔══██╗██║   ██║     ╚██╔╝  
#  ███████║███████╗╚██████╗╚██████╔╝██║  ██║██║   ██║      ██║   
#  ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝  ╚═╝╚═╝   ╚═╝      ╚═╝  

if (\$bad_referer){
  return 444;
}

location ~* (readme|changelog)\\.txt\$ {
  return 444;
}

# don't show this as it can leak info
location ~* /(\\.|(wp-config|xmlrpc)\\.php|(readme|license|changelog)\\.(html|txt)) {
  return 444;
}

location ~ /mu-plugins/ {
  return 444;
}

# no PHP execution in uploads/files
location ~* /(?:uploads|files)/.*\\.php\$ {
  deny all;
}

# hide contents of sensitive files
location ~* \\.(conf|engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\\.php)?|xtmpl)\$|^(\\..*|Entries.*|Repository|Root|Tag|Template)\$|\\.php_ {
  return 444;
}

# don't allow other executable file types
location ~* \\.(pl|cgi|py|sh|lua)\$ {
  return 444;
}

location = /robots.txt {
  if ( \$limit_bots != 1 ) {
    return 444;
  }
  expires 30d;
  add_header Cache-Control public;
  try_files /robots.txt @shared;
}

location @shared {
  root /var/www/shared;
}
SECURITY

# use a conf file to include our sites-enabled conf files
cat <<WORDPRESSCONF > /etc/nginx/includes/wordpress.conf
#  ██╗    ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗███████╗███████╗
#  ██║    ██║██╔═══██╗██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝
#  ██║ █╗ ██║██║   ██║██████╔╝██║  ██║██████╔╝██████╔╝█████╗  ███████╗███████╗
#  ██║███╗██║██║   ██║██╔══██╗██║  ██║██╔═══╝ ██╔══██╗██╔══╝  ╚════██║╚════██║
#  ╚███╔███╔╝╚██████╔╝██║  ██║██████╔╝██║     ██║  ██║███████╗███████║███████║
#   ╚══╝╚══╝  ╚═════╝ ╚═╝  ╚═╝╚═════╝ ╚═╝     ╚═╝  ╚═╝╚══════╝╚══════╝╚══════╝
                                                                           
# include standard security file
include /etc/nginx/includes/security.conf;

# allow CORS for fonts
location ~* \\.(ttf|ttc|otf|eot|woff2?|font.css|css|svg)\$ {
  add_header Access-Control-Allow-Origin *;
}

#Yoast sitemap
location ~ ([^/]*)sitemap(.*)\\.x(m|s)l\$ {
  ## this redirects sitemap.xml to /sitemap_index.xml
  rewrite ^/sitemap\\.xml\$ /sitemap_index.xml permanent;
  ## this makes the XML sitemaps work
  rewrite ^/([a-z]+)?-?sitemap\\.xsl\$ /index.php?xsl=\$1 last;
  rewrite ^/sitemap_index\\.xml\$ /index.php?sitemap=1 last;
  rewrite ^/([^/]+?)-sitemap([0-9]+)?\\.xml\$ /index.php?sitemap=\$1&sitemap_n=\$2 last;
  
  ## The following lines are optional for the premium extensions
  ## News SEO
  rewrite ^/news-sitemap\\.xml\$ /index.php?sitemap=wpseo_news last;
  ## Local SEO
  rewrite ^/locations\\.kml\$ /index.php?sitemap=wpseo_local_kml last;
  rewrite ^/geo-sitemap\\.xml\$ /index.php?sitemap=wpseo_local last;
  ## Video SEO
  rewrite ^/video-sitemap\\.xsl\$ /index.php?xsl=video last;
}

index index.php;

location / {
  try_files \$uri \$uri/ /index.php\$is_args\$args;
}

location ~ ^/(fpm-status|ping)\$ {
  include fastcgi_params;
  fastcgi_pass 127.0.0.1:9000;
  fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
  break;
}
                                  
location ~ \\.php\$ {
  # zero-day exploit defense.
  try_files \$uri =404;

  # logging vi PHP-FPM
  fastcgi_intercept_errors on;

  # pass request to fastcgi/php-cgi via spawn-fcgi
  fastcgi_pass unix:/var/run/php-fpm.sock;

  # default fastcgi_params
  include fastcgi_params;

  # max timeouts (should match php.ini)
  fastcgi_connect_timeout 600s;
  fastcgi_send_timeout 600s;
  fastcgi_read_timeout 600s;

  # override fastcgi_params
  fastcgi_param SERVER_NAME \$host;
  fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;

  break;
}

location ~ /purge(/.*) {
  #fastcgi_cache_purge WORDPRESS "\$scheme\$request_method\$host\$1";
}
WORDPRESSCONF

HTTPS Virtual Host

This is an example of a virtual host running WordPress.

# use a conf file to include our sites-enabled conf files
cat <<VIRTUALHOST > /etc/nginx/sites-available/virtualhost.conf
server {
  # Domain validation is on port 80
  listen 80;
 
  # Hostnames to listen on, you will pass each of these to letsencrypt with "-w www.example.com"
  server_name www.example.com;
  
  # Your document root, you will pass this path to letsencrypt with "-w /var/www/www.example.com/html/"
  root /var/www/www.example.com/html/;
  
  # handle letsencrypt domain validation
  location ~ /.well-known {
      allow all;
  }
  
  # permanently redirect everything else
  location / {
    return 301 https://$server_name$request_uri;
  }
}
    
server {
  # All SSL is served on 443. If available include "http2", otherwise remove it.
  listen 443 ssl http2;

  # Hostnames to listen on
  server_name www.example.com;

  # Add SSL Keys here once they are generated
  #ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  #ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  include /etc/nginx/includes/ssl.conf;
  
  # WordPress Sites
  # include /etc/nginx/includes/wordpress.conf;
  # include /var/www/www.example.com/html/nginx.conf;

  # handle all requests...
  # location / {
  # }
}
VIRTUALHOST

# link the virtual host using full pathnames for source and target!
# ln -s /etc/nginx/sites-available/virtualhost.conf /etc/nginx/sites-enabled/virtualhost.conf

nginx -t && service nginx reload

Generating Barcodes

yum install -y barcode barcode-devel php-tcpdf.noarch
cd /usr/local/src
wget https://ashberg.de/php-barcode/download/files/genbarcode-0.4.tar.gz
tar -xzvf genbarcode-0.4.tar.gz
cd genbarcode-0.4
make && make install

# use to generate via php
wget https://ashberg.de/php-barcode/download/files/php-barcode-0.4.tar.gz

The post LEMP: CentOS 7, NGINX, PHP7, and Redis for WordPress appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/linux/lemp-centos-7-nginx-php7-redis-wordpress/feed/ 3
Unban GoDaddy / MediaTemple WordPress Plugins https://www.justinsilver.com/technology/wordpress/unban-godaddy-mediatemple-wordpress-plugins/?utm_source=rss&utm_medium=rss&utm_campaign=unban-godaddy-mediatemple-wordpress-plugins https://www.justinsilver.com/technology/wordpress/unban-godaddy-mediatemple-wordpress-plugins/#respond Thu, 17 Mar 2016 19:22:03 +0000 http://www.justinsilver.com/?p=4007 GoDaddy managed WordPress hosting, and by ownership MediaTemple as well, prevents some plugins from being activated on website. Some of these may make some sense if the conflict with caching, etc, but I feel...

The post Unban GoDaddy / MediaTemple WordPress Plugins appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

GoDaddy managed WordPress hosting, and by ownership MediaTemple as well, prevents some plugins from being activated on website. Some of these may make some sense if the conflict with caching, etc, but I feel like including a list is good enough and if you want to install that plugin on the website you’re paying for, so be it. There are several plugins that are banned in the list that I regularly use in my website development, for example Gravity Forms.

If you try to enable this plugin on a site hosted on GoDaddy or MediaTemple rather than activating, you get an error that says:

Not Available: This plugin is not allowed on our system due to performance, security, or compatibility concerns. Please contact our support with any questions.

There is a Must-Use Plugin called gd-system-plugin that includes a class called GD_System_Plugin_Blacklist. This class hooks into the plugin installer code to hide links, show errors, and otherwise prevent some plugins from being installed and activated. The way that it does this is by calling the GD_System_Plugin_Blacklist->get_blacklist() method. This method tries to get a value for get_site_transient( 'gd_system_blacklist' ) and if it’s empty, then fetches the list from a URL and caches it.

Luckily there is a filter for pre_site_transient_* so you can short circuit the whole thing.

Unban GoDaddy Plugin Code

We can’t just return an empty array here, because there is a check for empty(), but we can return our own “list” of banned plugins instead of fetching it from a URL (or even the transient cache if it was already set). Plugin code is below, but the filter could just as easily be added to another plugin, functions.php in your theme, or another mu-plugin.

/*
Plugin Name: Unban Goddady/MediaTemple Plugins
Description: Allow plugins banned by GoDaddy to be used on your site
*/
add_filter( 'pre_site_transient_gd_system_blacklist', function(){
    return array( array( 'name'=>'godaddy', 'minVersion'=>0, 'maxVersion'=>1 ) );
} );

List of Plugins Banned on GoDaddy and MediaTemple

This is the list of plugins banned by GoDaddy as of the writing of this post.

6scan-backup
6scan-protection
adminer
adsense-click-fraud-monitoring
all-in-one-seo-pack
all-in-one-wp-migration
aspose-cloud-ebook-generator
aspose-doc-exporter
backupwordpress
backwpup
broken-link-checker
contextual-related-posts
custom-contact-forms
easy-coming-soon
ezpz-one-click-backup
fancybox-for-wordpress
favicon-by-realfavicongenerator
fuzzy-seo-booster
google-analytics-for-wordpress
google-sitemap-generator
google-xml-sitemaps-with-multisite-support
gravityforms
inboundio-marketing
iwp-client
jr-referrer
leads
liveforms
miwoftp
mp3-jplayer
newsletter
nextgen-gallery
pagelines
photo-gallery
php-event-calendar
platform
pluscaptcha
pods
portable-phpmyadmin
ptengine-real-time-web-analytics-and-heatmap
quick-cache
referrer-wp
schram-kljsdfjkl
seo-alrp
sgcachepress
similar-posts
statpress
synthesis
tdwordcount
the-codetree-backup
toolspack
ultimate-member
updraft
w3-total-cache
wordpress-beta-tester
wordpress-gzip-compression
wordpress-popular-posts
wordpress-seo
work-the-flow-file-upload
wp-all-import
wp-business-intelligence-lite
wp-cache
wp-cachecom
wp-copysafe-pdf
wp-copysafe-web
wp-engine-snapshot
wp-fast-cache
wp-fastest-cache
wp-file-cache
wp-phpmyadmin
wp-postviews
wp-power-stats
wp-slimstat
wp-super-cache
wp-ultimate-csv-importer
wpengine-common
wponlinebackup
wptouch
wysija-newsletters
yet-another-featured-posts-plugin
yet-another-related-posts-plugin

The post Unban GoDaddy / MediaTemple WordPress Plugins appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/wordpress/unban-godaddy-mediatemple-wordpress-plugins/feed/ 0
ACF: Link to Specific Tab https://www.justinsilver.com/technology/wordpress/acf-link-to-specific-tab/?utm_source=rss&utm_medium=rss&utm_campaign=acf-link-to-specific-tab https://www.justinsilver.com/technology/wordpress/acf-link-to-specific-tab/#comments Thu, 03 Sep 2015 16:04:50 +0000 http://justinsilver.com/?p=3931 Advanced Custom Fields, or ACF, provides a great user interface for defining and laying out custom fields for Posts, Pages, Custom Post Types, Options, Users and more in WordPress. The Tab Field provides an...

The post ACF: Link to Specific Tab appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

Advanced Custom Fields, or ACF, provides a great user interface for defining and laying out custom fields for Posts, Pages, Custom Post Types, Options, Users and more in WordPress. The Tab Field provides an easy way to clean up your user interface by seperating different field inputs into different tabs, making it easier for your users to find what they are looking for.

One feature that is missing from the Tab field however is the ability to link directly to a specific tab from another page. We can work around this with some pretty straightforward JavaScript. In short when the acf “ready” event fires, we want to look at all the tab buttons, and if any match our URL hash (in lowercase with spaces replaced with hyphens) then we “click” that link, loading the appropriate tab. Subsequently we can update the hash in the address bar any time a new tab is clicked making it easy for uses to bookmark the page or send the link to another user.

If your tab name is “Options Tab 1” the link to it would be something like edit.php?post_type=my-post-type#options-tab-1.

(function($){
	// run when ACF is ready
	acf.add_action('ready', function(){
		// check if there is a hash
		if (location.hash.length>1){
			// get everything after the #
			var hash = location.hash.substring(1);
			// loop through the tab buttons and try to find a match
			$('.acf-tab-wrap .acf-tab-button').each(function(i, button){ 
				if (hash==$(button).text().toLowerCase().replace(' ', '-')){
					// if we found a match, click it then exit the each() loop
					$(button).trigger('click');
					return false;
				}
			});
		}
		// when a tab is clicked, update the hash in the URL
		$('.acf-tab-wrap .acf-tab-button').on('click', function($el){
			location.hash='#'+$(this).text().toLowerCase().replace(' ', '-');
		});
	});
})(jQuery);

The post ACF: Link to Specific Tab appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/wordpress/acf-link-to-specific-tab/feed/ 5
Admin Columns Pro SSL Upgrade Fix https://www.justinsilver.com/technology/wordpress/admin-columns-pro-ssl-upgrade-fix/?utm_source=rss&utm_medium=rss&utm_campaign=admin-columns-pro-ssl-upgrade-fix https://www.justinsilver.com/technology/wordpress/admin-columns-pro-ssl-upgrade-fix/#comments Sun, 02 Nov 2014 19:21:28 +0000 http://justinsilver.com/?p=3801 I use the Admin Columns Pro plugin on several of my WordPress site to easily customize the layout in my admin tables. As it is a premium plugin updates to it are not hosted...

The post Admin Columns Pro SSL Upgrade Fix appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

I use the Admin Columns Pro plugin on several of my WordPress site to easily customize the layout in my admin tables. As it is a premium plugin updates to it are not hosted on the WordPress repository but rather come from their own private repository. This was working fine until a recently when I started getting errors during the upgrade process. It seems as though the SSL request was to www.admincolumns.com but for some reason the server was responding with a wildcard cert for *.wpengine.com – their hosting provider.

I opened a ticket on their support site and while helpful, unfortunately they were not able to come to a resolution. The error during the plugin update reads like the following.

Enabling Maintenance mode…

Updating Plugin Admin Columns Pro (1/1)
Downloading update from https://www.admincolumns.com?wc-api=software-licence-api&request=plugindownload&licence_key=XXXXXXXXXXX&plugin_name=admin-columns-pro…
An error occurred while updating Admin Columns Pro: Download failed. SSL: certificate subject name '*.wpengine.com' does not match target host name 'www.admincolumns.com'

Disabling Maintenance mode…

Root Issue

I was never able to determine the root issue other than it likely likes with WP Engine. The behavior is not consistent between environments, for example from my Mac running OSX Yosemite I can can use curl to load the Admin Columns Pro site via curl:

> curl -v https://www.admincolumns.com
* Adding handle: conn: 0x7fa2cc004000
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x7fa2cc004000) send_pipe: 1, recv_pipe: 0
* About to connect() to www.admincolumns.com port 443 (#0)
*   Trying 178.79.179.38...
* Connected to www.admincolumns.com (178.79.179.38) port 443 (#0)
* TLS 1.0 connection using TLS_RSA_WITH_AES_128_CBC_SHA
* Server certificate: www.admincolumns.com
* Server certificate: RapidSSL CA
* Server certificate: GeoTrust Global CA
> GET / HTTP/1.1
> User-Agent: curl/7.30.0
> Host: www.admincolumns.com
> Accept: */*
>
< HTTP/1.1 200 OK

But the same request did not work from my CentOS 5 servers:

[root@dev1 ~]# curl -v https://www.admincolumns.com
* About to connect() to www.admincolumns.com port 443 (#0)
*   Trying 178.79.179.38... connected
* Connected to www.admincolumns.com (178.79.179.38) port 443 (#0)
* successfully set certificate verify locations:
*   CAfile: /etc/pki/tls/certs/ca-bundle.crt
  CApath: none
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS handshake, Server finished (14):
* SSLv3, TLS handshake, Client key exchange (16):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSL connection using AES256-SHA
* Server certificate:
* 	subject: serialNumber=dzc7avuEuqhZCEL82HF5aqoCQMgtwixa; OU=GT41552380; OU=See www.rapidssl.com/resources/cps (c)14; OU=Domain Control Validated - RapidSSL(R); CN=*.wpengine.com
* 	start date: 2014-04-17 12:42:18 GMT
* 	expire date: 2018-05-19 17:27:48 GMT
* 	subjectAltName does not match www.admincolumns.com
* Closing connection #0
* SSLv3, TLS alert, Client hello (1):
* SSL peer certificate or SSH remote key was not OK
curl: (51) SSL peer certificate or SSH remote key was not OK

I noticed that the CentOS machines were using SSLv3, whereas my Mac was using TLS. I was then able to recreate the issue on my Mac by forcing curl to use SSLv3.

> curl -v -sslv3 https://www.admincolumns.com
* Adding handle: conn: 0x7fae0b004000
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x7fae0b004000) send_pipe: 1, recv_pipe: 0
* About to connect() to www.admincolumns.com port 443 (#0)
*   Trying 178.79.179.38...
* Connected to www.admincolumns.com (178.79.179.38) port 443 (#0)
* SSL certificate problem: Invalid certificate chain
* Closing connection 0
curl: (60) SSL certificate problem: Invalid certificate chain
More details here: http://curl.haxx.se/docs/sslcerts.html

Disable SSL Certificate Verification in WP_Http

With the SSL certificate for the request being invalid and the server not being in my control, the only option is to disable the SSL certificate verification in WP_Http. This is accomplished by setting a key in its configuration array called sslverify to false. We can do this by hooking into the http_request_args filter, checking the URL that it is loading, and disabling the verification for Admin Columns Pro.

add_filter( 'http_request_args', 'fix_acp_plugin_update', 10, 2 );
function fix_acp_plugin_update( $r, $url ){
	$starts_with = 'https://www.admincolumns.com?wc-api=software-licence-api&request=plugindownload';
	// if the url starts with ^ then don't verify SSL
	if ( 0 === strpos( $url, $starts_with ) ){
		$r['sslverify'] = false;
	}
	return $r;
}

Successfully Updated!

Et voila! The plugin is now able to update successfully.

Enabling Maintenance mode…

Updating Plugin Admin Columns Pro (1/1)
Admin Columns Pro updated successfully. Show Details.
Disabling Maintenance mode…

All updates have been completed.

The post Admin Columns Pro SSL Upgrade Fix appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/wordpress/admin-columns-pro-ssl-upgrade-fix/feed/ 2
Fix Backslashes in Advanced Custom Fields https://www.justinsilver.com/technology/wordpress/fix-backslashes-advanced-custom-fields/?utm_source=rss&utm_medium=rss&utm_campaign=fix-backslashes-advanced-custom-fields https://www.justinsilver.com/technology/wordpress/fix-backslashes-advanced-custom-fields/#respond Sat, 01 Nov 2014 21:53:44 +0000 http://justinsilver.com/?p=3798 There is a somewhat serious bug in Advanced Custom Fields 5.0 and its handling of backslashes, at least on my WordPress installs. The problem is that the slashes are stripped, but after the data...

The post Fix Backslashes in Advanced Custom Fields appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

There is a somewhat serious bug in Advanced Custom Fields 5.0 and its handling of backslashes, at least on my WordPress installs. The problem is that the slashes are stripped, but after the data has been serialized resulting in a mismatch between the declared string length in the serialized data, and it’s actual value length.

Imagine for example a Validated Field that uses a regular expression to check that the value is a digit. The validation pattern would look something like \d for our example. When this is serialized, it’s length is 2 and the data looks like s:2:"\d". The problem is that when this is represented as a string to insert into the database, the backslash just escapes the d and what gets saved to the database is s:2:"d".

Now we have a much bigger problem than a missing backslash – because the declared length is 2 but the length of the string "d" is 1, the serialized data can no longer be loaded from the database and all the field configuration values are lost.

This occurs during the upgrade process from ACF 4 to 5, as well as when saving a field with a backslash as part of its configuration. The field will default back to the “Text” field type in the UI, and the settings will be unloadable in the database. It is possible to manually fix this data by adding the missing \ characters back in, but this is going to be tedious work depending on how many fields are affected and the complexity. Even when fixed manually the field will break again the next time it is saved.

A bug has been listed on the ACF support site.

Fix for Backslashes in Field Configuration

The following code attempts to correct this behavior, the following code attempts to use the filters content_save_pre and acf/get_valid_field to double slash any single slashes found in the configuration so that they are saved correctly to the database.

class ACF5_Slash_Fix {
	function __construct(){

		// bug fix for acf with backslashes in the content.
		add_filter( 'content_save_pre', array( $this, 'fix_post_content' ) );
		add_filter( 'acf/get_valid_field', array( $this, 'fix_upgrade' ) );
	}

	function fix_upgrade( $field ){

		// the $_POST will tell us if this is an upgrade
		$is_5_upgrade = 
			isset( $_POST['action'] ) && $_POST['action'] == 'acf/admin/data_upgrade' && 
			isset( $_POST['version'] ) && $_POST['version'] == '5.0.0';

		// if it is an upgrade recursively fix the field values
		if ( $is_5_upgrade ){
			$field = $this->do_recursive_slash_fix( $field );
		}

		return $field;
	}

	function fix_post_content( $content ){
		global $post;

		// are we saving a field group?
		$is_field_group = get_post_type() == 'acf-field-group';

		// are we upgrading to ACF 5?
		$is_5_upgrade = 
			isset( $_POST['action'] ) && $_POST['action'] == 'acf/admin/data_upgrade' && 
			isset( $_POST['version'] ) && $_POST['version'] == '5.0.0';

		// if we are, we need to check the values for single, but not double, backslashes and make them double
		if ( $is_field_group || $is_5_upgrade ){
			$content = $this->do_slash_fix( $content );
		}
		
		return $content;
	}

	function do_slash_fix( $string ){
		return preg_match( '~(?<!\\\\)\\\\(?!\\\\)~', $string )?
			str_replace('\\', '\\\\', $string ) :
			$string;
	}

	function do_recursive_slash_fix( $array ){

		// loop through all levels of the array
		foreach( $array as $key => &$value ){
			if ( is_array( $value ) ){
				// div deeper
				$value = $this->do_recursive_slash_fix( $value );
			} elseif ( is_string( $value ) ){
				// fix single backslashes to double
				$value = $this->do_slash_fix( $value );
			}
		}

		return $array;
	}
}
new ACF5_Slash_Fix();

Included in Validated Field

This fix is also included in the Validated Field plugin available in the WordPress repository. It offers additional enhancements as well to support field validation, read-only values, unique key, and more.

The post Fix Backslashes in Advanced Custom Fields appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/wordpress/fix-backslashes-advanced-custom-fields/feed/ 0
W3 Total Cache Fragment Caching in WordPress https://www.justinsilver.com/technology/wordpress/w3-total-cache-fragment-caching-wordpress/?utm_source=rss&utm_medium=rss&utm_campaign=w3-total-cache-fragment-caching-wordpress https://www.justinsilver.com/technology/wordpress/w3-total-cache-fragment-caching-wordpress/#comments Fri, 31 Oct 2014 19:20:14 +0000 http://justinsilver.com/?p=3796 W3 Total Cache, also known as W3TC, is a very powerful caching plugin for WordPress which is notoriously slow without tuning. For guest users much of your content will typically be static which makes...

The post W3 Total Cache Fragment Caching in WordPress appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

W3 Total Cache, also known as W3TC, is a very powerful caching plugin for WordPress which is notoriously slow without tuning. For guest users much of your content will typically be static which makes serving up cached content to these users a pretty good idea. But what if you want part of the page to be be dynamic while most of the page is cached? Fragment caching to the rescue.

Configure W3 Total Cache

First you will need to make sure W3TC is properly configured for fragment caching. To enable it, from your WordPress Admin, visit Performance > General Settings and look under the Page Cache section.  Make sure that the Page cache method is set to Disk: Basic and save your settings.

Next visit Performance > Page Cache and scroll down to the Advanced section. Here you will need to check the box next to Late initialization to enable it. Notice the help text under it:

Enables support for WordPress functionality in fragment caching for the page caching engine. Use of this feature may increase response times.

Set W3TC_DYNAMIC_SECURITY Constant

For security reasons W3TC requires that you create a constant named W3TC_DYNAMIC_SECURITY and pass it to the sections of code you would like it to ignore (and execute!). An obvious choice for this is wp-config.php, but you might also have success with /wp-content/mu-plugins, a custom plugin, your theme’s functions.php, or even inline before your fragment – though the latter isn’t really recommended. You can set this to a hardcoded value, or use something random for each request.

define( 'W3TC_DYNAMIC_SECURITY', 'SOME_SECURE_STRING_YOU_CREATE' );

Update your template

The final step is to update your template files to indicate where you want W3TC to *not* cache the page. This is done by using an HTML comment called `mfunc`. You pass it your W3TC_DYNAMIC_SECURITY and include any PHP you want to execute (without <?php> tags!).

<!--mfunc <?php echo W3TC_DYNAMIC_SECURITY; ?> -->
echo 'The time is '.date( 'H:i:s', time() );
<!--/mfunc <?php echo W3TC_DYNAMIC_SECURITY; ?> -->

The post W3 Total Cache Fragment Caching in WordPress appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/wordpress/w3-total-cache-fragment-caching-wordpress/feed/ 44
ACF validate_value Filter With post_ID https://www.justinsilver.com/technology/wordpress/advanced-custom-fields-validate_value-filter-with-post_id/?utm_source=rss&utm_medium=rss&utm_campaign=advanced-custom-fields-validate_value-filter-with-post_id https://www.justinsilver.com/technology/wordpress/advanced-custom-fields-validate_value-filter-with-post_id/#comments Sun, 19 Oct 2014 21:22:18 +0000 http://justinsilver.com/?p=3769 Advanced Custom Forms Pro 5.0 is out, and it contains a major overhaul to the way that custom fields are handled. It also had a major impact on the way that third-party add-ons were...

The post ACF validate_value Filter With post_ID appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

Advanced Custom Forms Pro 5.0 is out, and it contains a major overhaul to the way that custom fields are handled. It also had a major impact on the way that third-party add-ons were written to extend ACF functionality which meant that it was considerable work to refactor my Validated Fields plugin to support the new architecture. The new architecture is definitely superior in my opinion and has some great new filters that we can leverage such as acf/validate_value and its siblings acf/validate_value/type={$field_type}acf/validate_value/name={$field_name}acf/validate_value/key={$field_key}.

Unfortunately this filter does not have the post_ID available to it, which greatly limits the range of things we can do with it. To work around this I found that by inserting a field named acf[post_ID] into the editor form – and the “acf” part is critical – it would be picked up and submitted with the rest of the form values. This meant that it would be available in the $_POST, really opening up the possibilities.

Sample Code

// use a unique value to prevent conflicts with other ACF fields
define( 'MY_ACF_FORM_VALUES', 'MY_ACF_FORM_VALUES' );

// add the post_ID to the acf[] form
function my_edit_form_after_editor( $post ){
	print( "<input type='hidden' name='acf[%1$s][post_ID]' value='%2$d'/>", 
		MY_ACF_FORM_VALUES, 
		$post->ID 
	);
}
add_action( 'edit_form_after_editor', 'my_edit_form_after_editor' );

// use the post_ID in your validation function
function my_validate_value( $valid, $value, $field, $input ) {
	$post_id = $_POST['acf'][MY_ACF_FORM_VALUES]['post_ID'];
	// more code!
	return $valid;
}
add_filter( "acf/validate_value", 'my_validate_value', 10, 4 );

The rest of the my_validate_value() will be up to you.

Shameless Plug.

Why reinvent the wheel? The above code is already included in Validated Field for ACF available in the WordPress repository.

The post ACF validate_value Filter With post_ID appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/wordpress/advanced-custom-fields-validate_value-filter-with-post_id/feed/ 8
Sort by Featured Image in WordPress https://www.justinsilver.com/technology/wordpress/sort-featured-image-wordpress/?utm_source=rss&utm_medium=rss&utm_campaign=sort-featured-image-wordpress https://www.justinsilver.com/technology/wordpress/sort-featured-image-wordpress/#comments Fri, 15 Aug 2014 02:10:52 +0000 http://justin.ag/?p=3703 Featured Images on a WordPress page or post can make it easy to use a single image to represent the content of your page. Given that they look better, why not feature them at...

The post Sort by Featured Image in WordPress appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

Featured Images on a WordPress page or post can make it easy to use a single image to represent the content of your page. Given that they look better, why not feature them at the top of your content, while still preserving the rest of your ORDER BY? The answer is that it’s not terribly straightforward but you do have a couple of options. We can make use of the “_thumbnail_id” meta_key that is stores the attachment ID of the featured image, but as it is missing for pages and posts that don’t have a featured image they are omitted from The Loop entirely, which is not what we want.

Filters on WP_Query

The most robust way to sort your posts by featured image while not interfering with your existing ORDER BY is to plug into the filters for posts_fields_requestposts_join_request, and posts_orderby_request. The SQL that is going to be run passes through these filters in pieces, allowing us to modify it – and make sure that pages and posts without featured images are last! Since we don’t want to modify every WP_Query, presumably just some of them, we can add another argument to handle this for us – we’ll call it featured_image_sort.

class Featured_Image_Sort {
	public static function init(){
		add_filter( 'pre_get_posts', array( __CLASS__, 'pre_get_posts' ) );
	}

	public static function pre_get_posts( $query ){
		if ( $query->query_vars['featured_image_sort'] ){
			add_filter( 'posts_fields_request', array( __CLASS__, 'posts_fields_request' ) );
			add_filter( 'posts_join_request', array( __CLASS__, 'posts_join_request' ) );
			add_filter( 'posts_orderby_request', array( __CLASS__, 'posts_orderby_request' ) );
		}
		return $query;
	}

	public static function posts_fields_request( $select ){
		global $wpdb;
		return "{$select}, IF ( _has_featured_image.meta_value IS NULL, 0, 1 ) AS has_featured_image ";
	}
	public static function posts_join_request( $join ){
		global $wpdb;
		return "{$join}\tLEFT JOIN {$wpdb->postmeta} AS _has_featured_image ON _has_featured_image.post_id = {$wpdb->posts}.ID and _has_featured_image.meta_key = '_thumbnail_id'";
	}
	public static function posts_orderby_request( $orderby ){
		return "has_featured_image DESC, {$orderby}";
	}
}
Featured_Image_Sort::init();

Using the new WP_Query argument

I like to keep things flexible, so if you want to add this to every query I would use a hook to pre_get_posts higher than a priority 10 to set the featured_image_sort flag. This is also an easy way to quickly test your code.

add_filter( 'pre_get_posts', 'sort_featured', 1 );
function sort_featured( $query ){
	$query->query_vars['featured_image_sort'] = true;
	return $query;
}

If you want to apply the sort to specific queries you would pass this flag as an argument to WP_Query as you would any other argument.

$args = array( 
	'orderby' => 'meta_value', 
	'meta_key' => 'some_meta_key', 
	'featured_image_sort' => true 
);
$query = new WP_Query( $args );

The post Sort by Featured Image in WordPress appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/wordpress/sort-featured-image-wordpress/feed/ 3
Using WP Better Emails with WooCommerce Email Templates https://www.justinsilver.com/technology/wordpress/using-wp-better-emails-woocommerce-email-templates/?utm_source=rss&utm_medium=rss&utm_campaign=using-wp-better-emails-woocommerce-email-templates https://www.justinsilver.com/technology/wordpress/using-wp-better-emails-woocommerce-email-templates/#comments Thu, 24 Jul 2014 22:35:00 +0000 http://justin.ag/?p=3673 On one site that I run there are several different functions, primarily provided by disparate plugins with custom actions/filters, that send emails both to site administrators and customers. For most emails that are sent...

The post Using WP Better Emails with WooCommerce Email Templates appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

On one site that I run there are several different functions, primarily provided by disparate plugins with custom actions/filters, that send emails both to site administrators and customers. For most emails that are sent from WordPress itself and several of the plugins, the content type is set to plain/text allowing me to use WP Better Emails to wrap content in an easily edited header and footer. WooCommerce on the other hand proved to be a little more tricky – there is the option to send emails via plain/text, but they definitely lack valuable formatting when displaying order information to a customer. This is further compounded if you insert any custom HTML into the content.

Conceptually all that needs to be done is to first remove the WooCommerce email header and footer, and second have WP Better Emails process it even though it has a content type of text/html. After checking out the code for both of the plugins in question, I devised a way to make it work.

Remove WooCommerce Email Headers and Footers

If you view the source of any of the WooCommerce email templates (at least the default ones) you will see a line for <?php do_action( 'woocommerce_email_header', $email_heading ); ?> and <?php do_action( 'woocommerce_email_footer' ); ?> which are the hooks that WooCommerce itself uses to insert the header and footer contents. By setting our own functions to these hooks earlier and later than the WooCommerce priorities, we can capture all of the content and hide it with ob_start() and ob_get_clean() to capture and clean the output buffer contents. We are also setting a filter to return true any time the woocommerce_email_header action is run.

Common sense says that we could just unhook all functions from these actions, however in my testing I found that some other, non-UI, logic was being performed in some of the attached functions. This method allows the code to run, but prevents the output from being inserted into the generated email.

Apply WP Better Emails Template

Next we need to apply the email template that WP Better Emails did not, because the content type was wrong – text/html and not text/plain. This is actually a good thing because we also want to avoid some of the formatting that WP Better Emails applies to the typical plain text email content. We can do this using the global $wp_better_emails and hooking into phpmailer_init after most of the work has been done – priority 20 works fine.

This example uses anonymous function which require PHP 5.3+, however you could also use create_function().

// Determine if it's an email using the WooCommerce email header
add_action( 'woocommerce_email_header', function(){ add_filter( "better_wc_email", "__return_true" ); } );

// Hide the WooCommerce Email header and footer
add_action( 'woocommerce_email_header', function(){ ob_start(); }, 1 );
add_action( 'woocommerce_email_header', function(){ ob_get_clean(); }, 100 );
add_action( 'woocommerce_email_footer', function(){ ob_start(); }, 1 );
add_action( 'woocommerce_email_footer', function(){ ob_get_clean(); }, 100 );

// Selectively apply WPBE template if it's a WooCommerce email
add_action( 'phpmailer_init', 'better_phpmailer_init', 20 );
function better_phpmailer_init( $phpmailer ){
	// this filter will return true if the woocommerce_email_header action has run
	if ( apply_filters( 'better_wc_email', false ) ){
		global $wp_better_emails;

		// Add template to message
		$phpmailer->Body = $wp_better_emails->set_email_template( $phpmailer->Body );

		// Replace variables in email
		$phpmailer->Body = apply_filters( 'wpbe_html_body', $wp_better_emails->template_vars_replacement( $phpmailer->Body ) );
	}
}

Success!

Your WP Better Emails templates should now be applied to your WooCommerce emails.

The post Using WP Better Emails with WooCommerce Email Templates appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/wordpress/using-wp-better-emails-woocommerce-email-templates/feed/ 5
Prepare IN and NOT IN Statements in WordPress https://www.justinsilver.com/technology/wordpress/prepare-statements-wordpress/?utm_source=rss&utm_medium=rss&utm_campaign=prepare-statements-wordpress https://www.justinsilver.com/technology/wordpress/prepare-statements-wordpress/#comments Mon, 30 Jun 2014 19:11:00 +0000 http://justin.ag/?p=3637 Why should I use prepared statements? Using some type of prepared statement protects you against SQL injection attacks when you need to interact with the database using parameters passed in from the client side. Depending...

The post Prepare IN and NOT IN Statements in WordPress appeared first on Justin Silver.

]]>
AmpedSense.OptimizeAdSpot('AP'); AmpedSense.OptimizeAdSpot('IL'); AmpedSense.OptimizeAdSpot('IR');

Why should I use prepared statements?

Using some type of prepared statement protects you against SQL injection attacks when you need to interact with the database using parameters passed in from the client side. Depending on the library you are using, how you do this will vary slightly, however when working directly with the database in WordPress it’s best to use the $wpdb global object to abstract the actual interface. The deprecated mysql_* extensions are still officially being used, however as of release of WordPress 3.9 when used in conjunction with PHP 5.5, you bumped over to the newer and improved mysqli. Either way, you can use $wpdb->prepare( $statement, $arg1, $arg2... ) and the WordPress API will handle the details of the implementation.

DO NOT DO THIS AT HOME (OR ANYWHERE)

This is an example of code that is susceptible to a SQL injection.

global $wpdb;
// is this really a numeric ID?
$id = $_POST['id'];
$sql = "SELECT post_name FROM {$wpdb->posts} WHERE ID = $id;";
$name = $wpdb->get_var( $sql );

Imagine what would happen if the value of $_POST['id'] was not a number like we expect, and is instead the string 0; DELETE FROM wp_posts;. The resulting $sql value that is executed would look like the following.

SELECT post_name
FROM wp_posts
WHERE ID = 0;
DELETE FROM wp_posts;

The right way, using prepare()

The correct way to write the above code so that it is not susceptible to SQL injection attacks would be to use $wpdb->prepare(), and it would look like this:

global $wpdb;
// is this really a numeric ID?
$id = $_POST['id'];
// Use %d for digits, or %s for strings when calling prepare()
$sql = $wpdb->prepare( "SELECT post_name FROM {$wpdb->posts} WHERE ID = %d", $id );
$name = $wpdb->get_var( $sql );

Using Prepared Statements with IN and NOT IN

Things get a bit more complicated when you need to pass an array of values into an IN or NOT IN clause however. The best way to deal with this situation is to use call_user_func_array() to pass an array as a list of arguments to $wpdb->prepare(). Since there may be other values we want to escape as well, I usually define a new function to handle this special case and end up calling prepare() twice. This particular function assumes you want to pass in a list of post ID’s, potentially having the list twice (an OR statement for example) – so you may need to adjust for your needs.

function my_function(){
	global $wpdb;
	$id = $_POST['id'];
	$id_array = $_POST['id_array'];

	$sql = "SELECT post_name FROM {$wpdb->posts} WHERE ID = %d or ID IN ([IN])";
	$sql = $wpdb->prepare( $sql, $id );
	$sql = prepare_in( $sql, $id_array );

	// SELECT post_name FROM wp_posts WHERE ID = 9 or ID IN ( 10, 11, 12 )
}

function prepare_in( $sql, $vals ){
	global $wpdb;
	$not_in_count = substr_count( $sql, '[IN]' );
	if ( $not_in_count > 0 ){
		$args = array( str_replace( '[IN]', implode( ', ', array_fill( 0, count( $vals ), '%d' ) ), str_replace( '%', '%%', $sql ) ) );
		// This will populate ALL the [IN]'s with the $vals, assuming you have more than one [IN] in the sql
		for ( $i=0; $i < substr_count( $sql, '[IN]' ); $i++ ) {
			$args = array_merge( $args, $vals );
		}
		$sql = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( $args ) );
	}
	return $sql;
}

The post Prepare IN and NOT IN Statements in WordPress appeared first on Justin Silver.

]]>
https://www.justinsilver.com/technology/wordpress/prepare-statements-wordpress/feed/ 2