1<?php
2/**
3 * DokuWiki Plugin letsencrypt (Helper Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Andreas Gohr <andi@splitbrain.org>
7 */
8
9// must be run within Dokuwiki
10use dokuwiki\plugin\letsencrypt\classes\CliLogger;
11use dokuwiki\plugin\letsencrypt\classes\HTMLLogger;
12use dokuwiki\plugin\letsencrypt\classes\Lescript;
13use dokuwiki\plugin\letsencrypt\classes\NullLogger;
14
15if(!defined('DOKU_INC')) die();
16
17require_once __DIR__ . '/Lescript.php';
18
19class helper_plugin_letsencrypt extends DokuWiki_Plugin {
20
21    protected $logger;
22
23    /**
24     * helper_plugin_letsencrypt constructor.
25     */
26    public function __construct() {
27        $this->logger = new NullLogger();
28    }
29
30    /**
31     * switch to HTML logging
32     */
33    public function setHTMLLogger() {
34        $this->logger = new HTMLLogger();
35    }
36
37    /**
38     * switch to Console logging
39     * @param DokuCLI $cli
40     */
41    public function setCliLogger($cli) {
42        $this->logger = new CliLogger($cli);
43    }
44
45    /**
46     * Get all know domains of this wiki (includes all animals)
47     *
48     * @return array
49     */
50    protected function getOwnDomains() {
51        $domains = array();
52
53        // get all domains from farming
54        /** @var helper_plugin_farmer $farmer */
55        $farmer = plugin_load('helper', 'farmer');
56        if($farmer) {
57            foreach($farmer->getAllAnimals() as $animal) {
58                $url = $farmer->getAnimalURL($animal);
59                $domains[] = parse_url($url, PHP_URL_HOST);
60            }
61            $farmconf = $farmer->getConfig();
62            if(isset($farmconf['base']['farmhost'])) {
63                $domains[] = $farmconf['base']['farmhost'];
64            }
65        }
66        $domains[] = parse_url(DOKU_URL, PHP_URL_HOST);
67        return $domains;
68    }
69
70    /**
71     * Get all domains configured in config file
72     *
73     * @return array $domains, $excludes
74     */
75    protected function getDomainConfig() {
76        $file = DOKU_CONF . '/letsencrypt-domains.conf';
77        if(!file_exists($file)) return array(array(), array());
78
79        $data = file($file);
80        $domains = array();
81        $excludes = array();
82
83        foreach($data as $line) {
84            $line = preg_replace('/(?<![&\\\\])#.*$/', '', $line);
85            $line = str_replace('\\#', '#', $line);
86            $line = trim($line);
87            if(empty($line)) continue;
88            if($line[0] == '!') {
89                $excludes[] = substr($line, 1);
90            } else {
91                $domains[] = $line;
92            }
93        }
94        return array($domains, $excludes);
95    }
96
97    /**
98     * Get all domains
99     *
100     * @return array
101     */
102    public function getAllDomains() {
103        list($domains, $excludes) = $this->getDomainConfig();
104        $domains = array_merge($this->getOwnDomains(), $domains);
105        $domains = array_unique($domains);
106        $domains = array_filter($domains, array($this, 'domainFilter'));
107        $excludes = array_unique($excludes);
108        $excludes = array_filter($excludes, array($this, 'domainFilter'));
109        $domains = array_diff($domains, $excludes);
110
111        $info = $this->getCertInfo();
112        $result = array();
113        foreach($domains as $domain) {
114            if($info['expires_in_days'] && in_array($domain, $info['domains'])) {
115                $expire = $info['expires_in_days'];
116            } else {
117                $expire = 0;
118            }
119
120            $result[$domain] = $expire;
121        }
122
123        return $result;
124    }
125
126    /**
127     * Filter callback to remove non-valid domains
128     *
129     * @param string $domain
130     * @return bool
131     */
132    public function domainFilter($domain) {
133        if(empty($domain)) return false;
134        if(strpos($domain, '.') === false) return false; // need at least one dot
135        if($domain == 'localhost.localdomain') return false;
136        if(preg_match('/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/', $domain)) return false;
137        return true;
138    }
139
140    /**
141     * @return bool is the account set up already?
142     */
143    public function hasAccount() {
144        return file_exists($this->getCertDir() . '/_account/private.pem');
145    }
146
147    public function checkDir() {
148        $root = $this->getRoot();
149        if($root) {
150            $wellknown = "$root/.well-known/acme-challenge";
151            io_makeFileDir("$wellknown/test.txt");
152            $ok = io_saveFile("$wellknown/test.txt", 'works');
153        } else {
154            $ok = false;
155        }
156
157        $certdir = $this->getConf('certificatedir');
158        io_makeFileDir("$certdir/test.txt");
159        $ok &= io_saveFile("$certdir/test.txt", 'works');
160
161        return $ok;
162    }
163
164    /**
165     * Register a new LE account
166     *
167     * @param string $code
168     * @param string $country
169     * @param string $email
170     */
171    public function register($code, $country, $email) {
172        set_time_limit(0);
173        $lescript = new Lescript($this->getCertDir(), $this->getRoot(), $this->logger);
174        $lescript->countryCode = $code;
175        $lescript->state = $country;
176        $email = trim($email);
177        if($email) {
178            $lescript->contact = array('mailto:' . $email);
179        }
180
181        $lescript->initAccount();
182    }
183
184    /**
185     * Creates or updates the certificates
186     */
187    public function updateCerts() {
188        set_time_limit(0);
189        $lescript = new Lescript($this->getCertDir(), $this->getRoot(), $this->logger);
190        $lescript->signDomains(array_keys($this->getAllDomains()));
191    }
192
193    /**
194     * The directory to store certificates
195     *
196     * @return string|null null if none is set or can be found
197     */
198    public function getCertDir() {
199        $certdir = $this->getConf('certificatedir');
200        if($certdir) return $certdir;
201
202        $root = $this->getRoot();
203        if($root) $certdir = fullpath("$root/../certs");
204        if($certdir) return $certdir;
205
206        return null;
207    }
208
209    /**
210     * Get info on our certificate
211     *
212     * @return array|null
213     */
214    protected function getCertInfo() {
215        $lescript = new Lescript($this->getCertDir(), $this->getRoot(), $this->logger);
216        $data = $lescript->getCertInfo('wiki');
217        return $data;
218    }
219
220    /**
221     * The webserver root directory
222     *
223     * @return string|null null if none is set or detected
224     */
225    public function getRoot() {
226        // did the user tell us?
227        $root = $this->getConf('documentroot');
228        if($root) return $root;
229
230        // does the webserver tell us?
231        $root = $_SERVER['DOCUMENT_ROOT'];
232        if($root) return $root;
233
234        // can we figure it out?
235        $len = -1 * strlen(DOKU_BASE);
236        if(substr(DOKU_INC, $len) == DOKU_BASE) {
237            return substr(DOKU_INC, 0, $len);
238        }
239
240        // we're lost
241        return null;
242    }
243}
244
245// vim:ts=4:sw=4:et:
246