503 ) ); } /** * Return only configuration digests; never open, stat, return or persist a configured path. * * @param int $network Network identifier selecting the configured sources. * @param int $site Site identifier selecting the configured sources. * @return array|\WP_Error Sorted source-key/configuration digests, or an error for invalid configuration. */ public static function configurations( int $network, int $site ): array|\WP_Error { if ( $network < 1 || $site < 1 ) { return self::error(); } if ( ! defined( 'LOGGED_AI_IMPORT_SOURCES' ) ) { return array(); } if ( ! is_array( LOGGED_AI_IMPORT_SOURCES ) || count( LOGGED_AI_IMPORT_SOURCES ) > 32 ) { return self::error(); } $out = array(); foreach ( LOGGED_AI_IMPORT_SOURCES as $name => $value ) { if ( ! self::identifier( $name ) || ! is_array( $value ) || ! is_int( $value['network_id'] ?? null ) || ! is_int( $value['site_id'] ?? null ) ) { return self::error(); } if ( $value['network_id'] !== $network || $value['site_id'] !== $site ) { continue; } $keys = array( 'network_id', 'site_id', 'source', 'site', 'files', 'allow_peer' ); if ( count( $value ) !== count( $keys ) || array_diff( array_keys( $value ), $keys ) || array_diff( $keys, array_keys( $value ) ) || ! self::identifier( $value['source'] ) || ! self::identifier( $value['site'] ) || ! is_bool( $value['allow_peer'] ) || ! is_array( $value['files'] ) || ! array_is_list( $value['files'] ) || ! $value['files'] || count( $value['files'] ) > 5 ) { return self::error(); } foreach ( $value['files'] as $file ) { if ( ! self::path_shape( $file ) ) { return self::error(); } } if ( count( array_unique( $value['files'], SORT_STRING ) ) !== count( $value['files'] ) ) { return self::error(); } // Matches NginxSource exactly: derived key precedes the binding; caps follow it. $key = hash( 'sha256', $name ); $value['key'] = $key; $json = wp_json_encode( $value ); if ( ! is_string( $json ) ) { return self::error(); } $out[ $key ] = hash( 'sha256', $json ); } ksort( $out, SORT_STRING ); return $out; } /** * Read the finite scope inventory used to invalidate copies after connector or configuration changes. * * @param \wpdb $db Native connection whose context and authority are sealed by the caller. * @param int $network Current network identifier. * @param int $site Current site identifier. * @return array|\WP_Error Current connector identities, availability and inventory digest, or a refusal. */ public static function inventory( \wpdb $db, int $network, int $site ): array|\WP_Error { $configured = self::configurations( $network, $site ); $table = $db->base_prefix . 'logged_ai_pro_import_sources'; if ( is_wp_error( $configured ) || ! preg_match( '/\A[a-zA-Z0-9_]{1,64}\z/D', $table ) ) { return self::error(); } $exists = RequestCaseLayout::rows( $db, $db->prepare( 'SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s LIMIT 1', $table ), 1 ); if ( null === $exists || ( $exists && array( array( 'name' => $table ) ) !== $exists ) ) { return self::error(); } $rows = array(); if ( $exists ) { if ( ! self::ready( $db, $table ) ) { return self::error(); } $rows = RequestCaseLayout::rows( $db, $db->prepare( "SELECT source_key,instance_uid,configuration_hash FROM %i WHERE network_id=%d AND site_id=%d ORDER BY source_key LIMIT 33 LOCK IN SHARE MODE", $table, $network, $site ), 33 ); if ( null === $rows || count( $rows ) > 32 ) { return self::error(); } } $current = array(); foreach ( $rows as $row ) { if ( array_keys( $row ) !== array( 'source_key', 'instance_uid', 'configuration_hash' ) || ! self::digest( $row['source_key'] ) || ! self::digest( $row['configuration_hash'] ) || ! self::uuid( $row['instance_uid'] ) || isset( $current[ $row['source_key'] ] ) ) { return self::error(); } $current[ $row['source_key'] ] = array( 'uid' => $row['instance_uid'], 'binding' => $row['configuration_hash'], 'available' => ( $configured[ $row['source_key'] ] ?? null ) === $row['configuration_hash'], ); } return array( 'sources' => $current, // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Preserve the exact versioned source-inventory fingerprint and JSON_THROW_ON_ERROR failure; this is an internal identity binding, not display text. 'binding' => hash( 'sha256', json_encode( array( 1, $network, $site, (bool) $exists, $configured, $current ), JSON_THROW_ON_ERROR ) ), ); } /** * Verify the existing connector table columns, engine and primary key. * * @param \wpdb $db Native connection used for bounded metadata reads. * @param string $table Previously validated native table identifier. * @return bool Whether the current metadata matches the required connector schema. */ private static function ready( \wpdb $db, string $table ): bool { $types = array( 'network_id' => 'bigint unsigned', 'site_id' => 'bigint unsigned', 'source_key' => 'char(64)', 'instance_uid' => 'char(36)', 'configuration_hash' => 'char(64)', 'state' => 'text', 'updated_at' => 'datetime', ); $columns = RequestCaseLayout::rows( $db, "SHOW COLUMNS FROM {$table}", 7 ); if ( null === $columns || count( $columns ) !== 7 ) { return false; } $seen = array(); foreach ( $columns as $column ) { $field = $column['Field'] ?? null; if ( ! is_string( $field ) || ! isset( $types[ $field ] ) || isset( $seen[ $field ] ) || ! is_string( $column['Type'] ?? null ) || preg_replace( '/\b(bigint|int)\([0-9]+\)/', '$1', strtolower( $column['Type'] ) ) !== $types[ $field ] || 'NO' !== ( $column['Null'] ?? null ) || '' !== ( $column['Extra'] ?? null ) ) { return false; } $seen[ $field ] = true; } $engine = RequestCaseLayout::rows( $db, $db->prepare( 'SELECT ENGINE AS engine FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s LIMIT 1', $table ), 1 ); $indexes = RequestCaseLayout::rows( $db, "SHOW INDEX FROM {$table} WHERE Key_name='PRIMARY'", 3 ); if ( array( array( 'engine' => 'InnoDB' ) ) !== $engine || null === $indexes || count( $indexes ) !== 3 ) { return false; } $fields = array(); foreach ( $indexes as $index ) { $seq = $index['Seq_in_index'] ?? null; if ( ! in_array( $seq, array( 1, 2, 3, '1', '2', '3' ), true ) || isset( $fields[ (int) $seq ] ) || '0' !== (string) ( $index['Non_unique'] ?? '' ) || ! array_key_exists( 'Sub_part', $index ) || null !== $index['Sub_part'] ) { return false; } $fields[ (int) $seq ] = $index['Column_name'] ?? null; } ksort( $fields ); return array_values( $fields ) === array( 'network_id', 'site_id', 'source_key' ); } /** * Check a bounded importer name or source label. * * @param mixed $value Candidate identifier string. * @return bool Whether the value matches the allowed identifier grammar. */ private static function identifier( mixed $value ): bool { return is_string( $value ) && 1 === preg_match( '/\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}\z/D', $value ) && ! str_contains( $value, '..' ); } /** * Check a configured path shape without inspecting the filesystem. * * @param mixed $path Candidate absolute configured path. * @return bool Whether the shape avoids the excluded characters, locations and basenames. */ private static function path_shape( mixed $path ): bool { return is_string( $path ) && strlen( $path ) <= 1024 && '' !== $path && '/' === $path[0] && ! preg_match( '/[\x00-\x20\x7f\\\\:*?\[\]{}]/', $path ) && ! str_contains( $path, '//' ) && ! preg_match( '~(?:^|/)\.{1,2}(?:/|$)~', $path ) && ! str_ends_with( $path, '/' ) && ! str_starts_with( $path, rtrim( ABSPATH, '/' ) . '/' ) && ! in_array( basename( $path ), array( 'wp-config.php', '.env', 'config.php' ), true ); } /** * Check the lowercase SHA-256 digest representation. * * @param mixed $value Candidate digest string. * @return bool Whether the value contains exactly sixty-four hexadecimal digits. */ private static function digest( mixed $value ): bool { return is_string( $value ) && 1 === preg_match( '/\A[a-f0-9]{64}\z/D', $value ); } /** * Check the lowercase UUID representation used by connector instances. * * @param mixed $value Candidate UUID string. * @return bool Whether the UUID has the required hexadecimal grouping. */ private static function uuid( mixed $value ): bool { return is_string( $value ) && 1 === preg_match( '/\A[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}\z/D', $value ); } }