From 6df4dba80b11d06b8f2a8aac0ac301119c8ac07c Mon Sep 17 00:00:00 2001
From: Ahmad Farhat <ahmad.af.farhat@gmail.com>
Date: Thu, 8 Jun 2023 15:28:43 -0400
Subject: [PATCH] Added support for protected recordings (#5220)

* First draft of fixing protected recordings

* Cleaned up code

* Eslint

* Add specs

* CR

* CR2

* Fix tests
---
 .../api/v1/recordings_controller.rb           | 20 +++++++-
 .../components/recordings/RecordingRow.jsx    | 19 ++++----
 .../recordings/useCopyRecordingUrl.jsx        | 37 ++++++++++++++
 .../recordings/useRedirectRecordingUrl.jsx    | 37 ++++++++++++++
 config/routes.rb                              |  1 +
 .../controllers/recordings_controller_spec.rb | 48 +++++++++++++++++++
 6 files changed, 150 insertions(+), 12 deletions(-)
 create mode 100644 app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx
 create mode 100644 app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx

diff --git a/app/controllers/api/v1/recordings_controller.rb b/app/controllers/api/v1/recordings_controller.rb
index f6f1bcd2..ece4ba6b 100644
--- a/app/controllers/api/v1/recordings_controller.rb
+++ b/app/controllers/api/v1/recordings_controller.rb
@@ -19,11 +19,11 @@
 module Api
   module V1
     class RecordingsController < ApiController
-      before_action :find_recording, only: %i[update update_visibility]
+      before_action :find_recording, only: %i[update update_visibility recording_url]
       before_action only: %i[destroy] do
         ensure_authorized('ManageRecordings', record_id: params[:id])
       end
-      before_action only: %i[update update_visibility] do
+      before_action only: %i[update update_visibility recording_url] do
         ensure_authorized(%w[ManageRecordings SharedRoom], record_id: params[:id])
       end
       before_action only: %i[index recordings_count] do
@@ -89,6 +89,22 @@ module Api
         render_data data: count, status: :ok
       end
 
+      # POST /api/v1/recordings/recording_url.json
+      def recording_url
+        record_format = params[:recording_format]
+
+        url = if @recording.visibility == 'Protected'
+                recording = BigBlueButtonApi.new(provider: current_provider).get_recording(record_id: @recording.record_id)
+                formats = recording[:playback][:format]
+
+                record_format.present? ? formats.find { |format| format[:type] == record_format }[:url] : formats.pluck(:url)
+              else
+                record_format.present? ? @recording.formats.find_by(recording_type: record_format).url : @recording.formats.pluck(:url)
+              end
+
+        render_data data: url, status: :ok
+      end
+
       private
 
       def recording_params
diff --git a/app/javascript/components/recordings/RecordingRow.jsx b/app/javascript/components/recordings/RecordingRow.jsx
index bfca6aed..849a091a 100644
--- a/app/javascript/components/recordings/RecordingRow.jsx
+++ b/app/javascript/components/recordings/RecordingRow.jsx
@@ -23,7 +23,6 @@ import PropTypes from 'prop-types';
 import {
   Button, Stack, Dropdown,
 } from 'react-bootstrap';
-import { toast } from 'react-toastify';
 import { useTranslation } from 'react-i18next';
 import { useAuth } from '../../contexts/auth/AuthProvider';
 import Spinner from '../shared_components/utilities/Spinner';
@@ -31,6 +30,8 @@ import UpdateRecordingForm from './forms/UpdateRecordingForm';
 import DeleteRecordingForm from './forms/DeleteRecordingForm';
 import Modal from '../shared_components/modals/Modal';
 import { localizeDateTimeString } from '../../helpers/DateTimeHelper';
+import useRedirectRecordingUrl from '../../hooks/mutations/recordings/useRedirectRecordingUrl';
+import useCopyRecordingUrl from '../../hooks/mutations/recordings/useCopyRecordingUrl';
 
 // TODO: Amir - Refactor this.
 export default function RecordingRow({
@@ -38,16 +39,14 @@ export default function RecordingRow({
 }) {
   const { t } = useTranslation();
 
-  function copyUrls() {
-    const formatUrls = recording.formats.map((format) => format.url);
-    navigator.clipboard.writeText(formatUrls);
-    toast.success(t('toast.success.recording.copied_urls'));
-  }
-
   const visibilityAPI = useVisibilityAPI();
   const [isEditing, setIsEditing] = useState(false);
   const [isUpdating, setIsUpdating] = useState(false);
+
   const currentUser = useAuth();
+  const redirectRecordingUrl = useRedirectRecordingUrl();
+  const copyRecordingUrl = useCopyRecordingUrl();
+
   const localizedTime = localizeDateTimeString(recording?.recorded_at, currentUser?.language);
   const formats = recording.formats.sort(
     (a, b) => (a.recording_type.toLowerCase() > b.recording_type.toLowerCase() ? 1 : -1),
@@ -114,7 +113,7 @@ export default function RecordingRow({
       <td className="border-0">
         {formats.map((format) => (
           <Button
-            onClick={() => window.open(format.url, '_blank')}
+            onClick={() => redirectRecordingUrl.mutate({ record_id: recording.record_id, format: format.recording_type })}
             className={`btn-sm rounded-pill me-1 mt-1 border-0 btn-format-${format.recording_type.toLowerCase()}`}
             key={`${format.recording_type}-${format.url}`}
           >
@@ -128,7 +127,7 @@ export default function RecordingRow({
             <Dropdown className="float-end cursor-pointer">
               <Dropdown.Toggle className="hi-s" as={EllipsisVerticalIcon} />
               <Dropdown.Menu>
-                <Dropdown.Item onClick={() => copyUrls()}>
+                <Dropdown.Item onClick={() => copyRecordingUrl.mutate({ record_id: recording.record_id })}>
                   <ClipboardDocumentIcon className="hi-s me-2" />
                   { t('recording.copy_recording_urls') }
                 </Dropdown.Item>
@@ -149,7 +148,7 @@ export default function RecordingRow({
               <Button
                 variant="icon"
                 className="mt-1 me-3"
-                onClick={() => copyUrls()}
+                onClick={() => copyRecordingUrl.mutate({ record_id: recording.record_id })}
               >
                 <ClipboardDocumentIcon className="hi-s text-muted" />
               </Button>
diff --git a/app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx b/app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx
new file mode 100644
index 00000000..b15ca620
--- /dev/null
+++ b/app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx
@@ -0,0 +1,37 @@
+// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+//
+// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
+//
+// This program is free software; you can redistribute it and/or modify it under the
+// terms of the GNU Lesser General Public License as published by the Free Software
+// Foundation; either version 3.0 of the License, or (at your option) any later
+// version.
+//
+// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License along
+// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
+
+import { useMutation } from 'react-query';
+import { toast } from 'react-toastify';
+import { useTranslation } from 'react-i18next';
+import axios from '../../../helpers/Axios';
+
+export default function useCopyRecordingUrl() {
+  const { t } = useTranslation();
+
+  return useMutation(
+    (data) => axios.post('/recordings/recording_url.json', { id: data.record_id })
+      .then((resp) => resp.data),
+    {
+      onSuccess: (url) => {
+        navigator.clipboard.writeText(url?.join('\n')).then(() => toast.success(t('toast.success.recording.copied_urls')));
+      },
+      onError: () => {
+        toast.error(t('toast.error.problem_completing_action'));
+      },
+    },
+  );
+}
diff --git a/app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx b/app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx
new file mode 100644
index 00000000..df48ff1c
--- /dev/null
+++ b/app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx
@@ -0,0 +1,37 @@
+// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+//
+// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
+//
+// This program is free software; you can redistribute it and/or modify it under the
+// terms of the GNU Lesser General Public License as published by the Free Software
+// Foundation; either version 3.0 of the License, or (at your option) any later
+// version.
+//
+// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License along
+// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
+
+import { useMutation } from 'react-query';
+import { toast } from 'react-toastify';
+import { useTranslation } from 'react-i18next';
+import axios from '../../../helpers/Axios';
+
+export default function useRedirectRecordingUrl() {
+  const { t } = useTranslation();
+
+  return useMutation(
+    (data) => axios.post('/recordings/recording_url.json', { id: data.record_id, recording_format: data.format })
+      .then((resp) => resp.data.data),
+    {
+      onSuccess: (url) => {
+        window.open(url, '_blank');
+      },
+      onError: () => {
+        toast.error(t('toast.error.problem_completing_action'));
+      },
+    },
+  );
+}
diff --git a/config/routes.rb b/config/routes.rb
index a9d7f495..4696bc02 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -62,6 +62,7 @@ Rails.application.routes.draw do
         collection do
           post '/update_visibility', to: 'recordings#update_visibility'
           get '/recordings_count', to: 'recordings#recordings_count'
+          post '/recording_url', to: 'recordings#recording_url'
         end
       end
       resources :shared_accesses, only: %i[create show destroy], param: :friendly_id do
diff --git a/spec/controllers/recordings_controller_spec.rb b/spec/controllers/recordings_controller_spec.rb
index f4dd5049..687d5330 100644
--- a/spec/controllers/recordings_controller_spec.rb
+++ b/spec/controllers/recordings_controller_spec.rb
@@ -268,6 +268,54 @@ RSpec.describe Api::V1::RecordingsController, type: :controller do
       expect(JSON.parse(response.body)['data']).to be(5)
     end
   end
+
+  describe '#recording_url' do
+    let(:room) { create(:room, user:) }
+
+    before do
+      allow_any_instance_of(BigBlueButtonApi).to receive(:get_recording).and_return(
+        playback: { format: [{ type: 'screenshare', url: 'https://test.com/screenshare' }, { type: 'video', url: 'https://test.com/video' }] }
+      )
+    end
+
+    context 'format not passed' do
+      it 'makes a call to BBB and returns the url returned if the recording is protected' do
+        recording = create(:recording, visibility: 'Protected', room:)
+
+        post :recording_url, params: { id: recording.record_id }
+
+        expect(JSON.parse(response.body)).to match_array ['https://test.com/screenshare', 'https://test.com/video']
+      end
+
+      it 'returns the formats url' do
+        recording = create(:recording, visibility: 'Published', room:)
+        create(:format, recording:)
+
+        post :recording_url, params: { id: recording.record_id }
+
+        expect(JSON.parse(response.body)).to match_array recording.formats.pluck(:url)
+      end
+    end
+
+    context 'format is passed' do
+      it 'makes a call to BBB and returns the url returned if the recording is protected' do
+        recording = create(:recording, visibility: 'Protected', room:)
+
+        post :recording_url, params: { id: recording.record_id, recording_format: 'screenshare' }
+
+        expect(JSON.parse(response.body)['data']).to eq 'https://test.com/screenshare'
+      end
+
+      it 'returns the formats url' do
+        recording = create(:recording, visibility: 'Published', room:)
+        format = create(:format, recording:, recording_type: 'podcast')
+
+        post :recording_url, params: { id: recording.record_id, recording_format: format.recording_type }
+
+        expect(JSON.parse(response.body)['data']).to eq format.url
+      end
+    end
+  end
 end
 
 def http_ok_response
-- 
GitLab