": anything but ">" and quotes, * or a quoted value, which may hold ">" (unclosed, it runs to the end). * A browser does not end a tag at a ">" inside a quoted value. */ private const TAG_BODY = '(?:[^>"\']|"[^"]*(?:"|\z)|\'[^\']*(?:\'|\z))*'; /** * Attributes that take a URL a browser follows or loads. */ private const URL_ATTRIBUTES = 'href|src|action|formaction|xlink:href|data|poster|background|srcdoc'; /** * How deep a value is read (nested arrays, JSON in JSON, blocks in * blocks), so a crafted value cannot exhaust the stack. */ private const MAX_DEPTH = 32; /** * Characters of a piece of code quoted in a refusal or an approval. */ private const QUOTE = 120; /** * Whether this site's plan lets the tools add code: the paid version's * CSS-editing plans, Creator and up. Never in the plugin from * WordPress.org. */ public static function allowed(): bool { return class_exists( \GripMCP\Premium\Paid_Writes::class ) && \GripMCP\Premium\Paid_Writes::code(); } /** * The pieces of code a change adds: those in the new value that the * stored one does not have as often. Empty when it adds none. * * @param mixed $before The value stored now ('' for a new one). * @param mixed $after The value that would be stored. * @return array */ public static function added( $before, $after ): array { $new = self::pieces( $after ); if ( empty( $new ) ) { return array(); } $old = self::pieces( $before ); $added = array(); foreach ( $new as $key => $piece ) { $extra = $piece['count'] - ( isset( $old[ $key ] ) ? $old[ $key ]['count'] : 0 ); for ( $i = 0; $i < $extra; $i++ ) { $added[] = array( 'label' => $piece['label'], 'code' => $piece['code'], ); } } return $added; } /** * The code a change to a post's own fields adds: title, content and * excerpt, each compared with what the post has now. * * @param \WP_Post|null $post The post as stored, or null for a new one. * @param array $fields Columns that would be written, with their new values. * @return array */ public static function in_post( ?\WP_Post $post, array $fields ): array { $added = array(); foreach ( array( 'post_title', 'post_content', 'post_excerpt' ) as $column ) { if ( ! array_key_exists( $column, $fields ) ) { continue; } $before = $post instanceof \WP_Post ? (string) $post->{$column} : ''; $added = array_merge( $added, self::added( $before, (string) $fields[ $column ] ) ); } return $added; } /** * The sentence a tool description adds about code, with a leading space. */ public static function description_note(): string { return ' ' . ( self::allowed() ? __( 'A change that adds code (a script, an embedded frame or object, an event handler, a javascript: link or a block\'s custom CSS) waits for the person\'s approval when approvals are on.', 'gripmcp' ) : __( 'It cannot add code (a script, an embedded frame or object, an event handler, a javascript: link or a block\'s custom CSS); code already saved is kept when sent back unchanged.', 'gripmcp' ) ); } /** * A fingerprint of a post's own fields, for a held change that adds code: * if the post changes before anyone approves, the code on the approval * page may no longer be all the change adds, so the approved call stops. * * @param int $post_id Post id. */ public static function post_state( int $post_id ): string { $post = $post_id > 0 ? get_post( $post_id ) : null; if ( ! $post instanceof \WP_Post ) { return ''; } return md5( (string) wp_json_encode( array( $post->post_title, $post->post_content, $post->post_excerpt ) ) ); } /** * Why the change cannot be saved on this plan, or '' when it adds no code * or the plan allows it. * * @param array $added What added() found. */ public static function refusal( array $added ): string { if ( empty( $added ) || self::allowed() ) { return ''; } $text = sprintf( /* translators: %s: what the code is, e.g. "a