mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-23 10:38:45 +02:00
N°3136 - Add creation and modification of n-n objects in object details (#378)
* Rebase onto develop * Use exit condition instead of englobing condition * Add informative modals that can be called from modal toolbox * Refactor "apply_modify" and "apply_new" into own controller, handle ajax requests with a json response and handle these responses in linkset creation/edition * Fix merge issues * Remove inverted condition * Move linkset create button to a better place, still needs to fix duplicate "New" button caused by a refactor * Handle "Cancel" button in modals * Do not display relations when editing an object in a modal * More elegant way to add "New" button to relations lists * Factorize vertical highlights in alerts and modal in a single mixin * Replace button name with dict entry code * Change route name to snake case * More elegant way to add "Create in modal" button to relations lists * Replace triple if with in_array * Move listener to body * Rename variable to match boolean rules * Rename event * Rename extra param * Add phpdoc * Revert changes * Check indirect linkset rights before allowing creation in modal
This commit is contained in:
@@ -10,15 +10,19 @@ use AjaxPage;
|
||||
use ApplicationException;
|
||||
use cmdbAbstractObject;
|
||||
use CMDBObjectSet;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\QuickCreate\QuickCreateHelper;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
|
||||
use Combodo\iTop\Controller\AbstractController;
|
||||
use CoreCannotSaveObjectException;
|
||||
use Dict;
|
||||
use IssueLog;
|
||||
use iTopWebPage;
|
||||
use JsonPage;
|
||||
use MetaModel;
|
||||
use SecurityException;
|
||||
use utils;
|
||||
use UserRights;
|
||||
use WebPage;
|
||||
|
||||
/**
|
||||
* Class ObjectController
|
||||
@@ -65,8 +69,41 @@ class ObjectController extends AbstractController
|
||||
}
|
||||
|
||||
// Prepare web page (should more likely be some kind of response object like for Symfony)
|
||||
$aFormExtraParams = array('wizard_container' => 1);
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$oPage = new AjaxPage('');
|
||||
$aFormExtraParams['js_handlers'] = [];
|
||||
$aFormExtraParams['noRelations'] = true;
|
||||
// We display this form in a modal, once we submit (in ajax) we probably want to only close the modal
|
||||
$aFormExtraParams['js_handlers']['form_on_submit'] =
|
||||
<<<JS
|
||||
event.preventDefault();
|
||||
if(bOnSubmitForm === true)
|
||||
{
|
||||
let oForm = $(this);
|
||||
let sUrl = oForm.attr('action');
|
||||
let sPosting = $.post( sUrl, oForm.serialize());
|
||||
|
||||
/* Alerts the results */
|
||||
sPosting.done(function(data) {
|
||||
if(data.success !== undefined && data.success === true) {
|
||||
oForm.closest('[data-role="ibo-modal"]').dialog('close');
|
||||
}
|
||||
else {
|
||||
CombodoModal.OpenInformativeModal(data.data.error_message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
JS;
|
||||
|
||||
|
||||
$aFormExtraParams['js_handlers']['cancel_button_on_click'] =
|
||||
<<<JS
|
||||
function() {
|
||||
$(this).closest('[data-role="ibo-modal"]').dialog('close');
|
||||
};
|
||||
JS;
|
||||
|
||||
} else {
|
||||
$oPage = new iTopWebPage('', $bPrintable);
|
||||
$oPage->DisableBreadCrumb();
|
||||
@@ -78,10 +115,398 @@ class ObjectController extends AbstractController
|
||||
}
|
||||
|
||||
// Note: Code duplicated to the case 'apply_modify' in UI.php when a data integrity issue has been found
|
||||
$oObj->DisplayModifyForm($oPage, array('wizard_container' => 1)); // wizard_container: Display the title above the form
|
||||
$oObj->DisplayModifyForm($oPage, $aFormExtraParams); // wizard_container: Display the title above the form
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \iTopWebPage|\JsonPage Object edit form in its webpage
|
||||
* @throws \ApplicationException
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \SecurityException
|
||||
*/
|
||||
public function OperationApplyNew()
|
||||
{
|
||||
$bPrintable = utils::ReadParam('printable', '0') === '1';
|
||||
$aResult = [];
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$oPage = new JsonPage();
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
$aResult['success'] = false;
|
||||
} else {
|
||||
$oPage = new iTopWebPage('', $bPrintable);
|
||||
$oPage->DisableBreadCrumb();
|
||||
}
|
||||
|
||||
$sClass = utils::ReadPostedParam('class', '', 'class');
|
||||
$sClassLabel = MetaModel::GetName($sClass);
|
||||
$sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id');
|
||||
$aErrors = array();
|
||||
$aWarnings = array();
|
||||
if ( empty($sClass) )
|
||||
{
|
||||
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not created (empty class)', $sClass, array(
|
||||
'$sTransactionId' => $sTransactionId,
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
|
||||
throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class'));
|
||||
}
|
||||
if (!utils::IsTransactionValid($sTransactionId, false))
|
||||
{
|
||||
$sUser = UserRights::GetUser();
|
||||
IssueLog::Error(__CLASS__.'::'.__METHOD__." : invalid transaction_id ! data: user='$sUser', class='$sClass'");
|
||||
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$aResult['data'] = ['error_message' => Dict::S('UI:Error:ObjectAlreadyCreated')];
|
||||
} else {
|
||||
$oErrorAlert = AlertUIBlockFactory::MakeForFailure(Dict::S('UI:Error:ObjectAlreadyCreated'));
|
||||
$oErrorAlert->SetIsClosable(false)
|
||||
->SetIsCollapsible(false);
|
||||
$oPage->AddUiBlock($oErrorAlert);
|
||||
}
|
||||
|
||||
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not created (invalid transaction_id)', $sClass, array(
|
||||
'$sTransactionId' => $sTransactionId,
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
$oObj = MetaModel::NewObject($sClass);
|
||||
if (MetaModel::HasLifecycle($sClass))
|
||||
{
|
||||
$sStateAttCode = MetaModel::GetStateAttributeCode($sClass);
|
||||
$sTargetState = utils::ReadPostedParam('obj_state', '');
|
||||
if ($sTargetState != '')
|
||||
{
|
||||
$sOrigState = utils::ReadPostedParam('obj_state_orig', '');
|
||||
if ($sTargetState != $sOrigState)
|
||||
{
|
||||
$aWarnings[] = Dict::S('UI:StateChanged');
|
||||
}
|
||||
$oObj->Set($sStateAttCode, $sTargetState);
|
||||
}
|
||||
}
|
||||
$aErrors = $oObj->UpdateObjectFromPostedForm();
|
||||
}
|
||||
if (isset($oObj) && is_object($oObj))
|
||||
{
|
||||
$sClass = get_class($oObj);
|
||||
$sClassLabel = MetaModel::GetName($sClass);
|
||||
|
||||
try
|
||||
{
|
||||
if (!empty($aErrors) || !empty($aWarnings))
|
||||
{
|
||||
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not created (see $aErrors)', $sClass, array(
|
||||
'$sTransactionId' => $sTransactionId,
|
||||
'$aErrors' => $aErrors,
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
|
||||
throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors));
|
||||
}
|
||||
|
||||
$oObj->DBInsertNoReload();// No need to reload
|
||||
|
||||
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object created', $sClass, array(
|
||||
'$id' => $oObj->GetKey(),
|
||||
'$sTransactionId' => $sTransactionId,
|
||||
'$aErrors' => $aErrors,
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
|
||||
utils::RemoveTransaction($sTransactionId);
|
||||
$oPage->set_title(Dict::S('UI:PageTitle:ObjectCreated'));
|
||||
QuickCreateHelper::AddClassToHistory($sClass);
|
||||
|
||||
// Compute the name, by reloading the object, even if it disappeared from the silo
|
||||
$oObj = MetaModel::GetObject($sClass, $oObj->GetKey(), true /* Must be found */, true /* Allow All Data*/);
|
||||
$sName = $oObj->GetName();
|
||||
$sMessage = Dict::Format('UI:Title:Object_Of_Class_Created', $sName, $sClassLabel);
|
||||
|
||||
$sNextAction = utils::ReadPostedParam('next_action', '');
|
||||
if (!empty($sNextAction)) {
|
||||
$oPage->add("<h1>$sMessage</h1>");
|
||||
try {
|
||||
ApplyNextAction($oPage, $oObj, $sNextAction);
|
||||
}
|
||||
catch (ApplicationException $e) {
|
||||
$sMessage = $e->getMessage();
|
||||
$sSeverity = 'info';
|
||||
ReloadAndDisplay($oPage, $oObj, 'create', $sMessage, $sSeverity);
|
||||
}
|
||||
} else {
|
||||
// Nothing more to do
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$aResult['success'] = true;
|
||||
} else {
|
||||
ReloadAndDisplay($oPage, $oObj, 'create', $sMessage, 'ok');
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (CoreCannotSaveObjectException $e) {
|
||||
// Found issues, explain and give the user a second chance
|
||||
//
|
||||
$aIssues = $e->getIssues();
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$aResult['data'] = ['error_message' => $e->getHtmlMessage()];
|
||||
} else {
|
||||
$sObjKey = $oObj->GetKey();
|
||||
$sClassIcon = MetaModel::GetClassIcon($sClass, false);
|
||||
$sHeaderTitle = Dict::Format('UI:CreationTitle_Class', $sClassLabel);
|
||||
|
||||
$oPage->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel));
|
||||
if (!empty($aIssues)) {
|
||||
$oPage->AddHeaderMessage($e->getHtmlMessage(), 'message_error');
|
||||
}
|
||||
if (!empty($aWarnings)) {
|
||||
$sWarnings = implode(', ', $aWarnings);
|
||||
$oPage->AddHeaderMessage($sWarnings, 'message_warning');
|
||||
}
|
||||
cmdbAbstractObject::DisplayCreationForm($oPage, $sClass, $oObj, [], ['transaction_id' => $sTransactionId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$oPage->SetData($aResult);
|
||||
}
|
||||
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \iTopWebPage|\JsonPage
|
||||
* @throws \ApplicationException
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \ConfigException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \DictExceptionMissingString
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public function OperationApplyModify(){
|
||||
$bPrintable = utils::ReadParam('printable', '0') === '1';
|
||||
$aResult = [];
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$oPage = new JsonPage();
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
$aResult['success'] = false;
|
||||
} else {
|
||||
$oPage = new iTopWebPage('', $bPrintable);
|
||||
$oPage->DisableBreadCrumb();
|
||||
}
|
||||
|
||||
$sClass = utils::ReadPostedParam('class', '', 'class');
|
||||
$sClassLabel = MetaModel::GetName($sClass);
|
||||
$id = utils::ReadPostedParam('id', '');
|
||||
$sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id');
|
||||
if ( empty($sClass) || empty($id))
|
||||
{
|
||||
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not updated (empty class or id)', $sClass, array(
|
||||
'$id' => $id,
|
||||
'$sTransactionId' => $sTransactionId,
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
// TODO 3.1 Do not crash with an exception in ajax
|
||||
throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id'));
|
||||
}
|
||||
$bDisplayDetails = true;
|
||||
$oObj = MetaModel::GetObject($sClass, $id, false);
|
||||
if ($oObj === null)
|
||||
{
|
||||
$bDisplayDetails = false;
|
||||
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$aResult['data'] = ['error_message' => Dict::S('UI:ObjectDoesNotExist')];
|
||||
} else {
|
||||
$oPage->set_title(Dict::S('UI:ErrorPageTitle'));
|
||||
|
||||
$oErrorAlert = AlertUIBlockFactory::MakeForFailure(Dict::S('UI:ObjectDoesNotExist'));
|
||||
$oErrorAlert->SetIsClosable(false)
|
||||
->SetIsCollapsible(false);
|
||||
$oPage->AddUiBlock($oErrorAlert);
|
||||
|
||||
}
|
||||
|
||||
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not updated (id not found)', $sClass, array(
|
||||
'$id' => $id,
|
||||
'$sTransactionId' => $sTransactionId,
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
}
|
||||
elseif (!utils::IsTransactionValid($sTransactionId, false))
|
||||
{
|
||||
//TODO: since $bDisplayDetails= true, there will be an redirection, thus, the content generated here is ignored, only the $sMessage and $sSeverity are used after the redirection
|
||||
$sUser = UserRights::GetUser();
|
||||
IssueLog::Error(__CLASS__.'::'.__METHOD__." : invalid transaction_id ! data: user='$sUser', class='$sClass'");
|
||||
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$aResult['data'] = ['error_message' => Dict::S('UI:Error:ObjectAlreadyUpdated')];
|
||||
} else {
|
||||
$oPage->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding
|
||||
$oPage->p("<strong>".Dict::S('UI:Error:ObjectAlreadyUpdated')."</strong>\n");
|
||||
}
|
||||
|
||||
$sMessage = Dict::Format('UI:Error:ObjectAlreadyUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName());
|
||||
$sSeverity = 'error';
|
||||
|
||||
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not updated (invalid transaction_id)', $sClass, array(
|
||||
'$id' => $id,
|
||||
'$sTransactionId' => $sTransactionId,
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
$aErrors = $oObj->UpdateObjectFromPostedForm();
|
||||
$sMessage = '';
|
||||
$sSeverity = 'ok';
|
||||
|
||||
if (!$oObj->IsModified() && empty($aErrors))
|
||||
{
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$aResult['data'] = ['error_message' => Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName())];
|
||||
} else {
|
||||
$oPage->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding
|
||||
}
|
||||
|
||||
$sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName());
|
||||
$sSeverity = 'info';
|
||||
|
||||
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not updated (see either $aErrors or IsModified)', $sClass, array(
|
||||
'$id' => $id,
|
||||
'$sTransactionId' => $sTransactionId,
|
||||
'$aErrors' => $aErrors,
|
||||
'IsModified' => $oObj->IsModified(),
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object updated', $sClass, array(
|
||||
'$id' => $id,
|
||||
'$sTransactionId' => $sTransactionId,
|
||||
'$aErrors' => $aErrors,
|
||||
'IsModified' => $oObj->IsModified(),
|
||||
'$sUser' => UserRights::GetUser(),
|
||||
'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'],
|
||||
'REQUEST_URI' => @$_SERVER['REQUEST_URI'],
|
||||
));
|
||||
|
||||
try
|
||||
{
|
||||
if (!empty($aErrors))
|
||||
{
|
||||
throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors));
|
||||
}
|
||||
// Transactions are now handled in DBUpdate
|
||||
$oObj->DBUpdate();
|
||||
$sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName());
|
||||
$sSeverity = 'ok';
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$aResult['success'] = true;
|
||||
}
|
||||
}
|
||||
catch (CoreCannotSaveObjectException $e)
|
||||
{
|
||||
// Found issues, explain and give the user a second chance
|
||||
//
|
||||
$bDisplayDetails = false;
|
||||
$aIssues = $e->getIssues();
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$aResult['data'] = ['error_message' => $e->getHtmlMessage()];
|
||||
} else {
|
||||
$oPage->AddHeaderMessage($e->getHtmlMessage(), 'message_error');
|
||||
$oObj->DisplayModifyForm($oPage,
|
||||
array('wizard_container' => true)); // wizard_container: display the wizard border and the title
|
||||
}
|
||||
|
||||
}
|
||||
catch (DeleteException $e)
|
||||
{
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$aResult['data'] = ['error_message' => Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName())];
|
||||
} else {
|
||||
// Say two things:
|
||||
// - 1) Don't be afraid nothing was modified
|
||||
$sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName());
|
||||
$sSeverity = 'info';
|
||||
cmdbAbstractObject::SetSessionMessage(get_class($oObj), $oObj->GetKey(), 'UI:Class_Object_NotUpdated', $sMessage,
|
||||
$sSeverity, 0, true /* must not exist */);
|
||||
// - 2) Ok, there was some trouble indeed
|
||||
$sMessage = $e->getMessage();
|
||||
$sSeverity = 'error';
|
||||
utils::RemoveTransaction($sTransactionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($bDisplayDetails)
|
||||
{
|
||||
$oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey()); //Workaround: reload the object so that the linkedset are displayed properly
|
||||
$sNextAction = utils::ReadPostedParam('next_action', '');
|
||||
if (!empty($sNextAction))
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplyNextAction($oPage, $oObj, $sNextAction);
|
||||
}
|
||||
catch (ApplicationException $e)
|
||||
{
|
||||
$sMessage = $e->getMessage();
|
||||
$sSeverity = 'info';
|
||||
ReloadAndDisplay($oPage, $oObj, 'update', $sMessage, $sSeverity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing more to do
|
||||
$sMessage = isset($sMessage) ? $sMessage : '';
|
||||
$sSeverity = isset($sSeverity) ? $sSeverity : null;
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
;
|
||||
} else{
|
||||
ReloadAndDisplay($oPage, $oObj, 'update', $sMessage, $sSeverity);
|
||||
}
|
||||
}
|
||||
|
||||
$bLockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled');
|
||||
if ($bLockEnabled)
|
||||
{
|
||||
// Release the concurrent lock, if any
|
||||
$sOwnershipToken = utils::ReadPostedParam('ownership_token', null, 'raw_data');
|
||||
if ($sOwnershipToken !== null)
|
||||
{
|
||||
// We're done, let's release the lock
|
||||
iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->IsHandlingXmlHttpRequest()) {
|
||||
$oPage->SetData($aResult);
|
||||
}
|
||||
return $oPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] Rel. paths (to iTop root folder) of required JS files for object modification (create, edit, stimulus, ...)
|
||||
|
||||
Reference in New Issue
Block a user