Gitosis Installation Script For Ubuntu 10.04

If you like programming, you might like version control as well, means you might like GIT and your own repository server at home to “sync” with your friends and fellows. That’s what I wanted as well for my Ubuntu (10.4) system, and I have to admit: Setting up gitosis can take some time, and there are some nice “pitfallish” details on the path as well, so I condensed the various tutorials in the web into a shell script, which installs gitosis, gitweb, git-daemon and optionally viewgit. The mainly used howtos are straight from the ubuntu docs (http://help.ubuntu.com/community/Git). I hope is saves you some time.

Sequence:

  • Download and extract the ZIP archive in your server home directory or /tmp/”,
  • Log into your server using SSH
  • Run in the shell ./gitosis-setup/setup install
  • Optionally before ./gitosis-setup/setup --help
  • Follow the instructions. You need this only if you don’t have a SSH key file, e.g. ~/.ssh/id_rsa, otherwise the script will run without asking questions

What happens then?

  • The script checks if the actual user has a ~/.ssh/id_rsa key file, which is required for gitosis. If not it generates one for you usind ssh-keygen (encryption 4096 bits). You will have to enter a passphrase for this keyfile.
  • The script installs git gitosis apache2 libapache2-mod-php5 php-geshi gitweb using aptitude
  • The gitosis setup generates a user “gitosis”. The script renames this user to “git”
  • The script initializes gitosis with your key file
  • The script adds the git-daemon to the services and adds an allow rule for the git port in the firewall (ufw) configuration.

Result

  • Your gitosis data are in /srv/gitosis.
  • You have a new user “git” on your machine, this user cannot login to the shell, cannot login via password and is strictly bound to the gitosis system (see /srv/gitosis/.ssh/authorized_keys, there is for each registered user the command that is executed when someone connects via SSH).
  • If you did not have it yet, you have now a running apache2 server with the document root /var/www/. You can access gitweb using http[s]://yourserver.whatever/gitweb, at least at http://localhost/gitweb. Note: The script does not change your document root if already existing. Gitweb is a perl construction that is added to the apache2 using a location alias.
  • The account you installed gitosis with is not the “admin” account of gitosis.

Now you configure gitosis with your server account, add projects and users:

cd ~/
git clone git@localhost:gitosis-admin.git
cd ~/gitosis-admin
nano gitosis.conf
cp ~/a-friends-public-key-file.pub ~/gitosis-admin/keydir/
etc.
etc.

Quelltext

#!/bin/bash

# locals
APTITUDE='/usr/bin/aptitude'
APTITUDE_INSTALL="$APTITUDE -q -y install"
TEMP_DIR='/tmp/setup-gitsvr'
GITHOST=localhost
ID_RSA_NAME="id_rsa"
LINE_INDENT=""
WWW_ROOT="/var/www"
APACHE_USER="www-data"
APACHE_GROUP="www-data"

# functions
function echo_error() { echo -e "\033[0;31m[FAILED] $@\033[0m"; }
function echo_ok()    { echo -e "\033[0;32m[OK] $@\033[0m"; }
function echo_info()  { echo -e "\033[0;33m$@\033[0m"; }

function echo_usage() {
	echo "Usage:"
	echo ""
	echo " To install gitosis with doxygen and gitweb:"
	echo "   $(basename $0) install"
	echo "   This will install git, gitosis. It will configure your server, so that:"
	echo ""
	echo "     - The repository is in /srv/gitosis (made by aptitude)."
	echo "     - The git user is 'git' (renamed from 'gitosis')."
	echo "     - The account you run it from is the gitosis admin (use 'sudo setup install'"
	echo "       and do not 'sudo su; setup install'."
	echo "       - If this account does not yet have an 'id_rsa' key file in ~/.ssh, we will"
	echo "         generate one during the setup process."
	echo ""
	echo "   After the setup you can directly edit the gitosis config using:"
	echo ""
	echo "       $ cd ~/ "
	echo -e "       $ \033[0;31mgit clone git@localhost:gitosis-admin.git\033[0m"
	echo "       $ cd gitosis-admin"
	echo "       $ [vim/nano] gitosis.conf"
	echo "       $ cp <path to new user key file>.pub ~/gitosis-admin/keydir/<name of the user>.pub"
	echo -e "       $ \033[0;31mgit commit -am 'Added <name of the user>, did something in the config'"
	echo -e "       $ \033[0;31mgit push origin master\033[0m"
	echo ""
	echo ""
	echo ""
	echo " To uninstall gitosis and gitweb:"
	echo "   $(basename $0) uninstall --really"
	echo "   This will remove gitosis and gitweb. Other the other packages have to be removed"
	echo "   manually using aptutude remove."
	echo ""
	echo " To install gitweb:"
	echo "   $(basename $0) install-gitweb"
	echo ""
	echo " To uninstall gitweb"
	echo "   $(basename $0) uninstall-gitweb"
	echo ""
	echo " To install viewgit:"
	echo "   $(basename $0) install-viewgit"
	echo ""
	echo " To uninstall viewgit:"
	echo "   $(basename $0) uninstall-viewgit"
	echo ""
	exit
}

# uninstall process
if [ "$1" = "uninstall" ]; then
	if [ "$2" != "--really" ]; then
		echo_error 'You must say "setup uninstall --really"'
		echo ""
		echo_usage
	fi
	sudo echo ""
	echo_info "----------------------------------------------------------------------"
	echo_info "UNINSTALL"
	echo_info "----------------------------------------------------------------------"

	echo_info "remove git-demon ..."
	if [ -f /etc/init.d/git-daemon ]; then
	echo_info "Stopping service git-daemon"
	sudo service git-daemon stop
	echo_info "Removing git port (9418) from firewall rules (ufw)"
	sudo ufw delete allow in 9418/tcp
	echo_info "Removing /etc/init.d/git-daemon"
	sudo update-rc.d -f git-daemon remove
	sudo rm /etc/init.d/git-daemon
	fi

	echo_info "remove gitosis ..."
	sudo $APTITUDE -q -y remove gitosis gitweb
	echo_info "purge gitosis ..."
	sudo $APTITUDE -q -y purge gitosis gitweb
	echo_info "remove repository ..."
	sudo rm -rf /srv/gitosis &> /dev/null
	echo_info "Ensure user/group gitosis is gone ..."
	sudo groupdel git &> /dev/null
	sudo userdel git &> /dev/null
	echo_ok "uninstalled"
	exit
elif [ "$1" = "install" ]; then
	# mark sudo
	sudo echo ""

	# initial checks
	if [ -d /srv/gitosis ]; then
		echo_error "gitosis already setup (in /srv/gitosis/)."
		exit;
	fi

	if [ `dirname $TEMP_DIR` != "/tmp" ]; then
		echo_error "temp dir is not a subdir of /tmp/"
		exit;
	fi

	if [ ! -f ~/.ssh/$ID_RSA_NAME ]; then
		echo_info "----------------------------------------------------------------------"
		echo_info "You don't have a personal user key file yet, we generate one now ..."

		if [ ! -d ~/.ssh ]; then
			mkdir ~/.ssh
			chmod 700 ~/.ssh
			echo_info "Created ~/.ssh with 700"
		fi

		echo_info "Keygen - now enter a passphrase for your private id key file ..."
		ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa
		chmod 600 ~/.ssh/id_rsa
		echo_info "~/.ssh/id_rsa set to mod 600"
		echo_info "Now we register you at the local machine for ssh keyfile login"
		echo_info "    (ssh-copy-id $USER@$GITHOST), you may need to enter your normal login password"
		echo_info '    +++ IF IT SAYS NOW "The authenticity of host ... cant be established", JUST SAY "yes" +++'
		echo ""
		ssh-copy-id $USER@$GITHOST
		echo_info "OK, let's go on with the normal gitosis installation ..."
		echo_info "----------------------------------------------------------------------"
	fi

	if [ ! -f ~/.ssh/$ID_RSA_NAME.pub ]; then
		echo_error "Public key file not there"
		exit
	fi

	# aptitude
	echo_info "Installing with aptitude: git gitosis"
	sudo $APTITUDE_INSTALL git gitosis

	# Change user to "git" as clone "git@myhost" is shorter and seems reasonable
	sudo killall -u gitosis
	sudo id gitosis
	sudo usermod -l git gitosis
	sudo groupmod -n git gitosis
	sudo id git
	sudo chown -R git:git /srv/gitosis

	# initialize gitosis
	echo_info "Initializing git (/srv/gitosis)"
	sudo -H -u git gitosis-init < ~/.ssh/$ID_RSA_NAME.pub

	# setup git daemon
	if [ ! -f /etc/init.d/git-daemon ]; then
		echo_info "Copying git-daemon to /etc/init.d/git-daemon"
		sudo cp "$(dirname $0)/git-daemon" /etc/init.d/
		sudo chmod +x /etc/init.d/git-daemon
		sudo update-rc.d git-daemon defaults
		echo_info "Allowing git port (9418) in firewall (ufw)"
		sudo ufw allow in 9418/tcp
		echo_info "Starting service git-daemon"
		sudo service git-daemon start
	fi

	echo ""
	echo_info "----------------------------------------------------------------------"
	echo_info "Now you can clone the gitosis config using: "
	echo_ok "    $ git clone git@$GITHOST:gitosis-admin.git"

elif [ "$1" = "install-gitweb" ]; then
	# mark sudo
	sudo echo ""
	echo_info "Installing with aptitude: apache2 libapache2-mod-php5 php-geshi"
	sudo $APTITUDE_INSTALL apache2 libapache2-mod-php5 php-geshi gitweb
	sudo adduser $APACHE_USER git

elif [ "$1" = "uninstall-gitweb" ]; then

	sudo $APTITUDE remove php-geshi gitweb

elif [ "$1" = "install-viewgit" ]; then

	sudo echo ""
	if [ ! -d $WWW_ROOT ]; then
		echo_error "Web root directory not there ($WWW_ROOT)"
		exit
	fi
	if [ -d $WWW_ROOT/viewgit ]; then
		echo_error "Already installed in ($WWW_ROOT/viewgit)"
		exit
	fi

	pushd &> /dev/null
	cd $WWW_ROOT
	echo_info "Getting web interface, will be then in $WWW_ROOT/viewgit"
	sudo git clone git://repo.or.cz/viewgit.git
	echo_info "Changing viewgit's user/group to $APACHE_USER:$APACHE_GROUP"
	sudo chown -R $APACHE_USER:$APACHE_GROUP viewgit
	echo_info "Preparing config file ($WWW_ROOT/viewgit/inc/localconfig.php)"
	cd $WWW_ROOT/viewgit/inc
	sudo cp config.php localconfig.php
	sudo chown $APACHE_USER:$APACHE_GROUP localconfig.php
	popd &> /dev/null
	echo_info "Adding apache user to gitosis group"
	sudo adduser $APACHE_USER git
	echo_ok "You can now edit the config file $WWW_ROOT/viewgit/inc/localconfig.php"

elif [ "$1" = "uninstall-viewgit" ]; then

	sudo echo ""
	if [ ! -d $WWW_ROOT/viewgit ]; then
		echo_error "Not yet installed in ($WWW_ROOT/viewgit)"
		exit
	fi
	pushd &> /dev/null
	sudo rm -rf $WWW_ROOT/viewgit &>/dev/null
	sudo adduser $APACHE_USER git
	popd &> /dev/null

else
	echo ""
	echo_usage
	exit
fi

Daemon script

Dieses Skript entspricht 1-zu-1 dem auf der Ubuntu gitosis Manual Page veröffentlichten Daemon.

# Taken from here: http://pastie.org/227647

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
NAME=git-daemon
PIDFILE=/var/run/$NAME.pid
DESC="the git daemon"
DAEMON=/usr/lib/git-core/git-daemon
DAEMON_OPTS="--base-path=/srv/gitosis/repositories --export-all --verbose --syslog --detach --pid-file=$PIDFILE --user=gitosis --group=nogroup"

test -x $DAEMON || exit 0

[ -r /etc/default/git-daemon ] && . /etc/default/git-daemon

. /lib/lsb/init-functions

start_git() {
  start-stop-daemon --start --quiet --pidfile $PIDFILE \
    --startas $DAEMON -- $DAEMON_OPTS
}

stop_git() {
  start-stop-daemon --stop --quiet --pidfile $PIDFILE
  rm -f $PIDFILE
}

status_git() {
  start-stop-daemon --stop --test --quiet --pidfile $PIDFILE >/dev/null 2>&1
}

case "$1" in
  start)
  log_begin_msg "Starting $DESC"
  start_git
  log_end_msg 0
  ;;
  stop)
  log_begin_msg "Stopping $DESC"
  stop_git
  log_end_msg 0
  ;;
  status)
  log_begin_msg "Testing $DESC: "
  if status_git
  then
    log_success_msg "Running"
    exit 0
  else
    log_failure_msg "Not running"
    exit 1
  fi
  ;;
  restart|force-reload)
  log_begin_msg "Restarting $DESC"
  stop_git
  sleep 1
  start_git
  log_end_msg 0
  ;;
  *)
  echo "Usage: $0 {start|stop|restart|force-reload|status}" >&2
  exit 1
  ;;
esac

exit 0
Posted in Programming, Version Control | Tagged , , , , , , , , | Comments Off

RSS 2.0 rendering class

Es kann recht einfach sein, in PHP RSS 2.0 Feeds zu erstellen – mit der Klasse Rss2Feed. Diese kann einen oder mehrere Feed-Channels in ein RSS-Feed rendern. Dazu muss sie lediglich durch Überladen der Methode onLoad()mit den verfügbaren Daten gefüttert werden. Die XML-Ausgabe kann entweder in einer .rss-Datei gespeichert oder direkt zum Browser gesendet werden. Das Beispiel zeigt, wie es geht:

Anwendungsbeispiel

<?php
// Before doing anything else, we include the library
include_once('swlib/swlib.class.php');

// Start the library with configuration
swlib::start(array(
    // path to library var directory, we use an one one
    // The var path must be writable for the server, it is NOT the
    // directory "/var" on Unix-Like systems, but a directory to serialize
    // "variables" in.
    'var_path' => sys_get_temp_dir(),
));

// Our class to customize the feed channel
class MyRss2Feed extends Rss2Feed {

    // That's the method we have to overload:
    protected function onLoad($identifier='', $numOfItems='') {
        // Instead of loading items from a database, we generate
        // a static array for this example:
        return array(
            'title' => 'CHANNEL TITLE',
            'description' => 'Channel description',
            'link' => '', // will be replaced with your $_SERVER[HTTP_HOST]
            'items' => array(
                array(
                    'title' => 'Item 1 title',
                    'description' => 'Item 1 description',
                    'link' => 'http://item1.link/',
                ),
                array(
                    'title' => 'Item 2 title &', // this will be escaped ("&")
                    'description' => 'Item 2 description',
                    'link' => 'http://item2.link/',
                ),
                array(
                    'title' => 'Item 2 title with an "ü"',
                    'description' => '', // No content
                    'link' => '', // link will be replaced
                ),
            )
        );
    }
}

// Generate the feed with 100 items, only the default channel
try {
    $rss = new MyRss2Feed();
    $xml = $rss->renderFeed(array(), 100);
} catch(Exception $e) {
    $exception = $e;
}

// Script output
if(!isset($exception) && isset($_GET['rss'])) {
    // View the RSS in a reader
    OutputBuffer::purge();
    Tracer::disable();
    print $xml;
} else {
    // View the RSS XML code in the browser
    print '<html><body><pre>';
    print htmlspecialchars($xml);
    print '</pre></body></html>';
}
?>

Ausgabe

<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:wfw="http://wellformedweb.org/CommentAPI/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
  xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
  >
 <channel>
  <title>CHANNEL TITLE</title>
  <link>http://wp.localhost/</link>
  <description></description>
  <generator>swlib</generator>
  <ttl>60</ttl>
  <item>
   <title>Item 1 title</title>
   <description>Item 1 description</description>
   <link>http://item1.link/</link>
  </item>
  <item>
   <title><![CDATA[Item 2 title &]]></title>
   <description>Item 2 description</description>
   <link>http://item2.link/</link>
  </item>
  <item>
   <title><![CDATA[Item 2 title ü]]></title>
   <description></description>
   <link>http://wp.localhost/</link>
  </item>
 </channel>
</rss>

Klassen-Quelltext

<?php
/**
 * Provides rendering one or more RSS channels into a RSS XML text.
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2010
 * @license GPL
 * @version 1.0
 */
class Rss2Feed {

    /**
     * That's how the channel specfication looks like:
     * @staticvar array
     */
    private static $dataTemplate = array(
        'title' => '',              // Main title if the feed, should contain the website url
        'link' => '',               // Link to the main web page of the channel
        'description' => '',        // Text description of the feed
        'category' => '',           // One or more category names
        'language' => '',           // Channel language
        'copyright' => '',          // Copyright information
        'generator' => 'swlib',     // Feed generator
        'managingeditor' => '',     // Email address of he managing editor
        'webMaster' => '',          // Email address of the admin
        'pubDate' => '',            // RFC822 time when the content was published
        'lastbuilddate' => '',      // RFC822 time when the channel was modified
        'docs' => '',               // RSS specification documentation link
        'cloud' => '',              // Cloud service interface provider
        'ttl' => '60',              // Time to live
        'image' => array(           // Channel image:
            'url' => '',            // Url to GIF, JPEG or PNG
            'title' => '',          // Like alt tag in HTML
            'link' => '',           // Page link (use link of the channel itself)
            'width' => '',          // Optional width, mad 144
            'height' => '',         // Optional height, mad 400
            'description' => '',    // Optional like HTML title=""
        ),
        'rating' => '',             //
        'textinput' => array(       // Input text box:
            'title' => '',          // The label of the Submit button in the text input area.
            'description' => '',    // Explains the text input area
            'name' => '',           // The name of the text object in the text input area
            'link' => ''            // The URL of the CGI script that processes text input requests
        ),
        'skiphours' => '',          // Hours to skip update: int 0 to 23
        'skipdays' => '',           // Days to skip update: Monday, Tuesday, Wednesday, ...
        'items' => array(),         // THESE ARE THE ITEMS OF THE CHANNEL
    );

    /**
     * That's how the item specfication looks like:
     * @staticvar array
     */
    private static $itemTemplate = array(
        'title' => '',              // Title of the Item
        'link' => '',               // Link to the related HTML page
        'description' => '',        // Text description
        'author' => '',             // Author of the item
        'category' => '',           // Item category
        'comments' => '',           // URI to item comments
        'source' => '',             // RSS channel where the content of this item is taken from
        'enclosure' => array(       // Attached media data (array contents are XML attributes)
            'url' => '',            // Link to the resource
            'type' => '',           // MIME type
            'length' => ''          // Content length in bytes
        ),
        'guid' => '',               // Global unique identifier
        'pubDate' => '',            // RFC822 time when the item was published
    );

    /**
     * The text encoding of the feed
     * @var string
     */
    private $encoding = "UTF-8";

    /**
     * The channel data retrieved from the database or other sources
     * @var array
     */
    private $data = array();    

    /**
     * Items, extracted form the data array returned by onLoad.
     * @var array
     */
    public $items = array();

    /**
     * Loads the data which are necessary to render an RSS feed.
     * OVERLOAD THIS FUNCTION TO CUSTOMIZE YOUR RSS CHANNELS.
     * Returns a reference to the result array, which must contain the
     * keys "title", "description" and "item"
     *
     * @param mixed $identifier
     * @param int $numOfItems
     * @return array
     */
    protected function onLoad($identifier='', $numOfItems=0) {
        return array();
    }

    /**
     * XML text escaping
     * @param string $text
     * @return string
     */
    private static function xmlEscape($text) {
        if(empty($text) || trim($text) == '') { // !empty for array()/null
            return '';
        } else if(!preg_match('/([^\x01-\x7f]|[&<>])/', $text)) {
            return $text;
        } if(strpos($text, ']]>') === false) {
            return "<![CDATA[$text]]>";
        } else {
            return str_replace(array('&','"',"'",'<','>'), array('&amp;','&quot;','&apos;','&lt;','&gt;'), $text);
        }
    }

    /**
     * Constructor
     * @param string $encoding
     */
    public function __construct($encoding="UTF-8") {
        $this->encoding = $encoding;
    }

    /**
     * Data access get
     * @param string $name
     * @return mixed
     */
    public function & __get($name) {
        $name = strtolower($name);
        return isset($this->data[$name]) ? $this->data[$name] : '';
    }

    /**
     * Data access set
     * @param string $name
     * @param mixed $value
     */
    public function __set($name,  $value) {
        $name = strtolower($name);
        if(!isset($this->data[$name])) {
            throw new Exception('No such RSS property to set');
        } else {
            $this->data[$name] = $value;
        }
    }

    /**
     * Loads the data using the overloadable onLoad() method.
     * @param mixed $identifier
     * @param int $numOfItems
     */
    public function load($identifier='', $numOfItems=0) {
        $this->items = null;
        $this->data = array_change_key_case(array_merge(self::$dataTemplate, $this->onLoad($identifier, $numOfItems)), CASE_LOWER);
        $this->items = &$this->data['items'];
        unset($this->data['items']);

        if($this->title == '') {
            throw new Exception('RSS channel title must be specified');
        }

        if(empty($this->link)) {
            $this->link = "http://{$_SERVER['HTTP_HOST']}/";
        }

        foreach($this->items as $key => $item) {
            $this->items[$key] = array_change_key_case($item, CASE_LOWER);
            if(empty($item['title'])) {
                Tracer::trace("RSS item $key removed, no title");
                $this->items[$key] = false;
            } else if(empty($item['link'])) {
                Tracer::trace("RSS item $key ({$item['title']}) added channel link");
                $this->items[$key]['link'] = $this->link;
            }
        }

        if(count($this->items) == 0) {
            throw new Exception('No RSS channel items defined');
        }
    }

    /**
     * Renders the channel
     * @return string
     */
    public function renderChannel($numOfItems=0) {
        $o = " <channel>\n";

        foreach($this->data as $tag => $value) {
            if(!empty($value) || $tag == 'title' || $tag == 'description' || $tag == 'link') {
                if(!is_array($value)) {
                    $o .= "  <$tag>" . self::xmlEscape($value) . "</$tag>\n";
                } else if(strlen(trim(implode('', $value), " \n\r\t")) > 0) {
                    $o .= " <$tag>";
                    foreach($value as $itag => $ivalue) {
                        $o .= "   <$itag>" . self::xmlEscape($ivalue) . "</$itag>\n";
                    }
                    $o .= " </$tag>";
                }
            }
        }

        $count = 0;
        foreach($this->items as $item) {
            if($numOfItems > 0 && ++$count > $numOfItems) {
                break;
            } else {
                $o .= "  <item>\n";
                foreach($item as $tag => $value) {
                    if(!is_array($value)) {
                        $o .= "   <$tag>" . self::xmlEscape($value) . "</$tag>\n";
                    } else if(strlen(trim(implode('', $value), " \n\r\t")) > 0) {
                        if($tag == 'enclosure') {
                            $o .= "<enclosure url=\"{$value['url']}\" length=\"{$value['length']}\" type=\"{$value['type']}\" />";
                        } else if(!empty($value) || $tag == 'title' || $tag == 'description' || $tag == 'link') {
                            $o .= "  <$tag>";
                            foreach($value as $itag => $ivalue) {
                                $o .= "    <$itag>" . self::xmlEscape($ivalue) . "</$itag>\n";
                            }
                            $o .= "  </$tag>\n";
                        }
                    }
                }
                $o .= "  </item>\n";
            }
        }
        $o .= " </channel>\n";
        return $o;
    }

    /**
     * Renders a list of channels in one feed
     * @param array $channelIdentifiers
     * @param int $numOfItems
     * @return string
     */
    public function renderFeed(array $channelIdentifiers=array(), $numOfItems=0) {
        $o =  '<?xml version="1.0" encoding="' . $this->encoding . '" ?>'
            . "\n" . '<rss version="2.0"'
            . "\n" . '  xmlns:content="http://purl.org/rss/1.0/modules/content/"'
            . "\n" . '  xmlns:wfw="http://wellformedweb.org/CommentAPI/"'
            . "\n" . '  xmlns:dc="http://purl.org/dc/elements/1.1/"'
            . "\n" . '  xmlns:atom="http://www.w3.org/2005/Atom"'
            . "\n" . '  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"'
            . "\n" . '  xmlns:slash="http://purl.org/rss/1.0/modules/slash/"'
            . "\n" . "  >\n";
        foreach($channelIdentifiers as $identifier) {
            try {
                $this->load($identifier, $numOfItems);
                $o .= $this->renderChannel($numOfItems);
            } catch(Exception $e) {
                Tracer::traceException($e);
            }
        }
        $o .= '</rss>';
        return $o;
    }
}
?>
Posted in PHP | Tagged , , | Comments Off

User verification image class

Um Bots davon abzuhalten, Online-Formulare abzuschicken oder sich automatisch zu registrieren, werden mit den Formularen üblicherweise verrauschte Bilder mitgeschickt, welche einen zufälligen Code aus Zahlen und Buchstaben enthalten. Der Nutzer muss diesen Code entziffern und eingeben. Mit dieser Klasse kann diese Technik auf einfache Weise angewandt werden:

Anwendungsbeispiel

<?php
// Before doing anything else, we include the library
include_once('swlib/swlib.class.php');

// Start the library with configuration
swlib::start(array(
    // path to library var directory, we use an one one
    // The var path must be writable for the server, it is NOT the
    // directory "/var" on Unix-Like systems, but a directory to serialize
    // "variables" in.
    'var_path' => sys_get_temp_dir(),
));

// This is the entry key we use for the $_SESSION for this sample code
$sessionKey = 'swlib-verification-image';

if(isset($_GET['img'])) {
    //
    // This section sends the imgage (we use the same script for sending HTML
    // and the binary image.
    //
    Tracer::disable();
    OutputBuffer::purge();

    // Check referrer and valid session
    if(isset($_SERVER['HTTP_REFERER']) && strpos(strtolower($_SERVER['HTTP_REFERER']),
            strtolower($_SERVER['HTTP_HOST'])) !== false
            && isset($_SESSION[$sessionKey])
            && is_array($_SESSION[$sessionKey])
            && isset($_SESSION[$sessionKey]['text'])
            && strlen(trim($_SESSION[$sessionKey]['text'])) > 0
    ){
        // Retrieve the data saved in the session and set the corresponding
        // instance properties
        $cfg = &$_SESSION[$sessionKey];
        $vi = new UserVerificationImage(trim($cfg['text']));
        if(isset($cfg['width'])) try { $vi->setWidth($cfg['width']); } catch(Exception $e) { ; }
        if(isset($cfg['height'])) try { $vi->setHeight($cfg['height']); } catch(Exception $e) { ; }
        if(isset($cfg['noise'])) try { $vi->setNoise($cfg['noise']); } catch(Exception $e) { ; }

        // Finally send the image as binay code to the browser and exit.
        $vi->sendImage();
    }

    /*
     *
     * Alternatively, you can use the static function to create an image
     * and save the corresponding SHA1 hash code in a cookie:
     *
     *      UserVerificationImage::sendValidationImageAndExit();
     *
     *  This method is much easier to use, but less configurable.
     *
     */

    // exit if the $vi->sendImage() method did not do this yet.
    exit();
}

////////////////////////////////////////////////////////////////////////////////
// This section is the normal HTML script:
//

// Create the instance
$vi = new UserVerificationImage();

// Check if the user has entered the code
if(isset($_GET['input'])) {

    try {
        // Verify the code saved in the session with the entered code.
        $vi->setText($_SESSION[$sessionKey]['text']);
        if($vi->verifyInput($_GET['input'])) {
            $inputCorrect = "Input is correct";
        } else {
            $inputCorrect = "Input is not correct";
        }
    } catch(Exception $e) {
        $inputCorrect = "Exception:" . $e->getMessage();
    }

    /*
     * Alternatively:
     *
     *  if(UserVerificationImage::verifyValidationImage($_GET['input'])) {
     *      $inputCorrect = "Input is correct";
     *  } else {
     *      $inputCorrect = "Input is not correct";
     *  }
     *
     * Use this static function to verify images sent using the function
     * UserVerificationImage::sendValidationImageAndExit();
     */
}

// Set a new random code with 8 characters from the character list
$vi->setRandomText(8, 'ABCDEFGHIJ0123456789klmnopqr?-');

// Write the session for the image lookup
$_SESSION[$sessionKey] = array('text' => $vi->getText());

// And print it all ...
$text = $vi->getText();

print <<<HERE
    <html><body>
    <style>
        body { font-family: monospace; }
        table { border-collapse:collapse; border: solid 1px black; padding: 2px; }
        td { border: solid 1px black; padding: 5px; }
    </style>
        <table>
            <tr><td>Original text</td><td>$text</td></tr>
            <tr><td>Image</td><td><img src="{$_SERVER['PHP_SELF']}?img" /></td></tr>
            <tr><td>Enter here</td><td>
            <form method="GET" action="{$_SERVER['PHP_SELF']}">
                <input type="text" name="input" value="" />
                <input type="submit" name="validate" value="validate">
            </form>
            </td></tr>
            <tr><td>Last value was ok:</td><td>$inputCorrect</td></tr>
        </table>
    </body></html>
HERE;
?>

Ausgabe

Die Ausgabe wird eine simple Tabelle sein, welche den zufällig generierten Code als Text und Bild enthält. Außerdem ist ein kleines Formular dabei, in dem der Code eingegeben werden kann. Im Feld darunter wird angezeigt, ob die Eingabe richtig war.

Klassen-Quelltext

<?php
/**
 * Creates a verification image for human user verification.
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2010
 * @license GPL
 * @version 1.0
 */
class UserVerificationImage {

    /**
     * Class configuration
     * @var array
     */
    private static $config = array(
    );

    /**
     * The width of the image, 0=auto
     * @var int
     */
    private $width = 0;

    /**
     * The height of the image
     * @var int
     */
    private $height = 15;

    /**
     * The text of the image.
     * @var string
     */
    private $text = '';

    /**
     * Noise factor
     * @var double
     */
    private $noise = 0.1;

    /**
     * Sends a verification image to the browser (including a SHA1 hash cookie).
     * The function ::verifyValidationImage() can be used to validate the user
     * input generated in this function.
     * @return void
     * @param bool $saveInSession=false
     * @param int $textLength
     */
    public static function sendValidationImageAndExit($saveInSession=true, $textLength=8) {
        Tracer::disable();
        OutputBuffer::purge();
        if(isset($_SERVER['HTTP_REFERER']) && strpos(strtolower($_SERVER['HTTP_REFERER']), strtolower($_SERVER['HTTP_HOST'])) !== false) {
            try {
                $vi = new self();
                if(isset($_SESSION[$k]['width'])) $vi->setWidth($_SESSION[$k]['width']);
                if(isset($_SESSION[$k]['height'])) $vi->setHeight($_SESSION[$k]['height']);
                if(isset($_SESSION[$k]['noise'])) $vi->setNoise($_SESSION[$k]['noise']);
                $vi->setRandomText($textLength);
                setcookie("swlibvimage", sha1(strtoupper($vi->getText())), time() + 30);
                $vi->sendImage();
                $key = strtolower(__CLASS__);
                if($saveInSession) {
                    $_SESSION[$key] = $vi->getText();
                } else if(isset($_SESSION[$key])) {
                    unset($_SESSION[$key]);
                }
            } catch(Exception $e) {
                // Send error image?
            }
        }
        exit();
    }

    /**
     * Hard-validates the text of a validation image that the user has entered.
     * There is no tolerance according to O<-->0 etc. The function uses cookies.
     * @param string $input
     * @return book
     */
    public static function verifyValidationImage($input) {
        if(!isset($_COOKIE['swlibvimage']) || empty($_COOKIE['swlibvimage'])) {
            Tracer::trace("No cookie set (swlibvimage)");
            return false;
        } else if($_COOKIE['swlibvimage'] == sha1(strtoupper(trim($input)))) {
            Tracer::trace("Cookie validation OK (swlibvimage)");
            return true;
        } else {
            $key = strtolower(__CLASS__);
            if(isset($_SESSION[$key])) {
                $vi = new self();
                $vi->setText($_SESSION[$key]);
                unset($_SESSION[$key]);
                return $vi->verifyInput($input);
                Tracer::trace("Session saved vcode is: $text");
            } else {
                Tracer::trace("No alternative session key saved ($key)");
            }
        }
        return false;
    }

    /**
     * Constructor
     * @param string $text
     */
    public function __construct($text='') {
        if(!empty($text)) $this->setText(trim($text));
    }

    /**
     * Returns the image width
     * @return int
     */
    public function getWidth() {
        return $this->width;
    }

    /**
     * Sets the image width
     * @param int $width
     */
    public function setWidth($width) {
        if(!is_numeric($width)) {
            throw new Exception("Invalid image width: '$width'" );
        } else {
            $width = intval($width);
            if($width < 10 || $width > 500) $width = 0;
            $this->width = $width;
        }
    }

    /**
     * Returns the image height
     * @return int
     */
    public function getHeight() {
        return $this->height;
    }

    /**
     * Sets the image height
     * @param int $height
     */
    public function setHeight($height) {
        if(!is_numeric($height)) {
            throw new Exception("Invalid image height: '$height'" );
        } else if(intval($height) < 20) {
            throw new Exception("Invalid image height: '$height' (min width is 20)" );
        } else {
            $this->height = intval($height);
        }
    }

    /**
     * Returns the image text
     * @return string
     */
    public function getText() {
        if(empty($this->text)) {
            $this->setRandomText();
        }
        return $this->text;
    }

    /**
     * Sets the text to display
     * @param string $text
     */
    public function setText($text) {
        if(!is_scalar($text)) {
            throw new Exception("Invalid image text (must be a scalar value)");
        } else if(empty($text) || strlen(trim($text)) == 0) {
            $this->text = '';
        } else {
            $this->text = strval($text);
        }
    }

    /**
     * Returns the noise factor
     * @return double
     */
    public function getNoise() {
        return $this->noise;
    }

    /**
     * Sets the noise factor
     * @param double $noise
     */
    public function setNoise($noise) {
        if(!is_numeric($noise)) {
            throw new Exception('Invalid image noise (not numeric)');
        } else if($noise > 1 || $noise < 0) {
            throw new Exception('Invalid image noise (must be between 0 and 1)');
        } else {
            $this->noise = $noise;
        }
    }

    /**
     * Sets a random text
     * @param int $size
     * @param string charset
     */
    public function setRandomText($size=8, $charset='abcdefghkmnpqrtwxyzABCDEFGHIJKLMNPRTUVWXYZ2346789') {
        $charset = preg_replace('/[\s]/', '', $charset);
        if(!is_numeric($size)) {
            throw new Exception('Invalid random text size ("' . $size . '")');
        } else if(empty($charset)) {
            throw new Exception('Invalid character set for random text (empty string)');
        } else {
            $l = strlen($charset)-1;
            $o = '';
            while(strlen($o) < $size) {
                $o .= substr($charset, rand(0, $l), 1);
            }
            $this->text = $o;
            return $o;
        }
    }

    /**
     * Generates the png image, returns the local file path
     * @return string
     */
    public function generateImage() {
        $yPositionRange = 2;
        $fontSize = $this->getHeight() - (2 * $yPositionRange);
        $charWidth = imagefontwidth($fontSize) + 3;
        $charHeight = imagefontheight($fontSize);
        $textWidth = $charWidth * strlen($this->getText());
        if($this->getWidth() == 0) $this->width = $textWidth + (2 * $yPositionRange);
        $this->width = 10 * ceil($this->width / 10);
        $noisePixels = $this->getWidth() * $this->getHeight() * $this->getNoise();
        $x0 = ceil(($this->getWidth() - $textWidth) / 2);
        $y0 = ceil(($this->getHeight() - $charHeight) / 2);

        $img = imagecreatetruecolor($this->getWidth(), $this->getHeight());
        imagefill($img, 0, 0, 0xffffff);

        // Noise
        $w = $this->getWidth()-1;
        $h = $this->getHeight()-1;
        for($i=0; $i<$noisePixels; $i++) {
            $c = 0x000000;
            imagesetpixel($img, rand(0, $w), rand(0, $h), $c);
        }

        $text = $this->getText();
        for($i=0; $i<strlen($text); $i++) {
            $ch = substr($text, $i, 1);
            imagestring($img, $fontSize, $x0+$i*$charWidth, $y0, $ch, 0x000000);
        }

        for($i=floor($noisePixels); $i<$noisePixels; $i++) {
            $c = 0xffffff;
            imagesetpixel($img, rand(0, $w), rand(0, $h), $c);
        }

        $file = FileSystem::getTempFileName();
        imagepng($img, $file);
        imagedestroy($img);
        return $file;
    }

    /**
     * Sends an image as PNG, flushes the output buffer.
     */
    public function sendImage() {
        $file = $this->generateImage();
        if(!empty($file)) {
            header('Content-type: image/png');
            header('Content-Length: ' . FileSystem::getFileSize($file));
            header('Cache-Control:no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
            header('Pragma:no-cache');
            header('Connection:Close');
            Tracer::disable();
            OutputBuffer::purge();
            try { print FileSystem::readFile($file); } catch(Exception $e) { ; }
            try { FileSystem::delete($file);  } catch(Exception $e) { ; }
        }
    }

    /**
     * Checks the input against the $text instance variable. One spelling
     * error is accepted for characters like O<-->0 and S<-->5.
     * @param string $input
     * @return bool
     */
    public function verifyInput($input) {
        $input = strtoupper(trim($input, " \n\r\t"));
        $text  = strtoupper(trim($this->getText(), " \n\r\t"));
        if(empty($input)) {
            Tracer::trace('Verification failed: No input');
            return false;
        } else if(strlen($input) != strlen($text)) {
            Tracer::trace('Verification failed: Text lengths differ');
            return false;
        } else if($input == $text) {
            Tracer::trace('Verification OK: Texts are equal');
            return true;
        } else if(levenshtein($input, $text) > 1) {
            Tracer::trace('Verification failed: More than one character difference');
            return false;
        } else {
            $t1 = $text;
            $t2 = $input;
            $t1 = str_replace(array('0','Q','5'), array('O','O','S'), $t1);
            $t2 = str_replace(array('0','Q','5'), array('O','O','S'), $t2);
            if($t1 == $t2) {
                Tracer::trace('Verification OK, after compensation of optical redundant characters');
                return true;
            } else {
                Tracer::trace('Verification failed: Even after correction of optical redundant characters');
                return false;
            }
        }
        return false;
    }

}
?>
Posted in PHP | Tagged , , | Comments Off

Version checking and publishing class

Versionsprüfungen und Auto-Updates finden mehr und mehr in PHP-Applikationen und CMS Anwendung. Die hier vorgestellte Klasse beinhaltet Methoden und statische Funktionen, um PHP-Dateien nach (Java-Style) Dokumentationskommentaren von Klassen/Interface-Definitionen zu durchsuchen und diese in eine übersichtliche Array-Struktur zu bringen. Weiterhin werden MD5 und SHA1 Checksummen über alle Dateien gebildet. Auch über die Quelltexte ohne Kommentare werden Checksummen gebildet, somit kann festgestellt werden, ob sich nur in der Dokumentation etwas geändert hat und ein Update nicht zwingend notwendig ist.
Diese Features kommen in zwei statischen Methoden zum Einsatz: Versioning::checkModuleVersion() und Versioning::publishModule(). Mit Versioning::publishModule() kann ein Package/Module/Library-Verzeichnis in einem ZIP gepackt und in einem Zielpfad zum download bereitgestellt werden. Zusätzlich werden dieser Datei die Checksummen, das Readme (readme), die Lizenzdatei (license) und die Versionsdatei (version), sowie die Download-Checksummen (zum Prüfen der heruntergeladenen ZIP-Datei) hinzugefügt. Beispielsweise sind folgende Dateien im “Repository” der SwLibrary (URI http://www.atwillys.de/repository/swlib/) auf diesem Server erreichbar:

  • swlib.zip: Download-Archiv des Packages
  • swlib.zip.md5: MD5 Checksumme der ZIP-Datei
  • swlib.zip.sha1: SHA1 Checksumme der ZIP-Datei
  • swlib.checksum: Checksumme über alle erkannten Quelltexte im Archiv
  • swlib.version: Kopie der Datei “version”
  • swlib.license: Kopie der Datei “license”
  • swlib.readme: Kopie der Datei “readme”
  • swlib.content: Details der Inhalte im JSON-Format

Für andere Packages sind die Namen entsprechend anders. Diese Information werden von der Funktion Versioning::checkModuleVersion() verwendet, um mithilfe eines HTTP-lookup der Versionsdatei und der Checksumme zu prüfen, ob ein Update verfügbar ist. Falls ein Update vorliegt wird geprüft, was sich geändert hat und ob ein Update wirklich notwendig ist (oder nur die Dokumentation oder Whitespaces sich geändert haben).

Anwendungsbeispiel

<?php
    require_once("swlib/swlib.class.php");
    define('TRACER_DEFAULT_LEVEL', 5);
    swlib::start(array(
        'var_path' => sys_get_temp_dir(),
        'tmp_path' => sys_get_temp_dir(),
    ));
}

print '<html><body><pre>';

// Instantiate the object
$ver = new Versioning();

// Get all files in library path of the SwLibrary (we use the swlib as example,
// there are plenty of classes in this path :)
$files = FileSystem::find(swlib::getLibPath());
// ... But we use only the first three files - it's the same for all other files
$files = array($files[0], $files[1], $files[2]);

// Scan all the files that you can scan
$ver->readFiles($files, swlib::getLibPath());

// Print the results we obtained:
print 'Classes = ' . print_r($ver->getClasses(), true) . "\n";
print 'SHA1 checksums = ' . print_r($ver->getFileSha1s(), true) . "\n";
print 'MD5 checksums = ' . print_r($ver->getFileMd5s(), true) . "\n";
print 'Stripped code checksums MD5) = ' . print_r($ver->getSourceCodeMd5s(), true) . "\n";
print 'File details = ' . print_r($ver->getFileDetails(), true) . "\n\n<hr><\n>";

// Example for publishing a package:
Versioning::publishModule('swlib', swlib::getLibPath(), dirname(__FILE__) . '/swlib');

// Check a version of a package
print_r(Versioning::checkModuleVersion('swlib', swlib::getLibPath(), 'http://htx.my/htx/tmp'));

// This is the function provided in the swlib core class, which uses this class.
print_r(swlib::checkForUpdates());

print '</pre></body></html>';
?>

Ausgabe

Classes = Array (
    [arrayfilter] => 1.0
    [cache] => 0.1
    [eexception] => 1.0
)

SHA1 checksums = Array (
    [ArrayFilter.class.php] => d313140fe1fd6cceccbe6e15185d999436cbcb49
    [Cache.class.php] => 0019328b74c32588bee35a8a0a2f837e95581f97
    [EException.class.php] => 5cd19b2abf98121ed59b15b934b5f458e2c0cbe6
)

MD5 checksums = Array (
    [ArrayFilter.class.php] => c1ea4599c43538fd84dbd7475a7a6789
    [Cache.class.php] => e532897b6b92e1ae6f89da12a3236790
    [EException.class.php] => 5ea9311e0cffb3fd1a0c6ffd941d8838
)

Stripped code checksums MD5) = Array (
    [ArrayFilter.class.php] => 49d70bd2637838221feb0a6f130f3508
    [Cache.class.php] => 8515aef2fda110d63aac3ec3c6e44924
    [EException.class.php] => 971e89b8a1811240cafecabe97f214f2
)

File details = Array (
    [ArrayFilter.class.php] => Array (
            [file_sha1] => d313140fe1fd6cceccbe6e15185d999436cbcb49
            [file_md5] => c1ea4599c43538fd84dbd7475a7a6789
            [source_md5] => 49d70bd2637838221feb0a6f130f3508
            [classes] => Array (
                    [arrayfilter] => Array (
                            1 => Provides static functions for simple array filtering and finding tasks.
                            [package] => de.atwillys.sw.php.swLib
                            [author] => Stefan Wilhelm
                            [copyright] => Stefan Wilhelm, 2007-2010
                            [license] => GPL
                            [version] => 1.0
                            [type] => class
                            [name] => ArrayFilter
                        )
                )
        )

    [Cache.class.php] => Array (
            [file_sha1] => 0019328b74c32588bee35a8a0a2f837e95581f97
            [file_md5] => e532897b6b92e1ae6f89da12a3236790
            [source_md5] => 8515aef2fda110d63aac3ec3c6e44924
            [classes] => Array (
                    [cache] => Array (
                            1 => Cache management class with GZ compression and uncompress caching of
contents and files. Package caches are stored in sub directories. Each
resource is defined by the SHA1 checksum.
NOTE: THIS CLASS IS BEING DEVELOPED
                            [package] => de.atwillys.sw.php.swLib
                            [author] => Stefan Wilhelm
                            [copyright] => Stefan Wilhelm, 2010
                            [license] => GPL
                            [version] => 0.1
                            [type] => class
                            [name] => Cache
                        )
                )
        )

    [EException.class.php] => Array (
            [file_sha1] => 5cd19b2abf98121ed59b15b934b5f458e2c0cbe6
            [file_md5] => 5ea9311e0cffb3fd1a0c6ffd941d8838
            [source_md5] => 971e89b8a1811240cafecabe97f214f2
            [classes] => Array(
                    [eexception] => Array(
                            1 => Implements global error, assertion and exception handling. Errors, warnings
and messages are categorized and either thrown as exception or only traced
(e.g. warnings, messagses). A global exception handler catches uncaught
exceptions, traces the details and prints a HTML error text (without details,
as a MySqlException('You have an error near SELECT * form users where password=...')
is nothing to be seen by the user. Assertions are only traced.
                            [package] => de.atwillys.sw.php.swLib
                            [author] => Stefan Wilhelm
                            [copyright] => Stefan Wilhelm, 2006-2010
                            [license] => GPL
                            [version] => 1.0
                            [uses] => Array (
                                    [exception] => Exception
                                    [(optional) tracer] => (optional) Tracer
                                )
                            [type] => class
                            [name] => EException
                            [extends] => Exception
                        )
                )
        )
)

Klassen-Quelltext

<?php
/**
 * Library version checking and reporting class. Scans a package source
 * directory for files, builds SHA1 and MD5 checksums for of each file and
 * (if PHP) fetches additional information about documented classes and
 * interfaces. The static function publishModule() allows to zip a package
 * source directory and generate info files containing the checksums, details,
 * version, readme and license and publish all this in a reporitory folder
 * (which has to be writable). The function checkModuleVersion() performs a
 * HTTP lookup to a repository generated by the function publishModule(),
 * compares version, checksum and details and reports a recommendation if the
 * the package has to be updated or not.
 *
 * @package de.atwillys.sw.php.swLib
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2010
 * @license GPL
 * @version 1.0
 */
final class Versioning {

    /**
     * Contains the SHA1 checksum over all scanned PHP code files.
     * @var string
     */
    private $checksum = '';

    /**
     * Contains the versions classes and interfaces that were found in the files.
     * This array is an associative array, where the keys are the lower case
     * classes and the values the version strings of these classes/interfaces.
     * The versions are determined using the @version entry.
     * @var array
     */
    private $versions = array();

    /**
     * Contains the files that were checked
     * @var array
     */
    private $files = array();

    /**
     * Contains the exceptions
     * @var array
     */
    private $exceptions = array();

    /**
     * Constructor
     */
    public final function __construct() {
    }

    /**
     * Destructor
     */
    public final function __destruct() {
    }

    /**
     * Returns the files which were searched
     * @return array
     */
    public final function getFileDetails() {
        return $this->files;
    }

    /**
     * Returns the classes that were found in the searched files
     * @return array
     */
    public final function getClasses() {
        return $this->versions;
    }

    /**
     * Returns the SHA1 checksum of all scanned code files (*.php).
     * @return string
     */
    public final function getChecksum() {
        return $this->checksum;
    }

    /**
     * Returns the file-MD5 checksums of the scanned files as associative array,
     * where the keys are the file paths/sub-paths and the values the checksums.
     * @return array.
     */
    public final function getFileMd5s() {
        $cs = array();
        foreach($this->files as $file => $desc) {
            $cs[$file] = $desc['file_md5'];
        }
        return $cs;
    }

    /**
     * Returns the file-SHA1 checksums of the scanned files as associative array,
     * where the keys are the file paths/sub-paths and the values the checksums.
     * @return array.
     */
    public final function getFileSha1s() {
        $cs = array();
        foreach($this->files as $file => $desc) {
            $cs[$file] = $desc['file_sha1'];
        }
        return $cs;
    }

    /**
     * Returns the MD5 checksums of the whitespace-strupped codes of the scanned
     * files as associative array, where the keys are the file paths/sub-paths
     * and the values the checksums.
     * @return array.
     */
    public final function getSourceCodeMd5s() {
        $cs = array();
        foreach($this->files as $file => $desc) {
            $cs[$file] = $desc['source_md5'];
        }
        return $cs;
    }

    /**
     * Returns the information about a selected class.
     * @return array
     */
    public final function getClass($class) {
        return isset($this->versions[$class]) ? $this->versions[$class] : array();
    }

    /**
     * Returns information about classes and interfaces in an assoc. array.
     * The class names (lower case) are saved in the array keys, the array
     * values are the information about the class:
     * array(
     *  'type' =>   Can be 'class' or 'interface'
     *  'name' =>   The name of the class or interface
     *  <OTHER> =>  Dynamic entries dependent on the @-comments (like @author or
     *              @final etc). These values are automatically extended from
     *              the class/interface definition line, e.g:
     *
     *                  final class B extends A implements I1, I2
     *
     *              would generate automatically without special comments:
     *
     *              array (
     *                  'type' => 'class'
     *                  'name' => 'B'
     *                  'final' => true
     *                  'extends' => 'A'
     *                  'implements' => array('i1'=>'I1', 'i2'=>'I2')
     *              )
     * )
     * @param string $source
     * @return array
     */
    public final function getPhpClassDocumentations($source) {
        // Conditionize
        $source = str_replace(array("\r", "\t"), array("\n", ' '), $source);
        while(strpos($source, "\n\n") !== false) {
            $source = str_replace("\n\n", "\n", $source);
        }

        // Parse the source
        $DOC_START = '/**';
        $DOC_END = '*/';
        $documented = array();
        $p0 = strpos($source, $DOC_START);
        while($p0 !== false) {
            $p1 = strpos($source, $DOC_END, $p0+1);
            if($p1 <= $p0) break; // on not found
            $doc = substr($source, $p0+strlen($DOC_START), $p1-$p0-strlen($DOC_START)-strlen($DOC_END));
            $doc = trim(preg_replace('/[\n\s]*\*([^\n]*)[\n]?/i', "$1\n", $doc), "\n ");
            $p2 = $p1 + strlen($DOC_END);
            while($p2 < strlen($source) && ctype_space(substr($source, $p2, 1))) $p2++;
            if($p2 >= strlen($source)) break; // on end of text
            $p3 = strpos($source, "\n", $p2+1);
            if($p3 <= $p2) break; // on not found
            $ref = trim(substr($source, $p2, $p3-$p2+1), "\n ");
            $documented[] = array(
                'for' => $ref,
                'doc' => $doc
            );
            $p0 = strpos($source, $DOC_START, $p1+1);
        }

        // Filter information
        $docs = array();

        foreach($documented as $key => $entry) {
            $exp = array('text' => '');

            // Parse the comment
            $doc = explode("\n", str_replace("\r", "\n", $entry['doc']));
            foreach($doc as $line) {
                $line = trim($line, "\t\n\r ");
                if(!empty($line)) {
                    if(strpos($line, '@') === 0) {
                        $line = explode(' ', $line, 2);
                        $docKey = trim(strtolower(reset($line)), '@ ');
                        $docValue = count($line) > 1 ? trim(end($line)) : '';
                        switch($docKey) {
                            case 'uses':
                                if(!isset($exp[$docKey])) {
                                    $exp[$docKey] = array();
                                }
                                $exp[$docKey][strtolower($docValue)] = $docValue;
                                break;
                            case 'see':
                            case 'todo':
                                if(!isset($exp[$docKey])) {
                                    $exp[$docKey] = array();
                                }
                                $exp[$docKey][] = $docValue;
                                break;
                            default:
                                $exp[$docKey] = $docValue;
                        }

                    } else {
                        $exp['text'] .= $line . "\n";
                    }
                }
            }
            $exp['text'] = trim($exp['text'], "\n\r\t ");

            // Parse the line
            $for = $entry['for'];
            if(strpos($for, '(') > 1) {
                $for = substr($for, 0, strpos($for, '('));
            }
            $check = preg_split('/[\W]+/i', trim(strtolower($for), " ;{"), -1, PREG_SPLIT_NO_EMPTY);
            $for = preg_split('/[\W]+/i', trim($for, " ;{"), -1, PREG_SPLIT_NO_EMPTY);

            if(in_array('interface', $check)) {
                $exp['type'] = 'interface';
                $exp['name'] = end($for); // overwrite @name if specified
            } else if(in_array('class', $check)) {
                $exp['type'] = 'class';
                while(!empty($for)) {
                    $desc = strtolower(array_shift($for));
                    switch($desc) {
                        case 'abstract':
                        case 'final':
                            $exp[strtolower($desc)] = true;
                            break;
                        case 'class';
                            $exp['name'] = array_shift($for); // overwrite @name if specified
                            break;
                        case 'extends':
                            $exp['extends'] = array_shift($for);
                            break;
                        case 'implements':
                            $exp['implements'] = array();
                            while(!empty($for)) {
                                $desc = trim(array_shift($for), ' ,');
                                $exp['implements'][strtolower($desc)] = $desc;
                            }
                            break;
                        default:
                            Tracer::trace("Unrecognized descriptor: $desc");
                    }
                }

            }

            // Add
            if(isset($exp['name'])) {
                $docs[strtolower($exp['name'])] = $exp;
            }
        }
        return $docs;
    }

    /**
     * Searches the files for documented classes and extracts information from
     * the documentation. Undocumented classes will be ignored as they cannot
     * be explicitly versioned. The method generates md5 and sha1 checksums for
     * all file contents, where the documentations are ignored (as they do not
     * influence the functionality.
     * @param array $files
     * @param string $rootDirectory
     */
    public final function readFiles(array $files, $rootDirectory='') {
        $rootDirectory = trim(rtrim($rootDirectory, ' /'));
        $rootDirectory = rtrim($rootDirectory != '' ? $rootDirectory : $_SERVER['DOCUMENT_ROOT'], ' /') . '/';

        $this->files = array();
        $this->versions =  array();
        $this->exceptions = array();
        $this->checksum = '';

        foreach($files as $file) {
            $this->files[str_replace($rootDirectory, '', $file)] = array();
            $f = &$this->files[str_replace($rootDirectory, '', $file)];

            if(FileSystem::isFile($file) && FileSystem::isReadable($file)) {
                $f['file_sha1'] = sha1_file($file);
                $f['file_md5'] = md5_file($file);
                $f['source_md5'] = md5(@php_strip_whitespace($file));
                $f['classes'] = array();
                $this->checksum .= basename($file) . $f['file_sha1'];
                try {
                    if(FileSystem::getExtension($file) == 'php') {
                        $f['classes'] = $this->getPhpClassDocumentations(FileSystem::readFile($file));
                        foreach($f['classes'] as $class => $desc) {
                            $this->versions[$class] = isset($desc['version']) ? $desc['version'] : '';
                        }
                    }
                } catch(Exception $e) {
                    $this->exceptions[$file] = $e->getMessage();
                }
            } else if(!FileSystem::isFile($file)) {
                $this->exceptions[$file] = 'File does not exist';
            } else {
                $this->exceptions[$file] = 'File is not readable';
            }
        }
        $this->checksum = sha1($this->checksum);
    }

    /**
     * Returns information about version, readme, license, main checksum, file
     * checksums, and file and class details.
     * @param string $sourceDirectory
     * @return array
     */
    public static final function readLocalModuleVersion($sourceDirectory) {
        $sourceDirectory = trim(rtrim($sourceDirectory, ' /'));
        if(!FileSystem::isDirectory($sourceDirectory)) {
            throw new Exception('Source directory read does not exist: ' . $sourceDirectory);
        } else {
            $ver = new self();
            $ver->readFiles(FileSystem::find($sourceDirectory), $sourceDirectory);
            $return = array(
                'version' => '0.0',
                'readme' => '',
                'license' => '',
                'checksum' => $ver->getChecksum(),
                'details' => $ver->getFileDetails()
            );

            if(FileSystem::isFile("$sourceDirectory/version")) {
                $return['version'] = trim(FileSystem::readFile("$sourceDirectory/version"), "\n\r\t ");
            } else if(FileSystem::isFile("$sourceDirectory/VERSION")) {
                $return['version'] = trim(FileSystem::readFile("$sourceDirectory/VERSION"), "\n\r\t ");
            }
            if(FileSystem::isFile("$sourceDirectory/readme")) {
                $return['readme'] = trim(FileSystem::readFile("$sourceDirectory/readme"), "\n\r");
            } else if(FileSystem::isFile("$sourceDirectory/README")) {
                $return['readme'] = trim(FileSystem::readFile("$sourceDirectory/README"), "\n\r");
            }
            if(FileSystem::isFile("$sourceDirectory/license")) {
                $return['license'] = trim(FileSystem::readFile("$sourceDirectory/license"), "\n\r");
            } else if(FileSystem::isFile("$sourceDirectory/LICENSE")) {
                $return['license'] = trim(FileSystem::readFile("$sourceDirectory/LICENSE"), "\n\r");
            }
        }
        return $return;
    }

    /**
     * Creates a zip file containing all files in the source directory. The zip
     * has the name $moduleName.zip. Additionally
     *      -   a file $moduleName.version is created, which contains the main
     *          SHA1 checksum over all single files checksums
     *      -   a file $moduleName.content is written, which contains details
     *          about documented PHP classes in JSON format
     *      -   a file $moduleName.zip.sha1 is written, which contains the SHA1
     *          checksum of the zip file
     *      -   a file $moduleName.zip.md5 is written, which contains the MD5
     *          checksum of the zip file
     *
     * @param <type> $moduleName
     * @param <type> $sourceDirectory
     * @param <type> $publicDirectory
     */
    public static final function publishModule($moduleName, $sourceDirectory, $publicDirectory) {
        $sourceDirectory = trim(rtrim($sourceDirectory, ' /'));
        $publicDirectory = trim(rtrim($publicDirectory, ' /'));
        $moduleName = strtolower(trim($moduleName));
        if(empty($moduleName) || strpos($moduleName, '/') !== false || strpos($moduleName, ' ') !== false) {
            throw new Exception('Module name is invalid: "' . $moduleName . '"');
        } else if(empty($sourceDirectory)) {
            throw new Exception('No source directory specified to publish');
        } else if(!FileSystem::isDirectory($sourceDirectory)) {
            throw new Exception('Source directory to publish does not exist: ' . $sourceDirectory);
        } else if(empty($publicDirectory)) {
            throw new Exception('No public directory specified to publish');
        } else if(!FileSystem::isDirectory($publicDirectory)) {
            throw new Exception('No such publishing directory: ' . $publicDirectory);
        } else if(!FileSystem::isWritable($publicDirectory)) {
            throw new Exception('Directory to publish in is not writable: ' . $publicDirectory);
        } else if(strpos($publicDirectory, $_SERVER['DOCUMENT_ROOT']) === false) {
            throw new Exception('Directory to publish in is not a sub-directory of this web server htdocs: ' . $publicDirectory);
        } else {
            $libzip  = $publicDirectory . '/' . $moduleName . '.zip';
            $chksum  = $publicDirectory . '/' . $moduleName . '.checksum';
            $version = $publicDirectory . '/' . $moduleName . '.version';
            $content = $publicDirectory . '/' . $moduleName . '.content';
            $license = $publicDirectory . '/' . $moduleName . '.license';
            $readme  = $publicDirectory . '/' . $moduleName . '.readme';

            // Remove actual published files
            if(FileSystem::isFile($libzip)) FileSystem::delete($libzip);
            if(FileSystem::isFile($chksum)) FileSystem::delete($chksum);
            if(FileSystem::isFile($content)) FileSystem::delete($content);
            if(FileSystem::isFile($version)) FileSystem::delete($version);
            if(FileSystem::isFile($license)) FileSystem::delete($license);
            if(FileSystem::isFile($readme)) FileSystem::delete($readme);

            // Write source folder contents as zip, checksum file, details and
            // zip file verification checksums (for auto updater)
            $ver = new self();
            $ver->readFiles(FileSystem::find($sourceDirectory), $sourceDirectory);
            ZipFile::compress($sourceDirectory, $libzip);
            FileSystem::writeFile("$libzip.sha1", sha1_file($libzip));
            FileSystem::writeFile("$libzip.md5", md5_file($libzip));
            FileSystem::writeFile($chksum, $ver->getChecksum());
            FileSystem::writeFile($content, json_encode($ver->getFileDetails()));

            // Copy info file contents
            if(FileSystem::isFile("$sourceDirectory/version")) {
                FileSystem::writeFile($version, FileSystem::readFile("$sourceDirectory/version"));
            } else if(FileSystem::isFile("$sourceDirectory/VERSION")) {
                FileSystem::writeFile($version, FileSystem::readFile("$sourceDirectory/VERSION"));
            }
            if(FileSystem::isFile("$sourceDirectory/license")) {
                FileSystem::writeFile($license, FileSystem::readFile("$sourceDirectory/license"));
            } else if(FileSystem::isFile("$sourceDirectory/LICENSE")) {
                FileSystem::writeFile($license, FileSystem::readFile("$sourceDirectory/LICENSE"));
            }
            if(FileSystem::isFile("$sourceDirectory/readme")) {
                FileSystem::writeFile($readme, FileSystem::readFile("$sourceDirectory/readme"));
            } else if(FileSystem::isFile("$sourceDirectory/README")) {
                FileSystem::writeFile($readme, FileSystem::readFile("$sourceDirectory/README"));
            }
        }
    }

    /**
     * Compares the version of a specified module with the version information
     * files on a rempte server ("repository") using HTTP lookup request. The
     * $sourceDirectory is the local folder containing the module source files,
     * the $referenceUri is the URL where the module is published (conform to the
     * Versioning::publishModule(...) method). Furthermore, a diff stat and
     * details about all documented files is generated, each for local and remote.
     * The following result types are possible:
     *
     *  (1) Versions are identical
     *  (2) Update available. (If new files are added at the remote server or only
     *      documentation/whitespace has changed, but not the algorighms
     *  (3) Update recommended. At least one file is different in the interpreted
     *      source code - which could be a security bug fix.
     *
     *  Results are presented in an associative array(
     *       'checked' => true if version was successfully checked
     *       'identical' => true if all files are identical
     *       'code_identical' => true if the local algorighms are up to date
     *       'text' => String representation of the result
     *       'diff' => Array containing all files (keys) information if they are
     *                 different
     *       'local' => array containing details about the local versions
     *       'remote' => array containing details about the remote versions
     *  )
     *
     * @param string $moduleName
     * @param string $sourceDirectory
     * @param string $referenceUri
     * @return array
     */
    public static final function checkModuleVersion($moduleName, $sourceDirectory, $referenceUri) {
        $moduleName = strtolower(trim($moduleName));
        $sourceDirectory = trim(rtrim($sourceDirectory, ' /'));
        $return = array(
            'checked' => false,
            'version_identical' => false,
            'update_recommended' => false,
            'update_available' => false,
            'new_features_available' => false,
            'local_untracked_features' => false,
            'identical' => false,
            'code_identical' => false,
            'text' => 'Checksum not checked: ',
            'diff' => array(),
            'local'  => array('checksum' => '', 'version' => '0.0', 'details' => array()),
            'remote' => array('checksum' => '', 'version' => '0.0', 'details' => array())
        );
        if(!FileSystem::isDirectory($sourceDirectory)) {
            throw new Exception("Local source directory does not exist: $sourceDirectory");
        } else if(empty($moduleName)) {
            throw new Exception("You did not specify a module name");
        } else {

            // Get the local statistics
            $return['local'] = self::readLocalModuleVersion($sourceDirectory);

            // Process checksum file
            $rq = new HttpRequest();
            if($rq->request("$referenceUri/$moduleName.checksum")->getResponseCode() != 200) {
                $return['text'] .= 'HTTP lookup failed: ' . "$referenceUri/$moduleName.checksum";
            } else {
                $return['remote']['checksum'] = trim($rq->getOutput(), "\n\r\t ");
                if($return['remote']['checksum'] == $return['local']['checksum']) {
                    $return['checked'] = true;
                    $return['identical'] = true;
                    $return['code_identical'] = true;
                    $return['version_identical'] = true;
                    $return['text'] = 'Versions are identical';
                    $return['remote']['details'] = $return['local']['details'];
                    $return['remote']['version'] = $return['local']['version'];
                } else if($rq->request("$referenceUri/$moduleName.content")->getResponseCode() != 200) {
                    $return['checked'] = true;
                    $return['identical'] = false;
                    $return['code_identical'] = false;
                    $return['text'] .= 'HTTP lookup failed:"' . "$referenceUri/$moduleName.content";
                } else {
                    $return['checked'] = true;
                    $return['identical'] = false;
                    $return['code_identical'] = false;
                    $return['text'] = 'Update available';
                    $return['remote']['details'] = @json_decode($rq->getOutput(), true);
                }
                unset($ver, $rq);

                if($return['checked']) {
                    // Build diff
                    $fkeys = array_merge(array_keys($return['local']['details']), array_keys($return['remote']['details']));
                    $files = array();
                    foreach($fkeys as $f) { $files[strtolower($f)] = $f; }
                    unset($fkeys);

                    $local = array_change_key_case($return['local']['details'], CASE_LOWER);
                    $remote = array_change_key_case($return['remote']['details'], CASE_LOWER);
                    $return['text'] = 'No update required';

                    if(!empty($remote) && !empty($local)) {
                        // Match local file against the remote files, shrink the
                        // arrays for each match
                        while(!empty($local)) {
                            reset($local);
                            $file = key($local);
                            $return['diff'][$files[$file]] = array(); // Use the original file name
                            $f = &$return['diff'][$files[$file]];
                            $f['local'] = true;
                            if(!isset($remote[$file])) {
                                $return['local_untracked_features'] = true;
                                $f['remote'] = false;
                                $f['identical'] = false;
                                $f['code_identical'] = false;
                                $f['text'] = 'Only local';
                            } else if($remote[$file]['file_sha1'] == $local[$file]['file_sha1']) {
                                $f['remote'] = true;
                                $f['identical'] = true;
                                $f['code_identical'] = true;
                                $f['text'] = 'Files are identical';
                            } else if($remote[$file]['source_md5'] == $local[$file]['source_md5']) {
                                $return['update_available'] = true;
                                $f['remote'] = true;
                                $f['identical'] = false;
                                $f['code_identical'] = true;
                                $f['text'] = 'Source codes are identical, but docs/whitespaces different';
                            } else {
                                $return['update_available'] = true;
                                $return['update_recommended'] = true;
                                $f['remote'] = true;
                                $f['identical'] = false;
                                $f['code_identical'] = false;
                                $f['text'] = 'Files are different';
                            }
                            unset($f); // unlink reference
                            array_shift($local); // implicit next local
                            if(isset($remote[$file])) unset($remote[$file]);
                        }

                        // All other files are only remote ("new files")
                        foreach($remote as $file => $desc) {
                            $return['update_available'] = true;
                            $return['new_features_available'] = true;
                            $return['diff'][$files[$file]] = array(
                                'local' => false,
                                'remote' => true,
                                'identical' => false,
                                'code_identical' => false,
                                'text' => 'Only remote'
                            );
                        }
                        unset($local, $remote, $f, $file, $desc);

                        // On updates check the remote version file
                        if($return['update_available']) {
                            // Get version file
                            $rq = new HttpRequest();
                            if($rq->request("$referenceUri/$moduleName.version")->getResponseCode() == 200) {
                                $return['remote']['version'] = trim($rq->getOutput(), "\n\r\t ");
                                if($return['remote']['version'] == $return['local']['version']) {
                                    $return['version_identical'] = true;
                                }
                            }
                        }

                        // Final text specification
                        if($return['update_recommended']) {
                            $return['text'] = "Update recommended to version " . $return['remote']['version'];
                        } else if($return['update_available']) {
                            $t = array();
                            if(!$return['code_identical']) {
                                $t[] = 'some changes in documentation/whitespace';
                            }
                            if($return['new_features_available']) {
                                $t[] = 'new features added';
                            }
                            if($return['local_untracked_features']) {
                                $t[] = 'features removed or you added stuff locally';
                            }
                            $t = implode(', ', $t);
                            if(!empty($t)) $t = "($t)";
                            $return['text'] = trim("Update available (remote version {$return['remote']['version']}), $t");
                        } else if($return['local_untracked_features']) {
                            $return['text'] = "No update required (Version is {$return['remote']['version']}, only a remote file was removed or you added a file locally)";
                        } else if($return['identical']) {
                            $return['text'] = "No update available (Version is {$return['local']['version']}. The modules are absolutely identical)";
                        } else {
                            $return['text'] = "No update available (Version is {$return['local']['version']})";
                        }
                    }
                }
            }
        }
        return $return;
    }
}
?>
Posted in PHP | Tagged , , | Comments Off

HTTP request class

Dies ist eine sehr einfach gestrickte Klasse, um HTTP-Anfragen an einen Webserver zu senden und die Response-Headers sowie die Ausgabe abzufangen. Es kann festgelegt werden, ob Exceptions bei einer Fehlermeldung des Servers (wie 404) geworfen werden sollen oder nicht. Das Anwendungsbeispiel zeigt wie es funktioniert:

Anwendungsbeispiel

<?php
// Before doing anything other, we include the library
include_once('swlib/swlib.class.php');

// The class source code automatically loads (requires) EException.class.php,
// Tracer.class.php, OutputBuffer.class.php and Session.class.php. These
// classes must be in the same directory.

// Start the library with configuration
swlib::start(array(
    // use output buffering, automatically start (this is default)
    'use_ob' => true,
    // use session, automatically start (this is default)
    'use_session' => true,
    // path to library var directory, we use an one one
    // The var path must be writable for the server, it is NOT the
    // directory "/var" on Unix-Like systems, but a directory to serialize
    // "variables" in.
    'var_path' => sys_get_temp_dir(),
    // path to library tmp directory, we use an one one
    // The tmp path must be writable for the server
    'tmp_path' => sys_get_temp_dir(),
    // extensions for variable export/import files (this is default)
    'var_extension' => '.tmp',
    // The include paths to search for classes
    'include_paths' => array()
));

Tracer::setLevel(5);

print '<html><body><pre>';

// Create a request object
$rq = new HttpRequest();

// Set the timeout to 5 seconds, default is 10 seconds
$rq->setTimeout(5);

// Set the port (80 is already the default port)
$rq->setPort(80);

// We want an exceptino on 404 etc.
$rq->setThrowExceptionOnErrorResponse(true);

try {
    // Let's get a page, in this case the version file of the library
    $rq->request('http://www.atwillys.de/repository/swlib/swlib.version');

    // This would be the check if exceptions are switched off.
    if(!$rq->isResponseOk()) {
        throw new Exception("...");
    }

    // Print the headers
    print "<b>Headers</b>:\n";
    foreach($rq->getResponseHeaders() as $header => $value) {
        print "   <i>$header</i>=$value\n";
    }

    // Print the content output:
    print "<b>Content:</b>\n";
    print htmlspecialchars($rq->getOutput());

} catch(Exception $e) {
    print $e->getMessage();
}

print '</pre></body></html>';
?>

Ausgabe

Headers:
   date=Tue, 24 Aug 2010 08:54:46 GMT
   server=Apache
   last-modified=Fri, 20 Aug 2010 10:39:21 GMT
   etag="2861c7a0-3-48e3ee9601b18"
   accept-ranges=bytes
   content-length=3
   connection=close
   content-type=text/plain; charset=UTF-8

Content:
1.0

Klassen-Quelltext

<?php
/**
 * Http socket request wrapper
 * @package de.atwillys.sw.php.swLib
 * @class HttpRequest
 * @author Stefan Wilhelm
 * @copyright Stefan Wilhelm, 2008-2010
 * @license GPL
 * @version 1.0
 */
class HttpRequest {

    /**
     * The port to connect to
     * @var int
     */
    private $port = 80;

    /**
     * The URL to connect to
     * @var string
     */
    private $uri = array();

    /**
     * Connection timeout in seconds
     * @var int
     */
    private $timeout = 10;

    /**
     * The request sent to the server
     * @var string
     */
    public $request = '';

    /**
     * The request headers, will be sent with
     * the request. Format: $requestHeaders['key'] = 'value'
     * will result in the header line "key: value" (+CRLF)
     * @var array
     */
    private $requestHeaders = array();

    /**
     * The response without the response headers
     * @var string
     */
    private $output = '';

    /**
     * Response code (200 is OK etc ...)
     * @var int
     */
    private $responseCode = 0;

    /**
     * Contains the response headers as assoc
     * array.
     * @var array
     */
    private $responseHeaders = array();

    /**
     * Throw an exception if server response is 404 or the like
     * @var bool
     */
    private $throwExceptionOnErrorResponse = false;

    /**
     * Returns the port to connect to
     * @return int
     */
    public function getPort() {
        return $this->port;
    }

    /**
     * Sets the port to connect to
     * @param int $port
     */
    public function setPort($port) {
        if(!settype($port, 'integer')) {
            throw new Exception('Port no integer: "' . $port . '"');
        } else {
            $this->port = $port;
        }
    }

    /**
     * Returns the URL to connect to
     * @return string
     */
    public function getUri() {
        return $this->uri;
    }

    /**
     * Sets the URL to connect to
     * @param string $uri
     */
    public function setUri($uri) {
        $uri = trim($uri);
        if(empty($uri)) {
            throw new Exception('URI is empty');
        } else {
            $this->uri = parse_url($uri);
            if($this->uri['path'] == '') {
                $this->uri['path'] = '/';
            }
        }
    }

    /**
     * Returns the connection and stream timeout in seconds
     * @return int
     */
    public function getTimeout() {
        return $this->timeout;
    }

    /**
     * Sets the connection and stream timeout in seconds
     * @param int $seconds
     */
    public function setTimeout($seconds) {
        if(!settype($seconds, 'integer')) {
            throw new Exception('Timeout no integer: "' . $seconds . '"');
        } else {
            $this->timeout = $seconds > 0 ? $seconds : 0;
        }
    }

    /**
     * Set if an exception shall be thrown by request() if the server
     * responds with an error message, such as 404
     * @param bool $throw
     */
    public function setThrowExceptionOnErrorResponse($throw) {
        $this->throwExceptionOnErrorResponse = (bool) $throw;
    }

    /**
     * Returns if an exception shall be thrown by request() if the server
     * responds with an error message, such as 404
     * @return bool
     */
    public function getThrowExceptionOnErrorResponse() {
        return $this->throwExceptionOnErrorResponse;
    }

    /**
     * Returns the response without headers
     * @return string
     */
    public function getOutput() {
        return $this->output;
    }

    /**
     * Returns an associative array containing the
     * response headers.
     * @return array
     */
    public function getResponseHeaders() {
        return $this->responseHeaders;
    }

    /**
     * Returns the server response code.
     * (200=OK ...)
     * @return int
     */
    public function getResponseCode() {
        return $this->responseCode;
    }

    /**
     * Returns if the request was successful
     * @return bool
     */
    public function isResponseOk() {
       return floor($this->responseCode / 100) == 2 || in_array($this->responseCode, array(302, 307));
    }

    /**
     * Start the request, parse the response.
     * You can also specify the URL by using
     * setURI(...) instead of using the optional
     * parameter $uri.
     * @param string $uri
     * @param int $port
     */
    public function request($uri=null, $port=null) {
        $this->output = '';
        $this->responseHeaders = array();
        $this->responseCode = 0;
        $this->request = '';
        if(!empty($uri)) $this->setURI($uri);
        if(!empty($port)) $this->setPort($port);
        $uri = $this->uri;
        $errno = 0;
        $errstr = '';
        $socket = fsockopen($uri['host'], $this->port, $errno, $errstr, 5);
        if(!$socket) {
            throw new Exception('Failed to connect to server ' . $uri['host'] . ':' . $errstr);
        } else {
            $this->requestHeaders['Host'] = $uri['host'];
            $this->requestHeaders['Connection'] = 'close';
            $this->request = 'GET ' . $uri['path'] .
                (!empty($uri['query']) ? ('?' . $uri['query']) : '' ) .
                (!empty($uri['fragment']) ? ('#' . $uri['fragment']) : '' ) .
                " HTTP/1.1";
            $request = $this->request . "\r\n";
            foreach($this->requestHeaders as $key => $value) {
               $request .= $key . ': ' . $value . "\r\n";
            }
            $request .= "\r\n";
            stream_set_timeout($socket, $this->timeout);
            fputs($socket, $request);
            while(!feof($socket)) {
                $this->output .= fgets($socket, 4096);
            }
            fclose($socket);

            if($this->output != '') {
                $header = explode("\r\n\r\n", $this->output, 2);
                $this->output = end($header);
                $header = array_filter(explode("\n", str_replace("\r","\n", reset($header))));
                if(count($header) > 0) {
                    $this->responseCode = explode(' ', array_shift($header));
                    $responseCodeMessage = array_pop($this->responseCode);
                    $this->responseCode = intval(array_pop($this->responseCode));
                    foreach($header as $rc) {
                        $rc = explode(':', $rc, 2);
                        if(count($rc) == 2) {
                            $this->responseHeaders[strtolower(trim($rc[0]))] = trim($rc[1]);
                        } else {
                            $this->responseHeaders[strtolower(trim($rc[0]))] = '';
                        }
                    }
                    if(isset($this->responseHeaders['transfer-encoding']) && $this->responseHeaders['transfer-encoding'] == 'chunked') {
                        if(strlen( ltrim($this->output)) > 0) {
                            $nl = "\r\n";
                            $t = $this->output;
                            $this->output = '';
                            do {
                                $t = ltrim($t);
                                $p = strpos($t, $nl);
                                if($p === false) {
                                    throw new Exception('Chunked HTTP response content invalid.');
                                } else {
                                    $l = hexdec(substr($t, 0, $p));
                                    if(!is_numeric($l) || $l < 0) {
                                        throw new Exception('Chunked HTTP response content invalid.');
                                    } else {
                                        $this->output .= substr($t, ($p + strlen($nl)), $l);
                                        $t  = substr($t, ($l + $p + strlen($nl)));
                                    }
                                }
                            }
                            while(strlen(trim($t)) > 0);
                            unset($t);
                        }
                    }
                }
            }
        }

        if($this->throwExceptionOnErrorResponse && !$this->isResponseOk()) {
            throw new Exception("HTTP request failed: $responseCodeMessage", $this->responseCode);
        }
        return $this;
    }
}
?>
Posted in PHP | Tagged , , | Comments Off