1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8/**
9 *
10 */
11class BigBlueButtonLib
12{
13	private $version = false;
14
15	/**
16	 * @return bool|string
17	 */
18	private function getVersion()
19	{
20		if ($this->version !== false) {
21			return $this->version;
22		}
23
24		if ($version = $this->performRequest('', [])) {
25			$values = $this->grabValues($version->documentElement);
26			$version = $values['version'];
27
28			if (false !== $pos = strpos($version, '-')) {
29				$version = substr($version, 0, $pos);
30			}
31
32			$this->version = $version;
33		} else {
34			$this->version = '0.6';
35		}
36
37		return $this->version;
38	}
39
40	/**
41	 * @return array|mixed
42	 */
43	public function getMeetings()
44	{
45		$cachelib = TikiLib::lib('cache');
46
47		if (! $meetings = $cachelib->getSerialized('bbb_meetinglist')) {
48			$meetings = [];
49
50			if ($dom = $this->performRequest('getMeetings', ['random' => 1])) {
51				foreach ($dom->getElementsByTagName('meeting') as $node) {
52					$meetings[] = $this->grabValues($node);
53				}
54			}
55
56			$cachelib->cacheItem('bbb_meetinglist', serialize($meetings));
57		}
58
59		return $meetings;
60	}
61
62	/**
63	 * @param $room
64	 * @return array
65	 */
66	public function getAttendees($room, $username = false)
67	{
68		if ($meeting = $this->getMeeting($room)) {
69			if ($dom = $this->performRequest('getMeetingInfo', ['meetingID' => $room, 'password' => $meeting['moderatorPW']])) {
70				$attendees = [];
71
72				foreach ($dom->getElementsByTagName('attendee') as $node) {
73					$attendees[] = $this->grabValues($node, $username);
74				}
75
76				return $attendees;
77			}
78		}
79	}
80
81	/**
82	 * @param $node
83	 * @return array
84	 */
85	private function grabValues($node, $username = false)
86	{
87		$values = [];
88
89		foreach ($node->childNodes as $n) {
90			if ($n instanceof DOMElement) {
91				$values[$n->tagName] = $n->textContent;
92			}
93		}
94		if ($username && $values['fullName']) {
95			preg_match('!\(([^\)]+)\)!', $values['fullName'], $match);
96			$values['fullName'] = $match[1];
97		} else {
98			$values['fullName'] = trim(preg_replace('!\(([^\)]+)\)!', '', $values['fullName']));
99		}
100		return $values;
101	}
102
103	/**
104	 * @param $room
105	 * @return bool
106	 */
107	public function roomExists($room)
108	{
109		foreach ($this->getMeetings() as $meeting) {
110			if ($meeting['meetingID'] == $room) {
111				return true;
112			}
113		}
114
115		return false;
116	}
117
118	/**
119	 * @param $room
120	 * @param array $params
121	 */
122	public function createRoom($room, array $params = [])
123	{
124		global $prefs;
125		$cachelib = TikiLib::lib('cache');
126		$tikilib = TikiLib::lib('tiki');
127		$params = array_merge(
128			['logout' => $tikilib->tikiUrl(''),],
129			$params
130		);
131
132		$request = [
133				'name' => $room,
134				'meetingID' => $room,
135				'logoutURL' => $params['logout'],
136		];
137
138		if (isset($params['welcome'])) {
139			$request['welcome'] = $params['welcome'];
140		}
141
142		if (isset($params['number'])) {
143			$request['dialNumber'] = $params['number'];
144		}
145
146		if (isset($params['voicebridge'])) {
147			$request['voiceBridge'] = $params['voicebridge'];
148		} else {
149			$request['voiceBridge'] = '7' . mt_rand(0, 9999);
150		}
151
152		if (isset($params['logout'])) {
153			$request['logoutURL'] = $tikilib->tikiUrl($params['logout']);
154		}
155
156		if (isset($params['recording']) && $params['recording'] > 0 && $this->isRecordingSupported()) {
157			$request['record'] = 'true';
158			$request['duration'] = $prefs['bigbluebutton_recording_max_duration'];
159		}
160
161		$this->performRequest('create', $request);
162		$cachelib->invalidate('bbb_meetinglist');
163	}
164
165	public function configureRoom($meetingName, $configuration)
166	{
167		global $prefs;
168
169		if (empty($configuration) || ! $this->isDynamicConfigurationSupported()) {
170			return null;
171		}
172
173		$content = $this->performRequest('getDefaultConfigXML', ['random' => '1'], false);
174
175		if (! $content) {
176			return null;
177		}
178
179		$config = new Tiki\BigBlueButton\Configuration($content);
180
181		if (isset($configuration['presentation']['active']) && ! $configuration['presentation']['active']) {
182			$config->removeModule('PresentModule');
183		}
184		$content = $config->getXml();
185
186		$parameters = [
187			'meetingID' => $meetingName,
188			'configXML' => rawurlencode($content),
189		];
190		$tikilib = TikiLib::lib('tiki');
191		$checksum = $this->generateChecksum('setConfigXML', $parameters);
192		$client = $tikilib->get_http_client($this->getBaseUrl('/api/setConfigXML.xml') . '?');
193		$client->setParameterPost(
194			[
195				'meetingID' => $meetingName,
196				'configXML' => rawurlencode($content),
197				'checksum' => $checksum,
198			]
199		);
200
201		$client->getRequest()->setMethod(Zend\Http\Request::METHOD_POST);
202		$response = $client->send();
203		$document = $response->getBody();
204
205		$dom = new DOMDocument;
206		$dom->loadXML($document);
207
208		$values = $this->grabValues($dom->documentElement);
209
210		if ($values['returncode'] == 'SUCCESS') {
211			return $values['configToken'];
212		}
213	}
214
215	/**
216	 * @param $room
217	 */
218	public function joinMeeting($room, $configToken = null)
219	{
220		$version = $this->getVersion();
221
222		$name = $this->getAttendeeName();
223		$password = $this->getAttendeePassword($room);
224
225		if ($name && $password) {
226			TikiLib::lib('logs')->add_action('Joined Room', $room, 'bigbluebutton');
227			$this->joinRawMeeting($room, $name, $password, $configToken);
228		}
229	}
230
231	/**
232	 * @param $recordingID
233	 */
234	public function removeRecording($recordingID)
235	{
236		if ($this->isRecordingSupported()) {
237			$this->performRequest(
238				'deleteRecordings',
239				['recordID' => $recordingID]
240			);
241		}
242	}
243
244	/**
245	 * @return bool|mixed|null|string
246	 */
247	private function getAttendeeName()
248	{
249		global $user, $tikilib;
250
251		if ($realName = $tikilib->get_user_preference($user, 'realName')) {
252			$realName .= " (" . $user . ")";
253			return $realName;
254		} elseif ($user) {
255			return $user;
256		} elseif (! empty($_SESSION['bbb_name'])) {
257			return $_SESSION['bbb_name'];
258		} else {
259			return tra('anonymous');
260		}
261	}
262
263	/**
264	 * @param $room
265	 * @return mixed
266	 */
267	private function getAttendeePassword($room)
268	{
269		if ($meeting = $this->getMeeting($room)) {
270			$perms = Perms::get('bigbluebutton', $room);
271
272			if ($perms->bigbluebutton_moderate) {
273				return $meeting['moderatorPW'];
274			} else {
275				return $meeting['attendeePW'];
276			}
277		}
278	}
279
280	/**
281	 * @param $room
282	 * @return mixed
283	 */
284	private function getMeeting($room)
285	{
286		$meetings = $this->getMeetings();
287
288		foreach ($meetings as $meeting) {
289			if ($meeting['meetingID'] == $room) {
290				return $meeting;
291			}
292		}
293	}
294
295	/**
296	 * @param $room
297	 * @param $name
298	 * @param $password
299	 */
300	public function joinRawMeeting($room, $name, $password, $configToken = null)
301	{
302		$parameters = [
303			'meetingID' => $room,
304			'fullName' => $name,
305			'password' => $password,
306		];
307
308		if ($configToken) {
309			$parameters['configToken'] = $configToken;
310		}
311
312		$url = $this->buildUrl('join', $parameters);
313
314		header('Location: ' . $url);
315		exit;
316	}
317
318	/**
319	 * @param $action
320	 * @param array $parameters
321	 * @return DOMDocument
322	 */
323	private function performRequest($action, array $parameters, $checkSuccess = true)
324	{
325		global $tikilib;
326
327		$url = $this->buildUrl($action, $parameters);
328
329		if ($result = $tikilib->httprequest($url)) {
330			$dom = new DOMDocument;
331			if ($dom->loadXML($result)) {
332				$nodes = $dom->getElementsByTagName('returncode');
333
334				if (! $checkSuccess) {
335					return $dom;
336				}
337
338				if ($nodes->length > 0 && ($returnCode = $nodes->item(0)) && $returnCode->textContent == 'SUCCESS') {
339					return $dom;
340				}
341			}
342		}
343	}
344
345	/**
346	 * @param $action
347	 * @param array $parameters
348	 * @return string
349	 */
350	private function buildUrl($action, array $parameters)
351	{
352		if ($action) {
353			if ($checksum = $this->generateChecksum($action, $parameters)) {
354				$parameters['checksum'] = $checksum;
355			}
356		}
357
358		$url = $this->getBaseUrl("/api/$action");
359		$url .= "?" . http_build_query($parameters, '', '&');
360		return $url;
361	}
362
363	private function getBaseUrl($path)
364	{
365		global $prefs;
366
367		$base = rtrim($prefs['bigbluebutton_server_location'], '/');
368		if (false === strpos($base, '/bigbluebutton')) {
369			$base .= '/bigbluebutton';
370		}
371
372		$url = "$base$path";
373
374		return $url;
375	}
376
377	/**
378	 * @param $action
379	 * @param array $parameters
380	 * @return string
381	 */
382	private function generateChecksum($action, array $parameters)
383	{
384		global $prefs;
385
386		if ($prefs['bigbluebutton_server_salt']) {
387			$query = http_build_query($parameters, '', '&');
388
389			$version = $this->getVersion();
390
391			if (-1 === version_compare($version, '0.7')) {
392				return sha1($query . $prefs['bigbluebutton_server_salt']);
393			} else {
394				return sha1($action . $query . $prefs['bigbluebutton_server_salt']);
395			}
396		}
397	}
398
399	/**
400	 * @return bool
401	 */
402	private function isRecordingSupported()
403	{
404		$version = $this->getVersion();
405		return version_compare($version, '0.8') >= 0;
406	}
407
408	/**
409	 * @return bool
410	 */
411	private function isDynamicConfigurationSupported()
412	{
413		global $prefs;
414		return $prefs['bigbluebutton_dynamic_configuration'] == 'y';
415	}
416
417	/**
418	 * @param $room
419	 * @return array
420	 */
421	public function getRecordings($room)
422	{
423		if (! $this->isRecordingSupported()) {
424			return [];
425		}
426
427		$result = $this->performRequest(
428			'getRecordings',
429			['meetingID' => $room,]
430		);
431
432		$data = [];
433		$recordings = $result->getElementsByTagName('recording');
434
435		foreach ($recordings as $recording) {
436			$recording = simplexml_import_dom($recording);
437			if ($recording->published == 'false') {
438				$published = false;
439			} else {
440				$published = true;
441			}
442			$info = [
443					'recordID' => (string) $recording->recordID,
444					'startTime' => floor(((string) $recording->startTime) / 1000),
445					'endTime' => ceil(((string) $recording->endTime) / 1000),
446					'playback' => [],
447					'published' => $published,
448			];
449
450			foreach ($recording->playback as $playback) {
451				$info['playback'][ (string) $playback->format->type ] = (string) $playback->format->url;
452			}
453
454			$data[] = $info;
455		}
456
457		usort($data, ["BigBlueButtonLib", "cmpStartTime"]);
458		return $data;
459	}
460
461	/**
462	 * @param $a
463	 * @param $b
464	 * @return int
465	 */
466	private static function cmpStartTime($a, $b)
467	{
468		if ($a['startTime'] == $b['startTime']) {
469			return 0;
470		}
471		return ($a['startTime'] > $b['startTime']) ? -1 : 1;
472	}
473}
474