Lets you browse exactly the packages that the libs section blacklist would remove on the next import, to review them visually before running it.
470 lines
17 KiB
Ruby
470 lines
17 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
|
|
)
|
|
# Semantic recommendations based on the package's own description
|
|
# embedding - a plain nearest neighbor query, no service call.
|
|
@related_packages = @package.related_packages
|
|
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)
|
|
|
|
# Maps package id to the search strategy that put it into the
|
|
# result list: :exact, :name_prefix, :name_contains, :semantic or
|
|
# :fulltext. Empty when browsing without a search term.
|
|
@search_sources = {}
|
|
|
|
# text search
|
|
if params[:search].present?
|
|
logger.debug "Searching for #{params[:search]}"
|
|
packages = search_packages(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'
|
|
when 'libs'
|
|
# Temporary inspection view: exactly the packages that the libs
|
|
# section blacklist would remove on the next import
|
|
packages = packages.where("section ~* '/?(old)?libs$'")
|
|
end
|
|
|
|
packages
|
|
end
|
|
|
|
# Text search for packages: an exact package name match wins without
|
|
# touching the vector service. Otherwise packages whose name starts
|
|
# with the query (or contains all query tokens, catching compound
|
|
# words like "sqlite browser" -> "sqlitebrowser") are promoted above
|
|
# the semantic (vector) nearest neighbor results. Classic PostgreSQL
|
|
# full-text search kicks in when the vector service cannot be reached.
|
|
MAX_PROMOTED_NAME_MATCHES = 8
|
|
MAX_SEMANTIC_RESULTS = 100
|
|
private_constant :MAX_PROMOTED_NAME_MATCHES, :MAX_SEMANTIC_RESULTS
|
|
|
|
def search_packages(query)
|
|
exact_match = Package.find_by(name: query)
|
|
if exact_match
|
|
@search_sources[exact_match.id] = :exact
|
|
return Package.where(id: exact_match.id)
|
|
end
|
|
|
|
prefix_matches = Package.name_starts_with(query).limit(MAX_PROMOTED_NAME_MATCHES).to_a
|
|
token_matches = Package.name_contains_all(query.to_s.split(/\s+/)).limit(
|
|
MAX_PROMOTED_NAME_MATCHES
|
|
).to_a
|
|
prefix_matches.each { |package| @search_sources[package.id] ||= :name_prefix }
|
|
token_matches.each { |package| @search_sources[package.id] ||= :name_contains }
|
|
|
|
semantic = []
|
|
if Package.lexical_overlap?(query)
|
|
begin
|
|
semantic = Package.nearest_to_text(query, limit: MAX_SEMANTIC_RESULTS).to_a
|
|
semantic.each { |package| @search_sources[package.id] ||= :semantic }
|
|
rescue Vectorizer::Error => e
|
|
logger.error "Semantic search failed: #{e.message}"
|
|
flash.now['error'] = 'Semantic search is unavailable right now. Showing full-text matches.'
|
|
semantic = Package.general_search(query).limit(100).to_a
|
|
semantic.each { |package| @search_sources[package.id] ||= :fulltext }
|
|
end
|
|
else
|
|
logger.debug "Query '#{query}' has no lexical overlap with package data"
|
|
end
|
|
|
|
results = (prefix_matches + token_matches + semantic).uniq(&:id)
|
|
|
|
# Queries that share no vocabulary with our package data can produce
|
|
# no meaningful results. Skip the vector service round-trip and show
|
|
# an honest empty result instead of random packages.
|
|
results.empty? ? Package.none : results
|
|
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
|