Category Archives: Wordpress

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);

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

Setting the correct forwarded IP address in WordPress Stream plugin

If you are using the WordPress Stream plugin and have a reverse proxy like Varnish in front of it, you might be seeing the IP of your Varnish machines rather than the client IP. This snippet lets you set the proper IP address that Varnish or other reverse proxies forward.

This code only works for Apache. If you are running Nginx you will need the getallheaders polyfill.

//Add proper forwarded IP to log records
add_filter('wp_stream_record_array', function($record) {
   //Get proper IP
   if(function_exists('getallheaders')) {
      $headers = getallheaders();

      //Exchange IP if header set
      if(isset($headers['X-Forwarded-For'])) {
        $record['ip'] = filter_var($headers['X-Forwarded-For'], FILTER_VALIDATE_IP);
      }
    }

    return $record;
});

Upgrading from Deployer 3 to Deployer 4

If you are upgrading Deployer from version 3 to 4 you will see issues when trying to deploy, for example:

[ERROR] Error: Call to undefined function set() in /Users/stakhr/site/deploy.php:5    
       Stack trace:                                                                                
       #0 phar:///Users/stakhr/site/deployer.phar/bin/dep(114): require()            
       #1 phar:///Users/stakhr/site/deployer.phar/bin/dep(115): {closure}()          
       #2 /Users/stakhr/site/deployer.phar(4): require('phar:///Users/s...')         
       #3 {main}  

To fix these issues, you need to do a few things:

  • Add a Deployer namespace declaration at the top of your deploy.php file, like this: namespace Deployer;
  • The env() function has been deprecated. Replace it with set() when setting variables (two parameters) and get() when getting variables (one parameter)
  • The underlying SSH implementation will be changed in Deployer 5, so add set('ssh_type', 'native'); to fix the nag that shows up.

Here is a full example of deploying a WordPress site using Deployer 4

PS. I’ve also noted that running deployer self-update on version 3 does not upgrade to version 4.

Creating dynamic sliders with Soliloquy for free

I wanted to create a simple dynamic slider using Advanced Custom Fields, but Soliloquy does not have this option out of the box. After poking around the source of Soliloquy I found the filter soliloquy_custom_slider_data which lets us add our own slider data. Here is the required code to dynamically generate a slider. (This code is not ACF-specific, you can use any data source.)

Your slider will be available using the shortcode:

[soliloquy type="frontpage"]

The code to generate this dynamic slider:

add_filter('soliloquy_custom_slider_data', function($data, $atts, $post) {

    //Bail early if not our slider type
    if(isset($atts['type']) && $atts['type'] !== 'frontpage') {
        return $data;
    }

    $data_dynamic = [
        'id' => 0,
        'slider' => [
            //This is where you enter all your dynamic slides!
            //72 and 73 below are the IDs of the image attachments used in the slider. 
            72 => [
                'status' => 'active',
                'id' => 72,
                'attachment_id' => 72,
                'title' => 'Image title',
                'link' => 'http://example.com',
                'alt' => 'Alt text',
                'caption' => 'Caption',
                'type' => 'image',
                'linktab' => 0
            ],
            73 => [
                'status' => 'active',
                'id' => 73,
                'attachment_id' => 73,
                'title' => 'Image title',
                'link' => 'http://example.com',
                'alt' => 'Alt text',
                'caption' => 'Caption',
                'type' => 'image',
                'linktab' => 0
            ]
        ],
        'config' => [
            //This is the general slider config
            'type' => 'default',
            'slider_theme' => 'base',
            'slider_width' => 1080,
            'slider_height' => 400,
            'transition' => 'fade',
            'duration' => 5000,
            'speed' => 400,
            'gutter' => 20,
            'slider' => 1,
            'aria_live' => 'polite',
            'classes' => [
                'frontpage-slider'
            ],
            'title' => '',
            'slug' => '',
            'rtl' => 0
        ]
    ];

    return $data_dynamic;
}, 11, 3);

I am using Soliloquy Lite, although I’m sure this works with the paid options as well. You can also easily create multiple dynamic sliders, each pulling data from different sources.