'string', 'single' => true, 'default' => '', 'show_in_rest' => true, 'revisions_enabled' => post_type_supports( $typ, 'revisions' ), 'auth_callback' => array( __CLASS__, 'can_edit' ), 'sanitize_callback' => self::META_BOX === $schluessel ? array( __CLASS__, 'sanitize_box_meta' ) : array( __CLASS__, 'sanitize_quick_meta' ), ) ); } } } /** * Recht für Meta über REST: genau dieser Beitrag muss bearbeitbar sein. * * @param bool $erlaubt Vorschlag des Kerns. * @param string $meta_key Schlüssel. * @param int $post_id Beitrag. * @return bool */ public static function can_edit( $erlaubt, $meta_key, $post_id ) { return current_user_can( 'edit_post', (int) $post_id ); } /** * Sanitize-Callback für _threerb_box. * * @param mixed $wert JSON oder Array. * @return string */ public static function sanitize_box_meta( $wert ) { $roh = is_string( $wert ) ? json_decode( $wert, true ) : $wert; if ( ! is_array( $roh ) ) { return ''; } return (string) wp_json_encode( self::clean( $roh )['box'], JSON_PRESERVE_ZERO_FRACTION ); } /** * Sanitize-Callback für _threerb_quick. * * @param mixed $wert JSON oder Array. * @return string */ public static function sanitize_quick_meta( $wert ) { $roh = is_string( $wert ) ? json_decode( $wert, true ) : $wert; if ( ! is_array( $roh ) ) { return ''; } return (string) wp_json_encode( self::quick_clean( $roh ), JSON_PRESERVE_ZERO_FRACTION ); } /** * Vorgaben der ausführlichen Box. * * @return array */ public static function defaults() { return array( 'v' => self::VERSION, 'item' => array( 'type' => 'product', 'sub' => '', 'url' => '', 'seller' => false, 'foreign' => false, 'date' => '', ), 'scale' => ThreeRB_Settings::value( 'scale' ), 'step' => (float) ThreeRB_Settings::value( 'step' ), 'criteria' => array(), 'override' => array( 'value' => null, 'reason' => '', ), 'pros' => array(), 'cons' => array(), 'verdict' => '', 'good_for' => array(), 'bad_for' => array(), 'tested' => '', 'alternatives' => array(), 'links' => array(), 'disclosure' => array( 'loan' => false, 'gift' => false, 'paid' => false, 'affiliate' => false, 'text' => '', ), 'schema' => true, 'display' => 'default', 'style' => 'default', 'heading' => 'default', ); } /** * Vorgaben des Schnellbereichs (Stern). * * @return array */ public static function quick_defaults() { return array( 'score' => null, 'name' => '', 'summary' => '', 'place' => array( 'top' => (bool) ThreeRB_Settings::value( 'summary_top' ), 'bottom' => 'bottom' === ThreeRB_Settings::value( 'position' ), 'sidebar' => false, ), ); } /** * Bereinigt eine ausführliche Box. Unbekannte Felder werden verworfen. * * @param array $roh Eingabe. * @return array{box:array, errors:array} */ public static function clean( $roh ) { $roh = is_array( $roh ) ? $roh : array(); $box = self::defaults(); $fehler = array(); $item = isset( $roh['item'] ) && is_array( $roh['item'] ) ? $roh['item'] : array(); if ( isset( $item['type'] ) && ( 'none' === $item['type'] || array_key_exists( $item['type'], ThreeRB_Schema::types() ) ) ) { $box['item']['type'] = $item['type']; } $box['item']['sub'] = self::text( isset( $item['sub'] ) ? $item['sub'] : '', 120 ); $box['item']['url'] = self::url( isset( $item['url'] ) ? $item['url'] : '' ); $box['item']['seller'] = ThreeRB_Settings::bool( isset( $item['seller'] ) ? $item['seller'] : false ); $box['item']['foreign'] = ThreeRB_Settings::bool( isset( $item['foreign'] ) ? $item['foreign'] : false ); if ( isset( $item['date'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', (string) $item['date'] ) ) { $box['item']['date'] = (string) $item['date']; } if ( isset( $roh['scale'] ) && null !== ThreeRB_Score::max( $roh['scale'] ) ) { $box['scale'] = $roh['scale']; } $schritte = ThreeRB_Score::scales()[ $box['scale'] ]['steps']; $schritt = isset( $roh['step'] ) ? self::zahl( $roh['step'] ) : null; $box['step'] = ( null !== $schritt && in_array( $schritt, $schritte, true ) ) ? $schritt : ( in_array( (float) $box['step'], $schritte, true ) ? (float) $box['step'] : $schritte[0] ); $kriterien = isset( $roh['criteria'] ) && is_array( $roh['criteria'] ) ? array_values( $roh['criteria'] ) : array(); foreach ( $kriterien as $i => $k ) { if ( count( $box['criteria'] ) >= 12 ) { $fehler[] = 'criteria.max'; break; } if ( ! is_array( $k ) ) { $fehler[] = "criteria.$i"; continue; } $label = self::text( isset( $k['label'] ) ? $k['label'] : '', 60 ); $leer = '' === $label && '' === trim( (string) ( isset( $k['score'] ) ? self::skalar( $k['score'] ) : '' ) ) && '' === trim( (string) ( isset( $k['weight'] ) ? self::skalar( $k['weight'] ) : '' ) ); if ( $leer ) { continue; } if ( '' === $label ) { $fehler[] = "criteria.$i.label"; continue; } $gewicht = isset( $k['weight'] ) ? self::zahl( $k['weight'] ) : null; if ( null === $gewicht || $gewicht < 0 || $gewicht > 100 || floor( $gewicht ) !== $gewicht ) { $fehler[] = "criteria.$i.weight"; $gewicht = 0; } $note = isset( $k['score'] ) ? ThreeRB_Score::snap( self::zahl( $k['score'] ), $box['scale'], $box['step'] ) : null; if ( null === $note ) { $fehler[] = "criteria.$i.score"; } $box['criteria'][] = array( 'label' => $label, 'weight' => (int) $gewicht, 'score' => $note, 'measure' => self::text( isset( $k['measure'] ) ? $k['measure'] : '', 40 ), 'note' => self::text( isset( $k['note'] ) ? $k['note'] : '', 200 ), ); } $ueber = isset( $roh['override'] ) && is_array( $roh['override'] ) ? $roh['override'] : array(); $grund = self::text( isset( $ueber['reason'] ) ? $ueber['reason'] : '', 200 ); $wert = isset( $ueber['value'] ) ? ThreeRB_Score::snap( self::zahl( $ueber['value'] ), $box['scale'], $box['step'] ) : null; if ( null !== $wert && mb_strlen( $grund ) >= 10 ) { $box['override'] = array( 'value' => $wert, 'reason' => $grund, ); } elseif ( null !== $wert ) { $fehler[] = 'override.reason'; } $box['pros'] = self::liste( isset( $roh['pros'] ) ? $roh['pros'] : array(), 10, 140, "\n" ); $box['cons'] = self::liste( isset( $roh['cons'] ) ? $roh['cons'] : array(), 10, 140, "\n" ); $box['good_for'] = self::liste( isset( $roh['good_for'] ) ? $roh['good_for'] : array(), 6, 40, ',' ); $box['bad_for'] = self::liste( isset( $roh['bad_for'] ) ? $roh['bad_for'] : array(), 6, 40, ',' ); $box['tested'] = self::text( isset( $roh['tested'] ) ? $roh['tested'] : '', 300 ); $fazit = isset( $roh['verdict'] ) ? wp_kses( (string) self::skalar( $roh['verdict'] ), self::verdict_html() ) : ''; if ( mb_strlen( wp_strip_all_tags( $fazit ) ) > 600 ) { $fehler[] = 'verdict'; $fazit = esc_html( mb_substr( wp_strip_all_tags( $fazit ), 0, 600 ) ); } $box['verdict'] = trim( $fazit ); $alternativen = isset( $roh['alternatives'] ) ? $roh['alternatives'] : array(); if ( is_string( $alternativen ) ) { $alternativen = array_filter( array_map( 'trim', explode( "\n", str_replace( "\r", '', $alternativen ) ) ) ); } $alternativen = is_array( $alternativen ) ? array_values( $alternativen ) : array(); foreach ( $alternativen as $a ) { if ( count( $box['alternatives'] ) >= 5 ) { break; } if ( is_string( $a ) ) { $teile = array_map( 'trim', explode( '|', $a, 2 ) ); $a = array( 'label' => $teile[0], 'url' => isset( $teile[1] ) ? $teile[1] : '', ); } if ( ! is_array( $a ) ) { continue; } $name = self::text( isset( $a['label'] ) ? $a['label'] : '', 60 ); if ( '' === $name ) { continue; } $box['alternatives'][] = array( 'label' => $name, 'url' => self::url( isset( $a['url'] ) ? $a['url'] : '' ), ); } $links = isset( $roh['links'] ) && is_array( $roh['links'] ) ? array_values( $roh['links'] ) : array(); foreach ( $links as $i => $l ) { if ( count( $box['links'] ) >= 5 ) { $fehler[] = 'links.max'; break; } if ( ! is_array( $l ) ) { continue; } $text = self::text( isset( $l['label'] ) ? $l['label'] : '', 40 ); $ziel = self::url( isset( $l['url'] ) ? $l['url'] : '' ); if ( '' === $text && '' === trim( (string) ( isset( $l['url'] ) ? self::skalar( $l['url'] ) : '' ) ) ) { continue; } if ( '' === $ziel ) { $fehler[] = "links.$i.url"; continue; } $box['links'][] = array( 'label' => '' === $text ? wp_parse_url( $ziel, PHP_URL_HOST ) : $text, 'url' => $ziel, 'primary' => ThreeRB_Settings::bool( isset( $l['primary'] ) ? $l['primary'] : false ), 'affiliate' => ThreeRB_Settings::bool( isset( $l['affiliate'] ) ? $l['affiliate'] : false ), ); } $hinweis = isset( $roh['disclosure'] ) && is_array( $roh['disclosure'] ) ? $roh['disclosure'] : array(); foreach ( array( 'loan', 'gift', 'paid', 'affiliate' ) as $art ) { $box['disclosure'][ $art ] = ThreeRB_Settings::bool( isset( $hinweis[ $art ] ) ? $hinweis[ $art ] : false ); } $box['disclosure']['text'] = self::text( isset( $hinweis['text'] ) ? $hinweis['text'] : '', 200 ); if ( array_key_exists( 'schema', $roh ) ) { $box['schema'] = ThreeRB_Settings::bool( $roh['schema'] ); } if ( isset( $roh['heading'] ) && in_array( $roh['heading'], array( 'default', 'h2', 'h3', 'h4' ), true ) ) { $box['heading'] = $roh['heading']; } if ( isset( $roh['display'] ) && in_array( $roh['display'], array( 'default', 'bars', 'stars', 'profile' ), true ) ) { $box['display'] = $roh['display']; } if ( isset( $roh['style'] ) && in_array( $roh['style'], array( 'default', 'theme', 'night' ), true ) ) { $box['style'] = $roh['style']; } return array( 'box' => $box, 'errors' => $fehler, ); } /** * Bereinigt den Schnellbereich (Stern). * * @param array $roh Eingabe. * @return array */ public static function quick_clean( $roh ) { $roh = is_array( $roh ) ? $roh : array(); $q = self::quick_defaults(); $note = isset( $roh['score'] ) ? self::zahl( $roh['score'] ) : null; $q['score'] = ( null !== $note && $note >= 0 && $note <= 100 ) ? round( $note, 3 ) : null; $q['name'] = self::text( isset( $roh['name'] ) ? $roh['name'] : '', 120 ); $q['summary'] = self::text( isset( $roh['summary'] ) ? $roh['summary'] : '', 220 ); $platz = isset( $roh['place'] ) && is_array( $roh['place'] ) ? $roh['place'] : array(); foreach ( array_keys( $q['place'] ) as $wo ) { if ( array_key_exists( $wo, $platz ) ) { $q['place'][ $wo ] = ThreeRB_Settings::bool( $platz[ $wo ] ); } } return $q; } /** * Speichert die ausführliche Box (bereits bereinigt). * * @param int $post_id Beitrag. * @param array $box Box. */ public static function save( $post_id, array $box ) { update_post_meta( $post_id, self::META_BOX, wp_slash( (string) wp_json_encode( $box, JSON_PRESERVE_ZERO_FRACTION ) ) ); } /** * Speichert den Schnellbereich (bereits bereinigt). * * @param int $post_id Beitrag. * @param array $quick Schnellbereich. */ public static function save_quick( $post_id, array $quick ) { update_post_meta( $post_id, self::META_QUICK, wp_slash( (string) wp_json_encode( $quick, JSON_PRESERVE_ZERO_FRACTION ) ) ); } /** * Box eines Beitrags, zusammengesetzt aus beiden Feldern, oder null. * * @param int $post_id Beitrag. * @return array|null */ public static function get( $post_id ) { $box_json = get_post_meta( (int) $post_id, self::META_BOX, true ); $quick_json = get_post_meta( (int) $post_id, self::META_QUICK, true ); if ( '' === $box_json && '' === $quick_json ) { return null; } $box = self::clean( is_string( $box_json ) ? json_decode( $box_json, true ) : array() )['box']; $quick = self::quick_clean( is_string( $quick_json ) ? json_decode( $quick_json, true ) : array() ); $box['score'] = null !== $quick['score'] ? ThreeRB_Score::snap( $quick['score'], $box['scale'], $box['step'] ) : null; $box['item']['name'] = $quick['name']; $box['summary'] = $quick['summary']; $box['place'] = $quick['place']; return $box; } /** * Hat die Box eine Note? * * @param array|null $box Box. * @return bool */ public static function has_rating( $box ) { return is_array( $box ) && null !== ThreeRB_Score::total( $box )['value']; } /** * Spiegelfelder für Sortierung und Spalten nachziehen, wenn sich die Box ändert. * * @param int $meta_id Meta-ID. * @param int $post_id Beitrag. * @param string $meta_key Schlüssel. */ public static function on_meta_change( $meta_id, $post_id, $meta_key ) { if ( self::META_BOX !== $meta_key && self::META_QUICK !== $meta_key ) { return; } self::mirror( (int) $post_id ); } /** * Spiegelfelder schreiben oder entfernen. * * @param int $post_id Beitrag. */ public static function mirror( $post_id ) { $box = self::get( $post_id ); $gesamt = $box ? ThreeRB_Score::total( $box ) : array( 'value' => null ); if ( null === $gesamt['value'] ) { delete_post_meta( $post_id, self::META_SCORE ); delete_post_meta( $post_id, self::META_SCALE ); delete_post_meta( $post_id, self::META_TYPE ); return; } update_post_meta( $post_id, self::META_SCORE, (int) round( $gesamt['share'] * 1000 ) ); update_post_meta( $post_id, self::META_SCALE, $box['scale'] ); update_post_meta( $post_id, self::META_TYPE, $box['item']['type'] ); } /** * Erlaubtes HTML im Fazit. * * @return array */ private static function verdict_html() { return array( 'p' => array(), 'br' => array(), 'strong' => array(), 'em' => array(), 'a' => array( 'href' => true ), ); } /** * Einzeiliger Text, gekürzt. * * @param mixed $wert Wert. * @param int $laenge Höchstlänge. * @return string */ private static function text( $wert, $laenge ) { return mb_substr( trim( sanitize_text_field( (string) self::skalar( $wert ) ) ), 0, $laenge ); } /** * Adresse, nur http und https. * * @param mixed $wert Wert. * @return string */ private static function url( $wert ) { $wert = trim( (string) self::skalar( $wert ) ); if ( '' === $wert || ! preg_match( '#^https?://#i', $wert ) ) { return ''; } return mb_substr( esc_url_raw( $wert, array( 'http', 'https' ) ), 0, 300 ); } /** * Zahl mit Punkt oder Komma, sonst null. * * @param mixed $wert Wert. * @return float|null */ private static function zahl( $wert ) { if ( is_int( $wert ) || is_float( $wert ) ) { return is_finite( (float) $wert ) ? (float) $wert : null; } $wert = str_replace( ',', '.', trim( (string) self::skalar( $wert ) ) ); return preg_match( '/^-?\d+(\.\d+)?$/', $wert ) ? (float) $wert : null; } /** * Nur Skalare durchlassen. * * @param mixed $wert Wert. * @return string|int|float */ private static function skalar( $wert ) { return is_scalar( $wert ) ? $wert : ''; } /** * Liste aus Array oder Text mit Trennzeichen. * * @param mixed $wert Wert. * @param int $anzahl Höchstanzahl. * @param int $laenge Höchstlänge je Eintrag. * @param string $trenner Trennzeichen bei Text. * @return array */ private static function liste( $wert, $anzahl, $laenge, $trenner ) { if ( is_string( $wert ) ) { $wert = explode( $trenner, str_replace( "\r", '', $wert ) ); } if ( ! is_array( $wert ) ) { return array(); } $ergebnis = array(); foreach ( $wert as $eintrag ) { $eintrag = self::text( $eintrag, $laenge ); if ( '' !== $eintrag ) { $ergebnis[] = $eintrag; } if ( count( $ergebnis ) >= $anzahl ) { break; } } return $ergebnis; } }