Classes to Create htdigest and htaccess Files

This is a first draft of some classes to manipulate .htaccess and htdigest files. I'm putting it up because there aren't many classes to do this, and seemingly none for htdigest files. (I just learned about the PEAR::Config classes. Neat.)

HTAuthFile is a superclass with a common writer. HTAccessFile is a very incomplete way to manipulate Apache .htaccess files. It is basically incorrect, because it doesn't support those <Scoping things> in Apache config files, and it doesn't take into account the order of directives, and it doesn't deal with using the same directive mulitiple times, but it will do for writing simple files to enable directory passwords. It's at times like this, I wish Apache used XML for the config format... but, this is the only time I've really had that feeling.

HTDigestFile manipulates files created by htdigest. It has much of the functionality of the htdigest program. There is a security risk here, in that htdigest files should be located outside of the docroot, and should not be writable from scripts on the website. (The same could be said about .htaccess files.) Still, people often do want to be able to set these things up via a web page.

HTGroupsFile is not yet implemented. HTPasswdFile is not yet implemented.

Sample usage follows the sources.

<?php
    // vim:set ts=4:sw=4: 

    /**
     * Classes to manipulate .htaccess .htdigest ,htpasswd and ,htgroups
     */
    class HTAuthFile 
    {
        var $path;
        var $file;
        function write()
        {
            $fh = fopen($this->path,'w') or die('Could not open file for writing.');
            foreach($this->file as $line)
                fwrite($fh, $line);
            fclose($fh);
        }
    }
    /**
     * Simplistic class that sets values in an htaccess file.
     * $htaccess = new HTAccessFile('hta');
     * echo $htaccess->get('AuthType');
     * $htaccess->set('AuthType','Digest');
     * $htaccess->write();
     * $htaccess = new HTAccessFile('hta');
     * echo $htaccess->get('AuthType');
     */
    class HTAccessFile extends HTAuthFile
    {
        function HTAccessFile($path)
        {
            $this->path = $path;
            if (file_exists($path))
                $this->file = file($path); 
        }
        function exists()
        {
            return file_exists($this->path);
        }
        /**
         * @returns the value of the key
         */
        function get($key)
        {
            for($i=0;$i<count($this->file);$i++)
            {
                if (preg_match("/^$key\s+(.+?)$/", $this->file[$i], $matches))
                {
                    return rtrim($matches[1]);
                }
            }
        }
        function set($key, $value)
        {
            //print "set($key,$value)";
            for($i=0;$i<count($this->file);$i++)
            {
                if (preg_match("/^$key\s+(.+?)$/", $this->file[$i], $matches))
                {
                    $this->file[$i] = "$key $value\n";
                    return;
                }
            }
            // if it falls through, we add a line
            $this->add($key,$value);
        }
        /**
         * Appends a line to the file.
         */
        function add($key, $value)
        {
            $this->file[] = "$key $value\n";
        }
    }
    class HTDigestFile extends HTAuthFile
    {
        function HTDigestFile( $path )
        {
            $this->path = $path;
            if (file_exists($path))
                $this->file = file($path);
        }
        function _get($domain,$user)
        {
            for($i=0;$i<count($this->file);$i++)
            {
                if (preg_match("/^$user:$domain:(.+?)$/",
                    $this->file[$i],$matches))
                {
                    return $matches[1];
                }
            }
        }
        function check($domain,$user,$password)
        {
            return $this->_get($domain,$user)==$this->_hash($domain,$user,$password);
        }
        function _hash($domain,$user,$password)
        {
            return md5("$user:$domain:$password");
        }
        function _set($domain,$user,$password)
        {
            for($i=0;$i<count($this->file);$i++)
            {
                if (preg_match("/^$user:$domain:(.+?)$/",
                    $this->file[$i],$matches))
                {
                    $this->file[$i] = "$user:$domain:".$this->_hash($domain,$user,$password);
                    return;
                }
            }
            // else it falls through to here
            $this->file[] = "$user:$domain:".$this->_hash($domain,$user,$password);
        }
        /**
         * Safe add that only adds if the user doesn't exist.
         */
        function add($domain,$user,$password)
        {
            if (!$this->_get($domain,$user))
                $this->_set($domain,$user,$password);
            else 
                return false;
            return true;
        }
        /**
         * Safe change that requires old password to match.
         */
        function change($domain,$user,$oldpass,$newpass)
        {
            if ($this->check($domain,$user,$oldpass))
                $this->_set($domain,$user,$newpass);
            else
                return false;
            return true;
        }
        /**
         * @returns array of strings of all security domains
         */
        function &domains()
        {
            $domains = array();
            for($i=0;$i<count($this->file);$i++)
            {
                $parts = explode(':',$this->file[$i]);
                if ($parts[1] and !in_array($parts[1],$domains))
                    array_push($domains,$parts[1]);
            }
            return $domains;
        }
    }
    class HTPasswdFile extends HTAuthFile
    {
        function HTPasswdFile( $path )
        {
        }
        function get($user)
        {
        }
        function set($user,$password)
        {
        }
    }
    class HTGroupFile extends HTAuthFile
    {
        function HTGroupFile( $path )
        {
        }
        function get($group)
        {
        }
        function add($group, $user)
        {
        }
        function remove($group, $user)
        {
        }
    }
?>

This sample use shows how you can load or create an htaccess file and an htdigest file, and the path to the htdigest file is specified correctly in the htaccess file. The code shows, somewhat, that the non-existence of the subject file doesn't cause errors. Rather, it simply behaves as if the file is there, but empty.

This might be a really cool feature I was trying to achieve... or a really stupid idea that I was indulging. Time will tell.

    include('HTAuthFile.class.php');

    ## if there is no .htaccess file in admin/, we create one
    $hta = new HTAccessFile('../admin/.htaccess');
    if ($hta->get('AuthDigestFile'))
        $digestPath = $hta->get('AuthDigestFile');
    else
        $digestPath = '../../htdigest';
    if (!$hta->exists())
    {
        $hta->add('AuthType','Digest');
        $hta->add('AuthName','admin');
        $hta->add('AuthDigestFile',realpath($digestPath));
        $hta->add('Require','valid-user');
        $hta->write();
    }
    if ($hta->get('AuthType')!='Digest')
        $hta->set('AuthType','Digest');
    $htd = new HTDigestFile($digestPath);
    $domains = $htd->domains();
        $htd->write();
        $hta->write();