PageRenderTime 49ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/all-in-one-event-calendar/app/model/class-ai1ec-database.php

https://gitlab.com/Blueprint-Marketing/interoccupy.net
PHP | 521 lines | 374 code | 21 blank | 126 comment | 32 complexity | 1f1ed8084591d2465221cc97b504cd1b MD5 | raw file
  1. <?php
  2. //
  3. // class-ai1ec-database.php
  4. // all-in-one-event-calendar
  5. //
  6. /**
  7. * Ai1ec_Database class
  8. *
  9. * Class responsible for generic database operations
  10. *
  11. * @package Models
  12. * @author Timely Network Inc
  13. **/
  14. class Ai1ec_Database
  15. {
  16. /**
  17. * @staticvar Ai1ec_Database Singletonian instance of self
  18. */
  19. static protected $_instance = NULL;
  20. /**
  21. * @var array Map of tables and their parsed definitions
  22. */
  23. protected $_schema_delta = array();
  24. /**
  25. * instance method
  26. *
  27. * Get singleton instance of self (Ai1ec_Database).
  28. *
  29. * @return Ai1ec_Database Initialized instance of self
  30. */
  31. static public function instance() {
  32. if ( ! ( self::$_instance instanceof Ai1ec_Database ) ) {
  33. self::$_instance = new Ai1ec_Database();
  34. }
  35. return self::$_instance;
  36. }
  37. /**
  38. * apply_delta method
  39. *
  40. * Attempt to parse and apply given database tables definition, as a delta.
  41. * Some validation is made prior to calling DB, and fields/indexes are also
  42. * checked for consistency after sending queries to DB.
  43. *
  44. * NOTICE: only "CREATE TABLE" statements are handled. Others will, likely,
  45. * be ignored, if passed through this method.
  46. *
  47. * @param string|array $query Single or multiple queries to perform on DB
  48. *
  49. * @return bool Success
  50. *
  51. * @throws Ai1ec_Database_Error In case of any error
  52. */
  53. public function apply_delta( $query ) {
  54. if ( ! function_exists( 'dbDelta' ) ) {
  55. require_once ABSPATH . 'wp-admin' . DIRECTORY_SEPARATOR .
  56. 'includes' . DIRECTORY_SEPARATOR . 'upgrade.php';
  57. }
  58. $this->_schema_delta = array();
  59. $queries = $this->_prepare_delta( $query );
  60. $result = dbDelta( $queries );
  61. return $this->_check_delta();
  62. }
  63. /**
  64. * _prepare_delta method
  65. *
  66. * Prepare statements for execution.
  67. * Attempt to parse various SQL definitions and compose the one, that is
  68. * most likely to be accepted by delta engine.
  69. *
  70. * @param string|array $queries Single or multiple queries to perform on DB
  71. *
  72. * @return bool Success
  73. *
  74. * @throws Ai1ec_Database_Error In case of any error
  75. */
  76. protected function _prepare_delta( $queries ) {
  77. if ( ! is_array( $queries ) ) {
  78. $queries = explode( ';', $queries );
  79. $queries = array_filter( $queries );
  80. }
  81. $current_table = NULL;
  82. $ctable_regexp = '#
  83. \s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([^ ]+)`?\s*
  84. \((.+)\)
  85. ([^()]*)
  86. #six';
  87. foreach ( $queries as $query ) {
  88. if ( preg_match( $ctable_regexp, $query, $matches ) ) {
  89. $this->_schema_delta[$matches[1]] = array(
  90. 'tblname' => $matches[1],
  91. 'cryptic' => NULL,
  92. 'creator' => '',
  93. 'columns' => array(),
  94. 'indexes' => array(),
  95. 'content' => preg_replace( '#`#', '', $matches[2] ),
  96. 'clauses' => $matches[3],
  97. );
  98. }
  99. }
  100. $this->_parse_delta();
  101. $sane_queries = array();
  102. foreach ( $this->_schema_delta as $table => $definition ) {
  103. $create = 'CREATE TABLE ' . $table . " (\n";
  104. foreach ( $definition['columns'] as $column ) {
  105. $create .= ' ' . $column['create'] . ",\n";
  106. }
  107. foreach ( $definition['indexes'] as $index ) {
  108. $create .= ' ' . $index['create'] . ",\n";
  109. }
  110. $create = substr( $create, 0, -2 ) . "\n";
  111. $create .= ')' . $definition['clauses'];
  112. $this->_schema_delta[$table]['creator'] = $create;
  113. $this->_schema_delta[$table]['cryptic'] = md5( $create );
  114. $sane_queries[] = $create;
  115. }
  116. return $sane_queries;
  117. }
  118. /**
  119. * _parse_delta method
  120. *
  121. * Parse table application (creation) statements into atomical particles.
  122. * Here "atomical particles" stands for either columns, or indexes.
  123. *
  124. * @return void Method does not return
  125. *
  126. * @throws Ai1ec_Database_Error In case of any error
  127. */
  128. protected function _parse_delta() {
  129. foreach ( $this->_schema_delta as $table => $definitions ) {
  130. $listing = explode( "\n", $definitions['content'] );
  131. $listing = array_filter( $listing, array( $this, '_is_not_empty_line' ) );
  132. $lines = count( $listing );
  133. $lineno = 0;
  134. foreach ( $listing as $line ) {
  135. ++$lineno;
  136. $line = trim( preg_replace( '#\s+#', ' ', $line ) );
  137. $line_new = rtrim( $line, ',' );
  138. if (
  139. $lineno < $lines && $line === $line_new ||
  140. $lineno == $lines && $line !== $line_new
  141. ) {
  142. throw new Ai1ec_Database_Error(
  143. 'Missing comma in line \'' . $line . '\''
  144. );
  145. }
  146. $line = $line_new;
  147. unset( $line_new );
  148. $type = 'indexes';
  149. if ( false === ( $record = $this->_parse_index( $line ) ) ) {
  150. $type = 'columns';
  151. $record = $this->_parse_column( $line );
  152. }
  153. if ( isset(
  154. $this->_schema_delta[$table][$type][$record['name']]
  155. ) ) {
  156. throw new Ai1ec_Database_Error(
  157. 'For table `' . $table . '` entry ' . $type .
  158. ' named `' . $record['name'] . '` was declared twice' .
  159. ' in ' . $definitions
  160. );
  161. }
  162. $this->_schema_delta[$table][$type][$record['name']] = $record;
  163. }
  164. }
  165. }
  166. /**
  167. * _parse_index method
  168. *
  169. * Given string attempts to detect, if it is an index, and if yes - parse
  170. * it to more navigable index definition for future validations.
  171. * Creates modified index create line, for delta application.
  172. *
  173. * @param string $description Single "line" of CREATE TABLE statement body
  174. *
  175. * @return array|bool Index definition, or false if input does not look like index
  176. *
  177. * @throws Ai1ec_Database_Error In case of any error
  178. */
  179. protected function _parse_index( $description ) {
  180. $description = preg_replace(
  181. '#^CONSTRAINT(\s+`?[^ ]+`?)?\s+#six',
  182. '',
  183. $description
  184. );
  185. $details = explode( ' ', $description );
  186. $index = array(
  187. 'name' => NULL,
  188. 'content' => array(),
  189. 'create' => '',
  190. );
  191. $details[0] = strtoupper( $details[0] );
  192. switch ( $details[0] ) {
  193. case 'PRIMARY':
  194. $index['name'] = 'PRIMARY';
  195. $index['create'] = 'PRIMARY KEY ';
  196. break;
  197. case 'UNIQUE':
  198. $name = $details[1];
  199. if (
  200. 0 === strcasecmp( 'KEY', $name ) ||
  201. 0 === strcasecmp( 'INDEX', $name )
  202. ) {
  203. $name = $details[2];
  204. }
  205. $index['name'] = $name;
  206. $index['create'] = 'UNIQUE KEY ' . $name;
  207. break;
  208. case 'KEY':
  209. case 'INDEX':
  210. $index['name'] = $details[1];
  211. $index['create'] = 'KEY ' . $index['name'];
  212. break;
  213. default:
  214. return false;
  215. }
  216. $index['content'] = $this->_parse_index_content( $description );
  217. $index['create'] .= ' (';
  218. foreach ( $index['content'] as $column => $length ) {
  219. $index['create'] .= $column;
  220. if ( NULL !== $length ) {
  221. $index['create'] .= '(' . $length . ')';
  222. }
  223. $index['create'] .= ',';
  224. }
  225. $index['create'] = substr( $index['create'], 0, -1 );
  226. $index['create'] .= ')';
  227. return $index;
  228. }
  229. /**
  230. * _parse_column method
  231. *
  232. * Parse column to parseable definition.
  233. * Some valid definitions may still be not recognizes (namely SET and ENUM)
  234. * thus one shall beware, when attempting to create such.
  235. * Create alternative create table entry line for delta application.
  236. *
  237. * @param string $description Single "line" of CREATE TABLE statement body
  238. *
  239. * @return array Column definition
  240. *
  241. * @throws Ai1ec_Database_Error In case of any error
  242. */
  243. protected function _parse_column( $description ) {
  244. $column_regexp = '#^
  245. ([a-z][a-z_]+)\s+
  246. (
  247. [A-Z]+
  248. (?:\s*\(\s*\d+(?:\s*,\s*\d+\s*)?\s*\))?
  249. (?:\s+UNSIGNED)?
  250. (?:\s+ZEROFILL)?
  251. (?:\s+BINARY)?
  252. (?:
  253. \s+CHARACTER\s+SET\s+[a-z][a-z_]+
  254. (?:\s+COLLATE\s+[a-z][a-z0-9_]+)?
  255. )?
  256. )
  257. (
  258. \s+(?:NOT\s+)?NULL
  259. )?
  260. (
  261. \s+DEFAULT\s+[^\s]+
  262. )?
  263. (\s+ON\s+UPDATE\s+CURRENT_(?:TIMESTAMP|DATE))?
  264. (\s+AUTO_INCREMENT)?
  265. \s*,?\s*
  266. $#six';
  267. if ( ! preg_match( $column_regexp, $description, $matches ) ) {
  268. throw new Ai1ec_Database_Error(
  269. 'Invalid column description ' . $description
  270. );
  271. }
  272. $column = array(
  273. 'name' => $matches[1],
  274. 'content' => array(),
  275. 'create' => '',
  276. );
  277. if ( 0 === strcasecmp( 'boolean', $matches[2] ) ) {
  278. $matches[2] = 'tinyint(1)';
  279. }
  280. $column['content']['type'] = $matches[2];
  281. $column['content']['null'] = (
  282. ! isset( $matches[3] ) ||
  283. 0 !== strcasecmp( 'NOT NULL', trim( $matches[3] ) )
  284. );
  285. $column['create'] = $column['name'] . ' ' . $column['content']['type'];
  286. if ( isset( $matches[3] ) ) {
  287. $column['create'] .= ' ' .
  288. implode(
  289. ' ',
  290. array_map(
  291. 'trim',
  292. array_slice( $matches, 3 )
  293. )
  294. );
  295. }
  296. return $column;
  297. }
  298. /**
  299. * _parse_index_content method
  300. *
  301. * Parse index content, to a map of columns and their length.
  302. * All index (content) cases shall be covered, although it is only tested.
  303. *
  304. * @param string Single line of CREATE TABLE statement, containing index definition
  305. *
  306. * @return array Map of columns and their length, as per index definition
  307. *
  308. * @throws Ai1ec_Database_Error In case of any error
  309. */
  310. protected function _parse_index_content( $description ) {
  311. if ( ! preg_match( '#^[^(]+\((.+)\)$#', $description, $matches ) ) {
  312. throw new Ai1ec_Database_Error(
  313. 'Invalid index description ' . $description
  314. );
  315. }
  316. $columns = array();
  317. $textual = explode( ',', $matches[1] );
  318. $column_regexp = '#\s*([^(]+)(?:\s*\(\s*(\d+)\s*\))?\s*#sx';
  319. foreach ( $textual as $column ) {
  320. if (
  321. ! preg_match( $column_regexp, $column, $matches ) || (
  322. isset( $matches[2] ) &&
  323. (string)$matches[2] !== (string)intval( $matches[2] )
  324. )
  325. ) {
  326. throw new Ai1ec_Database_Error(
  327. 'Invalid index (columns) description ' . $description .
  328. ' as per \'' . $column . '\''
  329. );
  330. }
  331. $matches[1] = trim( $matches[1] );
  332. $columns[$matches[1]] = NULL;
  333. if ( isset( $matches[2] ) ) {
  334. $columns[$matches[1]] = (int)$matches[2];
  335. }
  336. }
  337. return $columns;
  338. }
  339. /**
  340. * _check_delta method
  341. *
  342. * Given parsed schema definitions (in {@see self::$_schema_delta} map) this
  343. * method performs checks, to ensure that table exists, columns are of
  344. * expected type, and indexes match their definition in original query.
  345. *
  346. * @return bool Success
  347. *
  348. * @throws Ai1ec_Database_Error In case of any error
  349. */
  350. protected function _check_delta() {
  351. global $wpdb;
  352. if ( empty( $this->_schema_delta ) ) {
  353. return true;
  354. }
  355. foreach ( $this->_schema_delta as $table => $description ) {
  356. $columns = $wpdb->get_results( 'SHOW FULL COLUMNS FROM ' . $table );
  357. if ( empty( $columns ) ) {
  358. throw new Ai1ec_Database_Error(
  359. 'Required table `' . $table . '` was not created'
  360. );
  361. }
  362. $db_column_names = array();
  363. foreach ( $columns as $column ) {
  364. if ( ! isset( $description['columns'][$column->Field] ) ) {
  365. throw new Ai1ec_Database_Error(
  366. 'Unknown column `' . $column->Field .
  367. '` is present in table `' . $table . '`'
  368. );
  369. }
  370. $db_column_names[$column->Field] = $column->Field;
  371. $type_db = $column->Type;
  372. $collation = '';
  373. if ( $column->Collation ) {
  374. $collation = ' CHARACTER SET ' .
  375. substr(
  376. $column->Collation,
  377. 0,
  378. strpos( $column->Collation, '_' )
  379. ) . ' COLLATE ' . $column->Collation;
  380. }
  381. $type_req = $description['columns'][$column->Field]
  382. ['content']['type'];
  383. if (
  384. false !== stripos(
  385. $type_req,
  386. ' COLLATE '
  387. )
  388. ) {
  389. // suspend collation checking
  390. //$type_db .= $collation;
  391. $type_req = preg_replace(
  392. '#^
  393. (.+)
  394. \s+CHARACTER\s+SET\s+[a-z0-9_]+
  395. \s+COLLATE\s+[a-z0-9_]+
  396. (.+)?\s*
  397. $#six',
  398. '$1$2',
  399. $type_req
  400. );
  401. }
  402. $type_db = strtolower(
  403. preg_replace( '#\s+#', '', $type_db )
  404. );
  405. $type_req = strtolower(
  406. preg_replace( '#\s+#', '', $type_req )
  407. );
  408. if ( 0 !== strcmp( $type_db, $type_req ) ) {
  409. throw new Ai1ec_Database_Error(
  410. 'Field `' . $table . '`.`' . $column->Field .
  411. '` is of incompatible type'
  412. );
  413. }
  414. if (
  415. 'YES' === $column->Null &&
  416. false === $description['columns'][$column->Field]
  417. ['content']['null'] ||
  418. 'NO' === $column->Null &&
  419. true === $description['columns'][$column->Field]
  420. ['content']['null']
  421. ) {
  422. throw new Ai1ec_Database_Error(
  423. 'Field `' . $table . '`.`' . $column->Field .
  424. '` NULLability is flipped'
  425. );
  426. }
  427. }
  428. if (
  429. $missing = array_diff(
  430. array_keys( $description['columns'] ),
  431. $db_column_names
  432. )
  433. ) {
  434. throw new Ai1ec_Database_Error(
  435. 'In table `' . $table . '` fields are missing: ' .
  436. implode( ', ', $missing )
  437. );
  438. }
  439. $index_list = $wpdb->get_results( 'SHOW INDEXES FROM ' . $table );
  440. $indexes = array();
  441. foreach ( $index_list as $index_def ) {
  442. $name = $index_def->Key_name;
  443. if ( ! isset( $indexes[$name] ) ) {
  444. $indexes[$name] = array(
  445. 'columns' => array(),
  446. 'unique' => ( 0 !== $index_def->Non_unique ),
  447. );
  448. }
  449. $indexes[$name]['columns'][$index_def->Column_name] =
  450. $index_def->Sub_part;
  451. }
  452. foreach ( $indexes as $name => $definition ) {
  453. if ( ! isset( $description['indexes'][$name] ) ) {
  454. throw new Ai1ec_Database_Error(
  455. 'Unknown index `' . $name .
  456. '` is defined for table `' . $table . '`'
  457. );
  458. }
  459. if (
  460. $missed = array_diff_assoc(
  461. $description['indexes'][$name]['content'],
  462. $definition['columns']
  463. )
  464. ) {
  465. throw new Ai1ec_Database_Error(
  466. 'Index `' . $name .
  467. '` definition for table `' . $table . '` has invalid ' .
  468. ' fields: ' . implode( ', ', array_keys( $missed ) )
  469. );
  470. }
  471. }
  472. if (
  473. $missing = array_diff(
  474. array_keys( $description['indexes'] ),
  475. array_keys( $indexes )
  476. )
  477. ) {
  478. throw new Ai1ec_Database_Error(
  479. 'In table `' . $table . '` indexes are missing: ' .
  480. implode( ', ', $missing )
  481. );
  482. }
  483. }
  484. return true;
  485. }
  486. /**
  487. * _is_not_empty_line method
  488. *
  489. * Helper method, to check that any given line is not empty.
  490. * Aids array_filter in detecting empty SQL query lines.
  491. *
  492. * @param string $line Single line of DB query statement
  493. *
  494. * @return bool True if line is not empty, false otherwise
  495. */
  496. protected function _is_not_empty_line( $line ) {
  497. $line = trim( $line );
  498. return ! empty( $line );
  499. }
  500. }