module.php (15899B)
1 <?php 2 namespace Elementor\Modules\Usage; 3 4 use Elementor\Core\Base\Document; 5 use Elementor\Core\Base\Module as BaseModule; 6 use Elementor\Core\DynamicTags\Manager; 7 use Elementor\Modules\System_Info\Module as System_Info; 8 use Elementor\Plugin; 9 10 if ( ! defined( 'ABSPATH' ) ) { 11 exit; // Exit if accessed directly. 12 } 13 14 /** 15 * Elementor usage module. 16 * 17 * Elementor usage module handler class is responsible for registering and 18 * managing Elementor usage data. 19 * 20 */ 21 class Module extends BaseModule { 22 const GENERAL_TAB = 'general'; 23 const META_KEY = '_elementor_controls_usage'; 24 const OPTION_NAME = 'elementor_controls_usage'; 25 26 /** 27 * @var bool 28 */ 29 private $is_document_saving = false; 30 31 /** 32 * Get module name. 33 * 34 * Retrieve the usage module name. 35 * 36 * @access public 37 * 38 * @return string Module name. 39 */ 40 public function get_name() { 41 return 'usage'; 42 } 43 44 /** 45 * Get doc type count. 46 * 47 * Get count of documents based on doc type 48 * 49 * Remove 'wp-' from $doc_type for BC, support doc type change since 2.7.0. 50 * 51 * @param \Elementor\Core\Documents_Manager $doc_class 52 * @param String $doc_type 53 * 54 * @return int 55 */ 56 public function get_doc_type_count( $doc_class, $doc_type ) { 57 static $posts = null; 58 static $library = null; 59 60 if ( null === $posts ) { 61 $posts = \Elementor\Tracker::get_posts_usage(); 62 } 63 64 if ( null === $library ) { 65 $library = \Elementor\Tracker::get_library_usage(); 66 } 67 68 $posts_usage = $posts; 69 70 if ( $doc_class::get_property( 'show_in_library' ) ) { 71 $posts_usage = $library; 72 } 73 74 $doc_type_common = str_replace( 'wp-', '', $doc_type ); 75 76 $doc_usage = isset( $posts_usage[ $doc_type_common ] ) ? $posts_usage[ $doc_type_common ] : 0; 77 78 return is_array( $doc_usage ) ? $doc_usage['publish'] : $doc_usage; 79 } 80 81 /** 82 * Get formatted usage. 83 * 84 * Retrieve formatted usage, for frontend. 85 * 86 * @param String format 87 * 88 * @return array 89 */ 90 public function get_formatted_usage( $format = 'html' ) { 91 $usage = []; 92 93 foreach ( get_option( self::OPTION_NAME, [] ) as $doc_type => $elements ) { 94 $doc_class = Plugin::$instance->documents->get_document_type( $doc_type ); 95 96 if ( 'html' === $format && $doc_class ) { 97 $doc_title = $doc_class::get_title(); 98 } else { 99 $doc_title = $doc_type; 100 } 101 102 $doc_count = $this->get_doc_type_count( $doc_class, $doc_type ); 103 104 $tab_group = $doc_class::get_property( 'admin_tab_group' ); 105 106 if ( 'html' === $format && $tab_group ) { 107 $doc_title = ucwords( $tab_group ) . ' - ' . $doc_title; 108 } 109 110 // Replace element type with element title. 111 foreach ( $elements as $element_type => $data ) { 112 unset( $elements[ $element_type ] ); 113 114 if ( in_array( $element_type, [ 'section', 'column' ], true ) ) { 115 continue; 116 } 117 118 $widget_instance = Plugin::$instance->widgets_manager->get_widget_types( $element_type ); 119 120 if ( 'html' === $format && $widget_instance ) { 121 $widget_title = $widget_instance->get_title(); 122 } else { 123 $widget_title = $element_type; 124 } 125 126 $elements[ $widget_title ] = $data['count']; 127 } 128 129 // Sort elements by key. 130 ksort( $elements ); 131 132 $usage[ $doc_type ] = [ 133 'title' => $doc_title, 134 'elements' => $elements, 135 'count' => $doc_count, 136 ]; 137 138 // ' ? 1 : 0;' In sorters is compatibility for PHP8.0. 139 // Sort usage by title. 140 uasort( $usage, function( $a, $b ) { 141 return ( $a['title'] > $b['title'] ) ? 1 : 0; 142 } ); 143 144 // If title includes '-' will have lower priority. 145 uasort( $usage, function( $a ) { 146 return strpos( $a['title'], '-' ) ? 1 : 0; 147 } ); 148 } 149 150 return $usage; 151 } 152 153 /** 154 * Before document Save. 155 * 156 * Called on elementor/document/before_save, remove document from global & set saving flag. 157 * 158 * @param Document $document 159 * @param array $data new settings to save. 160 */ 161 public function before_document_save( $document, $data ) { 162 $current_status = get_post_status( $document->get_post() ); 163 $new_status = isset( $data['settings']['post_status'] ) ? $data['settings']['post_status'] : ''; 164 165 if ( $current_status === $new_status ) { 166 $this->remove_from_global( $document ); 167 } 168 169 $this->is_document_saving = true; 170 } 171 172 /** 173 * After document save. 174 * 175 * Called on elementor/document/after_save, adds document to global & clear saving flag. 176 * 177 * @param Document $document 178 */ 179 public function after_document_save( $document ) { 180 if ( Document::STATUS_PUBLISH === $document->get_post()->post_status || Document::STATUS_PRIVATE === $document->get_post()->post_status ) { 181 $this->save_document_usage( $document ); 182 } 183 184 $this->is_document_saving = false; 185 } 186 187 /** 188 * On status change. 189 * 190 * Called on transition_post_status. 191 * 192 * @param string $new_status 193 * @param string $old_status 194 * @param \WP_Post $post 195 */ 196 public function on_status_change( $new_status, $old_status, $post ) { 197 if ( wp_is_post_autosave( $post ) ) { 198 return; 199 } 200 201 // If it's from elementor editor, the usage should be saved via `before_document_save`/`after_document_save`. 202 if ( $this->is_document_saving ) { 203 return; 204 } 205 206 $document = Plugin::$instance->documents->get( $post->ID ); 207 208 if ( ! $document ) { 209 return; 210 } 211 212 $is_public_unpublish = 'publish' === $old_status && 'publish' !== $new_status; 213 $is_private_unpublish = 'private' === $old_status && 'private' !== $new_status; 214 215 if ( $is_public_unpublish || $is_private_unpublish ) { 216 $this->remove_from_global( $document ); 217 } 218 219 $is_public_publish = 'publish' !== $old_status && 'publish' === $new_status; 220 $is_private_publish = 'private' !== $old_status && 'private' === $new_status; 221 222 if ( $is_public_publish || $is_private_publish ) { 223 $this->save_document_usage( $document ); 224 } 225 } 226 227 /** 228 * On before delete post. 229 * 230 * Called on on_before_delete_post. 231 * 232 * @param int $post_id 233 */ 234 public function on_before_delete_post( $post_id ) { 235 $document = Plugin::$instance->documents->get( $post_id ); 236 237 if ( $document->get_id() !== $document->get_main_id() ) { 238 return; 239 } 240 241 $this->remove_from_global( $document ); 242 } 243 244 /** 245 * Add's tracking data. 246 * 247 * Called on elementor/tracker/send_tracking_data_params. 248 * 249 * @param array $params 250 * 251 * @return array 252 */ 253 public function add_tracking_data( $params ) { 254 $params['usages']['elements'] = get_option( self::OPTION_NAME ); 255 256 return $params; 257 } 258 259 /** 260 * Recalculate usage. 261 * 262 * Recalculate usage for all elementor posts. 263 * 264 * @param int $limit 265 * @param int $offset 266 * 267 * @return int 268 */ 269 public function recalc_usage( $limit = -1, $offset = 0 ) { 270 // While requesting recalc_usage, data should be deleted. 271 // if its in a batch the data should be deleted only on the first batch. 272 if ( 0 === $offset ) { 273 delete_option( self::OPTION_NAME ); 274 } 275 276 $post_types = get_post_types( array( 'public' => true ) ); 277 278 $query = new \WP_Query( [ 279 'meta_key' => '_elementor_data', 280 'post_type' => $post_types, 281 'post_status' => [ 'publish', 'private' ], 282 'posts_per_page' => $limit, 283 'offset' => $offset, 284 ] ); 285 286 foreach ( $query->posts as $post ) { 287 $document = Plugin::$instance->documents->get( $post->ID ); 288 289 if ( ! $document ) { 290 continue; 291 } 292 293 $this->after_document_save( $document ); 294 } 295 296 // Clear query memory before leave. 297 wp_cache_flush(); 298 299 return count( $query->posts ); 300 } 301 302 /** 303 * Increase controls count. 304 * 305 * Increase controls count, for each element. 306 * 307 * @param array &$element_ref 308 * @param string $tab 309 * @param string $section 310 * @param string $control 311 * @param int $count 312 */ 313 private function increase_controls_count( &$element_ref, $tab, $section, $control, $count ) { 314 if ( ! isset( $element_ref['controls'][ $tab ] ) ) { 315 $element_ref['controls'][ $tab ] = []; 316 } 317 318 if ( ! isset( $element_ref['controls'][ $tab ][ $section ] ) ) { 319 $element_ref['controls'][ $tab ][ $section ] = []; 320 } 321 322 if ( ! isset( $element_ref['controls'][ $tab ][ $section ][ $control ] ) ) { 323 $element_ref['controls'][ $tab ][ $section ][ $control ] = 0; 324 } 325 326 $element_ref['controls'][ $tab ][ $section ][ $control ] += $count; 327 } 328 329 /** 330 * Add Controls 331 * 332 * Add's controls to this element_ref, returns changed controls count. 333 * 334 * @param array $settings_controls 335 * @param array $element_controls 336 * @param array &$element_ref 337 * 338 * @return int ($changed_controls_count). 339 */ 340 private function add_controls( $settings_controls, $element_controls, &$element_ref ) { 341 $changed_controls_count = 0; 342 343 // Loop over all element settings. 344 foreach ( $settings_controls as $control => $value ) { 345 if ( empty( $element_controls[ $control ] ) ) { 346 continue; 347 } 348 349 $control_config = $element_controls[ $control ]; 350 351 if ( ! isset( $control_config['section'], $control_config['default'] ) ) { 352 continue; 353 } 354 355 $tab = $control_config['tab']; 356 $section = $control_config['section']; 357 358 // If setting value is not the control default. 359 if ( $value !== $control_config['default'] ) { 360 $this->increase_controls_count( $element_ref, $tab, $section, $control, 1 ); 361 362 $changed_controls_count++; 363 } 364 } 365 366 return $changed_controls_count; 367 } 368 369 /** 370 * Add general controls. 371 * 372 * Extract general controls to element ref, return clean `$settings_control`. 373 * 374 * @param array $settings_controls 375 * @param array &$element_ref 376 * 377 * @return array ($settings_controls). 378 */ 379 private function add_general_controls( $settings_controls, &$element_ref ) { 380 if ( ! empty( $settings_controls[ Manager::DYNAMIC_SETTING_KEY ] ) ) { 381 $settings_controls = array_merge( $settings_controls, $settings_controls[ Manager::DYNAMIC_SETTING_KEY ] ); 382 383 // Add dynamic count to controls under `general` tab. 384 $this->increase_controls_count( 385 $element_ref, 386 self::GENERAL_TAB, 387 Manager::DYNAMIC_SETTING_KEY, 388 'count', 389 count( $settings_controls[ Manager::DYNAMIC_SETTING_KEY ] ) 390 ); 391 } 392 393 return $settings_controls; 394 } 395 396 /** 397 * Add to global. 398 * 399 * Add's usage to global (update database). 400 * 401 * @param string $doc_name 402 * @param array $doc_usage 403 */ 404 private function add_to_global( $doc_name, $doc_usage ) { 405 $global_usage = get_option( self::OPTION_NAME, [] ); 406 407 foreach ( $doc_usage as $element_type => $element_data ) { 408 if ( ! isset( $global_usage[ $doc_name ] ) ) { 409 $global_usage[ $doc_name ] = []; 410 } 411 412 if ( ! isset( $global_usage[ $doc_name ][ $element_type ] ) ) { 413 $global_usage[ $doc_name ][ $element_type ] = [ 414 'count' => 0, 415 'controls' => [], 416 ]; 417 } 418 419 $global_element_ref = &$global_usage[ $doc_name ][ $element_type ]; 420 $global_element_ref['count'] += $element_data['count']; 421 422 if ( empty( $element_data['controls'] ) ) { 423 continue; 424 } 425 426 foreach ( $element_data['controls'] as $tab => $sections ) { 427 foreach ( $sections as $section => $controls ) { 428 foreach ( $controls as $control => $count ) { 429 $this->increase_controls_count( $global_element_ref, $tab, $section, $control, $count ); 430 } 431 } 432 } 433 } 434 435 update_option( self::OPTION_NAME, $global_usage, false ); 436 } 437 438 /** 439 * Remove from global. 440 * 441 * Remove's usage from global (update database). 442 * 443 * @param Document $document 444 */ 445 private function remove_from_global( $document ) { 446 $prev_usage = $document->get_meta( self::META_KEY ); 447 448 if ( empty( $prev_usage ) ) { 449 return; 450 } 451 452 $doc_name = $document->get_name(); 453 454 $global_usage = get_option( self::OPTION_NAME, [] ); 455 456 foreach ( $prev_usage as $element_type => $doc_value ) { 457 if ( isset( $global_usage[ $doc_name ][ $element_type ]['count'] ) ) { 458 $global_usage[ $doc_name ][ $element_type ]['count'] -= $prev_usage[ $element_type ]['count']; 459 460 if ( 0 === $global_usage[ $doc_name ][ $element_type ]['count'] ) { 461 unset( $global_usage[ $doc_name ][ $element_type ] ); 462 463 if ( 0 === count( $global_usage[ $doc_name ] ) ) { 464 unset( $global_usage[ $doc_name ] ); 465 } 466 467 continue; 468 } 469 470 foreach ( $prev_usage[ $element_type ]['controls'] as $tab => $sections ) { 471 foreach ( $sections as $section => $controls ) { 472 foreach ( $controls as $control => $count ) { 473 if ( isset( $global_usage[ $doc_name ][ $element_type ]['controls'][ $tab ][ $section ][ $control ] ) ) { 474 $section_ref = &$global_usage[ $doc_name ][ $element_type ]['controls'][ $tab ][ $section ]; 475 476 $section_ref[ $control ] -= $count; 477 478 if ( 0 === $section_ref[ $control ] ) { 479 unset( $section_ref[ $control ] ); 480 } 481 } 482 } 483 } 484 } 485 } 486 } 487 488 update_option( self::OPTION_NAME, $global_usage, false ); 489 490 $document->delete_meta( self::META_KEY ); 491 } 492 493 /** 494 * Get elements usage. 495 * 496 * Get's the current elements usage by passed elements array parameter. 497 * 498 * @param array $elements 499 * 500 * @return array 501 */ 502 private function get_elements_usage( $elements ) { 503 $usage = []; 504 505 Plugin::$instance->db->iterate_data( $elements, function ( $element ) use ( &$usage ) { 506 if ( empty( $element['widgetType'] ) ) { 507 $type = $element['elType']; 508 $element_instance = Plugin::$instance->elements_manager->get_element_types( $type ); 509 } else { 510 $type = $element['widgetType']; 511 $element_instance = Plugin::$instance->widgets_manager->get_widget_types( $type ); 512 } 513 514 if ( ! isset( $usage[ $type ] ) ) { 515 $usage[ $type ] = [ 516 'count' => 0, 517 'control_percent' => 0, 518 'controls' => [], 519 ]; 520 } 521 522 $usage[ $type ]['count']++; 523 524 if ( ! $element_instance ) { 525 return $element; 526 } 527 528 $element_controls = $element_instance->get_controls(); 529 530 if ( isset( $element['settings'] ) ) { 531 $settings_controls = $element['settings']; 532 $element_ref = &$usage[ $type ]; 533 534 // Add dynamic values. 535 $settings_controls = $this->add_general_controls( $settings_controls, $element_ref ); 536 537 $changed_controls_count = $this->add_controls( $settings_controls, $element_controls, $element_ref ); 538 539 $percent = $changed_controls_count / ( count( $element_controls ) / 100 ); 540 541 $usage[ $type ] ['control_percent'] = (int) round( $percent ); 542 } 543 544 return $element; 545 } ); 546 547 return $usage; 548 } 549 550 /** 551 * Save document usage. 552 * 553 * Save requested document usage, and update global. 554 * 555 * @param Document $document 556 */ 557 private function save_document_usage( Document $document ) { 558 if ( ! $document::get_property( 'is_editable' ) && ! $document->is_built_with_elementor() ) { 559 return; 560 } 561 562 // Get data manually to avoid conflict with `\Elementor\Core\Base\Document::get_elements_data... convert_to_elementor`. 563 $data = $document->get_json_meta( '_elementor_data' ); 564 565 if ( ! empty( $data ) ) { 566 try { 567 $usage = $this->get_elements_usage( $document->get_elements_raw_data( $data ) ); 568 569 $document->update_meta( self::META_KEY, $usage ); 570 571 $this->add_to_global( $document->get_name(), $usage ); 572 } catch ( \Exception $exception ) { 573 Plugin::$instance->logger->get_logger()->error( $exception->getMessage(), [ 574 'document_id' => $document->get_id(), 575 'document_name' => $document->get_name(), 576 ] ); 577 578 return; 579 }; 580 } 581 } 582 583 /** 584 * Add system info report. 585 */ 586 public function add_system_info_report() { 587 System_Info::add_report( 'usage', [ 588 'file_name' => __DIR__ . '/usage-reporter.php', 589 'class_name' => __NAMESPACE__ . '\Usage_Reporter', 590 ] ); 591 } 592 593 /** 594 * Usage module constructor. 595 * 596 * Initializing Elementor usage module. 597 * 598 * @access public 599 */ 600 public function __construct() { 601 add_action( 'transition_post_status', [ $this, 'on_status_change' ], 10, 3 ); 602 add_action( 'before_delete_post', [ $this, 'on_before_delete_post' ] ); 603 604 add_action( 'elementor/document/before_save', [ $this, 'before_document_save' ], 10, 2 ); 605 add_action( 'elementor/document/after_save', [ $this, 'after_document_save' ] ); 606 607 add_filter( 'elementor/tracker/send_tracking_data_params', [ $this, 'add_tracking_data' ] ); 608 609 add_action( 'admin_init', [ $this, 'add_system_info_report' ], 50 ); 610 } 611 }