Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 92 additions & 1 deletion app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def create
authorize @project

validate_hackatime_projects
validate_urls

if @project.errors.empty? && @project.save
# Create membership for the current user as owner
Expand All @@ -52,7 +53,10 @@ def edit
def update
authorize @project

if @project.update(project_params)
@project.assign_attributes(project_params)
validate_urls

if @project.errors.empty? && @project.save
flash[:notice] = "Project updated successfully"
redirect_to @project
else
Expand Down Expand Up @@ -101,6 +105,93 @@ def validate_hackatime_projects
@project.errors.add(:base, "The following Hackatime projects are already linked: #{already_linked.pluck(:name).join(', ')}")
end

def validate_urls
if @project.demo_url.blank? && @project.repo_url.blank? && @project.readme_url.blank?
return
end


if @project.demo_url.present? && @project.repo_url.present?
if @project.demo_url == @project.repo_url || @project.demo_url == @project.readme_url
@project.errors.add(:base, "Demo URL and Repository URL cannot be the same")
end
end

validate_url_not_dead(:demo_url, "Demo URL") if @project.demo_url.present? && @project.errors.empty?

validate_url_not_dead(:repo_url, "Repository URL") if @project.repo_url.present? && @project.errors.empty?
validate_url_not_dead(:readme_url, "Readme URL") if @project.readme_url.present? && @project.errors.empty?
end

def validate_url_not_dead(attribute, name)
require "uri"
require "faraday"

return unless @project.send(attribute).present?

uri = URI.parse(@project.send(attribute))
conn = Faraday.new(
url: uri.to_s,
headers: { "User-Agent" => "Flavortown project validtor (https://flavortown.hackclub.com/)" }
)
response = conn.get() do |req|
req.options.timeout = 5
req.options.open_timeout = 5
end

unless response.status == 200
@project.errors.add(attribute, "Your #{name} needs to return a 200 status. I got #{response.status}, is your code/website set to public!?!?")
end


# Copy pasted from https://github.com/hackclub/summer-of-making/blob/29e572dd6df70627d37f3718a6ebd4bafb07f4c7/app/controllers/projects_controller.rb#L275
if attribute != :demo_url
repo_patterns = [
%r{/blob/}, %r{/tree/}, %r{/src/}, %r{/raw/}, %r{/commits/},
%r{/pull/}, %r{/issues/}, %r{/compare/}, %r{/releases/},
/\.git$/, %r{/commit/}, %r{/branch/}, %r{/blame/},

%r{/projects/}, %r{/repositories/}, %r{/gitea/}, %r{/cgit/},
%r{/gitweb/}, %r{/gogs/}, %r{/git/}, %r{/scm/},

/\.(md|py|js|ts|jsx|tsx|html|css|scss|php|rb|go|rs|java|cpp|c|h|cs|swift)$/
]

# Known code hosting platforms (not required, but used for heuristic)
known_platforms = [
"github", "gitlab", "bitbucket", "dev.azure", "sourceforge",
"codeberg", "sr.ht", "replit", "vercel", "netlify", "glitch",
"hackclub", "gitea", "git", "repo", "code"
]

path = uri.path.downcase
host = uri.host.downcase

is_valid_repo_url = false

if repo_patterns.any? { |pattern| path.match?(pattern) }
is_valid_repo_url = true
elsif attribute == :readme_url && (host.include?("raw.githubusercontent") || path.include?("/readme") || path.end_with?(".md") || path.end_with?("readme.txt"))
is_valid_repo_url = true
elsif known_platforms.any? { |platform| host.include?(platform) }
is_valid_repo_url = path.split("/").size > 2
elsif path.split("/").size > 1 && path.exclude?("wp-") && path.exclude?("blog")
is_valid_repo_url = true
end

unless is_valid_repo_url
@project.errors.add(attribute, "#{name} does not appear to be a valid repository or project URL")
end
end

rescue URI::InvalidURIError
@project.errors.add(attribute, "#{name} is not a valid URL")
rescue Faraday::ConnectionFailed => e
@project.errors.add(attribute, "Please make sure the url is valid and reachable: #{e.message}")
rescue StandardError => e
@project.errors.add(attribute, "#{name} could not be verified (idk why, pls let a admin know if this is happning alot and your sure that the url is valid): #{e.message}")
end

def link_hackatime_projects
return if hackatime_project_ids.empty?

Expand Down
24 changes: 24 additions & 0 deletions app/javascript/controllers/project_form_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export default class extends Controller {

this.updateSubmitState(); // submit button

this.restorReadmeWhenThereIsAError();

if (
this.hasRepoUrlTarget &&
this.hasReadmeUrlTarget &&
Expand Down Expand Up @@ -238,6 +240,28 @@ export default class extends Controller {
}
}

restorReadmeWhenThereIsAError() {
if (!this.hasReadmeContainerTarget || !this.hasReadmeUrlTarget) return;
const value = (this.readmeUrlTarget.value || "").trim();
if (!value) return;

this.readmeContainerTarget.hidden = false;

if (this.readmeUrlTarget.dataset.autofilled === "true") {
this.readmeUrlTarget.readOnly = true;
const control = this.readmeUrlTarget.closest(".input__control");
if (control) control.classList.add("input__control--locked");
this.readmeUrlTarget.title = "Autodetected from repository (locked)";
this.userEditedReadme = false;
} else {
this.readmeUrlTarget.readOnly = false;
const control = this.readmeUrlTarget.closest(".input__control");
if (control) control.classList.remove("input__control--locked");
this.readmeUrlTarget.removeAttribute("title");
this.userEditedReadme = true;
}
}

// util
debounce(fn, wait) {
let t;
Expand Down