diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php
index ebc411fd1..47e7979df 100644
--- a/lib/Controller/ApiController.php
+++ b/lib/Controller/ApiController.php
@@ -570,9 +570,14 @@ public function newQuestion(int $formId, ?string $type = null, ?string $subtype
try {
$sourceQuestion = $this->questionMapper->findById($fromId);
- // Only allow cloning questions that belong to the same form
+ // A question may be cloned from another form, but only from one the user is
+ // allowed to edit. Without that check any question could be read out of any
+ // form by guessing ids.
if ($sourceQuestion->getFormId() !== $formId) {
- throw new OCSBadRequestException('Question doesn\'t belong to given form');
+ $this->formsService->getFormIfAllowed(
+ $sourceQuestion->getFormId(),
+ Constants::PERMISSION_EDIT,
+ );
}
$sourceOptions = $this->optionMapper->findByQuestion($fromId);
} catch (IMapperException) {
@@ -584,6 +589,9 @@ public function newQuestion(int $formId, ?string $type = null, ?string $subtype
$questionData = $sourceQuestion->read();
unset($questionData['id']);
+ // read() carries the source question's formId, so a clone taken from another
+ // form would otherwise be created back in that form rather than this one.
+ $questionData['formId'] = $formId;
if ($position !== null) {
$position = $this->shiftQuestionsForInsert($allQuestions, $position);
diff --git a/src/components/ImportQuestionsDialog.vue b/src/components/ImportQuestionsDialog.vue
new file mode 100644
index 000000000..c6201eb58
--- /dev/null
+++ b/src/components/ImportQuestionsDialog.vue
@@ -0,0 +1,332 @@
+
+
+
+
+
+
+
+
+
+ {{
+ t(
+ 'forms',
+ 'Questions can be imported from any form you are able to edit.',
+ )
+ }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('forms', 'That form has no questions to copy.') }}
+