db.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. <?php
  2. namespace Elementor;
  3. use Elementor\Core\DynamicTags\Manager;
  4. if ( ! defined( 'ABSPATH' ) ) {
  5. exit; // Exit if accessed directly.
  6. }
  7. /**
  8. * Elementor database.
  9. *
  10. * Elementor database handler class is responsible for communicating with the
  11. * DB, save and retrieve Elementor data and meta data.
  12. *
  13. * @since 1.0.0
  14. */
  15. class DB {
  16. /**
  17. * Current DB version of the editor.
  18. */
  19. const DB_VERSION = '0.4';
  20. /**
  21. * Post publish status.
  22. */
  23. const STATUS_PUBLISH = 'publish';
  24. /**
  25. * Post draft status.
  26. */
  27. const STATUS_DRAFT = 'draft';
  28. /**
  29. * Post private status.
  30. */
  31. const STATUS_PRIVATE = 'private';
  32. /**
  33. * Post autosave status.
  34. */
  35. const STATUS_AUTOSAVE = 'autosave';
  36. /**
  37. * Post pending status.
  38. */
  39. const STATUS_PENDING = 'pending';
  40. /**
  41. * Switched post data.
  42. *
  43. * Holds the switched post data.
  44. *
  45. * @since 1.5.0
  46. * @access protected
  47. *
  48. * @var array Switched post data. Default is an empty array.
  49. */
  50. protected $switched_post_data = [];
  51. /**
  52. * Switched data.
  53. *
  54. * Holds the switched data.
  55. *
  56. * @since 2.0.0
  57. * @access protected
  58. *
  59. * @var array Switched data. Default is an empty array.
  60. */
  61. protected $switched_data = [];
  62. /**
  63. * Save editor.
  64. *
  65. * Save data from the editor to the database.
  66. *
  67. * @since 1.0.0
  68. * @deprecated 2.0.0 Use `Plugin::$instance->documents->save()` method instead.
  69. *
  70. * @access public
  71. *
  72. * @param int $post_id Post ID.
  73. * @param array $data Post data.
  74. * @param string $status Optional. Post status. Default is `publish`.
  75. *
  76. * @return bool
  77. */
  78. public function save_editor( $post_id, $data, $status = self::STATUS_PUBLISH ) {
  79. // TODO: _deprecated_function( __METHOD__, '2.0.0', 'Plugin::$instance->documents->save()' );
  80. $document = Plugin::$instance->documents->get( $post_id );
  81. if ( self::STATUS_AUTOSAVE === $status ) {
  82. $document = $document->get_autosave( 0, true );
  83. }
  84. return $document->save( [
  85. 'elements' => $data,
  86. 'settings' => [
  87. 'post_status' => $status,
  88. ],
  89. ] );
  90. }
  91. /**
  92. * Get builder.
  93. *
  94. * Retrieve editor data from the database.
  95. *
  96. * @since 1.0.0
  97. *
  98. * @access public
  99. *
  100. * @param int $post_id Post ID.
  101. * @param string $status Optional. Post status. Default is `publish`.
  102. *
  103. * @return array Editor data.
  104. */
  105. public function get_builder( $post_id, $status = self::STATUS_PUBLISH ) {
  106. if ( self::STATUS_DRAFT === $status ) {
  107. $document = Plugin::$instance->documents->get_doc_or_auto_save( $post_id );
  108. } else {
  109. $document = Plugin::$instance->documents->get( $post_id );
  110. }
  111. if ( $document ) {
  112. $editor_data = $document->get_elements_raw_data( null, true );
  113. } else {
  114. $editor_data = [];
  115. }
  116. return $editor_data;
  117. }
  118. /**
  119. * Get JSON meta.
  120. *
  121. * Retrieve post meta data, and return the JSON decoded data.
  122. *
  123. * @since 1.0.0
  124. * @access protected
  125. *
  126. * @param int $post_id Post ID.
  127. * @param string $key The meta key to retrieve.
  128. *
  129. * @return array Decoded JSON data from post meta.
  130. */
  131. protected function _get_json_meta( $post_id, $key ) {
  132. $meta = get_post_meta( $post_id, $key, true );
  133. if ( is_string( $meta ) && ! empty( $meta ) ) {
  134. $meta = json_decode( $meta, true );
  135. }
  136. if ( empty( $meta ) ) {
  137. $meta = [];
  138. }
  139. return $meta;
  140. }
  141. /**
  142. * Get plain editor.
  143. *
  144. * Retrieve post data that was saved in the database. Raw data before it
  145. * was parsed by elementor.
  146. *
  147. * @since 1.0.0
  148. * @deprecated 2.0.0 Use `Plugin::$instance->documents->get_elements_data()` method instead.
  149. *
  150. * @access public
  151. *
  152. * @param int $post_id Post ID.
  153. * @param string $status Optional. Post status. Default is `publish`.
  154. *
  155. * @return array Post data.
  156. */
  157. public function get_plain_editor( $post_id, $status = self::STATUS_PUBLISH ) {
  158. // TODO: _deprecated_function( __METHOD__, '2.0.0', 'Plugin::$instance->documents->get_elements_data()' );
  159. $document = Plugin::$instance->documents->get( $post_id );
  160. if ( $document ) {
  161. return $document->get_elements_data( $status );
  162. }
  163. return [];
  164. }
  165. /**
  166. * Get auto-saved post revision.
  167. *
  168. * Retrieve the auto-saved post revision that is newer than current post.
  169. *
  170. * @since 1.9.0
  171. * @deprecated 2.0.0 Use `Plugin::$instance->documents->get_newer_autosave()` method instead.
  172. *
  173. * @access public
  174. *
  175. * @param int $post_id Post ID.
  176. *
  177. * @return \WP_Post|false The auto-saved post, or false.
  178. */
  179. public function get_newer_autosave( $post_id ) {
  180. // TODO: _deprecated_function( __METHOD__, '2.0.0', 'Plugin::$instance->documents->get_newer_autosave()' );
  181. $document = Plugin::$instance->documents->get( $post_id );
  182. return $document->get_newer_autosave();
  183. }
  184. /**
  185. * Get new editor from WordPress editor.
  186. *
  187. * When editing the with Elementor the first time, the current page content
  188. * is parsed into Text Editor Widget that contains the original data.
  189. *
  190. * @since 2.1.0
  191. * @access public
  192. *
  193. * @param int $post_id Post ID.
  194. *
  195. * @return array Content in Elementor format.
  196. */
  197. public function get_new_editor_from_wp_editor( $post_id ) {
  198. $post = get_post( $post_id );
  199. if ( empty( $post ) || empty( $post->post_content ) ) {
  200. return [];
  201. }
  202. // Check if it's only a shortcode.
  203. preg_match_all( '/' . get_shortcode_regex() . '/', $post->post_content, $matches, PREG_SET_ORDER );
  204. if ( ! empty( $matches ) ) {
  205. foreach ( $matches as $shortcode ) {
  206. if ( trim( $post->post_content ) === $shortcode[0] ) {
  207. $widget_type = Plugin::$instance->widgets_manager->get_widget_types( 'shortcode' );
  208. $settings = [
  209. 'shortcode' => $post->post_content,
  210. ];
  211. break;
  212. }
  213. }
  214. }
  215. if ( empty( $widget_type ) ) {
  216. $widget_type = Plugin::$instance->widgets_manager->get_widget_types( 'text-editor' );
  217. $settings = [
  218. 'editor' => $post->post_content,
  219. ];
  220. }
  221. // TODO: Better coding to start template for editor
  222. return [
  223. [
  224. 'id' => Utils::generate_random_string(),
  225. 'elType' => 'section',
  226. 'elements' => [
  227. [
  228. 'id' => Utils::generate_random_string(),
  229. 'elType' => 'column',
  230. 'elements' => [
  231. [
  232. 'id' => Utils::generate_random_string(),
  233. 'elType' => $widget_type::get_type(),
  234. 'widgetType' => $widget_type->get_name(),
  235. 'settings' => $settings,
  236. ],
  237. ],
  238. ],
  239. ],
  240. ],
  241. ];
  242. }
  243. /**
  244. * Get new editor from WordPress editor.
  245. *
  246. * When editing the with Elementor the first time, the current page content
  247. * is parsed into Text Editor Widget that contains the original data.
  248. *
  249. * @since 1.0.0
  250. * @deprecated 2.1.0 Use `DB::get_new_editor_from_wp_editor()` instead
  251. * @access public
  252. *
  253. * @param int $post_id Post ID.
  254. *
  255. * @return array Content in Elementor format.
  256. */
  257. public function _get_new_editor_from_wp_editor( $post_id ) {
  258. // TODO: _deprecated_function( __METHOD__, '2.1.0', __CLASS__ . '::get_new_editor_from_wp_editor()' );
  259. return $this->get_new_editor_from_wp_editor( $post_id );
  260. }
  261. /**
  262. * Is using Elementor.
  263. *
  264. * Set whether the page is using Elementor or not.
  265. *
  266. * @since 1.5.0
  267. * @access public
  268. *
  269. * @param int $post_id Post ID.
  270. * @param bool $is_elementor Optional. Whether the page is elementor page.
  271. * Default is true.
  272. */
  273. public function set_is_elementor_page( $post_id, $is_elementor = true ) {
  274. if ( $is_elementor ) {
  275. // Use the string `builder` and not a boolean for rollback compatibility
  276. update_post_meta( $post_id, '_elementor_edit_mode', 'builder' );
  277. } else {
  278. delete_post_meta( $post_id, '_elementor_edit_mode' );
  279. }
  280. }
  281. /**
  282. * Render element plain content.
  283. *
  284. * When saving data in the editor, this method renders recursively the plain
  285. * content containing only the content and the HTML. No CSS data.
  286. *
  287. * @since 2.0.0
  288. * @access private
  289. *
  290. * @param array $element_data Element data.
  291. */
  292. private function render_element_plain_content( $element_data ) {
  293. if ( 'widget' === $element_data['elType'] ) {
  294. /** @var Widget_Base $widget */
  295. $widget = Plugin::$instance->elements_manager->create_element_instance( $element_data );
  296. if ( $widget ) {
  297. $widget->render_plain_content();
  298. }
  299. }
  300. if ( ! empty( $element_data['elements'] ) ) {
  301. foreach ( $element_data['elements'] as $element ) {
  302. $this->render_element_plain_content( $element );
  303. }
  304. }
  305. }
  306. /**
  307. * Save plain text.
  308. *
  309. * Retrieves the raw content, removes all kind of unwanted HTML tags and saves
  310. * the content as the `post_content` field in the database.
  311. *
  312. * @since 1.9.0
  313. * @access public
  314. *
  315. * @param int $post_id Post ID.
  316. */
  317. public function save_plain_text( $post_id ) {
  318. // Switch $dynamic_tags to parsing mode = remove.
  319. $dynamic_tags = Plugin::$instance->dynamic_tags;
  320. $parsing_mode = $dynamic_tags->get_parsing_mode();
  321. $dynamic_tags->set_parsing_mode( Manager::MODE_REMOVE );
  322. $plain_text = $this->get_plain_text( $post_id );
  323. wp_update_post(
  324. [
  325. 'ID' => $post_id,
  326. 'post_content' => $plain_text,
  327. ]
  328. );
  329. // Restore parsing mode.
  330. $dynamic_tags->set_parsing_mode( $parsing_mode );
  331. }
  332. /**
  333. * Iterate data.
  334. *
  335. * Accept any type of Elementor data and a callback function. The callback
  336. * function runs recursively for each element and his child elements.
  337. *
  338. * @since 1.0.0
  339. * @access public
  340. *
  341. * @param array $data_container Any type of elementor data.
  342. * @param callable $callback A function to iterate data by.
  343. *
  344. * @return mixed Iterated data.
  345. */
  346. public function iterate_data( $data_container, $callback ) {
  347. if ( isset( $data_container['elType'] ) ) {
  348. if ( ! empty( $data_container['elements'] ) ) {
  349. $data_container['elements'] = $this->iterate_data( $data_container['elements'], $callback );
  350. }
  351. return $callback( $data_container );
  352. }
  353. foreach ( $data_container as $element_key => $element_value ) {
  354. $element_data = $this->iterate_data( $data_container[ $element_key ], $callback );
  355. if ( null === $element_data ) {
  356. continue;
  357. }
  358. $data_container[ $element_key ] = $element_data;
  359. }
  360. return $data_container;
  361. }
  362. /**
  363. * Safely copy Elementor meta.
  364. *
  365. * Make sure the original page was built with Elementor and the post is not
  366. * auto-save. Only then copy elementor meta from one post to another using
  367. * `copy_elementor_meta()`.
  368. *
  369. * @since 1.9.2
  370. * @access public
  371. *
  372. * @param int $from_post_id Original post ID.
  373. * @param int $to_post_id Target post ID.
  374. */
  375. public function safe_copy_elementor_meta( $from_post_id, $to_post_id ) {
  376. // It's from WP-Admin & not from Elementor.
  377. if ( ! did_action( 'elementor/db/before_save' ) ) {
  378. if ( ! Plugin::$instance->db->is_built_with_elementor( $from_post_id ) ) {
  379. return;
  380. }
  381. // It's an exited Elementor auto-save
  382. if ( get_post_meta( $to_post_id, '_elementor_data', true ) ) {
  383. return;
  384. }
  385. }
  386. $this->copy_elementor_meta( $from_post_id, $to_post_id );
  387. }
  388. /**
  389. * Copy Elementor meta.
  390. *
  391. * Duplicate the data from one post to another.
  392. *
  393. * Consider using `safe_copy_elementor_meta()` method instead.
  394. *
  395. * @since 1.1.0
  396. * @access public
  397. *
  398. * @param int $from_post_id Original post ID.
  399. * @param int $to_post_id Target post ID.
  400. */
  401. public function copy_elementor_meta( $from_post_id, $to_post_id ) {
  402. $from_post_meta = get_post_meta( $from_post_id );
  403. $core_meta = [
  404. '_wp_page_template',
  405. '_thumbnail_id',
  406. ];
  407. foreach ( $from_post_meta as $meta_key => $values ) {
  408. // Copy only meta with the `_elementor` prefix
  409. if ( 0 === strpos( $meta_key, '_elementor' ) || in_array( $meta_key, $core_meta, true ) ) {
  410. $value = $values[0];
  411. // The elementor JSON needs slashes before saving
  412. if ( '_elementor_data' === $meta_key ) {
  413. $value = wp_slash( $value );
  414. } else {
  415. $value = maybe_unserialize( $value );
  416. }
  417. // Don't use `update_post_meta` that can't handle `revision` post type
  418. update_metadata( 'post', $to_post_id, $meta_key, $value );
  419. }
  420. }
  421. }
  422. /**
  423. * Is built with Elementor.
  424. *
  425. * Check whether the post was built with Elementor.
  426. *
  427. * @since 1.0.10
  428. * @access public
  429. *
  430. * @param int $post_id Post ID.
  431. *
  432. * @return bool Whether the post was built with Elementor.
  433. */
  434. public function is_built_with_elementor( $post_id ) {
  435. return ! ! get_post_meta( $post_id, '_elementor_edit_mode', true );
  436. }
  437. /**
  438. * Switch to post.
  439. *
  440. * Change the global WordPress post to the requested post.
  441. *
  442. * @since 1.5.0
  443. * @access public
  444. *
  445. * @param int $post_id Post ID to switch to.
  446. */
  447. public function switch_to_post( $post_id ) {
  448. $post_id = absint( $post_id );
  449. // If is already switched, or is the same post, return.
  450. if ( get_the_ID() === $post_id ) {
  451. $this->switched_post_data[] = false;
  452. return;
  453. }
  454. $this->switched_post_data[] = [
  455. 'switched_id' => $post_id,
  456. 'original_id' => get_the_ID(), // Note, it can be false if the global isn't set
  457. ];
  458. $GLOBALS['post'] = get_post( $post_id ); // WPCS: override ok.
  459. setup_postdata( $GLOBALS['post'] );
  460. }
  461. /**
  462. * Restore current post.
  463. *
  464. * Rollback to the previous global post, rolling back from `DB::switch_to_post()`.
  465. *
  466. * @since 1.5.0
  467. * @access public
  468. */
  469. public function restore_current_post() {
  470. $data = array_pop( $this->switched_post_data );
  471. // If not switched, return.
  472. if ( ! $data ) {
  473. return;
  474. }
  475. // It was switched from an empty global post, restore this state and unset the global post
  476. if ( false === $data['original_id'] ) {
  477. unset( $GLOBALS['post'] );
  478. return;
  479. }
  480. $GLOBALS['post'] = get_post( $data['original_id'] ); // WPCS: override ok.
  481. setup_postdata( $GLOBALS['post'] );
  482. }
  483. /**
  484. * Switch to query.
  485. *
  486. * Change the WordPress query to a new query with the requested
  487. * query variables.
  488. *
  489. * @since 2.0.0
  490. * @access public
  491. *
  492. * @param array $query_vars New query variables.
  493. */
  494. public function switch_to_query( $query_vars ) {
  495. global $wp_query;
  496. $current_query_vars = $wp_query->query;
  497. // If is already switched, or is the same query, return.
  498. if ( $current_query_vars === $query_vars ) {
  499. $this->switched_data[] = false;
  500. return;
  501. }
  502. $new_query = new \WP_Query( $query_vars );
  503. $this->switched_data[] = [
  504. 'switched' => $new_query,
  505. 'original' => $wp_query,
  506. ];
  507. $wp_query = $new_query; // WPCS: override ok.
  508. // Ensure the global post is set only if needed
  509. unset( $GLOBALS['post'] );
  510. if ( $new_query->is_singular() && isset( $new_query->posts[0] ) ) {
  511. $GLOBALS['post'] = $new_query->posts[0]; // WPCS: override ok.
  512. setup_postdata( $GLOBALS['post'] );
  513. } elseif ( $new_query->is_author() ) {
  514. $GLOBALS['authordata'] = get_userdata( $new_query->get( 'author' ) ); // WPCS: override ok.
  515. }
  516. }
  517. /**
  518. * Restore current query.
  519. *
  520. * Rollback to the previous query, rolling back from `DB::switch_to_query()`.
  521. *
  522. * @since 2.0.0
  523. * @access public
  524. */
  525. public function restore_current_query() {
  526. $data = array_pop( $this->switched_data );
  527. // If not switched, return.
  528. if ( ! $data ) {
  529. return;
  530. }
  531. global $wp_query;
  532. $wp_query = $data['original']; // WPCS: override ok.
  533. // Ensure the global post/authordata is set only if needed.
  534. unset( $GLOBALS['post'] );
  535. unset( $GLOBALS['authordata'] );
  536. if ( $wp_query->is_singular() && isset( $wp_query->posts[0] ) ) {
  537. $GLOBALS['post'] = $wp_query->posts[0]; // WPCS: override ok.
  538. setup_postdata( $GLOBALS['post'] );
  539. } elseif ( $wp_query->is_author() ) {
  540. $GLOBALS['authordata'] = get_userdata( $wp_query->get( 'author' ) ); // WPCS: override ok.
  541. }
  542. }
  543. /**
  544. * Get plain text.
  545. *
  546. * Retrieve the post plain text.
  547. *
  548. * @since 1.9.0
  549. * @access public
  550. *
  551. * @param int $post_id Post ID.
  552. *
  553. * @return string Post plain text.
  554. */
  555. public function get_plain_text( $post_id ) {
  556. $data = $this->get_plain_editor( $post_id );
  557. return $this->get_plain_text_from_data( $data );
  558. }
  559. /**
  560. * Get plain text from data.
  561. *
  562. * Retrieve the post plain text from any given Elementor data.
  563. *
  564. * @since 1.9.2
  565. * @access public
  566. *
  567. * @param array $data Post ID.
  568. *
  569. * @return string Post plain text.
  570. */
  571. public function get_plain_text_from_data( $data ) {
  572. ob_start();
  573. if ( $data ) {
  574. foreach ( $data as $element_data ) {
  575. $this->render_element_plain_content( $element_data );
  576. }
  577. }
  578. $plain_text = ob_get_clean();
  579. // Remove unnecessary tags.
  580. $plain_text = preg_replace( '/<\/?div[^>]*\>/i', '', $plain_text );
  581. $plain_text = preg_replace( '/<\/?span[^>]*\>/i', '', $plain_text );
  582. $plain_text = preg_replace( '#<script(.*?)>(.*?)</script>#is', '', $plain_text );
  583. $plain_text = preg_replace( '/<i [^>]*><\\/i[^>]*>/', '', $plain_text );
  584. $plain_text = preg_replace( '/ class=".*?"/', '', $plain_text );
  585. // Remove empty lines.
  586. $plain_text = preg_replace( '/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/', "\n", $plain_text );
  587. $plain_text = trim( $plain_text );
  588. return $plain_text;
  589. }
  590. }