0, 'id' => 0, 'status' => '', 'per_page' => 0, 'page' => '', ), $raw, 'entrystage' ); $fixed = Query::shortcode_filters( $raw ); $atts['user_filter'] = $fixed['user']; $atts['fixed'] = $fixed['fields']; $form_id = absint( $atts['form'] ? $atts['form'] : $atts['id'] ); $form = $form_id ? \GFAPI::get_form( $form_id ) : false; if ( ! $form || empty( $form['is_active'] ) || ! empty( $form['is_trash'] ) ) { return ''; } $settings = Settings::get( $form ); if ( '' !== trim( (string) $atts['page'] ) ) { // page="…": form page of this list (page id, path or address of this site). list( $page_id, $page_url ) = Settings::resolve_page( $atts['page'] ); if ( $page_id || '' !== $page_url ) { $settings['page_id'] = $page_id; $settings['page_url'] = $page_url; } } if ( ! Access::can_see_list( $settings ) ) { return is_user_logged_in() || empty( $settings['enabled'] ) ? '' : '

' . esc_html__( 'Please log in to see your entries.', 'entrystage' ) . '

'; } Addon::get_instance()->enqueue_front_assets(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view. $view_id = isset( $_GET['estg_view'], $_GET['estg_f'] ) && absint( $_GET['estg_f'] ) === $form_id ? absint( $_GET['estg_view'] ) : 0; if ( $view_id ) { $entry = \GFAPI::get_entry( $view_id ); if ( ! is_wp_error( $entry ) && (int) $entry['form_id'] === $form_id && Access::can( 'view', $entry, $settings ) ) { return self::entry_view( $form, $entry, $settings ); } } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view. if ( ! empty( $settings['print'] ) && isset( $_GET['estg_print'] ) && absint( $_GET['estg_print'] ) === $form_id ) { return self::print_html( $form, $settings, $atts ); } return self::list_html( $form, $settings, $atts ); } /** * Personal space: several forms, one tab per form with its number of entries; the tab shows the * list of that form as set in its settings. [entrystage forms="12,15" user="current"] * * @param array $raw Shortcode attributes. * @return string */ private static function space( array $raw ) { $ids = array_values( array_unique( array_filter( array_map( 'absint', preg_split( '/[\s,]+/', (string) $raw['forms'] ) ) ) ) ); unset( $raw['forms'], $raw['form'], $raw['id'], $raw['page'] ); $fixed = Query::shortcode_filters( $raw ); $tabs = array(); foreach ( array_slice( $ids, 0, 20 ) as $id ) { $form = \GFAPI::get_form( $id ); if ( ! $form || empty( $form['is_active'] ) || ! empty( $form['is_trash'] ) ) { continue; } $settings = Settings::get( $form ); if ( empty( $settings['enabled'] ) || ! Access::can_see_list( $settings ) ) { continue; } // number of entries as the list opens (without the search of the visitor). $state = Query::state( $form, $settings, array(), array( 'defaults' => true, 'user_filter' => $fixed['user'], 'fixed' => $fixed['fields'], ) ); $count = Query::run( $form, $settings, array_merge( $state, array( 'page' => 1, 'per_page' => 1 ) ) ); $tabs[ $id ] = array( 'title' => (string) $form['title'], 'count' => (int) $count['total'], ); } if ( ! $tabs ) { return is_user_logged_in() ? '' : '

' . esc_html__( 'Please log in to see your entries.', 'entrystage' ) . '

'; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only view. $active = isset( $_GET['estg_tab'] ) ? absint( $_GET['estg_tab'] ) : 0; if ( ! isset( $tabs[ $active ] ) && isset( $_GET['estg_f'] ) ) { $active = absint( $_GET['estg_f'] ); // back from an action or a page of a list. } // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( ! isset( $tabs[ $active ] ) ) { $active = (int) array_key_first( $tabs ); } /** * Filters the tabs of a personal space (form id => title and count). * * @param array $tabs Tabs. * @param int $active Form id of the tab shown. */ $tabs = (array) apply_filters( 'estg_space_tabs', $tabs, $active ); $h = '
'; if ( count( $tabs ) > 1 ) { $h .= ''; } $h .= self::shortcode( array_merge( $raw, array( 'form' => $active ) ) ); return $h . '
'; } /** Most entries on one printed list. */ const PRINT_LIMIT = 500; /** * Printable list: the entries matching the filters, every page, without buttons. front.js opens the * print dialog once the page is loaded. * * @param array $form Form object. * @param array $settings Form settings. * @param array $atts Shortcode attributes. * @return string */ private static function print_html( $form, array $settings, array $atts ) { $form_id = (int) $form['id']; $layout = Settings::layout( $form, $settings ); $state = Query::state( $form, $settings, array(), $atts ); $back = Urls::list_url( $form_id, $state ); $state = array_merge( $state, array( 'page' => 1, 'per_page' => self::PRINT_LIMIT, ) ); $result = Query::run( $form, $settings, $state ); $title = get_the_title(); $title = '' !== trim( wp_strip_all_tags( (string) $title ) ) ? $title : $form['title']; $h = '
'; $h .= '
'; $h .= ' ← ' . esc_html__( 'Back to the list', 'entrystage' ) . '
'; $h .= '

' . esc_html( wp_strip_all_tags( (string) $title ) ) . '

'; $info = array( /* translators: %s: date and time */ sprintf( __( 'Printed on %s', 'entrystage' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ), /* translators: %s: number of entries */ sprintf( _n( '%s entry', '%s entries', $result['total'], 'entrystage' ), number_format_i18n( $result['total'] ) ), ); if ( '' !== (string) $state['search'] ) { /* translators: %s: text searched */ $info[] = sprintf( __( 'Search: %s', 'entrystage' ), $state['search'] ); } $h .= '

' . esc_html( implode( ' · ', $info ) ) . '

'; if ( $result['total'] > self::PRINT_LIMIT ) { /* translators: %d: most entries printed */ $h .= '

' . esc_html( sprintf( __( 'Only the first %d entries are printed. Narrow the list with the filters to print the others.', 'entrystage' ), self::PRINT_LIMIT ) ) . '

'; } if ( empty( $result['entries'] ) ) { return $h . '

' . esc_html( $settings['empty_text'] ) . '

'; } $h .= ''; foreach ( $layout as $item ) { $h .= ''; } $h .= ''; foreach ( $result['entries'] as $entry ) { $h .= ''; foreach ( $layout as $item ) { $class = 'estg-col-' . sanitize_html_class( $item['key'] ); if ( 'field' === $item['kind'] ) { $h .= ''; } else { $h .= ''; } } $h .= ''; } return $h . '
' . esc_html( $item['label'] ) . '
' . Fields::render( $form, $entry, $item['column']['field'], $item['mode'], $settings ) . '' . esc_html( \GFCommon::format_date( $entry['date_created'], false, '', false ) ) . '
'; } /** * The list. * * @param array $form Form object. * @param array $settings Form settings. * @param array $atts Shortcode attributes. * @return string */ private static function list_html( $form, array $settings, array $atts ) { $form_id = (int) $form['id']; $layout = Settings::layout( $form, $settings ); $state = Query::state( $form, $settings, array(), $atts ); // the board (EntryStage Pro) fetches its entries column by column. $result = empty( $state['board'] ) ? Query::run( $form, $settings, $state ) : array( 'entries' => array(), 'total' => 0, ); $here = Urls::list_url( $form_id, $state ); $with_bulk = false; $more = false; ob_start(); echo '
'; do_action( 'estg_before_list', $form, $settings, $state ); if ( ( ! empty( $settings['edit'] ) || ! empty( $settings['duplicate'] ) ) && empty( $settings['page_id'] ) && empty( $settings['page_url'] ) && Access::is_manager( $settings ) ) { // Only managers see this hint. echo '
' . esc_html__( 'Edit and Duplicate are hidden: no page displaying this form was found. Choose it in the form settings, EntryStage tab, "Page containing the form".', 'entrystage' ) . '
'; } echo self::notice( $form_id ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in notice(). echo self::toolbar( $form, $settings, $state, $layout ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in toolbar(). if ( empty( $result['entries'] ) ) { echo '

' . esc_html( $settings['empty_text'] ) . '

'; } else { echo '
'; wp_nonce_field( Urls::nonce_action( 'list', $form_id ), 'estg_list_nonce' ); echo ''; echo ''; $fixed = false; foreach ( $layout as $item ) { $fixed = $fixed || '' !== $item['width']; } // a width in % is a share of the space left by the actions, the chevron and the boxes // (applied by front.js, data-estg-share), so that columns adding up to 100 % (or less) never push // the table out of its container. Without JavaScript the % applies to the whole table. echo '
'; if ( $fixed ) { // Chosen widths are applied with a fixed table layout so that themes cannot override them; // columns without a width share the remaining space. echo ''; foreach ( $layout as $item ) { $share = self::share( $item['width'] ); echo '' !== $item['width'] ? '' : ''; } echo ''; echo ''; } echo ''; foreach ( $layout as $item ) { $style = '' !== $item['width'] ? ' style="width:' . esc_attr( $item['width'] ) . '"' : ''; $head = '' !== $item['sort_key'] ? self::sort_link( $form_id, $state, $item['sort_key'], $item['label'] ) : '' . esc_html( $item['label'] ) . ''; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above and in sort_link(). } // at most 25 % of the table (front.js, data-estg-em); without JavaScript, the CSS max-width. echo ''; echo ''; $span = count( $layout ) + 1 + ( $with_bulk ? 1 : 0 ) + ( $more ? 1 : 0 ); foreach ( $result['entries'] as $entry ) { echo self::row( $form, $entry, $settings, $layout, $with_bulk, $here, $more ? $span : 0 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in row(). } echo '
' . $head . '' . esc_html( $settings['actions_header'] ) . '
'; echo self::pagination( $form_id, $state, $result['total'], $settings ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in pagination(). } do_action( 'estg_after_list', $form, $settings, $state ); echo '
'; return (string) ob_get_clean(); } /** * One row. * * @param array $form Form object. * @param array $entry Entry. * @param array $settings Form settings. * @param array $layout Columns in display order. * @param bool $with_bulk Show the selection box. * @param string $here Current list address. * @param int $span Number of cells of a row when "More details" is used, 0 otherwise. * @return string */ private static function row( $form, $entry, array $settings, array $layout, $with_bulk, $here, $span = 0 ) { $id = (int) $entry['id']; $classes = (array) apply_filters( 'estg_row_classes', array( 'estg-row' ), $entry, $settings ); $style = ''; $h = ''; foreach ( $layout as $item ) { $class = 'estg-col-' . sanitize_html_class( $item['key'] ); if ( 'field' === $item['kind'] ) { $h .= '' . Fields::render( $form, $entry, $item['column']['field'], $item['mode'], $settings ) . ''; } else { $h .= '' . esc_html( \GFCommon::format_date( $entry['date_created'], false, '', false ) ) . ''; } } $acts = self::row_actions( $entry, $settings, $here ); $top = ''; $h .= '' . $top . '
' . implode( ' ', $acts ) . '
'; return $h . ''; } /** * Action links of a row. * * @param array $entry Entry. * @param array $settings Form settings. * @param string $here Current list address. * @return string[] Safe HTML. */ private static function row_actions( $entry, array $settings, $here ) { $id = (int) $entry['id']; $form_id = (int) $entry['form_id']; $out = array(); if ( Access::can( 'view', $entry, $settings ) ) { $url = add_query_arg( array( 'estg_f' => $form_id, 'estg_view' => $id, ), $here ); $out[] = '' . self::act_inner( $settings, 'view' ) . ''; } foreach ( array( 'edit', 'duplicate' ) as $action ) { if ( Access::can( $action, $entry, $settings ) ) { $url = Urls::form_url( $settings, $action, $id, $here ); if ( '' !== $url ) { $out[] = '' . self::act_inner( $settings, $action ) . ''; } } } if ( Access::can( 'archive', $entry, $settings ) ) { $out[] = ''; } /** * Filters the action links of a row (each item is safe HTML). * * @param string[] $out Links. * @param array $entry Entry. * @param array $settings Form settings. */ return (array) apply_filters( 'estg_row_actions', $out, $entry, $settings ); } /** * Share of a column whose width is a percentage, for its data-estg-share attribute (read by front.js). * * @param string $width Clean width (Settings::clean_width()). * @return string The share (for example "0.15"), or '' when the width is not a percentage. */ private static function share( $width ) { return '%' === substr( $width, -1 ) ? (string) round( (float) $width / 100, 4 ) : ''; } /** * Width kept for the actions column when the table layout is fixed. * * @param array $settings Form settings. * @return string CSS width. */ private static function actions_width( array $settings ) { $count = 0; $actions = array( 'view', 'edit', 'duplicate', 'archive' ); foreach ( $actions as $action ) { $count += ! empty( $settings[ $action ] ) ? 1 : 0; } $count = max( 1, $count ); $per = array( 'icons' => 2.6, 'both' => 7.5, 'text' => 6, ); $em = $count * ( isset( $per[ $settings['action_display'] ] ) ? $per[ $settings['action_display'] ] : 2.6 ) + 2; // + cell padding (1.4em). return max( 6, $em ) . 'em'; } /** * Tooltip and accessible name of an action button (icons only). * * @param array $settings Form settings. * @param string $action Action. * @return string */ private static function act_attrs( array $settings, $action ) { return 'icons' === $settings['action_display'] ? ' title="' . esc_attr( $settings[ $action . '_label' ] ) . '"' : ''; } /** * Content of an action button: icon, text or both, as set for the form. * * @param array $settings Form settings. * @param string $action Action. * @return string */ private static function act_inner( array $settings, $action ) { $display = $settings['action_display']; $icon = 'text' !== $display ? self::action_icon( $settings, $action ) : ''; $class = 'icons' === $display ? 'screen-reader-text' : 'estg-act-text'; return $icon . '' . esc_html( $settings[ $action . '_label' ] ) . ''; } /** * Icon of an action button: the picture chosen in the settings, or the built-in icon. * * @param array $settings Form settings. * @param string $action view | edit | duplicate | archive | pdf | next. * @return string */ private static function action_icon( array $settings, $action ) { return self::icon( $action ); } /** * Inline SVG icon (drawn with the text colour). * * @param string $name view | edit | duplicate | archive | pdf | next | export | download | file | chevron. * @return string */ public static function icon( $name ) { $paths = array( 'view' => '', 'edit' => '', 'duplicate' => '', 'archive' => '', 'pdf' => '', 'next' => '', 'export' => '', 'download' => '', 'file' => '', 'chevron' => '', 'search' => '', 'print' => '', 'list' => '', 'board' => '', ); /** * Filters the SVG content of an icon (the inner elements of a 24x24 ). * * @param string $svg SVG elements. * @param string $name Icon name. */ $inner = (string) apply_filters( 'estg_icon', isset( $paths[ $name ] ) ? $paths[ $name ] : '', $name ); if ( '' === $inner ) { return ''; } return ''; } /** * Column header with sort link. * * @param int $form_id Form id. * @param array $state List state. * @param string $key Sort key. * @param string $label Label. * @return string */ private static function sort_link( $form_id, array $state, $key, $label ) { $active = (string) $state['orderby'] === (string) $key; $dir = $active && 'ASC' === $state['dir'] ? 'DESC' : 'ASC'; $url = Urls::list_url( $form_id, $state, array( 'estg_o' => $key, 'estg_d' => $dir, 'estg_p' => null, ) ); $arrow = $active ? ( 'ASC' === $state['dir'] ? ' ↑' : ' ↓' ) : ''; return '' . esc_html( $label ) . $arrow . ''; } /** * Search, filters, page size and export. * * @param array $form Form object. * @param array $settings Form settings. * @param array $state List state. * @param array $layout Columns (Settings::layout()). * @return string */ private static function toolbar( $form, array $settings, array $state, array $layout ) { $form_id = (int) $form['id']; $has_tools = ! empty( $settings['search'] ); $export = false; $print = ! empty( $settings['print'] ); $switch = false; if ( ! $has_tools && ! $export && ! $print && ! $switch ) { return ''; } $uid = 'estg-' . $form_id; $active = '' !== (string) $state['search']; $h = '
'; if ( ! empty( $settings['search'] ) ) { $h .= ''; } $h .= '
'; if ( $active ) { $clear = array_merge( $state, array( 'search' => '', 'from' => '', 'to' => '', 'filters' => array(), 'page' => 1, ) ); $h .= '' . esc_html__( 'Clear the filters', 'entrystage' ) . ''; } $h .= '
'; } if ( $export || $print || $switch ) { $h .= '
'; } if ( $print ) { $h .= '' . self::icon( 'print' ) . '' . esc_html__( 'Print', 'entrystage' ) . ''; } if ( $export || $print || $switch ) { $h .= '
'; } return $h . ''; } /** * Rows per page, next to the pagination. Changing it reloads the list (front.js); the * button is there for browsers without JavaScript. * * @param int $form_id Form id. * @param array $state List state. * @param array $settings Form settings. * @return string */ private static function per_page( $form_id, array $state, array $settings ) { if ( count( $settings['per_page_choices'] ) < 2 ) { return ''; } $args = array(); wp_parse_str( (string) wp_parse_url( Urls::list_url( $form_id, array_merge( $state, array( 'page' => 1 ) ), array( 'estg_pp' => null ) ), PHP_URL_QUERY ), $args ); $h = '
'; foreach ( $args as $name => $value ) { foreach ( is_array( $value ) ? $value : array( '' => $value ) as $sub => $v ) { if ( is_scalar( $v ) ) { $h .= ''; } } } $h .= '
'; return $h; } /** * Pagination. * * @param int $form_id Form id. * @param array $state List state. * @param int $total Total entries. * @param array $settings Form settings. * @return string */ private static function pagination( $form_id, array $state, $total, array $settings ) { $pages = (int) ceil( $total / max( 1, $state['per_page'] ) ); $h = ''; } /** * Message after an action (from the redirect). * * @param int $form_id Form id. * @return string */ private static function notice( $form_id ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only. if ( ! isset( $_GET['estg_msg'], $_GET['estg_f'] ) || absint( $_GET['estg_f'] ) !== (int) $form_id ) { return ''; } $code = sanitize_key( wp_unslash( $_GET['estg_msg'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $n = isset( $_GET['estg_n'] ) ? absint( $_GET['estg_n'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $messages = array( /* translators: %d: number of entries */ 'archived' => _n( '%d entry archived.', '%d entries archived.', $n, 'entrystage' ), /* translators: %d: number of entries */ 'duplicated' => _n( '%d entry duplicated.', '%d entries duplicated.', $n, 'entrystage' ), /* translators: %d: number of entries */ 'updated' => __( 'Entry updated.', 'entrystage' ), 'denied' => __( 'This action is not allowed.', 'entrystage' ), 'none' => __( 'No entry selected.', 'entrystage' ), ); if ( ! isset( $messages[ $code ] ) ) { return ''; } $error = in_array( $code, array( 'denied', 'none' ), true ); return '
' . esc_html( sprintf( $messages[ $code ], $n ) ) . '
'; } /** * Read-only view of an entry. * * @param array $form Form object. * @param array $entry Entry. * @param array $settings Form settings. * @return string */ private static function entry_view( $form, $entry, array $settings ) { $back = remove_query_arg( array( 'estg_view' ), Urls::list_url( (int) $form['id'], Query::state( $form, $settings ) ) ); $skip = array( 'html', 'section', 'page', 'captcha', 'password' ); $h = '

← ' . esc_html__( 'Back to the list', 'entrystage' ) . '

'; $h .= '

' . esc_html( sprintf( /* translators: %d: entry id */ __( 'Entry #%d', 'entrystage' ), (int) $entry['id'] ) ) . '

'; $page = array(); if ( $page ) { // the fields and section titles chosen in the settings, in their order; empty values are left out, // and so is a section title followed by no value. $open = false; $pending = ''; foreach ( $page as $item ) { if ( 'section' === $item['type'] ) { $pending = '

' . esc_html( $item['label'] ) . '

'; continue; } $html = Fields::render( $form, $entry, $item['field'], $item['mode'], $settings ); if ( '' === trim( wp_strip_all_tags( (string) $html, true ) ) && false === strpos( (string) $html, '' : '' ) . $pending; $open = false; $pending = ''; } if ( ! $open ) { $h .= '
'; $open = true; } $h .= '
' . esc_html( $item['label'] ) . '
' . wp_kses_post( $html ) . '
'; } $h .= $open ? '
' : ''; } else { $h .= '
'; foreach ( (array) $form['fields'] as $field ) { if ( in_array( $field->type, $skip, true ) || ! empty( rgobj( $field, 'adminOnly' ) ) || in_array( rgobj( $field, 'visibility' ), array( 'hidden', 'administrative' ), true ) ) { continue; } $value = \GFFormsModel::get_lead_field_value( $entry, $field ); $html = Fields::display( $field, $value, $entry, 'html' ); if ( '' === trim( wp_strip_all_tags( (string) $html ) ) ) { continue; } $h .= '
' . esc_html( wp_strip_all_tags( \GFCommon::get_label( $field ) ) ) . '
' . wp_kses_post( $html ) . '
'; } $h .= '
'; } $h .= '
'; return (string) apply_filters( 'estg_entry_view_html', $h, $entry, $form, $settings ); } }