The post LEMP: CentOS 7, NGINX, PHP7, and Redis for WordPress appeared first on Justin Silver.
]]>Scripts to setup a WordPress server on CentOS 7 with NGINX, PHP/PHP-FPM 7, Redis and more.
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
Use Letsencrypt for free SSL certificates.
yum -y install letsencrypt openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
#!/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 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
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
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
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
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
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
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.
]]>The post Unban GoDaddy / MediaTemple WordPress Plugins appeared first on Justin Silver.
]]>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.
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 ) ); } );
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.
]]>The post ACF: Link to Specific Tab appeared first on Justin Silver.
]]>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.
]]>The post Admin Columns Pro SSL Upgrade Fix appeared first on Justin Silver.
]]>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…
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
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; }
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.
]]>The post Fix Backslashes in Advanced Custom Fields appeared first on Justin Silver.
]]>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.
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();
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.
]]>The post W3 Total Cache Fragment Caching in WordPress appeared first on Justin Silver.
]]>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.
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.
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' );
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.
]]>The post ACF validate_value Filter With post_ID appeared first on Justin Silver.
]]>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.
// 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.
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.
]]>The post Sort by Featured Image in WordPress appeared first on Justin Silver.
]]>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.
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_request
, posts_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();
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.
]]>The post Using WP Better Emails with WooCommerce Email Templates appeared first on Justin Silver.
]]>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.
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.
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 ) ); } }
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.
]]>The post Prepare IN and NOT IN Statements in WordPress appeared first on Justin Silver.
]]>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.
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 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 );
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.
]]>