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).' &quot;<a href="'.$link.'">'.htmlspecialchars($page).'</a>&quot;';
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