1<?php /** @noinspection DuplicatedCode */ 2/** 3 * Plugin Airtable: Syncs Airtable Content to dokuWiki 4 * 5 * Syntax: <airtable>TYPE: xxx, TABLE: xxx, WHERE, .......</airtable> - will be replaced with airtable content 6 * 7 * @license GPL 3 (https://www.gnu.org/licenses/quick-guide-gplv3.html) 8 * @author Cameron Ward <cameronward007@gmail.com> 9 */ 10// must be run within DokuWiki 11if(!defined('DOKU_INC')) die(); 12 13/** 14 * Class InvalidAirtableString 15 * 16 * Handles the airtable query string exception and 17 * 18 */ 19class InvalidAirtableString extends Exception { 20 public function errorMessage(): string { 21 return $this->getMessage(); 22 } 23} 24 25/** 26 * All DokuWiki plugins to extend the parser/rendering mechanism 27 * need to inherit from this class 28 */ 29class syntax_plugin_airtable extends DokuWiki_Syntax_Plugin { 30 31 /** 32 * Get the type of syntax this plugin defines. 33 * 34 * @param 35 * @return String <tt>'substition'</tt> (i.e. 'substitution'). 36 * @public 37 * @static 38 */ 39 function getType(): string { 40 return 'substition'; 41 } 42 43 /** 44 * Define how this plugin is handled regarding paragraphs. 45 * 46 * <p> 47 * This method is important for correct XHTML nesting. It returns 48 * one of the following values: 49 * </p> 50 * <dl> 51 * <dt>normal</dt><dd>The plugin can be used inside paragraphs.</dd> 52 * <dt>block</dt><dd>Open paragraphs need to be closed before 53 * plugin output.</dd> 54 * <dt>stack</dt><dd>Special case: Plugin wraps other paragraphs.</dd> 55 * </dl> 56 * @param 57 * @return String <tt>'block'</tt>. 58 * @public 59 * @static 60 */ 61 function getPType(): string { 62 return 'normal'; 63 } 64 65 /** 66 * Where to sort in? 67 * 68 * @param 69 * @return Integer <tt>6</tt>. 70 * @public 71 * @static 72 */ 73 function getSort(): int { 74 return 1; 75 } 76 77 /** 78 * Connect lookup pattern to lexer. 79 * 80 * @param $mode //The desired rendermode. 81 * @return void 82 * @public 83 * @see render() 84 */ 85 function connectTo($mode) { 86 $this->Lexer->addEntryPattern('{{airtable>', $mode, 'plugin_airtable'); 87 } 88 89 function postConnect() { 90 $this->Lexer->addExitPattern('}}', 'plugin_airtable'); 91 } 92 93 /** 94 * Handler to prepare matched data for the rendering process. 95 * 96 * <p> 97 * The <tt>$aState</tt> parameter gives the type of pattern 98 * which triggered the call to this method: 99 * </p> 100 * <dl> 101 * <dt>DOKU_LEXER_ENTER</dt> 102 * <dd>a pattern set by <tt>addEntryPattern()</tt></dd> 103 * <dt>DOKU_LEXER_MATCHED</dt> 104 * <dd>a pattern set by <tt>addPattern()</tt></dd> 105 * <dt>DOKU_LEXER_EXIT</dt> 106 * <dd> a pattern set by <tt>addExitPattern()</tt></dd> 107 * <dt>DOKU_LEXER_SPECIAL</dt> 108 * <dd>a pattern set by <tt>addSpecialPattern()</tt></dd> 109 * <dt>DOKU_LEXER_UNMATCHED</dt> 110 * <dd>ordinary text encountered within the plugin's syntax mode 111 * which doesn't match any pattern.</dd> 112 * </dl> 113 * @param $match //String The text matched by the patterns. 114 * @param $state //Integer The lexer state for the match. 115 * @param $pos //Integer The character position of the matched text. 116 * @param $handler //Object Reference to the Doku_Handler object. 117 * @return array The current lexer state for the match. 118 * @public 119 * @see render() 120 * @static 121 * @throws InvalidEmbed 122 */ 123 function handle($match, $state, $pos, $handler): array { 124 switch($state) { 125 case DOKU_LEXER_EXIT: 126 case DOKU_LEXER_ENTER : 127 /** @var array $data */ 128 $data = array(); 129 return $data; 130 131 case DOKU_LEXER_SPECIAL: 132 case DOKU_LEXER_MATCHED : 133 break; 134 135 case DOKU_LEXER_UNMATCHED : 136 if(!empty($match)) { 137 try { 138 define('BASE_ID', $this->getConf('Base_ID')); 139 define('API_KEY', $this->getConf('API_Key')); 140 define('MAX_RECORDS', $this->getConf('Max_Records')); 141 $user_string = $match; 142 $display_type = $this->getDisplayType($user_string); //check type is set correctly 143 // MAIN PROGRAM: 144 switch(true) { //parse string based on type set 145 case ($display_type === "tbl"): 146 $parameter_array = $this->parseTableString($user_string); 147 $api_response = $this->sendTableRequest($parameter_array); 148 $parameter_array['thumbnails'] = $this->findMedia($api_response); 149 if(count($api_response['records']) == 1) { //if query resulted in one record, render as a template: 150 $html = $this->renderRecord($parameter_array, $api_response['records'][0]); 151 } else { 152 $html = $this->renderTable($parameter_array, $api_response); 153 } 154 return array('airtable_html' => $html); 155 case ($display_type === "record"): 156 $parameter_array = $this->parseRecordString($user_string); 157 $api_response = $this->sendRecordRequest($parameter_array); 158 $parameter_array['thumbnails'] = $this->findMedia($api_response); 159 $html = $this->renderRecord($parameter_array, $api_response); 160 return array('airtable_html' => $html); 161 case ($display_type === "img"): 162 $parameter_array = $this->parseImageString($user_string); 163 $api_response = $this->sendRecordRequest($parameter_array); 164 $thumbnails = $this->findMedia($api_response); 165 if($thumbnails === false or $thumbnails === null) { 166 throw new InvalidAirtableString("Unknown 'parseImageRequest' error"); 167 } 168 $html = $this->renderMedia($parameter_array, $thumbnails, "max-width: 250px;"); 169 return array('airtable_html' => $html); 170 case ($display_type === "txt"): 171 $parameter_array = $this->parseTextString($user_string); 172 $api_response = $this->sendRecordRequest($parameter_array); 173 $html = $this->renderText($parameter_array, $api_response); 174 return array('airtable_html' => $html); 175 default: 176 throw new InvalidEmbed("Unknown Embed Type"); 177 } 178 } catch(InvalidAirtableString $e) { 179 $html = "<p style='color: red; font-weight: bold;'>Airtable Error: " . $e->getMessage() . "</p>"; 180 return array('airtable_html' => $html); 181 } 182 } 183 } 184 $data = array(); 185 return $data; 186 } 187 188 /** 189 * Handle the actual output creation. 190 * 191 * <p> 192 * The method checks for the given <tt>$aFormat</tt> and returns 193 * <tt>FALSE</tt> when a format isn't supported. <tt>$aRenderer</tt> 194 * contains a reference to the renderer object which is currently 195 * handling the rendering. The contents of <tt>$aData</tt> is the 196 * return value of the <tt>handle()</tt> method. 197 * </p> 198 * @param $mode //String The output format to generate. 199 * @param $renderer Doku_Renderer A reference to the renderer object. 200 * @param $data //Array The data created by the <tt>handle()</tt> 201 * method. 202 * @return Boolean <tt>TRUE</tt> if rendered successfully, or 203 * <tt>FALSE</tt> otherwise. 204 * @public 205 * @see handle() 206 * @noinspection PhpParameterNameChangedDuringInheritanceInspection 207 */ 208 public 209 function render($mode, Doku_Renderer $renderer, $data): bool { 210 //<airtable>Type: Image, Table: tblwWxohDeMeAAzdW, WHERE: {Ref #} = 19, image-size: small, alt-tag: marble-machine-x</airtable> 211 212 if($mode != 'xhtml') return false; 213 214 if(!empty($data['airtable_html'])) { 215 $renderer->doc .= $data['airtable_html']; 216 return true; 217 } else { 218 return false; 219 } 220 } 221 222 /** 223 * Method for rendering a table 224 * 225 * @param $parameter_array 226 * @param $api_response 227 * @return string 228 * @throws InvalidAirtableString 229 */ 230 private 231 function renderTable($parameter_array, $api_response): string { 232 $html = '<div style="overflow-x: auto"><table class="airtable-table"><thead><tr>'; 233 foreach($parameter_array['fields'] as $field) { 234 $html .= '<th>' . $field . '</th>'; 235 } 236 $html .= '</tr></thead><tbody>'; 237 foreach($api_response['records'] as $record) { 238 $html .= '<tr>'; 239 foreach($parameter_array['fields'] as $field) { 240 if(is_array($record['fields'][$field])) { 241 if($image = $this->findMedia($record['fields'][$field])) { 242 $field = $this->renderMedia($parameter_array, $image); 243 $html .= '<td>' . $field . '</td>'; 244 continue; 245 } 246 } 247 $html .= '<td>' . $this->renderAnyExternalLinks(htmlspecialchars($record['fields'][$field])) . '</td>'; 248 } 249 $html .= '</tr>'; 250 } 251 $html .= '</tbody></table></div>'; 252 return $html; 253 } 254 255 /** 256 * Private Method for rendering a single record. 257 * Fields and field data appear on the left. If there is an image present, 258 * it will appear to the top right of the text 259 * 260 * @param $parameter_array 261 * @param $api_response 262 * @return string 263 * @throws InvalidAirtableString 264 */ 265 private 266 function renderRecord($parameter_array, $api_response): string { 267 $fields = $parameter_array['fields']; 268 $html = '<div class="airtable-record">'; 269 if($parameter_array['thumbnails'] !== false) { 270 $parameter_array['image-size'] = "large"; 271 $image_styles = 'float: right; max-width: 350px; margin-left: 10px'; 272 $html .= $this->renderMedia($parameter_array, $parameter_array['thumbnails'], $image_styles); 273 } 274 foreach($fields as $field) { 275 if(!array_key_exists($field, $api_response['fields'])) { //if field is not present in array: 276 throw new InvalidAirtableString("Invalid field name: " . htmlspecialchars($field)); 277 } 278 if(is_array($api_response['fields'][$field])) { 279 continue; 280 } 281 $html .= ' 282 <div> 283 <h3>' . $field . '</h3> 284 <p>' . $this->renderAnyExternalLinks($api_response['fields'][$field]) . '</p> 285 </div>'; 286 } 287 $html .= '<div style="clear: both;"></div>'; 288 $html .= '</div>'; 289 return $html; 290 } 291 292 /** 293 * Generates HTML for rendering a single image: 294 * 295 * @param $data 296 * @param $images 297 * @param string $image_styles 298 * @return string 299 * @throws InvalidAirtableString 300 */ 301 private 302 function renderImage($data, $images, $image_styles = ""): string { 303 if(!key_exists('thumbnails', $images)) { 304 throw new InvalidAirtableString('Could not find thumbnails in image query'); 305 } 306 if($data['position'] == "centre") { 307 $position = "mediacenter"; 308 } elseif($data['position'] == 'right') { 309 $position = "mediaright"; 310 } elseif($data['position'] == "left") { 311 $position = "medialeft"; 312 } else { 313 $position = ''; 314 } 315 316 if(!key_exists('image-size', $data)) { 317 $data['image-size'] = 'large'; 318 } 319 return ' 320 <div> 321 <a href="' . $images['thumbnails']['full']['url'] . '" target="_blank" rel="noopener" title="' . $images["filename"] . '"> 322 <img alt ="' . $data['alt-tag'] . '" src="' . $images['thumbnails'][$data['image-size']]['url'] . '" style="' . $image_styles . '" class="airtable-image ' . $position . '"> 323 </a> 324 </div>'; 325 } 326 327 /** 328 * Private method for rendering text. 329 * 330 * @param $parameter_array 331 * @param $api_response 332 * @return string 333 * @throws InvalidAirtableString 334 */ 335 private 336 function renderText($parameter_array, $api_response): string { 337 $fields = $parameter_array['fields']; 338 $html = ''; 339 foreach($fields as $field) { 340 if(!array_key_exists($field, $api_response['fields'])) { //if field is not present in array: 341 throw new InvalidAirtableString("Invalid field name: " . htmlspecialchars($field)); 342 } 343 $html .= $this->renderAnyExternalLinks(htmlspecialchars($api_response['fields'][$field])) . ' '; 344 } 345 $html = rtrim($html); 346 return $html; 347 } 348 349 /** 350 * Method that chooses the correct rendering type for the given media: 351 * 352 * @param $data 353 * @param $media 354 * @param string $media_styles 355 * @return string 356 * @throws InvalidAirtableString 357 */ 358 private 359 function renderMedia($data, $media, $media_styles = ""): string { 360 $type = $media['type']; 361 if($type == 'image/jpeg' || $type == 'image/jpg' || $type == 'image/png') { 362 return $this->renderImage($data, $media, $media_styles); 363 } 364 if($type == 'video/mp4' || $type == 'video/quicktime') { 365 return $this->renderVideo($media, $media_styles); 366 } 367 throw new InvalidAirtableString("Unknown media type: " . $type); 368 } 369 370 /** 371 * Generates HTML for rendering a video 372 * 373 * @param $video 374 * @param $video_styles 375 * @return string 376 */ 377 private 378 function renderVideo($video, $video_styles): string { 379 return '<video controls class="airtable-video" style="' . $video_styles . '"><source src="' . $video["url"] . '" type="video/mp4"></video>'; 380 } 381 382 /** 383 * Sets the required parameters for type: table 384 * 385 * @param $user_string 386 * @return array 387 * @throws InvalidAirtableString 388 */ 389 private 390 function parseTableString($user_string): array { 391 $table_parameter_types = array("type" => true, "table" => true, "fields" => true, "record-url" => true, "where" => "", "order-by" => "", "order" => "asc", "max-records" => ""); 392 $table_parameter_values = array("order" => ["asc", "desc"]); 393 $table_query = $this->decodeRecordURL($this->getParameters($user_string)); 394 return $this->checkParameters($table_query, $table_parameter_types, $table_parameter_values); 395 } 396 397 /** 398 * Sets the required parameters for type: record 399 * @param $user_string 400 * @return array 401 * @throws InvalidAirtableString 402 */ 403 private 404 function parseRecordString($user_string): array { 405 $record_parameter_types = array("type" => true, "record-url" => true, "table" => true, "fields" => true, "record-id" => true, "alt-tag" => ""); 406 $record_parameter_values = array(); 407 $record_query = $this->decodeRecordURL($this->getParameters($user_string)); 408 return $this->checkParameters($record_query, $record_parameter_types, $record_parameter_values); 409 } 410 411 /** 412 * Sets the required parameters for type: image 413 * Also sets accepted values for specific parameters 414 * 415 * @param $user_string 416 * @return array The decoded string with the parameter names stored as keys 417 * @throws InvalidAirtableString 418 */ 419 private 420 function parseImageString($user_string): array { 421 $image_parameter_types = array("type" => true, "record-url" => true, 'table' => true, 'record-id' => true, "alt-tag" => "", "image-size" => "large", "position" => "block"); // accepted parameter names with default values or true if parameter is required. 422 $image_parameter_values = array("image-size" => ["", "small", "large", "full"], "position" => ['', 'left', 'centre', 'right', 'block']); // can be empty (substitute default), small, large, full 423 $image_query = $this->decodeRecordURL($this->getParameters($user_string)); 424 return $this->checkParameters($image_query, $image_parameter_types, $image_parameter_values); 425 } 426 427 /** 428 * Sets the required parameters for type: text 429 * 430 * @param $user_string 431 * @return array 432 * @throws InvalidAirtableString 433 */ 434 private 435 function parseTextString($user_string): array { 436 $text_parameter_types = array("type" => true, "table" => true, "fields" => true, "record-id" => true, "record-url" => true); 437 $text_parameter_values = array(); 438 $text_query = $this->decodeRecordURL($this->getParameters($user_string)); 439 return $this->checkParameters($text_query, $text_parameter_types, $text_parameter_values); 440 } 441 442 /** 443 * parse query string and return the type 444 * 445 * @param $user_string //data between airtable tags e.g.: <airtable>user_string</airtable> 446 * @return string //the display type (image, table, text) 447 * @throws InvalidAirtableString 448 */ 449 private 450 function getDisplayType($user_string): string { 451 $type = substr($user_string, 0, strpos($user_string, " | ")); 452 if($type == "") { 453 throw new InvalidAirtableString("Missing Type Parameter / Not Enough Parameters"); 454 } 455 $decoded_string = explode("type: ", strtolower($type))[1]; 456 $decoded_type = str_replace('"', '', $decoded_string); 457 if($decoded_type == null) { 458 throw new InvalidAirtableString("Missing Type Parameter"); 459 } 460 $decoded_type = strtolower($decoded_type); 461 $accepted_types = array("img", "image", "picture", "text", "txt", "table", "tbl", "record"); 462 if(array_search($decoded_type, $accepted_types) === false) { 463 throw new InvalidAirtableString( 464 "Invalid Type Parameter: " . htmlspecialchars($decoded_type) . " 465 <br>Accepted Types: " . implode(" | ", $accepted_types) 466 ); 467 } 468 //Set to a standard type: 469 if($decoded_type == "img" || $decoded_type == "image" || $decoded_type == "picture") { 470 $decoded_type = "img"; 471 } 472 if($decoded_type == "text") { 473 $decoded_type = "txt"; 474 } 475 if($decoded_type == "table") { 476 $decoded_type = "tbl"; 477 } 478 return $decoded_type; 479 } 480 481 /** 482 * Splits the query string into an associative array of Type => Value pairs 483 * 484 * @param $user_string string The user's airtable query 485 * @return array 486 */ 487 private 488 function getParameters(string $user_string): array { 489 $query = array(); 490 $string_array = explode(' | ', $user_string); 491 foreach($string_array as $item) { 492 $parameter = explode(": ", $item); //creates key value pairs for parameters e.g. [type] = "image" 493 $query[strtolower($parameter[0])] = trim(str_replace('"', '', $parameter[1])); //removes quotes 494 } 495 if(array_key_exists("fields", $query)) { // separate field names into an array if it exists 496 $fields = array_map("trim", explode(",", $query['fields'])); //todo: url encode fields here? 497 $query['fields'] = $fields; 498 } 499 return $query; 500 } 501 502 /** 503 * Extracts the table, view and record ID's from record-url 504 * 505 * @param $query 506 * @return mixed 507 * @throws InvalidAirtableString 508 */ 509 private 510 function decodeRecordURL($query) { 511 if(array_key_exists("record-url", $query)) { 512 //// "tbl\w+|viw\w+|rec\w+/ig" One line preg match? 513 preg_match("/tbl\w+/i", $query["record-url"], $table); //extract table, view, record from url 514 preg_match("/viw\w+/i", $query["record-url"], $view); 515 preg_match("/rec\w+/i", $query["record-url"], $record_id); 516 517 $query['table'] = urlencode($table[0]); //url encode each part 518 $query['view'] = urlencode($view[0]); 519 $query['record-id'] = urlencode($record_id[0]); 520 return $query; 521 } else { 522 throw new InvalidAirtableString("Missing record-url parameter"); 523 } 524 } 525 526 /** 527 * Checks query parameters to make sure: 528 * Required parameters are present 529 * Missing parameters are substituted with default params 530 * Parameter values match expected values 531 * 532 * @param $query_array array 533 * @param $required_parameters array 534 * @param $parameter_values array 535 * @return array // query array with added default parameters 536 * @throws InvalidAirtableString 537 */ 538 private 539 function checkParameters(array &$query_array, array $required_parameters, array $parameter_values): array { 540 foreach($required_parameters as $key => $value) { 541 if(!array_key_exists($key, $query_array)) { // if parameter is missing: 542 if($value === true) { // check if parameter is required 543 throw new InvalidAirtableString("Missing Parameter: " . $key); 544 } 545 $query_array[$key] = $value; // substitute default 546 } 547 if(($query_array[$key] == null || $query_array[$key] === "") && $value === true) { //if parameter is required but value is not present 548 throw new InvalidAirtableString("Missing Parameter Value for: '" . $key . "'."); 549 } 550 if(array_key_exists($key, $parameter_values)) { //check accepted parameter_values array 551 if(!in_array($query_array[$key], $parameter_values[$key])) { //if parameter value is not accepted: 552 $message = "Invalid Parameter Value: '" . htmlspecialchars($query_array[$key]) . "' for Key: '" . $key . "'. 553 <br>Possible values: " . implode(" | ", $parameter_values[$key]); 554 if(in_array("", $parameter_values[$key])) { 555 $message .= " or ''"; 556 } 557 throw new InvalidAirtableString($message); 558 } 559 } 560 } 561 return $query_array; 562 } 563 564 /** 565 * Method for checking text and replacing links with <a> tags for external linking 566 * If there are no links present, return the text with no modification 567 * 568 * @param $string // The string to find links in 569 * @return string 570 */ 571 private 572 function renderAnyExternalLinks($string): string { 573 $regular_expression = "/(?i)\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))/"; 574 575 if(preg_match_all($regular_expression, $string, $url_matches)) { // store all url matches in the $url array 576 foreach($url_matches[0] as $link) { 577 if(strstr($link, ':') === false) { //if link is missing http, add it to the front of the url 578 $url = 'http://' . $link; 579 } else { 580 $url = $link; 581 } 582 $search = $link; 583 $replace = '<a href = "' . $url . '" title = "' . $link . '" target = "_blank" rel = "noopener" class = "urlextern">' . $url . '</a>'; 584 $string = str_replace($search, $replace, $string); 585 } 586 } 587 return $string; 588 } 589 590 /** 591 * Recursive method to find an array (needle) within the JSON api_response (haystack) 592 * 593 * @param $haystack 594 * @param string $needle 595 * @return false|array 596 */ 597 private 598 function findMedia($haystack, $needle = "type") { 599 foreach($haystack as $key) { 600 if(is_array($key)) { 601 if(array_key_exists($needle, $key)) { 602 return $key; 603 } 604 $search = $this->findMedia($key, $needle); 605 if($search === false) { 606 continue; 607 } else { 608 return $search; // image attachment found 609 } 610 } 611 } 612 return false; 613 } 614 615 /** 616 * Method to encode a record request 617 * 618 * @param $data 619 * @return false|string //JSON String 620 * @throws InvalidAirtableString 621 */ 622 private 623 function sendRecordRequest($data) { 624 $request = $data['table'] . '/' . urlencode($data['record-id']); 625 return $this->sendRequest($request); 626 } 627 628 /** 629 * Method to encode a table request 630 * 631 * @param $data 632 * @return false|string 633 * @throws InvalidAirtableString 634 */ 635 private 636 function sendTableRequest($data) { 637 $request = $data['table'] . '?'; 638 //Add each field to the request string 639 foreach($data['fields'] as $index => $field) { 640 if($index >= 1) { 641 $request .= '&' . urlencode('fields[]') . '=' . urlencode($field); 642 } else { 643 $request .= urlencode('fields[]') . '=' . urlencode($field); //don't add a '&' for the first field 644 } 645 } 646 647 //add filter: 648 if(key_exists('where', $data)) { 649 if($data['where'] !== "") { 650 $request .= '&filterByFormula=' . urlencode($data['where']); 651 } 652 } 653 654 //Set max records: 655 if(key_exists('max-records', $data)) { 656 if($data['max-records'] !== "") { 657 if((int) $data['max-records'] <= MAX_RECORDS) { 658 $max_records = $data['max-records']; 659 } else { 660 $max_records = MAX_RECORDS; 661 } 662 } else { 663 $max_records = MAX_RECORDS; 664 } 665 } else { 666 $max_records = MAX_RECORDS; 667 } 668 $request .= '&maxRecords=' . $max_records; 669 670 //set order by which field and order direction: 671 if(key_exists('order', $data)) { 672 $order = $data['order']; 673 } else { 674 $order = "asc"; 675 } 676 677 if(key_exists('order-by', $data)) { 678 if($data['order-by'] !== "") { 679 $request .= '&' . urlencode('sort[0][field]') . '=' . urlencode($data['order-by']); 680 $request .= '&' . urlencode('sort[0][direction]') . '=' . urlencode($order); 681 } 682 } 683 684 return $this->sendRequest($request); 685 } 686 687 /** 688 * Method to call the airtable API 689 * 690 * @param $request 691 * @return false|string 692 * @throws InvalidAirtableString 693 */ 694 private 695 function sendRequest($request) { 696 $url = 'https://api.airtable.com/v0/' . BASE_ID . '/' . $request; 697 $curl = curl_init($url); 698 curl_setopt($curl, CURLOPT_URL, $url); 699 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 700 701 $headers = array( 702 'Authorization: Bearer ' . API_KEY 703 ); 704 705 curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); 706 707 //TODO: remove once in production: 708 curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); 709 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);// 710 711 $api_response = json_decode(curl_exec($curl), true); //decode JSON to associative array 712 713 if(curl_getinfo($curl, CURLINFO_HTTP_CODE) != 200) { 714 if(key_exists("error", $api_response)) { 715 $message = $api_response['error']['message']; 716 } else { 717 $message = "Unknown API api_response error"; 718 } 719 throw new InvalidAirtableString($message); 720 } 721 curl_close($curl); 722 return $api_response; 723 } 724}