Merge pull request #965 from shuoli84/develop
Fix subdomain, add test, space and comment fix
Showing
4 changed files
with
106 additions
and
50 deletions
| ... | @@ -17,13 +17,10 @@ package config | ... | @@ -17,13 +17,10 @@ package config |
| 17 | import ( | 17 | import ( |
| 18 | "encoding/json" | 18 | "encoding/json" |
| 19 | "errors" | 19 | "errors" |
| 20 | "fmt" | ||
| 21 | "io/ioutil" | 20 | "io/ioutil" |
| 22 | "os" | 21 | "os" |
| 23 | "path" | ||
| 24 | "strings" | 22 | "strings" |
| 25 | "sync" | 23 | "sync" |
| 26 | "time" | ||
| 27 | ) | 24 | ) |
| 28 | 25 | ||
| 29 | // JsonConfig is a json config parser and implements Config interface. | 26 | // JsonConfig is a json config parser and implements Config interface. |
| ... | @@ -41,13 +38,19 @@ func (js *JsonConfig) Parse(filename string) (ConfigContainer, error) { | ... | @@ -41,13 +38,19 @@ func (js *JsonConfig) Parse(filename string) (ConfigContainer, error) { |
| 41 | if err != nil { | 38 | if err != nil { |
| 42 | return nil, err | 39 | return nil, err |
| 43 | } | 40 | } |
| 41 | |||
| 42 | return js.ParseData(content) | ||
| 43 | } | ||
| 44 | |||
| 45 | // ParseData returns a ConfigContainer with json string | ||
| 46 | func (js *JsonConfig) ParseData(data []byte) (ConfigContainer, error) { | ||
| 44 | x := &JsonConfigContainer{ | 47 | x := &JsonConfigContainer{ |
| 45 | data: make(map[string]interface{}), | 48 | data: make(map[string]interface{}), |
| 46 | } | 49 | } |
| 47 | err = json.Unmarshal(content, &x.data) | 50 | err := json.Unmarshal(data, &x.data) |
| 48 | if err != nil { | 51 | if err != nil { |
| 49 | var wrappingArray []interface{} | 52 | var wrappingArray []interface{} |
| 50 | err2 := json.Unmarshal(content, &wrappingArray) | 53 | err2 := json.Unmarshal(data, &wrappingArray) |
| 51 | if err2 != nil { | 54 | if err2 != nil { |
| 52 | return nil, err | 55 | return nil, err |
| 53 | } | 56 | } |
| ... | @@ -56,16 +59,6 @@ func (js *JsonConfig) Parse(filename string) (ConfigContainer, error) { | ... | @@ -56,16 +59,6 @@ func (js *JsonConfig) Parse(filename string) (ConfigContainer, error) { |
| 56 | return x, nil | 59 | return x, nil |
| 57 | } | 60 | } |
| 58 | 61 | ||
| 59 | func (js *JsonConfig) ParseData(data []byte) (ConfigContainer, error) { | ||
| 60 | // Save memory data to temporary file | ||
| 61 | tmpName := path.Join(os.TempDir(), "beego", fmt.Sprintf("%d", time.Now().Nanosecond())) | ||
| 62 | os.MkdirAll(path.Dir(tmpName), os.ModePerm) | ||
| 63 | if err := ioutil.WriteFile(tmpName, data, 0655); err != nil { | ||
| 64 | return nil, err | ||
| 65 | } | ||
| 66 | return js.Parse(tmpName) | ||
| 67 | } | ||
| 68 | |||
| 69 | // A Config represents the json configuration. | 62 | // A Config represents the json configuration. |
| 70 | // Only when get value, support key as section:name type. | 63 | // Only when get value, support key as section:name type. |
| 71 | type JsonConfigContainer struct { | 64 | type JsonConfigContainer struct { |
| ... | @@ -88,11 +81,10 @@ func (c *JsonConfigContainer) Bool(key string) (bool, error) { | ... | @@ -88,11 +81,10 @@ func (c *JsonConfigContainer) Bool(key string) (bool, error) { |
| 88 | // DefaultBool return the bool value if has no error | 81 | // DefaultBool return the bool value if has no error |
| 89 | // otherwise return the defaultval | 82 | // otherwise return the defaultval |
| 90 | func (c *JsonConfigContainer) DefaultBool(key string, defaultval bool) bool { | 83 | func (c *JsonConfigContainer) DefaultBool(key string, defaultval bool) bool { |
| 91 | if v, err := c.Bool(key); err != nil { | 84 | if v, err := c.Bool(key); err == nil { |
| 92 | return defaultval | ||
| 93 | } else { | ||
| 94 | return v | 85 | return v |
| 95 | } | 86 | } |
| 87 | return defaultval | ||
| 96 | } | 88 | } |
| 97 | 89 | ||
| 98 | // Int returns the integer value for a given key. | 90 | // Int returns the integer value for a given key. |
| ... | @@ -110,11 +102,10 @@ func (c *JsonConfigContainer) Int(key string) (int, error) { | ... | @@ -110,11 +102,10 @@ func (c *JsonConfigContainer) Int(key string) (int, error) { |
| 110 | // DefaultInt returns the integer value for a given key. | 102 | // DefaultInt returns the integer value for a given key. |
| 111 | // if err != nil return defaltval | 103 | // if err != nil return defaltval |
| 112 | func (c *JsonConfigContainer) DefaultInt(key string, defaultval int) int { | 104 | func (c *JsonConfigContainer) DefaultInt(key string, defaultval int) int { |
| 113 | if v, err := c.Int(key); err != nil { | 105 | if v, err := c.Int(key); err == nil { |
| 114 | return defaultval | ||
| 115 | } else { | ||
| 116 | return v | 106 | return v |
| 117 | } | 107 | } |
| 108 | return defaultval | ||
| 118 | } | 109 | } |
| 119 | 110 | ||
| 120 | // Int64 returns the int64 value for a given key. | 111 | // Int64 returns the int64 value for a given key. |
| ... | @@ -132,11 +123,10 @@ func (c *JsonConfigContainer) Int64(key string) (int64, error) { | ... | @@ -132,11 +123,10 @@ func (c *JsonConfigContainer) Int64(key string) (int64, error) { |
| 132 | // DefaultInt64 returns the int64 value for a given key. | 123 | // DefaultInt64 returns the int64 value for a given key. |
| 133 | // if err != nil return defaltval | 124 | // if err != nil return defaltval |
| 134 | func (c *JsonConfigContainer) DefaultInt64(key string, defaultval int64) int64 { | 125 | func (c *JsonConfigContainer) DefaultInt64(key string, defaultval int64) int64 { |
| 135 | if v, err := c.Int64(key); err != nil { | 126 | if v, err := c.Int64(key); err == nil { |
| 136 | return defaultval | ||
| 137 | } else { | ||
| 138 | return v | 127 | return v |
| 139 | } | 128 | } |
| 129 | return defaultval | ||
| 140 | } | 130 | } |
| 141 | 131 | ||
| 142 | // Float returns the float value for a given key. | 132 | // Float returns the float value for a given key. |
| ... | @@ -154,11 +144,10 @@ func (c *JsonConfigContainer) Float(key string) (float64, error) { | ... | @@ -154,11 +144,10 @@ func (c *JsonConfigContainer) Float(key string) (float64, error) { |
| 154 | // DefaultFloat returns the float64 value for a given key. | 144 | // DefaultFloat returns the float64 value for a given key. |
| 155 | // if err != nil return defaltval | 145 | // if err != nil return defaltval |
| 156 | func (c *JsonConfigContainer) DefaultFloat(key string, defaultval float64) float64 { | 146 | func (c *JsonConfigContainer) DefaultFloat(key string, defaultval float64) float64 { |
| 157 | if v, err := c.Float(key); err != nil { | 147 | if v, err := c.Float(key); err == nil { |
| 158 | return defaultval | ||
| 159 | } else { | ||
| 160 | return v | 148 | return v |
| 161 | } | 149 | } |
| 150 | return defaultval | ||
| 162 | } | 151 | } |
| 163 | 152 | ||
| 164 | // String returns the string value for a given key. | 153 | // String returns the string value for a given key. |
| ... | @@ -175,35 +164,37 @@ func (c *JsonConfigContainer) String(key string) string { | ... | @@ -175,35 +164,37 @@ func (c *JsonConfigContainer) String(key string) string { |
| 175 | // DefaultString returns the string value for a given key. | 164 | // DefaultString returns the string value for a given key. |
| 176 | // if err != nil return defaltval | 165 | // if err != nil return defaltval |
| 177 | func (c *JsonConfigContainer) DefaultString(key string, defaultval string) string { | 166 | func (c *JsonConfigContainer) DefaultString(key string, defaultval string) string { |
| 178 | if v := c.String(key); v == "" { | 167 | // TODO FIXME should not use "" to replace non existance |
| 179 | return defaultval | 168 | if v := c.String(key); v != "" { |
| 180 | } else { | ||
| 181 | return v | 169 | return v |
| 182 | } | 170 | } |
| 171 | return defaultval | ||
| 183 | } | 172 | } |
| 184 | 173 | ||
| 185 | // Strings returns the []string value for a given key. | 174 | // Strings returns the []string value for a given key. |
| 186 | func (c *JsonConfigContainer) Strings(key string) []string { | 175 | func (c *JsonConfigContainer) Strings(key string) []string { |
| 176 | stringVal := c.String(key) | ||
| 177 | if stringVal == "" { | ||
| 178 | return []string{} | ||
| 179 | } | ||
| 187 | return strings.Split(c.String(key), ";") | 180 | return strings.Split(c.String(key), ";") |
| 188 | } | 181 | } |
| 189 | 182 | ||
| 190 | // DefaultStrings returns the []string value for a given key. | 183 | // DefaultStrings returns the []string value for a given key. |
| 191 | // if err != nil return defaltval | 184 | // if err != nil return defaltval |
| 192 | func (c *JsonConfigContainer) DefaultStrings(key string, defaultval []string) []string { | 185 | func (c *JsonConfigContainer) DefaultStrings(key string, defaultval []string) []string { |
| 193 | if v := c.Strings(key); len(v) == 0 { | 186 | if v := c.Strings(key); len(v) > 0 { |
| 194 | return defaultval | ||
| 195 | } else { | ||
| 196 | return v | 187 | return v |
| 197 | } | 188 | } |
| 189 | return defaultval | ||
| 198 | } | 190 | } |
| 199 | 191 | ||
| 200 | // GetSection returns map for the given section | 192 | // GetSection returns map for the given section |
| 201 | func (c *JsonConfigContainer) GetSection(section string) (map[string]string, error) { | 193 | func (c *JsonConfigContainer) GetSection(section string) (map[string]string, error) { |
| 202 | if v, ok := c.data[section]; ok { | 194 | if v, ok := c.data[section]; ok { |
| 203 | return v.(map[string]string), nil | 195 | return v.(map[string]string), nil |
| 204 | } else { | ||
| 205 | return nil, errors.New("not exist setction") | ||
| 206 | } | 196 | } |
| 197 | return nil, errors.New("nonexist section " + section) | ||
| 207 | } | 198 | } |
| 208 | 199 | ||
| 209 | // SaveConfigFile save the config into file | 200 | // SaveConfigFile save the config into file |
| ... | @@ -222,7 +213,7 @@ func (c *JsonConfigContainer) SaveConfigFile(filename string) (err error) { | ... | @@ -222,7 +213,7 @@ func (c *JsonConfigContainer) SaveConfigFile(filename string) (err error) { |
| 222 | return err | 213 | return err |
| 223 | } | 214 | } |
| 224 | 215 | ||
| 225 | // WriteValue writes a new value for key. | 216 | // Set writes a new value for key. |
| 226 | func (c *JsonConfigContainer) Set(key, val string) error { | 217 | func (c *JsonConfigContainer) Set(key, val string) error { |
| 227 | c.Lock() | 218 | c.Lock() |
| 228 | defer c.Unlock() | 219 | defer c.Unlock() |
| ... | @@ -241,18 +232,20 @@ func (c *JsonConfigContainer) DIY(key string) (v interface{}, err error) { | ... | @@ -241,18 +232,20 @@ func (c *JsonConfigContainer) DIY(key string) (v interface{}, err error) { |
| 241 | 232 | ||
| 242 | // section.key or key | 233 | // section.key or key |
| 243 | func (c *JsonConfigContainer) getData(key string) interface{} { | 234 | func (c *JsonConfigContainer) getData(key string) interface{} { |
| 244 | c.RLock() | ||
| 245 | defer c.RUnlock() | ||
| 246 | if len(key) == 0 { | 235 | if len(key) == 0 { |
| 247 | return nil | 236 | return nil |
| 248 | } | 237 | } |
| 249 | sectionKey := strings.Split(key, "::") | 238 | |
| 250 | if len(sectionKey) >= 2 { | 239 | c.RLock() |
| 251 | curValue, ok := c.data[sectionKey[0]] | 240 | defer c.RUnlock() |
| 241 | |||
| 242 | sectionKeys := strings.Split(key, "::") | ||
| 243 | if len(sectionKeys) >= 2 { | ||
| 244 | curValue, ok := c.data[sectionKeys[0]] | ||
| 252 | if !ok { | 245 | if !ok { |
| 253 | return nil | 246 | return nil |
| 254 | } | 247 | } |
| 255 | for _, key := range sectionKey[1:] { | 248 | for _, key := range sectionKeys[1:] { |
| 256 | if v, ok := curValue.(map[string]interface{}); ok { | 249 | if v, ok := curValue.(map[string]interface{}); ok { |
| 257 | if curValue, ok = v[key]; !ok { | 250 | if curValue, ok = v[key]; !ok { |
| 258 | return nil | 251 | return nil | ... | ... |
| ... | @@ -21,6 +21,7 @@ import ( | ... | @@ -21,6 +21,7 @@ import ( |
| 21 | 21 | ||
| 22 | var jsoncontext = `{ | 22 | var jsoncontext = `{ |
| 23 | "appname": "beeapi", | 23 | "appname": "beeapi", |
| 24 | "testnames": "foo;bar", | ||
| 24 | "httpport": 8080, | 25 | "httpport": 8080, |
| 25 | "mysqlport": 3600, | 26 | "mysqlport": 3600, |
| 26 | "PI": 3.1415976, | 27 | "PI": 3.1415976, |
| ... | @@ -28,8 +29,8 @@ var jsoncontext = `{ | ... | @@ -28,8 +29,8 @@ var jsoncontext = `{ |
| 28 | "autorender": false, | 29 | "autorender": false, |
| 29 | "copyrequestbody": true, | 30 | "copyrequestbody": true, |
| 30 | "database": { | 31 | "database": { |
| 31 | "host": "host", | 32 | "host": "host", |
| 32 | "port": "port", | 33 | "port": "port", |
| 33 | "database": "database", | 34 | "database": "database", |
| 34 | "username": "username", | 35 | "username": "username", |
| 35 | "password": "password", | 36 | "password": "password", |
| ... | @@ -122,6 +123,12 @@ func TestJson(t *testing.T) { | ... | @@ -122,6 +123,12 @@ func TestJson(t *testing.T) { |
| 122 | if jsonconf.String("runmode") != "dev" { | 123 | if jsonconf.String("runmode") != "dev" { |
| 123 | t.Fatal("runmode not equal to dev") | 124 | t.Fatal("runmode not equal to dev") |
| 124 | } | 125 | } |
| 126 | if v := jsonconf.Strings("unknown"); len(v) > 0 { | ||
| 127 | t.Fatal("unknown strings, the length should be 0") | ||
| 128 | } | ||
| 129 | if v := jsonconf.Strings("testnames"); len(v) != 2 { | ||
| 130 | t.Fatal("testnames length should be 2") | ||
| 131 | } | ||
| 125 | if v, err := jsonconf.Bool("autorender"); err != nil || v != false { | 132 | if v, err := jsonconf.Bool("autorender"); err != nil || v != false { |
| 126 | t.Error(v) | 133 | t.Error(v) |
| 127 | t.Fatal(err) | 134 | t.Fatal(err) |
| ... | @@ -179,4 +186,8 @@ func TestJson(t *testing.T) { | ... | @@ -179,4 +186,8 @@ func TestJson(t *testing.T) { |
| 179 | if _, err := jsonconf.Bool("unknown"); err == nil { | 186 | if _, err := jsonconf.Bool("unknown"); err == nil { |
| 180 | t.Error("unknown keys should return an error when expecting a Bool") | 187 | t.Error("unknown keys should return an error when expecting a Bool") |
| 181 | } | 188 | } |
| 189 | |||
| 190 | if !jsonconf.DefaultBool("unknow", true) { | ||
| 191 | t.Error("unknown keys with default value wrong") | ||
| 192 | } | ||
| 182 | } | 193 | } | ... | ... |
| ... | @@ -27,7 +27,7 @@ import ( | ... | @@ -27,7 +27,7 @@ import ( |
| 27 | "github.com/astaxie/beego/session" | 27 | "github.com/astaxie/beego/session" |
| 28 | ) | 28 | ) |
| 29 | 29 | ||
| 30 | // BeegoInput operates the http request header ,data ,cookie and body. | 30 | // BeegoInput operates the http request header, data, cookie and body. |
| 31 | // it also contains router params and current session. | 31 | // it also contains router params and current session. |
| 32 | type BeegoInput struct { | 32 | type BeegoInput struct { |
| 33 | CruSession session.SessionStore | 33 | CruSession session.SessionStore |
| ... | @@ -153,12 +153,12 @@ func (input *BeegoInput) IsSecure() bool { | ... | @@ -153,12 +153,12 @@ func (input *BeegoInput) IsSecure() bool { |
| 153 | return input.Scheme() == "https" | 153 | return input.Scheme() == "https" |
| 154 | } | 154 | } |
| 155 | 155 | ||
| 156 | // IsSecure returns boolean of this request is in webSocket. | 156 | // IsWebsocket returns boolean of this request is in webSocket. |
| 157 | func (input *BeegoInput) IsWebsocket() bool { | 157 | func (input *BeegoInput) IsWebsocket() bool { |
| 158 | return input.Header("Upgrade") == "websocket" | 158 | return input.Header("Upgrade") == "websocket" |
| 159 | } | 159 | } |
| 160 | 160 | ||
| 161 | // IsSecure returns boolean of whether file uploads in this request or not.. | 161 | // IsUpload returns boolean of whether file uploads in this request or not.. |
| 162 | func (input *BeegoInput) IsUpload() bool { | 162 | func (input *BeegoInput) IsUpload() bool { |
| 163 | return strings.Contains(input.Header("Content-Type"), "multipart/form-data") | 163 | return strings.Contains(input.Header("Content-Type"), "multipart/form-data") |
| 164 | } | 164 | } |
| ... | @@ -189,16 +189,24 @@ func (input *BeegoInput) Proxy() []string { | ... | @@ -189,16 +189,24 @@ func (input *BeegoInput) Proxy() []string { |
| 189 | return []string{} | 189 | return []string{} |
| 190 | } | 190 | } |
| 191 | 191 | ||
| 192 | // Referer returns http referer header. | ||
| 193 | func (input *BeegoInput) Referer() string { | ||
| 194 | return input.Header("Referer") | ||
| 195 | } | ||
| 196 | |||
| 192 | // Refer returns http referer header. | 197 | // Refer returns http referer header. |
| 193 | func (input *BeegoInput) Refer() string { | 198 | func (input *BeegoInput) Refer() string { |
| 194 | return input.Header("Referer") | 199 | return input.Referer() |
| 195 | } | 200 | } |
| 196 | 201 | ||
| 197 | // SubDomains returns sub domain string. | 202 | // SubDomains returns sub domain string. |
| 198 | // if aa.bb.domain.com, returns aa.bb . | 203 | // if aa.bb.domain.com, returns aa.bb . |
| 199 | func (input *BeegoInput) SubDomains() string { | 204 | func (input *BeegoInput) SubDomains() string { |
| 200 | parts := strings.Split(input.Host(), ".") | 205 | parts := strings.Split(input.Host(), ".") |
| 201 | return strings.Join(parts[:len(parts)-2], ".") | 206 | if len(parts) >= 3 { |
| 207 | return strings.Join(parts[:len(parts)-2], ".") | ||
| 208 | } | ||
| 209 | return "" | ||
| 202 | } | 210 | } |
| 203 | 211 | ||
| 204 | // Port returns request client port. | 212 | // Port returns request client port. |
| ... | @@ -237,6 +245,7 @@ func (input *BeegoInput) Query(key string) string { | ... | @@ -237,6 +245,7 @@ func (input *BeegoInput) Query(key string) string { |
| 237 | } | 245 | } |
| 238 | 246 | ||
| 239 | // Header returns request header item string by a given string. | 247 | // Header returns request header item string by a given string. |
| 248 | // if non-existed, return empty string. | ||
| 240 | func (input *BeegoInput) Header(key string) string { | 249 | func (input *BeegoInput) Header(key string) string { |
| 241 | return input.Request.Header.Get(key) | 250 | return input.Request.Header.Get(key) |
| 242 | } | 251 | } |
| ... | @@ -252,11 +261,12 @@ func (input *BeegoInput) Cookie(key string) string { | ... | @@ -252,11 +261,12 @@ func (input *BeegoInput) Cookie(key string) string { |
| 252 | } | 261 | } |
| 253 | 262 | ||
| 254 | // Session returns current session item value by a given key. | 263 | // Session returns current session item value by a given key. |
| 264 | // if non-existed, return empty string. | ||
| 255 | func (input *BeegoInput) Session(key interface{}) interface{} { | 265 | func (input *BeegoInput) Session(key interface{}) interface{} { |
| 256 | return input.CruSession.Get(key) | 266 | return input.CruSession.Get(key) |
| 257 | } | 267 | } |
| 258 | 268 | ||
| 259 | // Body returns the raw request body data as bytes. | 269 | // CopyBody returns the raw request body data as bytes. |
| 260 | func (input *BeegoInput) CopyBody() []byte { | 270 | func (input *BeegoInput) CopyBody() []byte { |
| 261 | requestbody, _ := ioutil.ReadAll(input.Request.Body) | 271 | requestbody, _ := ioutil.ReadAll(input.Request.Body) |
| 262 | input.Request.Body.Close() | 272 | input.Request.Body.Close() | ... | ... |
| ... | @@ -70,3 +70,45 @@ func TestParse(t *testing.T) { | ... | @@ -70,3 +70,45 @@ func TestParse(t *testing.T) { |
| 70 | } | 70 | } |
| 71 | fmt.Println(user) | 71 | fmt.Println(user) |
| 72 | } | 72 | } |
| 73 | |||
| 74 | func TestSubDomain(t *testing.T) { | ||
| 75 | r, _ := http.NewRequest("GET", "http://www.example.com/?id=123&isok=true&ft=1.2&ol[0]=1&ol[1]=2&ul[]=str&ul[]=array&user.Name=astaxie", nil) | ||
| 76 | beegoInput := NewInput(r) | ||
| 77 | |||
| 78 | subdomain := beegoInput.SubDomains() | ||
| 79 | if subdomain != "www" { | ||
| 80 | t.Fatal("Subdomain parse error, got" + subdomain) | ||
| 81 | } | ||
| 82 | |||
| 83 | r, _ = http.NewRequest("GET", "http://localhost/", nil) | ||
| 84 | beegoInput.Request = r | ||
| 85 | if beegoInput.SubDomains() != "" { | ||
| 86 | t.Fatal("Subdomain parse error, should be empty, got " + beegoInput.SubDomains()) | ||
| 87 | } | ||
| 88 | |||
| 89 | r, _ = http.NewRequest("GET", "http://aa.bb.example.com/", nil) | ||
| 90 | beegoInput.Request = r | ||
| 91 | if beegoInput.SubDomains() != "aa.bb" { | ||
| 92 | t.Fatal("Subdomain parse error, got " + beegoInput.SubDomains()) | ||
| 93 | } | ||
| 94 | |||
| 95 | /* TODO Fix this | ||
| 96 | r, _ = http.NewRequest("GET", "http://127.0.0.1/", nil) | ||
| 97 | beegoInput.Request = r | ||
| 98 | if beegoInput.SubDomains() != "" { | ||
| 99 | t.Fatal("Subdomain parse error, got " + beegoInput.SubDomains()) | ||
| 100 | } | ||
| 101 | */ | ||
| 102 | |||
| 103 | r, _ = http.NewRequest("GET", "http://example.com/", nil) | ||
| 104 | beegoInput.Request = r | ||
| 105 | if beegoInput.SubDomains() != "" { | ||
| 106 | t.Fatal("Subdomain parse error, got " + beegoInput.SubDomains()) | ||
| 107 | } | ||
| 108 | |||
| 109 | r, _ = http.NewRequest("GET", "http://aa.bb.cc.dd.example.com/", nil) | ||
| 110 | beegoInput.Request = r | ||
| 111 | if beegoInput.SubDomains() != "aa.bb.cc.dd" { | ||
| 112 | t.Fatal("Subdomain parse error, got " + beegoInput.SubDomains()) | ||
| 113 | } | ||
| 114 | } | ... | ... |
-
Please register or sign in to post a comment