store = $store; } /** * Hidden fields every form posts. * * @param string $action Action without prefix. * @param string $context Context. * @param string $scope site or network. * @param array $extra More hidden fields. * @return void */ public static function hidden( $action, $context, $scope, array $extra = array() ) { echo ''; echo ''; echo ''; foreach ( $extra as $name => $value ) { echo ''; } wp_nonce_field( FormHandler::nonce_action( $action ) ); } /** * Form opening tag posting to admin-post.php. * * @param string $css_class CSS class. * @return void */ public static function open_form( $css_class = '' ) { echo '
'; } /** * A small inline form with one button. * * @param string $action Action. * @param string $context Context. * @param string $scope Scope. * @param array $fields Hidden fields. * @param string $label Button label. * @param string $css_class Button classes. * @return void */ public static function button( $action, $context, $scope, array $fields, $label, $css_class = 'button button-small' ) { self::open_form( 'codo-csp-inline' ); self::hidden( $action, $context, $scope, $fields ); echo ''; } /** * Render the policy of a context. * * @param string $context site or login. * @param string $scope site or network. * @return void */ public function render( $context, $scope ) { $network = $this->store->network( $context ); if ( 'network' === $scope ) { $working = $network; $live = $network; $network = new Policy(); } else { $live = $this->store->local( $context ); $draft = $this->store->local_draft( $context ); $working = null === $draft ? $live : $draft; if ( null !== $draft ) { $this->render_draft_banner( $context, $live, $draft ); } } // Sources copied in when a network default created a directive. $extra = 'network' === $scope ? new Policy() : PolicyStore::compose( $network, $working ); $this->render_table( $context, $scope, $network, $live, $working, $extra ); $this->render_add_form( $context, $scope ); $this->render_presets( $context, $scope ); } /** * The draft summary with promote and discard. * * @param string $context Context. * @param Policy $live Live local rules. * @param Policy $draft Draft local rules. * @return void */ private function render_draft_banner( $context, Policy $live, Policy $draft ) { $added = $draft->diff( $live ); $removed = $live->diff( $draft ); echo '

' . esc_html__( 'A draft is being tested.', 'codo-content-security-policy' ) . ' '; echo esc_html( sprintf( /* translators: %s: date. */ __( 'Since %s, visitors\' browsers have checked the draft in Report-Only alongside the enforced policy. Nothing in the draft is blocked yet. Check the Reports tab for anything only the draft would block, then promote it.', 'codo-content-security-policy' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $this->store->draft_started( $context ) ) ) ); echo '

    '; foreach ( $added as $rule ) { echo '
  • + ' . esc_html( $rule['d'] . ' ' . $rule['s'] ) . '
  • '; } foreach ( $removed as $rule ) { echo '
  • − ' . esc_html( $rule['d'] . ' ' . $rule['s'] ) . '
  • '; } echo '

'; self::button( 'draft', $context, 'site', array( 'op' => 'promote' ), __( 'Promote draft to live', 'codo-content-security-policy' ), 'button button-primary' ); echo ' '; self::button( 'draft', $context, 'site', array( 'op' => 'discard' ), __( 'Discard draft', 'codo-content-security-policy' ), 'button' ); echo '

'; } /** * The rules table. * * @param string $context Context. * @param string $scope Scope. * @param Policy $network Network rules (read-only here). * @param Policy $live Live local rules. * @param Policy $working Draft (or live) local rules. * @param Policy $merged Network and local rules as merged (for inherited sources). * @return void */ private function render_table( $context, $scope, Policy $network, Policy $live, Policy $working, Policy $merged ) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; $any = false; foreach ( Directives::all() as $directive ) { $rows = array(); foreach ( $network->rules() as $rule ) { if ( $rule['d'] === $directive ) { $rows[] = array( $rule, 'network' ); } } foreach ( $merged->rules() as $rule ) { if ( $rule['d'] === $directive && 'inherited' === $rule['o'] && ! $network->has( $rule['d'], $rule['s'] ) && ! $working->has( $rule['d'], $rule['s'] ) ) { $rows[] = array( $rule, 'network-inherited' ); } } foreach ( $working->rules() as $rule ) { if ( $rule['d'] === $directive && ! $network->has( $rule['d'], $rule['s'] ) ) { $rows[] = array( $rule, $live->has( $rule['d'], $rule['s'] ) ? 'live' : 'pending-add' ); } } foreach ( $live->rules() as $rule ) { if ( $rule['d'] === $directive && ! $working->has( $rule['d'], $rule['s'] ) ) { $rows[] = array( $rule, 'pending-remove' ); } } if ( empty( $rows ) ) { continue; } $any = true; $first = true; foreach ( $rows as $row ) { $this->render_row( $context, $scope, $row[0], $row[1], $first ? count( $rows ) : 0 ); $first = false; } } if ( ! $any ) { echo ''; } echo '
' . esc_html__( 'Directive', 'codo-content-security-policy' ) . '' . esc_html__( 'Source', 'codo-content-security-policy' ) . '' . esc_html__( 'Added by', 'codo-content-security-policy' ) . '' . esc_html__( 'Note', 'codo-content-security-policy' ) . '' . esc_html__( 'Actions', 'codo-content-security-policy' ) . '
' . esc_html__( 'No rules yet.', 'codo-content-security-policy' ) . '
'; } /** * One rule row. * * @param string $context Context. * @param string $scope Scope. * @param array $rule Rule. * @param string $state live, network, pending-add or pending-remove. * @param int $rowspan Rows in this directive (0 when not the first). * @return void */ private function render_row( $context, $scope, array $rule, $state, $rowspan ) { echo ''; if ( $rowspan > 0 ) { echo '' . esc_html( $rule['d'] ) . '

' . esc_html( Directives::describe( $rule['d'] ) ) . '

'; } echo '' . esc_html( $rule['s'] ) . ''; if ( 'pending-add' === $state ) { echo ' ' . esc_html__( 'in draft', 'codo-content-security-policy' ) . ''; } elseif ( 'pending-remove' === $state ) { echo ' ' . esc_html__( 'removal in draft', 'codo-content-security-policy' ) . ''; } $warning = Source::warning( $rule['d'], $rule['s'] ); if ( '' !== $warning ) { echo '

' . esc_html( $warning ) . '

'; } echo ''; echo '' . esc_html( self::origin_label( 'network' === $state ? 'network' : $rule['o'] ) ); if ( $rule['t'] > 0 ) { echo '
' . esc_html( wp_date( get_option( 'date_format' ), $rule['t'] ) ) . ''; } echo '' . esc_html( $rule['n'] ) . ''; $fields = array( 'directive' => $rule['d'], 'source' => $rule['s'], ); if ( 'network' === $state ) { echo '' . esc_html__( 'Network default', 'codo-content-security-policy' ) . ''; } elseif ( 'network-inherited' === $state ) { echo '' . esc_html__( 'Kept because a network default created this directive', 'codo-content-security-policy' ) . ''; } elseif ( 'pending-remove' === $state ) { self::button( 'add_rule', $context, $scope, $fields, __( 'Keep', 'codo-content-security-policy' ) ); } else { self::button( 'remove_rule', $context, $scope, $fields, __( 'Remove', 'codo-content-security-policy' ) ); } echo ''; } /** * Human label for a rule's origin. * * @param string $origin Origin. * @return string */ public static function origin_label( $origin ) { if ( 0 === strpos( $origin, 'preset:' ) ) { $presets = Presets::all(); $id = substr( $origin, 7 ); /* translators: %s: preset name. */ return sprintf( __( 'Preset: %s', 'codo-content-security-policy' ), isset( $presets[ $id ] ) ? $presets[ $id ]['label'] : $id ); } $labels = array( 'default' => __( 'Default', 'codo-content-security-policy' ), 'manual' => __( 'Added by hand', 'codo-content-security-policy' ), 'scan' => __( 'From a scan', 'codo-content-security-policy' ), 'import' => __( 'Imported from a scan', 'codo-content-security-policy' ), 'report' => __( 'From a report', 'codo-content-security-policy' ), 'inherited' => __( 'Inherited (fallback)', 'codo-content-security-policy' ), 'network' => __( 'Network default', 'codo-content-security-policy' ), ); return isset( $labels[ $origin ] ) ? $labels[ $origin ] : $origin; } /** * The add form. * * @param string $context Context. * @param string $scope Scope. * @return void */ private function render_add_form( $context, $scope ) { echo '

' . esc_html__( 'Add a source', 'codo-content-security-policy' ) . '

'; self::open_form( 'codo-csp-add' ); self::hidden( 'add_rule', $context, $scope ); $id = 'codo-csp-' . $scope . '-' . $context; echo '


'; echo '


'; echo '

' . esc_html__( 'A host (https://cdn.example.com, *.example.com), a scheme (data:, https:), a keyword (self, none, unsafe-inline, unsafe-eval) or a hash (sha256-...). Pasted URLs keep their path but lose the query string.', 'codo-content-security-policy' ) . '

'; echo '


'; submit_button( __( 'Add source', 'codo-content-security-policy' ), 'secondary', 'submit', false ); echo ''; } /** * The presets form. * * @param string $context Context. * @param string $scope Scope. * @return void */ private function render_presets( $context, $scope ) { echo '

' . esc_html__( 'Add a common service', 'codo-content-security-policy' ) . '

'; echo '

' . esc_html__( 'Adds every source a service needs across directives. Presets only add sources, so they apply straight away even when the policy is enforced.', 'codo-content-security-policy' ) . '

'; self::open_form( 'codo-csp-presets' ); self::hidden( 'preset', $context, $scope ); $id = 'codo-csp-preset-' . $scope . '-' . $context; echo '

'; submit_button( __( 'Add service', 'codo-content-security-policy' ), 'secondary', 'submit', false ); echo '

'; } }