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.5'; 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']; 78 /* TODO: This doesn't seem to be properly populuated when the user edit comes via XMLRPC, 79 * see: https://github.com/dokuwiki/dokuwiki/issues/3544 80 */ 81 if (empty($user)) 82 { 83 /* hotfix */ 84 $user = sprintf($this->getLang('anonymous'), gethostbyaddr($_SERVER['REMOTE_ADDR'])); /* TODO: do we need to handle fail safe? */ 85 } 86 87 $link = $this->compose_url($event, null); 88 $page = $event->data['id']; 89 90 $data = [ 91 'create' => ['loc_title' => 't_created', 'loc_event' => 'e_created', 'emoji' => ''], 92 'edit' => ['loc_title' => 't_updated', 'loc_event' => 'e_updated', 'emoji' => ''], 93 'edit minor' => ['loc_title' => 't_minor_upd', 'loc_event' => 'e_minor_upd', 'emoji' => ''], 94 'delete' => ['loc_title' => 't_removed', 'loc_event' => 'e_removed', 'emoji' => "\u{1F5D1}"], /* 'Wastebasket' emoji */ 95 ]; 96 97 $d = $data[$this->_event]; 98 $title = $this->getLang($d['loc_title']); 99 $useraction = $user.' '.$this->getLang($d['loc_event']); 100 101 $descr_raw = $title.' · '.$useraction.' "'.$page.'" ('.$link.')'; 102 $descr_html = $d['emoji'].' <strong>'.htmlspecialchars($title).'</strong> · '.htmlspecialchars($useraction).' "<a href="'.$link.'">'.htmlspecialchars($page).'</a>"'; 103 104 if (($this->_event != 'delete') && ($this->_event != 'create')) 105 { 106 $oldRev = $GLOBALS['INFO']['meta']['last_change']['date']; 107 108 if (!empty($oldRev)) 109 { 110 $diffURL = $this->compose_url($event, $oldRev); 111 $descr_raw .= ' ('.$this->getLang('compare').': '.$diffURL.')'; 112 $descr_html .= ' (<a href="'.$diffURL.'">'.$this->getLang('compare').'</a>)'; 113 } 114 } 115 116 if (($this->_event != 'delete') && $this->getConf('notify_show_summary')) 117 { 118 $summary = strip_tags($this->_summary); 119 120 if ($summary) 121 { 122 $descr_raw .= ' · '.$this->getLang('l_summary').': '.$summary; 123 $descr_html .= ' · '.$this->getLang('l_summary').': <i>'.$summary.'</i>'; 124 } 125 } 126 127 $this->_payload = array( 128 'msgtype' => 'm.text', 129 'body' => $descr_raw, 130 'format' => 'org.matrix.custom.html', 131 'formatted_body' => $descr_html, 132 ); 133 } 134 135 private function compose_url($event = null, $rev = null) 136 { 137 $page = $event->data['id']; 138 $userewrite = $GLOBALS['conf']['userewrite']; /* 0 = no rewrite, 1 = htaccess, 2 = internal */ 139 140 if ((($userewrite == 1) || ($userewrite == 2)) && $GLOBALS['conf']['useslash'] == true) 141 { 142 $page = str_replace(":", "/", $page); 143 } 144 145 $url = sprintf(['%sdoku.php?id=%s', '%s%s', '%sdoku.php/%s'][$userewrite], DOKU_URL, $page); 146 147 if ($rev != null) 148 { 149 $url .= ('&??'[$userewrite])."do=diff&rev={$rev}"; 150 } 151 152 return $url; 153 } 154 155 private function submit_payload() 156 { 157 $homeserver = $this->getConf('homeserver'); 158 $roomid = $this->getConf('room'); 159 $accesstoken = $this->getConf('accesstoken'); 160 161 if (!($homeserver && $roomid && $accesstoken)) 162 { 163 error_log('matrixnotifer: At least one of the required configuration options \'homeserver\', \'room\', or \'accesstoken\' is not set.'); 164 return; 165 } 166 167 $homeserver = rtrim(trim($homeserver), '/'); 168 $endpoint = $homeserver.'/_matrix/client/r0/rooms/'.rawurlencode($roomid).'/send/m.room.message/'.uniqid('docuwiki', true).'-'.md5(strval(random_int(0, PHP_INT_MAX))); 169 170 171 $json_payload = json_encode($this->_payload); 172 if (!is_string($json_payload)) 173 { 174 return; 175 } 176 177 $ch = curl_init($endpoint); 178 if ($ch) 179 { 180 /* Use a proxy, if defined 181 * 182 * Note: still entirely untested, was full of very obvious bugs, so nobody 183 * has ever used this succesfully anyway 184 */ 185 $proxy = $GLOBALS['conf']['proxy']; 186 if (!empty($proxy['host'])) 187 { 188 // configure proxy address and port 189 $proxyAddress = $proxy['host'].':'.$proxy['port']; 190 curl_setopt($ch, CURLOPT_PROXY, $proxyAddress); 191 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 192 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 193 194 // include username and password if defined 195 if (!empty($proxy['user']) && !empty($proxy['pass'])) 196 { 197 $proxyAuth = $proxy['user'].':'.conf_decodeString($proxy['pass']); 198 curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyAuth ); 199 } 200 } 201 202 /* Submit Payload 203 */ 204 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); 205 curl_setopt($ch, CURLOPT_HTTPHEADER, array( 206 'Content-type: application/json', 207 'Content-length: '.strlen($json_payload), 208 'User-agent: DocuWiki Matrix Notifier Plugin '.self::__PLUGIN_VERSION__, 209 'Authorization: Bearer '.$accesstoken, 210 'Cache-control: no-cache', 211 )); 212 curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload); 213 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 214 215 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); 216 curl_setopt($ch, CURLOPT_TIMEOUT, 10); 217 218 219 /* kludge, temp. fix for Let's Encrypt madness. 220 */ 221 if($this->getConf('nosslverify')) 222 { 223 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 224 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 225 } 226 227 $r = curl_exec($ch); 228 229 if ($r === false) 230 { 231 error_log('matrixnotifier: curl_exec() failure <'.strval(curl_error($ch)).'>'); 232 } 233 234 curl_close($ch); 235 } 236 } 237 238 public function sendUpdate($event) 239 { 240 if((strpos($event->data['file'], 'data/attic') === false) && $this->valid_namespace() && $this->check_event($event)) 241 { 242 $this->update_payload($event); 243 $this->submit_payload(); 244 } 245 } 246} 247