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