PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 4.0.8
Visualizer – Tables & Charts Manager with Built-in AI Generator v4.0.8
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 3.10.3 All 149 releases
← All changes | classes/Visualizer/Module.php +214 -78 3.10.14.0.8 View file →
@@ -67,10 +67,9 @@
67 67 $this->_addFilter( Visualizer_Plugin::FILTER_UNDO_REVISIONS, 'undoRevisions', 10, 2 );
68 68 $this->_addFilter( Visualizer_Plugin::FILTER_HANDLE_REVISIONS, 'handleExistingRevisions', 10, 2 );
69 69 $this->_addFilter( Visualizer_Plugin::FILTER_GET_CHART_DATA_AS, 'getDataAs', 10, 3 );
70 70 $this->_addAction( 'pre_get_posts', 'PreGetPosts' );
71 - register_shutdown_function( array($this, 'onShutdown') );
72 -
71 + register_shutdown_function( array( $this, 'onShutdown' ) );
73 72 }
74 73
75 74 /**
76 75 * Register a shutdown hook to catch fatal errors.
@@ -113,18 +112,18 @@
113 112 * @access public
114 113 * @param string $tag The name of the AJAX action to which the $method is hooked.
115 114 * @param string $method Optional. The name of the method to be called. If the name of the method is not provided, tag name will be used as method name.
116 115 * @param bool $methodClass The root of the method.
117 - * @param boolean $private Optional. Determines if we should register hook for logged in users.
118 - * @param boolean $public Optional. Determines if we should register hook for not logged in users.
116 + * @param boolean $logged_in Optional. Determines if we should register hook for logged in users.
117 + * @param boolean $logged_out Optional. Determines if we should register hook for not logged in users.
119 118 * @return Visualizer_Module
120 119 */
121 - protected function _addAjaxAction( $tag, $method = '', $methodClass = null, $private = true, $public = false ) {
122 - if ( $private ) {
120 + protected function _addAjaxAction( $tag, $method = '', $methodClass = null, $logged_in = true, $logged_out = false ) {
121 + if ( $logged_in ) {
123 122 $this->_addAction( 'wp_ajax_' . $tag, $method, $methodClass );
124 123 }
125 124
126 - if ( $public ) {
125 + if ( $logged_out ) {
127 126 $this->_addAction( 'wp_ajax_nopriv_' . $tag, $method, $methodClass );
128 127 }
129 128
130 129 return $this;
@@ -137,9 +136,9 @@
137 136 * @uses add_filter() To register filter hook.
138 137 *
139 138 * @access protected
140 139 * @param string $tag The name of the filter to hook the $method to.
141 - * @param type $method The name of the method to be called when the filter is applied.
140 + * @param string $method The name of the method to be called when the filter is applied.
142 141 * @param int $priority optional. Used to specify the order in which the functions associated with a particular action are executed (default: 10). Lower numbers correspond with earlier execution, and functions with the same priority are executed in the order in which they were added to the action.
143 142 * @param int $accepted_args optional. The number of arguments the function accept (default 1).
144 143 * @return Visualizer_Module
145 144 */
@@ -168,9 +167,9 @@
168 167 * A wrapper around the actual function _getDataAs. This function is invoked as a filter.
169 168 *
170 169 * @since 3.2.0
171 170 */
172 - public function getDataAs( $final, $chart_id, $type ) {
171 + public function getDataAs( $data, $chart_id, $type ) {
173 172 return $this->_getDataAs( $chart_id, $type );
174 173 }
175 174
176 175 /**
@@ -234,9 +233,8 @@
234 233 $title = 'visualizer#' . $chart_id;
235 234 }
236 235
237 236 $filename = $title;
238 -
239 237 switch ( $type ) {
240 238 case 'csv':
241 239 $final = $this->_getCSV( $rows, $filename, false );
242 240 break;
@@ -270,14 +268,17 @@
270 268
271 269 $bom = chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF );
272 270 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
273 271 $fp = function_exists( 'tmpfile' ) ? @tmpfile() : null;
274 - if ( null === $fp ) {
272 + if ( ! $fp ) {
275 273 if ( ! function_exists( 'wp_tempnam' ) ) {
276 274 require_once ABSPATH . 'wp-admin/includes/file.php';
277 275 }
278 276 $fp = fopen( wp_tempnam(), 'w+' );
279 277 }
278 + if ( ! $fp ) {
279 + return array( 'csv' => '', 'name' => $filename, 'string' => '' );
280 + }
280 281 if ( ! apply_filters( 'vizualizer_export_include_series_type', true ) ) {
281 282 unset( $rows[1] );
282 283 $rows = array_values( $rows );
283 284 }
@@ -287,10 +288,10 @@
287 288 fputcsv( $fp, $row );
288 289 }
289 290 rewind( $fp );
290 291 $csv = '';
291 - // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
292 - while ( ( $array = fgetcsv( $fp ) ) !== false ) {
292 + $array = fgetcsv( $fp );
293 + while ( $array !== false ) {
293 294 if ( strlen( $csv ) > 0 ) {
294 295 $csv .= PHP_EOL;
295 296 }
296 297 // if enclosure is required, check every item of this line
@@ -304,9 +305,10 @@
304 305 $temp_array[] = $item;
305 306 }
306 307 $array = $temp_array;
307 308 }
308 - $csv .= implode( ',', $array );
309 + $csv .= implode( ',', $array );
310 + $array = fgetcsv( $fp );
309 311 }
310 312 fclose( $fp );
311 313
312 314 return array(
@@ -323,16 +325,16 @@
323 325 * @param array $rows The array of data.
324 326 * @param string $filename The name of the file to use.
325 327 */
326 328 private function _getExcel( $rows, $filename ) {
327 - // PHPExcel did not like sheet names longer than 31 characters and we will assume the same with PhpSpreadsheet
328 - $chart = substr( $filename, 0, 30 );
329 - $filename .= '.xlsx';
329 + // OpenSpout allows for long sheet names, but let's keep the same limit for compatibility.
330 + $chart = substr( $filename, 0, 30 );
331 + $filename .= '.xlsx';
330 332 if ( ! apply_filters( 'vizualizer_export_include_series_type', true ) ) {
331 333 unset( $rows[1] );
332 334 $rows = array_values( $rows );
333 335 $rows = array_map(
334 - function( $r ) {
336 + function ( $r ) {
335 337 return array_map( 'strval', $r );
336 338 },
337 339 $rows
338 340 );
@@ -338,24 +340,34 @@
338 340 );
339 341 }
340 342 $vendor_file = VISUALIZER_ABSPATH . '/vendor/autoload.php';
341 343 if ( is_readable( $vendor_file ) ) {
342 - include_once( $vendor_file );
344 + include_once $vendor_file;
343 345 }
344 - $xlsData = '';
345 - if ( class_exists( 'PhpOffice\PhpSpreadsheet\Spreadsheet' ) ) {
346 - $doc = new PhpOffice\PhpSpreadsheet\Spreadsheet();
347 - $doc->getActiveSheet()->fromArray( $rows, null, 'A1' );
348 - $doc->getActiveSheet()->setTitle( sanitize_title( $chart ) );
349 - $doc = apply_filters( 'visualizer_excel_doc', $doc );
350 - $writer = PhpOffice\PhpSpreadsheet\IOFactory::createWriter( $doc, 'Xlsx' );
351 - ob_start();
352 - $writer->save( 'php://output' );
353 - $xlsData = ob_get_contents();
354 - ob_end_clean();
346 + $xlsData = '';
347 + if ( class_exists( 'OpenSpout\Writer\Common\Creator\WriterEntityFactory' ) ) {
348 + try {
349 + // Use OpenSpout to create the XLSX file in memory.
350 + $writer = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createXLSXWriter();
351 + $writer->openToFile( 'php://output' ); // Open to output instead of a file.
352 + $writer->getCurrentSheet()->setName( sanitize_title( $chart ) );
353 +
354 + // Write rows.
355 + foreach ( $rows as $row ) {
356 + $rowFromValues = \OpenSpout\Writer\Common\Creator\WriterEntityFactory::createRowFromArray( $row );
357 + $writer->addRow( $rowFromValues );
358 + }
359 +
360 + ob_start();
361 + $writer->close(); // Saves and closes the file in the output buffer.
362 + $xlsData = ob_get_clean();
363 + } catch ( Exception $e ) {
364 + do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, 'OpenSpout writer error: ' . $e->getMessage(), 'error', __FILE__, __LINE__ );
365 + error_log( 'OpenSpout writer error: ' . $e->getMessage() );
366 + }
355 367 } else {
356 - do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, 'Class PhpOffice\PhpSpreadsheet\Spreadsheet does not exist!', 'error', __FILE__, __LINE__ );
357 - error_log( 'Class PhpOffice\PhpSpreadsheet\Spreadsheet does not exist!' );
368 + do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, 'Class OpenSpout\Writer\Common\Creator\WriterEntityFactory does not exist!', 'error', __FILE__, __LINE__ );
369 + error_log( 'Class OpenSpout\Writer\Common\Creator\WriterEntityFactory does not exist!' );
358 370 }
359 371 return array(
360 372 'csv' => 'data:application/vnd.ms-excel;base64,' . base64_encode( $xlsData ),
361 373 'name' => $filename,
@@ -392,9 +404,9 @@
392 404 $index = 0;
393 405 foreach ( $rows as $row ) {
394 406 // skip the data type row.
395 407 if ( 1 === $index ) {
396 - $index++;
408 + ++$index;
397 409 continue;
398 410 }
399 411
400 412 $table .= '<tr>';
@@ -405,9 +417,9 @@
405 417 $table .= '<td>' . $col . '</td>';
406 418 }
407 419 }
408 420 $table .= '</tr>';
409 - $index++;
421 + ++$index;
410 422 }
411 423 $table .= '</table>';
412 424
413 425 $html .= apply_filters( 'visualizer_print_table', $table ) . '
@@ -420,11 +432,11 @@
420 432
421 433 /**
422 434 * Disable revisions temporarily for visualizer post type.
423 435 */
424 - protected final function disableRevisionsTemporarily() {
436 + final protected function disableRevisionsTemporarily() {
425 437 add_filter(
426 - 'wp_revisions_to_keep', function( $num, $post ) {
438 + 'wp_revisions_to_keep', function ( $num, $post ) {
427 439 if ( $post->post_type === Visualizer_Plugin::CPT_VISUALIZER ) {
428 440 return 0;
429 441 }
430 442 return $num;
@@ -436,9 +448,9 @@
436 448 * Undo revisions for the chart, and if necessary, restore the earliest version.
437 449 *
438 450 * @return bool If any revisions were found.
439 451 */
440 - public final function undoRevisions( $chart_id, $restore = false ) {
452 + final public function undoRevisions( $chart_id, $restore = false ) {
441 453 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'undoRevisions for %d with%s restore', $chart_id, ( $restore ? '' : 'out' ) ), 'debug', __FILE__, __LINE__ );
442 454 if ( get_post_type( $chart_id ) !== Visualizer_Plugin::CPT_VISUALIZER ) {
443 455 return false;
444 456 }
@@ -468,14 +480,19 @@
468 480
469 481 /**
470 482 * If existing revisions exist for the chart, restore the earliest version and then create a new revision to initiate editing.
471 483 */
472 - public final function handleExistingRevisions( $chart_id, $chart ) {
484 + final public function handleExistingRevisions( $chart_id, $chart ) {
473 485
474 486 do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'handleExistingRevisions for %d', $chart_id ), 'debug', __FILE__, __LINE__ );
475 487 if ( get_post_type( $chart_id ) !== Visualizer_Plugin::CPT_VISUALIZER ) {
476 488 return $chart_id;
477 489 }
490 + // AI Builder (D3) charts use their own publish flow — skip the classic revision-restore
491 + // mechanism, which would otherwise overwrite the saved title and settings.
492 + if ( 'd3' === get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true ) ) {
493 + return $chart;
494 + }
478 495 // undo revisions.
479 496 $revisions_found = $this->undoRevisions( $chart_id, true );
480 497
481 498 // create revision for the edit action.
@@ -519,48 +536,55 @@
519 536 if ( VISUALIZER_TEST_JS_CUSTOMIZATION ) {
520 537 return $default;
521 538 }
522 539
523 - require_once( ABSPATH . 'wp-admin/includes/file.php' );
524 - WP_Filesystem();
525 - global $wp_filesystem;
540 + try {
541 + require_once ABSPATH . 'wp-admin/includes/file.php';
542 + WP_Filesystem();
543 + global $wp_filesystem;
544 + if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) {
545 + return $default;
546 + }
526 547
527 - $multisite_arg = '/';
528 - if ( is_multisite() && ! is_main_site() ) {
529 - $multisite_arg = '/sites/' . get_current_blog_id() . '/';
530 - }
548 + $multisite_arg = '/';
549 + if ( is_multisite() && ! is_main_site() ) {
550 + $multisite_arg = '/sites/' . get_current_blog_id() . '/';
551 + }
531 552
532 - $dir = $wp_filesystem->wp_content_dir() . 'uploads' . $multisite_arg . 'visualizer';
533 - $file = $wp_filesystem->wp_content_dir() . 'uploads' . $multisite_arg . 'visualizer/customization.js';
553 + $dir = $wp_filesystem->wp_content_dir() . 'uploads' . $multisite_arg . 'visualizer';
554 + $file = $wp_filesystem->wp_content_dir() . 'uploads' . $multisite_arg . 'visualizer/customization.js';
534 555
535 - if ( $wp_filesystem->is_readable( $file ) ) {
536 - return $specific;
537 - }
556 + if ( $wp_filesystem->is_readable( $file ) ) {
557 + return $specific;
558 + }
538 559
539 - if ( $wp_filesystem->exists( $file ) && ! $wp_filesystem->is_readable( $file ) ) {
540 - do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Unable to read file %s', $file ), 'error', __FILE__, __LINE__ );
541 - return $default;
542 - }
560 + if ( $wp_filesystem->exists( $file ) && ! $wp_filesystem->is_readable( $file ) ) {
561 + do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Unable to read file %s', $file ), 'error', __FILE__, __LINE__ );
562 + return $default;
563 + }
543 564
544 - if ( ! $wp_filesystem->exists( $dir ) ) {
545 - // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found
546 - if ( ( $done = $wp_filesystem->mkdir( $dir ) ) === false ) {
547 - do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Unable to create directory %s', $dir ), 'error', __FILE__, __LINE__ );
548 - return $default;
565 + if ( ! $wp_filesystem->exists( $dir ) ) {
566 + $done = $wp_filesystem->mkdir( $dir );
567 + if ( $done === false ) {
568 + do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Unable to create directory %s', $dir ), 'error', __FILE__, __LINE__ );
569 + return $default;
570 + }
549 571 }
550 - }
551 572
552 - // if file does not exist, copy.
553 - if ( ! $wp_filesystem->exists( $file ) ) {
554 - $src = str_replace( ABSPATH, $wp_filesystem->abspath(), VISUALIZER_ABSPATH . '/js/customization.js' );
555 - // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found
556 - if ( ( $done = $wp_filesystem->copy( $src, $file ) ) === false ) {
557 - do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Unable to copy file %s to %s', $src, $file ), 'error', __FILE__, __LINE__ );
558 - return $default;
573 + // if file does not exist, copy.
574 + if ( ! $wp_filesystem->exists( $file ) ) {
575 + $src = str_replace( ABSPATH, $wp_filesystem->abspath(), VISUALIZER_ABSPATH . '/js/customization.js' );
576 + $done = $wp_filesystem->copy( $src, $file );
577 + if ( $done === false ) {
578 + do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Unable to copy file %s to %s', $src, $file ), 'error', __FILE__, __LINE__ );
579 + return $default;
580 + }
559 581 }
582 +
583 + return $specific;
584 + } catch ( \Throwable $e ) {
585 + return $default;
560 586 }
561 -
562 - return $specific;
563 587 }
564 588
565 589 /**
566 590 * Load the class for the given chart's chart type so that its assets can be loaded.
@@ -565,15 +589,18 @@
565 589 /**
566 590 * Load the class for the given chart's chart type so that its assets can be loaded.
567 591 */
568 592 protected function load_chart_type( $chart_id ) {
593 + // D3/AI charts have no traditional type class — return the library string directly.
594 + $lib = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true );
595 + if ( 'd3' === $lib ) {
596 + return 'd3';
597 + }
598 +
569 599 $name = $this->load_chart_class_name( $chart_id );
570 600 $class = null;
571 601 if ( class_exists( $name ) || true === apply_filters( 'visualizer_load_chart', false, $name ) ) {
572 - if ( 'Visualizer_Render_Sidebar_Type_DataTable_DataTable' === $name ) {
573 - $name = 'Visualizer_Render_Sidebar_Type_DataTable_Tabular';
574 - }
575 - $class = new $name;
602 + $class = new $name();
576 603 }
577 604
578 605 if ( is_null( $class ) && Visualizer_Module::is_pro() ) {
579 606 // lets see if this type exists in pro. New Lite(3.1.0+) & old Pro(1.8.0-).
@@ -622,9 +649,9 @@
622 649 }
623 650 $class_name = $id . $name;
624 651 $properties = implode( ' !important; ', array_filter( $attributes ) );
625 652 if ( ! empty( $properties ) ) {
626 - $css .= '.' . $class_name . ' {' . $properties . ' !important;}';
653 + $css .= wp_strip_all_tags( '.' . $class_name . ' {' . $properties . ' !important;}' );
627 654 $classes[ $name ] = $class_name;
628 655 }
629 656 }
630 657 $settings['cssClassNames'] = $classes;
@@ -695,8 +722,29 @@
695 722 return $q->found_posts;
696 723 }
697 724
698 725 /**
726 + * Checks whether the current user may edit a specific chart.
727 + *
728 + * @param int $chart_id Chart ID.
729 + * @return bool
730 + */
731 + public static function can_edit_chart( $chart_id ) {
732 + $chart_id = absint( $chart_id );
733 + if ( ! $chart_id ) {
734 + return false;
735 + }
736 +
737 + $chart = get_post( $chart_id );
738 + return $chart
739 + && Visualizer_Plugin::CPT_VISUALIZER === $chart->post_type
740 + && (
741 + current_user_can( 'edit_post', $chart_id )
742 + || ( (int) $chart->post_author === get_current_user_id() && current_user_can( 'edit_posts' ) )
743 + );
744 + }
745 +
746 + /**
699 747 * Checks if the PRO version is active.
700 748 *
701 749 * @since 3.3.0
702 750 */
@@ -732,18 +780,106 @@
732 780
733 781 /**
734 782 * Gets the features for the provided license type.
735 783 */
736 - public static final function get_features_for_license( $plan ) {
784 + final public static function get_features_for_license( $plan ) {
785 + $is_new_personal = apply_filters( 'visualizer_is_new_personal', false );
737 786 switch ( $plan ) {
738 787 case 1:
739 - return array( 'import-wp', 'db-query', 'import-wc-report' );
788 + $features = array( 'import-wp', 'import-wc-report', 'import-file', 'import-url' );
789 + if ( ! $is_new_personal ) {
790 + $features[] = 'db-query';
791 + }
792 + return $features;
740 793 case 2:
741 - return array( 'schedule-chart', 'chart-permissions' );
794 + $features = array( 'schedule-chart', 'chart-permissions', 'import-chart', 'data-filter-configuration', 'frontend-actions' );
795 + if ( $is_new_personal ) {
796 + $features[] = 'db-query';
797 + }
798 + return $features;
742 799 }
743 800 }
744 801
745 802 /**
803 + * Safely unserialize chart/source content, blocking PHP object injection.
804 + *
805 + * Single guarded chokepoint shared by chart/source content sinks so the
806 + * allowed_classes guard cannot be dropped from one call site independently.
807 + *
808 + * @param mixed $content The serialized content (only strings are decoded).
809 + * @return mixed The decoded value (array for valid chart data), or false.
810 + */
811 + public static function decode_content( $content ) {
812 + if ( ! is_string( $content ) ) {
813 + return false;
814 + }
815 + $value = unserialize( trim( $content ), array( 'allowed_classes' => false ) );
816 + if ( self::contains_references( $value ) ) {
817 + return false;
818 + }
819 + return self::strip_incomplete_objects( $value );
820 + }
821 +
822 + /**
823 + * Check decoded arrays for references before recursively processing them.
824 + *
825 + * Cyclic serialized arrays necessarily contain a reference. Rejecting all
826 + * references also prevents shared references from becoming cycles later,
827 + * so strip_incomplete_objects() cannot recurse without terminating.
828 + *
829 + * @param mixed $value The decoded value.
830 + * @return bool Whether the value contains an array reference.
831 + */
832 + private static function contains_references( $value ) {
833 + if ( ! is_array( $value ) ) {
834 + return false;
835 + }
836 + foreach ( array_keys( $value ) as $key ) {
837 + if ( null !== ReflectionReference::fromArrayElement( $value, $key ) ) {
838 + return true;
839 + }
840 + if ( is_array( $value[ $key ] ) && self::contains_references( $value[ $key ] ) ) {
841 + return true;
842 + }
843 + }
844 + return false;
845 + }
846 +
847 + /**
848 + * Remove the __PHP_Incomplete_Class stubs the allowed_classes guard leaves
849 + * behind; they crash map_deep() when the decoded value is written back to
850 + * post meta. Legitimate chart content is nested arrays/scalars only.
851 + *
852 + * @param mixed $value The decoded value.
853 + * @return mixed The value without object stubs; false for a top-level stub.
854 + */
855 + private static function strip_incomplete_objects( $value ) {
856 + if ( $value instanceof __PHP_Incomplete_Class ) {
857 + return false;
858 + }
859 + if ( is_array( $value ) ) {
860 + foreach ( $value as $key => $item ) {
861 + if ( $item instanceof __PHP_Incomplete_Class ) {
862 + unset( $value[ $key ] );
863 + } elseif ( is_array( $item ) ) {
864 + $value[ $key ] = self::strip_incomplete_objects( $item );
865 + }
866 + }
867 + }
868 + return $value;
869 + }
870 +
871 + /**
872 + * Object-injection-safe drop-in for maybe_unserialize().
873 + *
874 + * @param mixed $value Raw meta/content value.
875 + * @return mixed The decoded value for serialized input, the value unchanged otherwise.
876 + */
877 + public static function maybe_decode_content( $value ) {
878 + return is_serialized( $value ) ? self::decode_content( $value ) : $value;
879 + }
880 +
881 + /**
746 882 * Gets the chart content after common manipulations.
747 883 */
748 884 public static function get_chart_data( $chart, $type, $run_filter = true ) {
749 885 // change HTML entities
@@ -756,9 +892,9 @@
756 892 }
757 893 },
758 894 $post_content
759 895 );
760 - $data = unserialize( $post_content );
896 + $data = self::decode_content( $post_content );
761 897 $altered = array();
762 898 if ( ! empty( $data ) ) {
763 899 foreach ( $data as $index => $array ) {
764 900 if ( ! is_array( $index ) && is_array( $array ) ) {