http: add Accept-Language header if possible

Add an Accept-Language header which indicates the user's preferred languages defined by $LANGUAGE, $LC_ALL, $LC_MESSAGES and $LANG. Examples: LANGUAGE= -> "" LANGUAGE=ko:en -> "Accept-Language: ko, en;q=0.9, *;q=0.1" LANGUAGE=ko LANG=en_US.UTF-8 -> "Accept-Language: ko, *;q=0.1" LANGUAGE= LANG=en_US.UTF-8 -> "Accept-Language: en-US, *;q=0.1" This gives git servers a chance to display remote error messages in the user's preferred language. Limit the number of languages to 1,000 because q-value must not be smaller than 0.001, and limit the length of Accept-Language header to 4,000 bytes for some HTTP servers which cannot accept such long header. Signed-off-by: Yi EungJun <eungjun.yi@navercorp.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Yi EungJun committed Jan 28, 2015 at 21:04 UTC f18604bbf2c391c689a41fca14cbaeff5e106255
3 files changed +191
http.c
+147
@@ -68,6 +68,8 @@ static struct curl_slist *no_pragma_header;
68
69 static struct active_request_slot *active_queue_head;
70
71 +static char *cached_accept_language;
72 +
73 size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
74 {
75 size_t size = eltsize * nmemb;
@@ -515,6 +517,9 @@ void http_cleanup(void)
517 cert_auth.password = NULL;
518 }
519 ssl_cert_password_required = 0;
520 +
521 + free(cached_accept_language);
522 + cached_accept_language = NULL;
523 }
524
525 struct active_request_slot *get_active_slot(void)
@@ -986,6 +991,142 @@ static void extract_content_type(struct strbuf *raw, struct strbuf *type,
991 strbuf_addstr(charset, "ISO-8859-1");
992 }
993
994 +/*
995 + * Guess the user's preferred languages from the value in LANGUAGE environment
996 + * variable and LC_MESSAGES locale category if NO_GETTEXT is not defined.
997 + *
998 + * The result can be a colon-separated list like "ko:ja:en".
999 + */
1000 +static const char *get_preferred_languages(void)
1001 +{
1002 + const char *retval;
1003 +
1004 + retval = getenv("LANGUAGE");
1005 + if (retval && *retval)
1006 + return retval;
1007 +
1008 +#ifndef NO_GETTEXT
1009 + retval = setlocale(LC_MESSAGES, NULL);
1010 + if (retval && *retval &&
1011 + strcmp(retval, "C") &&
1012 + strcmp(retval, "POSIX"))
1013 + return retval;
1014 +#endif
1015 +
1016 + return NULL;
1017 +}
1018 +
1019 +static void write_accept_language(struct strbuf *buf)
1020 +{
1021 + /*
1022 + * MAX_DECIMAL_PLACES must not be larger than 3. If it is larger than
1023 + * that, q-value will be smaller than 0.001, the minimum q-value the
1024 + * HTTP specification allows. See
1025 + * http://tools.ietf.org/html/rfc7231#section-5.3.1 for q-value.
1026 + */
1027 + const int MAX_DECIMAL_PLACES = 3;
1028 + const int MAX_LANGUAGE_TAGS = 1000;
1029 + const int MAX_ACCEPT_LANGUAGE_HEADER_SIZE = 4000;
1030 + char **language_tags = NULL;
1031 + int num_langs = 0;
1032 + const char *s = get_preferred_languages();
1033 + int i;
1034 + struct strbuf tag = STRBUF_INIT;
1035 +
1036 + /* Don't add Accept-Language header if no language is preferred. */
1037 + if (!s)
1038 + return;
1039 +
1040 + /*
1041 + * Split the colon-separated string of preferred languages into
1042 + * language_tags array.
1043 + */
1044 + do {
1045 + /* collect language tag */
1046 + for (; *s && (isalnum(*s) || *s == '_'); s++)
1047 + strbuf_addch(&tag, *s == '_' ? '-' : *s);
1048 +
1049 + /* skip .codeset, @modifier and any other unnecessary parts */
1050 + while (*s && *s != ':')
1051 + s++;
1052 +
1053 + if (tag.len) {
1054 + num_langs++;
1055 + REALLOC_ARRAY(language_tags, num_langs);
1056 + language_tags[num_langs - 1] = strbuf_detach(&tag, NULL);
1057 + if (num_langs >= MAX_LANGUAGE_TAGS - 1) /* -1 for '*' */
1058 + break;
1059 + }
1060 + } while (*s++);
1061 +
1062 + /* write Accept-Language header into buf */
1063 + if (num_langs) {
1064 + int last_buf_len = 0;
1065 + int max_q;
1066 + int decimal_places;
1067 + char q_format[32];
1068 +
1069 + /* add '*' */
1070 + REALLOC_ARRAY(language_tags, num_langs + 1);
1071 + language_tags[num_langs++] = "*"; /* it's OK; this won't be freed */
1072 +
1073 + /* compute decimal_places */
1074 + for (max_q = 1, decimal_places = 0;
1075 + max_q < num_langs && decimal_places <= MAX_DECIMAL_PLACES;
1076 + decimal_places++, max_q *= 10)
1077 + ;
1078 +
1079 + sprintf(q_format, ";q=0.%%0%dd", decimal_places);
1080 +
1081 + strbuf_addstr(buf, "Accept-Language: ");
1082 +
1083 + for (i = 0; i < num_langs; i++) {
1084 + if (i > 0)
1085 + strbuf_addstr(buf, ", ");
1086 +
1087 + strbuf_addstr(buf, language_tags[i]);
1088 +
1089 + if (i > 0)
1090 + strbuf_addf(buf, q_format, max_q - i);
1091 +
1092 + if (buf->len > MAX_ACCEPT_LANGUAGE_HEADER_SIZE) {
1093 + strbuf_remove(buf, last_buf_len, buf->len - last_buf_len);
1094 + break;
1095 + }
1096 +
1097 + last_buf_len = buf->len;
1098 + }
1099 + }
1100 +
1101 + /* free language tags -- last one is a static '*' */
1102 + for (i = 0; i < num_langs - 1; i++)
1103 + free(language_tags[i]);
1104 + free(language_tags);
1105 +}
1106 +
1107 +/*
1108 + * Get an Accept-Language header which indicates user's preferred languages.
1109 + *
1110 + * Examples:
1111 + * LANGUAGE= -> ""
1112 + * LANGUAGE=ko:en -> "Accept-Language: ko, en; q=0.9, *; q=0.1"
1113 + * LANGUAGE=ko_KR.UTF-8:sr@latin -> "Accept-Language: ko-KR, sr; q=0.9, *; q=0.1"
1114 + * LANGUAGE=ko LANG=en_US.UTF-8 -> "Accept-Language: ko, *; q=0.1"
1115 + * LANGUAGE= LANG=en_US.UTF-8 -> "Accept-Language: en-US, *; q=0.1"
1116 + * LANGUAGE= LANG=C -> ""
1117 + */
1118 +static const char *get_accept_language(void)
1119 +{
1120 + if (!cached_accept_language) {
1121 + struct strbuf buf = STRBUF_INIT;
1122 + write_accept_language(&buf);
1123 + if (buf.len > 0)
1124 + cached_accept_language = strbuf_detach(&buf, NULL);
1125 + }
1126 +
1127 + return cached_accept_language;
1128 +}
1129 +
1130 /* http_request() targets */
1131 #define HTTP_REQUEST_STRBUF 0
1132 #define HTTP_REQUEST_FILE 1
@@ -998,6 +1139,7 @@ static int http_request(const char *url,
1139 struct slot_results results;
1140 struct curl_slist *headers = NULL;
1141 struct strbuf buf = STRBUF_INIT;
1142 + const char *accept_language;
1143 int ret;
1144
1145 slot = get_active_slot();
@@ -1023,6 +1165,11 @@ static int http_request(const char *url,
1165 fwrite_buffer);
1166 }
1167
1168 + accept_language = get_accept_language();
1169 +
1170 + if (accept_language)
1171 + headers = curl_slist_append(headers, accept_language);
1172 +
1173 strbuf_addstr(&buf, "Pragma:");
1174 if (options && options->no_cache)
1175 strbuf_addstr(&buf, " no-cache");
remote-curl.c
+2
@@ -962,6 +962,8 @@ int main(int argc, const char **argv)
962 struct strbuf buf = STRBUF_INIT;
963 int nongit;
964
965 + git_setup_gettext();
966 +
967 git_extract_argv0_path(argv[0]);
968 setup_git_directory_gently(&nongit);
969 if (argc < 2) {
t/t5550-http-fetch-dumb.sh
+42
@@ -196,5 +196,47 @@ test_expect_success 'reencoding is robust to whitespace oddities' '
196 grep "this is the error message" stderr
197 '
198
199 +check_language () {
200 + case "$2" in
201 + '')
202 + >expect
203 + ;;
204 + ?*)
205 + echo "Accept-Language: $1" >expect
206 + ;;
207 + esac &&
208 + GIT_CURL_VERBOSE=1 \
209 + LANGUAGE=$2 \
210 + git ls-remote "$HTTPD_URL/dumb/repo.git" >output 2>&1 &&
211 + tr -d '\015' <output |
212 + sort -u |
213 + sed -ne '/^Accept-Language:/ p' >actual &&
214 + test_cmp expect actual
215 +}
216 +
217 +test_expect_success 'git client sends Accept-Language based on LANGUAGE' '
218 + check_language "ko-KR, *;q=0.9" ko_KR.UTF-8'
219 +
220 +test_expect_success 'git client sends Accept-Language correctly with unordinary LANGUAGE' '
221 + check_language "ko-KR, *;q=0.9" "ko_KR:" &&
222 + check_language "ko-KR, en-US;q=0.9, *;q=0.8" "ko_KR::en_US" &&
223 + check_language "ko-KR, *;q=0.9" ":::ko_KR" &&
224 + check_language "ko-KR, en-US;q=0.9, *;q=0.8" "ko_KR!!:en_US" &&
225 + check_language "ko-KR, ja-JP;q=0.9, *;q=0.8" "ko_KR en_US:ja_JP"'
226 +
227 +test_expect_success 'git client sends Accept-Language with many preferred languages' '
228 + check_language "ko-KR, en-US;q=0.9, fr-CA;q=0.8, de;q=0.7, sr;q=0.6, \
229 +ja;q=0.5, zh;q=0.4, sv;q=0.3, pt;q=0.2, *;q=0.1" \
230 + ko_KR.EUC-KR:en_US.UTF-8:fr_CA:de.UTF-8@euro:sr@latin:ja:zh:sv:pt &&
231 + check_language "ko-KR, en-US;q=0.99, fr-CA;q=0.98, de;q=0.97, sr;q=0.96, \
232 +ja;q=0.95, zh;q=0.94, sv;q=0.93, pt;q=0.92, nb;q=0.91, *;q=0.90" \
233 + ko_KR.EUC-KR:en_US.UTF-8:fr_CA:de.UTF-8@euro:sr@latin:ja:zh:sv:pt:nb
234 +'
235 +
236 +test_expect_success 'git client does not send an empty Accept-Language' '
237 + GIT_CURL_VERBOSE=1 LANGUAGE= git ls-remote "$HTTPD_URL/dumb/repo.git" 2>stderr &&
238 + ! grep "^Accept-Language:" stderr
239 +'
240 +
241 stop_httpd
242 test_done