From 70f2f198bbb5b56edd8f78bc91d7ca4787e032c0 Mon Sep 17 00:00:00 2001
From: Justin Zeng <hi.justinzeng@gmail.com>
Date: Thu, 18 Apr 2024 06:31:46 +0000
Subject: [PATCH] Add REST API to update pages settings

Changelog: added
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147227
EE: false
---
 app/services/pages/update_service.rb       | 39 ++++++++++++
 doc/api/pages.md                           | 74 +++++++++++++++++++++-
 lib/api/pages.rb                           | 28 ++++++++
 locale/gitlab.pot                          |  3 +
 spec/requests/api/pages_spec.rb            | 61 ++++++++++++++++++
 spec/services/pages/update_service_spec.rb | 56 ++++++++++++++++
 6 files changed, 259 insertions(+), 2 deletions(-)
 create mode 100644 app/services/pages/update_service.rb
 create mode 100644 spec/services/pages/update_service_spec.rb

diff --git a/app/services/pages/update_service.rb b/app/services/pages/update_service.rb
new file mode 100644
index 000000000000..f27b1c29442f
--- /dev/null
+++ b/app/services/pages/update_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Pages
+  class UpdateService < BaseService
+    include Gitlab::Allowable
+
+    def execute
+      unless can_update_page_settings?
+        return ServiceResponse.error(message: _('The current user is not authorized to update the page settings'),
+          reason: :forbidden)
+      end
+
+      Project.transaction do
+        update_pages_unique_domain_enabled!
+        update_pages_https_only!
+      end
+
+      ServiceResponse.success(payload: { project: project })
+    end
+
+    private
+
+    def update_pages_unique_domain_enabled!
+      return unless params.key?(:pages_unique_domain_enabled)
+
+      project.project_setting.update!(pages_unique_domain_enabled: params[:pages_unique_domain_enabled])
+    end
+
+    def update_pages_https_only!
+      return unless params.key?(:pages_https_only)
+
+      project.update!(pages_https_only: params[:pages_https_only])
+    end
+
+    def can_update_page_settings?
+      current_user&.can_read_all_resources? && can?(current_user, :update_pages, project)
+    end
+  end
+end
diff --git a/doc/api/pages.md b/doc/api/pages.md
index 7d8f7e99e543..5060670dc806 100644
--- a/doc/api/pages.md
+++ b/doc/api/pages.md
@@ -34,7 +34,7 @@ DELETE /projects/:id/pages
 curl --request 'DELETE' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/2/pages"
 ```
 
-## Get pages settings for a project
+## Get Pages settings for a project
 
 > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/436932) in GitLab 16.8.
 
@@ -59,7 +59,7 @@ response attributes:
 
 | Attribute                                 | Type       | Description                                                                                                                  |
 | ----------------------------------------- | ---------- | -----------------------                                                                                                      |
-| `url`                                     | string     | URL to access this project pages.                                                                                            |
+| `url`                                     | string     | URL to access this project's Pages.                                                                                            |
 | `is_unique_domain_enabled`                | boolean    | If [unique domain](../user/project/pages/introduction.md) is enabled.                                                        |
 | `force_https`                             | boolean    | `true` if the project is set to force HTTPS.                                                                                      |
 | `deployments[]`                           | array      | List of current active deployments.                                                                                          |
@@ -100,3 +100,73 @@ Example response:
   ]
 }
 ```
+
+## Update Pages settings for a project
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147227) in GitLab 16.11.
+
+Prerequisites:
+
+- You must have administrator access to the instance.
+
+Update Pages settings for the project.
+
+```plaintext
+PATCH /projects/:id/pages
+```
+
+Supported attributes:
+
+| Attribute                       | Type           | Required | Description                                                                                                         |
+| --------------------------------| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------|
+| `id`                            | integer/string | Yes      | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
+| `pages_unique_domain_enabled`   | boolean        | No       | Whether to use unique domain                                                                                        |
+| `pages_https_only`              | boolean        | No       | Whether to force HTTPs                                                                                              |
+
+If successful, returns [`200`](rest/index.md#status-codes) and the following
+response attributes:
+
+| Attribute                                 | Type       | Description                                                                                                                  |
+| ----------------------------------------- | ---------- | -----------------------                                                                                                      |
+| `url`                                     | string     | URL to access this project's Pages.                                                                                            |
+| `is_unique_domain_enabled`                | boolean    | If [unique domain](../user/project/pages/introduction.md) is enabled.                                                        |
+| `force_https`                             | boolean    | `true` if the project is set to force HTTPS.                                                                                      |
+| `deployments[]`                           | array      | List of current active deployments.                                                                                          |
+
+| `deployments[]` attribute                 | Type       | Description                                                                                                                  |
+| ----------------------------------------- | ---------- | -----------------------                                                                                                      |
+| `created_at`                              | date       | Date deployment was created.                                                                                                 |
+| `url`                                     | string     | URL for this deployment.                                                                                                     |
+| `path_prefix`                             | string     | Path prefix of this deployment when using [multiple deployments](../user/project/pages/index.md#create-multiple-deployments). |
+| `root_directory`                          | string     | Root directory.                                                                                                              |
+
+Example request:
+
+```shell
+curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" --url "https://gitlab.example.com/api/v4/projects/:id/pages" \
+--form 'pages_unique_domain_enabled=true' --form 'pages_https_only=true'
+```
+
+Example response:
+
+```json
+{
+  "url": "http://html-root-4160ce5f0e9a6c90ccb02755b7fc80f5a2a09ffbb1976cf80b653.pages.gdk.test:3010",
+  "is_unique_domain_enabled": true,
+  "force_https": false,
+  "deployments": [
+    {
+      "created_at": "2024-01-05T18:58:14.916Z",
+      "url": "http://html-root-4160ce5f0e9a6c90ccb02755b7fc80f5a2a09ffbb1976cf80b653.pages.gdk.test:3010/",
+      "path_prefix": "",
+      "root_directory": null
+    },
+    {
+      "created_at": "2024-01-05T18:58:46.042Z",
+      "url": "http://html-root-4160ce5f0e9a6c90ccb02755b7fc80f5a2a09ffbb1976cf80b653.pages.gdk.test:3010/mr3",
+      "path_prefix": "mr3",
+      "root_directory": null
+    }
+  ]
+}
+```
diff --git a/lib/api/pages.rb b/lib/api/pages.rb
index 30e126b34cbb..8f038c175bd5 100644
--- a/lib/api/pages.rb
+++ b/lib/api/pages.rb
@@ -31,6 +31,34 @@ class Pages < ::API::Base
         no_content!
       end
 
+      desc 'Update pages settings' do
+        detail 'Update page settings for a project. User must have administrative access.'
+        success code: 200
+        failure [
+          { code: 401, message: 'Unauthorized' },
+          { code: 404, message: 'Not Found' }
+        ]
+        tags %w[pages]
+      end
+      params do
+        optional :pages_unique_domain_enabled, type: Boolean, desc: 'Whether to use unique domain'
+        optional :pages_https_only, type: Boolean, desc: 'Whether to force HTTPS'
+      end
+      patch ':id/pages' do
+        authenticated_with_can_read_all_resources!
+        authorize! :update_pages, user_project
+
+        break not_found! unless user_project.pages_enabled?
+
+        response = ::Pages::UpdateService.new(user_project, current_user, params).execute
+
+        if response.success?
+          present ::Pages::ProjectSettings.new(response.payload[:project]), with: Entities::Pages::ProjectSettings
+        else
+          forbidden!(response.message)
+        end
+      end
+
       desc 'Get pages settings' do
         detail 'Get pages URL and other settings. This feature was introduced in Gitlab 16.8'
         success code: 200
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a0f8363796cf..0ad864addb26 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -51360,6 +51360,9 @@ msgstr ""
 msgid "The current user is not authorized to set pipeline schedule variables"
 msgstr ""
 
+msgid "The current user is not authorized to update the page settings"
+msgstr ""
+
 msgid "The current user is not authorized to update the pipeline schedule"
 msgstr ""
 
diff --git a/spec/requests/api/pages_spec.rb b/spec/requests/api/pages_spec.rb
index 23ffeb143cba..a4a066075a02 100644
--- a/spec/requests/api/pages_spec.rb
+++ b/spec/requests/api/pages_spec.rb
@@ -88,4 +88,65 @@
       end
     end
   end
+
+  describe 'PATCH /projects/:id/pages' do
+    let(:path) { "/projects/#{project.id}/pages" }
+    let(:params) { { pages_unique_domain_enabled: true, pages_https_only: true } }
+
+    before do
+      stub_pages_setting(external_https: true)
+    end
+
+    context 'when the user is authorized' do
+      context 'and the update succeeds' do
+        it 'updates the pages settings and returns 200' do
+          patch api(path, admin, admin_mode: true), params: params
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response['force_https']).to eq(true)
+          expect(json_response['is_unique_domain_enabled']).to eq(true)
+        end
+      end
+    end
+
+    context 'when the user is unauthorized' do
+      it 'returns a 403 forbidden' do
+        patch api(path, user), params: params
+
+        expect(response).to have_gitlab_http_status(:forbidden)
+      end
+    end
+
+    context 'when pages feature is disabled' do
+      before do
+        stub_pages_setting(enabled: false)
+      end
+
+      it 'returns a 404 not found' do
+        patch api(path, admin, admin_mode: true), params: params
+
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+
+    context 'when there is no project' do
+      it 'returns 404 not found' do
+        non_existent_project_id = -1
+        patch api("/projects/#{non_existent_project_id}/pages", admin, admin_mode: true), params: params
+
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+
+    context 'when the parameters are invalid' do
+      let(:invalid_params) { { pages_unique_domain_enabled: nil, pages_https_only: "not_a_boolean" } }
+
+      it 'returns a 400 bad request' do
+        patch api(path, admin, admin_mode: true), params: invalid_params
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['error']).to eq('pages_https_only is invalid')
+      end
+    end
+  end
 end
diff --git a/spec/services/pages/update_service_spec.rb b/spec/services/pages/update_service_spec.rb
new file mode 100644
index 000000000000..35086956124c
--- /dev/null
+++ b/spec/services/pages/update_service_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Pages::UpdateService, feature_category: :pages do
+  let_it_be(:admin) { create(:admin) }
+  let_it_be(:user) { create(:user) }
+  let_it_be_with_reload(:project) { create(:project) }
+  let(:params) { { pages_unique_domain_enabled: false, pages_https_only: false } }
+
+  before do
+    stub_pages_setting(external_https: true)
+  end
+
+  describe '#execute' do
+    context 'with sufficient permissions' do
+      let(:service) { described_class.new(project, admin, params) }
+
+      before do
+        allow(admin).to receive(:can_read_all_resources?).and_return(true)
+        allow(service).to receive(:can?).with(admin, :update_pages, project).and_return(true)
+      end
+
+      context 'when updating page setting succeeds' do
+        it 'updates page settings' do
+          create(:project_setting, project: project, pages_unique_domain_enabled: true,
+            pages_unique_domain: "random-unique-domain-here")
+
+          expect { service.execute }
+            .to change { project.reload.pages_https_only }.from(true).to(false)
+            .and change { project.project_setting.pages_unique_domain_enabled }.from(true).to(false)
+        end
+
+        it 'returns a success response' do
+          result = service.execute
+
+          expect(result).to be_a(ServiceResponse)
+          expect(result).to be_success
+          expect(result.payload[:project]).to eq(project)
+        end
+      end
+    end
+
+    context 'with insufficient permissions' do
+      let(:service) { described_class.new(project, user, params) }
+
+      it 'returns a forbidden response' do
+        result = service.execute
+
+        expect(result).to be_a(ServiceResponse)
+        expect(result.error?).to be(true)
+        expect(result.message).to eq(_('The current user is not authorized to update the page settings'))
+      end
+    end
+  end
+end
-- 
GitLab