Provi2.php

<?php

namespace Tlf;

class Provi2 {


    /**
     * the state of the current request
     */
    public $state = [];
    /**
     * the directory containing all your git repos
     */
    public string $dir;
    /**
     * all urls for documentation will begin with this url. This url will show a list of projects
     */
    public string $base_url;

    /**
     * the directory where all the views are found
     */
    public string $view_dir;

    public string $gitlab_hook_path;
    /** the token sent with the webhook request */
    public ?string $secret_token = null;

    public string $git_icon_path;

    /** dir to store temporary info in */
    public string $cache_dir;

    /**
     * `json_decode`d project settings file
     */
    public array $settings;

    /**
     * the name of the vendor these projects belong to
     */
    public string $name;

    public array $public_file_params = [];

    /**
     * Provi2 doesn't seem to have a connection to liaison, and I don't want to risk disrupting any of the mess that is this package, so here's a prop to avoid that. This is used by some new features (setting seo title!).
     */
    public ?\Lia $actual_liaison = null;

    /**
     * @param $project_dir the directory containing all your git repos
     * @param $base_url all urls for documentation will begin with this url. This url will show a list of projects
     * @param $vendor_name the name of the vendor these projects belong to
     */
    public function __construct(string $project_dir, string $base_url, string $vendor_name, ?\Lia $lia = null){
        $this->dir = $project_dir;
        $this->base_url = $base_url;
        $this->view_dir = dirname(__DIR__).'/view/';
        $this->name = $vendor_name;
        $this->gitlab_hook_path = dirname(__DIR__).'/public/gitlab-hook.php';
        $this->git_icon_path = dirname(__DIR__).'/public/git-icon-1788c-sm.png';
        $this->cache_dir = dirname(__DIR__).'/cache/';
        $this->settings = json_decode(file_get_contents($this->settings_file()),true);

        $this->actual_liaison = $lia;
    }

    public function url($rel_path=''){
        return str_replace(['///','//'],'/',$this->base_url.'/'.$rel_path);
    }

    /**
     * Get a response to a request
     * @return false on failure or on success an array containing keys `css`, `js`, and `content`, where `content` is a string, and the other two are arrays of `key=>value` where `key` is url & `value` is file path
     */
    public function route(){
        $url = $_SERVER['REQUEST_URI'];
        $pos = strpos($url, '?');
        if ($pos!==false)$url = substr($url,0, $pos);
        // if (substr($url,0,$len=strlen($this->base_url))!==$this->base_url)return false;
        // $url = substr($url,$len);
        if (@$url[0]!='/')$url = '/'.$url;
        if ($url==$this->base_url.'gitlab-hook/'
            ||$url==$this->base_url.'git-icon-1788c-sm.png'
        ){
            return;
        }

        $resources = $this->view('Resources')->resources();
        

        $response_view = $this->get_response($url);
        if ($response_view===false)return false;
        $response_resources = $response_view->resources();

        $all_resources = array_merge($resources, $response_resources);
        $all_resources = $this->cleanup_resources($all_resources);

        $response = $all_resources;
        $response['content'] = $response_view.'';
        $response['view'] = $response_view;

        return $response;
    }

    /**
     * Get a url-specific response ... @see(route()) wraps this to make it easier to send all resource files
     *
     * @param $url the relative url (base url is removed)
     * @return a view on success or `false` on failure
     */
    public function get_response($url){
        $parsed = $this->parse_url2($url);
        if ($parsed==false)return false;

        if ($parsed->type=='not-a-project'){
            return $this->view('Error');
        }
        if ($parsed->type=='project_listing'){
            $view = $this->view('ProjectSelector');
            return $view;
        }

        $view = $this->view('ProjectLayout',['project'=>$parsed]);

        return $view;
    }

    /**
     * does nothing ... just here so `\Lia\View` doesn't crash 
     */
    public function addResourceFile(){}


    /**
     * get array of all projects from settings file
     * @return array of ... idk
     */
    public function all_projects(): array{
        // echo "Thsi page will return very soon!";
        $settings = $this->settings;
        // print_r($settings);exit;
        $projects = [];

        foreach ($settings as $file => $info){
            if (!is_dir($path = $this->dir.'/'.$file))continue;
            $projects[$file] = $this->get_project($file);
            if (isset($settings[$file]['description']))$projects[$file]->description = $settings[$file]['description'];
            else {
                $projects[$file]->description = "No description found for '$file'";
            }
        }

        //foreach (scandir($this->dir) as $file){
            //if ($file=='.'||$file=='..')continue;
            //if (!is_dir($path = $this->dir.'/'.$file))continue;
            //$projects[$file] = $this->get_project($file);
            //if (isset($settings[$file]['description']))$projects[$file]->description = $settings[$file]['description'];
            //else {
                //$projects[$file]->description = "No description found for '$file'";
            //}
        //}

        return $projects;
    }

    /**
     * get project info for the list of all projects
     *
     * @param $project_dir the relative path name inside `$this->dir` 
     */
    public function get_project($project_dir){
        $path = $this->dir.'/'.$project_dir;
        $url = $this->base_url.'/'.$project_dir;
        $url = str_replace('//','/',$url);
        $project = (object)[
            'name'=>basename($project_dir),
            'dir'=>$project_dir,
            // @todo make docs dir configurable
            'url'=>$url,
            // @todo add project descriptions
            'description'=>'project descriptions not yet implemented.',
            'src_url'=>$url.'-src',
            'docs_url'=>$url,
            // 'file'=>
        ];

        return $project;
    }


    /**
     * take the file paths as given by `Lia\Obj\View` and convert them into an array like `['css'=>['/url.css'=>'/path/to/css/file.css'], 'js'=>[...]]` 
     */
    public function cleanup_resources(array $resources){

        $clean = [];
        foreach ($resources as $file_path){
            $url = substr($file_path,strlen($this->view_dir));
            $ext = pathinfo($file_path,PATHINFO_EXTENSION);
            $url = $this->base_url.'/files/'.$url;
            $url = str_replace('//', '/', $url);
            $clean[$ext][$url] = $file_path;
        }

        return $clean;
    }

    public function view($rel_name, $args=[]){

        if ($this->actual_liaison != null){
            $args['actual_liaison'] = $this->actual_liaison;
        }
        $args['lia'] = $this;
        $args['provi'] = $this;
        $args['state'] = $this->state;
        $view = new \Lia\Obj\View('Docu/'.$rel_name, $this->view_dir, $args);
        return $view;
    }

    /**
     * @return false if not a request for provi. or an stdClass object of parsed info
     */
    public function parse_url2(string $url){
        $parsed = [];


        $url = str_replace('%20',' ', $url);
        $base_url = $this->base_url;

        // check if it's a request to provi
        if (substr($url,0,strlen($base_url))==$base_url){
            $parsed['prefix'] = $base_url;
        } else {
            return false;
        }
        // check if request to project listing
        $rel_url = substr($url,strlen($base_url));
        if ($rel_url==''){
            $project = new \Tlf\Provi\Project();
            $project->type = 'project_listing';
            $project->prefix = $base_url;
            return $project;
        }
        // parse out the project name & branch
        $parts = explode('/', $rel_url);
            // array_shift($parts); // might need this if the prefix does not end with a `/`
        $project_name_and_branch = array_shift($parts);
        $parsed['project_url'] = $base_url.'/'.$project_name_and_branch.'/';
        $pnab_parts = explode(':', $project_name_and_branch);

        // parse out the type (src or docs)
        $project_name_and_type = $pnab_parts[0];
        if (substr($project_name_and_type,-4)=='-src'){
            $parsed['project_name'] = substr($project_name_and_type,0,-4);
            $parsed['type']='src';
        } else {
            $parsed['project_name'] = $project_name_and_type;
            $parsed['type'] = 'docs';
        }

        if (!isset($this->settings[$parsed['project_name']])){
            
            return (object)['type'=>'not-a-project'];
        }


        $parsed['default_branch'] = $this->get_default_branch((object)['name'=>$parsed['project_name']]);
        if (count($pnab_parts)>1){
            $parsed['branch'] = $pnab_parts[1];
            $parsed['is_default_branch'] = false;
        } else {
            // what info do i need to get the default branch?
            // project name
            $parsed['branch'] = $this->get_default_branch((object)['name'=>$parsed['project_name']]);
            $parsed['is_default_branch'] = true;
        } 


        // determine the branch dir
        $project_branch_dir = $this->dir.'/'.$parsed['project_name'].'/'.$parsed['branch'].'/';
        $parsed['branch_dir'] = $project_branch_dir;
        if ($parsed['type']=='docs'){
            $docs_dir = $this->get_docs_dir($parsed['branch_dir']);
            $file_base_dir = $project_branch_dir.'/'.$docs_dir.'/';//$this->get_docs_dir();
        } else {
            $file_base_dir = $project_branch_dir;
        }

        // build the absolute paths
        $parsed['docs_dir'] = $project_branch_dir.'/'.$this->get_docs_dir($parsed['branch_dir']).'/';
        $parsed['rel_file_path'] = implode('/', $parts);
        $parsed['abs_file_path'] = str_replace('//','/',
            $file_base_dir.'/'.$parsed['rel_file_path']
        );

        if (is_dir($parsed['abs_file_path'])){
            $parsed['rel_dir_path'] = $parsed['rel_file_path'].'/';
            $parsed['abs_dir_path'] = $parsed['abs_file_path'].'/';
            // check for a README
            if (file_exists($readme_path=$parsed['abs_file_path'].'README.md')){
                $parsed['abs_file_path'] = $readme_path;
                $parsed['rel_file_path'] .= 'README.md';
            }
        } else {
            $parsed['rel_dir_path'] = dirname($parsed['rel_file_path']).'/';
            if ($parsed['rel_dir_path']=='./')$parsed['rel_dir_path'] = '/';
            $parsed['abs_dir_path'] = dirname($parsed['abs_file_path']).'/';
        }


        // remove duplicate forward slashes
        $parsed = array_map(
            function($v){
                return str_replace('//', '/',$v);
            },
            $parsed
        );

        // convert array to object
        $project = new \Tlf\Provi\Project();
        foreach ($parsed as $k=>$v){
            $project->$k = $v;
        }

        $project->git_url = $this->get_git_url($project->project_name);

        return $project; 
    }

    /** get the url to the git repo for this project
     * @param $project_name should be the same as the project's directory name
     */
    public function get_git_url(string $project_name){
        $cache_settings_file = $this->cache_dir.'/projects/'.$project_name;
        if (!file_exists($cache_settings_file)){
            $full_settings = $this->settings;
            $proj_settings = $full_settings[$project_name];
            if (!file_exists(dirname($cache_settings_file))){
                mkdir(dirname($cache_settings_file), 0755, true);
            }
            file_put_contents($cache_settings_file, serialize($proj_settings));
        }
        $settings = unserialize(file_get_contents($cache_settings_file));
        return $settings['git'];
    }

    /**
     * @return false on failure or the relative path inside the branch dir in which docs are contained
     */
    public function get_docs_dir(string $branch_dir){
        $try = [
            'doc','docs'
        ];
        foreach ($try as $d){
            if (is_dir($branch_dir.'/'.$d)){
                return $d;
            }
        }

        return false;
    }

    /**
     * @param $project the project as from get_project()
     */
    public function get_default_branch(object $project){
        $settings = $this->settings; 
        $branch = $settings[$project->name]['default'];
        return $branch;
        //@todo actually figure out default branch
        // return 'v0.9';
    }

    public function settings_file(){
        return $this->dir.'/settings.json';
    }

    /**
     * get an array of files & directories in the given dir.
     *
     * @return array ['files'=>['file1','file2'], 'dirs'=>['dir1','dir2']]
     */
    public function files_in_dir(string $dir): array{

        $files = ['files'=>[],'dirs'=>[]];
        if (!is_dir($dir))return $files;
        foreach (scandir($dir) as $f){
            if ($f=='..'||$f=='.')continue;
            if (is_file($dir.'/'.$f))$files['files'][] = $f;
            else $files['dirs'][] = $f;
        }

        sort($files['dirs']);
        sort($files['files']);
        
        return $files;
    }
}