Restrict viewing of uploaded attachments to logged-in users using WordPress and Nginx

Even if you lock down your WordPress site so outside visitors can’t see it, all uploaded attachment such as media, images and PDFs are available to anyone as long as they know the direct link to those files, or can find it by other means such as search engines. Sometimes that’s not acceptable and you want to make sure only logged in users can see uploaded media files.

This approach works by proxying all files in /wp-content/uploads through a script (dl-file.php) which should be placed in the root of your WordPress installation. The script checks whether the user is logged in before serving the file. If the user is not logged in a redirect is performed so that the user can log in before viewing the file.

Because this approach uses the same check as WordPress itself, it should be considered a very safe way of protecting attachments. You could also extend the proxy script to check for different access levels (for example if you have a membership site) and much more!

The Nginx config

# Hotlink protection
location ~ ^/wp-content/uploads/(.*) {
    try_files /dl-file.php =403;
    include fastcgi_params;
    fastcgi_pass php7;
}

This config should be placed in a server block and the example is for servers running EasyEngine, but it should work with minor modification on any php-fpm server.

The proxy script

<?php
/*
 * dl-file.php
 *
 * Protect uploaded files with login.
 *
 * Based on http://wordpress.stackexchange.com/questions/37144/protect-wordpress-uploads-if-user-is-not-logged-in
 * 
 * @author hakre <http://hakre.wordpress.com/> / khromov
 * @license GPL-3.0+
 * @registry SPDX
 */

require_once('wp-load.php');

//Check if we are logged in before attempting to serve the file
is_user_logged_in() || auth_redirect();

//FIXME ... simplify this
list($basedir) = array_values(array_intersect_key(wp_upload_dir(), array('basedir' => 1)))+array(NULL);

//Check if file exists
$getFile = isset($_GET[ 'file' ]) ? $_GET[ 'file' ] : $_SERVER['REQUEST_URI'];

//Normalize file path
$getFile = str_replace('..', '', $getFile);
$getFile = str_replace('wp-content/uploads/', '', $getFile);

//This provides a notice in the log
trigger_error('Loading protected file ' . $getFile);

$file =  trailingslashit($basedir) . $getFile;

//If file is missing, 404 it
if (!$basedir || !is_file($file)) {
    status_header(404);
    die('404 &#8212; File not found.');
}

$mime = wp_check_filetype($file);
if( false === $mime[ 'type' ] && function_exists( 'mime_content_type' ) )
    $mime[ 'type' ] = mime_content_type( $file );

if( $mime[ 'type' ] )
    $mimetype = $mime[ 'type' ];
else
    $mimetype = 'image/' . substr( $file, strrpos( $file, '.' ) + 1 );

header( 'Content-Type: ' . $mimetype ); // always send this

// If we made it this far, just serve the file
readfile( $file );

Similar StackOverflow question

Photo by Micah Williams on Unsplash

Fix broken / overlapping Instagram embed for WordPress

At some point recently (February 2018), Instagram broke their oEmbed implementation which causes JavaScript errors and embeds that overlap each other. Use the snippet below to mitigate this issue and hopefully Instagram will fix it in the future. The snippet makes sure only one

/**
 * Remove Instagram embed.js script on each embed
 */
add_filter('embed_oembed_html', function($html, $url, $attr, $post_id) {
  $regex =    '/<script.*instagram\.com\/embed.js.*\s?script>/U';
  $regex_2 =  '/<script.*platform\.instagram\.com\/.*\/embeds\.js.*script>/U';

  if(preg_match($regex, $html) || preg_match($regex_2, $html)) {
    add_filter('kh_has_instagram_embed', '__return_true');

    $html = preg_replace($regex, '', $html);
    $html = preg_replace($regex_2, '', $html);

    return $html;
  }

  return $html;
}, 100, 4);

/**
 * Enqueue the embed.js script once at the bottom of the page, if at least one Instagram embed is enqueued
 */
add_filter('wp_footer', function() {
  if(apply_filters('kh_has_instagram_embed', false)) :
    ?>
      <script async defer src="//www.instagram.com/embed.js"></script>
    <?php
  endif;
}, 999);

Shorthand caching pattern in PHP

Here’s a short and simple caching pattern you can use to cache an expensive function call:

if(!$result = Cache::get('expensive-function-cache-key')) {
  $result = expensive_function();
  Cache::set('expensive-function-cache-key', $result);
}

//The result variable will always contain the proper value, whether the call was cached or not.
echo $result;

Deploy any Git branch to your Heroku application

It’s pretty common on Heroku to have a staging app and a production app, with a Pipeline to promote changes from staging to production.

Typically if you use a pipeline, you write git push heroku master to push the latest master branch to your staging app and then promote it to production through the Heroku web UI.

But sometimes you may wish to temporarily test a different branch than master on your staging app. To do this, run:

git push heroku your-feature-branch:master

Where your-feature-branch is the name of the branch you want to deploy.

Remember that you shouldn’t promote this version to production. What you do is to merge the branch with master first after you’ve tested it and run git push heroku master to deploy the master branch to staging again before you finally promote your changes to production.

Source

List authors by post count in WordPress using MySQL

A query to list authors / users by post count:

SELECT wp_users.ID, wp_users.user_nicename, COUNT(*) as count FROM wp_posts, wp_users WHERE wp_posts.post_type='post' AND wp_posts.post_status='publish' AND wp_posts.post_author = wp_users.ID GROUP BY post_author ORDER BY count DESC LIMIT 5 ;

Resulting table:

ID,user_nicename,count
29,"user-a",18
66,"user-b",16
26,"user-c",10
24,"user-f",9
48,"user-z",6