├── README.md ├── composer.json └── stage-file-proxy.php /README.md: -------------------------------------------------------------------------------- 1 | stage-file-proxy 2 | ================ 3 | 4 | Mirror (or header to) uploaded files from a remote production site on your local development copy. Saves the trouble of downloading a giant uploads directory without sacrificing the images that accompany content. 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alleyinteractive/stage-file-proxy", 3 | "description": "Get only the files you need from your production environment into your WordPress development environment.", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-2.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "Alley", 9 | "email": "info@alley.com" 10 | } 11 | ], 12 | "config": { 13 | "lock": false 14 | }, 15 | "require": { 16 | "php": "^8.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stage-file-proxy.php: -------------------------------------------------------------------------------- 1 | $resize['width'], 84 | 'h' => $resize['height'], 85 | 'resize' => $resize['crop'] ? "{$resize['width']},{$resize['height']}" : null, 86 | ), 87 | sfp_get_base_url() . $resize['filename'] 88 | ) ); 89 | exit; 90 | } 91 | 92 | $uploads_dir = wp_upload_dir(); 93 | $basefile = $uploads_dir['basedir'] . '/' . $resize['filename']; 94 | sfp_resize_image( $basefile, $resize ); 95 | $relative_path = $resize['filename']; 96 | } else if ( 'photon' === $mode ) { 97 | header( "Location: " . sfp_get_base_url() . $relative_path ); 98 | exit; 99 | } 100 | 101 | // Download a full-size original from the remote server. 102 | // If it needs to be resized, it will be on the next load. 103 | $remote_url = sfp_get_base_url() . $relative_path; 104 | 105 | /** 106 | * Filter: sfp_http_request_args 107 | * 108 | * Alter the args of the GET request. 109 | * 110 | * @param array $remote_http_request_args The request arguments. 111 | */ 112 | $remote_http_request_args = apply_filters( 'sfp_http_remote_args', array( 'timeout' => 30 ) ); 113 | $remote_request = wp_remote_get( $remote_url, $remote_http_request_args ); 114 | 115 | if ( is_wp_error( $remote_request ) || $remote_request['response']['code'] > 400 ) { 116 | // If local mode, failover to local files 117 | if ( 'local' === $mode ) { 118 | // Cache replacement image by hashed request URI 119 | $transient_key = 'sfp_image_' . md5( $_SERVER['REQUEST_URI'] ); 120 | if ( false === ( $basefile = get_transient( $transient_key ) ) ) { 121 | $basefile = sfp_get_random_local_file_path( $doing_resize ); 122 | set_transient( $transient_key, $basefile ); 123 | } 124 | 125 | // Resize if necessary 126 | if ( $doing_resize ) { 127 | sfp_resize_image( $basefile, $resize ); 128 | } else { 129 | sfp_serve_requested_file( $basefile ); 130 | } 131 | } elseif ( 'lorempixel' === $mode ) { 132 | $width = $doing_resize && ! empty( $resize['width'] ) ? $resize['width'] : 800; 133 | $height = $doing_resize && ! empty( $resize['height'] ) ? $resize['height'] : 600; 134 | header( 'Location: http://lorempixel.com/' . $resize['width'] . '/' . $resize['height'] ); 135 | exit; 136 | } else { 137 | sfp_error(); 138 | } 139 | } 140 | 141 | // we could be making some dangerous assumptions here, but if WP is setup normally, this will work: 142 | $path_parts = explode( '/', $remote_url ); 143 | $name = array_pop( $path_parts ); 144 | 145 | if ( strpos( $name, '?' ) ) { 146 | list( $name, $crap ) = explode( '?', $name, 2 ); 147 | } 148 | 149 | $month = array_pop( $path_parts ); 150 | $year = array_pop( $path_parts ); 151 | 152 | $upload = wp_upload_bits( $name, null, $remote_request['body'], "$year/$month" ); 153 | 154 | if ( ! $upload['error'] ) { 155 | // if there was some other sort of error, and the file now does not exist, we could loop on accident. 156 | // should think about some other strategies. 157 | if ( $doing_resize ) { 158 | sfp_dispatch(); 159 | } else { 160 | sfp_serve_requested_file( $upload['file'] ); 161 | } 162 | } else { 163 | sfp_error(); 164 | } 165 | } 166 | 167 | /** 168 | * Resizes $basefile based on parameters in $resize 169 | */ 170 | function sfp_resize_image( $basefile, $resize ) { 171 | if ( file_exists( $basefile ) ) { 172 | $suffix = $resize['width'] . 'x' . $resize['height']; 173 | if ( $resize['crop'] ) { 174 | $suffix .= 'c'; 175 | } 176 | if ( 'r' == $resize['mode'] ) { 177 | $suffix = 'r-' . $suffix; 178 | } 179 | $img = wp_get_image_editor( $basefile ); 180 | 181 | // wp_get_image_editor can return a WP_Error if the file exists but is corrupted. 182 | if ( is_wp_error( $img ) ) { 183 | sfp_error(); 184 | } 185 | 186 | $img->resize( $resize['width'], $resize['height'], $resize['crop'] ); 187 | $info = pathinfo( $basefile ); 188 | $path_to_new_file = $info['dirname'] . '/' . $info['filename'] . '-' . $suffix . '.' .$info['extension']; 189 | $img->save( $path_to_new_file ); 190 | sfp_serve_requested_file( $path_to_new_file ); 191 | } 192 | } 193 | 194 | /** 195 | * Serve the file directly. 196 | */ 197 | function sfp_serve_requested_file( $filename ) { 198 | // find the mime type 199 | $finfo = finfo_open( FILEINFO_MIME_TYPE ); 200 | $type = finfo_file( $finfo, $filename ); 201 | // serve the image this one time (next time the webserver will do it for us) 202 | ob_end_clean(); 203 | header( 'Content-Type: '. $type ); 204 | header( 'Content-Length: ' . filesize( $filename ) ); 205 | readfile( $filename ); 206 | exit; 207 | } 208 | 209 | /** 210 | * prevent WP from generating resized images on upload 211 | */ 212 | function sfp_image_sizes_advanced( $sizes ) { 213 | global $dynimg_image_sizes; 214 | 215 | // save the sizes to a global, because the next function needs them to lie to WP about what sizes were generated 216 | $dynimg_image_sizes = $sizes; 217 | 218 | // force WP to not make sizes by telling it there's no sizes to make 219 | return array(); 220 | } 221 | add_filter( 'intermediate_image_sizes_advanced', 'sfp_image_sizes_advanced' ); 222 | 223 | /** 224 | * Trick WP into thinking the images were generated anyways. 225 | */ 226 | function sfp_generate_metadata( $meta ) { 227 | global $dynimg_image_sizes; 228 | 229 | if ( ! is_array( $dynimg_image_sizes ) ) { 230 | return $meta; 231 | } 232 | 233 | foreach ($dynimg_image_sizes as $sizename => $size) { 234 | // figure out what size WP would make this: 235 | $newsize = image_resize_dimensions( $meta['width'], $meta['height'], $size['width'], $size['height'], $size['crop'] ); 236 | 237 | if ($newsize) { 238 | $info = pathinfo( $meta['file'] ); 239 | $ext = $info['extension']; 240 | $name = wp_basename( $meta['file'], ".$ext" ); 241 | 242 | $suffix = "r-{$newsize[4]}x{$newsize[5]}"; 243 | if ( $size['crop'] ) $suffix .='c'; 244 | 245 | // build the fake meta entry for the size in question 246 | $resized = array( 247 | 'file' => "{$name}-{$suffix}.{$ext}", 248 | 'width' => $newsize[4], 249 | 'height' => $newsize[5], 250 | ); 251 | 252 | $meta['sizes'][$sizename] = $resized; 253 | } 254 | } 255 | 256 | return $meta; 257 | } 258 | add_filter( 'wp_generate_attachment_metadata', 'sfp_generate_metadata' ); 259 | 260 | /** 261 | * Get the relative file path by stripping out the /wp-content/uploads/ business. 262 | */ 263 | function sfp_get_relative_path() { 264 | static $path; 265 | if ( !$path ) { 266 | $path = preg_replace( '/.*\/wp\-content\/uploads(\/sites\/\d+)?\//i', '', $_SERVER['REQUEST_URI'] ); 267 | } 268 | /** 269 | * Filter: sfp_relative_path 270 | * 271 | * Alter the relative path of an image in SFP. 272 | * 273 | * @param string $path The relative path of the file. 274 | */ 275 | $path = apply_filters( 'sfp_relative_path', $path ); 276 | return $path; 277 | } 278 | 279 | /** 280 | * Grab a random file from a local directory and return the path 281 | */ 282 | function sfp_get_random_local_file_path( $doing_resize ) { 283 | static $local_dir; 284 | $transient_key = 'sfp-replacement-images'; 285 | if ( ! $local_dir ) { 286 | $local_dir = get_option( 'sfp_local_dir' ); 287 | if ( ! $local_dir ) { 288 | $local_dir = 'sfp-images'; 289 | } 290 | } 291 | 292 | $replacement_image_path = get_template_directory() . '/' . $local_dir . '/'; 293 | 294 | // Cache image directory contents 295 | if ( false === ( $images = get_transient( $transient_key ) ) ) { 296 | foreach ( glob( $replacement_image_path . '*' ) as $filename ) { 297 | // Exclude resized images 298 | if ( ! preg_match( '/.+[0-9]+x[0-9]+c?\.(jpe?g|png|gif)$/iU', $filename ) ) { 299 | $images[] = basename( $filename ); 300 | } 301 | } 302 | set_transient( $transient_key, $images ); 303 | } 304 | 305 | $rand = rand( 0, count( $images ) - 1 ); 306 | return $replacement_image_path . $images[ $rand ]; 307 | } 308 | 309 | /** 310 | * SFP can operate in two modes, 'download' and 'header' 311 | */ 312 | function sfp_get_mode() { 313 | static $mode; 314 | if ( ! $mode ) { 315 | $mode = get_option( 'sfp_mode' ); 316 | if ( ! $mode ) { 317 | $mode = 'header'; 318 | } 319 | } 320 | return $mode; 321 | } 322 | 323 | /** 324 | * Get the base URL of the uploads directory (i.e. the first possible directory on the remote side that could store a file) 325 | */ 326 | function sfp_get_base_url() { 327 | static $url; 328 | $mode = sfp_get_mode(); 329 | if ( ! $url ) { 330 | $url = get_option( 'sfp_url' ); 331 | if ( ! $url && 'local' !== $mode ) { 332 | sfp_error(); 333 | } 334 | } 335 | return $url; 336 | } 337 | 338 | function sfp_error() { 339 | die( 'SFP tried to load, but encountered an error' ); 340 | } 341 | --------------------------------------------------------------------------------