1# frozen_string_literal: true 2 3class ProjectsController < Projects::ApplicationController 4 include API::Helpers::RelatedResourcesHelpers 5 include IssuableCollections 6 include ExtractsPath 7 include PreviewMarkdown 8 include SendFileUpload 9 include RecordUserLastActivity 10 include ImportUrlParams 11 include FiltersEvents 12 13 prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } 14 15 around_action :allow_gitaly_ref_name_caching, only: [:index, :show] 16 17 before_action :disable_query_limiting, only: [:show, :create] 18 before_action :authenticate_user!, except: [:index, :show, :activity, :refs, :resolve, :unfoldered_environment_names] 19 before_action :redirect_git_extension, only: [:show] 20 before_action :project, except: [:index, :new, :create, :resolve] 21 before_action :repository, except: [:index, :new, :create, :resolve] 22 before_action :verify_git_import_enabled, only: [:create] 23 before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export] 24 before_action :present_project, only: [:edit] 25 before_action :authorize_download_code!, only: [:refs] 26 27 # Authorize 28 before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export] 29 before_action :authorize_archive_project!, only: [:archive, :unarchive] 30 before_action :event_filter, only: [:show, :activity] 31 32 # Project Export Rate Limit 33 before_action :check_export_rate_limit!, only: [:export, :download_export, :generate_new_export] 34 35 before_action do 36 push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml) 37 push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) 38 push_frontend_feature_flag(:highlight_js, @project, default_enabled: :yaml) 39 push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml) 40 push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml) 41 push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks) 42 end 43 44 layout :determine_layout 45 46 feature_category :projects, [ 47 :index, :show, :new, :create, :edit, :update, :transfer, 48 :destroy, :resolve, :archive, :unarchive, :toggle_star, :activity 49 ] 50 51 feature_category :source_code_management, [:remove_fork, :housekeeping, :refs] 52 feature_category :team_planning, [:preview_markdown, :new_issuable_address] 53 feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export] 54 feature_category :code_review, [:unfoldered_environment_names] 55 56 urgency :low, [:refs] 57 urgency :high, [:unfoldered_environment_names] 58 59 def index 60 redirect_to(current_user ? root_path : explore_root_path) 61 end 62 63 # rubocop: disable CodeReuse/ActiveRecord 64 def new 65 @namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id] 66 return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace) 67 68 @project = Project.new(namespace_id: @namespace&.id) 69 end 70 # rubocop: enable CodeReuse/ActiveRecord 71 72 def edit 73 @badge_api_endpoint = expose_path(api_v4_projects_badges_path(id: @project.id)) 74 render_edit 75 end 76 77 def create 78 @project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute 79 80 if @project.saved? 81 experiment(:new_project_sast_enabled, user: current_user).track(:created, 82 property: active_new_project_tab, 83 checked: Gitlab::Utils.to_boolean(project_params[:initialize_with_sast]), 84 project: @project, 85 namespace: @project.namespace 86 ) 87 88 redirect_to( 89 project_path(@project, custom_import_params), 90 notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name } 91 ) 92 else 93 render 'new' 94 end 95 end 96 97 def update 98 result = ::Projects::UpdateService.new(@project, current_user, project_params).execute 99 100 # Refresh the repo in case anything changed 101 @repository = @project.repository 102 103 if result[:status] == :success 104 flash[:notice] = _("Project '%{project_name}' was successfully updated.") % { project_name: @project.name } 105 redirect_to(edit_project_path(@project, anchor: 'js-general-project-settings')) 106 else 107 flash[:alert] = result[:message] 108 @project.reset 109 render 'edit' 110 end 111 end 112 113 # rubocop: disable CodeReuse/ActiveRecord 114 def transfer 115 return access_denied! unless can?(current_user, :change_namespace, @project) 116 117 namespace = Namespace.find_by(id: params[:new_namespace_id]) 118 ::Projects::TransferService.new(project, current_user).execute(namespace) 119 120 if @project.errors[:new_namespace].present? 121 flash[:alert] = @project.errors[:new_namespace].first 122 return redirect_to edit_project_path(@project) 123 end 124 125 redirect_to edit_project_path(@project) 126 end 127 # rubocop: enable CodeReuse/ActiveRecord 128 129 def remove_fork 130 return access_denied! unless can?(current_user, :remove_fork_project, @project) 131 132 if ::Projects::UnlinkForkService.new(@project, current_user).execute 133 flash[:notice] = _('The fork relationship has been removed.') 134 end 135 136 redirect_to edit_project_path(@project) 137 end 138 139 def activity 140 respond_to do |format| 141 format.html 142 format.json do 143 load_events 144 pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) }) 145 end 146 end 147 end 148 149 def show 150 @id, @ref, @path = extract_ref_path 151 152 if @project.import_in_progress? 153 redirect_to project_import_path(@project, custom_import_params) 154 return 155 end 156 157 if @project.pending_delete? 158 flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } 159 end 160 161 respond_to do |format| 162 format.html do 163 @notification_setting = current_user.notification_settings_for(@project) if current_user 164 @project = @project.present(current_user: current_user) 165 166 render_landing_page 167 end 168 169 format.atom do 170 load_events 171 @events = @events.select { |event| event.visible_to_user?(current_user) } 172 render layout: 'xml.atom' 173 end 174 end 175 end 176 177 def destroy 178 return access_denied! unless can?(current_user, :remove_project, @project) 179 180 ::Projects::DestroyService.new(@project, current_user, {}).async_execute 181 flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name } 182 183 redirect_to dashboard_projects_path, status: :found 184 rescue Projects::DestroyService::DestroyError => ex 185 redirect_to edit_project_path(@project), status: :found, alert: ex.message 186 end 187 188 def new_issuable_address 189 return render_404 unless Gitlab::IncomingEmail.supports_issue_creation? 190 191 current_user.reset_incoming_email_token! 192 render json: { new_address: @project.new_issuable_address(current_user, params[:issuable_type]) } 193 end 194 195 def archive 196 ::Projects::UpdateService.new(@project, current_user, archived: true).execute 197 198 respond_to do |format| 199 format.html { redirect_to project_path(@project) } 200 end 201 end 202 203 def unarchive 204 ::Projects::UpdateService.new(@project, current_user, archived: false).execute 205 206 respond_to do |format| 207 format.html { redirect_to project_path(@project) } 208 end 209 end 210 211 def housekeeping 212 ::Repositories::HousekeepingService.new(@project, :gc).execute 213 214 redirect_to( 215 project_path(@project), 216 notice: _("Housekeeping successfully started") 217 ) 218 rescue ::Repositories::HousekeepingService::LeaseTaken => ex 219 redirect_to( 220 edit_project_path(@project, anchor: 'js-project-advanced-settings'), 221 alert: ex.to_s 222 ) 223 end 224 225 def export 226 @project.add_export_job(current_user: current_user) 227 228 redirect_to( 229 edit_project_path(@project, anchor: 'js-export-project'), 230 notice: _("Project export started. A download link will be sent by email and made available on this page.") 231 ) 232 end 233 234 def download_export 235 if @project.export_file_exists? 236 if @project.export_archive_exists? 237 send_upload(@project.export_file, attachment: @project.export_file.filename) 238 else 239 redirect_to( 240 edit_project_path(@project, anchor: 'js-export-project'), 241 alert: _("The file containing the export is not available yet; it may still be transferring. Please try again later.") 242 ) 243 end 244 else 245 redirect_to( 246 edit_project_path(@project, anchor: 'js-export-project'), 247 alert: _("Project export link has expired. Please generate a new export from your project settings.") 248 ) 249 end 250 end 251 252 def remove_export 253 if @project.remove_exports 254 flash[:notice] = _("Project export has been deleted.") 255 else 256 flash[:alert] = _("Project export could not be deleted.") 257 end 258 259 redirect_to(edit_project_path(@project, anchor: 'js-export-project')) 260 end 261 262 def generate_new_export 263 if @project.remove_exports 264 export 265 else 266 redirect_to( 267 edit_project_path(@project, anchor: 'js-export-project'), 268 alert: _("Project export could not be deleted.") 269 ) 270 end 271 end 272 273 def toggle_star 274 current_user.toggle_star(@project) 275 @project.reset 276 277 render json: { 278 star_count: @project.star_count 279 } 280 end 281 282 # rubocop: disable CodeReuse/ActiveRecord 283 def refs 284 find_refs = params['find'] 285 286 find_branches = true 287 find_tags = true 288 find_commits = true 289 290 unless find_refs.nil? 291 find_branches = find_refs.include?('branches') 292 find_tags = find_refs.include?('tags') 293 find_commits = find_refs.include?('commits') 294 end 295 296 options = {} 297 298 if find_branches 299 branches = BranchesFinder.new(@repository, params).execute.take(100).map(&:name) 300 options['Branches'] = branches 301 end 302 303 if find_tags && @repository.tag_count.nonzero? 304 tags = begin 305 TagsFinder.new(@repository, params).execute 306 rescue Gitlab::Git::CommandError 307 [] 308 end 309 310 options['Tags'] = tags.take(100).map(&:name) 311 end 312 313 # If reference is commit id - we should add it to branch/tag selectbox 314 ref = Addressable::URI.unescape(params[:ref]) 315 if find_commits && ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/ 316 options['Commits'] = [ref] 317 end 318 319 render json: options.to_json 320 end 321 # rubocop: enable CodeReuse/ActiveRecord 322 323 def resolve 324 @project = Project.find(params[:id]) 325 326 if can?(current_user, :read_project, @project) 327 redirect_to @project 328 else 329 render_404 330 end 331 end 332 333 def unfoldered_environment_names 334 respond_to do |format| 335 format.json do 336 render json: Environments::EnvironmentNamesFinder.new(@project, current_user).execute 337 end 338 end 339 end 340 341 private 342 343 # Render project landing depending of which features are available 344 # So if page is not available in the list it renders the next page 345 # 346 # pages list order: repository readme, wiki home, issues list, customize workflow 347 def render_landing_page 348 if can?(current_user, :download_code, @project) 349 return render 'projects/no_repo' unless @project.repository_exists? 350 351 render 'projects/empty' if @project.empty_repo? 352 else 353 if can?(current_user, :read_wiki, @project) 354 @wiki = @project.wiki 355 @wiki_home = @wiki.find_page('home', params[:version_id]) 356 elsif @project.feature_available?(:issues, current_user) 357 @issues = issuables_collection.page(params[:page]) 358 @issuable_meta_data = Gitlab::IssuableMetadata.new(current_user, @issues).data 359 end 360 361 render :show 362 end 363 end 364 365 def finder_type 366 IssuesFinder 367 end 368 369 def determine_layout 370 if [:new, :create].include?(action_name.to_sym) 371 'application' 372 elsif [:edit, :update].include?(action_name.to_sym) 373 'project_settings' 374 else 375 'project' 376 end 377 end 378 379 # rubocop: disable CodeReuse/ActiveRecord 380 def load_events 381 projects = Project.where(id: @project.id) 382 383 @events = EventCollection 384 .new(projects, offset: params[:offset].to_i, filter: event_filter) 385 .to_a 386 .map(&:present) 387 end 388 # rubocop: enable CodeReuse/ActiveRecord 389 390 def project_params(attributes: []) 391 params.require(:project) 392 .permit(project_params_attributes + attributes) 393 .merge(import_url_params) 394 end 395 396 def project_feature_attributes 397 %i[ 398 builds_access_level 399 issues_access_level 400 forking_access_level 401 merge_requests_access_level 402 repository_access_level 403 snippets_access_level 404 wiki_access_level 405 pages_access_level 406 metrics_dashboard_access_level 407 analytics_access_level 408 operations_access_level 409 security_and_compliance_access_level 410 container_registry_access_level 411 ] 412 end 413 414 def project_setting_attributes 415 %i[ 416 show_default_award_emojis 417 squash_option 418 mr_default_target_self 419 warn_about_potentially_unwanted_characters 420 ] 421 end 422 423 def project_params_attributes 424 [ 425 :allow_merge_on_skipped_pipeline, 426 :avatar, 427 :build_allow_git_fetch, 428 :build_coverage_regex, 429 :build_timeout_human_readable, 430 :resolve_outdated_diff_discussions, 431 :container_registry_enabled, 432 :default_branch, 433 :description, 434 :emails_disabled, 435 :external_authorization_classification_label, 436 :import_url, 437 :issues_tracker, 438 :issues_tracker_id, 439 :last_activity_at, 440 :lfs_enabled, 441 :name, 442 :only_allow_merge_if_all_discussions_are_resolved, 443 :only_allow_merge_if_pipeline_succeeds, 444 :path, 445 :printing_merge_request_link_enabled, 446 :public_builds, 447 :remove_source_branch_after_merge, 448 :request_access_enabled, 449 :runners_token, 450 :tag_list, 451 :topics, 452 :visibility_level, 453 :template_name, 454 :template_project_id, 455 :merge_method, 456 :initialize_with_sast, 457 :initialize_with_readme, 458 :autoclose_referenced_issues, 459 :suggestion_commit_message, 460 :packages_enabled, 461 :service_desk_enabled, 462 :merge_commit_template, 463 :squash_commit_template, 464 project_setting_attributes: project_setting_attributes 465 ] + [project_feature_attributes: project_feature_attributes] 466 end 467 468 def project_params_create_attributes 469 [:namespace_id] 470 end 471 472 def custom_import_params 473 {} 474 end 475 476 def active_new_project_tab 477 project_params[:import_url].present? ? 'import' : 'blank' 478 end 479 480 def repo_exists? 481 project.repository_exists? && !project.empty_repo? 482 483 rescue Gitlab::Git::Repository::NoRepository 484 project.repository.expire_exists_cache 485 486 false 487 end 488 489 def project_view_files? 490 if current_user 491 current_user.project_view == 'files' 492 else 493 project_view_files_allowed? 494 end 495 end 496 497 # Override extract_ref from ExtractsPath, which returns the branch and file path 498 # for the blob/tree, which in this case is just the root of the default branch. 499 # This way we avoid to access the repository.ref_names. 500 def extract_ref(_id) 501 [get_id, ''] 502 end 503 504 # Override get_id from ExtractsPath in this case is just the root of the default branch. 505 def get_id 506 project.repository.root_ref 507 end 508 509 def project_view_files_allowed? 510 !project.empty_repo? && can?(current_user, :download_code, project) 511 end 512 513 def build_canonical_path(project) 514 params[:namespace_id] = project.namespace.to_param 515 params[:id] = project.to_param 516 517 url_for(safe_params) 518 end 519 520 def verify_git_import_enabled 521 render_404 if project_params[:import_url] && !git_import_enabled? 522 end 523 524 def project_export_enabled 525 render_404 unless Gitlab::CurrentSettings.project_export_enabled? 526 end 527 528 # Redirect from localhost/group/project.git to localhost/group/project 529 def redirect_git_extension 530 return unless params[:format] == 'git' 531 532 # `project` calls `find_routable!`, so this will trigger the usual not-found 533 # behaviour when the user isn't authorized to see the project 534 return if project.nil? || performed? 535 536 redirect_to(request.original_url.sub(%r{\.git/?\Z}, '')) 537 end 538 539 def disable_query_limiting 540 Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20826') 541 end 542 543 def present_project 544 @project = @project.present(current_user: current_user) 545 end 546 547 def check_export_rate_limit! 548 prefixed_action = "project_#{params[:action]}".to_sym 549 550 project_scope = params[:action] == 'download_export' ? @project : nil 551 552 check_rate_limit!(prefixed_action, scope: [current_user, project_scope].compact) 553 end 554 555 def render_edit 556 render 'edit' 557 end 558end 559 560ProjectsController.prepend_mod_with('ProjectsController') 561