debshots/app/controllers/packages_controller.rb
2026-02-26 00:28:00 +01:00

411 lines
14 KiB
Ruby

class PackagesController < ApplicationController
# Allow legacy /uploadfile URL without CSRF protection
protect_from_forgery except: :legacy_uploadfile
def list
@packages = query_packages.paginate(page: params[:page], per_page: 6)
@view_style = :list
render :browse
end
def grid
@packages = query_packages.paginate(page: params[:page], per_page: 24)
@view_style = :grid
render :browse
end
def details
@package = Package.find_by(name: params[:name])
# Highlight a certain screenshot (by ID). This is used by the logs.slim
# view to link to a certain screenshot of a package.
@highlight_id = params[:highlight].to_i if params[:highlight]
if @package.nil?
@packagename = params[:name]
render 'notfound', status: 404
else
@page = params[:page]
# @screenshots = screenshots_visible_to_user(@package).paginate(page: @page, per_page: 6)
@screenshots = @package.screenshots.accessible_by(current_ability, :view).paginate(
page: @page, per_page: 6
)
end
end
# Show upload form for new images
def upload
@package = Package.find_by!(name: params[:name])
end
# POST target of the screenshots upload form.
# Receives uploaded images. Checks if they are valid. Asks for description.
# This action saves the screenshots already if they are valid. The user is
# then given the chance to comment on and delete the screenshots again.
def upload_receive
@package = Package.find_by!(name: params[:name])
@valid_images = []
@invalid_images = []
# If Javascript is disabled a user may submit an empty selection of images.
if params[:file].nil?
flash[:error] = 'You have not selected any images.'
redirect_to upload_path
return
end
all_errors = []
files = params[:file]
# Turn into array if a single image was uploaded through AJAX
files = [files] if files.class != Array
files.each do |img|
new_screenshot = @package.screenshots.new(simage: img)
# Check if the image was valid
if new_screenshot.valid?
# new_screenshot.uploaderhash = session.id.to_s
new_screenshot.uploaderip = request.remote_ip
new_screenshot.version = @package.version
# ActiveStorage does not yet create a file checksum automatically.
# Let's do that. It helps detect duplicate uploads later.
new_screenshot.image_fingerprint = Digest::MD5.hexdigest(File.read(img.path))
# Check that this screenshot is not a duplicate for this package
if @package.screenshots.where(image_fingerprint: new_screenshot.image_fingerprint).any?
auditlog "Duplicate image with fingerprint #{new_screenshot.image_fingerprint} found. Rejecting.",
package: @package
all_errors << "Your file #{img.original_filename} is a duplicate. Sorry."
else
# TODO: Can the upload get approved automatically?
# Create a pseudo user account for the user.
# The user won't know that an account is created.
# But this makes it easier to track who screenshots belong to.
unless user_signed_in?
sign_in User.create_pseudo_user
auditlog 'New pseudo user for anonymous upload created and logged in.',
package: @package, screenshot: new_screenshot
end
new_screenshot.user = current_user
new_screenshot.approve! if can?(:approve, new_screenshot)
# Pre-render screenshot in different sizes
new_screenshot.simage_derivatives!
new_screenshot.save!
auditlog "Screenshot #{new_screenshot.id} uploaded successfully.",
screenshot: new_screenshot, package: @package
@valid_images.push new_screenshot
end
else
errors = new_screenshot.errors[:simage]
auditlog "Screenshot #{img.original_filename} invalid (#{errors}).",
package: @package
# @invalid_images.push img.original_filename
# raise
all_errors << "Your file #{img.original_filename} #{errors.join(' and ')}."
end
end
flash[:error] = all_errors if all_errors.any?
# # Redirect back to upload form if all uploads were invalid
# unless @valid_images.any?
# auditlog "No valid images uploaded. Back to upload form."
# redirect_to(upload_path, error: all_errors) and return
# end
# Show a list of invalid uploads by default. Or redirect to the review page
# if all uploads were okay.
# redirect_to upload_review_path unless @invalid_images
# TODO
# if @invalid_images…
# ' #{image.image_file_name} (#{image.errors[:image].join(' and ')})
# Inform the admins about the upload
AdminMailer.with(package: @package).new_uploads_email.deliver_now if @valid_images.any?
# Rails does not allow dots in the URL. So we cannot use the 'respond_to'
# and 'format' ways to handle parameters. Instead the 'returns' parameters
# is set in routes.rb to signal that this method was called by AJAX.
if params[:returns] == :json
# TODO: send all_errors back as JSON and make Javascript display it in #messages
render json: { errors: all_errors.join(' ') }
else
redirect_to package_path
end
end
# Legacy action to upload an image along with metadata.
# This was used in Debshots 1.x as the default upload method.
# This method allows that old-style way to upload screenshots.
# It is probably used by screenshotting tools.
#
# Parameters:
# - packagename
# - version
# - description
# - file
def legacy_uploadfile
@package = Package.find_by!(name: params[:packagename])
new_screenshot = @package.screenshots.new(image: params[:file])
# Check if the image was valid
if new_screenshot.valid?
# new_screenshot.uploaderhash = session[:token]
new_screenshot.uploaderip = session[:ip]
new_screenshot.version = @package.version
new_screenshot.save
auditlog "Screenshot #{new_screenshot.id} uploaded successfully from legacy upload form.",
screenshot: new_screenshot, package: @package
redirect_to package_path(params[:packagename])
else
auditlog "Screenshot upload rejected. Errors: #{new_screenshot.errors.to_a}",
package: @package
head :not_acceptable
end
end
def hide_screenshot
# Is the user allowed to hide this screenshot?
@screenshot = Screenshot.find(params[:id])
# Check if the user is allowed to change this screenshot
# - Is this the user's own screenshot? (anonymous)
if can? :hide, @screenshot
auditlog "Screenshot #{@screenshot.id} hidden",
package: @screenshot.package
@screenshot.hide!
flash['notice'] = 'Screenshot hidden.'
redirect_back(fallback_location: package_path(name: @screenshot.package.name))
else
head :forbidden
end
end
def unhide_screenshot
# Is the user allowed to unhide this screenshot?
@screenshot = Screenshot.find(params[:id])
# Check if the user is allowed to change this screenshot
# - Is this the user's own screenshot? (anonymous)
if can? :unhide, @screenshot
auditlog "Screenshot #{@screenshot.id} un-hidden",
package: @screenshot.package
@screenshot.unhide!
flash['notice'] = 'Screenshot un-hidden.'
redirect_back(fallback_location: package_path(name: @screenshot.package.name))
else
head :forbidden
end
end
def delete_screenshot
# Is the user allowed to delete the screenshot?
@screenshot = Screenshot.find(params[:id])
# Check if the user is allowed to change this screenshot
# - Is this the user's own screenshot? (anonymous)
if can? :destroy, @screenshot
auditlog "Screenshot #{@screenshot.id} deleted",
package: @screenshot.package
@screenshot.destroy
# Increase the rejection counter for the user (social scoring)
# if the screenshot is new and pending approval
if !@screenshot.approved && @screenshot.user
@screenshot.user.rejected_screenshots += 1
@screenshot.user.save!
end
flash['notice'] = 'Screenshot deleted.'
redirect_back(fallback_location: package_path(name: @screenshot.package.name))
else
head :forbidden
end
end
def approve_screenshot
@screenshot = Screenshot.find(params[:id])
unless can? :approve, @screenshot
head :forbidden
return
end
@screenshot.approve!
auditlog 'Screenshot approved',
package: @screenshot.package, screenshot: @screenshot
if @screenshot.user
# Increase the approval counter for the user (social scoring)
@screenshot.user.approved_screenshots += 1
@screenshot.user.save!
end
flash['notice'] = 'Screenshot approved.'
redirect_back(fallback_location: package_path(name: @screenshot.package.name))
# head :forbidden
end
# Returns either…
# - 160x120 thumbnail image
# - 320x240 small image
# - full-size screenshot
#
# If the package is not found it returns a dummy image along with status 404.
# If the package is found but has no screenshots then it also returns a
# dummy image along with status 404.
#
# Return a specific screenshot (if the screenshot_id parameter is given)
# or just the first (newest) one.
def send_image
size = params[:size] # :small, :large or :thumb
@package = Package.find_by(name: params[:name]) # package name
@screenshot_id = params[:screenshot_id] # screenshot ID (optional)
unless @package
Rails.logger.debug 'no such package -> 404'
screenshot404 if %i[large small].include?(size)
thumbnail404 if size == :thumb
return
end
@image = nil
if params[:screenshot_id]
# Called as /screenshot/:name/:screenshot_id
# TODO: 'name' is useless here
Rails.logger.debug "Called as /screenshot/#{params[:name]}/#{params[:screenshot_id]}"
@image = Screenshot.find(params[:screenshot_id])
unless can? :view, @image
return screenshot403
end
elsif params[:version]
# Called as /screenshot-with-version/:name/:version
# or /thumbnail-with-version/:name/:version
# TODO: permissions check!?
@image = @package.best_screenshot_for_version(params[:version])
else
# Called as /screenshot/:name
@image = @package.screenshots.accessible_by(current_ability, :view).first
end
# Return a 404 if the package has no screenshots or the image was not found
if @image
send_file(File.join(@image.simage.storage.directory, @image.simage(size).id),
disposition: 'inline')
else
Rails.logger.debug 'no such image -> 404'
case size
when :large
screenshot404
when :small
screenshot404
when :thumb
Rails.logger.debug 'thumb404'
thumbnail404
end
end
end
# Receives a form with a simple text field 'description' so that users can update
# the description of their screenshot.
def update_screenshot_description
# TODO: Check permissions to do that
@screenshot = Screenshot.find(params[:id])
# @screenshot.description = params[:description]
@screenshot.update params_screenshot_description
@screenshot.save!
flash['notice'] = 'Description updated.'
redirect_back(fallback_location: package_path(name: @screenshot.package.name))
end
# Show an HTML partial with reviews of this package from the Ubuntu API
# def reviews
# expires_in 1.day, public: true
# # @reviews = Package.find_by_name!(params[:name]).ubuntu_reviews
# @reviews = get_ubuntu_reviews params[:name]
# render '_reviews', layout: false
# end
private
# Send a dummy thumbnail reading "No screenshot available. Sorry."
def thumbnail404
send_file Rails.root.join('public/images/dummy/thumbnail404.png'), type: 'image/png',
disposition: 'inline', status: 404
end
# Send a dummy screenshot reading "No screenshot available. Sorry."
def screenshot404
send_file Rails.root.join('public/images/dummy/screenshot404.png'), type: 'image/png',
disposition: 'inline', status: 404
end
# Send a dummy screenshot reading "No screenshot available. Sorry."
def screenshot403
send_file Rails.root.join('public/images/dummy/screenshot403.png'), type: 'image/png',
disposition: 'inline', status: 404
end
# Return packages matching the criteria given by parameters
def query_packages
packages = Package # .order(visits: :desc)
# text search
if params[:search].present?
logger.debug "Searching for #{params[:search]}"
packages = packages.general_search(params[:search])
end
case params[:show]
when 'with'
# Enrich the result with the screenshots readable by the current user (CanCanCan)
packages = packages.with_public_screenshots
logger.debug 'Limiting packages to those with screenshots'
when 'without'
packages = packages.without_screenshots
logger.debug 'Limiting packages to those without screenshots'
end
packages
end
# Store a random identifier and the client's IP address in the session
# for later identification.
# def create_user_token
# session[:token] ||= SecureRandom.hex
# session[:ip] ||= request.remote_ip
# end
# Get reviews of this package from the Ubuntu API
# def get_ubuntu_reviews(packagename)
# # Use the URL defined in the configuration to get a JSON string
# url = Rails.configuration.ubuntu_reviews_api_url % packagename
# logger.debug "Loading Ubuntu reviews for package #{packagename} from #{url}"
# body = open(url).read
# # Turn JSON into a Ruby data structure
# json = JSON.parse(body)
# # Only show english reviews
# # TODO: Support further languages
# json = json.select { |x| x['language'] == 'en' }
# # Sort by 'usefulness_total' (how many people found this review useful)
# json.sort { |x, y| y['usefulness_total'].to_i <=> x['usefulness_total'].to_i }
# end
# def params_screenshot_description
# params.require(:screenshot).permit(:description)
# end
# def screenshots_visible_to_user(package)
# package.screenshots.accessible_by(current_ability, :view).order('created_at DESC')
# end
end