#539: Make registration of namespaces actually work (Vladimir Nadvornik, S M Ryan)
This commit is contained in:
parent
1b1df1e649
commit
2e3672d211
@ -54,6 +54,9 @@ set Xmp.tiff.ImageDescription lang=de-DE TIFF Bildbeschreibung
|
|||||||
# Register a namespace which Exiv2 doesn't know yet with a prefix.
|
# Register a namespace which Exiv2 doesn't know yet with a prefix.
|
||||||
reg ns myNamespace/
|
reg ns myNamespace/
|
||||||
|
|
||||||
|
# Add a property in the new custom namespace.
|
||||||
|
set Xmp.ns.myProperty myValue
|
||||||
|
|
||||||
# There are no built-in Exiv2 value types for structures, qualifiers and
|
# There are no built-in Exiv2 value types for structures, qualifiers and
|
||||||
# nested types. However, these can be added by using an XmpText value and a
|
# nested types. However, these can be added by using an XmpText value and a
|
||||||
# path as the key.
|
# path as the key.
|
||||||
@ -65,7 +68,7 @@ set Xmp.xmpDM.videoFrameSize/stDim:unit inch
|
|||||||
|
|
||||||
# Add an element with a qualifier (using the namespace registered earlier)
|
# Add an element with a qualifier (using the namespace registered earlier)
|
||||||
set Xmp.dc.publisher James Bond
|
set Xmp.dc.publisher James Bond
|
||||||
set Xmp.dc.publisher/?ns:role secret agent
|
set Xmp.dc.publisher[1]/?ns:role secret agent
|
||||||
|
|
||||||
# Add a qualifer to an array element of Xmp.dc.creator (added above)
|
# Add a qualifer to an array element of Xmp.dc.creator (added above)
|
||||||
set Xmp.dc.creator[2]/?ns:role programmer
|
set Xmp.dc.creator[2]/?ns:role programmer
|
||||||
|
|||||||
@ -75,6 +75,10 @@ try {
|
|||||||
// image, namespaces are decoded and registered at the same time.
|
// image, namespaces are decoded and registered at the same time.
|
||||||
Exiv2::XmpProperties::registerNs("myNamespace/", "ns");
|
Exiv2::XmpProperties::registerNs("myNamespace/", "ns");
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Add a property in the new custom namespace.
|
||||||
|
xmpData["Xmp.ns.myProperty"] = "myValue";
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// There are no specialized values for structures, qualifiers and nested
|
// There are no specialized values for structures, qualifiers and nested
|
||||||
// types. However, these can be added by using an XmpTextValue and a path as
|
// types. However, these can be added by using an XmpTextValue and a path as
|
||||||
|
|||||||
@ -1413,7 +1413,7 @@ namespace Action {
|
|||||||
std::cout << _("Reg ") << modifyCmd.key_ << "=\""
|
std::cout << _("Reg ") << modifyCmd.key_ << "=\""
|
||||||
<< modifyCmd.value_ << "\"" << std::endl;
|
<< modifyCmd.value_ << "\"" << std::endl;
|
||||||
}
|
}
|
||||||
Exiv2::XmpProperties::registerNs(modifyCmd.value_, modifyCmd.key_);
|
// Registration has been done immediately after parsing the command.
|
||||||
}
|
}
|
||||||
|
|
||||||
Modify::AutoPtr Modify::clone() const
|
Modify::AutoPtr Modify::clone() const
|
||||||
|
|||||||
@ -1015,6 +1015,12 @@ namespace {
|
|||||||
modifyCmd.explicitType_ = explicitType;
|
modifyCmd.explicitType_ = explicitType;
|
||||||
modifyCmd.value_ = value;
|
modifyCmd.value_ = value;
|
||||||
|
|
||||||
|
if (cmdId == reg) {
|
||||||
|
// Registration needs to be done immediately as the new namespaces are
|
||||||
|
// looked up during parsing of subsequent lines (to validate XMP keys).
|
||||||
|
Exiv2::XmpProperties::registerNs(modifyCmd.value_, modifyCmd.key_);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} // parseLine
|
} // parseLine
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,8 @@ EXIV2_RCSID("@(#) $Id$")
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
// *****************************************************************************
|
// *****************************************************************************
|
||||||
// class member definitions
|
// class member definitions
|
||||||
@ -812,16 +814,57 @@ namespace Exiv2 {
|
|||||||
|
|
||||||
XmpProperties::NsRegistry XmpProperties::nsRegistry_;
|
XmpProperties::NsRegistry XmpProperties::nsRegistry_;
|
||||||
|
|
||||||
|
const XmpNsInfo* XmpProperties::lookupNsRegistry(const XmpNsInfo::Prefix& prefix)
|
||||||
|
{
|
||||||
|
for (NsRegistry::const_iterator i = nsRegistry_.begin();
|
||||||
|
i != nsRegistry_.end(); ++i) {
|
||||||
|
if (i->second == prefix) return &(i->second);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void XmpProperties::registerNs(const std::string& ns,
|
void XmpProperties::registerNs(const std::string& ns,
|
||||||
const std::string& prefix)
|
const std::string& prefix)
|
||||||
{
|
{
|
||||||
std::string ns2 = ns;
|
std::string ns2 = ns;
|
||||||
if ( ns2.substr(ns2.size() - 1, 1) != "/"
|
if ( ns2.substr(ns2.size() - 1, 1) != "/"
|
||||||
&& ns2.substr(ns2.size() - 1, 1) != "#") ns2 += "/";
|
&& ns2.substr(ns2.size() - 1, 1) != "#") ns2 += "/";
|
||||||
nsRegistry_[ns2] = prefix;
|
// Allocated memory is freed when the namespace is unregistered.
|
||||||
|
// Using malloc/free for better system compatibility in case
|
||||||
|
// users don't unregister their namespaces explicitely.
|
||||||
|
XmpNsInfo xn;
|
||||||
|
char* c = static_cast<char*>(std::malloc(ns2.size() + 1));
|
||||||
|
std::strcpy(c, ns2.c_str());
|
||||||
|
xn.ns_ = c;
|
||||||
|
c = static_cast<char*>(std::malloc(prefix.size() + 1));
|
||||||
|
std::strcpy(c, prefix.c_str());
|
||||||
|
xn.prefix_ = c;
|
||||||
|
xn.xmpPropertyInfo_ = 0;
|
||||||
|
xn.desc_ = "";
|
||||||
|
nsRegistry_[ns2] = xn;
|
||||||
XmpParser::registerNs(ns2, prefix);
|
XmpParser::registerNs(ns2, prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void XmpProperties::unregisterNs(const std::string& ns)
|
||||||
|
{
|
||||||
|
NsRegistry::iterator i = nsRegistry_.find(ns);
|
||||||
|
if (i != nsRegistry_.end()) {
|
||||||
|
XmpParser::unregisterNs(ns);
|
||||||
|
std::free(const_cast<char*>(i->second.prefix_));
|
||||||
|
std::free(const_cast<char*>(i->second.ns_));
|
||||||
|
nsRegistry_.erase(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void XmpProperties::unregisterNs()
|
||||||
|
{
|
||||||
|
NsRegistry::iterator i = nsRegistry_.begin();
|
||||||
|
while (i != nsRegistry_.end()) {
|
||||||
|
NsRegistry::iterator kill = i++;
|
||||||
|
unregisterNs(kill->first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string XmpProperties::prefix(const std::string& ns)
|
std::string XmpProperties::prefix(const std::string& ns)
|
||||||
{
|
{
|
||||||
std::string ns2 = ns;
|
std::string ns2 = ns;
|
||||||
@ -829,7 +872,7 @@ namespace Exiv2 {
|
|||||||
NsRegistry::const_iterator i = nsRegistry_.find(ns2);
|
NsRegistry::const_iterator i = nsRegistry_.find(ns2);
|
||||||
std::string p;
|
std::string p;
|
||||||
if (i != nsRegistry_.end()) {
|
if (i != nsRegistry_.end()) {
|
||||||
p = i->second;
|
p = i->second.prefix_;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const XmpNsInfo* xn = find(xmpNsInfo, XmpNsInfo::Ns(ns2));
|
const XmpNsInfo* xn = find(xmpNsInfo, XmpNsInfo::Ns(ns2));
|
||||||
@ -840,13 +883,8 @@ namespace Exiv2 {
|
|||||||
|
|
||||||
std::string XmpProperties::ns(const std::string& prefix)
|
std::string XmpProperties::ns(const std::string& prefix)
|
||||||
{
|
{
|
||||||
std::string n;
|
const XmpNsInfo* xn = lookupNsRegistry(XmpNsInfo::Prefix(prefix));
|
||||||
for (NsRegistry::const_iterator i = nsRegistry_.begin();
|
if (xn != 0) return xn->ns_;
|
||||||
i != nsRegistry_.end(); ++i) {
|
|
||||||
if (i->second == prefix) {
|
|
||||||
return i->first;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nsInfo(prefix)->ns_;
|
return nsInfo(prefix)->ns_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -894,7 +932,9 @@ namespace Exiv2 {
|
|||||||
|
|
||||||
const XmpNsInfo* XmpProperties::nsInfo(const std::string& prefix)
|
const XmpNsInfo* XmpProperties::nsInfo(const std::string& prefix)
|
||||||
{
|
{
|
||||||
const XmpNsInfo* xn = find(xmpNsInfo, XmpNsInfo::Prefix(prefix));
|
const XmpNsInfo::Prefix pf(prefix);
|
||||||
|
const XmpNsInfo* xn = lookupNsRegistry(pf);
|
||||||
|
if (!xn) xn = find(xmpNsInfo, pf);
|
||||||
if (!xn) throw Error(35, prefix);
|
if (!xn) throw Error(35, prefix);
|
||||||
return xn;
|
return xn;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -181,13 +181,33 @@ namespace Exiv2 {
|
|||||||
@brief Register namespace \em ns with preferred prefix \em prefix.
|
@brief Register namespace \em ns with preferred prefix \em prefix.
|
||||||
|
|
||||||
If the namespace is a known or previously registered namespace, the
|
If the namespace is a known or previously registered namespace, the
|
||||||
prefix is overwritten. This also invalidates XMP keys generated with
|
prefix is overwritten.
|
||||||
the previous prefix.
|
|
||||||
|
@note This invalidates XMP keys generated with the previous prefix.
|
||||||
*/
|
*/
|
||||||
static void registerNs(const std::string& ns, const std::string& prefix);
|
static void registerNs(const std::string& ns, const std::string& prefix);
|
||||||
|
/*!
|
||||||
|
@brief Unregister a custom namespace \em ns.
|
||||||
|
|
||||||
|
The function only has an effect if there is a namespace \em ns
|
||||||
|
registered earlier, it does not unregister built-in namespaces.
|
||||||
|
|
||||||
|
@note This invalidates XMP keys generated in this namespace.
|
||||||
|
*/
|
||||||
|
static void unregisterNs(const std::string& ns);
|
||||||
|
/*!
|
||||||
|
@brief Unregister all custom namespaces.
|
||||||
|
|
||||||
|
The function only unregisters namespaces registered earlier, it does not
|
||||||
|
unregister built-in namespaces.
|
||||||
|
|
||||||
|
@note This invalidates XMP keys generated in any custom namespace.
|
||||||
|
*/
|
||||||
|
static void unregisterNs();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
typedef std::map<std::string, std::string> NsRegistry;
|
typedef std::map<std::string, XmpNsInfo> NsRegistry;
|
||||||
|
static const XmpNsInfo* lookupNsRegistry(const XmpNsInfo::Prefix& prefix);
|
||||||
static NsRegistry nsRegistry_;
|
static NsRegistry nsRegistry_;
|
||||||
|
|
||||||
}; // class XmpProperties
|
}; // class XmpProperties
|
||||||
|
|||||||
16
src/xmp.cpp
16
src/xmp.cpp
@ -371,6 +371,7 @@ namespace Exiv2 {
|
|||||||
|
|
||||||
void XmpParser::terminate()
|
void XmpParser::terminate()
|
||||||
{
|
{
|
||||||
|
XmpProperties::unregisterNs();
|
||||||
if (initialized_) {
|
if (initialized_) {
|
||||||
#ifdef EXV_HAVE_XMP_TOOLKIT
|
#ifdef EXV_HAVE_XMP_TOOLKIT
|
||||||
SXMPMeta::Terminate();
|
SXMPMeta::Terminate();
|
||||||
@ -394,7 +395,20 @@ namespace Exiv2 {
|
|||||||
initialize();
|
initialize();
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
} // XmpParser::registerNs
|
} // XmpParser::registerNs
|
||||||
|
|
||||||
|
void XmpParser::unregisterNs(const std::string& ns)
|
||||||
|
{
|
||||||
|
#ifdef EXV_HAVE_XMP_TOOLKIT
|
||||||
|
try {
|
||||||
|
// Throws XMP Toolkit error 8: Unimplemented method XMPMeta::DeleteNamespace
|
||||||
|
// SXMPMeta::DeleteNamespace(ns.c_str());
|
||||||
|
}
|
||||||
|
catch (const XMP_Error& e) {
|
||||||
|
throw Error(40, e.GetID(), e.GetErrMsg());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
} // XmpParser::unregisterNs
|
||||||
|
|
||||||
#ifdef EXV_HAVE_XMP_TOOLKIT
|
#ifdef EXV_HAVE_XMP_TOOLKIT
|
||||||
int XmpParser::decode( XmpData& xmpData,
|
int XmpParser::decode( XmpData& xmpData,
|
||||||
|
|||||||
@ -257,6 +257,7 @@ namespace Exiv2 {
|
|||||||
*/
|
*/
|
||||||
class XmpParser {
|
class XmpParser {
|
||||||
friend void XmpProperties::registerNs(const std::string&, const std::string&);
|
friend void XmpProperties::registerNs(const std::string&, const std::string&);
|
||||||
|
friend void XmpProperties::unregisterNs(const std::string&);
|
||||||
public:
|
public:
|
||||||
/*!
|
/*!
|
||||||
@brief Decode XMP metadata from an XMP packet \em xmpPacket into
|
@brief Decode XMP metadata from an XMP packet \em xmpPacket into
|
||||||
@ -299,7 +300,7 @@ namespace Exiv2 {
|
|||||||
*/
|
*/
|
||||||
static bool initialize();
|
static bool initialize();
|
||||||
/*!
|
/*!
|
||||||
@brief Terminate the XMP Toolkit.
|
@brief Terminate the XMP Toolkit and unregister custom namespaces.
|
||||||
|
|
||||||
Call this method when the XmpParser is no longer needed to
|
Call this method when the XmpParser is no longer needed to
|
||||||
allow the XMP Toolkit to cleanly shutdown.
|
allow the XMP Toolkit to cleanly shutdown.
|
||||||
@ -316,6 +317,12 @@ namespace Exiv2 {
|
|||||||
*/
|
*/
|
||||||
static bool registerNs(const std::string& ns,
|
static bool registerNs(const std::string& ns,
|
||||||
const std::string& prefix);
|
const std::string& prefix);
|
||||||
|
/*!
|
||||||
|
@brief Delete a namespace from the XMP Toolkit.
|
||||||
|
|
||||||
|
XmpProperties::unregisterNs calls this to synchronize namespaces.
|
||||||
|
*/
|
||||||
|
static void unregisterNs(const std::string& ns);
|
||||||
|
|
||||||
// DATA
|
// DATA
|
||||||
static bool initialized_; //! Indicates if the XMP Toolkit has been initialized
|
static bool initialized_; //! Indicates if the XMP Toolkit has been initialized
|
||||||
|
|||||||
@ -54,6 +54,9 @@ set Xmp.tiff.ImageDescription lang=de-DE TIFF Bildbeschreibung
|
|||||||
# Register a namespace which Exiv2 doesn't know yet with a prefix.
|
# Register a namespace which Exiv2 doesn't know yet with a prefix.
|
||||||
reg ns myNamespace/
|
reg ns myNamespace/
|
||||||
|
|
||||||
|
# Add a property in the new custom namespace.
|
||||||
|
set Xmp.ns.myProperty myValue
|
||||||
|
|
||||||
# There are no built-in Exiv2 value types for structures, qualifiers and
|
# There are no built-in Exiv2 value types for structures, qualifiers and
|
||||||
# nested types. However, these can be added by using an XmpText value and a
|
# nested types. However, these can be added by using an XmpText value and a
|
||||||
# path as the key.
|
# path as the key.
|
||||||
@ -65,7 +68,7 @@ set Xmp.xmpDM.videoFrameSize/stDim:unit inch
|
|||||||
|
|
||||||
# Add an element with a qualifier (using the namespace registered earlier)
|
# Add an element with a qualifier (using the namespace registered earlier)
|
||||||
set Xmp.dc.publisher James Bond
|
set Xmp.dc.publisher James Bond
|
||||||
set Xmp.dc.publisher/?ns:role secret agent
|
set Xmp.dc.publisher[1]/?ns:role secret agent
|
||||||
|
|
||||||
# Add a qualifer to an array element of Xmp.dc.creator (added above)
|
# Add a qualifer to an array element of Xmp.dc.creator (added above)
|
||||||
set Xmp.dc.creator[2]/?ns:role programmer
|
set Xmp.dc.creator[2]/?ns:role programmer
|
||||||
|
|||||||
@ -293,6 +293,7 @@ Xmp.dc.format XmpText 10 image/jpeg
|
|||||||
Xmp.dc.creator XmpSeq 3 1) The first creator, 2) The second creator, 3) And another one
|
Xmp.dc.creator XmpSeq 3 1) The first creator, 2) The second creator, 3) And another one
|
||||||
Xmp.dc.description LangAlt 2 lang="x-default" Hello, World, lang="de-DE" Hallo, Welt
|
Xmp.dc.description LangAlt 2 lang="x-default" Hello, World, lang="de-DE" Hallo, Welt
|
||||||
Xmp.tiff.ImageDescription LangAlt 2 lang="x-default" TIFF image description, lang="de-DE" TIFF Bildbeschreibung
|
Xmp.tiff.ImageDescription LangAlt 2 lang="x-default" TIFF image description, lang="de-DE" TIFF Bildbeschreibung
|
||||||
|
Xmp.ns.myProperty XmpText 7 myValue
|
||||||
Xmp.xmpDM.videoFrameSize/stDim:w XmpText 2 16
|
Xmp.xmpDM.videoFrameSize/stDim:w XmpText 2 16
|
||||||
Xmp.xmpDM.videoFrameSize/stDim:h XmpText 1 9
|
Xmp.xmpDM.videoFrameSize/stDim:h XmpText 1 9
|
||||||
Xmp.xmpDM.videoFrameSize/stDim:unit XmpText 4 inch
|
Xmp.xmpDM.videoFrameSize/stDim:unit XmpText 4 inch
|
||||||
@ -323,7 +324,8 @@ Xmp.xmpBJ.JobRef[2]/stJob:role XmpText 8 Best man
|
|||||||
dc:five="256"
|
dc:five="256"
|
||||||
dc:six="false"
|
dc:six="false"
|
||||||
dc:seven="Seven"
|
dc:seven="Seven"
|
||||||
dc:format="image/jpeg">
|
dc:format="image/jpeg"
|
||||||
|
ns:myProperty="myValue">
|
||||||
<dc:subject>
|
<dc:subject>
|
||||||
<rdf:Bag>
|
<rdf:Bag>
|
||||||
<rdf:li>Palmtree</rdf:li>
|
<rdf:li>Palmtree</rdf:li>
|
||||||
@ -427,11 +429,12 @@ Set Xmp.dc.description "Hello, World" (LangAlt)
|
|||||||
Set Xmp.tiff.ImageDescription "TIFF image description" (LangAlt)
|
Set Xmp.tiff.ImageDescription "TIFF image description" (LangAlt)
|
||||||
Set Xmp.tiff.ImageDescription "lang=de-DE TIFF Bildbeschreibung" (LangAlt)
|
Set Xmp.tiff.ImageDescription "lang=de-DE TIFF Bildbeschreibung" (LangAlt)
|
||||||
Reg ns="myNamespace/"
|
Reg ns="myNamespace/"
|
||||||
|
Set Xmp.ns.myProperty "myValue" (XmpText)
|
||||||
Set Xmp.xmpDM.videoFrameSize/stDim:w "16" (XmpText)
|
Set Xmp.xmpDM.videoFrameSize/stDim:w "16" (XmpText)
|
||||||
Set Xmp.xmpDM.videoFrameSize/stDim:h "9" (XmpText)
|
Set Xmp.xmpDM.videoFrameSize/stDim:h "9" (XmpText)
|
||||||
Set Xmp.xmpDM.videoFrameSize/stDim:unit "inch" (XmpText)
|
Set Xmp.xmpDM.videoFrameSize/stDim:unit "inch" (XmpText)
|
||||||
Set Xmp.dc.publisher "James Bond" (XmpBag)
|
Set Xmp.dc.publisher "James Bond" (XmpBag)
|
||||||
Set Xmp.dc.publisher/?ns:role "secret agent" (XmpText)
|
Set Xmp.dc.publisher[1]/?ns:role "secret agent" (XmpText)
|
||||||
Set Xmp.dc.creator[2]/?ns:role "programmer" (XmpText)
|
Set Xmp.dc.creator[2]/?ns:role "programmer" (XmpText)
|
||||||
Set Xmp.xmpBJ.JobRef "type=Bag" (XmpText)
|
Set Xmp.xmpBJ.JobRef "type=Bag" (XmpText)
|
||||||
Set Xmp.xmpBJ.JobRef[1]/stJob:name "Birthday party" (XmpText)
|
Set Xmp.xmpBJ.JobRef[1]/stJob:name "Birthday party" (XmpText)
|
||||||
@ -457,8 +460,9 @@ Xmp.dc.creator[2]/?ns:role XmpText 10 programmer
|
|||||||
Xmp.dc.creator[3] XmpText 18 3) And another one
|
Xmp.dc.creator[3] XmpText 18 3) And another one
|
||||||
Xmp.dc.description LangAlt 2 lang="x-default" Hello, World, lang="de-DE" Hallo, Welt
|
Xmp.dc.description LangAlt 2 lang="x-default" Hello, World, lang="de-DE" Hallo, Welt
|
||||||
Xmp.dc.publisher XmpText 0 type="Bag"
|
Xmp.dc.publisher XmpText 0 type="Bag"
|
||||||
Xmp.dc.publisher/?ns:role XmpText 12 secret agent
|
|
||||||
Xmp.dc.publisher[1] XmpText 10 James Bond
|
Xmp.dc.publisher[1] XmpText 10 James Bond
|
||||||
|
Xmp.dc.publisher[1]/?ns:role XmpText 12 secret agent
|
||||||
|
Xmp.ns.myProperty XmpText 7 myValue
|
||||||
Xmp.tiff.ImageDescription LangAlt 2 lang="x-default" TIFF image description, lang="de-DE" TIFF Bildbeschreibung
|
Xmp.tiff.ImageDescription LangAlt 2 lang="x-default" TIFF image description, lang="de-DE" TIFF Bildbeschreibung
|
||||||
Xmp.xmpDM.videoFrameSize XmpText 0 type="Struct"
|
Xmp.xmpDM.videoFrameSize XmpText 0 type="Struct"
|
||||||
Xmp.xmpDM.videoFrameSize/stDim:w XmpText 2 16
|
Xmp.xmpDM.videoFrameSize/stDim:w XmpText 2 16
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user