1<?php 2if (!defined('DOKU_INC')) { die(); } 3 4/** 5 * DokuWiki Plugin matrixnotifier (Helper Component) 6 * 7 * @license GPL 2 (http://www.gnu.org/licenses/gpl-2.0.html) 8 * 9 * @author Wilhelm/ JPTV.club 10 */ 11 12class helper_plugin_matrixnotifier extends \dokuwiki\Extension\Plugin 13{ 14 CONST __PLUGIN_VERSION__ = '1.6'; 15 16 private $_event = null; 17 private $_summary = null; 18 private $_payload = null; 19 20 private function valid_namespace() 21 { 22 $validNamespaces = $this->getConf('namespaces'); 23 if (!empty($validNamespaces)) 24 { 25 $validNamespacesArr = array_map('trim', explode(',', $validNamespaces)); 26 $thisNamespaceArr = explode(':', $GLOBALS['INFO']['namespace']); 27 28 return in_array($thisNamespaceArr[0], $validNamespacesArr); 29 } 30 31 return true; 32 } 33 34 private function check_event($event) 35 { 36 $etype = $event->data['changeType']; 37 38 if (($etype == 'C') && ($this->getConf('notify_create') == 1)) 39 { 40 $this->_event = 'create'; 41 } 42 elseif (($etype == 'E') && ($this->getConf('notify_edit') == 1)) 43 { 44 $this->_event = 'edit'; 45 } 46 elseif (($etype == 'e') && ($this->getConf('notify_edit') == 1) && ($this->getConf('notify_edit_minor') == 1)) 47 { 48 $this->_event = 'edit minor'; 49 } 50 elseif (($etype == 'D') && ($this->getConf('notify_delete') == 1)) 51 { 52 $this->_event = 'delete'; 53 } 54 /* 55 elseif (($etype == 'R') && ($this->getConf('notify_revert') == 1)) 56 { 57 $this->_event = 'revert'; 58 return true; 59 } 60 */ 61 else 62 { 63 return false; 64 } 65 66 $summary = $event->data['summary']; 67 if (!empty($summary)) 68 { 69 $this->_summary = $summary; 70 } 71 72 return true; 73 } 74 75 private function update_payload($event) 76 { 77 $user = $GLOBALS['INFO']['userinfo']['name'] ?? null; /* also works if userinfo is null */ 78 79 /* TODO: This doesn't seem to be properly populuated when the user edit comes via XMLRPC, 80 * see: https://github.com/dokuwiki/dokuwiki/issues/3544 81 */ 82 if (empty($user)) 83 { 84 /* hotfix 85 */ 86 $mode = $this->getConf('include_hosts'); 87 88 if ($mode === 'room') 89 { 90 $user = sprintf($this->getLang('anonymous'), gethostbyaddr($_SERVER['REMOTE_ADDR'])); // TODO: do we need to handle fail safe? 91 } 92 else 93 { 94 $user = $this->getLang('anonymous_safe'); 95 96 if ($mode === 'log') 97 { 98 /* Note: multiple error log lines my appear in a single message (PHP fpm) when using the 'move' plugin, f.ex., see: 99 * https://github.com/php/php-src/issues/10890 100 */ 101 error_log( sprintf("matrixnotifer: Update request from unidentified user (%s) received.", gethostbyaddr($_SERVER['REMOTE_ADDR'])) ); 102 } 103 } 104 } 105 106 $link = $this->compose_url($event, null); 107 $page = $event->data['id']; 108 109 $data = [ 110 'create' => ['loc_title' => 't_created', 'loc_event' => 'e_created', 'emoji' => ''], 111 'edit' => ['loc_title' => 't_updated', 'loc_event' => 'e_updated', 'emoji' => ''], 112 'edit minor' => ['loc_title' => 't_minor_upd', 'loc_event' => 'e_minor_upd', 'emoji' => ''], 113 'delete' => ['loc_title' => 't_removed', 'loc_event' => 'e_removed', 'emoji' => "\u{1F5D1}"], /* 'Wastebasket' emoji */ 114 ]; 115 116 $d = $data[$this->_event]; 117 $title = $this->getLang($d['loc_title']); 118 $useraction = $user.' '.$this->getLang($d['loc_event']); 119 120 $descr_raw = $title.' · '.$useraction.' "'.$page.'" ('.$link.')'; 121 $descr_html = $d['emoji'].' <strong>'.htmlspecialchars($title).'</strong> · '.htmlspecialchars($useraction).' "<a href="'.$link.'">'.htmlspecialchars($page).'</a>"'; 122 123 if (($this->_event != 'delete') && ($this->_event != 'create')) 124 { 125 $oldRev = $GLOBALS['INFO']['meta']['last_change']['date']; 126 127 if (!empty($oldRev)) 128 { 129 $diffURL = $this->compose_url($event, $oldRev); 130 $descr_raw .= ' ('.$this->getLang('compare').': '.$diffURL.')'; 131 $descr_html .= ' (<a href="'.$diffURL.'">'.$this->getLang('compare').'</a>)'; 132 } 133 } 134 135 if (($this->_event != 'delete') && $this->getConf('notify_show_summary')) 136 { 137 $summary = strip_tags($this->_summary); 138 139 if ($summary) 140 { 141 $descr_raw .= ' · '.$this->getLang('l_summary').': '.$summary; 142 $descr_html .= ' · '.$this->getLang('l_summary').': <i>'.$summary.'</i>'; 143 } 144 } 145 146 $this->_payload = array( 147 'msgtype' => 'm.text', 148 'body' => $descr_raw, 149 'format' => 'org.matrix.custom.html', 150 'formatted_body' => $descr_html, 151 ); 152 } 153 154 private function compose_url($event = null, $rev = null) 155 { 156 $page = $event->data['id']; 157 $userewrite = $GLOBALS['conf']['userewrite']; /* 0 = no rewrite, 1 = htaccess, 2 = internal */ 158 159 if ((($userewrite == 1) || ($userewrite == 2)) && $GLOBALS['conf']['useslash'] == true) 160 { 161 $page = str_replace(":", "/", $page); 162 } 163 164 $url = sprintf(['%sdoku.php?id=%s', '%s%s', '%sdoku.php/%s'][$userewrite], DOKU_URL, $page); 165 166 if ($rev != null) 167 { 168 $url .= ('&??'[$userewrite])."do=diff&rev={$rev}"; 169 } 170 171 return $url; 172 } 173 174 private function submit_payload() 175 { 176 $homeserver = $this->getConf('homeserver'); 177 $roomid = $this->getConf('room'); 178 $accesstoken = $this->getConf('accesstoken'); 179 180 if (!($homeserver && $roomid && $accesstoken)) 181 { 182 error_log('matrixnotifer: At least one of the required configuration options \'homeserver\', \'room\', or \'accesstoken\' is not set.'); 183 return; 184 } 185 186 $homeserver = rtrim(trim($homeserver), '/'); 187 $endpoint = $homeserver.'/_matrix/client/r0/rooms/'.rawurlencode($roomid).'/send/m.room.message/'.uniqid('docuwiki', true).'-'.md5(strval(random_int(0, PHP_INT_MAX))); 188 189 190 $json_payload = json_encode($this->_payload); 191 if (!is_string($json_payload)) 192 { 193 return; 194 } 195 196 $ch = curl_init($endpoint); 197 if ($ch) 198 { 199 /* Use a proxy, if defined 200 * 201 * Note: still entirely untested, was full of very obvious bugs, so nobody 202 * has ever used this succesfully anyway 203 */ 204 $proxy = $GLOBALS['conf']['proxy']; 205 if (!empty($proxy['host'])) 206 { 207 // configure proxy address and port 208 $proxyAddress = $proxy['host'].':'.$proxy['port']; 209 curl_setopt($ch, CURLOPT_PROXY, $proxyAddress); 210 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 211 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 212 213 // include username and password if defined 214 if (!empty($proxy['user']) && !empty($proxy['pass'])) 215 { 216 $proxyAuth = $proxy['user'].':'.conf_decodeString($proxy['pass']); 217 curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyAuth ); 218 } 219 } 220 221 /* Submit Payload 222 */ 223 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); 224 curl_setopt($ch, CURLOPT_HTTPHEADER, array( 225 'Content-type: application/json', 226 'Content-length: '.strlen($json_payload), 227 'User-agent: DocuWiki Matrix Notifier Plugin '.self::__PLUGIN_VERSION__, 228 'Authorization: Bearer '.$accesstoken, 229 'Cache-control: no-cache', 230 )); 231 curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload); 232 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 233 234 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); 235 curl_setopt($ch, CURLOPT_TIMEOUT, 10); 236 237 238 /* kludge, temp. fix for Let's Encrypt madness. 239 */ 240 if($this->getConf('nosslverify')) 241 { 242 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 243 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 244 } 245 246 $r = curl_exec($ch); 247 248 if ($r === false) 249 { 250 error_log('matrixnotifier: curl_exec() failure <'.strval(curl_error($ch)).'>'); 251 } 252 253 curl_close($ch); 254 } 255 } 256 257 public function sendUpdate($event) 258 { 259 if((strpos($event->data['file'], 'data/attic') === false) && $this->valid_namespace() && $this->check_event($event)) 260 { 261 $this->update_payload($event); 262 $this->submit_payload(); 263 } 264 } 265} 266