2
\$\begingroup\$

This PHP class is to allow user to enter a URL of a remote image file and have it download the image to local server. It will be used in a Markdown editor just like Stack Overflow / Stack Exchange uses for their Markdown editor image dialog.

To make it more reliable across different server configurations, since my code will be used in a module that will be on hundreds of servers, I have 4 methods of upload:

  • cURL - requires curl to be installed
  • file_get_contents() - requires PHP INI allow_url_fopen = true
  • GD library - requires GD installed and PHP INI allow_url_fopen = true
  • Socket streams

As always, I am welcoming your feedback as my end goal is to make it as reliable on as many different servers as possible and fail proof.

Remote URL image uploader class

<?php

/**
 * Upload a image file to local server using just a URL of the image on the remote server.
 * Upload methods include:
 * - cURL - Requires curl be installed
 * - file_get_contents() - Requires PHP.INI allow_url_fopen = true
 * - GD Library - fopen to get data and create image with GD library. Requires PHP.INI allow_url_fopen = true
 * - Socket Streams
 */
class UploadImage
{

    public $imageQuality;
    public $upload_method;
    public $remote_image_url;
    public $local_image_url;
    public $local_image_folder;
    public $allow_url_fopen = false;

    // not used, still considering using it to loop over and try methods when 1 fails
    public $upload_methods = array(
        'curl' => 'Upload using the cURL Extension if available.',
        'fopen' => 'Upload using file_get_contents() if PHP INI Setting allow_url_fopen is set to TRUE.',
        'gd' => 'Upload by re-creating image using the GD Image Processing Program',
        'sockets' => 'Upload image',
    );


    public function __construct($local_image_folder) // default method: cURL
    {
        $this->local_image_folder = $local_image_folder;
        $this->canUseRemoteUrlFopen();
    }

    public function canUseRemoteUrlFopen(){
        if(ini_get('allow_url_fopen')){
            $this->allow_url_fopen = true;
        }
    }

    /**
     * Upload a Remote image by supplying a URL of remote image.  Upload method can be set
     * manually oherwise it will try to auto-detect which methods are available on a per server basis!
     * @param  string $remote_image_url URL to the remote server image
     * @param  string $upload_method  Optional.  PHP method used to upload image.
     * [curl, gd, fopen, sockets]
     * $upload_method = 'curl, gd, fopen, sockets'
     * @return
     */
    function uploadRemoteImage($remote_image_url, $upload_method = '') // default method: cURL
    {
        $this->remote_image_url = $remote_image_url;
        $info = @GetImageSize($this->remote_image_url);
        $mime = $info['mime'];

        // What sort of image?
        $type = substr(strrchr($mime, '/'), 1);

        switch ($type) {
            case 'jpeg':
                $image_create_func = 'ImageCreateFromJPEG';
                $image_save_func = 'ImageJPEG';
                $new_image_ext = 'jpg';

                // Best Quality: 100
                $imageQuality = isset($this->imageQuality) ? $this->imageQuality : 100;
                break;

            case 'png':
                $image_create_func = 'ImageCreateFromPNG';
                $image_save_func = 'ImagePNG';
                $new_image_ext = 'png';

                // Compression Level: from 0  (no compression) to 9
                $imageQuality = isset($this->imageQuality) ? $this->imageQuality : 0;
                break;

            case 'bmp':
                $image_create_func = 'ImageCreateFromBMP';
                $image_save_func = 'ImageBMP';
                $new_image_ext = 'bmp';
                break;

            case 'gif':
                $image_create_func = 'ImageCreateFromGIF';
                $image_save_func = 'ImageGIF';
                $new_image_ext = 'gif';
                break;

            case 'vnd.wap.wbmp':
                $image_create_func = 'ImageCreateFromWBMP';
                $image_save_func = 'ImageWBMP';
                $new_image_ext = 'bmp';
                break;

            case 'xbm':
                $image_create_func = 'ImageCreateFromXBM';
                $image_save_func = 'ImageXBM';
                $new_image_ext = 'xbm';
                break;

            default:

                die('Not a valid image type');

                $image_create_func = 'ImageCreateFromJPEG';
                $image_save_func = 'ImageJPEG';
                $new_image_ext = 'jpg';
        }

        $timestamp = time();

        $ext = strrchr($this->remote_image_url, ".");
        $strlen = strlen($ext);

        $new_name = basename(substr($this->remote_image_url, 0, -$strlen)) . '-' . $timestamp .
            '.' . $new_image_ext;

        // local file path + new filename
        $save_to = $this->local_image_folder.'/'.$new_name;


        // Upload using defined Upload Method, otherwise try all of them until we get one that might work
        if(isset($upload_method) && $upload_method != ''){
            $this->upload_method = $upload_method;
        }else{

            // check if CURL is installed
            if (function_exists('curl_init')){
                $this->upload_method = 'curl';
            // Check if PHP allows file_get_contents to use URL instead of file paths
            }elseif($this->allow_url_fopen){
                $this->upload_method = 'fopen';
            // Try GD library also requires PHP INI allow_url_fopen = true
            }elseif (extension_loaded('gd') && function_exists('gd_info') && $this->allow_url_fopen) {
                $this->upload_method = 'gd';
            // Try Sockets
            }else{
                $this->upload_method = 'sockets';
            }
        }

        switch ($this->upload_method) {
            case 'curl':
                $save_image = $this->curl_fetch_image($save_to);
                break;

            case 'fopen':
                $save_image = $this->fopen_fetch_image($save_to);
                break;

            case 'gd':
                $img = $image_create_func($this->remote_image_url);

                if (isset($imageQuality)) {
                    $save_image = $image_save_func($img, $save_to, $imageQuality);
                } else {
                    $save_image = $image_save_func($img, $save_to);
                }
                $save_image = $save_to;
                break;

            case 'sockets':
                $save_image = $this->sockets_fetch_image($save_to);
                break;
            default:
                $save_image = 'ERROR';
        }

        return $save_image;
    }


    public function curl_fetch_image($save_to)
    {
        $ch = curl_init($this->remote_image_url);
        $fp = fopen($save_to, "wb");

        // set URL and other appropriate options
        $options = array(
            CURLOPT_FILE => $fp,
            CURLOPT_HEADER => 0,
            CURLOPT_FOLLOWLOCATION => 1,
            CURLOPT_TIMEOUT => 60); // 1 minute timeout (should be enough)

        curl_setopt_array($ch, $options);

        $curl_result = curl_exec($ch);
        curl_close($ch);
        fclose($fp);

        if($curl_result){
            return $save_to;
        }else{
            die('ERROR Saving Remote File using cURL');
        }

    }


    public function fopen_fetch_image($save_to) {
        $image_to_fetch = file_get_contents($this->remote_image_url, false, NULL);
        //file_put_contents($save_to, $image_to_fetch);

        $local_image_file = fopen($save_to, 'w+');
        chmod($save_to, 0755);
        $imageFIle = fwrite($local_image_file, $image_to_fetch);
        fclose($local_image_file);

        if($imageFIle){
            return $save_to;
        }else{
            die('ERROR Saving Remote File using file_get_contents()');
        }


    }


    /**
     * [sockets_fetch_image description]
     * @param  [type] $save_to [description]
     * @return [type]          [description]
     */
    public function sockets_fetch_image($save_to)
    {
        $remoteImageResource = fopen($this->remote_image_url, 'r');
        $localImageResource = fopen($save_to, 'w+');
        stream_copy_to_stream($remoteImageResource, $localImageResource);

        fclose($remoteImageResource);
        fclose($localImageResource);

        return $save_to;
    }

}

Usage Example

$remote_image_url = 'http://l.yimg.com/a/i/ww/beta/y3.gif';
$local_image_folder = 'remoteuploads';

// You can set the prefered upload method.  
// If you do not set it, then it will try all of them until it can use one!
//$upload_method = 'curl';
//$upload_method = 'gd';
//$upload_method = 'fopen';
//$upload_method = 'sockets';
$upload_method = '';

// initialize the class
$image = new UploadImage($local_image_folder);
$get = $image->uploadRemoteImage($remote_image_url, $upload_method);

I plan to use it to allow users to upload remote images with a URL into a Markdown editor like Stack Exchange does.

enter image description here

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Single responsibility principle

The uploadRemoteImage is doing too much:

  • determine image type
  • set output path
  • decide output method
  • execute output method

It would be better to create smaller functions with a single purpose.

Dead code

The code below die is pointless. What is it doing there?

default:
    die('Not a valid image type');

    $image_create_func = 'ImageCreateFromJPEG';
    $image_save_func = 'ImageJPEG';
    $new_image_ext = 'jpg';

Closing files as soon as possible

You're doing a mostly good job of closing file handles pretty soon. But you could do slightly even better, for example here, you could move the chmod after closing the file:

    $local_image_file = fopen($save_to, 'w+');
    chmod($save_to, 0755);
    $imageFIle = fwrite($local_image_file, $image_to_fetch);
    fclose($local_image_file);

Inconsistent naming

It's not a good practice to mix different naming conventions in the same program. Although the code uses mostly snake_case, there are some camelCase examples. Then there's this:

    $imageFIle = fwrite($local_image_file, $image_to_fetch);

What's up with the capitalized "FI" in the middle of $imageFIle ?

Simplifications

In this code:

if($imageFIle){
    return $save_to;
}else{
    die('ERROR Saving Remote File using file_get_contents()');
}

You can drop the else, like this:

if ($imageFIle) {
    return $save_to;
}
die('ERROR Saving Remote File using file_get_contents()');
\$\endgroup\$
7
  • \$\begingroup\$ Thanks for the comments and feedback. As for the ` $imageFIle = FI` that is just a type. The different camelCase vs snake_case is mostly just an oversight I built this between 12 and 4am. I'm not making excuses (I know better), just mentioning the reasons! .... \$\endgroup\$
    – JasonDavis
    Commented Jun 26, 2015 at 21:16
  • \$\begingroup\$ Code after die() is another case of a silly mistake. It seems all you mentioned are things I should know better to do however I managed to let the slip in anyways! I do appreciate you pointing them out so I can it all up at least. As for uploadRemoteImage doing to much. I agree it could be broken down into about 3-4 more methods and I will probably do that. Again thanks for reviewing it, it;s amazing the things you can overlook yourself after hours of looking at something. I think the main source of issues is the fact I had refactored the whole Class about 8 times so it kept changing \$\endgroup\$
    – JasonDavis
    Commented Jun 26, 2015 at 21:16
  • \$\begingroup\$ Can I ask, do you know of any upload methods I may have missed? I was thinking of using the array of upload methods I defined at the top to maybe loop over attempting the upload and if it tries one and fails, it would move on and try the next available method. I assume using a try{} and catch block could help with this now that I know what most of these methods return on failure. \$\endgroup\$
    – JasonDavis
    Commented Jun 26, 2015 at 21:21
  • \$\begingroup\$ I don't know other methods, but I'm not an expert at this \$\endgroup\$
    – janos
    Commented Jun 26, 2015 at 21:28
  • \$\begingroup\$ Using your suggestions as well as some ideas from a WordPress core file I have a re-write I am going to post as an answer here so others might be able to use it as well as improve upon it and would love your feedback on it if you're up for it. There doesn't seem to be any open source libraries for this purpose that I can find anyways! I now have a function that iterates an array of transport methods to test each one until one passes and then calls a function with that name. Also is function for each upload transport method so its more modular now \$\endgroup\$
    – JasonDavis
    Commented Jul 11, 2015 at 3:45
2
\$\begingroup\$

Switch

I'll focus on the following switch, because there's way to do way easier :

        switch ($type) {
        case 'jpeg':
            $image_create_func = 'ImageCreateFromJPEG';
            $image_save_func = 'ImageJPEG';
            $new_image_ext = 'jpg';

            // Best Quality: 100
            $imageQuality = isset($this->imageQuality) ? $this->imageQuality : 100;
            break;

        case 'png':
            $image_create_func = 'ImageCreateFromPNG';
            $image_save_func = 'ImagePNG';
            $new_image_ext = 'png';

            // Compression Level: from 0  (no compression) to 9
            $imageQuality = isset($this->imageQuality) ? $this->imageQuality : 0;
            break;

        case 'bmp':
            $image_create_func = 'ImageCreateFromBMP';
            $image_save_func = 'ImageBMP';
            $new_image_ext = 'bmp';
            break;

        case 'gif':
            $image_create_func = 'ImageCreateFromGIF';
            $image_save_func = 'ImageGIF';
            $new_image_ext = 'gif';
            break;

        case 'vnd.wap.wbmp':
            $image_create_func = 'ImageCreateFromWBMP';
            $image_save_func = 'ImageWBMP';
            $new_image_ext = 'bmp';
            break;

        case 'xbm':
            $image_create_func = 'ImageCreateFromXBM';
            $image_save_func = 'ImageXBM';
            $new_image_ext = 'xbm';
            break;

        default:

            die('Not a valid image type');

            $image_create_func = 'ImageCreateFromJPEG';
            $image_save_func = 'ImageJPEG';
            $new_image_ext = 'jpg';
    }

It's important to get what it does, and that's really not a lot : it defines 3 variables, 4 in two cases and test validity of the type. Okay, let's do this with arrays. Let's define an array like that :

$imageProperties = array( 
   "jpeg" => array( 
      "createFunction" => "ImageCreateFromJPEG", 
      "saveFunction" => "ImageJPEG", 
      "imageExtension" => "jpg" ),
   "png" => array( 
      "createFunction" => "ImageCreateFromPNG", 
      "saveFunction" => "ImagePNG", 
      "imageExtension" => "png",
      "maxQuality" => 100 ) ) ;

I won't list all the types here as this won't add much to the answer. Then, to replace your switch, you could simply :

  • to verify that the type is valid : use in_array and array_keys functions
  • if you really want to get $image_create_func and the other two variables, just fetch the data using the type as key
  • to define the quality, you can check first that the index exists for that type, and then if it was defined by the user of the function

Using that method, you can easily add/modify or delete types and how to handle them. For example, if you feel like your website should not support GIF, just delete it from the list. If your list is in a file that is included, all your code will now stop supporting GIF, just by modifying one file.

File naming

One another thing, focusing on how you name the images :

  • The way to build the filename seems way too complex
  • And it does not seem to address the case when two images have the same filename ?

A simple way to counter that would be to "manually" define the filename, using for example the function uniqid to get an unique id for your image, and then just appending the extension.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.