motionId != $motion->id || $comment->status != IComment::STATUS_VISIBLE) { throw new Internal(\Yii::t('comment', 'err_not_found')); } if ($needsScreeningRights) { if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_SCREENING, null)) { throw new Internal(\Yii::t('comment', 'err_no_screening')); } } return $comment; } private function writeComment(Motion $motion, array &$viewParameters): void { $postComment = $this->getHttpRequest()->post('comment'); $replyTo = null; if (isset($postComment['parentCommentId']) && $postComment['parentCommentId']) { $replyTo = MotionComment::findOne([ 'id' => $postComment['parentCommentId'], 'motionId' => $motion->id, 'parentCommentId' => null, ]); if ($replyTo && $replyTo->status === IComment::STATUS_DELETED) { $replyTo = null; } } $commentForm = new CommentForm($motion, $replyTo); $commentForm->setAttributes($postComment, $motion->getActiveSections()); $redirectUrl = null; try { $commentForm->saveNotificationSettings(); $comment = $commentForm->saveMotionCommentWithChecks($motion); if ($comment->status === MotionComment::STATUS_SCREENING) { $this->getHttpSession()->setFlash('screening', \Yii::t('comment', 'created_needs_screening')); } else { $this->getHttpSession()->setFlash('screening', \Yii::t('comment', 'created')); } $redirectUrl = UrlHelper::createMotionCommentUrl($comment); } catch (\Exception $e) { $viewParameters['commentForm'] = $commentForm; if (!isset($viewParameters['openedComments'][$commentForm->sectionId])) { $viewParameters['openedComments'][$commentForm->sectionId] = []; } $viewParameters['openedComments'][$commentForm->sectionId][] = $commentForm->paragraphNo; $this->getHttpSession()->setFlash('error', $e->getMessage()); } if ($redirectUrl) { throw new ResponseException(new RedirectResponse($redirectUrl)); } } /** * @throws DB * @throws Internal */ private function deleteComment(Motion $motion, int $commentId): void { $comment = $this->getComment($motion, $commentId, false); if (!$comment->canDelete(User::getCurrentUser())) { throw new Internal(\Yii::t('comment', 'err_no_del')); } $comment->status = IComment::STATUS_DELETED; if (!$comment->save(false)) { throw new DB($comment->getErrors()); } ConsultationLog::logCurrUser( $motion->getMyConsultation(), ConsultationLog::MOTION_COMMENT_DELETE, $comment->id ); $this->getHttpSession()->setFlash('success', \Yii::t('comment', 'del_done')); } /** * @throws Internal */ private function screenCommentAccept(Motion $motion, int $commentId): void { $comment = MotionComment::findOne($commentId); if (!$comment || $comment->motionId !== $motion->id) { throw new Internal(\Yii::t('comment', 'err_not_found')); } if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_SCREENING, null)) { throw new Internal(\Yii::t('comment', 'err_no_screening')); } $comment->status = IComment::STATUS_VISIBLE; $comment->save(); $motion->refresh(); ConsultationLog::logCurrUser( $motion->getMyConsultation(), ConsultationLog::MOTION_COMMENT_SCREEN, $comment->id ); $comment->trigger(IComment::EVENT_PUBLISHED); } /** * @throws Internal */ private function screenCommentReject(Motion $motion, int $commentId): void { $comment = MotionComment::findOne($commentId); if (!$comment || $comment->motionId !== $motion->id) { throw new Internal(\Yii::t('comment', 'err_not_found')); } if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_SCREENING, null)) { throw new Internal(\Yii::t('comment', 'err_no_screening')); } $comment->status = IComment::STATUS_DELETED; $comment->save(); $motion->refresh(); ConsultationLog::logCurrUser( $motion->getMyConsultation(), ConsultationLog::MOTION_COMMENT_DELETE, $comment->id ); } /** * @throws FormError * @throws Internal */ private function motionLikeDislike(Motion $motion, string $role, string $string, string $name, string $orga = '', string $gender = '', bool $nonPublic = false): void { $currentUser = User::getCurrentUser(); if (!$motion->motionType->getMotionSupportPolicy()->checkCurrUser(allowAdmins: false)) { throw new FormError('Supporting this motion is not possible'); } MotionSupporter::createSupport($motion, $currentUser, $name, $orga, $role, $gender, $nonPublic); $this->getHttpSession()->setFlash('success', $string); } /** * @throws FormError * @throws Internal */ private function motionLike(Motion $motion): void { if (!($motion->getLikeDislikeSettings() & SupportBase::LIKEDISLIKE_LIKE)) { throw new FormError('Not supported'); } $name = (User::getCurrentUser() ? '' : $this->getHttpRequest()->post('likeName', '')); $this->motionLikeDislike($motion, MotionSupporter::ROLE_LIKE, \Yii::t('motion', 'like_done'), $name); ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_LIKE, $motion->id); } /** * @throws Access * @throws FormError */ private function motionSupport(Motion $motion): void { $user = User::getCurrentUser(); $supportType = $motion->getMyMotionType()->getMotionSupportTypeClass(); $dto = SupportRequest::fromWebRequest($this->getPostValues(), $user, $supportType); if (!MotionSupporter::createSupportFromRequest($motion, $user, $dto)) { $this->getHttpSession()->setFlash('success', \Yii::t('motion', 'support_already')); return; } $this->getHttpSession()->set('user_gender', $dto->gender ?? ''); $this->getHttpSession()->setFlash('success', \Yii::t('motion', 'support_done')); } /** * @throws FormError * @throws Internal */ private function motionDislike(Motion $motion): void { if (!($motion->getLikeDislikeSettings() & SupportBase::LIKEDISLIKE_DISLIKE)) { throw new FormError('Not supported'); } $name = (User::getCurrentUser() ? '' : $this->getHttpRequest()->post('likeName', '')); $this->motionLikeDislike($motion, MotionSupporter::ROLE_DISLIKE, \Yii::t('motion', 'dislike_done'), $name); ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_DISLIKE, $motion->id); } /** * @throws Access */ private function motionSupportRevoke(Motion $motion): void { $currentUser = User::getCurrentUser(); MotionSupporter::revokeSupportFromRequest($motion, $currentUser, MotionSupporter::getMyLoginlessSupportIds()); $this->getHttpSession()->setFlash('success', \Yii::t('motion', 'neutral_done')); } private function motionSupportFinish(Motion $motion): void { if (!$motion->canFinishSupportCollection()) { $this->getHttpSession()->setFlash('error', \Yii::t('motion', 'support_finish_err')); return; } $motion->trigger(Motion::EVENT_CREATED, new MotionEvent($motion)); if ($motion->status == Motion::STATUS_SUBMITTED_SCREENED) { $motion->trigger(Motion::EVENT_PUBLISHED, new MotionEvent($motion)); } ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_SUPPORT_FINISH, $motion->id); $this->getHttpSession()->setFlash('success', \Yii::t('motion', 'support_finish_done')); } /** * @throws Internal */ private function motionAddTag(Motion $motion): void { if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_MOTION_STATUS_EDIT, PrivilegeQueryContext::motion($motion))) { throw new Internal(\Yii::t('comment', 'err_no_screening')); } foreach ($motion->getMyConsultation()->getSortedTags(ConsultationSettingsTag::TYPE_PUBLIC_TOPIC) as $tag) { if ($tag->id === intval($this->getHttpRequest()->post('tagId'))) { $motion->link('tags', $tag); } } } /** * @throws Internal */ private function motionDelTag(Motion $motion): void { if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_MOTION_STATUS_EDIT, PrivilegeQueryContext::motion($motion))) { throw new Internal(\Yii::t('comment', 'err_no_screening')); } foreach ($motion->getMyConsultation()->getSortedTags(ConsultationSettingsTag::TYPE_PUBLIC_TOPIC) as $tag) { if ($tag->id === intval($this->getHttpRequest()->post('tagId'))) { $motion->unlink('tags', $tag, true); } } } private function setProposalAgreement(Motion $motion, int $status, ?string $comment): void { $procedureToken = $this->getHttpRequest()->post('procedureToken'); $proposal = $motion->getProposalByToken($procedureToken); if (!$proposal->canAgreeToProposedProcedure($procedureToken) || !$proposal->proposalFeedbackHasBeenRequested()) { $this->getHttpSession()->setFlash('error', 'Not allowed to perform this action'); return; } if ($proposal->userStatus === Amendment::STATUS_ACCEPTED) { // No changing once accepted return; } $proposal->userStatus = $status; $proposal->save(); $data = ProposedProcedureAgreement::create(true, $proposal->version, $proposal->id, $comment)->jsonSerialize(); if ($status === Motion::STATUS_ACCEPTED) { ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_ACCEPT_PROPOSAL, $motion->id, $data); } if ($status === Motion::STATUS_REJECTED) { ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_REJECT_PROPOSAL, $motion->id, $data); } $this->getHttpSession()->setFlash('success', \Yii::t('amend', 'proposal_user_saved')); } /** * @throws \Throwable */ private function savePrivateNote(Motion $motion): void { $user = User::getCurrentUser(); if (!$user || !$user->getSettingsObj()->allowPrivateComments) { return; } $noteText = trim($this->getHttpRequest()->post('noteText', '')); $paragraph = IntVal($this->getHttpRequest()->post('paragraphNo', -1)); if ($this->getHttpRequest()->post('sectionId', 0) > 0) { $section = IntVal($this->getHttpRequest()->post('sectionId', 0)); } else { $section = null; } $comment = $motion->getPrivateComment($section, $paragraph); if ($comment && $noteText === '') { $comment->delete(); $motion->refresh(); return; } if (!$comment) { $comment = new MotionComment(); } $comment->motionId = $motion->id; $comment->userId = $user->id; $comment->text = $noteText; $comment->status = IComment::STATUS_PRIVATE; $comment->paragraph = $paragraph; $comment->sectionId = $section; $comment->dateCreation = date('Y-m-d H:i:s'); $comment->name = ($user->name ? $user->name : '-'); $comment->save(); $motion->refresh(); throw new ResponseException(new RedirectResponse(UrlHelper::createMotionCommentUrl($comment))); } /** * @throws \Throwable */ private function performShowActions(Motion $motion, int $commentId, array &$viewParameters): void { $post = $this->getPostValues(); if ($commentId === 0 && isset($post['commentId'])) { $commentId = intval($post['commentId']); } if (isset($post['deleteComment'])) { $this->deleteComment($motion, $commentId); } elseif (isset($post['commentScreeningAccept'])) { $this->screenCommentAccept($motion, $commentId); } elseif (isset($post['commentScreeningReject'])) { $this->screenCommentReject($motion, $commentId); } elseif (isset($post['motionLike'])) { $this->motionLike($motion); } elseif (isset($post['motionDislike'])) { $this->motionDislike($motion); } elseif (isset($post['motionSupport'])) { $this->motionSupport($motion); } elseif (isset($post['motionSupportRevoke'])) { $this->motionSupportRevoke($motion); } elseif (isset($post['motionSupportFinish'])) { $this->motionSupportFinish($motion); } elseif (isset($post['addTag'])) { $this->motionAddTag($motion); } elseif (isset($post['delTag'])) { $this->motionDelTag($motion); } elseif (isset($post['writeComment'])) { $this->writeComment($motion, $viewParameters); } elseif (isset($post['setProposalAgree'])) { $this->setProposalAgreement($motion, Motion::STATUS_ACCEPTED, $post['comment'] ?? null); } elseif (isset($post['setProposalDisagree'])) { $this->setProposalAgreement($motion, Motion::STATUS_REJECTED, $post['comment'] ?? null); } elseif (isset($post['savePrivateNote'])) { $this->savePrivateNote($motion); } } public function actionDelProposalComment(string $motionSlug): ResponseInterface { $motion = $this->getMotionWithCheck($motionSlug); $commentId = $this->getHttpRequest()->post('id'); $comment = MotionAdminComment::findOne(['id' => $commentId, 'motionId' => $motion->id]); if ($comment && User::isCurrentUser($comment->getMyUser())) { $comment->delete(); return new JsonResponse(['success' => true]); } else { return new RestApiExceptionResponse(403, 'No permission to delete this comment'); } } public function actionSaveProposalStatus(string $motionSlug): ResponseInterface { $motion = $this->consultation->getMotion($motionSlug); if (!$motion) { return new RestApiExceptionResponse(404, 'Motion not found'); } $post = $this->getPostValues(); if (!in_array('version', array_keys($post))) { return new RestApiExceptionResponse(400, 'No version provided'); } $proposal = $motion->getProposalById($post['version']); if (!$proposal->canEditLimitedProposedProcedure()) { return new RestApiExceptionResponse(403, 'Not permitted to change the status'); } $proposalIsNew = $proposal->isNewRecord; $proposal->save(); // Make sure we know the ID $canChangeProposalUnlimitedly = $proposal->canEditProposedProcedure(); $response = []; $msgAlert = null; $ppChanges = ProposedProcedureChange::create($proposalIsNew, $proposal->id, $proposal->version); if ($this->getHttpRequest()->post('setStatus', null) !== null) { $originalMotionId = $motion->id; $originalProposalStatus = $proposal->proposalStatus; foreach (AntragsgruenApp::getActivePlugins() as $plugin) { /** @var Motion $motion */ $motion = $plugin::onBeforeProposedProcedureStatusSave($motion); } if ($canChangeProposalUnlimitedly) { $setStatus = intval($this->getHttpRequest()->post('setStatus')); if ($proposal->proposalStatus !== $setStatus) { $ppChanges->setProposalStatusChanges($proposal->proposalStatus, $setStatus); if ($proposal->userStatus !== null) { $msgAlert = \Yii::t('amend', 'proposal_user_change_reset'); } $proposal->userStatus = null; } $proposal->proposalStatus = $setStatus; $ppChanges->setProposalCommentChanges($proposal->comment, $this->getHttpRequest()->post('proposalComment', '')); $proposal->comment = $this->getHttpRequest()->post('proposalComment', ''); } $motion->setProposedProcedureTags($this->getHttpRequest()->post('tags', []), $ppChanges); if ($canChangeProposalUnlimitedly) { $proposalExplanationPre = $proposal->explanation; if ($this->getHttpRequest()->post('proposalExplanation', null) !== null) { if (trim($this->getHttpRequest()->post('proposalExplanation', '')) === '') { $proposal->explanation = null; } else { $proposal->explanation = $this->getHttpRequest()->post('proposalExplanation', ''); } } else { $proposal->explanation = null; } $ppChanges->setProposalExplanationChanges($proposalExplanationPre, $proposal->explanation); if ($this->getHttpRequest()->post('visible', 0)) { if ($proposal->visibleFrom === null) { // Reload the page, to update section titles and permissions to edit the proposed procedure $response['redirectToUrl'] = UrlHelper::createMotionUrl($motion, 'view'); } $proposal->setPublished(); } else { $proposal->visibleFrom = null; } } try { $motion->setProposalVotingPropertiesFromRequest( $this->getHttpRequest()->post('votingStatus', null), $this->getHttpRequest()->post('votingBlockId', null), $this->getHttpRequest()->post('votingItemBlockId', []), $this->getHttpRequest()->post('votingItemBlockName', ''), $this->getHttpRequest()->post('votingBlockTitle', ''), true, $ppChanges ); } catch (FormError $e) { return new RestApiExceptionResponse(400, $e->getMessage()); } if ($ppChanges->hasChanges()) { ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_SET_PROPOSAL, $motion->id, $ppChanges->jsonSerialize()); } $response['success'] = false; if ($proposal->save()) { $response['success'] = true; } $this->consultation->refresh(); $motion->refresh(); $response['html'] = $this->renderPartial('_set_proposed_procedure', [ 'motion' => $motion, 'proposal' => $proposal, 'msgAlert' => $msgAlert, 'context' => $this->getHttpRequest()->post('context', 'view'), ]); $response['proposalStr'] = $proposal->getFormattedProposalStatus(true); if ($proposal->proposalStatus === IMotion::STATUS_MODIFIED_ACCEPTED && $originalProposalStatus !== $proposal->proposalStatus) { $response['redirectToUrl'] = UrlHelper::createMotionUrl($motion, 'edit-proposed-change', ['proposalVersion' => $proposal->version]); } elseif ($motion->id !== $originalMotionId) { // This can happen if a plugin enforces the creation of a new motion when saving $response['redirectToUrl'] = UrlHelper::createMotionUrl($motion, 'view'); } } if ($this->getHttpRequest()->post('notifyProposer', false)) { try { new MotionProposedProcedure( $proposal, $this->getHttpRequest()->post('text'), $this->getHttpRequest()->post('fromName'), $this->getHttpRequest()->post('replyTo') ); $proposal->notifiedAt = date('Y-m-d H:i:s'); $proposal->save(); $data = ProposedProcedureUserNotification::create($this->getHttpRequest()->post('text'), $proposal->version, $proposal->id)->jsonSerialize(); ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_NOTIFY_PROPOSAL, $motion->id, $data); $response['success'] = true; $response['html'] = $this->renderPartial('_set_proposed_procedure', [ 'motion' => $motion, 'proposal' => $proposal, 'msgAlert' => $msgAlert, 'context' => $this->getHttpRequest()->post('context', 'view'), ]); } catch (MailNotSent $e) { $response['success'] = false; $response['error'] = 'The mail could not be sent: ' . $e->getMessage(); } } if ($this->getHttpRequest()->post('setProposerHasAccepted')) { $proposal->userStatus = Motion::STATUS_ACCEPTED; $proposal->save(); $data = ProposedProcedureAgreement::create(false, $proposal->version, $proposal->id, null)->jsonSerialize(); ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_ACCEPT_PROPOSAL, $motion->id, $data); $response['success'] = true; $response['html'] = $this->renderPartial('_set_proposed_procedure', [ 'motion' => $motion, 'proposal' => $proposal, 'msgAlert' => $msgAlert, 'context' => $this->getHttpRequest()->post('context', 'view'), ]); } if ($this->getHttpRequest()->post('writeComment')) { $adminComment = new MotionAdminComment(); $adminComment->userId = User::getCurrentUser()->id; $adminComment->text = $this->getHttpRequest()->post('writeComment'); $adminComment->status = MotionAdminComment::TYPE_PROPOSED_PROCEDURE; $adminComment->dateCreation = date('Y-m-d H:i:s'); $adminComment->motionId = $motion->id; if (!$adminComment->save()) { return new RestApiExceptionResponse(500, 'could not save the comment'); } $response['success'] = true; $response['proposalId'] = $proposal->id; $response['comment'] = [ 'username' => $adminComment->getMyUser()->name, 'id' => $adminComment->id, 'text' => $adminComment->text, 'delLink' => UrlHelper::createMotionUrl($motion, 'del-proposal-comment'), 'dateFormatted' => Tools::formatMysqlDateTime($adminComment->dateCreation), ]; } if ($this->getHttpRequest()->post('deleteProposal')) { if ($proposal->isNewRecord) { return new RestApiExceptionResponse(400, 'This proposal version has not been saved yet'); } if (!$proposal->canEditProposedProcedure()) { return new RestApiExceptionResponse(403, 'Not permitted to delete this proposal version'); } $proposal->softDelete(); $response['success'] = true; $response['redirectToUrl'] = UrlHelper::createMotionUrl($motion, 'view'); } return new JsonResponse($response); } public function actionEditProposedChange(string $motionSlug, int $proposalVersion): ResponseInterface { $motion = $this->consultation->getMotion($motionSlug); if (!$motion) { return new HtmlErrorResponse(404, 'Motion not found'); } if ($this->getHttpRequest()->post('newVersion', null) === 'new') { $oldProposal = $motion->getProposalByVersion($proposalVersion); $proposal = $motion->getProposalById(null); // Create a new one $proposal->proposalStatus = $oldProposal->proposalStatus; } else { $proposal = $motion->getProposalByVersion($proposalVersion); } if (!$proposal->canEditProposedProcedure()) { return new HtmlErrorResponse(403, 'Not permitted to edit the proposed procedure'); } $isNewProposal = $proposal->isNewRecord; $proposal->save(); if ($this->getHttpRequest()->post('reset', null) !== null) { $reference = $proposal->getMyProposalReference(); if ($reference && $reference->status === Amendment::STATUS_PROPOSED_MODIFIED_MOTION) { foreach ($reference->sections as $section) { $section->delete(); } $proposal->proposalReferenceId = null; $proposal->save(); $reference->delete(); $ppChanges = ProposedProcedureChange::create($isNewProposal, $proposal->id, $proposal->version); $ppChanges->setProposalTextChanged(); ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_SET_PROPOSAL, $motion->id, $ppChanges->jsonSerialize()); } $motion->flushCacheItems(['procedure']); } $form = new ProposedChangeForm($motion, $proposal); $msgSuccess = null; $msgAlert = null; if ($this->getHttpRequest()->post('save', null) !== null) { $form->save($this->getPostValues(), $_FILES); $this->getHttpSession()->setFlash('success', \Yii::t('base', 'saved')); if ($proposal->userStatus !== null) { $this->getHttpSession()->setFlash('info', \Yii::t('amend', 'proposal_user_change_reset')); } $proposal->userStatus = null; $proposal->save(); $proposal->flushViewCaches(); $ppChanges = ProposedProcedureChange::create($isNewProposal, $proposal->id, $proposal->version); $ppChanges->setProposalTextChanged(); ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_SET_PROPOSAL, $motion->id, $ppChanges->jsonSerialize()); return new RedirectResponse(UrlHelper::createMotionUrl($motion, 'view', ['proposalVersion' => $proposal->version])); } return new HtmlResponse($this->render('edit_proposed_change', [ 'msgSuccess' => $msgSuccess, 'msgAlert' => $msgAlert, 'motion' => $motion, 'proposal' => $proposal, 'form' => $form, ])); } public function actionSaveEditorial(string $motionSlug, int $sectionId): ResponseInterface { $motion = $this->consultation->getMotion($motionSlug); if (!$motion) { return new RestApiExceptionResponse(404, 'Motion not found'); } if (!User::havePrivilege($this->consultation, Privileges::PRIVILEGE_CHANGE_EDITORIAL, PrivilegeQueryContext::motion($motion))) { return new RestApiExceptionResponse(403, 'Not permitted to change the editorial'); } $section = null; foreach ($motion->getActiveSections(ISectionType::TYPE_TEXT_EDITORIAL) as $searchSection) { if ($searchSection->sectionId === $sectionId) { $section = $searchSection; } } if (!$section) { return new RestApiExceptionResponse(404, 'Section not found'); } /** @var TextEditorial $sectionType */ $sectionType = $section->getSectionType(); $sectionType->setEditorialData( $this->getPostValue('data'), $this->getPostValue('author'), $this->getPostValue('updateDate') ? new \DateTime() : $sectionType->getSectionMetadata()['lastUpdate'] ); $section->save(); return new JsonResponse([ 'success' => true, 'html' => $section->getData(), 'metadataFormatted' => $sectionType->getFormattedSectionMetadata(true), ]); } }