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