mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
New XMLMap func which makes a map value type an xml compatible content to render and give option to render a Problem as XML - rel to #1335
Former-commit-id: dcf21098ff7af6becfa9896df5f82c3b0b53f0ac
This commit is contained in:
parent
8d188817a6
commit
75ea978194
|
@ -87,6 +87,11 @@ func problemExample(ctx iris.Context) {
|
||||||
JSON: iris.JSON{
|
JSON: iris.JSON{
|
||||||
Indent: " ",
|
Indent: " ",
|
||||||
},
|
},
|
||||||
|
// OR
|
||||||
|
// Render as XML:
|
||||||
|
// RenderXML: true,
|
||||||
|
// XML: iris.XML{Indent: " "},
|
||||||
|
//
|
||||||
// Sets the "Retry-After" response header.
|
// Sets the "Retry-After" response header.
|
||||||
//
|
//
|
||||||
// Can accept:
|
// Can accept:
|
||||||
|
|
|
@ -778,19 +778,23 @@ type Context interface {
|
||||||
HTML(format string, args ...interface{}) (int, error)
|
HTML(format string, args ...interface{}) (int, error)
|
||||||
// JSON marshals the given interface object and writes the JSON response.
|
// JSON marshals the given interface object and writes the JSON response.
|
||||||
JSON(v interface{}, options ...JSON) (int, error)
|
JSON(v interface{}, options ...JSON) (int, error)
|
||||||
// Problem writes a JSON problem response.
|
// JSONP marshals the given interface object and writes the JSON response.
|
||||||
|
JSONP(v interface{}, options ...JSONP) (int, error)
|
||||||
|
// XML marshals the given interface object and writes the XML response.
|
||||||
|
// To render maps as XML see the `XMLMap` package-level function.
|
||||||
|
XML(v interface{}, options ...XML) (int, error)
|
||||||
|
// Problem writes a JSON or XML problem response.
|
||||||
// Order of Problem fields are not always rendered the same.
|
// Order of Problem fields are not always rendered the same.
|
||||||
//
|
//
|
||||||
// Behaves exactly like `Context.JSON`
|
// Behaves exactly like `Context.JSON`
|
||||||
// but with default ProblemOptions.JSON indent of " " and
|
// but with default ProblemOptions.JSON indent of " " and
|
||||||
// a response content type of "application/problem+json" instead.
|
// a response content type of "application/problem+json" instead.
|
||||||
//
|
//
|
||||||
|
// Use the options.RenderXML and XML fields to change this behavior and
|
||||||
|
// send a response of content type "application/problem+xml" instead.
|
||||||
|
//
|
||||||
// Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers
|
// Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers
|
||||||
Problem(v interface{}, opts ...ProblemOptions) (int, error)
|
Problem(v interface{}, opts ...ProblemOptions) (int, error)
|
||||||
// JSONP marshals the given interface object and writes the JSON response.
|
|
||||||
JSONP(v interface{}, options ...JSONP) (int, error)
|
|
||||||
// XML marshals the given interface object and writes the XML response.
|
|
||||||
XML(v interface{}, options ...XML) (int, error)
|
|
||||||
// Markdown parses the markdown to html and renders its result to the client.
|
// Markdown parses the markdown to html and renders its result to the client.
|
||||||
Markdown(markdownB []byte, options ...Markdown) (int, error)
|
Markdown(markdownB []byte, options ...Markdown) (int, error)
|
||||||
// YAML parses the "v" using the yaml parser and renders its result to the client.
|
// YAML parses the "v" using the yaml parser and renders its result to the client.
|
||||||
|
@ -3015,9 +3019,12 @@ const (
|
||||||
ContentHTMLHeaderValue = "text/html"
|
ContentHTMLHeaderValue = "text/html"
|
||||||
// ContentJSONHeaderValue header value for JSON data.
|
// ContentJSONHeaderValue header value for JSON data.
|
||||||
ContentJSONHeaderValue = "application/json"
|
ContentJSONHeaderValue = "application/json"
|
||||||
// ContentJSONProblemHeaderValue header value for API problem error.
|
// ContentJSONProblemHeaderValue header value for JSON API problem error.
|
||||||
// Read more at: https://tools.ietf.org/html/rfc7807
|
// Read more at: https://tools.ietf.org/html/rfc7807
|
||||||
ContentJSONProblemHeaderValue = "application/problem+json"
|
ContentJSONProblemHeaderValue = "application/problem+json"
|
||||||
|
// ContentXMLProblemHeaderValue header value for XML API problem error.
|
||||||
|
// Read more at: https://tools.ietf.org/html/rfc7807
|
||||||
|
ContentXMLProblemHeaderValue = "application/problem+xml"
|
||||||
// ContentJavascriptHeaderValue header value for JSONP & Javascript data.
|
// ContentJavascriptHeaderValue header value for JSONP & Javascript data.
|
||||||
ContentJavascriptHeaderValue = "application/javascript"
|
ContentJavascriptHeaderValue = "application/javascript"
|
||||||
// ContentTextHeaderValue header value for Text data.
|
// ContentTextHeaderValue header value for Text data.
|
||||||
|
@ -3187,35 +3194,6 @@ func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Problem writes a JSON problem response.
|
|
||||||
// Order of Problem fields are not always rendered the same.
|
|
||||||
//
|
|
||||||
// Behaves exactly like `Context.JSON`
|
|
||||||
// but with default ProblemOptions.JSON indent of " " and
|
|
||||||
// a response content type of "application/problem+json" instead.
|
|
||||||
//
|
|
||||||
// Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers
|
|
||||||
func (ctx *context) Problem(v interface{}, opts ...ProblemOptions) (int, error) {
|
|
||||||
options := DefaultProblemOptions
|
|
||||||
if len(opts) > 0 {
|
|
||||||
options = opts[0]
|
|
||||||
// Currently apply only if custom options passsed, otherwise,
|
|
||||||
// with the current settings, it's not required.
|
|
||||||
// This may change in the future though.
|
|
||||||
options.Apply(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p, ok := v.(Problem); ok {
|
|
||||||
p.updateTypeToAbsolute(ctx)
|
|
||||||
code, _ := p.getStatus()
|
|
||||||
ctx.StatusCode(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.contentTypeOnce(ContentJSONProblemHeaderValue, "")
|
|
||||||
|
|
||||||
return ctx.JSON(v, options.JSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
finishCallbackB = []byte(");")
|
finishCallbackB = []byte(");")
|
||||||
)
|
)
|
||||||
|
@ -3279,6 +3257,46 @@ func (ctx *context) JSONP(v interface{}, opts ...JSONP) (int, error) {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type xmlMapEntry struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
Value interface{} `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLMap wraps a map[string]interface{} to compatible xml marshaler,
|
||||||
|
// in order to be able to render maps as XML on the `Context.XML` method.
|
||||||
|
//
|
||||||
|
// Example: `Context.XML(XMLMap("Root", map[string]interface{}{...})`.
|
||||||
|
func XMLMap(elementName string, v Map) xml.Marshaler {
|
||||||
|
return xmlMap{
|
||||||
|
entries: v,
|
||||||
|
elementName: elementName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlMap struct {
|
||||||
|
entries Map
|
||||||
|
elementName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalXML marshals a map to XML.
|
||||||
|
func (m xmlMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if len(m.entries) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
start.Name = xml.Name{Local: m.elementName}
|
||||||
|
err := e.EncodeToken(start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range m.entries {
|
||||||
|
e.Encode(xmlMapEntry{XMLName: xml.Name{Local: k}, Value: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.EncodeToken(start.End())
|
||||||
|
}
|
||||||
|
|
||||||
// WriteXML marshals the given interface object and writes the XML response to the writer.
|
// WriteXML marshals the given interface object and writes the XML response to the writer.
|
||||||
func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
|
func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
|
||||||
if prefix := options.Prefix; prefix != "" {
|
if prefix := options.Prefix; prefix != "" {
|
||||||
|
@ -3306,6 +3324,7 @@ func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
|
||||||
var DefaultXMLOptions = XML{}
|
var DefaultXMLOptions = XML{}
|
||||||
|
|
||||||
// XML marshals the given interface object and writes the XML response to the client.
|
// XML marshals the given interface object and writes the XML response to the client.
|
||||||
|
// To render maps as XML see the `XMLMap` package-level function.
|
||||||
func (ctx *context) XML(v interface{}, opts ...XML) (int, error) {
|
func (ctx *context) XML(v interface{}, opts ...XML) (int, error) {
|
||||||
options := DefaultXMLOptions
|
options := DefaultXMLOptions
|
||||||
|
|
||||||
|
@ -3325,6 +3344,47 @@ func (ctx *context) XML(v interface{}, opts ...XML) (int, error) {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Problem writes a JSON or XML problem response.
|
||||||
|
// Order of Problem fields are not always rendered the same.
|
||||||
|
//
|
||||||
|
// Behaves exactly like `Context.JSON`
|
||||||
|
// but with default ProblemOptions.JSON indent of " " and
|
||||||
|
// a response content type of "application/problem+json" instead.
|
||||||
|
//
|
||||||
|
// Use the options.RenderXML and XML fields to change this behavior and
|
||||||
|
// send a response of content type "application/problem+xml" instead.
|
||||||
|
//
|
||||||
|
// Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers
|
||||||
|
func (ctx *context) Problem(v interface{}, opts ...ProblemOptions) (int, error) {
|
||||||
|
options := DefaultProblemOptions
|
||||||
|
if len(opts) > 0 {
|
||||||
|
options = opts[0]
|
||||||
|
// Currently apply only if custom options passsed, otherwise,
|
||||||
|
// with the current settings, it's not required.
|
||||||
|
// This may change in the future though.
|
||||||
|
options.Apply(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p, ok := v.(Problem); ok {
|
||||||
|
// if !p.Validate() {
|
||||||
|
// ctx.StatusCode(http.StatusInternalServerError)
|
||||||
|
// return ErrNotValidProblem
|
||||||
|
// }
|
||||||
|
p.updateURIsToAbs(ctx)
|
||||||
|
code, _ := p.getStatus()
|
||||||
|
ctx.StatusCode(code)
|
||||||
|
|
||||||
|
if options.RenderXML {
|
||||||
|
ctx.contentTypeOnce(ContentXMLProblemHeaderValue, "")
|
||||||
|
// Problem is an xml Marshaler already, don't use `XMLMap`.
|
||||||
|
return ctx.XML(v, options.XML)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.contentTypeOnce(ContentJSONProblemHeaderValue, "")
|
||||||
|
return ctx.JSON(v, options.JSON)
|
||||||
|
}
|
||||||
|
|
||||||
// WriteMarkdown parses the markdown to html and writes these contents to the writer.
|
// WriteMarkdown parses the markdown to html and writes these contents to the writer.
|
||||||
func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, error) {
|
func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, error) {
|
||||||
buf := blackfriday.Run(markdownB)
|
buf := blackfriday.Run(markdownB)
|
||||||
|
@ -3571,7 +3631,7 @@ func (ctx *context) Negotiate(v interface{}) (int, error) {
|
||||||
return ctx.Markdown(v.([]byte))
|
return ctx.Markdown(v.([]byte))
|
||||||
case ContentJSONHeaderValue:
|
case ContentJSONHeaderValue:
|
||||||
return ctx.JSON(v)
|
return ctx.JSON(v)
|
||||||
case ContentJSONProblemHeaderValue:
|
case ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue:
|
||||||
return ctx.Problem(v)
|
return ctx.Problem(v)
|
||||||
case ContentJavascriptHeaderValue:
|
case ContentJavascriptHeaderValue:
|
||||||
return ctx.JSONP(v)
|
return ctx.JSONP(v)
|
||||||
|
@ -3702,9 +3762,9 @@ func (n *NegotiationBuilder) JSON(v ...interface{}) *NegotiationBuilder {
|
||||||
return n.MIME(ContentJSONHeaderValue, content)
|
return n.MIME(ContentJSONHeaderValue, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Problem registers the "application/problem+json" content type and, optionally,
|
// Problem registers the "application/problem+xml" or "application/problem+xml" content type and, optionally,
|
||||||
// a value that `Context.Negotiate` will render
|
// a value that `Context.Negotiate` will render
|
||||||
// when a client accepts the "application/problem+json" content type.
|
// when a client accepts the "application/problem+json" or the "application/problem+xml" content type.
|
||||||
//
|
//
|
||||||
// Returns itself for recursive calls.
|
// Returns itself for recursive calls.
|
||||||
func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder {
|
func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder {
|
||||||
|
@ -3712,7 +3772,7 @@ func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder {
|
||||||
if len(v) > 0 {
|
if len(v) > 0 {
|
||||||
content = v[0]
|
content = v[0]
|
||||||
}
|
}
|
||||||
return n.MIME(ContentJSONProblemHeaderValue, content)
|
return n.MIME(ContentJSONProblemHeaderValue+","+ContentXMLProblemHeaderValue, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONP registers the "application/javascript" content type and, optionally,
|
// JSONP registers the "application/javascript" content type and, optionally,
|
||||||
|
@ -3968,10 +4028,11 @@ func (n *NegotiationAcceptBuilder) JSON() *NegotiationAcceptBuilder {
|
||||||
return n.MIME(ContentJSONHeaderValue)
|
return n.MIME(ContentJSONHeaderValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Problem adds the "application/problem+json" as accepted client content type.
|
// Problem adds the "application/problem+json" and "application/problem-xml"
|
||||||
|
// as accepted client content types.
|
||||||
// Returns itself.
|
// Returns itself.
|
||||||
func (n *NegotiationAcceptBuilder) Problem() *NegotiationAcceptBuilder {
|
func (n *NegotiationAcceptBuilder) Problem() *NegotiationAcceptBuilder {
|
||||||
return n.MIME(ContentJSONProblemHeaderValue)
|
return n.MIME(ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONP adds the "application/javascript" as accepted client content type.
|
// JSONP adds the "application/javascript" as accepted client content type.
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -57,10 +59,10 @@ func isEmptyTypeURI(uri string) bool {
|
||||||
return uri == "" || uri == "about:blank"
|
return uri == "" || uri == "about:blank"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Problem) getType() string {
|
func (p Problem) getURI(key string) string {
|
||||||
typeField, found := p["type"]
|
f, found := p[key]
|
||||||
if found {
|
if found {
|
||||||
if typ, ok := typeField.(string); ok {
|
if typ, ok := f.(string); ok {
|
||||||
if !isEmptyTypeURI(typ) {
|
if !isEmptyTypeURI(typ) {
|
||||||
return typ
|
return typ
|
||||||
}
|
}
|
||||||
|
@ -71,18 +73,22 @@ func (p Problem) getType() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates "type" field to absolute URI, recursively.
|
// Updates "type" field to absolute URI, recursively.
|
||||||
func (p Problem) updateTypeToAbsolute(ctx Context) {
|
func (p Problem) updateURIsToAbs(ctx Context) {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if uriRef := p.getType(); uriRef != "" {
|
if uriRef := p.getURI("type"); uriRef != "" {
|
||||||
p.Type(ctx.AbsoluteURI(uriRef))
|
p.Type(ctx.AbsoluteURI(uriRef))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if uriRef := p.getURI("instance"); uriRef != "" {
|
||||||
|
p.Instance(ctx.AbsoluteURI(uriRef))
|
||||||
|
}
|
||||||
|
|
||||||
if cause, ok := p["cause"]; ok {
|
if cause, ok := p["cause"]; ok {
|
||||||
if causeP, ok := cause.(Problem); ok {
|
if causeP, ok := cause.(Problem); ok {
|
||||||
causeP.updateTypeToAbsolute(ctx)
|
causeP.updateURIsToAbs(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,6 +169,14 @@ func (p Problem) Detail(detail string) Problem {
|
||||||
return p.Key("detail", detail)
|
return p.Key("detail", detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instance sets the problem's instance field.
|
||||||
|
// A URI reference that identifies the specific
|
||||||
|
// occurrence of the problem. It may or may not yield further
|
||||||
|
// information if dereferenced.
|
||||||
|
func (p Problem) Instance(instanceURI string) Problem {
|
||||||
|
return p.Key("instance", instanceURI)
|
||||||
|
}
|
||||||
|
|
||||||
// Cause sets the problem's cause field.
|
// Cause sets the problem's cause field.
|
||||||
// Any chain of problems.
|
// Any chain of problems.
|
||||||
func (p Problem) Cause(cause Problem) Problem {
|
func (p Problem) Cause(cause Problem) Problem {
|
||||||
|
@ -196,9 +210,29 @@ func (p Problem) Error() string {
|
||||||
return fmt.Sprintf("[%d] %s", p["status"], p["title"])
|
return fmt.Sprintf("[%d] %s", p["status"], p["title"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML makes this Problem XML-compatible content to render.
|
||||||
|
func (p Problem) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.EncodeToken(start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range p {
|
||||||
|
// convert keys like "type" to "Type", "productName" to "ProductName" and e.t.c. when xml.
|
||||||
|
e.Encode(xmlMapEntry{XMLName: xml.Name{Local: strings.Title(k)}, Value: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.EncodeToken(start.End())
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultProblemOptions the default options for `Context.Problem` method.
|
// DefaultProblemOptions the default options for `Context.Problem` method.
|
||||||
var DefaultProblemOptions = ProblemOptions{
|
var DefaultProblemOptions = ProblemOptions{
|
||||||
JSON: JSON{Indent: " "},
|
JSON: JSON{Indent: " "},
|
||||||
|
XML: XML{Indent: " "},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProblemOptions the optional settings when server replies with a Problem.
|
// ProblemOptions the optional settings when server replies with a Problem.
|
||||||
|
@ -207,6 +241,13 @@ type ProblemOptions struct {
|
||||||
// JSON are the optional JSON renderer options.
|
// JSON are the optional JSON renderer options.
|
||||||
JSON JSON
|
JSON JSON
|
||||||
|
|
||||||
|
// RenderXML set to true if want to render as XML doc.
|
||||||
|
// See `XML` option field too.
|
||||||
|
RenderXML bool
|
||||||
|
// XML are the optional XML renderer options.
|
||||||
|
// Affect only when `RenderXML` field is set to true.
|
||||||
|
XML XML
|
||||||
|
|
||||||
// RetryAfter sets the Retry-After response header.
|
// RetryAfter sets the Retry-After response header.
|
||||||
// https://tools.ietf.org/html/rfc7231#section-7.1.3
|
// https://tools.ietf.org/html/rfc7231#section-7.1.3
|
||||||
// The value can be one of those:
|
// The value can be one of those:
|
||||||
|
|
5
go19.go
5
go19.go
|
@ -67,7 +67,10 @@ type (
|
||||||
//
|
//
|
||||||
// It is an alias of the `context#JSON` type.
|
// It is an alias of the `context#JSON` type.
|
||||||
JSON = context.JSON
|
JSON = context.JSON
|
||||||
|
// XML the optional settings for XML renderer.
|
||||||
|
//
|
||||||
|
// It is an alias of the `context#XML` type.
|
||||||
|
XML = context.XML
|
||||||
// Supervisor is a shortcut of the `host#Supervisor`.
|
// Supervisor is a shortcut of the `host#Supervisor`.
|
||||||
// Used to add supervisor configurators on common Runners
|
// Used to add supervisor configurators on common Runners
|
||||||
// without the need of importing the `core/host` package.
|
// without the need of importing the `core/host` package.
|
||||||
|
|
7
iris.go
7
iris.go
|
@ -503,6 +503,13 @@ var (
|
||||||
//
|
//
|
||||||
// A shortcut for the `context#NewProblem`.
|
// A shortcut for the `context#NewProblem`.
|
||||||
NewProblem = context.NewProblem
|
NewProblem = context.NewProblem
|
||||||
|
// XMLMap wraps a map[string]interface{} to compatible xml marshaler,
|
||||||
|
// in order to be able to render maps as XML on the `Context.XML` method.
|
||||||
|
//
|
||||||
|
// Example: `Context.XML(XMLMap("Root", map[string]interface{}{...})`.
|
||||||
|
//
|
||||||
|
// A shortcut for the `context#XMLMap`.
|
||||||
|
XMLMap = context.XMLMap
|
||||||
)
|
)
|
||||||
|
|
||||||
// Contains the enum values of the `Context.GetReferrer()` method,
|
// Contains the enum values of the `Context.GetReferrer()` method,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user