uploads_folder = $uploads_folder; $this->settings = $settings; $this->plugin = $plugin; $this->loader = null; $this->twig = null; $this->wp_filesystem = null; } protected function get_wp_filesystem(): WP_Filesystem_Base { if ( null === $this->wp_filesystem ) { global $wp_filesystem; require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); $this->wp_filesystem = $wp_filesystem; } return $this->wp_filesystem; } /** * @return array> */ protected function get_custom_functions(): array { return array( array( 'wp_interactivity_state', function ( string $store_namespace, array $state = array() ) { if ( false === function_exists( 'wp_interactivity_state' ) ) { return; } wp_interactivity_state( $store_namespace, $state ); }, ), array( 'wp_interactivity_data_wp_context', function ( array $context ) { if ( false === function_exists( 'wp_interactivity_data_wp_context' ) ) { return; } // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo wp_interactivity_data_wp_context( $context ); }, ), ); } /** * @return array> */ protected function get_custom_filters(): array { return array(); } protected function init_twig(): void { // @phpstan-ignore-next-line $this->loader = new FilesystemLoader( $this->uploads_folder ); // @phpstan-ignore-next-line $this->twig = new Environment( $this->loader, array( // will generate exception if a var doesn't exist instead of replace to NULL. 'strict_variables' => true, // 'html' by default, just highlight that it's secure to not escape TWIG variable values in PHP 'autoescape' => 'html', ) ); // reminder: TwigFunctions automatically escape the output // (as long you not pass ['is_safe' => ['html']] to the constructor). $custom_functions = $this->get_custom_functions(); $custom_filters = $this->get_custom_filters(); foreach ( $custom_functions as $custom_function ) { $function_name = key_exists( 0, $custom_function ) && is_string( $custom_function[0] ) ? $custom_function[0] : ''; $function_callback = key_exists( 1, $custom_function ) && is_callable( $custom_function[1] ) ? $custom_function[1] : null; $function_args = key_exists( 2, $custom_function ) && is_array( $custom_function[2] ) ? $custom_function[2] : array(); if ( '' === $function_name || null === $function_callback ) { continue; } // @phpstan-ignore-next-line $this->twig->addFunction( // @phpstan-ignore-next-line new TwigFunction( $function_name, $function_callback, $function_args ) ); } foreach ( $custom_filters as $custom_filter ) { $filter_name = key_exists( 0, $custom_filter ) && is_string( $custom_filter[0] ) ? $custom_filter[0] : ''; $filter_callback = key_exists( 1, $custom_filter ) && is_callable( $custom_filter[1] ) ? $custom_filter[1] : null; $filter_args = key_exists( 2, $custom_filter ) && is_array( $custom_filter[2] ) ? $custom_filter[2] : array(); if ( '' === $filter_name || null === $filter_callback ) { continue; } // @phpstan-ignore-next-line $this->twig->addFilter( // @phpstan-ignore-next-line new TwigFilter( $filter_name, $filter_callback, $filter_args ) ); } } // @phpstan-ignore-next-line protected function get_twig(): ?Environment { if ( null === $this->twig ) { if ( false === $this->get_wp_filesystem()->is_dir( $this->uploads_folder ) ) { return null; } $this->init_twig(); } return $this->twig; } protected function print_error_message( string $unique_view_id, string $error_message ): void { printf( '

Advanced Views (%s) template: %s

', esc_html( $unique_view_id ), esc_html( $error_message ) ); } protected function is_templates_dir_writable(): bool { $templates_dir = $this->uploads_folder; $wp_filesystem = $this->get_wp_filesystem(); if ( false === $wp_filesystem->is_dir( $templates_dir ) ) { return false; } $test_file = $templates_dir . '/test.txt'; // the best way to check is to make test write // (check of permissions or 'is_writable' is not enough, as it can be set to 777, but the folder can be owned by another user). $is_written = false !== $wp_filesystem->put_contents( $test_file, 'test' ); if ( false === $is_written ) { return false; } $content = $wp_filesystem->get_contents( $test_file ); $is_writable = 'test' === $content; $is_removed = $wp_filesystem->delete( $test_file ); return true === $is_writable && true === $is_removed; } /** * @param array $args */ public function print( string $unique_id, string $template, array $args, bool $is_validation = false ): void { $twig = $this->get_twig(); if ( null === $twig ) { $this->get_logger()->warning( "can't render the twig template as the templates folder is not writable", array( 'unique_id' => $unique_id, ) ); $this->print_error_message( $unique_id, 'Templates folder is not writable' ); return; } // emulate the template file for every View. // as Twig generates a PHP class for every template file // so if you use the same, it'll have HTML of the very first View. $template_name = sprintf( '%s.twig', $unique_id ); $template_file = $this->uploads_folder . '/' . $template_name; $wp_filesystem = $this->get_wp_filesystem(); $is_written = false !== $wp_filesystem->put_contents( $template_file, $template ); // check 'is_file' too, as it seems on some servers 'put_contents' returns true, but the dir/file is missing. if ( false === $is_written || false === $wp_filesystem->is_file( $template_file ) ) { $this->get_logger()->warning( "can't write the twig template file", array( 'unique_id' => $unique_id, ) ); $this->print_error_message( $unique_id, "Can't write template file" ); return; } try { // @phpstan-ignore-next-line $html = $twig->render( $template_name, $args ); if ( false !== strpos( $html, 'data-wp-interactive' ) && true === function_exists( 'wp_interactivity_process_directives' ) ) { $html = wp_interactivity_process_directives( $html ); } // @phpcs:ignore echo $html; } catch ( Exception $e ) { $is_admin_user = in_array( 'administrator', wp_get_current_user()->roles, true ); $is_debug_mode = $is_admin_user && $this->settings->is_dev_mode(); $error_message = $e->getMessage(); // the right line number is available only for unminified template (for validation during saving). if ( true === $is_validation ) { $error_message .= ' Line ' . $e->getLine(); } else { // only real render error should be logged // (we don't need to log the validation attempts). $this->get_logger()->warning( "can't render the twig template, as it contains an error", array( 'unique_id' => $unique_id, 'error' => $error_message, ) ); } $this->print_error_message( $unique_id, $error_message ); // do not include in case of the validation, it doesn't have sense + breaks the error grep regex. if ( $is_debug_mode && false === $is_validation ) { // @phpcs:ignore WordPress.PHP.DevelopmentFunctions echo '
' . esc_html( print_r( $args, true ) ) . '
'; } } $wp_filesystem->delete( $template_file ); } public function create_templates_dir(): void { $templates_dir = $this->uploads_folder; $wp_filesystem = $this->get_wp_filesystem(); // skip if already exists. if ( true === $wp_filesystem->is_dir( $templates_dir ) ) { return; } $is_created_dir = $wp_filesystem->mkdir( $templates_dir, 0755 ); if ( false === $is_created_dir ) { $this->get_logger()->warning( "can't create the templates directory", array( 'path' => $templates_dir, ) ); return; } $wp_filesystem->put_contents( $templates_dir . '/readme.txt', 'This directory is used by the Advanced Views plugin to store logs and temporarily store Twig templates during execution.' ); $wp_filesystem->put_contents( $templates_dir . '/index.php', 'put_contents( $templates_dir . '/.htaccess', "Order Deny,Allow\nDeny from all\n" ); } public function remove_templates_dir(): void { // do not remove if switching versions. // Because activation hooks won't be called, so dir will be missing. if ( true === $this->plugin->is_switching_versions() ) { return; } $wp_filesystem = $this->get_wp_filesystem(); $templates_dir = $this->uploads_folder; if ( false === $wp_filesystem->is_dir( $templates_dir ) ) { return; } // remove the dir. $wp_filesystem->rmdir( $templates_dir, true ); } public function show_templates_dir_is_not_writable_warning(): void { $screen = get_current_screen(); // show only on the list pages of Views & Cards. if ( null === $screen || ! in_array( $screen->post_type, array( Views_Cpt::NAME, Cards_Cpt::NAME ), true ) || 'edit' !== $screen->base ) { return; } if ( true === $this->is_templates_dir_writable() ) { return; } echo '

'; echo esc_html( __( 'The templates directory is not writable.', 'acf-views' ) ); echo ' (path = ' . esc_html( $this->uploads_folder ) . ')
'; echo esc_html( __( 'Most likely, the WordPress uploads directory is not writable.', 'acf-views' ) ) . '
'; echo esc_html( __( 'Check and fix file permissions, then deactivate and activate back the Advanced Views plugin. If the issue persists, contact support.', 'acf-views' ) ); echo '

'; } public function set_hooks( Current_Screen $current_screen ): void { if ( false === $current_screen->is_admin() ) { return; } add_action( 'admin_notices', array( $this, 'show_templates_dir_is_not_writable_warning' ) ); } }