GithubWebhook.php

<?php

namespace tlf;

class GithubWebhook {

    protected $payload;
    protected $keyfile;
    protected $workDir;
    protected $outDir;
    protected $repos;
    protected $users;
    protected $body;
    protected $logging = false;

    protected $user;
    protected $repo;
    protected $userDir;
    protected $repoDir;
    protected $defaultBranch;

    protected $currentBranch;

    public function __construct($payload, $privateKey, $workingDir, $outputDir,$allowedRepos, $validOwners){
        if (!is_dir($workingDir)){
            throw new \Exception("Working dir '{$workingDir}' must exist.");
        }
        if (!is_dir($outputDir)){
            throw new \Exception("Output dir '{$outputDir}' must exist.");
        }
        $this->payload = json_decode($payload,true);
        $this->key = $privateKey;
        $this->workDir = $workingDir;
        $this->outDir = $outputDir;
        $this->repos = $allowedRepos;
        //@TODO should we also check against the user who did the push?
        $this->users = $validOwners;
        $this->body = trim(file_get_contents("php://input"));
        $headers = getallheaders();
        $this->signature = $headers['X-Hub-Signature'];
        //@TODO implement logging
        $this->logFile = $this->workingDir.'/webhook-log';

        $this->repo = $this->payload['repository']['name'];
        $this->user = $this->payload['repository']['owner']['name'];
        $this->userDir = $this->workDir.'/'.$this->user.'/';
        $this->repoDir = $this->userDir.'/'.$this->repo.'/';
        $this->defaultBranch = $this->payload['repository']['default_branch'];

        $ref = $this->payload['ref'];
        if (strpos($ref, $prefix='refs/heads/')===0){
            $this->currentBranch = substr($ref,strlen($prefix));
        } else {
            echo "The current branch is null. Ref was '{$ref}' and should have started with 'refs/heads/' followed by the branch name.";
            echo "We will update the default branch since we don't know what was pushed.";
            $this->currentBranch = $this->defaultBranch;
        }
    }

    // public function enableLogging(){
    //     $this->logging = true;
    // }

    public function isValidRequest(){
        $body = $this->body;
        $headers = getallheaders();
        $log = $this->logFile;
        $user = $this->user;
        $repo = $this->repo;
        
        if (!in_array($user,$this->users)){
            echo "The user '{$user}' who owns the repo '{$repo}' is authorized.\n";
            return false;
        }
        if (!in_array($repo,$this->repos)){
            echo "The repo '{$repo}' is not authorized.\n";
            return false;
        }

        $signature = $this->signature;
        // $keyFile = $this->keyFile;
        // trim(shell_exec('ssh-keygen -y -f '.$keyFile));
        $body = $this->body;
        $pubKey = $this->key;
        $hash = 'sha1='.hash_hmac('sha1',$body,$pubKey);

        $success = hash_equals($hash,$signature);

        if ($success===true)return true;

        echo "local hash:\n{$has}\n\nReceived:\n{$signature}";

        return false;
    }

    public function retrieveCurrentRepo(){
        if (!$this->isValidRequest()){
            echo "The request was invalid or is not from github.\n";
            return;
        }

        $userDir = $this->userDir;
        $repoDir = $this->repoDir;
        if (!is_dir($userDir))mkdir($userDir,0771,false);
        
        if (is_dir($repoDir)&&count(scandir($repoDir))>2){
            if (!is_dir($repoDir.'/.git')){
                throw new \Exception("Your repo dir '{$repoDir}' exists, but does not contain a .git folder. Delete the directory & start fresh.");
            }
            $this->pullChanges();
            return;
        } 
        
        if (is_dir($repoDir)){
            rmdir($this->repoDir);
        }

        $this->cloneRepo();

    }
    private function pullChanges(){
        $dir = $this->repoDir;
        echo "pull changes into '{$dir}'\n";
        $command = "cd {$dir};\ngit pull;";
        $output = shell_exec($command);
        sleep(1);
    }

    private function cloneRepo(){
        $dir = $this->userDir;
        $cloneUrl = 'https://github.com/'.$this->user.'/'.$this->repo.'.git';
        echo "clone chagnes into '{$dir}' from {$cloneUrl}\n";
        $command = "cd {$dir};\ngit clone {$cloneUrl};";
        $output = shell_exec($command);
        sleep(1);
    }

    public function outputCurrentBranch(){
        if (!$this->isValidRequest()){
            echo "The request or keys were invalid... It may not be from github, or your key & webhook secret are configured wrong.\n";
            return;
        }
        $this->retrieveCurrentRepo();
        $repo = $this->repo;
        $branch = $this->currentBranch;
        $repoDir = $this->repoDir;
        $outDir = $this->outDir.'/'.$this->user;
        $repoOutDir = $outDir.'/'.$repo;
        $branchOutDir = $repoOutDir.'/'.$branch;
        if (!is_dir($outDir))mkdir($outDir,0771,false);
        if (!is_dir($repoOutDir))mkdir($repoOutDir,0771,false);
        if (!is_dir($branchOutDir))mkdir($branchOutDir,0771,false);

        echo "Preparing to rsync files. Repo dir is '{$repoDir}'";
        echo "Rsync files into '{$branchOutDir}'\n";
        $inFiles = print_r(scandir($repoDir),true);
        echo "Input Files ('{$repoDir}'):\n".$inFiles."\n";
        $command = 
        <<<BASH
            cd {$repoDir};
            git checkout {$branch};
            rsync -a --exclude=.git --exclude=".git" --exclude=".git/*" {$repoDir}/ {$branchOutDir};
        BASH;
        $outFiles = scandir($branchOutDir);
        echo "Output Files ('{$branchOutDir}'):\n".print_r($files,true)."\n";
        shell_exec($command);

    }
}