fix security errors
This commit is contained in:
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
rm -rf build
|
||||
mkdir -p "build/${plugin_slug}"
|
||||
cp btranslate.php README.md "build/${plugin_slug}/"
|
||||
cp btranslate.php uninstall.php readme.txt README.md README.zh-CN.md LICENSE "build/${plugin_slug}/"
|
||||
cp -R includes "build/${plugin_slug}/"
|
||||
(
|
||||
cd build
|
||||
@@ -41,6 +41,7 @@ jobs:
|
||||
)
|
||||
|
||||
unzip -p "${archive_name}" "${plugin_slug}/btranslate.php" | grep -q '^ \* Plugin Name: Btranslate$'
|
||||
unzip -p "${archive_name}" "${plugin_slug}/readme.txt" | grep -q "^Stable tag: ${version}$"
|
||||
|
||||
printf 'archive=%s\n' "${archive_name}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ After changing routing settings, rewrite rules are refreshed. The initial implem
|
||||
|
||||
Credentials are stored in the WordPress `btranslate_settings` option and are not rendered on the front end. The plugin stores the translation identity, language pair, field context, source fingerprint, translated value, status, and timestamps. It does not store full Baidu response payloads.
|
||||
|
||||
The **Log every Baidu translation request** setting is disabled by default. When enabled, each request writes safe metadata to the PHP error log: language pair, field context, source fingerprint and length, result status, and an error code when applicable. Credentials, signatures, source text, translated text, and complete API responses are never logged.
|
||||
The **Log every Baidu translation request** setting is disabled by default. When enabled, each request fires the `btranslate_translation_request_logged` action with safe metadata: language pair, field context, source fingerprint and length, result status, and an error code when applicable. A site-specific logging integration can subscribe to this action. Credentials, signatures, source text, translated text, and complete API responses are never included.
|
||||
|
||||
Use HTTPS and restrict database access appropriately because WordPress options are database-backed. Production sites should configure a real cron runner for reliable translation processing.
|
||||
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ Btranslate 是一个 WordPress 插件骨架,使用百度翻译 API 持久化
|
||||
|
||||
凭据保存在 WordPress 的 `btranslate_settings` 选项中,不会在前端输出。插件会存储翻译标识、语言对、字段上下文、源内容指纹、翻译值、状态和时间戳;不会保存完整的百度响应负载。
|
||||
|
||||
**记录每次百度翻译请求** 默认关闭。开启后,每次请求都会向 PHP 错误日志写入安全元数据:语言对、字段上下文、源文本指纹和长度、结果状态以及适用时的错误码。日志绝不会包含凭据、签名、原文、译文或完整 API 响应。
|
||||
**记录每次百度翻译请求** 默认关闭。开启后,每次请求都会触发 `btranslate_translation_request_logged` 操作,并提供语言对、字段上下文、源文本指纹和长度、结果状态以及适用时的错误码。站点可通过自定义日志集成订阅该操作。操作数据绝不会包含凭据、签名、原文、译文或完整 API 响应。
|
||||
|
||||
由于 WordPress 选项由数据库保存,请使用 HTTPS 并适当限制数据库访问权限。生产站点应配置真实的 Cron 执行器,确保翻译任务可靠运行。
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class BTRANSLATE_Admin {
|
||||
<tr id="btranslate-domain-bindings-row"<?php echo in_array( 'domain', $settings['routing_mode'], true ) ? '' : ' style="display:none"'; ?>><th scope="row"><label for="btranslate-domain-bindings">域名绑定</label></th><td><textarea id="btranslate-domain-bindings" name="btranslate_settings[domain_bindings]" rows="4" class="large-text" placeholder="en.example.com=en"><?php echo esc_textarea( implode( "\n", $bindings ) ); ?></textarea><p class="description">每行一个 <code>域名=语言代码</code>,例如 <code>en.example.com=en</code>。请勿填写 <code>https://</code>、路径或端口。</p></td></tr>
|
||||
<tr><th scope="row"><label for="btranslate-baidu-app-id">百度应用 ID</label></th><td><input id="btranslate-baidu-app-id" name="btranslate_settings[baidu_app_id]" type="text" value="<?php echo esc_attr( $settings['baidu_app_id'] ); ?>" class="regular-text" /></td></tr>
|
||||
<tr><th scope="row"><label for="btranslate-baidu-secret-key">百度密钥</label></th><td><input id="btranslate-baidu-secret-key" name="btranslate_settings[baidu_secret_key]" type="password" value="<?php echo esc_attr( $settings['baidu_secret_key'] ); ?>" class="regular-text" autocomplete="new-password" /></td></tr>
|
||||
<tr><th scope="row">请求日志</th><td><label for="btranslate-log-requests"><input id="btranslate-log-requests" name="btranslate_settings[log_requests]" type="checkbox" value="1" <?php checked( $settings['log_requests'] ); ?> /> 记录每次百度翻译请求</label><p class="description">日志写入 PHP 错误日志,包含语言、字段、文本指纹、长度和结果状态;不会记录密钥、原文、译文或完整 API 响应。</p></td></tr>
|
||||
<tr><th scope="row">请求日志</th><td><label for="btranslate-log-requests"><input id="btranslate-log-requests" name="btranslate_settings[log_requests]" type="checkbox" value="1" <?php checked( $settings['log_requests'] ); ?> /> 记录每次百度翻译请求</label><p class="description">启用后触发 <code>btranslate_translation_request_logged</code> 操作,提供语言、字段、文本指纹、长度和结果状态;不会提供密钥、原文、译文或完整 API 响应。</p></td></tr>
|
||||
</table>
|
||||
<?php submit_button(); ?>
|
||||
</form>
|
||||
|
||||
@@ -79,6 +79,6 @@ class BTRANSLATE_Baidu_Provider implements BTRANSLATE_Translation_Provider {
|
||||
'error_code' => sanitize_key( $error_code ),
|
||||
);
|
||||
|
||||
error_log( 'BTRANSLATE translation request: ' . wp_json_encode( $entry ) );
|
||||
do_action( 'btranslate_translation_request_logged', $entry );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class BTRANSLATE_Language_Router {
|
||||
return $do_parse_request;
|
||||
}
|
||||
|
||||
$request_uri = wp_unslash( $_SERVER['REQUEST_URI'] );
|
||||
$request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
|
||||
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
||||
$query = wp_parse_url( $request_uri, PHP_URL_QUERY );
|
||||
|
||||
@@ -54,7 +54,7 @@ class BTRANSLATE_Language_Router {
|
||||
|
||||
public function resolve_domain_language( $wp ) {
|
||||
$settings = BTRANSLATE_Settings::get();
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? BTRANSLATE_Settings::normalize_domain( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? BTRANSLATE_Settings::normalize_domain( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
|
||||
$bindings = (array) $settings['domain_bindings'];
|
||||
|
||||
if ( '' !== $this->request_language ) {
|
||||
@@ -130,14 +130,14 @@ class BTRANSLATE_Language_Router {
|
||||
}
|
||||
|
||||
private function request_uses_domain( $language ) {
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? BTRANSLATE_Settings::normalize_domain( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? BTRANSLATE_Settings::normalize_domain( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
|
||||
$bindings = (array) BTRANSLATE_Settings::get()['domain_bindings'];
|
||||
|
||||
return '' !== $host && isset( $bindings[ $host ] ) && sanitize_key( $bindings[ $host ] ) === $language;
|
||||
}
|
||||
|
||||
private function source_origin_parts( $parts ) {
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? BTRANSLATE_Settings::normalize_domain( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? BTRANSLATE_Settings::normalize_domain( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
|
||||
$bindings = (array) BTRANSLATE_Settings::get()['domain_bindings'];
|
||||
|
||||
if ( '' === $host || ! isset( $bindings[ $host ] ) ) {
|
||||
|
||||
@@ -65,7 +65,7 @@ class BTRANSLATE_Sitemap_Controller {
|
||||
}
|
||||
|
||||
private function request_path() {
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
||||
|
||||
return is_string( $path ) ? '/' . ltrim( $path, '/' ) : '';
|
||||
@@ -80,7 +80,7 @@ class BTRANSLATE_Sitemap_Controller {
|
||||
}
|
||||
|
||||
if ( BTRANSLATE_Settings::is_routing_mode_enabled( 'domain' ) && '/sitemap.xml' === untrailingslashit( $path ) ) {
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? BTRANSLATE_Settings::normalize_domain( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
|
||||
$host = isset( $_SERVER['HTTP_HOST'] ) ? BTRANSLATE_Settings::normalize_domain( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) : '';
|
||||
$bindings = (array) $settings['domain_bindings'];
|
||||
$language = isset( $bindings[ $host ] ) ? sanitize_key( $bindings[ $host ] ) : '';
|
||||
|
||||
|
||||
@@ -39,13 +39,14 @@ class BTRANSLATE_Translation_Store {
|
||||
public function find_valid( $identity_key ) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::table_name();
|
||||
$sql = $wpdb->prepare(
|
||||
"SELECT * FROM {$table_name} WHERE identity_key = %s AND status = 'complete' LIMIT 1",
|
||||
return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The custom translation table is the plugin's persistent cache.
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM %i WHERE identity_key = %s AND status = 'complete' LIMIT 1",
|
||||
self::table_name(),
|
||||
$identity_key
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $wpdb->get_row( $sql, ARRAY_A );
|
||||
}
|
||||
|
||||
public function save( $identity_key, $source_language, $target_language, $field_context, $source_fingerprint, $translated_value, $status ) {
|
||||
@@ -53,7 +54,7 @@ class BTRANSLATE_Translation_Store {
|
||||
|
||||
$now = current_time( 'mysql', true );
|
||||
|
||||
return $wpdb->replace(
|
||||
return $wpdb->replace( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Writes to the plugin-owned persistent translation cache.
|
||||
self::table_name(),
|
||||
array(
|
||||
'identity_key' => $identity_key,
|
||||
@@ -73,29 +74,30 @@ class BTRANSLATE_Translation_Store {
|
||||
public function clear() {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->query( 'DELETE FROM ' . self::table_name() );
|
||||
return $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- This explicitly clears the plugin's persistent translation cache.
|
||||
$wpdb->prepare( 'DELETE FROM %i', self::table_name() )
|
||||
);
|
||||
}
|
||||
|
||||
public function get_post_language_status( $post_id, $target_language ) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::table_name();
|
||||
$sql = $wpdb->prepare(
|
||||
return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The custom translation table is the plugin's persistent cache.
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) AS translated_fields, MAX(updated_at) AS last_translated_at
|
||||
FROM {$table_name}
|
||||
FROM %i
|
||||
WHERE target_language = %s
|
||||
AND status = 'complete'
|
||||
AND field_context LIKE %s",
|
||||
self::table_name(),
|
||||
$target_language,
|
||||
'post:' . absint( $post_id ) . ':%'
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $wpdb->get_row( $sql, ARRAY_A );
|
||||
}
|
||||
|
||||
public function get_completed_item_counts( $target_languages, $post_ids, $term_ids, $since = '' ) {
|
||||
global $wpdb;
|
||||
|
||||
$target_languages = array_values( array_filter( array_map( 'sanitize_key', (array) $target_languages ) ) );
|
||||
$post_ids = array_values( array_filter( array_map( 'absint', (array) $post_ids ) ) );
|
||||
$term_ids = array_values( array_filter( array_map( 'absint', (array) $term_ids ) ) );
|
||||
@@ -106,23 +108,19 @@ class BTRANSLATE_Translation_Store {
|
||||
);
|
||||
}
|
||||
|
||||
$table_name = self::table_name();
|
||||
$language_placeholders = implode( ',', array_fill( 0, count( $target_languages ), '%s' ) );
|
||||
$since_clause = '' === $since ? '' : ' AND updated_at >= %s';
|
||||
$post_sql = $this->get_completed_item_count_sql( 'post', $post_ids, $target_languages, $language_placeholders, $since, $since_clause, $table_name );
|
||||
$term_sql = $this->get_completed_item_count_sql( 'term', $term_ids, $target_languages, $language_placeholders, $since, $since_clause, $table_name );
|
||||
|
||||
return array(
|
||||
'posts' => (int) $wpdb->get_var( $post_sql ),
|
||||
'terms' => (int) $wpdb->get_var( $term_sql ),
|
||||
'posts' => $this->get_completed_item_count( 'post', $post_ids, $target_languages, $language_placeholders, $since ),
|
||||
'terms' => $this->get_completed_item_count( 'term', $term_ids, $target_languages, $language_placeholders, $since ),
|
||||
);
|
||||
}
|
||||
|
||||
private function get_completed_item_count_sql( $item_type, $item_ids, $target_languages, $language_placeholders, $since, $since_clause, $table_name ) {
|
||||
private function get_completed_item_count( $item_type, $item_ids, $target_languages, $language_placeholders, $since ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( empty( $item_ids ) ) {
|
||||
return 'SELECT 0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
$contexts = array_map(
|
||||
@@ -137,13 +135,16 @@ class BTRANSLATE_Translation_Store {
|
||||
$query_args[] = $since;
|
||||
}
|
||||
|
||||
return $wpdb->prepare(
|
||||
$since_clause = '' === $since ? '' : ' AND updated_at >= %s';
|
||||
return (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The custom translation table is the plugin's persistent cache.
|
||||
$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IN clauses contain only generated %s placeholders; the optional clause is a fixed SQL fragment.
|
||||
"SELECT COUNT(DISTINCT CONCAT(target_language, '|', SUBSTRING_INDEX(field_context, ':', 2)))
|
||||
FROM {$table_name}
|
||||
FROM %i
|
||||
WHERE status = 'complete'
|
||||
AND target_language IN ({$language_placeholders})
|
||||
AND SUBSTRING_INDEX(field_context, ':', 2) IN ({$context_placeholders}){$since_clause}",
|
||||
$query_args
|
||||
array_merge( array( self::table_name() ), $query_args )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
=== Btranslate ===
|
||||
Contributors: hanyixuanten
|
||||
Requires at least: 6.4
|
||||
Tested up to: 6.4
|
||||
Requires PHP: 8.1
|
||||
Stable tag: 0.2.1
|
||||
License: GPLv3
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Persistent multilingual WordPress translations powered by the Baidu Translate API.
|
||||
|
||||
== Description ==
|
||||
|
||||
Btranslate translates supported WordPress content with the Baidu Translate API and persists each translation for reuse. It supports language-specific subdirectory URLs and domain bindings.
|
||||
|
||||
See README.md for installation, routing, data retention, translation lifecycle, supported content, and known limitations.
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the `btranslate` directory to `/wp-content/plugins/`.
|
||||
2. Activate Btranslate through the Plugins screen in WordPress.
|
||||
3. Open Settings > Btranslate and configure the Baidu credentials, languages, and routing mode.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 0.2.1 =
|
||||
|
||||
* Declare GPLv3 licensing and WordPress.org plugin metadata.
|
||||
Reference in New Issue
Block a user