Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]
1 <?php 2 /** 3 * Helper functions. 4 * 5 * Less commonly used functions are placed here to reduce size of adodb.inc.php. 6 * 7 * This file is part of ADOdb, a Database Abstraction Layer library for PHP. 8 * 9 * @package ADOdb 10 * @link https://adodb.org Project's web site and documentation 11 * @link https://github.com/ADOdb/ADOdb Source code and issue tracker 12 * 13 * The ADOdb Library is dual-licensed, released under both the BSD 3-Clause 14 * and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option, 15 * any later version. This means you can use it in proprietary products. 16 * See the LICENSE.md file distributed with this source code for details. 17 * @license BSD-3-Clause 18 * @license LGPL-2.1-or-later 19 * 20 * @copyright 2000-2013 John Lim 21 * @copyright 2014 Damien Regad, Mark Newnham and the ADOdb community 22 */ 23 24 // security - hide paths 25 if (!defined('ADODB_DIR')) die(); 26 27 global $ADODB_INCLUDED_LIB; 28 $ADODB_INCLUDED_LIB = 1; 29 30 /** 31 * Strip the ORDER BY clause from the outer SELECT. 32 * 33 * @param string $sql 34 * 35 * @return string 36 */ 37 function adodb_strip_order_by($sql) 38 { 39 $num = preg_match_all('/(\sORDER\s+BY\s(?:[^)](?!LIMIT))*)/is', $sql, $matches, PREG_OFFSET_CAPTURE); 40 if ($num) { 41 // Get the last match 42 list($last_order_by, $offset) = array_pop($matches[1]); 43 44 // If we find a ')' after the last order by, then it belongs to a 45 // sub-query, not the outer SQL statement and should not be stripped 46 if (strpos($sql, ')', $offset) === false) { 47 $sql = str_replace($last_order_by, '', $sql); 48 } 49 } 50 return $sql; 51 } 52 53 function adodb_probetypes($array,&$types,$probe=8) 54 { 55 // probe and guess the type 56 $types = array(); 57 if ($probe > sizeof($array)) $max = sizeof($array); 58 else $max = $probe; 59 60 61 for ($j=0;$j < $max; $j++) { 62 $row = $array[$j]; 63 if (!$row) break; 64 $i = -1; 65 foreach($row as $v) { 66 $i += 1; 67 68 if (isset($types[$i]) && $types[$i]=='C') continue; 69 70 //print " ($i ".$types[$i]. "$v) "; 71 $v = trim($v); 72 73 if (!preg_match('/^[+-]{0,1}[0-9\.]+$/',$v)) { 74 $types[$i] = 'C'; // once C, always C 75 76 continue; 77 } 78 if ($j == 0) { 79 // If empty string, we presume is character 80 // test for integer for 1st row only 81 // after that it is up to testing other rows to prove 82 // that it is not an integer 83 if (strlen($v) == 0) $types[$i] = 'C'; 84 if (strpos($v,'.') !== false) $types[$i] = 'N'; 85 else $types[$i] = 'I'; 86 continue; 87 } 88 89 if (strpos($v,'.') !== false) $types[$i] = 'N'; 90 91 } 92 } 93 94 } 95 96 function adodb_transpose(&$arr, &$newarr, &$hdr, $fobjs) 97 { 98 $oldX = sizeof(reset($arr)); 99 $oldY = sizeof($arr); 100 101 if ($hdr) { 102 $startx = 1; 103 $hdr = array('Fields'); 104 for ($y = 0; $y < $oldY; $y++) { 105 $hdr[] = $arr[$y][0]; 106 } 107 } else 108 $startx = 0; 109 110 for ($x = $startx; $x < $oldX; $x++) { 111 if ($fobjs) { 112 $o = $fobjs[$x]; 113 $newarr[] = array($o->name); 114 } else 115 $newarr[] = array(); 116 117 for ($y = 0; $y < $oldY; $y++) { 118 $newarr[$x-$startx][] = $arr[$y][$x]; 119 } 120 } 121 } 122 123 124 function _adodb_replace($zthis, $table, $fieldArray, $keyCol, $autoQuote, $has_autoinc) 125 { 126 // Add Quote around table name to support use of spaces / reserved keywords 127 $table=sprintf('%s%s%s', $zthis->nameQuote,$table,$zthis->nameQuote); 128 129 if (count($fieldArray) == 0) return 0; 130 131 if (!is_array($keyCol)) { 132 $keyCol = array($keyCol); 133 } 134 $uSet = ''; 135 foreach($fieldArray as $k => $v) { 136 if ($v === null) { 137 $v = 'NULL'; 138 $fieldArray[$k] = $v; 139 } else if ($autoQuote && /*!is_numeric($v) /*and strncmp($v,"'",1) !== 0 -- sql injection risk*/ strcasecmp($v,$zthis->null2null)!=0) { 140 $v = $zthis->qstr($v); 141 $fieldArray[$k] = $v; 142 } 143 if (in_array($k,$keyCol)) continue; // skip UPDATE if is key 144 145 // Add Quote around column name to support use of spaces / reserved keywords 146 $uSet .= sprintf(',%s%s%s=%s',$zthis->nameQuote,$k,$zthis->nameQuote,$v); 147 } 148 $uSet = ltrim($uSet, ','); 149 150 // Add Quote around column name in where clause 151 $where = ''; 152 foreach ($keyCol as $v) { 153 if (isset($fieldArray[$v])) { 154 $where .= sprintf(' and %s%s%s=%s ', $zthis->nameQuote,$v,$zthis->nameQuote,$fieldArray[$v]); 155 } 156 } 157 if ($where) { 158 $where = substr($where, 5); 159 } 160 161 if ($uSet && $where) { 162 $update = "UPDATE $table SET $uSet WHERE $where"; 163 $rs = $zthis->Execute($update); 164 165 if ($rs) { 166 if ($zthis->poorAffectedRows) { 167 // The Select count(*) wipes out any errors that the update would have returned. 168 // PHPLens Issue No: 5696 169 if ($zthis->ErrorNo()<>0) return 0; 170 171 // affected_rows == 0 if update field values identical to old values 172 // for mysql - which is silly. 173 $cnt = $zthis->GetOne("select count(*) from $table where $where"); 174 if ($cnt > 0) return 1; // record already exists 175 } else { 176 if (($zthis->Affected_Rows()>0)) return 1; 177 } 178 } else 179 return 0; 180 } 181 182 $iCols = $iVals = ''; 183 foreach($fieldArray as $k => $v) { 184 if ($has_autoinc && in_array($k,$keyCol)) continue; // skip autoinc col 185 186 // Add Quote around Column Name 187 $iCols .= sprintf(',%s%s%s',$zthis->nameQuote,$k,$zthis->nameQuote); 188 $iVals .= ",$v"; 189 } 190 $iCols = ltrim($iCols, ','); 191 $iVals = ltrim($iVals, ','); 192 193 $insert = "INSERT INTO $table ($iCols) VALUES ($iVals)"; 194 $rs = $zthis->Execute($insert); 195 return ($rs) ? 2 : 0; 196 } 197 198 function _adodb_getmenu($zthis, $name,$defstr='',$blank1stItem=true,$multiple=false, 199 $size=0, $selectAttr='',$compareFields0=true) 200 { 201 global $ADODB_FETCH_MODE; 202 203 $s = _adodb_getmenu_select($name, $defstr, $blank1stItem, $multiple, $size, $selectAttr); 204 205 $hasvalue = $zthis->FieldCount() > 1; 206 if (!$hasvalue) { 207 $compareFields0 = true; 208 } 209 210 $value = ''; 211 while(!$zthis->EOF) { 212 $zval = rtrim(reset($zthis->fields)); 213 214 if ($blank1stItem && $zval == "") { 215 $zthis->MoveNext(); 216 continue; 217 } 218 219 if ($hasvalue) { 220 if ($ADODB_FETCH_MODE == ADODB_FETCH_ASSOC) { 221 // Get 2nd field's value regardless of its name 222 $zval2 = current(array_slice($zthis->fields, 1, 1)); 223 } else { 224 // With NUM or BOTH fetch modes, we have a numeric index 225 $zval2 = $zthis->fields[1]; 226 } 227 $zval2 = trim($zval2); 228 $value = 'value="' . htmlspecialchars($zval2) . '"'; 229 } 230 231 /** @noinspection PhpUndefinedVariableInspection */ 232 $s .= _adodb_getmenu_option($defstr, $compareFields0 ? $zval : $zval2, $value, $zval); 233 234 $zthis->MoveNext(); 235 } // while 236 237 return $s ."\n</select>\n"; 238 } 239 240 function _adodb_getmenu_gp($zthis, $name,$defstr='',$blank1stItem=true,$multiple=false, 241 $size=0, $selectAttr='',$compareFields0=true) 242 { 243 global $ADODB_FETCH_MODE; 244 245 $s = _adodb_getmenu_select($name, $defstr, $blank1stItem, $multiple, $size, $selectAttr); 246 247 $hasvalue = $zthis->FieldCount() > 1; 248 $hasgroup = $zthis->FieldCount() > 2; 249 if (!$hasvalue) { 250 $compareFields0 = true; 251 } 252 253 $value = ''; 254 $optgroup = null; 255 $firstgroup = true; 256 while(!$zthis->EOF) { 257 $zval = rtrim(reset($zthis->fields)); 258 $group = ''; 259 260 if ($blank1stItem && $zval=="") { 261 $zthis->MoveNext(); 262 continue; 263 } 264 265 if ($hasvalue) { 266 if ($ADODB_FETCH_MODE == ADODB_FETCH_ASSOC) { 267 // Get 2nd field's value regardless of its name 268 $fields = array_slice($zthis->fields, 1); 269 $zval2 = current($fields); 270 if ($hasgroup) { 271 $group = trim(next($fields)); 272 } 273 } else { 274 // With NUM or BOTH fetch modes, we have a numeric index 275 $zval2 = $zthis->fields[1]; 276 if ($hasgroup) { 277 $group = trim($zthis->fields[2]); 278 } 279 } 280 $zval2 = trim($zval2); 281 $value = "value='".htmlspecialchars($zval2)."'"; 282 } 283 284 if ($optgroup != $group) { 285 $optgroup = $group; 286 if ($firstgroup) { 287 $firstgroup = false; 288 } else { 289 $s .="\n</optgroup>"; 290 } 291 $s .="\n<optgroup label='". htmlspecialchars($group) ."'>"; 292 } 293 294 /** @noinspection PhpUndefinedVariableInspection */ 295 $s .= _adodb_getmenu_option($defstr, $compareFields0 ? $zval : $zval2, $value, $zval); 296 297 $zthis->MoveNext(); 298 } // while 299 300 // closing last optgroup 301 if($optgroup != null) { 302 $s .= "\n</optgroup>"; 303 } 304 return $s ."\n</select>\n"; 305 } 306 307 /** 308 * Generate the opening SELECT tag for getmenu functions. 309 * 310 * ADOdb internal function, used by _adodb_getmenu() and _adodb_getmenu_gp(). 311 * 312 * @param string $name 313 * @param string $defstr 314 * @param bool $blank1stItem 315 * @param bool $multiple 316 * @param int $size 317 * @param string $selectAttr 318 * 319 * @return string HTML 320 */ 321 function _adodb_getmenu_select($name, $defstr = '', $blank1stItem = true, 322 $multiple = false, $size = 0, $selectAttr = '') 323 { 324 if ($multiple || is_array($defstr)) { 325 if ($size == 0 ) { 326 $size = 5; 327 } 328 $attr = ' multiple size="' . $size . '"'; 329 if (!strpos($name,'[]')) { 330 $name .= '[]'; 331 } 332 } elseif ($size) { 333 $attr = ' size="' . $size . '"'; 334 } else { 335 $attr = ''; 336 } 337 338 $html = '<select name="' . $name . '"' . $attr . ' ' . $selectAttr . '>'; 339 if ($blank1stItem) { 340 if (is_string($blank1stItem)) { 341 $barr = explode(':',$blank1stItem); 342 if (sizeof($barr) == 1) { 343 $barr[] = ''; 344 } 345 $html .= "\n<option value=\"" . $barr[0] . "\">" . $barr[1] . "</option>"; 346 } else { 347 $html .= "\n<option></option>"; 348 } 349 } 350 351 return $html; 352 } 353 354 /** 355 * Print the OPTION tags for getmenu functions. 356 * 357 * ADOdb internal function, used by _adodb_getmenu() and _adodb_getmenu_gp(). 358 * 359 * @param string $defstr Default values 360 * @param string $compare Value to compare against defaults 361 * @param string $value Ready-to-print `value="xxx"` (or empty) string 362 * @param string $display Display value 363 * 364 * @return string HTML 365 */ 366 function _adodb_getmenu_option($defstr, $compare, $value, $display) 367 { 368 if ( is_array($defstr) && in_array($compare, $defstr) 369 || !is_array($defstr) && strcasecmp($compare, $defstr) == 0 370 ) { 371 $selected = ' selected="selected"'; 372 } else { 373 $selected = ''; 374 } 375 376 return "\n<option $value$selected>" . htmlspecialchars($display) . '</option>'; 377 } 378 379 /* 380 Count the number of records this sql statement will return by using 381 query rewriting heuristics... 382 383 Does not work with UNIONs, except with postgresql and oracle. 384 385 Usage: 386 387 $conn->Connect(...); 388 $cnt = _adodb_getcount($conn, $sql); 389 390 */ 391 function _adodb_getcount($zthis, $sql,$inputarr=false,$secs2cache=0) 392 { 393 $qryRecs = 0; 394 395 /* 396 * These databases require a "SELECT * FROM (SELECT" type 397 * statement to have an alias for the result 398 */ 399 $requiresAlias = ''; 400 $requiresAliasArray = array('postgres9','postgres','mysql','mysqli','mssql','mssqlnative','sqlsrv'); 401 if (in_array($zthis->databaseType,$requiresAliasArray) 402 || in_array($zthis->dsnType,$requiresAliasArray) 403 ) { 404 $requiresAlias = '_ADODB_ALIAS_'; 405 } 406 407 if (!empty($zthis->_nestedSQL) 408 || preg_match("/^\s*SELECT\s+DISTINCT/is", $sql) 409 || preg_match('/\s+GROUP\s+BY\s+/is',$sql) 410 || preg_match('/\s+UNION\s+/is',$sql) 411 ) { 412 $rewritesql = adodb_strip_order_by($sql); 413 414 // ok, has SELECT DISTINCT or GROUP BY so see if we can use a table alias 415 // but this is only supported by oracle and postgresql... 416 if ($zthis->dataProvider == 'oci8') { 417 // Allow Oracle hints to be used for query optimization, Chris Wrye 418 if (preg_match('#/\\*+.*?\\*\\/#', $sql, $hint)) { 419 $rewritesql = "SELECT ".$hint[0]." COUNT(*) FROM (".$rewritesql.")"; 420 } else 421 $rewritesql = "SELECT COUNT(*) FROM (".$rewritesql.")"; 422 } else { 423 $rewritesql = "SELECT COUNT(*) FROM ($rewritesql) $requiresAlias"; 424 } 425 426 } else { 427 // Replace 'SELECT ... FROM' with 'SELECT COUNT(*) FROM' 428 // Parse the query one char at a time starting after the SELECT 429 // to find the FROM clause's position, ignoring any sub-queries. 430 $start = stripos($sql, 'SELECT') + 7; 431 if ($start === false) { 432 // Not a SELECT statement - probably should trigger an exception here 433 return 0; 434 } 435 $len = strlen($sql); 436 $numParentheses = 0; 437 for ($pos = $start; $pos < $len; $pos++) { 438 switch ($sql[$pos]) { 439 case '(': $numParentheses++; continue 2; 440 case ')': $numParentheses--; continue 2; 441 } 442 // Ignore whatever is between parentheses (sub-queries) 443 if ($numParentheses > 0) { 444 continue; 445 } 446 // Exit loop if 'FROM' keyword was found 447 if (strtoupper(substr($sql, $pos, 4)) == 'FROM') { 448 break; 449 } 450 } 451 $rewritesql = 'SELECT COUNT(*) ' . substr($sql, $pos); 452 453 // fix by alexander zhukov, alex#unipack.ru, because count(*) and 'order by' fails 454 // with mssql, access and postgresql. Also a good speedup optimization - skips sorting! 455 // also see PHPLens Issue No: 12752 456 $rewritesql = adodb_strip_order_by($rewritesql); 457 } 458 459 if (isset($rewritesql) && $rewritesql != $sql) { 460 if (preg_match('/\sLIMIT\s+[0-9]+/i',$sql,$limitarr)) { 461 $rewritesql .= $limitarr[0]; 462 } 463 464 if ($secs2cache) { 465 // we only use half the time of secs2cache because the count can quickly 466 // become inaccurate if new records are added 467 $qryRecs = $zthis->CacheGetOne($secs2cache/2,$rewritesql,$inputarr); 468 469 } else { 470 $qryRecs = $zthis->GetOne($rewritesql,$inputarr); 471 } 472 if ($qryRecs !== false) return $qryRecs; 473 } 474 475 //-------------------------------------------- 476 // query rewrite failed - so try slower way... 477 478 // strip off unneeded ORDER BY if no UNION 479 if (preg_match('/\s*UNION\s*/is', $sql)) { 480 $rewritesql = $sql; 481 } else { 482 $rewritesql = adodb_strip_order_by($sql); 483 } 484 485 if (preg_match('/\sLIMIT\s+[0-9]+/i',$sql,$limitarr)) { 486 $rewritesql .= $limitarr[0]; 487 } 488 489 if ($secs2cache) { 490 $rstest = $zthis->CacheExecute($secs2cache,$rewritesql,$inputarr); 491 if (!$rstest) $rstest = $zthis->CacheExecute($secs2cache,$sql,$inputarr); 492 } else { 493 $rstest = $zthis->Execute($rewritesql,$inputarr); 494 if (!$rstest) $rstest = $zthis->Execute($sql,$inputarr); 495 } 496 if ($rstest) { 497 $qryRecs = $rstest->RecordCount(); 498 if ($qryRecs == -1) { 499 // some databases will return -1 on MoveLast() - change to MoveNext() 500 while(!$rstest->EOF) { 501 $rstest->MoveNext(); 502 } 503 $qryRecs = $rstest->_currentRow; 504 } 505 $rstest->Close(); 506 if ($qryRecs == -1) return 0; 507 } 508 return $qryRecs; 509 } 510 511 /* 512 Code originally from "Cornel G" <conyg@fx.ro> 513 514 This code might not work with SQL that has UNION in it 515 516 Also if you are using CachePageExecute(), there is a strong possibility that 517 data will get out of synch. use CachePageExecute() only with tables that 518 rarely change. 519 */ 520 function _adodb_pageexecute_all_rows($zthis, $sql, $nrows, $page, 521 $inputarr=false, $secs2cache=0) 522 { 523 $atfirstpage = false; 524 $atlastpage = false; 525 526 // If an invalid nrows is supplied, 527 // we assume a default value of 10 rows per page 528 if (!isset($nrows) || $nrows <= 0) $nrows = 10; 529 530 $qryRecs = _adodb_getcount($zthis,$sql,$inputarr,$secs2cache); 531 $lastpageno = (int) ceil($qryRecs / $nrows); 532 $zthis->_maxRecordCount = $qryRecs; 533 534 // ***** Here we check whether $page is the last page or 535 // whether we are trying to retrieve 536 // a page number greater than the last page number. 537 if ($page >= $lastpageno) { 538 $page = $lastpageno; 539 $atlastpage = true; 540 } 541 542 // If page number <= 1, then we are at the first page 543 if (empty($page) || $page <= 1) { 544 $page = 1; 545 $atfirstpage = true; 546 } 547 548 // We get the data we want 549 $offset = $nrows * ($page-1); 550 if ($secs2cache > 0) 551 $rsreturn = $zthis->CacheSelectLimit($secs2cache, $sql, $nrows, $offset, $inputarr); 552 else 553 $rsreturn = $zthis->SelectLimit($sql, $nrows, $offset, $inputarr, $secs2cache); 554 555 556 // Before returning the RecordSet, we set the pagination properties we need 557 if ($rsreturn) { 558 $rsreturn->_maxRecordCount = $qryRecs; 559 $rsreturn->rowsPerPage = $nrows; 560 $rsreturn->AbsolutePage($page); 561 $rsreturn->AtFirstPage($atfirstpage); 562 $rsreturn->AtLastPage($atlastpage); 563 $rsreturn->LastPageNo($lastpageno); 564 } 565 return $rsreturn; 566 } 567 568 // Iván Oliva version 569 function _adodb_pageexecute_no_last_page($zthis, $sql, $nrows, $page, $inputarr=false, $secs2cache=0) 570 { 571 $atfirstpage = false; 572 $atlastpage = false; 573 574 if (!isset($page) || $page <= 1) { 575 // If page number <= 1, then we are at the first page 576 $page = 1; 577 $atfirstpage = true; 578 } 579 if ($nrows <= 0) { 580 // If an invalid nrows is supplied, we assume a default value of 10 rows per page 581 $nrows = 10; 582 } 583 584 $pagecounteroffset = ($page * $nrows) - $nrows; 585 586 // To find out if there are more pages of rows, simply increase the limit or 587 // nrows by 1 and see if that number of records was returned. If it was, 588 // then we know there is at least one more page left, otherwise we are on 589 // the last page. Therefore allow non-Count() paging with single queries 590 // rather than three queries as was done before. 591 $test_nrows = $nrows + 1; 592 if ($secs2cache > 0) { 593 $rsreturn = $zthis->CacheSelectLimit($secs2cache, $sql, $nrows, $pagecounteroffset, $inputarr); 594 } else { 595 $rsreturn = $zthis->SelectLimit($sql, $test_nrows, $pagecounteroffset, $inputarr, $secs2cache); 596 } 597 598 // Now check to see if the number of rows returned was the higher value we asked for or not. 599 if ( $rsreturn->_numOfRows == $test_nrows ) { 600 // Still at least 1 more row, so we are not on last page yet... 601 // Remove the last row from the RS. 602 $rsreturn->_numOfRows = ( $rsreturn->_numOfRows - 1 ); 603 } elseif ( $rsreturn->_numOfRows == 0 && $page > 1 ) { 604 // Likely requested a page that doesn't exist, so need to find the last 605 // page and return it. Revert to original method and loop through pages 606 // until we find some data... 607 $pagecounter = $page + 1; 608 609 $rstest = $rsreturn; 610 if ($rstest) { 611 while ($rstest && $rstest->EOF && $pagecounter > 0) { 612 $atlastpage = true; 613 $pagecounter--; 614 $pagecounteroffset = $nrows * ($pagecounter - 1); 615 $rstest->Close(); 616 if ($secs2cache>0) { 617 $rstest = $zthis->CacheSelectLimit($secs2cache, $sql, $nrows, $pagecounteroffset, $inputarr); 618 } 619 else { 620 $rstest = $zthis->SelectLimit($sql, $nrows, $pagecounteroffset, $inputarr, $secs2cache); 621 } 622 } 623 if ($rstest) $rstest->Close(); 624 } 625 if ($atlastpage) { 626 // If we are at the last page or beyond it, we are going to retrieve it 627 $page = $pagecounter; 628 if ($page == 1) { 629 // We have to do this again in case the last page is the same as 630 // the first page, that is, the recordset has only 1 page. 631 $atfirstpage = true; 632 } 633 } 634 // We get the data we want 635 $offset = $nrows * ($page-1); 636 if ($secs2cache > 0) { 637 $rsreturn = $zthis->CacheSelectLimit($secs2cache, $sql, $nrows, $offset, $inputarr); 638 } 639 else { 640 $rsreturn = $zthis->SelectLimit($sql, $nrows, $offset, $inputarr, $secs2cache); 641 } 642 } elseif ( $rsreturn->_numOfRows < $test_nrows ) { 643 // Rows is less than what we asked for, so must be at the last page. 644 $atlastpage = true; 645 } 646 647 // Before returning the RecordSet, we set the pagination properties we need 648 if ($rsreturn) { 649 $rsreturn->rowsPerPage = $nrows; 650 $rsreturn->AbsolutePage($page); 651 $rsreturn->AtFirstPage($atfirstpage); 652 $rsreturn->AtLastPage($atlastpage); 653 } 654 return $rsreturn; 655 } 656 657 /** 658 * Performs case conversion and quoting of the given field name. 659 * 660 * See Global variable $ADODB_QUOTE_FIELDNAMES. 661 * 662 * @param ADOConnection $zthis 663 * @param string $fieldName 664 * 665 * @return string Quoted field name 666 */ 667 function _adodb_quote_fieldname($zthis, $fieldName) 668 { 669 global $ADODB_QUOTE_FIELDNAMES; 670 671 // Case conversion - defaults to UPPER 672 $case = is_bool($ADODB_QUOTE_FIELDNAMES) ? 'UPPER' : $ADODB_QUOTE_FIELDNAMES; 673 switch ($case) { 674 case 'LOWER': 675 $fieldName = strtolower($fieldName); 676 break; 677 case 'NATIVE': 678 // Do nothing 679 break; 680 case 'UPPER': 681 case 'BRACKETS': 682 default: 683 $fieldName = strtoupper($fieldName); 684 break; 685 } 686 687 // Quote field if requested, or necessary (field contains space) 688 if ($ADODB_QUOTE_FIELDNAMES || strpos($fieldName, ' ') !== false ) { 689 if ($ADODB_QUOTE_FIELDNAMES === 'BRACKETS') { 690 return $zthis->leftBracket . $fieldName . $zthis->rightBracket; 691 } else { 692 return $zthis->nameQuote . $fieldName . $zthis->nameQuote; 693 } 694 } else { 695 return $fieldName; 696 } 697 } 698 699 function _adodb_getupdatesql(&$zthis, $rs, $arrFields, $forceUpdate=false, $force=2) 700 { 701 if (!$rs) { 702 printf(ADODB_BAD_RS,'GetUpdateSQL'); 703 return false; 704 } 705 706 $fieldUpdatedCount = 0; 707 if (is_array($arrFields)) 708 $arrFields = array_change_key_case($arrFields,CASE_UPPER); 709 710 $hasnumeric = isset($rs->fields[0]); 711 $setFields = ''; 712 713 // Loop through all of the fields in the recordset 714 for ($i=0, $max=$rs->fieldCount(); $i < $max; $i++) { 715 // Get the field from the recordset 716 $field = $rs->fetchField($i); 717 718 // If the recordset field is one 719 // of the fields passed in then process. 720 $upperfname = strtoupper($field->name); 721 if (adodb_key_exists($upperfname, $arrFields, $force)) { 722 723 // If the existing field value in the recordset 724 // is different from the value passed in then 725 // go ahead and append the field name and new value to 726 // the update query. 727 728 if ($hasnumeric) $val = $rs->fields[$i]; 729 else if (isset($rs->fields[$upperfname])) $val = $rs->fields[$upperfname]; 730 else if (isset($rs->fields[$field->name])) $val = $rs->fields[$field->name]; 731 else if (isset($rs->fields[strtolower($upperfname)])) $val = $rs->fields[strtolower($upperfname)]; 732 else $val = ''; 733 734 if ($forceUpdate || $val !== $arrFields[$upperfname]) { 735 // Set the counter for the number of fields that will be updated. 736 $fieldUpdatedCount++; 737 738 // Based on the datatype of the field 739 // Format the value properly for the database 740 $type = $rs->metaType($field->type); 741 742 if ($type == 'null') { 743 $type = 'C'; 744 } 745 746 $fnameq = _adodb_quote_fieldname($zthis, $field->name); 747 748 //********************************************************// 749 if (is_null($arrFields[$upperfname]) 750 || (empty($arrFields[$upperfname]) && strlen($arrFields[$upperfname]) == 0) 751 || $arrFields[$upperfname] === $zthis->null2null 752 ) { 753 754 switch ($force) { 755 756 //case 0: 757 // // Ignore empty values. This is already handled in "adodb_key_exists" function. 758 // break; 759 760 case 1: 761 // set null 762 $setFields .= $fnameq . " = null, "; 763 break; 764 765 case 2: 766 // set empty 767 $arrFields[$upperfname] = ""; 768 $setFields .= _adodb_column_sql($zthis, 'U', $type, $upperfname, $fnameq, $arrFields); 769 break; 770 771 default: 772 case 3: 773 // set the value that was given in array, so you can give both null and empty values 774 if (is_null($arrFields[$upperfname]) || $arrFields[$upperfname] === $zthis->null2null) { 775 $setFields .= $fnameq . " = null, "; 776 } else { 777 $setFields .= _adodb_column_sql($zthis, 'U', $type, $upperfname, $fnameq, $arrFields); 778 } 779 break; 780 781 case ADODB_FORCE_NULL_AND_ZERO: 782 783 switch ($type) { 784 case 'N': 785 case 'I': 786 case 'L': 787 $setFields .= $fnameq . ' = 0, '; 788 break; 789 default: 790 $setFields .= $fnameq . ' = null, '; 791 break; 792 } 793 break; 794 795 } 796 //********************************************************// 797 } else { 798 // we do this so each driver can customize the sql for 799 // DB specific column types. 800 // Oracle needs BLOB types to be handled with a returning clause 801 // postgres has special needs as well 802 $setFields .= _adodb_column_sql($zthis, 'U', $type, $upperfname, $fnameq, $arrFields); 803 } 804 } 805 } 806 } 807 808 // If there were any modified fields then build the rest of the update query. 809 if ($fieldUpdatedCount > 0 || $forceUpdate) { 810 // Get the table name from the existing query. 811 if (!empty($rs->tableName)) { 812 $tableName = $rs->tableName; 813 } else { 814 preg_match("/FROM\s+".ADODB_TABLE_REGEX."/is", $rs->sql, $tableName); 815 $tableName = $tableName[1]; 816 } 817 818 // Get the full where clause excluding the word "WHERE" from the existing query. 819 preg_match('/\sWHERE\s(.*)/is', $rs->sql, $whereClause); 820 821 $discard = false; 822 // not a good hack, improvements? 823 if ($whereClause) { 824 if (preg_match('/\s(ORDER\s.*)/is', $whereClause[1], $discard)); 825 else if (preg_match('/\s(LIMIT\s.*)/is', $whereClause[1], $discard)); 826 else if (preg_match('/\s(FOR UPDATE.*)/is', $whereClause[1], $discard)); 827 else preg_match('/\s.*(\) WHERE .*)/is', $whereClause[1], $discard); # see https://sourceforge.net/p/adodb/bugs/37/ 828 } else { 829 $whereClause = array(false, false); 830 } 831 832 if ($discard) { 833 $whereClause[1] = substr($whereClause[1], 0, strlen($whereClause[1]) - strlen($discard[1])); 834 } 835 836 $sql = 'UPDATE '.$tableName.' SET '.substr($setFields, 0, -2); 837 if (strlen($whereClause[1]) > 0) { 838 $sql .= ' WHERE '.$whereClause[1]; 839 } 840 return $sql; 841 } else { 842 return false; 843 } 844 } 845 846 function adodb_key_exists($key, $arr,$force=2) 847 { 848 if ($force<=0) { 849 // the following is the old behaviour where null or empty fields are ignored 850 return (!empty($arr[$key])) || (isset($arr[$key]) && strlen($arr[$key])>0); 851 } 852 853 if (isset($arr[$key])) 854 return true; 855 ## null check below 856 return array_key_exists($key,$arr); 857 } 858 859 /** 860 * There is a special case of this function for the oci8 driver. 861 * The proper way to handle an insert w/ a blob in oracle requires 862 * a returning clause with bind variables and a descriptor blob. 863 * 864 * 865 */ 866 function _adodb_getinsertsql(&$zthis, $rs, $arrFields, $force=2) 867 { 868 static $cacheRS = false; 869 static $cacheSig = 0; 870 static $cacheCols; 871 872 $tableName = ''; 873 $values = ''; 874 $fields = ''; 875 if (is_array($arrFields)) 876 $arrFields = array_change_key_case($arrFields,CASE_UPPER); 877 $fieldInsertedCount = 0; 878 879 if (is_string($rs)) { 880 //ok we have a table name 881 //try and get the column info ourself. 882 $tableName = $rs; 883 884 //we need an object for the recordSet 885 //because we have to call MetaType. 886 //php can't do a $rsclass::MetaType() 887 $rsclass = $zthis->rsPrefix.$zthis->databaseType; 888 $recordSet = new $rsclass(ADORecordSet::DUMMY_QUERY_ID, $zthis->fetchMode); 889 $recordSet->connection = $zthis; 890 891 if (is_string($cacheRS) && $cacheRS == $rs) { 892 $columns = $cacheCols; 893 } else { 894 $columns = $zthis->MetaColumns( $tableName ); 895 $cacheRS = $tableName; 896 $cacheCols = $columns; 897 } 898 } else if (is_subclass_of($rs, 'adorecordset')) { 899 if (isset($rs->insertSig) && is_integer($cacheRS) && $cacheRS == $rs->insertSig) { 900 $columns = $cacheCols; 901 } else { 902 $columns = []; 903 for ($i=0, $max=$rs->FieldCount(); $i < $max; $i++) 904 $columns[] = $rs->FetchField($i); 905 $cacheRS = $cacheSig; 906 $cacheCols = $columns; 907 $rs->insertSig = $cacheSig++; 908 } 909 $recordSet = $rs; 910 911 } else { 912 printf(ADODB_BAD_RS,'GetInsertSQL'); 913 return false; 914 } 915 916 // Loop through all of the fields in the recordset 917 foreach( $columns as $field ) { 918 $upperfname = strtoupper($field->name); 919 if (adodb_key_exists($upperfname, $arrFields, $force)) { 920 $bad = false; 921 $fnameq = _adodb_quote_fieldname($zthis, $field->name); 922 $type = $recordSet->MetaType($field->type); 923 924 /********************************************************/ 925 if (is_null($arrFields[$upperfname]) 926 || (empty($arrFields[$upperfname]) && strlen($arrFields[$upperfname]) == 0) 927 || $arrFields[$upperfname] === $zthis->null2null 928 ) { 929 switch ($force) { 930 931 case ADODB_FORCE_IGNORE: // we must always set null if missing 932 $bad = true; 933 break; 934 935 case ADODB_FORCE_NULL: 936 $values .= "null, "; 937 break; 938 939 case ADODB_FORCE_EMPTY: 940 //Set empty 941 $arrFields[$upperfname] = ""; 942 $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq, $arrFields); 943 break; 944 945 default: 946 case ADODB_FORCE_VALUE: 947 //Set the value that was given in array, so you can give both null and empty values 948 if (is_null($arrFields[$upperfname]) || $arrFields[$upperfname] === $zthis->null2null) { 949 $values .= "null, "; 950 } else { 951 $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq, $arrFields); 952 } 953 break; 954 955 case ADODB_FORCE_NULL_AND_ZERO: 956 switch ($type) { 957 case 'N': 958 case 'I': 959 case 'L': 960 $values .= '0, '; 961 break; 962 default: 963 $values .= "null, "; 964 break; 965 } 966 break; 967 968 } // switch 969 970 /*********************************************************/ 971 } else { 972 //we do this so each driver can customize the sql for 973 //DB specific column types. 974 //Oracle needs BLOB types to be handled with a returning clause 975 //postgres has special needs as well 976 $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq, $arrFields); 977 } 978 979 if ($bad) { 980 continue; 981 } 982 // Set the counter for the number of fields that will be inserted. 983 $fieldInsertedCount++; 984 985 // Get the name of the fields to insert 986 $fields .= $fnameq . ", "; 987 } 988 } 989 990 991 // If there were any inserted fields then build the rest of the insert query. 992 if ($fieldInsertedCount <= 0) return false; 993 994 // Get the table name from the existing query. 995 if (!$tableName) { 996 if (!empty($rs->tableName)) $tableName = $rs->tableName; 997 else if (preg_match("/FROM\s+".ADODB_TABLE_REGEX."/is", $rs->sql, $tableName)) 998 $tableName = $tableName[1]; 999 else 1000 return false; 1001 } 1002 1003 // Strip off the comma and space on the end of both the fields 1004 // and their values. 1005 $fields = substr($fields, 0, -2); 1006 $values = substr($values, 0, -2); 1007 1008 // Append the fields and their values to the insert query. 1009 return 'INSERT INTO '.$tableName.' ( '.$fields.' ) VALUES ( '.$values.' )'; 1010 } 1011 1012 1013 /** 1014 * This private method is used to help construct 1015 * the update/sql which is generated by GetInsertSQL and GetUpdateSQL. 1016 * It handles the string construction of 1 column -> sql string based on 1017 * the column type. We want to do 'safe' handling of BLOBs 1018 * 1019 * @param string the type of sql we are trying to create 1020 * 'I' or 'U'. 1021 * @param string column data type from the db::MetaType() method 1022 * @param string the column name 1023 * @param array the column value 1024 * 1025 * @return string 1026 * 1027 */ 1028 function _adodb_column_sql_oci8(&$zthis,$action, $type, $fname, $fnameq, $arrFields) 1029 { 1030 // Based on the datatype of the field 1031 // Format the value properly for the database 1032 switch ($type) { 1033 case 'B': 1034 //in order to handle Blobs correctly, we need 1035 //to do some magic for Oracle 1036 1037 //we need to create a new descriptor to handle 1038 //this properly 1039 if (!empty($zthis->hasReturningInto)) { 1040 if ($action == 'I') { 1041 $sql = 'empty_blob(), '; 1042 } else { 1043 $sql = $fnameq . '=empty_blob(), '; 1044 } 1045 //add the variable to the returning clause array 1046 //so the user can build this later in 1047 //case they want to add more to it 1048 $zthis->_returningArray[$fname] = ':xx' . $fname . 'xx'; 1049 } else { 1050 if (empty($arrFields[$fname])) { 1051 if ($action == 'I') { 1052 $sql = 'empty_blob(), '; 1053 } else { 1054 $sql = $fnameq . '=empty_blob(), '; 1055 } 1056 } else { 1057 //this is to maintain compatibility 1058 //with older adodb versions. 1059 $sql = _adodb_column_sql($zthis, $action, $type, $fname, $fnameq, $arrFields, false); 1060 } 1061 } 1062 break; 1063 1064 case "X": 1065 //we need to do some more magic here for long variables 1066 //to handle these correctly in oracle. 1067 1068 //create a safe bind var name 1069 //to avoid conflicts w/ dupes. 1070 if (!empty($zthis->hasReturningInto)) { 1071 if ($action == 'I') { 1072 $sql = ':xx' . $fname . 'xx, '; 1073 } else { 1074 $sql = $fnameq . '=:xx' . $fname . 'xx, '; 1075 } 1076 //add the variable to the returning clause array 1077 //so the user can build this later in 1078 //case they want to add more to it 1079 $zthis->_returningArray[$fname] = ':xx' . $fname . 'xx'; 1080 } else { 1081 //this is to maintain compatibility 1082 //with older adodb versions. 1083 $sql = _adodb_column_sql($zthis, $action, $type, $fname, $fnameq, $arrFields, false); 1084 } 1085 break; 1086 1087 default: 1088 $sql = _adodb_column_sql($zthis, $action, $type, $fname, $fnameq, $arrFields, false); 1089 break; 1090 } 1091 1092 return $sql; 1093 } 1094 1095 function _adodb_column_sql(&$zthis, $action, $type, $fname, $fnameq, $arrFields, $recurse=true) 1096 { 1097 1098 if ($recurse) { 1099 switch($zthis->dataProvider) { 1100 case 'postgres': 1101 if ($type == 'L') $type = 'C'; 1102 break; 1103 case 'oci8': 1104 return _adodb_column_sql_oci8($zthis, $action, $type, $fname, $fnameq, $arrFields); 1105 1106 } 1107 } 1108 1109 switch($type) { 1110 case "C": 1111 case "X": 1112 case 'B': 1113 $val = $zthis->qstr($arrFields[$fname]); 1114 break; 1115 1116 case "D": 1117 $val = $zthis->DBDate($arrFields[$fname]); 1118 break; 1119 1120 case "T": 1121 $val = $zthis->DBTimeStamp($arrFields[$fname]); 1122 break; 1123 1124 case "N": 1125 $val = $arrFields[$fname]; 1126 if (!is_numeric($val)) $val = str_replace(',', '.', (float)$val); 1127 break; 1128 1129 case "I": 1130 case "R": 1131 $val = $arrFields[$fname]; 1132 if (!is_numeric($val)) $val = (integer) $val; 1133 break; 1134 1135 default: 1136 $val = str_replace(array("'"," ","("),"",$arrFields[$fname]); // basic sql injection defence 1137 if (empty($val)) $val = '0'; 1138 break; 1139 } 1140 1141 if ($action == 'I') return $val . ", "; 1142 1143 return $fnameq . "=" . $val . ", "; 1144 } 1145 1146 1147 /** 1148 * Replaces standard _execute when debug mode is enabled 1149 * 1150 * @param ADOConnection $zthis An ADOConnection object 1151 * @param string|string[] $sql A string or array of SQL statements 1152 * @param string[]|null $inputarr An optional array of bind parameters 1153 * 1154 * @return handle|void A handle to the executed query 1155 */ 1156 function _adodb_debug_execute($zthis, $sql, $inputarr) 1157 { 1158 // Unpack the bind parameters 1159 $ss = ''; 1160 if ($inputarr) { 1161 foreach ($inputarr as $kk => $vv) { 1162 if (is_string($vv) && strlen($vv) > 64) { 1163 $vv = substr($vv, 0, 64) . '...'; 1164 } 1165 if (is_null($vv)) { 1166 $ss .= "($kk=>null) "; 1167 } else { 1168 if (is_array($vv)) { 1169 $vv = sprintf("Array Of Values: [%s]", implode(',', $vv)); 1170 } 1171 $ss .= "($kk=>'$vv') "; 1172 } 1173 } 1174 $ss = "[ $ss ]"; 1175 } 1176 1177 $sqlTxt = is_array($sql) ? $sql[0] : $sql; 1178 1179 // Remove newlines and tabs, compress repeating spaces 1180 $sqlTxt = preg_replace('/\s+/', ' ', $sqlTxt); 1181 1182 // check if running from browser or command-line 1183 $inBrowser = isset($_SERVER['HTTP_USER_AGENT']); 1184 1185 $myDatabaseType = $zthis->databaseType; 1186 if (!isset($zthis->dsnType)) { 1187 // Append the PDO driver name 1188 $myDatabaseType .= '-' . $zthis->dsnType; 1189 } 1190 1191 if ($inBrowser) { 1192 if ($ss) { 1193 // Default formatting for passed parameter 1194 $ss = sprintf('<code class="adodb-debug">%s</code>', htmlspecialchars($ss)); 1195 } 1196 if ($zthis->debug === -1) { 1197 $outString = "<br class='adodb-debug'>(%s): %s %s<br class='adodb-debug'>"; 1198 ADOConnection::outp(sprintf($outString, $myDatabaseType, htmlspecialchars($sqlTxt), $ss), false); 1199 } elseif ($zthis->debug !== -99) { 1200 $outString = "<hr class='adodb-debug'>(%s): %s %s<hr class='adodb-debug'>"; 1201 ADOConnection::outp(sprintf($outString, $myDatabaseType, htmlspecialchars($sqlTxt), $ss), false); 1202 } 1203 } else { 1204 // CLI output 1205 if ($zthis->debug !== -99) { 1206 $outString = sprintf("%s\n%s\n %s %s \n%s\n", str_repeat('-', 78), $myDatabaseType, $sqlTxt, $ss, str_repeat('-', 78)); 1207 ADOConnection::outp($outString, false); 1208 } 1209 } 1210 1211 // Now execute the query 1212 $qID = $zthis->_query($sql, $inputarr); 1213 1214 // Alexios Fakios notes that ErrorMsg() must be called before ErrorNo() for mssql 1215 // because ErrorNo() calls Execute('SELECT @ERROR'), causing recursion 1216 if ($zthis->databaseType == 'mssql') { 1217 // ErrorNo is a slow function call in mssql 1218 if ($emsg = $zthis->ErrorMsg()) { 1219 if ($err = $zthis->ErrorNo()) { 1220 if ($zthis->debug === -99) { 1221 ADOConnection::outp("<hr>\n($myDatabaseType): " . htmlspecialchars($sqlTxt) . " $ss\n<hr>\n", false); 1222 } 1223 1224 ADOConnection::outp($err . ': ' . $emsg); 1225 } 1226 } 1227 } else { 1228 if (!$qID) { 1229 // Statement execution has failed 1230 if ($zthis->debug === -99) { 1231 if ($inBrowser) { 1232 $outString = "<hr class='adodb-debug'>(%s): %s %s<hr class='adodb-debug'>"; 1233 ADOConnection::outp(sprintf($outString, $myDatabaseType, htmlspecialchars($sqlTxt), $ss), false); 1234 } else { 1235 $outString = sprintf("%s\n%s\n %s %s \n%s\n",str_repeat('-',78),$myDatabaseType,$sqlTxt,$ss,str_repeat('-',78)); 1236 ADOConnection::outp($outString, false); 1237 } 1238 } 1239 1240 // Send last error to output 1241 $errno = $zthis->ErrorNo(); 1242 if ($errno) { 1243 ADOConnection::outp($errno . ': ' . $zthis->ErrorMsg()); 1244 } 1245 } 1246 } 1247 1248 if ($qID === false || $zthis->debug === 99) { 1249 _adodb_backtrace(); 1250 } 1251 return $qID; 1252 } 1253 1254 /** 1255 * Pretty print the debug_backtrace function 1256 * 1257 * @param string[]|bool $printOrArr Whether to print the result directly or return the result 1258 * @param int $maximumDepth The maximum depth of the array to traverse 1259 * @param int $elementsToIgnore The backtrace array indexes to ignore 1260 * @param null|bool $ishtml True if we are in a CGI environment, false for CLI, 1261 * null to auto detect 1262 * 1263 * @return string Formatted backtrace 1264 */ 1265 function _adodb_backtrace($printOrArr=true, $maximumDepth=9999, $elementsToIgnore=0, $ishtml=null) 1266 { 1267 if (!function_exists('debug_backtrace')) { 1268 return ''; 1269 } 1270 1271 if ($ishtml === null) { 1272 // Auto determine if we in a CGI enviroment 1273 $html = (isset($_SERVER['HTTP_USER_AGENT'])); 1274 } else { 1275 $html = $ishtml; 1276 } 1277 1278 $cgiString = "</font><font color=#808080 size=-1> %% line %4d, file: <a href=\"file:/%s\">%s</a></font>"; 1279 $cliString = "%% line %4d, file: %s"; 1280 $fmt = ($html) ? $cgiString : $cliString; 1281 1282 $MAXSTRLEN = 128; 1283 1284 $s = ($html) ? '<pre align=left>' : ''; 1285 1286 if (is_array($printOrArr)) { 1287 $traceArr = $printOrArr; 1288 } else { 1289 $traceArr = debug_backtrace(); 1290 } 1291 1292 // Remove first 2 elements that just show calls to adodb_backtrace 1293 array_shift($traceArr); 1294 array_shift($traceArr); 1295 1296 // We want last element to have no indent 1297 $tabs = sizeof($traceArr) - 1; 1298 1299 foreach ($traceArr as $arr) { 1300 if ($elementsToIgnore) { 1301 // Ignore array element at start of array 1302 $elementsToIgnore--; 1303 $tabs--; 1304 continue; 1305 } 1306 $maximumDepth--; 1307 if ($maximumDepth < 0) { 1308 break; 1309 } 1310 1311 $args = array(); 1312 1313 if ($tabs) { 1314 $s .= str_repeat($html ? ' ' : "\t", $tabs); 1315 $tabs--; 1316 } 1317 if ($html) { 1318 $s .= '<font face="Courier New,Courier">'; 1319 } 1320 1321 if (isset($arr['class'])) { 1322 $s .= $arr['class'] . '.'; 1323 } 1324 1325 if (isset($arr['args'])) { 1326 foreach ($arr['args'] as $v) { 1327 if (is_null($v)) { 1328 $args[] = 'null'; 1329 } elseif (is_array($v)) { 1330 $args[] = 'Array[' . sizeof($v) . ']'; 1331 } elseif (is_object($v)) { 1332 $args[] = 'Object:' . get_class($v); 1333 } elseif (is_bool($v)) { 1334 $args[] = $v ? 'true' : 'false'; 1335 } else { 1336 $v = (string)@$v; 1337 // Truncate 1338 $v = substr($v, 0, $MAXSTRLEN); 1339 // Remove newlines and tabs, compress repeating spaces 1340 $v = preg_replace('/\s+/', ' ', $v); 1341 // Convert htmlchars (not sure why we do this in CLI) 1342 $str = htmlspecialchars($v); 1343 1344 if (strlen($v) > $MAXSTRLEN) { 1345 $str .= '...'; 1346 } 1347 1348 $args[] = $str; 1349 } 1350 } 1351 } 1352 $s .= $arr['function'] . '(' . implode(', ', $args) . ')'; 1353 $s .= @sprintf($fmt, $arr['line'], $arr['file'], basename($arr['file'])); 1354 $s .= "\n"; 1355 } 1356 if ($html) { 1357 $s .= '</pre>'; 1358 } 1359 if ($printOrArr) { 1360 print $s; 1361 } 1362 1363 return $s; 1364 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body