Login   Register  
PHP Classes
elePHPant
Icontem

File: includes/htmlSaveComplete.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Sarfraz Ahmed  >  PHP Save Complete HTML Page  >  includes/htmlSaveComplete.php  >  Download  
File: includes/htmlSaveComplete.php
Role: Class source
Content type: text/plain
Description: htmlSaveComplete.php
Class: PHP Save Complete HTML Page
Save HTML pages complete with images, CSS and JS
Author: By
Last change: it is now able to fetch pages from secure urls
Date: 2013-11-02 15:26
Size: 18,562 bytes
 

Contents

Class file image Download
<?php
/**
 * The htmlSaveComplete class can be used to save specified URLs completely in single file
 * by converting images to data URIs and extracting all CSS.
 *
 * Author: Sarfraz Ahmed
 * http://sarfraznawaz.wordpress.com
 *
 *
 * NOTE: If you are using this class, please keep above author information intact. Thanks
 */

/* Issues
 ---------------------------------------------------------
Does not convert images from @import URLs to data URIs, removed this (eg not using getImportStyles function)
due to performance issue.
*/

set_time_limit(300); // no more than 5 minutes !!!

require_once 'includes/content_extractor.php'; # class to extract "main" content part of given page
require_once 'includes/url_to_absolute.php'; # or implement your own function to convert relative URLs to absolute

class htmlSaveComplete
{

    # set debug mode on or off with 1, 0 or true, false respectively
    const DEBUG = 0;

    # holds debug data
    private $debugOutput = array();
    # url to save complete page from
    private $url = '';
    # set user agent mode on or off with 1, 0 or true, false respectively
    private $useUserAgent = FALSE;
    # holds parsed html
    private $html = '';
    # holds DOM object
    private $dom = '';

    /**
     *  The constructor function. Allows setting url to save data from and whether to use user agent.
     *
     * @param $url - url to save complete page from.
     * @param bool $useUserAgent - whether to use user agent to get page data
     *
     * @throws exception - throws an exception if provided url isn't in proper format
     */
    public function __construct($url, $useUserAgent = FALSE)
    {
        # see if Data URIs are supported by the browser
        $isDatauriSupported = preg_match('#(Opera|Gecko|MSIE 8)#', $_SERVER['HTTP_USER_AGENT']);

        if (! $isDatauriSupported)
        {
            throw new Exception('Your Browser does not support Data URIs');
        }

        # validate the URL
        if (! filter_var($url, FILTER_VALIDATE_URL))
        {
            throw new Exception('Invalid URL. Make sure to specify http(s) part.');
        }

        $this->url = $url;
        $this->useUserAgent = $useUserAgent;

        # suppress DOM parsing errors
        libxml_use_internal_errors(TRUE);

        $this->dom = new DOMDocument();
        $this->dom->preserveWhiteSpace = FALSE;
        # avoid strict error checking
        $this->dom->strictErrorChecking = FALSE;
    }

    /**
     * Gets complete page data and returns generated string
     *
     * @param bool $keepjs - whether to keep javascript
     * @param bool $contentOnly - whether to extract main content part of the page only
     * @param bool $compress - whether to remove extra whitespac
     *
     * @return string|void
     */
    public function getCompletePage($keepjs = FALSE, $contentOnly = FALSE, $compress = FALSE)
    {
        $scriptBuffer = '';
        $cssBuffer = '<style>';

        if ($this->useUserAgent)
        {
            $this->html = $this->getUrlContents($this->url);
        }
        else
        {
            $this->html = file_get_contents($this->url);
        }

        # get document stylesheets
        $stylesheets = $this->getStyleSheets();

        foreach ($stylesheets as $stylesheet)
        {
            $cssBuffer .= $this->getContents($this->getFullUrl($stylesheet)) . "\r\n\r\n";
        }

        # get @import URLS and merge it in CSS
//      $importURLs = $this->getImportStyles();
//
//      foreach ( $importURLs as $importURL )
//      {
//          $cssBuffer .= $this->getContents( $this->getFullUrl($importURL) ) . "\r\n\r\n";
//      }

        $cssBuffer .= '</style>' . "\r\n\r\n";

        # get document scripts
        if ($keepjs)
        {
            $scriptBuffer .= '<script>';
            $scripts = $this->getScripts();

            foreach ($scripts as $script)
            {
                $scriptBuffer .= $this->getContents($this->getFullUrl($script)) . "\r\n\r\n";
            }

            $scriptBuffer .= '</script>' . "\r\n\r\n";
        }

        # convert URLs from CSS styles to data URIs
        $cssBuffer = $this->toDataUri($cssBuffer);

        # remove useless stuff such as <link>, <meta> and <script> tags
        $this->removeUseless($keepjs);

        # convert URLs from @import styles to data URIs
        $this->html = $this->toDataUri($this->html);

        # see if we need to extract main content part
        if ($contentOnly)
        {
            $extractor = new ContentExtractor();
            $this->html = $extractor->extract($this->html);
        }

        # convert <img> tags to data URIs
        $this->convertImageToDataUri();

        # convert all relative links for <a> tags to absolute
        $this->toAbsoluteURLs();

        # finally join the css and html and inssert information header
        if (strlen($this->html) > 300)
        { # we did get some html back
            $this->html = $cssBuffer . $scriptBuffer . $this->html;
            $this->insertHeader();

            if (self::DEBUG)
            {
                $this->showDebugInfo();
                exit();
            }

            if ($compress)
            {
                return $this->compress($this->html);
            }
            else
            {
                return $this->html;
            }
        }
        else
        {
            return '';
        }
    }

    /**
     * Converts images to data URIs
     */
    private function convertImageToDataUri()
    {
        $tags = $this->getTags('//img');
        $tagsLength = $tags->length;

        # loop over all <img> tags and convert them to data uri
        for ($i = 0; $i < $tagsLength; $i ++)
        {
            $tag = $tags->item($i);
            $src = $this->getFullUrl($tag->getAttribute('src'));

            if ($this->remote_file_exists($src))
            {
                $dataUri = $this->imageToDataUri($src);
                $tag->setAttribute('src', $dataUri);
            }
        }

        # now save html with converted images
        $this->html = $this->dom->saveHTML();
    }

    /**
     * Returns tags list for specified selector
     *
     * @param $selector - xpath selector expression
     *
     * @return DOMNodeList
     */
    private function getTags($selector)
    {
        $this->dom->loadHTML($this->html);
        $xpath = new DOMXpath($this->dom);
        $tags = $xpath->query($selector);

        # free memory
        libxml_use_internal_errors(FALSE);
        libxml_use_internal_errors(TRUE);
        libxml_clear_errors();
        unset($xpath);
        $xpath = NULL;

        return $tags;
    }

    /**
     * Converts URLs with format url(....) to data URIs
     *
     * @param $html
     *
     * @return mixed
     */
    private function toDataUri($html)
    {
        # convert css URLs to data URIs
        $html = preg_replace_callback('#(url\([\'\"]?)([^\"\'\)]+)([\"\']?\))#', array($this, 'createDataUri'), $html);

        return $html;
    }

    /**
     * Inserts htmlSaveComplete information header on very start of page
     */
    private function insertHeader()
    {
        $header = '<!-- This page was saved with htmlSaveComplete (http://sarfraznawaz.wordpress.com) -->' . "\r\n\r\n";
        $this->html = $header . $this->html;
    }

    /**
     * Checks whether or not remote file exists
     *
     * @param $url
     *
     * @return bool
     */
    private function remote_file_exists($url)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        # don't download content
        curl_setopt($ch, CURLOPT_NOBODY, 1);
        curl_setopt($ch, CURLOPT_FAILONERROR, 1);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

        if (curl_exec($ch) !== FALSE)
        {
            return TRUE;
        }

        return FALSE;
    }

    /**
     * Converts images from <img> tags to data URIs
     *
     * @param $path - image path eg src value
     *
     * @return string - generated data uri
     */
    private function imageToDataUri($path)
    {
        $fileType = trim(strtolower(pathinfo($path, PATHINFO_EXTENSION)));
        $mimType = $fileType;

        # since jpg/jpeg images have image/jpeg mime-type
        if (! $fileType || $fileType === 'jpg')
        {
            $mimType = 'jpeg';
        }
        else
        {
            if ($fileType === 'ico')
            {
                $mimType = 'x-icon';
            }
        }

        # make sure that it is an image and convert to data uri
        if (preg_match('#^(gif|png|jp[e]?g|bmp)$#i', $fileType) || $this->isImage($path))
        {
            # in case of images from gravatar, etc
            if ($mimType === 'php' || stripos($mimType, 'php') !== FALSE)
            {
                $mimType = 'jpeg';
            }

            $data = $this->getContents($path);
            $base64 = 'data:image/' . $mimType . ';base64,' . base64_encode($data);

            return $base64;
        }
    }

    /**
     * Removes <link>, <meta> and <script> tags from generated page
     */
    private function removeUseless($keepjs)
    {
        # remove empty lines
        //$this->html = preg_replace('#(\r\n[ \t]*){2,}#', "\r\n", $this->html);

        # remove @import declarations
        //preg_replace('#(@import url\([\'\"]?)([^\"\'\)]+)([\"\']?\))#', '', $this->html);

        # fix showing up of garbage characters
        $this->html = mb_convert_encoding($this->html, 'HTML-ENTITIES', 'UTF-8');

        $tags = $this->getTags('//meta | //link | //script');

        $tagsLength = $tags->length;

        # get all <link>, <meta> and <script> tags and remove them
        for ($i = 0; $i < $tagsLength; $i ++)
        {
            $tag = $tags->item($i);

            # delete only external scripts
            if (strtolower($tag->nodeName) === 'script')
            {
                if ($keepjs)
                {
                    if ($tag->getAttribute('src') !== '')
                    {
                        $tag->parentNode->removeChild($tag);
                    }
                }
                else
                {
                    $tag->parentNode->removeChild($tag);
                }
            }
            elseif (strtolower($tag->nodeName) === 'meta')
            {
                # keep the charset meta
                if (stripos($tag->getAttribute('content'), 'charset') === FALSE)
                {
                    $tag->parentNode->removeChild($tag);
                }
            }
            else
            {
                $tag->parentNode->removeChild($tag);
            }
        }

        $this->html = $this->dom->saveHTML();
    }

    /**
     * Gets all external stylesheets of the page
     *
     * @return array
     */
    private function getStyleSheets()
    {
        $styleSheets = array();
        $links = $this->getTags('//link[contains(@rel, "stylesheet")]');

        foreach ($links as $link)
        {
            if (self::DEBUG)
            {
                $this->debugStore(array('stylesheet' => $link->getAttribute('href')));
            }

            array_push($styleSheets, $link->getAttribute('href'));
        }

        return $styleSheets;
    }

    /**
     * Gets all external scripts of the page.
     *
     * @return array
     */
    private function getScripts()
    {
        $scripts = array();
        $links = $this->getTags('//script[contains(@src, "")]');

        foreach ($links as $link)
        {
            //$src = preg_replace('#\?.*#', '', $link->getAttribute('src'));
            $src = $link->getAttribute('src');

            if (strpos($src, '.js') !== FALSE)
            {
                if (self::DEBUG)
                {
                    $this->debugStore(array('script' => $src));
                }

                array_push($scripts, $src);
            }
        }

        return $scripts;
    }

    /**
     * Gets all
     *
     * @import URLs
     *
     * @return array
     */
    private function getImportStyles()
    {
        $importURLs = array();
        $styles = $this->getTags('//style');

        foreach ($styles as $style)
        {
            $content = $style->textContent;
            preg_match_all('#(@import url\([\'\"]?)([^\"\'\)]+)([\"\']?\))#', $content, $matches);

            if (isset($matches[2]) && count($matches[2]))
            {
                foreach ($matches[2] as $importUrl)
                {
                    $importURLs[] = $importUrl;
                }
            }
        }

        return $importURLs;
    }

    /**
     * Converts relative <a> tag paths to absolute paths
     */
    private function toAbsoluteURLs()
    {
        $links = $this->getTags('//a');

        foreach ($links as $link)
        {
            $link->setAttribute('href', $this->getFullUrl($link->getAttribute('href')));
        }

        $this->html = $this->dom->saveHTML();
    }

    /**
     * Compresses generated page by removing extra whitespace
     */
    private function compress($string)
    {
        # remove whitespace
        return str_replace(array("\r\n", "\r", "\n", "\t", '  ', '    ', '    '), '', $string);
    }

    /**
     * Gets content for given url
     *
     * @param $url
     *
     * @return string
     */
    private function getContents($url)
    {
        $data = @file_get_contents($url);

        if ($data)
        {
            return $data;
        }

        return @file_get_contents(trim($url));
    }

    /**
     * Converts matched URLs to data URIs
     *
     * @param $matches
     *
     * @return string
     */
    private function createDataUri($matches)
    {
        $fileType = explode('.', $matches[2]);
        $fileType = trim(strtolower($fileType[count($fileType) - 1]));

        # replace ?whatever=value from extensions
        $fileType = preg_replace('#\?.*#', '', $fileType);
        $mimeType = $fileType;

        # since jpg/jpeg images have image/jpeg mime-type
        if ($fileType === 'jpg')
        {
            $mimeType = 'jpeg';
        }
        else
        {
            if ($fileType === 'ico')
            {
                $mimeType = 'x-icon';
            }
            else
            {
                if ($fileType === 'css')
                {
                    $mimeType = 'css';
                }
            }
        }

        $datauri = $this->getFullUrl($matches[2]);

        #if the file is an image from CSS URLs convert it to data uri
        if (preg_match('#^(gif|png|jp[e]?g|bmp|css)$#i', $fileType))
        {
            if (self::DEBUG)
            {
                $this->debugStore(array('datauri' => $datauri));
            }

            $data = $this->getContents($datauri);

            if (! $data)
            {
                # return whatever there was originally
                return $matches[0];
            }

            $data = base64_encode($data);

            $mime = $fileType === 'css' ? 'text' : 'image';

            return $matches[1] . 'data:' . $mime . '/' . $mimeType . ';base64,' . $data . $matches[3];
        }
        else
        {
            # return whatever there was originally
            return $matches[0];
        }

    }

    /**
     * Gets content for given url using curl and optionally using user agent
     *
     * @param $url
     * @param int $timeout
     * @param string $userAgent
     *
     * @return int|mixed
     */
    private function getUrlContents(
        $url,
        $timeout = 0,
        $userAgent = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10'
    ) {
        $rawHtml = curl_init();
        curl_setopt($rawHtml, CURLOPT_URL, $url);
        curl_setopt($rawHtml, CURLOPT_RETURNTRANSFER, 1); # return result as string rather than direct output
        curl_setopt($rawHtml, CURLOPT_CONNECTTIMEOUT, $timeout); # set the timeout
        curl_setopt($rawHtml, CURLOPT_USERAGENT, $userAgent); # set our 'user agent'

        curl_setopt($rawHtml, CURLOPT_SSL_VERIFYPEER, false);

        $output = curl_exec($rawHtml);
        curl_close($rawHtml);

        if (! $output)
        {
            return - 1;
        }

        return $output;
    }

    /**
     * Converts relative URLs to absolute URLs
     *
     * @param $url
     *
     * @return bool|string
     */
    private function getFullUrl($url)
    {
        if (strpos($url, '//') === FALSE)
        {
            return url_to_absolute($this->url, $url);
        }

        return $url;
    }

    /**
     * Checks if provided path is an image
     *
     * @param $path
     *
     * @return bool
     */
    private function isImage($path)
    {
        list($width) = @getimagesize($path);

        if (isset($width) && $width)
        {
            return TRUE;
        }

        return FALSE;
    }

    /**
     * Stores debug information
     *
     * @param $data
     */
    private function debugStore($data)
    {
        if (self::DEBUG)
        {
            if (is_array($data))
            {
                foreach ($data as $key => $value)
                {
                    if (isset($this->debugOutput[$key]))
                    {
                        $this->debugOutput[$key . count($this->debugOutput)] = $value;
                    }
                    else
                    {
                        $this->debugOutput[$key] = $value;
                    }
                }
            }
            else
            {
                $this->debugOutput[] = $data;
            }
        }
    }

    /**
     * Shows debug information
     */
    private function showDebugInfo()
    {
        echo '<pre>';
        ksort($this->debugOutput);
        print_r($this->debugOutput);
        echo '</pre>';
    }

}