From ee99952d90578065c427e823b8c44dbb61f0d90c Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Mon, 17 Jan 2022 20:06:26 -0500 Subject: [PATCH 01/27] Support OCI (Read/Write) --- lib/Data/Database.php | 127 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 104 insertions(+), 23 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 0c66d330..a2b1fe1a 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -199,12 +199,30 @@ class Database extends AbstractData $burnafterreading = $paste['adata'][3]; } try { + $big_string = $isVersion1 ? $paste['data'] : Json::encode($paste); + if (self::$_type === 'oci') { + # It is not possible to execute in the normal way if strlen($big_string) >= 4000 + $stmt = self::$_db->prepare( + 'INSERT INTO ' . self::_sanitizeIdentifier('paste') . + ' VALUES(?,?,?,?,?,?,?,?,?)' + ); + $stmt->bindParam(1, $pasteid); + $stmt->bindParam(2, $big_string, PDO::PARAM_STR, strlen($big_string)); + $stmt->bindParam(3, $created, PDO::PARAM_INT); + $stmt->bindParam(4, $expire_date, PDO::PARAM_INT); + $stmt->bindParam(5, $opendiscussion, PDO::PARAM_INT); + $stmt->bindParam(6, $burnafterreading, PDO::PARAM_INT); + $stmt->bindParam(7, Json::encode($meta)); + $stmt->bindParam(8, $attachment, PDO::PARAM_STR, strlen($attachment)); + $stmt->bindParam(9, $attachmentname); + return $stmt->execute(); + } return self::_exec( 'INSERT INTO ' . self::_sanitizeIdentifier('paste') . ' VALUES(?,?,?,?,?,?,?,?,?)', array( $pasteid, - $isVersion1 ? $paste['data'] : Json::encode($paste), + $big_string, $created, $expire_date, (int) $opendiscussion, @@ -233,11 +251,15 @@ class Database extends AbstractData } self::$_cache[$pasteid] = false; + $rawData = ""; try { $paste = self::_select( 'SELECT * FROM ' . self::_sanitizeIdentifier('paste') . ' WHERE dataid = ?', array($pasteid), true ); + if ($paste !== false) { + $rawData = self::$_type === 'oci' ? self::_clob($paste['DATA']) : $paste['data']; + } } catch (Exception $e) { $paste = false; } @@ -245,25 +267,25 @@ class Database extends AbstractData return false; } // create array - $data = Json::decode($paste['data']); + $data = Json::decode($rawData); $isVersion2 = array_key_exists('v', $data) && $data['v'] >= 2; if ($isVersion2) { self::$_cache[$pasteid] = $data; list($createdKey) = self::_getVersionedKeys(2); } else { - self::$_cache[$pasteid] = array('data' => $paste['data']); + self::$_cache[$pasteid] = array('data' => $paste[self::_sanitizeColumn('data')]); list($createdKey) = self::_getVersionedKeys(1); } try { - $paste['meta'] = Json::decode($paste['meta']); + $paste['meta'] = Json::decode($paste[self::_sanitizeColumn('meta')]); } catch (Exception $e) { $paste['meta'] = array(); } $paste = self::upgradePreV1Format($paste); self::$_cache[$pasteid]['meta'] = $paste['meta']; - self::$_cache[$pasteid]['meta'][$createdKey] = (int) $paste['postdate']; - $expire_date = (int) $paste['expiredate']; + self::$_cache[$pasteid]['meta'][$createdKey] = (int) $paste[self::_sanitizeColumn('postdate')]; + $expire_date = (int) $paste[self::_sanitizeColumn('expiredate')]; if ($expire_date > 0) { self::$_cache[$pasteid]['meta']['expire_date'] = $expire_date; } @@ -272,16 +294,16 @@ class Database extends AbstractData } // support v1 attachments - if (array_key_exists('attachment', $paste) && strlen($paste['attachment'])) { - self::$_cache[$pasteid]['attachment'] = $paste['attachment']; - if (array_key_exists('attachmentname', $paste) && strlen($paste['attachmentname'])) { - self::$_cache[$pasteid]['attachmentname'] = $paste['attachmentname']; + if (array_key_exists(self::_sanitizeColumn('attachment'), $paste) && strlen($paste[self::_sanitizeColumn('attachment')])) { + self::$_cache[$pasteid]['attachment'] = $paste[self::_sanitizeColumn('attachment')]; + if (array_key_exists(self::_sanitizeColumn('attachmentname'), $paste) && strlen($paste[self::_sanitizeColumn('attachmentname')])) { + self::$_cache[$pasteid]['attachmentname'] = $paste[self::_sanitizeColumn('attachmentname')]; } } - if ($paste['opendiscussion']) { + if ($paste[self::_sanitizeColumn('opendiscussion')]) { self::$_cache[$pasteid]['meta']['opendiscussion'] = true; } - if ($paste['burnafterreading']) { + if ($paste[self::_sanitizeColumn('burnafterreading')]) { self::$_cache[$pasteid]['meta']['burnafterreading'] = true; } @@ -356,6 +378,21 @@ class Database extends AbstractData } } try { + if (self::$_type === 'oci') { + # It is not possible to execute in the normal way if strlen($big_string) >= 4000 + $stmt = self::$_db->prepare( + 'INSERT INTO ' . self::_sanitizeIdentifier('comment') . + ' VALUES(?,?,?,?,?,?,?)' + ); + $stmt->bindParam(1, $commentid); + $stmt->bindParam(2, $pasteid); + $stmt->bindParam(3, $parentid); + $stmt->bindParam(4, $data, PDO::PARAM_STR, strlen($data)); + $stmt->bindParam(5, $meta['nickname']); + $stmt->bindParam(6, $meta[$iconKey]); + $stmt->bindParam(7, $meta[$createdKey], PDO::PARAM_INT); + return $stmt->execute(); + } return self::_exec( 'INSERT INTO ' . self::_sanitizeIdentifier('comment') . ' VALUES(?,?,?,?,?,?,?)', @@ -392,22 +429,33 @@ class Database extends AbstractData $comments = array(); if (count($rows)) { foreach ($rows as $row) { - $i = $this->getOpenSlot($comments, (int) $row['postdate']); - $data = Json::decode($row['data']); + $i = $this->getOpenSlot($comments, (int) $row[self::_sanitizeColumn('postdate')]); + $id = $row[self::_sanitizeColumn('dataid')]; + if (self::$_type === 'oci') { + $newrow = self::_select( + 'SELECT data FROM ' . self::_sanitizeIdentifier('comment') . + ' WHERE dataid = ?', array($id), true + ); + $rawData = self::_clob($newrow['DATA']); + } + else { + $rawData = $row['data']; + } + $data = Json::decode($rawData); if (array_key_exists('v', $data) && $data['v'] >= 2) { $version = 2; $comments[$i] = $data; } else { $version = 1; - $comments[$i] = array('data' => $row['data']); + $comments[$i] = array('data' => $rawData); } list($createdKey, $iconKey) = self::_getVersionedKeys($version); - $comments[$i]['id'] = $row['dataid']; - $comments[$i]['parentid'] = $row['parentid']; - $comments[$i]['meta'] = array($createdKey => (int) $row['postdate']); + $comments[$i]['id'] = $id; + $comments[$i]['parentid'] = $row[self::_sanitizeColumn('parentid')]; + $comments[$i]['meta'] = array($createdKey => (int) $row[self::_sanitizeColumn('postdate')]); foreach (array('nickname' => 'nickname', 'vizhash' => $iconKey) as $rowKey => $commentKey) { - if (array_key_exists($rowKey, $row) && !empty($row[$rowKey])) { - $comments[$i]['meta'][$commentKey] = $row[$rowKey]; + if (array_key_exists(self::_sanitizeColumn($rowKey), $row) && !empty($row[self::_sanitizeColumn($rowKey)])) { + $comments[$i]['meta'][$commentKey] = $row[self::_sanitizeColumn($rowKey)]; } } } @@ -518,7 +566,8 @@ class Database extends AbstractData $pastes = array(); $rows = self::_select( 'SELECT dataid FROM ' . self::_sanitizeIdentifier('paste') . - ' WHERE expiredate < ? AND expiredate != ? LIMIT ?', + ' WHERE expiredate < ? AND expiredate != ? ' . + (self::$_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?'), array(time(), 0, $batchsize) ); if (count($rows)) { @@ -658,7 +707,7 @@ class Database extends AbstractData } catch (PDOException $e) { return ''; } - return $row ? $row['value'] : ''; + return $row ? $row[self::_sanitizeColumn('value')] : ''; } /** @@ -789,7 +838,21 @@ class Database extends AbstractData */ private static function _sanitizeIdentifier($identifier) { - return preg_replace('/[^A-Za-z0-9_]+/', '', self::$_prefix . $identifier); + $id = preg_replace('/[^A-Za-z0-9_]+/', '', self::$_prefix . $identifier); + return self::_sanitizeColumn($id); + } + + /** + * sanitizes column name because OCI + * + * @access private + * @static + * @param string $name + * @return string + */ + private static function _sanitizeColumn($name) + { + return self::$_type === 'oci' ? strtoupper($name) : $name; } /** @@ -865,4 +928,22 @@ class Database extends AbstractData ); } } + + /** + * read CLOB for OCI + * https://stackoverflow.com/questions/36200534/pdo-oci-into-a-clob-field + * + * @access private + * @static + * @param object $column + * @return string + */ + private static function _clob($column) + { + if ($column == null) return null; + $str = ""; + while ($column !== null and $tmp = fread($column, 1024)) + $str .= $tmp; + return $str; + } } From 6a489d35ab39041b3dd793d91beaf8681489bf4f Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 18 Jan 2022 11:21:25 -0500 Subject: [PATCH 02/27] Support OCI (Create table) --- lib/Data/Database.php | 71 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index a2b1fe1a..617676c3 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -572,7 +572,7 @@ class Database extends AbstractData ); if (count($rows)) { foreach ($rows as $row) { - $pastes[] = $row['dataid']; + $pastes[] = $row[self::_sanitizeIdentifier('dataid')]; } } return $pastes; @@ -710,6 +710,18 @@ class Database extends AbstractData return $row ? $row[self::_sanitizeColumn('value')] : ''; } + /** + * OCI cannot accept semicolons + * + * @access private + * @static + * @return string + */ + private static function _getSemicolon() + { + return self::$_type === 'oci' ? "" : ";"; + } + /** * get the primary key clauses, depending on the database driver * @@ -721,7 +733,7 @@ class Database extends AbstractData private static function _getPrimaryKeyClauses($key = 'dataid') { $main_key = $after_key = ''; - if (self::$_type === 'mysql') { + if (self::$_type === 'mysql' || self::$_type === 'oci') { $after_key = ", PRIMARY KEY ($key)"; } else { $main_key = ' PRIMARY KEY'; @@ -740,13 +752,13 @@ class Database extends AbstractData */ private static function _getDataType() { - return self::$_type === 'pgsql' ? 'TEXT' : 'BLOB'; + return self::$_type === 'pgsql' ? 'TEXT' : (self::$_type === 'oci' ? 'VARCHAR2(4000)' : 'BLOB'); } /** * get the attachment type, depending on the database driver * - * PostgreSQL uses a different API for BLOBs then SQL, hence we use TEXT + * PostgreSQL and OCI use different APIs for BLOBs then SQL, hence we use TEXT and CLOB * * @access private * @static @@ -754,7 +766,21 @@ class Database extends AbstractData */ private static function _getAttachmentType() { - return self::$_type === 'pgsql' ? 'TEXT' : 'MEDIUMBLOB'; + return self::$_type === 'pgsql' ? 'TEXT' : (self::$_type === 'oci' ? 'CLOB' : 'MEDIUMBLOB'); + } + + /** + * get the meta type, depending on the database driver + * + * OCI can't even accept TEXT so it has to be VARCHAR2(200) + * + * @access private + * @static + * @return string + */ + private static function _getMetaType() + { + return self::$_type === 'oci' ? 'VARCHAR2(4000)' : 'TEXT'; } /** @@ -768,6 +794,7 @@ class Database extends AbstractData list($main_key, $after_key) = self::_getPrimaryKeyClauses(); $dataType = self::_getDataType(); $attachmentType = self::_getAttachmentType(); + $metaType = self::_getMetaType(); self::$_db->exec( 'CREATE TABLE ' . self::_sanitizeIdentifier('paste') . ' ( ' . "dataid CHAR(16) NOT NULL$main_key, " . @@ -776,12 +803,26 @@ class Database extends AbstractData 'expiredate INT, ' . 'opendiscussion INT, ' . 'burnafterreading INT, ' . - 'meta TEXT, ' . + "meta $metaType, " . "attachment $attachmentType, " . - "attachmentname $dataType$after_key );" + "attachmentname $dataType$after_key )" . self::_getSemicolon() ); } + /** + * get the nullable text type, depending on the database driver + * + * OCI will pad CHAR columns with spaces, hence VARCHAR2 + * + * @access private + * @static + * @return string + */ + private static function _getParentType() + { + return self::$_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)'; + } + /** * create the paste table * @@ -792,19 +833,21 @@ class Database extends AbstractData { list($main_key, $after_key) = self::_getPrimaryKeyClauses(); $dataType = self::_getDataType(); + $parentType = self::_getParentType(); + $attachmentType = self::_getAttachmentType(); self::$_db->exec( 'CREATE TABLE ' . self::_sanitizeIdentifier('comment') . ' ( ' . "dataid CHAR(16) NOT NULL$main_key, " . 'pasteid CHAR(16), ' . - 'parentid CHAR(16), ' . - "data $dataType, " . + "parentid $parentType, " . + "data $attachmentType, " . "nickname $dataType, " . "vizhash $dataType, " . - "postdate INT$after_key );" + "postdate INT$after_key )" . self::_getSemicolon() ); self::$_db->exec( - 'CREATE INDEX IF NOT EXISTS comment_parent ON ' . - self::_sanitizeIdentifier('comment') . '(pasteid);' + 'CREATE INDEX comment_parent ON ' . + self::_sanitizeIdentifier('comment') . '(pasteid)' . self::_getSemicolon() ); } @@ -817,9 +860,11 @@ class Database extends AbstractData private static function _createConfigTable() { list($main_key, $after_key) = self::_getPrimaryKeyClauses('id'); + $charType = self::$_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)'; + $textType = self::$_type === 'oci' ? 'VARCHAR2(4000)' : 'TEXT'; self::$_db->exec( 'CREATE TABLE ' . self::_sanitizeIdentifier('config') . - " ( id CHAR(16) NOT NULL$main_key, value TEXT$after_key );" + " ( id $charType NOT NULL$main_key, value $textType$after_key )" . self::_getSemicolon() ); self::_exec( 'INSERT INTO ' . self::_sanitizeIdentifier('config') . From 041ef7f7a5865c6bf8163e9f8219a2029d05a2e1 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Thu, 20 Jan 2022 13:33:23 -0500 Subject: [PATCH 03/27] Support OCI (Satisfy the CI) --- lib/Data/Database.php | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 617676c3..a89da4b9 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -199,9 +199,10 @@ class Database extends AbstractData $burnafterreading = $paste['adata'][3]; } try { - $big_string = $isVersion1 ? $paste['data'] : Json::encode($paste); + $big_string = $isVersion1 ? $paste['data'] : Json::encode($paste); + $metajson = Json::encode($meta); if (self::$_type === 'oci') { - # It is not possible to execute in the normal way if strlen($big_string) >= 4000 + // It is not possible to execute in the normal way if strlen($big_string) >= 4000 $stmt = self::$_db->prepare( 'INSERT INTO ' . self::_sanitizeIdentifier('paste') . ' VALUES(?,?,?,?,?,?,?,?,?)' @@ -212,7 +213,7 @@ class Database extends AbstractData $stmt->bindParam(4, $expire_date, PDO::PARAM_INT); $stmt->bindParam(5, $opendiscussion, PDO::PARAM_INT); $stmt->bindParam(6, $burnafterreading, PDO::PARAM_INT); - $stmt->bindParam(7, Json::encode($meta)); + $stmt->bindParam(7, $metajson); $stmt->bindParam(8, $attachment, PDO::PARAM_STR, strlen($attachment)); $stmt->bindParam(9, $attachmentname); return $stmt->execute(); @@ -251,7 +252,7 @@ class Database extends AbstractData } self::$_cache[$pasteid] = false; - $rawData = ""; + $rawData = ''; try { $paste = self::_select( 'SELECT * FROM ' . self::_sanitizeIdentifier('paste') . @@ -379,7 +380,7 @@ class Database extends AbstractData } try { if (self::$_type === 'oci') { - # It is not possible to execute in the normal way if strlen($big_string) >= 4000 + // It is not possible to execute in the normal way if strlen($big_string) >= 4000 $stmt = self::$_db->prepare( 'INSERT INTO ' . self::_sanitizeIdentifier('comment') . ' VALUES(?,?,?,?,?,?,?)' @@ -437,8 +438,7 @@ class Database extends AbstractData ' WHERE dataid = ?', array($id), true ); $rawData = self::_clob($newrow['DATA']); - } - else { + } else { $rawData = $row['data']; } $data = Json::decode($rawData); @@ -719,7 +719,7 @@ class Database extends AbstractData */ private static function _getSemicolon() { - return self::$_type === 'oci' ? "" : ";"; + return self::$_type === 'oci' ? '' : ';'; } /** @@ -980,15 +980,18 @@ class Database extends AbstractData * * @access private * @static - * @param object $column + * @param resource $column * @return string */ private static function _clob($column) { - if ($column == null) return null; - $str = ""; - while ($column !== null and $tmp = fread($column, 1024)) + if ($column == null) { + return null; + } + $str = ''; + while ($tmp = fread($column, 1024)) { $str .= $tmp; + } return $str; } } From 2182cdd44f092277ce829cb9effd861ceb415c06 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 22 Jan 2022 08:45:12 +0100 Subject: [PATCH 04/27] generalize OCI handling of queries and results --- CHANGELOG.md | 11 +- CREDITS.md | 1 + lib/Data/Database.php | 242 ++++++++++++++------------------------ tst/Data/DatabaseTest.php | 12 ++ 4 files changed, 105 insertions(+), 161 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d964a57a..428a60ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * ADDED: Opt-out of federated learning of cohorts (FLoC) (#776) * ADDED: Configuration option to exempt IPs from the rate-limiter (#787) * ADDED: Google Cloud Storage backend support (#795) + * ADDED: Oracle database support (#868) * CHANGED: Language selection cookie only transmitted over HTTPS (#472) * CHANGED: Upgrading libraries to: random_compat 2.0.20 * CHANGED: Removed automatic `.ini` configuration file migration (#808) @@ -65,7 +66,7 @@ * CHANGED: Upgrading libraries to: DOMpurify 2.0.1 * FIXED: Enabling browsers without WASM to create pastes and read uncompressed ones (#454) * FIXED: Cloning related issues (#489, #491, #493, #494) - * FIXED: Enable file operation only when editing (#497) + * FIXED: Enable file operation only when editing (#497) * FIXED: Clicking 'New' on a previously submitted paste does not blank address bar (#354) * FIXED: Clear address bar when create new paste from existing paste (#479) * FIXED: Discussion section not hiding when new/clone paste is clicked on (#484) @@ -228,7 +229,7 @@ encryption), i18n (translation, counterpart of i18n.php) and helper (stateless u * FIXED: 2 minor corrections to avoid notices in php log. * FIXED: Sources converted to UTF-8. * **Alpha 0.14 (2012-04-20):** - * ADDED: GD presence is checked. + * ADDED: GD presence is checked. * CHANGED: Traffic limiter data files moved to data/ (→easier rights management) * ADDED: "Burn after reading" implemented. Opening the URL will display the paste and immediately destroy it on server. * **Alpha 0.13 (2012-04-18):** @@ -236,16 +237,16 @@ encryption), i18n (translation, counterpart of i18n.php) and helper (stateless u * FIXED: $error not properly initialized in index.php * **Alpha 0.12 (2012-04-18):** * **DISCUSSIONS !** Now you can enable discussions on your pastes. Of course, posted comments and nickname are also encrypted and the server cannot see them. - * This feature implies a change in storage format. You will have to delete all previous pastes in your ZeroBin. + * This feature implies a change in storage format. You will have to delete all previous pastes in your ZeroBin. * Added [[php:vizhash_gd|Vizhash]] as avatars, so you can match posters IP addresses without revealing them. (Same image = same IP). Of course the IP address cannot be deduced from the Vizhash. * Remaining time before expiration is now displayed. - * Explicit tags were added to CSS and jQuery selectors (eg. div#aaa instead of #aaa) to speed up browser. + * Explicit tags were added to CSS and jQuery selectors (eg. div#aaa instead of #aaa) to speed up browser. * Better cleaning of the URL (to make sure the key is not broken by some stupid redirection service) * **Alpha 0.11 (2012-04-12):** * Automatically ignore parameters (such as &utm_source=...) added //after// the anchor by some stupid Web 2.0 services. * First public release. * **Alpha 0.10 (2012-04-12):** - * IE9 does not seem to correctly support ''pre-wrap'' either. Special handling mode activated for all version of IE<10. (Note: **ALL other browsers** correctly support this feature.) + * IE9 does not seem to correctly support ''pre-wrap'' either. Special handling mode activated for all version of IE<10. (Note: **ALL other browsers** correctly support this feature.) * **Alpha 0.9 (2012-04-11):** * Oh bummer... IE 8 is as shitty as IE6/7: Its does not seem to support ''white-space:pre-wrap'' correctly. I had to activate the special handling mode. I still have to test IE 9. * **Alpha 0.8 (2012-04-11):** diff --git a/CREDITS.md b/CREDITS.md index 612749c0..6c2f647c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -29,6 +29,7 @@ Sébastien Sauvage - original idea and main developer * Lucas Savva - configurable config file location, NixOS packaging * rodehoed - option to exempt ips from the rate-limiter * Mark van Holsteijn - Google Cloud Storage backend +* Austin Huang - Oracle database support ## Translations * Hexalyse - French diff --git a/lib/Data/Database.php b/lib/Data/Database.php index a89da4b9..f616a96a 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -199,31 +199,12 @@ class Database extends AbstractData $burnafterreading = $paste['adata'][3]; } try { - $big_string = $isVersion1 ? $paste['data'] : Json::encode($paste); - $metajson = Json::encode($meta); - if (self::$_type === 'oci') { - // It is not possible to execute in the normal way if strlen($big_string) >= 4000 - $stmt = self::$_db->prepare( - 'INSERT INTO ' . self::_sanitizeIdentifier('paste') . - ' VALUES(?,?,?,?,?,?,?,?,?)' - ); - $stmt->bindParam(1, $pasteid); - $stmt->bindParam(2, $big_string, PDO::PARAM_STR, strlen($big_string)); - $stmt->bindParam(3, $created, PDO::PARAM_INT); - $stmt->bindParam(4, $expire_date, PDO::PARAM_INT); - $stmt->bindParam(5, $opendiscussion, PDO::PARAM_INT); - $stmt->bindParam(6, $burnafterreading, PDO::PARAM_INT); - $stmt->bindParam(7, $metajson); - $stmt->bindParam(8, $attachment, PDO::PARAM_STR, strlen($attachment)); - $stmt->bindParam(9, $attachmentname); - return $stmt->execute(); - } return self::_exec( 'INSERT INTO ' . self::_sanitizeIdentifier('paste') . ' VALUES(?,?,?,?,?,?,?,?,?)', array( $pasteid, - $big_string, + $isVersion1 ? $paste['data'] : Json::encode($paste), $created, $expire_date, (int) $opendiscussion, @@ -252,15 +233,11 @@ class Database extends AbstractData } self::$_cache[$pasteid] = false; - $rawData = ''; try { $paste = self::_select( 'SELECT * FROM ' . self::_sanitizeIdentifier('paste') . ' WHERE dataid = ?', array($pasteid), true ); - if ($paste !== false) { - $rawData = self::$_type === 'oci' ? self::_clob($paste['DATA']) : $paste['data']; - } } catch (Exception $e) { $paste = false; } @@ -268,25 +245,25 @@ class Database extends AbstractData return false; } // create array - $data = Json::decode($rawData); + $data = Json::decode($paste['data']); $isVersion2 = array_key_exists('v', $data) && $data['v'] >= 2; if ($isVersion2) { self::$_cache[$pasteid] = $data; list($createdKey) = self::_getVersionedKeys(2); } else { - self::$_cache[$pasteid] = array('data' => $paste[self::_sanitizeColumn('data')]); + self::$_cache[$pasteid] = array('data' => $paste['data']); list($createdKey) = self::_getVersionedKeys(1); } try { - $paste['meta'] = Json::decode($paste[self::_sanitizeColumn('meta')]); + $paste['meta'] = Json::decode($paste['meta']); } catch (Exception $e) { $paste['meta'] = array(); } $paste = self::upgradePreV1Format($paste); self::$_cache[$pasteid]['meta'] = $paste['meta']; - self::$_cache[$pasteid]['meta'][$createdKey] = (int) $paste[self::_sanitizeColumn('postdate')]; - $expire_date = (int) $paste[self::_sanitizeColumn('expiredate')]; + self::$_cache[$pasteid]['meta'][$createdKey] = (int) $paste['postdate']; + $expire_date = (int) $paste['expiredate']; if ($expire_date > 0) { self::$_cache[$pasteid]['meta']['expire_date'] = $expire_date; } @@ -295,16 +272,16 @@ class Database extends AbstractData } // support v1 attachments - if (array_key_exists(self::_sanitizeColumn('attachment'), $paste) && strlen($paste[self::_sanitizeColumn('attachment')])) { - self::$_cache[$pasteid]['attachment'] = $paste[self::_sanitizeColumn('attachment')]; - if (array_key_exists(self::_sanitizeColumn('attachmentname'), $paste) && strlen($paste[self::_sanitizeColumn('attachmentname')])) { - self::$_cache[$pasteid]['attachmentname'] = $paste[self::_sanitizeColumn('attachmentname')]; + if (array_key_exists('attachment', $paste) && strlen($paste['attachment'])) { + self::$_cache[$pasteid]['attachment'] = $paste['attachment']; + if (array_key_exists('attachmentname', $paste) && strlen($paste['attachmentname'])) { + self::$_cache[$pasteid]['attachmentname'] = $paste['attachmentname']; } } - if ($paste[self::_sanitizeColumn('opendiscussion')]) { + if ($paste['opendiscussion']) { self::$_cache[$pasteid]['meta']['opendiscussion'] = true; } - if ($paste[self::_sanitizeColumn('burnafterreading')]) { + if ($paste['burnafterreading']) { self::$_cache[$pasteid]['meta']['burnafterreading'] = true; } @@ -379,21 +356,6 @@ class Database extends AbstractData } } try { - if (self::$_type === 'oci') { - // It is not possible to execute in the normal way if strlen($big_string) >= 4000 - $stmt = self::$_db->prepare( - 'INSERT INTO ' . self::_sanitizeIdentifier('comment') . - ' VALUES(?,?,?,?,?,?,?)' - ); - $stmt->bindParam(1, $commentid); - $stmt->bindParam(2, $pasteid); - $stmt->bindParam(3, $parentid); - $stmt->bindParam(4, $data, PDO::PARAM_STR, strlen($data)); - $stmt->bindParam(5, $meta['nickname']); - $stmt->bindParam(6, $meta[$iconKey]); - $stmt->bindParam(7, $meta[$createdKey], PDO::PARAM_INT); - return $stmt->execute(); - } return self::_exec( 'INSERT INTO ' . self::_sanitizeIdentifier('comment') . ' VALUES(?,?,?,?,?,?,?)', @@ -430,32 +392,22 @@ class Database extends AbstractData $comments = array(); if (count($rows)) { foreach ($rows as $row) { - $i = $this->getOpenSlot($comments, (int) $row[self::_sanitizeColumn('postdate')]); - $id = $row[self::_sanitizeColumn('dataid')]; - if (self::$_type === 'oci') { - $newrow = self::_select( - 'SELECT data FROM ' . self::_sanitizeIdentifier('comment') . - ' WHERE dataid = ?', array($id), true - ); - $rawData = self::_clob($newrow['DATA']); - } else { - $rawData = $row['data']; - } - $data = Json::decode($rawData); + $i = $this->getOpenSlot($comments, (int) $row['postdate']); + $data = Json::decode($row['data']); if (array_key_exists('v', $data) && $data['v'] >= 2) { $version = 2; $comments[$i] = $data; } else { $version = 1; - $comments[$i] = array('data' => $rawData); + $comments[$i] = array('data' => $row['data']); } list($createdKey, $iconKey) = self::_getVersionedKeys($version); - $comments[$i]['id'] = $id; - $comments[$i]['parentid'] = $row[self::_sanitizeColumn('parentid')]; - $comments[$i]['meta'] = array($createdKey => (int) $row[self::_sanitizeColumn('postdate')]); + $comments[$i]['id'] = $row['dataid']; + $comments[$i]['parentid'] = $row['parentid']; + $comments[$i]['meta'] = array($createdKey => (int) $row['postdate']); foreach (array('nickname' => 'nickname', 'vizhash' => $iconKey) as $rowKey => $commentKey) { - if (array_key_exists(self::_sanitizeColumn($rowKey), $row) && !empty($row[self::_sanitizeColumn($rowKey)])) { - $comments[$i]['meta'][$commentKey] = $row[self::_sanitizeColumn($rowKey)]; + if (array_key_exists($rowKey, $row) && !empty($row[$rowKey])) { + $comments[$i]['meta'][$commentKey] = $row[$rowKey]; } } } @@ -572,7 +524,7 @@ class Database extends AbstractData ); if (count($rows)) { foreach ($rows as $row) { - $pastes[] = $row[self::_sanitizeIdentifier('dataid')]; + $pastes[] = $row['dataid']; } } return $pastes; @@ -591,7 +543,22 @@ class Database extends AbstractData private static function _exec($sql, array $params) { $statement = self::$_db->prepare($sql); - $result = $statement->execute($params); + if (self::$_type === 'oci') { + // It is not possible to execute in the normal way if strlen($param) >= 4000 + foreach ($params as $key => $parameter) { + $position = $key + 1; + if (is_int($parameter)) { + $statement->bindParam($position, $parameter, PDO::PARAM_INT); + } elseif ($length = strlen($parameter) >= 4000) { + $statement->bindParam($position, $parameter, PDO::PARAM_STR, $length); + } else { + $statement->bindParam($position, $parameter); + } + } + $result = $statement->execute(); + } else { + $result = $statement->execute($params); + } $statement->closeCursor(); return $result; } @@ -615,6 +582,14 @@ class Database extends AbstractData $statement->fetch(PDO::FETCH_ASSOC) : $statement->fetchAll(PDO::FETCH_ASSOC); $statement->closeCursor(); + if (self::$_type === 'oci') { + // returned column names are all upper case, convert these back + // returned CLOB values are streams, convert these into strings + $result = array_combine( + array_map('strtolower', array_keys($result)), + array_map('self::_sanitizeClob', array_values($result)) + ); + } return $result; } @@ -707,19 +682,7 @@ class Database extends AbstractData } catch (PDOException $e) { return ''; } - return $row ? $row[self::_sanitizeColumn('value')] : ''; - } - - /** - * OCI cannot accept semicolons - * - * @access private - * @static - * @return string - */ - private static function _getSemicolon() - { - return self::$_type === 'oci' ? '' : ';'; + return $row ? $row['value'] : ''; } /** @@ -744,7 +707,7 @@ class Database extends AbstractData /** * get the data type, depending on the database driver * - * PostgreSQL uses a different API for BLOBs then SQL, hence we use TEXT + * PostgreSQL and OCI uses a different API for BLOBs then SQL, hence we use TEXT and CLOB * * @access private * @static @@ -752,7 +715,7 @@ class Database extends AbstractData */ private static function _getDataType() { - return self::$_type === 'pgsql' ? 'TEXT' : (self::$_type === 'oci' ? 'VARCHAR2(4000)' : 'BLOB'); + return self::$_type === 'pgsql' ? 'TEXT' : (self::$_type === 'oci' ? 'CLOB' : 'BLOB'); } /** @@ -772,7 +735,7 @@ class Database extends AbstractData /** * get the meta type, depending on the database driver * - * OCI can't even accept TEXT so it has to be VARCHAR2(200) + * OCI doesn't accept TEXT so it has to be VARCHAR2(4000) * * @access private * @static @@ -798,31 +761,17 @@ class Database extends AbstractData self::$_db->exec( 'CREATE TABLE ' . self::_sanitizeIdentifier('paste') . ' ( ' . "dataid CHAR(16) NOT NULL$main_key, " . - "data $attachmentType, " . + "data $dataType, " . 'postdate INT, ' . 'expiredate INT, ' . 'opendiscussion INT, ' . 'burnafterreading INT, ' . "meta $metaType, " . "attachment $attachmentType, " . - "attachmentname $dataType$after_key )" . self::_getSemicolon() + "attachmentname $dataType$after_key )" ); } - /** - * get the nullable text type, depending on the database driver - * - * OCI will pad CHAR columns with spaces, hence VARCHAR2 - * - * @access private - * @static - * @return string - */ - private static function _getParentType() - { - return self::$_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)'; - } - /** * create the paste table * @@ -833,21 +782,19 @@ class Database extends AbstractData { list($main_key, $after_key) = self::_getPrimaryKeyClauses(); $dataType = self::_getDataType(); - $parentType = self::_getParentType(); - $attachmentType = self::_getAttachmentType(); self::$_db->exec( 'CREATE TABLE ' . self::_sanitizeIdentifier('comment') . ' ( ' . "dataid CHAR(16) NOT NULL$main_key, " . 'pasteid CHAR(16), ' . - "parentid $parentType, " . - "data $attachmentType, " . + 'parentid CHAR(16), ' . + "data $dataType, " . "nickname $dataType, " . "vizhash $dataType, " . - "postdate INT$after_key )" . self::_getSemicolon() + "postdate INT$after_key )" ); self::$_db->exec( - 'CREATE INDEX comment_parent ON ' . - self::_sanitizeIdentifier('comment') . '(pasteid)' . self::_getSemicolon() + 'CREATE INDEX IF NOT EXISTS comment_parent ON ' . + self::_sanitizeIdentifier('comment') . '(pasteid)' ); } @@ -864,7 +811,7 @@ class Database extends AbstractData $textType = self::$_type === 'oci' ? 'VARCHAR2(4000)' : 'TEXT'; self::$_db->exec( 'CREATE TABLE ' . self::_sanitizeIdentifier('config') . - " ( id $charType NOT NULL$main_key, value $textType$after_key )" . self::_getSemicolon() + " ( id $charType NOT NULL$main_key, value $textType$after_key )" ); self::_exec( 'INSERT INTO ' . self::_sanitizeIdentifier('config') . @@ -873,6 +820,24 @@ class Database extends AbstractData ); } + /** + * sanitizes CLOB values used with OCI + * + * From: https://stackoverflow.com/questions/36200534/pdo-oci-into-a-clob-field + * + * @access private + * @static + * @param int|string|resource $value + * @return int|string + */ + public static function _sanitizeClob($value) + { + if (is_resource($value)) { + $value = stream_get_contents($value); + } + return $value; + } + /** * sanitizes identifiers * @@ -883,21 +848,7 @@ class Database extends AbstractData */ private static function _sanitizeIdentifier($identifier) { - $id = preg_replace('/[^A-Za-z0-9_]+/', '', self::$_prefix . $identifier); - return self::_sanitizeColumn($id); - } - - /** - * sanitizes column name because OCI - * - * @access private - * @static - * @param string $name - * @return string - */ - private static function _sanitizeColumn($name) - { - return self::$_type === 'oci' ? strtoupper($name) : $name; + return preg_replace('/[^A-Za-z0-9_]+/', '', self::$_prefix . $identifier); } /** @@ -915,43 +866,43 @@ class Database extends AbstractData case '0.21': // create the meta column if necessary (pre 0.21 change) try { - self::$_db->exec('SELECT meta FROM ' . self::_sanitizeIdentifier('paste') . ' LIMIT 1;'); + self::$_db->exec('SELECT meta FROM ' . self::_sanitizeIdentifier('paste') . ' LIMIT 1'); } catch (PDOException $e) { - self::$_db->exec('ALTER TABLE ' . self::_sanitizeIdentifier('paste') . ' ADD COLUMN meta TEXT;'); + self::$_db->exec('ALTER TABLE ' . self::_sanitizeIdentifier('paste') . ' ADD COLUMN meta TEXT'); } // SQLite only allows one ALTER statement at a time... self::$_db->exec( 'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . - " ADD COLUMN attachment $attachmentType;" + " ADD COLUMN attachment $attachmentType" ); self::$_db->exec( - 'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . " ADD COLUMN attachmentname $dataType;" + 'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . " ADD COLUMN attachmentname $dataType" ); // SQLite doesn't support MODIFY, but it allows TEXT of similar // size as BLOB, so there is no need to change it there if (self::$_type !== 'sqlite') { self::$_db->exec( 'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . - " ADD PRIMARY KEY (dataid), MODIFY COLUMN data $dataType;" + " ADD PRIMARY KEY (dataid), MODIFY COLUMN data $dataType" ); self::$_db->exec( 'ALTER TABLE ' . self::_sanitizeIdentifier('comment') . " ADD PRIMARY KEY (dataid), MODIFY COLUMN data $dataType, " . - "MODIFY COLUMN nickname $dataType, MODIFY COLUMN vizhash $dataType;" + "MODIFY COLUMN nickname $dataType, MODIFY COLUMN vizhash $dataType" ); } else { self::$_db->exec( 'CREATE UNIQUE INDEX IF NOT EXISTS paste_dataid ON ' . - self::_sanitizeIdentifier('paste') . '(dataid);' + self::_sanitizeIdentifier('paste') . '(dataid)' ); self::$_db->exec( 'CREATE UNIQUE INDEX IF NOT EXISTS comment_dataid ON ' . - self::_sanitizeIdentifier('comment') . '(dataid);' + self::_sanitizeIdentifier('comment') . '(dataid)' ); } self::$_db->exec( 'CREATE INDEX IF NOT EXISTS comment_parent ON ' . - self::_sanitizeIdentifier('comment') . '(pasteid);' + self::_sanitizeIdentifier('comment') . '(pasteid)' ); // no break, continue with updates for 0.22 and later case '1.3': @@ -961,7 +912,7 @@ class Database extends AbstractData if (self::$_type !== 'sqlite' && self::$_type !== 'pgsql') { self::$_db->exec( 'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . - " MODIFY COLUMN data $attachmentType;" + " MODIFY COLUMN data $attachmentType" ); } // no break, continue with updates for all newer versions @@ -973,25 +924,4 @@ class Database extends AbstractData ); } } - - /** - * read CLOB for OCI - * https://stackoverflow.com/questions/36200534/pdo-oci-into-a-clob-field - * - * @access private - * @static - * @param resource $column - * @return string - */ - private static function _clob($column) - { - if ($column == null) { - return null; - } - $str = ''; - while ($tmp = fread($column, 1024)) { - $str .= $tmp; - } - return $str; - } } diff --git a/tst/Data/DatabaseTest.php b/tst/Data/DatabaseTest.php index 1cfc0bea..c433b5a1 100644 --- a/tst/Data/DatabaseTest.php +++ b/tst/Data/DatabaseTest.php @@ -388,4 +388,16 @@ class DatabaseTest extends PHPUnit_Framework_TestCase $this->assertEquals(Controller::VERSION, $result['value']); Helper::rmDir($this->_path); } + + public function testOciClob() + { + $int = (int) random_bytes(1); + $string = random_bytes(10); + $clob = fopen('php://memory', 'r+'); + fwrite($clob, $string); + rewind($clob); + $this->assertEquals($int, Database::_sanitizeClob($int)); + $this->assertEquals($string, Database::_sanitizeClob($string)); + $this->assertEquals($string, Database::_sanitizeClob($clob)); + } } From 585d5db983d3a2e25197c548c2e77d0a0fbb3061 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 22 Jan 2022 08:47:34 +0100 Subject: [PATCH 05/27] apply StyleCI recommendation --- tst/Data/DatabaseTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tst/Data/DatabaseTest.php b/tst/Data/DatabaseTest.php index c433b5a1..16e6fcb8 100644 --- a/tst/Data/DatabaseTest.php +++ b/tst/Data/DatabaseTest.php @@ -391,9 +391,9 @@ class DatabaseTest extends PHPUnit_Framework_TestCase public function testOciClob() { - $int = (int) random_bytes(1); + $int = (int) random_bytes(1); $string = random_bytes(10); - $clob = fopen('php://memory', 'r+'); + $clob = fopen('php://memory', 'r+'); fwrite($clob, $string); rewind($clob); $this->assertEquals($int, Database::_sanitizeClob($int)); From c725b4f0feb2f44cc46cc67b3236d29078fe8716 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 22 Jan 2022 21:29:39 +0100 Subject: [PATCH 06/27] handle 'IF NOT EXISTS' differently in OCI --- lib/Data/Database.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index f616a96a..59c7017b 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -792,10 +792,24 @@ class Database extends AbstractData "vizhash $dataType, " . "postdate INT$after_key )" ); - self::$_db->exec( - 'CREATE INDEX IF NOT EXISTS comment_parent ON ' . - self::_sanitizeIdentifier('comment') . '(pasteid)' - ); + if (self::$_type === 'oci') { + self::$_db->exec( + 'declare + already_exists exception; + columns_indexed exception; + pragma exception_init( already_exists, -955 ); + pragma exception_init(columns_indexed, -1408); + begin + execute immediate \'create index comment_parent on ' . self::_sanitizeIdentifier('comment') . ' (pasteid)\'; + exception + end' + ); + } else { + self::$_db->exec( + 'CREATE INDEX IF NOT EXISTS comment_parent ON ' . + self::_sanitizeIdentifier('comment') . '(pasteid)' + ); + } } /** From 35ef64ff7989839ebe4e3083f82d2317ce07c001 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 22 Jan 2022 22:11:49 +0100 Subject: [PATCH 07/27] remove duplication, kudos @rugk --- lib/Data/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 59c7017b..7dc56747 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -822,7 +822,7 @@ class Database extends AbstractData { list($main_key, $after_key) = self::_getPrimaryKeyClauses('id'); $charType = self::$_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)'; - $textType = self::$_type === 'oci' ? 'VARCHAR2(4000)' : 'TEXT'; + $textType = self::_getMetaType(); self::$_db->exec( 'CREATE TABLE ' . self::_sanitizeIdentifier('config') . " ( id $charType NOT NULL$main_key, value $textType$after_key )" From 83336b294904ff808b351ee449396482c47ce4a4 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 23 Jan 2022 07:11:06 +0100 Subject: [PATCH 08/27] documented changes for Postgres and Oracle --- INSTALL.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 0e728237..da6812e0 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -190,8 +190,14 @@ CREATE TABLE prefix_config ( INSERT INTO prefix_config VALUES('VERSION', '1.3.5'); ``` -In **PostgreSQL**, the data, attachment, nickname and vizhash columns needs to -be TEXT and not BLOB or MEDIUMBLOB. +In **PostgreSQL**, the `data`, `attachment`, `nickname` and `vizhash` columns +need to be `TEXT` and not `BLOB` or `MEDIUMBLOB`. The key names in brackets, +after `PRIMARY KEY`, need to be removed. + +In **Oracle**, the `data`, `attachment`, `nickname` and `vizhash` columns need +to be `CLOB` and not `BLOB` or `MEDIUMBLOB`, the `id` column in the `config` +table needs to be `VARCHAR2(16)` and the `meta` column in the `paste` table +and the `value` column in the `config` table need to be `VARCHAR2(4000)`. ### Using Google Cloud Storage If you want to deploy PrivateBin in a serverless manner in the Google Cloud, you From 47deaeb7ca960def189f232f3a79db53eb33494e Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 23 Jan 2022 07:11:36 +0100 Subject: [PATCH 09/27] use the correct function --- lib/Data/Database.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 7dc56747..d725bb3b 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -548,11 +548,11 @@ class Database extends AbstractData foreach ($params as $key => $parameter) { $position = $key + 1; if (is_int($parameter)) { - $statement->bindParam($position, $parameter, PDO::PARAM_INT); + $statement->bindValue($position, $parameter, PDO::PARAM_INT); } elseif ($length = strlen($parameter) >= 4000) { - $statement->bindParam($position, $parameter, PDO::PARAM_STR, $length); + $statement->bindValue($position, $parameter, PDO::PARAM_STR, $length); } else { - $statement->bindParam($position, $parameter); + $statement->bindValue($position, $parameter); } } $result = $statement->execute(); From b54308a77eb7baed177030e54d4d4379dac88c53 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 23 Jan 2022 07:19:35 +0100 Subject: [PATCH 10/27] don't mangle non-arrays --- lib/Data/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index d725bb3b..a0694b06 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -582,7 +582,7 @@ class Database extends AbstractData $statement->fetch(PDO::FETCH_ASSOC) : $statement->fetchAll(PDO::FETCH_ASSOC); $statement->closeCursor(); - if (self::$_type === 'oci') { + if (self::$_type === 'oci' && is_array($result)) { // returned column names are all upper case, convert these back // returned CLOB values are streams, convert these into strings $result = array_combine( From b133c2e2330927312206c3566072eadf25ee807e Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 23 Jan 2022 07:32:28 +0100 Subject: [PATCH 11/27] sanitize both single rows and multiple ones --- lib/Data/Database.php | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index a0694b06..8c73a206 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -585,10 +585,9 @@ class Database extends AbstractData if (self::$_type === 'oci' && is_array($result)) { // returned column names are all upper case, convert these back // returned CLOB values are streams, convert these into strings - $result = array_combine( - array_map('strtolower', array_keys($result)), - array_map('self::_sanitizeClob', array_values($result)) - ); + $result = $firstOnly ? + self::_sanitizeOciRow($result) : + array_map('self::_sanitizeOciRow', $result); } return $result; } @@ -839,7 +838,7 @@ class Database extends AbstractData * * From: https://stackoverflow.com/questions/36200534/pdo-oci-into-a-clob-field * - * @access private + * @access public * @static * @param int|string|resource $value * @return int|string @@ -865,6 +864,22 @@ class Database extends AbstractData return preg_replace('/[^A-Za-z0-9_]+/', '', self::$_prefix . $identifier); } + /** + * sanitizes row returned by OCI + * + * @access private + * @static + * @param array $row + * @return array + */ + private static function _sanitizeOciRow($row) + { + return array_combine( + array_map('strtolower', array_keys($row)), + array_map('self::_sanitizeClob', array_values($row)) + ); + } + /** * upgrade the database schema from an old version * From 0be55e05bf45c5761360e8f6bd1c224ba1b9fbac Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 23 Jan 2022 20:59:02 +0100 Subject: [PATCH 12/27] use quoted identifiers, tell MySQL to expect ANSI SQL --- lib/Data/Database.php | 203 ++++++++++++++++++++---------------------- 1 file changed, 98 insertions(+), 105 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 8c73a206..d0616a7d 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -97,6 +97,11 @@ class Database extends AbstractData self::$_type = strtolower( substr($options['dsn'], 0, strpos($options['dsn'], ':')) ); + // MySQL uses backticks to quote identifiers by default, + // tell it to expect ANSI SQL double quotes + if (self::$_type === 'mysql' && defined('PDO::MYSQL_ATTR_INIT_COMMAND')) { + $options['opt'][PDO::MYSQL_ATTR_INIT_COMMAND] = "SET sql_mode='ANSI_QUOTES'"; + } $tableQuery = self::_getTableQuery(self::$_type); self::$_db = new PDO( $options['dsn'], @@ -200,8 +205,8 @@ class Database extends AbstractData } try { return self::_exec( - 'INSERT INTO ' . self::_sanitizeIdentifier('paste') . - ' VALUES(?,?,?,?,?,?,?,?,?)', + 'INSERT INTO "' . self::_sanitizeIdentifier('paste') . + '" VALUES(?,?,?,?,?,?,?,?,?)', array( $pasteid, $isVersion1 ? $paste['data'] : Json::encode($paste), @@ -235,8 +240,8 @@ class Database extends AbstractData self::$_cache[$pasteid] = false; try { $paste = self::_select( - 'SELECT * FROM ' . self::_sanitizeIdentifier('paste') . - ' WHERE dataid = ?', array($pasteid), true + 'SELECT * FROM "' . self::_sanitizeIdentifier('paste') . + '" WHERE "dataid" = ?', array($pasteid), true ); } catch (Exception $e) { $paste = false; @@ -297,12 +302,12 @@ class Database extends AbstractData public function delete($pasteid) { self::_exec( - 'DELETE FROM ' . self::_sanitizeIdentifier('paste') . - ' WHERE dataid = ?', array($pasteid) + 'DELETE FROM "' . self::_sanitizeIdentifier('paste') . + '" WHERE "dataid" = ?', array($pasteid) ); self::_exec( - 'DELETE FROM ' . self::_sanitizeIdentifier('comment') . - ' WHERE pasteid = ?', array($pasteid) + 'DELETE FROM "' . self::_sanitizeIdentifier('comment') . + '" WHERE "pasteid" = ?', array($pasteid) ); if ( array_key_exists($pasteid, self::$_cache) @@ -357,8 +362,8 @@ class Database extends AbstractData } try { return self::_exec( - 'INSERT INTO ' . self::_sanitizeIdentifier('comment') . - ' VALUES(?,?,?,?,?,?,?)', + 'INSERT INTO "' . self::_sanitizeIdentifier('comment') . + '" VALUES(?,?,?,?,?,?,?)', array( $commentid, $pasteid, @@ -384,8 +389,8 @@ class Database extends AbstractData public function readComments($pasteid) { $rows = self::_select( - 'SELECT * FROM ' . self::_sanitizeIdentifier('comment') . - ' WHERE pasteid = ?', array($pasteid) + 'SELECT * FROM "' . self::_sanitizeIdentifier('comment') . + '" WHERE "pasteid" = ?', array($pasteid) ); // create comment list @@ -429,8 +434,8 @@ class Database extends AbstractData { try { return (bool) self::_select( - 'SELECT dataid FROM ' . self::_sanitizeIdentifier('comment') . - ' WHERE pasteid = ? AND parentid = ? AND dataid = ?', + 'SELECT "dataid" FROM "' . self::_sanitizeIdentifier('comment') . + '" WHERE "pasteid" = ? AND "parentid" = ? AND "dataid" = ?', array($pasteid, $parentid, $commentid), true ); } catch (Exception $e) { @@ -458,8 +463,8 @@ class Database extends AbstractData } } return self::_exec( - 'UPDATE ' . self::_sanitizeIdentifier('config') . - ' SET value = ? WHERE id = ?', + 'UPDATE "' . self::_sanitizeIdentifier('config') . + '" SET "value" = ? WHERE "id" = ?', array($value, strtoupper($namespace)) ); } @@ -479,8 +484,8 @@ class Database extends AbstractData if ($value === '') { // initialize the row, so that setValue can rely on UPDATE queries self::_exec( - 'INSERT INTO ' . self::_sanitizeIdentifier('config') . - ' VALUES(?,?)', + 'INSERT INTO "' . self::_sanitizeIdentifier('config') . + '" VALUES(?,?)', array($configKey, '') ); @@ -517,8 +522,8 @@ class Database extends AbstractData { $pastes = array(); $rows = self::_select( - 'SELECT dataid FROM ' . self::_sanitizeIdentifier('paste') . - ' WHERE expiredate < ? AND expiredate != ? ' . + 'SELECT "dataid" FROM "' . self::_sanitizeIdentifier('paste') . + '" WHERE "expiredate" < ? AND "expiredate" != ? ' . (self::$_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?'), array(time(), 0, $batchsize) ); @@ -586,8 +591,12 @@ class Database extends AbstractData // returned column names are all upper case, convert these back // returned CLOB values are streams, convert these into strings $result = $firstOnly ? - self::_sanitizeOciRow($result) : - array_map('self::_sanitizeOciRow', $result); + array_map('self::_sanitizeClob', $result) : + array_map( + function ($row) { + return array_map('self::_sanitizeClob', $row); + }, $result + ); } return $result; } @@ -621,39 +630,39 @@ class Database extends AbstractData { switch ($type) { case 'ibm': - $sql = 'SELECT tabname FROM SYSCAT.TABLES '; + $sql = 'SELECT "tabname" FROM "SYSCAT"."TABLES"'; break; case 'informix': - $sql = 'SELECT tabname FROM systables '; + $sql = 'SELECT "tabname" FROM "systables"'; break; case 'mssql': - $sql = 'SELECT name FROM sysobjects ' - . "WHERE type = 'U' ORDER BY name"; + $sql = 'SELECT "name" FROM "sysobjects" ' + . 'WHERE "type" = \'U\' ORDER BY "name"'; break; case 'mysql': $sql = 'SHOW TABLES'; break; case 'oci': - $sql = 'SELECT table_name FROM all_tables'; + $sql = 'SELECT "table_name" FROM "all_tables"'; break; case 'pgsql': - $sql = 'SELECT c.relname AS table_name ' - . 'FROM pg_class c, pg_user u ' - . "WHERE c.relowner = u.usesysid AND c.relkind = 'r' " - . 'AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) ' - . "AND c.relname !~ '^(pg_|sql_)' " + $sql = 'SELECT c."relname" AS "table_name" ' + . 'FROM "pg_class" c, "pg_user" u ' + . "WHERE c.\"relowner\" = u.\"usesysid\" AND c.\"relkind\" = 'r' " + . 'AND NOT EXISTS (SELECT 1 FROM "pg_views" WHERE "viewname" = c."relname") ' + . "AND c.\"relname\" !~ '^(pg_|sql_)' " . 'UNION ' - . 'SELECT c.relname AS table_name ' - . 'FROM pg_class c ' - . "WHERE c.relkind = 'r' " - . 'AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) ' - . 'AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner) ' - . "AND c.relname !~ '^pg_'"; + . 'SELECT c."relname" AS "table_name" ' + . 'FROM "pg_class" c ' + . "WHERE c.\"relkind\" = 'r' " + . 'AND NOT EXISTS (SELECT 1 FROM "pg_views" WHERE "viewname" = c."relname") ' + . 'AND NOT EXISTS (SELECT 1 FROM "pg_user" WHERE "usesysid" = c."relowner") ' + . "AND c.\"relname\" !~ '^pg_'"; break; case 'sqlite': - $sql = "SELECT name FROM sqlite_master WHERE type='table' " - . 'UNION ALL SELECT name FROM sqlite_temp_master ' - . "WHERE type='table' ORDER BY name"; + $sql = 'SELECT "name" FROM "sqlite_master" WHERE "type"=\'table\' ' + . 'UNION ALL SELECT "name" FROM "sqlite_temp_master" ' + . 'WHERE "type"=\'table\' ORDER BY "name"'; break; default: throw new Exception( @@ -675,8 +684,8 @@ class Database extends AbstractData { try { $row = self::_select( - 'SELECT value FROM ' . self::_sanitizeIdentifier('config') . - ' WHERE id = ?', array($key), true + 'SELECT "value" FROM "' . self::_sanitizeIdentifier('config') . + '" WHERE "id" = ?', array($key), true ); } catch (PDOException $e) { return ''; @@ -696,7 +705,7 @@ class Database extends AbstractData { $main_key = $after_key = ''; if (self::$_type === 'mysql' || self::$_type === 'oci') { - $after_key = ", PRIMARY KEY ($key)"; + $after_key = ", PRIMARY KEY (\"$key\")"; } else { $main_key = ' PRIMARY KEY'; } @@ -758,16 +767,16 @@ class Database extends AbstractData $attachmentType = self::_getAttachmentType(); $metaType = self::_getMetaType(); self::$_db->exec( - 'CREATE TABLE ' . self::_sanitizeIdentifier('paste') . ' ( ' . - "dataid CHAR(16) NOT NULL$main_key, " . - "data $dataType, " . - 'postdate INT, ' . - 'expiredate INT, ' . - 'opendiscussion INT, ' . - 'burnafterreading INT, ' . - "meta $metaType, " . - "attachment $attachmentType, " . - "attachmentname $dataType$after_key )" + 'CREATE TABLE "' . self::_sanitizeIdentifier('paste') . '" ( ' . + "\"dataid\" CHAR(16) NOT NULL$main_key, " . + "\"data\" $dataType, " . + '"postdate" INT, ' . + '"expiredate" INT, ' . + '"opendiscussion" INT, ' . + '"burnafterreading" INT, ' . + "\"meta\" $metaType, " . + "\"attachment\" $attachmentType, " . + "\"attachmentname\" $dataType$after_key )" ); } @@ -782,14 +791,14 @@ class Database extends AbstractData list($main_key, $after_key) = self::_getPrimaryKeyClauses(); $dataType = self::_getDataType(); self::$_db->exec( - 'CREATE TABLE ' . self::_sanitizeIdentifier('comment') . ' ( ' . - "dataid CHAR(16) NOT NULL$main_key, " . - 'pasteid CHAR(16), ' . - 'parentid CHAR(16), ' . - "data $dataType, " . - "nickname $dataType, " . - "vizhash $dataType, " . - "postdate INT$after_key )" + 'CREATE TABLE "' . self::_sanitizeIdentifier('comment') . '" ( ' . + "\"dataid\" CHAR(16) NOT NULL$main_key, " . + '"pasteid" CHAR(16), ' . + '"parentid" CHAR(16), ' . + "\"data\" $dataType, " . + "\"nickname\" $dataType, " . + "\"vizhash\" $dataType, " . + "\"postdate\" INT$after_key )" ); if (self::$_type === 'oci') { self::$_db->exec( @@ -799,14 +808,14 @@ class Database extends AbstractData pragma exception_init( already_exists, -955 ); pragma exception_init(columns_indexed, -1408); begin - execute immediate \'create index comment_parent on ' . self::_sanitizeIdentifier('comment') . ' (pasteid)\'; + execute immediate \'create index "comment_parent" on "' . self::_sanitizeIdentifier('comment') . '" ("pasteid")\'; exception end' ); } else { self::$_db->exec( - 'CREATE INDEX IF NOT EXISTS comment_parent ON ' . - self::_sanitizeIdentifier('comment') . '(pasteid)' + 'CREATE INDEX IF NOT EXISTS "comment_parent" ON "' . + self::_sanitizeIdentifier('comment') . '" ("pasteid")' ); } } @@ -823,12 +832,12 @@ class Database extends AbstractData $charType = self::$_type === 'oci' ? 'VARCHAR2(16)' : 'CHAR(16)'; $textType = self::_getMetaType(); self::$_db->exec( - 'CREATE TABLE ' . self::_sanitizeIdentifier('config') . - " ( id $charType NOT NULL$main_key, value $textType$after_key )" + 'CREATE TABLE "' . self::_sanitizeIdentifier('config') . + "\" ( \"id\" $charType NOT NULL$main_key, \"value\" $textType$after_key )" ); self::_exec( - 'INSERT INTO ' . self::_sanitizeIdentifier('config') . - ' VALUES(?,?)', + 'INSERT INTO "' . self::_sanitizeIdentifier('config') . + '" VALUES(?,?)', array('VERSION', Controller::VERSION) ); } @@ -864,22 +873,6 @@ class Database extends AbstractData return preg_replace('/[^A-Za-z0-9_]+/', '', self::$_prefix . $identifier); } - /** - * sanitizes row returned by OCI - * - * @access private - * @static - * @param array $row - * @return array - */ - private static function _sanitizeOciRow($row) - { - return array_combine( - array_map('strtolower', array_keys($row)), - array_map('self::_sanitizeClob', array_values($row)) - ); - } - /** * upgrade the database schema from an old version * @@ -895,43 +888,43 @@ class Database extends AbstractData case '0.21': // create the meta column if necessary (pre 0.21 change) try { - self::$_db->exec('SELECT meta FROM ' . self::_sanitizeIdentifier('paste') . ' LIMIT 1'); + self::$_db->exec('SELECT "meta" FROM "' . self::_sanitizeIdentifier('paste') . '" LIMIT 1'); } catch (PDOException $e) { - self::$_db->exec('ALTER TABLE ' . self::_sanitizeIdentifier('paste') . ' ADD COLUMN meta TEXT'); + self::$_db->exec('ALTER TABLE "' . self::_sanitizeIdentifier('paste') . '" ADD COLUMN "meta" TEXT'); } // SQLite only allows one ALTER statement at a time... self::$_db->exec( - 'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . - " ADD COLUMN attachment $attachmentType" + 'ALTER TABLE "' . self::_sanitizeIdentifier('paste') . + "\" ADD COLUMN \"attachment\" $attachmentType" ); self::$_db->exec( - 'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . " ADD COLUMN attachmentname $dataType" + 'ALTER TABLE "' . self::_sanitizeIdentifier('paste') . "\" ADD COLUMN \"attachmentname\" $dataType" ); // SQLite doesn't support MODIFY, but it allows TEXT of similar // size as BLOB, so there is no need to change it there if (self::$_type !== 'sqlite') { self::$_db->exec( - 'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . - " ADD PRIMARY KEY (dataid), MODIFY COLUMN data $dataType" + 'ALTER TABLE "' . self::_sanitizeIdentifier('paste') . + "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType" ); self::$_db->exec( - 'ALTER TABLE ' . self::_sanitizeIdentifier('comment') . - " ADD PRIMARY KEY (dataid), MODIFY COLUMN data $dataType, " . - "MODIFY COLUMN nickname $dataType, MODIFY COLUMN vizhash $dataType" + 'ALTER TABLE "' . self::_sanitizeIdentifier('comment') . + "\" ADD PRIMARY KEY (\"dataid\"), MODIFY COLUMN \"data\" $dataType, " . + "MODIFY COLUMN \"nickname\" $dataType, MODIFY COLUMN \"vizhash\" $dataType" ); } else { self::$_db->exec( - 'CREATE UNIQUE INDEX IF NOT EXISTS paste_dataid ON ' . - self::_sanitizeIdentifier('paste') . '(dataid)' + 'CREATE UNIQUE INDEX IF NOT EXISTS "paste_dataid" ON "' . + self::_sanitizeIdentifier('paste') . '" ("dataid")' ); self::$_db->exec( - 'CREATE UNIQUE INDEX IF NOT EXISTS comment_dataid ON ' . - self::_sanitizeIdentifier('comment') . '(dataid)' + 'CREATE UNIQUE INDEX IF NOT EXISTS "comment_dataid" ON "' . + self::_sanitizeIdentifier('comment') . '" ("dataid")' ); } self::$_db->exec( - 'CREATE INDEX IF NOT EXISTS comment_parent ON ' . - self::_sanitizeIdentifier('comment') . '(pasteid)' + 'CREATE INDEX IF NOT EXISTS "comment_parent" ON "' . + self::_sanitizeIdentifier('comment') . '" ("pasteid")' ); // no break, continue with updates for 0.22 and later case '1.3': @@ -940,15 +933,15 @@ class Database extends AbstractData // to change it there if (self::$_type !== 'sqlite' && self::$_type !== 'pgsql') { self::$_db->exec( - 'ALTER TABLE ' . self::_sanitizeIdentifier('paste') . - " MODIFY COLUMN data $attachmentType" + 'ALTER TABLE "' . self::_sanitizeIdentifier('paste') . + "\" MODIFY COLUMN \"data\" $attachmentType" ); } // no break, continue with updates for all newer versions default: self::_exec( - 'UPDATE ' . self::_sanitizeIdentifier('config') . - ' SET value = ? WHERE id = ?', + 'UPDATE "' . self::_sanitizeIdentifier('config') . + '" SET "value" = ? WHERE "id" = ?', array(Controller::VERSION, 'VERSION') ); } From 8d6392192428d6a0099fa0cc29ec8c52d7c99deb Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 23 Jan 2022 21:24:28 +0100 Subject: [PATCH 13/27] workaround bug in OCI PDO driver --- lib/Data/Database.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index d0616a7d..1e9adea4 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -583,9 +583,17 @@ class Database extends AbstractData { $statement = self::$_db->prepare($sql); $statement->execute($params); - $result = $firstOnly ? - $statement->fetch(PDO::FETCH_ASSOC) : - $statement->fetchAll(PDO::FETCH_ASSOC); + if ($firstOnly) { + $result = $statement->fetch(PDO::FETCH_ASSOC); + } elseif (self::$_type === 'oci') { + // workaround for https://bugs.php.net/bug.php?id=46728 + $result = array(); + while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { + $result[] = $row; + } + } else { + $result = $statement->fetchAll(PDO::FETCH_ASSOC); + } $statement->closeCursor(); if (self::$_type === 'oci' && is_array($result)) { // returned column names are all upper case, convert these back From 4f051fe5a5d7891e0d3d2f69ea9787ea637114f8 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 23 Jan 2022 21:31:40 +0100 Subject: [PATCH 14/27] revert regression --- lib/Data/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 1e9adea4..1441648c 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -777,7 +777,7 @@ class Database extends AbstractData self::$_db->exec( 'CREATE TABLE "' . self::_sanitizeIdentifier('paste') . '" ( ' . "\"dataid\" CHAR(16) NOT NULL$main_key, " . - "\"data\" $dataType, " . + "\"data\" $attachmentType, " . '"postdate" INT, ' . '"expiredate" INT, ' . '"opendiscussion" INT, ' . From 0cc2b677538ac948132c1ee674c433ccdf104640 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 23 Jan 2022 21:45:22 +0100 Subject: [PATCH 15/27] bindValue doesn't need the length --- lib/Data/Database.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 1441648c..583c4008 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -395,7 +395,7 @@ class Database extends AbstractData // create comment list $comments = array(); - if (count($rows)) { + if (is_array($rows) && count($rows)) { foreach ($rows as $row) { $i = $this->getOpenSlot($comments, (int) $row['postdate']); $data = Json::decode($row['data']); @@ -527,7 +527,7 @@ class Database extends AbstractData (self::$_type === 'oci' ? 'FETCH NEXT ? ROWS ONLY' : 'LIMIT ?'), array(time(), 0, $batchsize) ); - if (count($rows)) { + if (is_array($rows) && count($rows)) { foreach ($rows as $row) { $pastes[] = $row['dataid']; } @@ -554,8 +554,6 @@ class Database extends AbstractData $position = $key + 1; if (is_int($parameter)) { $statement->bindValue($position, $parameter, PDO::PARAM_INT); - } elseif ($length = strlen($parameter) >= 4000) { - $statement->bindValue($position, $parameter, PDO::PARAM_STR, $length); } else { $statement->bindValue($position, $parameter); } From a8e1c33b54ed612fd7fdbdb00a2663ee1ebdb9f3 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 24 Jan 2022 17:26:09 +0100 Subject: [PATCH 16/27] stick to single convention of binding parameters --- lib/Data/Database.php | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 583c4008..92aeec75 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -548,20 +548,15 @@ class Database extends AbstractData private static function _exec($sql, array $params) { $statement = self::$_db->prepare($sql); - if (self::$_type === 'oci') { - // It is not possible to execute in the normal way if strlen($param) >= 4000 - foreach ($params as $key => $parameter) { - $position = $key + 1; - if (is_int($parameter)) { - $statement->bindValue($position, $parameter, PDO::PARAM_INT); - } else { - $statement->bindValue($position, $parameter); - } + foreach ($params as $key => $parameter) { + $position = $key + 1; + if (is_int($parameter)) { + $statement->bindValue($position, $parameter, PDO::PARAM_INT); + } else { + $statement->bindValue($position, $parameter); } - $result = $statement->execute(); - } else { - $result = $statement->execute($params); } + $result = $statement->execute(); $statement->closeCursor(); return $result; } From 56c54dd8800399b16403ff9901e8a3048811a7aa Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 24 Jan 2022 17:48:27 +0100 Subject: [PATCH 17/27] prefer switch statements for complex logic, all comparing the same variable --- lib/Data/Database.php | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 92aeec75..27e71c9a 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -705,10 +705,14 @@ class Database extends AbstractData private static function _getPrimaryKeyClauses($key = 'dataid') { $main_key = $after_key = ''; - if (self::$_type === 'mysql' || self::$_type === 'oci') { - $after_key = ", PRIMARY KEY (\"$key\")"; - } else { - $main_key = ' PRIMARY KEY'; + switch (self::$_type) { + case 'mysql': + case 'oci': + $after_key = ", PRIMARY KEY (\"$key\")"; + break; + default: + $main_key = ' PRIMARY KEY'; + break; } return array($main_key, $after_key); } @@ -724,7 +728,14 @@ class Database extends AbstractData */ private static function _getDataType() { - return self::$_type === 'pgsql' ? 'TEXT' : (self::$_type === 'oci' ? 'CLOB' : 'BLOB'); + switch (self::$_type) { + case 'oci': + return 'CLOB'; + case 'pgsql': + return 'TEXT'; + default: + return 'BLOB'; + } } /** @@ -738,7 +749,14 @@ class Database extends AbstractData */ private static function _getAttachmentType() { - return self::$_type === 'pgsql' ? 'TEXT' : (self::$_type === 'oci' ? 'CLOB' : 'MEDIUMBLOB'); + switch (self::$_type) { + case 'oci': + return 'CLOB'; + case 'pgsql': + return 'TEXT'; + default: + return 'MEDIUMBLOB'; + } } /** @@ -752,7 +770,12 @@ class Database extends AbstractData */ private static function _getMetaType() { - return self::$_type === 'oci' ? 'VARCHAR2(4000)' : 'TEXT'; + switch (self::$_type) { + case 'oci': + return 'VARCHAR2(4000)'; + default: + return 'TEXT'; + } } /** From 0b6af67b99151905541e5dad65246051192f3bc8 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 24 Jan 2022 17:50:24 +0100 Subject: [PATCH 18/27] removed obsolete comment --- lib/Data/Database.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 27e71c9a..080da533 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -589,7 +589,6 @@ class Database extends AbstractData } $statement->closeCursor(); if (self::$_type === 'oci' && is_array($result)) { - // returned column names are all upper case, convert these back // returned CLOB values are streams, convert these into strings $result = $firstOnly ? array_map('self::_sanitizeClob', $result) : From b8e8755fb1f70e5259323e2dcb16de49fefb1dba Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 24 Jan 2022 21:36:18 +0100 Subject: [PATCH 19/27] Basically it wants a non-empty catch statement Co-authored-by: Austin Huang --- lib/Data/Database.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 080da533..c9901dc4 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -833,7 +833,9 @@ class Database extends AbstractData begin execute immediate \'create index "comment_parent" on "' . self::_sanitizeIdentifier('comment') . '" ("pasteid")\'; exception - end' + when already_exists or columns_indexed then + NULL; + end;' ); } else { self::$_db->exec( From 0c4852c099cd1eb9015cdfac31cc7613b10c5b82 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 24 Jan 2022 21:40:10 +0100 Subject: [PATCH 20/27] this fixes the comment display issue Co-authored-by: Austin Huang --- lib/Data/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index c9901dc4..8dcbe0c5 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -582,7 +582,7 @@ class Database extends AbstractData // workaround for https://bugs.php.net/bug.php?id=46728 $result = array(); while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { - $result[] = $row; + $result[] = array_map('self::_sanitizeClob', $row); } } else { $result = $statement->fetchAll(PDO::FETCH_ASSOC); From 535f038daaa1b558e3ae8f3093481c499d190635 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 24 Jan 2022 21:43:31 +0100 Subject: [PATCH 21/27] handle `LIMIT` in oci Co-authored-by: Austin Huang --- lib/Data/Database.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 8dcbe0c5..aed8db69 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -913,7 +913,10 @@ class Database extends AbstractData case '0.21': // create the meta column if necessary (pre 0.21 change) try { - self::$_db->exec('SELECT "meta" FROM "' . self::_sanitizeIdentifier('paste') . '" LIMIT 1'); + self::$_db->exec( + 'SELECT "meta" FROM "' . self::_sanitizeIdentifier('paste') . '" ' . + (self::$_type === 'oci' ? 'FETCH NEXT 1 ROWS ONLY' : 'LIMIT 1') + ); } catch (PDOException $e) { self::$_db->exec('ALTER TABLE "' . self::_sanitizeIdentifier('paste') . '" ADD COLUMN "meta" TEXT'); } From 55db9426b99bd7514fa2bb12b29c15a9c379d7bf Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 24 Jan 2022 21:43:48 +0100 Subject: [PATCH 22/27] Throws `ORA-00942: table or view does not exist` otherwise Co-authored-by: Austin Huang --- lib/Data/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index aed8db69..657107bf 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -643,7 +643,7 @@ class Database extends AbstractData $sql = 'SHOW TABLES'; break; case 'oci': - $sql = 'SELECT "table_name" FROM "all_tables"'; + $sql = 'SELECT table_name FROM all_tables'; break; case 'pgsql': $sql = 'SELECT c."relname" AS "table_name" ' From f4438a01036bb53e42b879a6da6e6f63052453d6 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Mon, 24 Jan 2022 21:44:20 +0100 Subject: [PATCH 23/27] inserting CLOB absolutely requires a length argument Co-authored-by: Austin Huang --- lib/Data/Database.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 657107bf..0e605987 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -548,12 +548,13 @@ class Database extends AbstractData private static function _exec($sql, array $params) { $statement = self::$_db->prepare($sql); - foreach ($params as $key => $parameter) { - $position = $key + 1; + foreach ($params as $key => &$parameter) { if (is_int($parameter)) { - $statement->bindValue($position, $parameter, PDO::PARAM_INT); + $statement->bindParam($key + 1, $parameter, PDO::PARAM_INT); + } elseif (strlen($parameter) >= 4000) { + $statement->bindParam($key + 1, $parameter, PDO::PARAM_STR, strlen($parameter)); } else { - $statement->bindValue($position, $parameter); + $statement->bindParam($key + 1, $parameter); } } $result = $statement->execute(); From 0333777a37c77cd9540962d231306b0a09a6df12 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 25 Jan 2022 05:59:22 +0100 Subject: [PATCH 24/27] remove duplicate CLOB sanitation --- lib/Data/Database.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 0e605987..3a9a8060 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -593,11 +593,7 @@ class Database extends AbstractData // returned CLOB values are streams, convert these into strings $result = $firstOnly ? array_map('self::_sanitizeClob', $result) : - array_map( - function ($row) { - return array_map('self::_sanitizeClob', $row); - }, $result - ); + $result; } return $result; } From 53c0e4976bf2321e4da0205e95a068c1652f98d5 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Wed, 26 Jan 2022 05:26:47 +0100 Subject: [PATCH 25/27] document what the U type stands for --- lib/Data/Database.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index 3a9a8060..babcd254 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -633,6 +633,7 @@ class Database extends AbstractData $sql = 'SELECT "tabname" FROM "systables"'; break; case 'mssql': + // U: tables created by the user $sql = 'SELECT "name" FROM "sysobjects" ' . 'WHERE "type" = \'U\' ORDER BY "name"'; break; From 1d20eee1693a67a6ce71d50458122e0f6ebbe915 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Wed, 26 Jan 2022 05:28:29 +0100 Subject: [PATCH 26/27] readability --- lib/Data/Database.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index babcd254..a35726cb 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -549,12 +549,13 @@ class Database extends AbstractData { $statement = self::$_db->prepare($sql); foreach ($params as $key => &$parameter) { + $position = $key + 1; if (is_int($parameter)) { - $statement->bindParam($key + 1, $parameter, PDO::PARAM_INT); + $statement->bindParam($position, $parameter, PDO::PARAM_INT); } elseif (strlen($parameter) >= 4000) { - $statement->bindParam($key + 1, $parameter, PDO::PARAM_STR, strlen($parameter)); + $statement->bindParam($position, $parameter, PDO::PARAM_STR, strlen($parameter)); } else { - $statement->bindParam($key + 1, $parameter); + $statement->bindParam($position, $parameter); } } $result = $statement->execute(); From 29ffd25c181352a327e0dd99d512f394b88dc3b9 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 30 Jan 2022 21:42:24 +0100 Subject: [PATCH 27/27] apply suggestion of @r4sas --- lib/Data/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Data/Database.php b/lib/Data/Database.php index a35726cb..03e60612 100644 --- a/lib/Data/Database.php +++ b/lib/Data/Database.php @@ -647,7 +647,7 @@ class Database extends AbstractData case 'pgsql': $sql = 'SELECT c."relname" AS "table_name" ' . 'FROM "pg_class" c, "pg_user" u ' - . "WHERE c.\"relowner\" = u.\"usesysid\" AND c.\"relkind\" = 'r' " + . 'WHERE c."relowner" = u."usesysid" AND c."relkind" = \'r\' ' . 'AND NOT EXISTS (SELECT 1 FROM "pg_views" WHERE "viewname" = c."relname") ' . "AND c.\"relname\" !~ '^(pg_|sql_)' " . 'UNION '