mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-23 21:18:58 +00:00
(search) Site info view is mostly done
Also optimize the rendering a bit to avoid having to allocate huge string buffers, writing directly to Spark's response instead.
This commit is contained in:
parent
2f4500be5a
commit
7c8a60b8cf
@ -16,6 +16,7 @@ dependencies {
|
||||
|
||||
implementation libs.bundles.handlebars
|
||||
implementation libs.guice
|
||||
implementation libs.spark
|
||||
|
||||
testImplementation libs.bundles.slf4j.test
|
||||
testImplementation libs.bundles.junit
|
||||
|
@ -8,9 +8,12 @@ import lombok.SneakyThrows;
|
||||
import nu.marginalia.renderer.config.HandlebarsConfigurator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -48,6 +51,23 @@ public class MustacheRenderer<T> {
|
||||
return template.apply(model);
|
||||
}
|
||||
|
||||
private Writer getWriter(Response response) throws IOException {
|
||||
|
||||
// response.raw() has a getWriter() method that fits here, but this is a trap, as subsequent
|
||||
// calls to response.raw().getOutputStream() will fail with an IllegalStateException; and we
|
||||
// have internal code that does this.
|
||||
|
||||
return new OutputStreamWriter(response.raw().getOutputStream());
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Object renderInto(Response response, T model) {
|
||||
|
||||
template.apply(model, getWriter(response));
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public <T2> String render(T model, String name, List<T2> children) {
|
||||
Context ctx = Context.newBuilder(model).combine(name, children).build();
|
||||
@ -55,10 +75,22 @@ public class MustacheRenderer<T> {
|
||||
return template.apply(ctx);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public <T2> void renderInto(Response response, T model, String name, List<T2> children) {
|
||||
Context ctx = Context.newBuilder(model).combine(name, children).build();
|
||||
|
||||
template.apply(ctx, getWriter(response));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public <T2> String render(T model, Map<String, ?> children) {
|
||||
Context ctx = Context.newBuilder(model).combine(children).build();
|
||||
return template.apply(ctx);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public <T2> void renderInto(Response response, T model, Map<String, ?> children) {
|
||||
Context ctx = Context.newBuilder(model).combine(children).build();
|
||||
template.apply(ctx, getWriter(response));
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +75,14 @@ public class SearchOperator {
|
||||
|
||||
return searchQueryService.getResultsFromQuery(queryResponse);
|
||||
}
|
||||
public List<UrlDetails> doBacklinkSearch(Context ctx,
|
||||
String domain) {
|
||||
|
||||
var queryParams = paramFactory.forBacklinkSearch(domain);
|
||||
var queryResponse = queryClient.search(ctx, queryParams);
|
||||
|
||||
return searchQueryService.getResultsFromQuery(queryResponse);
|
||||
}
|
||||
public DecoratedSearchResults doSearch(Context ctx, SearchParameters userParams) {
|
||||
|
||||
Future<String> eval = searchUnitConversionService.tryEval(ctx, userParams.query());
|
||||
|
@ -51,4 +51,22 @@ public class SearchQueryParamFactory {
|
||||
SearchSetIdentifier.NONE
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public QueryParams forBacklinkSearch(String domain) {
|
||||
return new QueryParams("links:"+domain,
|
||||
null,
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
SpecificationLimit.none(),
|
||||
SpecificationLimit.none(),
|
||||
SpecificationLimit.none(),
|
||||
SpecificationLimit.none(),
|
||||
List.of(),
|
||||
new QueryLimits(100, 100, 100, 512),
|
||||
SearchSetIdentifier.NONE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ public class SearchService extends Service {
|
||||
SearchErrorPageService errorPageService,
|
||||
SearchAddToCrawlQueueService addToCrawlQueueService,
|
||||
SearchFlagSiteService flagSiteService,
|
||||
SearchSiteInfoService siteInfoService,
|
||||
SearchQueryService searchQueryService
|
||||
) {
|
||||
super(params);
|
||||
@ -50,10 +51,10 @@ public class SearchService extends Service {
|
||||
|
||||
Spark.post("/public/site/suggest/", addToCrawlQueueService::suggestCrawling);
|
||||
|
||||
Spark.get("/public/site/flag-site/:domainId", flagSiteService::flagSiteForm);
|
||||
Spark.post("/public/site/flag-site/:domainId", flagSiteService::flagSiteAction);
|
||||
Spark.get("/public/site-search/:site/*", this::siteSearchRedir);
|
||||
Spark.get("/public/site/:site", this::siteSearchRedir);
|
||||
|
||||
Spark.get("/public/site/:site", siteInfoService::handle);
|
||||
Spark.post("/public/site/:site", siteInfoService::handlePost);
|
||||
|
||||
Spark.exception(Exception.class, (e,p,q) -> {
|
||||
logger.error("Error during processing", e);
|
||||
|
@ -3,6 +3,7 @@ package nu.marginalia.search.command;
|
||||
import com.google.inject.Inject;
|
||||
import nu.marginalia.search.command.commands.*;
|
||||
import nu.marginalia.client.Context;
|
||||
import spark.Response;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -17,30 +18,32 @@ public class CommandEvaluator {
|
||||
BrowseCommand browse,
|
||||
ConvertCommand convert,
|
||||
DefinitionCommand define,
|
||||
SiteListCommand site,
|
||||
BangCommand bang,
|
||||
SiteRedirectCommand siteRedirect,
|
||||
SearchCommand search
|
||||
) {
|
||||
specialCommands.add(browse);
|
||||
specialCommands.add(convert);
|
||||
specialCommands.add(define);
|
||||
specialCommands.add(site);
|
||||
specialCommands.add(bang);
|
||||
specialCommands.add(siteRedirect);
|
||||
|
||||
defaultCommand = search;
|
||||
}
|
||||
|
||||
public Object eval(Context ctx, SearchParameters parameters) {
|
||||
public Object eval(Context ctx, Response response, SearchParameters parameters) {
|
||||
for (var cmd : specialCommands) {
|
||||
var ret = cmd.process(ctx, parameters);
|
||||
if (ret.isPresent()) {
|
||||
return ret.get();
|
||||
if (cmd.process(ctx, response, parameters)) {
|
||||
// The commands will write directly to the response, so we don't need to do anything else
|
||||
// but it's important we don't return null, as this signals to Spark that we haven't handled
|
||||
// the request.
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Always process the search command last
|
||||
return defaultCommand.process(ctx, parameters)
|
||||
.orElseThrow(() -> new IllegalStateException("Search Command returned Optional.empty()!") /* This Should Not be Possible™ */ );
|
||||
defaultCommand.process(ctx, response, parameters);
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,9 +2,8 @@ package nu.marginalia.search.command;
|
||||
|
||||
|
||||
import nu.marginalia.client.Context;
|
||||
|
||||
import java.util.Optional;
|
||||
import spark.Response;
|
||||
|
||||
public interface SearchCommandInterface {
|
||||
Optional<Object> process(Context ctx, SearchParameters parameters);
|
||||
boolean process(Context ctx, Response response, SearchParameters parameters);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import nu.marginalia.search.command.SearchCommandInterface;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.client.Context;
|
||||
import nu.marginalia.search.exceptions.RedirectException;
|
||||
import spark.Response;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -23,7 +24,7 @@ public class BangCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
||||
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||
|
||||
for (var entry : bangsToPattern.entrySet()) {
|
||||
String bangPattern = entry.getKey();
|
||||
@ -37,7 +38,7 @@ public class BangCommand implements SearchCommandInterface {
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
return false;
|
||||
}
|
||||
|
||||
private Optional<String> matchBangPattern(String query, String bangKey) {
|
||||
|
@ -17,6 +17,7 @@ import nu.marginalia.renderer.MustacheRenderer;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
@ -56,16 +57,22 @@ public class BrowseCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
||||
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||
if (!queryPatternPredicate.test(parameters.query())) {
|
||||
return Optional.empty();
|
||||
return false;
|
||||
}
|
||||
|
||||
return Optional.ofNullable(browseSite(ctx, parameters.query()))
|
||||
.map(results -> browseResultsRenderer.render(results,
|
||||
var model = browseSite(ctx, parameters.query());
|
||||
|
||||
if (null == model)
|
||||
return false;
|
||||
|
||||
browseResultsRenderer.renderInto(response, model,
|
||||
Map.of("query", parameters.query(),
|
||||
"profile", parameters.profileStr(),
|
||||
"focusDomain", results.focusDomain())));
|
||||
"focusDomain", model.focusDomain())
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,16 +1,17 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import lombok.SneakyThrows;
|
||||
import nu.marginalia.search.command.SearchCommandInterface;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.svc.SearchUnitConversionService;
|
||||
import nu.marginalia.client.Context;
|
||||
import nu.marginalia.renderer.MustacheRenderer;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ConvertCommand implements SearchCommandInterface {
|
||||
private final SearchUnitConversionService searchUnitConversionService;
|
||||
@ -24,16 +25,19 @@ public class ConvertCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
||||
@SneakyThrows
|
||||
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||
var conversion = searchUnitConversionService.tryConversion(ctx, parameters.query());
|
||||
if (conversion.isEmpty()) {
|
||||
return Optional.empty();
|
||||
return false;
|
||||
}
|
||||
|
||||
return Optional.of(conversionRenderer.render(Map.of(
|
||||
conversionRenderer.renderInto(response, Map.of(
|
||||
"query", parameters.query(),
|
||||
"result", conversion.get(),
|
||||
"profile", parameters.profileStr()))
|
||||
"profile", parameters.profileStr())
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -12,10 +12,10 @@ import nu.marginalia.renderer.MustacheRenderer;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -38,17 +38,19 @@ public class DefinitionCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
||||
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||
if (!queryPatternPredicate.test(parameters.query())) {
|
||||
return Optional.empty();
|
||||
return false;
|
||||
}
|
||||
|
||||
var results = lookupDefinition(ctx, parameters.query());
|
||||
|
||||
return Optional.of(dictionaryRenderer.render(results,
|
||||
dictionaryRenderer.renderInto(response, results,
|
||||
Map.of("query", parameters.query(),
|
||||
"profile", parameters.profileStr())
|
||||
));
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
@ -10,9 +10,9 @@ import nu.marginalia.search.model.DecoratedSearchResults;
|
||||
import nu.marginalia.search.model.UrlDetails;
|
||||
import nu.marginalia.renderer.MustacheRenderer;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SearchCommand implements SearchCommandInterface {
|
||||
private final DomainBlacklist blacklist;
|
||||
@ -32,10 +32,12 @@ public class SearchCommand implements SearchCommandInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
||||
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||
DecoratedSearchResults results = searchOperator.doSearch(ctx, parameters);
|
||||
|
||||
return Optional.of(searchResultsRenderer.render(results));
|
||||
searchResultsRenderer.renderInto(response, results);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isBlacklisted(UrlDetails details) {
|
||||
|
@ -1,119 +0,0 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import nu.marginalia.db.DbDomainQueries;
|
||||
import nu.marginalia.model.EdgeDomain;
|
||||
import nu.marginalia.search.SearchOperator;
|
||||
import nu.marginalia.search.model.UrlDetails;
|
||||
import nu.marginalia.search.command.SearchCommandInterface;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import nu.marginalia.search.model.DomainInformation;
|
||||
import nu.marginalia.search.model.SearchProfile;
|
||||
import nu.marginalia.search.siteinfo.DomainInformationService;
|
||||
import nu.marginalia.search.svc.SearchQueryIndexService;
|
||||
import nu.marginalia.client.Context;
|
||||
import nu.marginalia.renderer.MustacheRenderer;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SiteListCommand implements SearchCommandInterface {
|
||||
private final DbDomainQueries domainQueries;
|
||||
private final DomainInformationService domainInformationService;
|
||||
private final SearchQueryIndexService searchQueryIndexService;
|
||||
private final SearchOperator searchOperator;
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private final MustacheRenderer<DomainInformation> siteInfoRenderer;
|
||||
|
||||
private final Predicate<String> queryPatternPredicate = Pattern.compile("^site:[.A-Za-z\\-0-9]+$").asPredicate();
|
||||
|
||||
@Inject
|
||||
public SiteListCommand(
|
||||
DomainInformationService domainInformationService,
|
||||
DbDomainQueries domainQueries,
|
||||
RendererFactory rendererFactory,
|
||||
SearchQueryIndexService searchQueryIndexService, SearchOperator searchOperator)
|
||||
throws IOException
|
||||
{
|
||||
this.domainQueries = domainQueries;
|
||||
this.domainInformationService = domainInformationService;
|
||||
|
||||
siteInfoRenderer = rendererFactory.renderer("search/site-info");
|
||||
this.searchQueryIndexService = searchQueryIndexService;
|
||||
this.searchOperator = searchOperator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Object> process(Context ctx, SearchParameters parameters) {
|
||||
if (!queryPatternPredicate.test(parameters.query())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
var results = siteInfo(ctx, parameters.query());
|
||||
var domain = results.getDomain();
|
||||
|
||||
List<UrlDetails> resultSet;
|
||||
Path screenshotPath = null;
|
||||
int domainId = -1;
|
||||
if (null != domain) {
|
||||
resultSet = searchOperator.doSiteSearch(ctx, domain.toString());
|
||||
|
||||
var maybeId = domainQueries.tryGetDomainId(domain);
|
||||
if (maybeId.isPresent()) {
|
||||
domainId = maybeId.getAsInt();
|
||||
screenshotPath = Path.of("/screenshot/" + domainId);
|
||||
}
|
||||
else {
|
||||
domainId = -1;
|
||||
screenshotPath = Path.of("/screenshot/0");
|
||||
}
|
||||
}
|
||||
else {
|
||||
resultSet = Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<String, Object> renderObject = new HashMap<>(10);
|
||||
|
||||
renderObject.put("query", parameters.query());
|
||||
renderObject.put("hideRanking", true);
|
||||
renderObject.put("profile", parameters.profileStr());
|
||||
renderObject.put("results", resultSet);
|
||||
renderObject.put("screenshot", screenshotPath == null ? "" : screenshotPath.toString());
|
||||
renderObject.put("domainId", domainId);
|
||||
renderObject.put("focusDomain", domain);
|
||||
|
||||
return Optional.of(siteInfoRenderer.render(results, renderObject));
|
||||
}
|
||||
|
||||
|
||||
private DomainInformation siteInfo(Context ctx, String humanQuery) {
|
||||
String definePrefix = "site:";
|
||||
String word = humanQuery.substring(definePrefix.length()).toLowerCase();
|
||||
|
||||
logger.info("Fetching Site Info: {}", word);
|
||||
|
||||
var results = domainInformationService
|
||||
.domainInfo(word)
|
||||
.orElseGet(() -> unknownSite(word));
|
||||
|
||||
logger.debug("Results = {}", results);
|
||||
|
||||
return results;
|
||||
|
||||
}
|
||||
|
||||
private DomainInformation unknownSite(String url) {
|
||||
return DomainInformation.builder()
|
||||
.domain(new EdgeDomain(url))
|
||||
.suggestForCrawling(true)
|
||||
.unknownDomain(true)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import lombok.SneakyThrows;
|
||||
import nu.marginalia.client.Context;
|
||||
import nu.marginalia.search.command.SearchCommandInterface;
|
||||
import nu.marginalia.search.command.SearchParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import spark.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SiteRedirectCommand implements SearchCommandInterface {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private final Predicate<String> queryPatternPredicate = Pattern.compile("^site:[.A-Za-z\\-0-9]+$").asPredicate();
|
||||
|
||||
@Inject
|
||||
public SiteRedirectCommand() {
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public boolean process(Context ctx, Response response, SearchParameters parameters) {
|
||||
if (!queryPatternPredicate.test(parameters.query())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String definePrefix = "site:";
|
||||
String domain = parameters.query().substring(definePrefix.length()).toLowerCase();
|
||||
|
||||
// Use an HTML redirect here, so we can use relative URLs
|
||||
|
||||
response.raw().getOutputStream().println("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; url=/site/%s">
|
||||
""".formatted(domain));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -76,7 +76,7 @@ public class DomainInformationService {
|
||||
.linkingDomains(linkingDomains)
|
||||
.inCrawlQueue(inCrawlQueue)
|
||||
.nodeAffinity(nodeAffinity)
|
||||
.suggestForCrawling((pagesVisited == 0 && !inCrawlQueue))
|
||||
.suggestForCrawling((pagesVisited == 0 && outboundLinks == 0 && !inCrawlQueue))
|
||||
.build();
|
||||
|
||||
return Optional.of(di);
|
||||
|
@ -2,13 +2,7 @@ package nu.marginalia.search.svc;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import nu.marginalia.renderer.MustacheRenderer;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
import spark.Spark;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
@ -21,7 +15,6 @@ import java.util.stream.Collectors;
|
||||
* DomainComplaintService in control-service
|
||||
*/
|
||||
public class SearchFlagSiteService {
|
||||
private final MustacheRenderer<FlagSiteViewModel> formTemplate;
|
||||
private final HikariDataSource dataSource;
|
||||
|
||||
private final CategoryItem unknownCategory = new CategoryItem("unknown", "Unknown");
|
||||
@ -39,62 +32,21 @@ public class SearchFlagSiteService {
|
||||
private final Map<String, CategoryItem> categoryItemMap =
|
||||
categories.stream().collect(Collectors.toMap(CategoryItem::categoryName, Function.identity()));
|
||||
@Inject
|
||||
public SearchFlagSiteService(RendererFactory rendererFactory,
|
||||
HikariDataSource dataSource) throws IOException {
|
||||
formTemplate = rendererFactory.renderer("search/indict/indict-form");
|
||||
public SearchFlagSiteService(HikariDataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public Object flagSiteForm(Request request, Response response) throws SQLException {
|
||||
final int domainId = Integer.parseInt(request.params("domainId"));
|
||||
|
||||
var model = getModel(domainId, false);
|
||||
return formTemplate.render(model);
|
||||
public List<CategoryItem> getCategories() {
|
||||
return categories;
|
||||
}
|
||||
|
||||
public Object flagSiteAction(Request request, Response response) throws SQLException {
|
||||
|
||||
int domainId = Integer.parseInt(request.params("domainId"));
|
||||
|
||||
var formData = new FlagSiteFormData(
|
||||
domainId,
|
||||
request.queryParams("category"),
|
||||
request.queryParams("description"),
|
||||
request.queryParams("samplequery")
|
||||
);
|
||||
|
||||
insertComplaint(formData);
|
||||
|
||||
return formTemplate.render(getModel(domainId, true));
|
||||
}
|
||||
|
||||
private void insertComplaint(FlagSiteFormData formData) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement(
|
||||
"""
|
||||
INSERT INTO DOMAIN_COMPLAINT(DOMAIN_ID, CATEGORY, DESCRIPTION, SAMPLE) VALUES (?, ?, ?, ?)
|
||||
""")) {
|
||||
stmt.setInt(1, formData.domainId);
|
||||
stmt.setString(2, formData.category);
|
||||
stmt.setString(3, formData.description);
|
||||
stmt.setString(4, formData.sampleQuery);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private FlagSiteViewModel getModel(int id, boolean isSubmitted) throws SQLException {
|
||||
|
||||
|
||||
public List<FlagSiteComplaintModel> getExistingComplaints(int id) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var complaintsStmt = conn.prepareStatement("""
|
||||
SELECT CATEGORY, FILE_DATE, REVIEWED, DECISION
|
||||
FROM DOMAIN_COMPLAINT
|
||||
WHERE DOMAIN_ID=?
|
||||
""");
|
||||
var stmt = conn.prepareStatement(
|
||||
"""
|
||||
SELECT DOMAIN_NAME FROM EC_DOMAIN WHERE EC_DOMAIN.ID=?
|
||||
"""))
|
||||
"""))
|
||||
{
|
||||
List<FlagSiteComplaintModel> complaints = new ArrayList<>();
|
||||
|
||||
@ -109,21 +61,25 @@ public class SearchFlagSiteService {
|
||||
rs.getString(4)));
|
||||
}
|
||||
|
||||
stmt.setInt(1, id);
|
||||
rs = stmt.executeQuery();
|
||||
if (!rs.next()) {
|
||||
Spark.halt(404);
|
||||
}
|
||||
return new FlagSiteViewModel(id,
|
||||
rs.getString(1),
|
||||
categories,
|
||||
complaints,
|
||||
isSubmitted);
|
||||
return complaints;
|
||||
}
|
||||
}
|
||||
|
||||
public void insertComplaint(FlagSiteFormData formData) throws SQLException {
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement(
|
||||
"""
|
||||
INSERT INTO DOMAIN_COMPLAINT(DOMAIN_ID, CATEGORY, DESCRIPTION, SAMPLE) VALUES (?, ?, ?, ?)
|
||||
""")) {
|
||||
stmt.setInt(1, formData.domainId);
|
||||
stmt.setString(2, formData.category);
|
||||
stmt.setString(3, formData.description);
|
||||
stmt.setString(4, formData.sampleQuery);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public record CategoryItem(String categoryName, String categoryDesc) {}
|
||||
public record FlagSiteViewModel(int domainId, String domain, List<CategoryItem> category, List<FlagSiteComplaintModel> complaints, boolean isSubmitted) {}
|
||||
public record FlagSiteComplaintModel(String category, String submitTime, boolean isReviewed, String decision) {}
|
||||
public record FlagSiteFormData(int domainId, String category, String description, String sampleQuery) {};
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ public class SearchQueryService {
|
||||
final var ctx = Context.fromRequest(request);
|
||||
|
||||
try {
|
||||
return searchCommandEvaulator.eval(ctx, parseParameters(request));
|
||||
return searchCommandEvaulator.eval(ctx, response, parseParameters(request));
|
||||
}
|
||||
catch (RedirectException ex) {
|
||||
response.redirect(ex.newUrl);
|
||||
|
@ -0,0 +1,236 @@
|
||||
package nu.marginalia.search.svc;
|
||||
import com.google.inject.Inject;
|
||||
import nu.marginalia.client.Context;
|
||||
import nu.marginalia.db.DbDomainQueries;
|
||||
import nu.marginalia.model.EdgeDomain;
|
||||
import nu.marginalia.renderer.MustacheRenderer;
|
||||
import nu.marginalia.renderer.RendererFactory;
|
||||
import nu.marginalia.search.SearchOperator;
|
||||
import nu.marginalia.search.model.DomainInformation;
|
||||
import nu.marginalia.search.model.UrlDetails;
|
||||
import nu.marginalia.search.siteinfo.DomainInformationService;
|
||||
import nu.marginalia.search.svc.SearchFlagSiteService.FlagSiteFormData;
|
||||
import spark.*;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
public class SearchSiteInfoService {
|
||||
|
||||
private final SearchOperator searchOperator;
|
||||
private final DomainInformationService domainInformationService;
|
||||
private final SearchFlagSiteService flagSiteService;
|
||||
private final DbDomainQueries domainQueries;
|
||||
private final MustacheRenderer<Object> renderer;
|
||||
|
||||
@Inject
|
||||
public SearchSiteInfoService(SearchOperator searchOperator,
|
||||
DomainInformationService domainInformationService,
|
||||
RendererFactory rendererFactory,
|
||||
SearchFlagSiteService flagSiteService,
|
||||
DbDomainQueries domainQueries) throws IOException {
|
||||
this.searchOperator = searchOperator;
|
||||
this.domainInformationService = domainInformationService;
|
||||
this.flagSiteService = flagSiteService;
|
||||
this.domainQueries = domainQueries;
|
||||
|
||||
this.renderer = rendererFactory.renderer("search/site-info/site-info");
|
||||
}
|
||||
|
||||
public Object handle(Request request, Response response) throws SQLException {
|
||||
String domainName = request.params("site");
|
||||
String view = request.queryParamOrDefault("view", "info");
|
||||
|
||||
if (null == domainName || domainName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var ctx = Context.fromRequest(request);
|
||||
|
||||
var model = switch (view) {
|
||||
case "links" -> listLinks(ctx, domainName);
|
||||
case "docs" -> listDocs(ctx, domainName);
|
||||
case "info" -> siteInfo(ctx, domainName);
|
||||
case "report" -> reportSite(ctx, domainName);
|
||||
default -> siteInfo(ctx, domainName);
|
||||
};
|
||||
|
||||
return renderer.renderInto(response, model);
|
||||
}
|
||||
|
||||
public Object handlePost(Request request, Response response) throws SQLException {
|
||||
String domainName = request.params("site");
|
||||
String view = request.queryParamOrDefault("view", "info");
|
||||
|
||||
if (null == domainName || domainName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!view.equals("report"))
|
||||
return null;
|
||||
|
||||
final int domainId = domainQueries.getDomainId(new EdgeDomain(domainName));
|
||||
|
||||
FlagSiteFormData formData = new FlagSiteFormData(
|
||||
domainId,
|
||||
request.queryParams("category"),
|
||||
request.queryParams("description"),
|
||||
request.queryParams("sampleQuery")
|
||||
);
|
||||
flagSiteService.insertComplaint(formData);
|
||||
|
||||
var complaints = flagSiteService.getExistingComplaints(domainId);
|
||||
|
||||
var model = new ReportDomain(domainName, domainId, complaints, List.of(), true);
|
||||
|
||||
return renderer.renderInto(response, model);
|
||||
}
|
||||
|
||||
private Object reportSite(Context ctx, String domainName) throws SQLException {
|
||||
int domainId = domainQueries.getDomainId(new EdgeDomain(domainName));
|
||||
var existingComplaints = flagSiteService.getExistingComplaints(domainId);
|
||||
|
||||
return new ReportDomain(domainName,
|
||||
domainId,
|
||||
existingComplaints,
|
||||
flagSiteService.getCategories(),
|
||||
false);
|
||||
}
|
||||
|
||||
private SiteInfo siteInfo(Context ctx, String domainName) {
|
||||
OptionalInt id = domainQueries.tryGetDomainId(new EdgeDomain(domainName));
|
||||
|
||||
if (id.isEmpty()) {
|
||||
return new SiteInfo(domainName, -1, null, dummyInformation(domainName));
|
||||
}
|
||||
|
||||
String screenshotPath = "/screenshot/"+id.getAsInt();
|
||||
DomainInformation domainInfo = domainInformationService
|
||||
.domainInfo(domainName)
|
||||
.orElseGet(() -> dummyInformation(domainName));
|
||||
|
||||
return new SiteInfo(domainName, id.getAsInt(), screenshotPath, domainInfo);
|
||||
}
|
||||
|
||||
private DomainInformation dummyInformation(String domainName) {
|
||||
return DomainInformation.builder()
|
||||
.domain(new EdgeDomain(domainName))
|
||||
.suggestForCrawling(true)
|
||||
.unknownDomain(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Backlinks listLinks(Context ctx, String domainName) {
|
||||
return new Backlinks(domainName,
|
||||
domainQueries.tryGetDomainId(new EdgeDomain(domainName)).orElse(-1),
|
||||
searchOperator.doBacklinkSearch(ctx, domainName));
|
||||
}
|
||||
|
||||
private Docs listDocs(Context ctx, String domainName) {
|
||||
return new Docs(domainName,
|
||||
domainQueries.tryGetDomainId(new EdgeDomain(domainName)).orElse(-1),
|
||||
searchOperator.doSiteSearch(ctx, domainName));
|
||||
}
|
||||
|
||||
public record SiteInfo(Map<String, Boolean> view,
|
||||
Map<String, Boolean> domainState,
|
||||
long domainId,
|
||||
String domain,
|
||||
@Nullable String screenshotUrl,
|
||||
DomainInformation domainInformation)
|
||||
{
|
||||
public SiteInfo(String domain,
|
||||
long domainId,
|
||||
@Nullable String screenshotUrl,
|
||||
DomainInformation domainInformation)
|
||||
{
|
||||
this(Map.of("info", true),
|
||||
Map.of(domainInfoState(domainInformation), true),
|
||||
domainId,
|
||||
domain,
|
||||
screenshotUrl,
|
||||
domainInformation);
|
||||
}
|
||||
|
||||
private static String domainInfoState(DomainInformation info) {
|
||||
if (info.isBlacklisted()) {
|
||||
return "blacklisted";
|
||||
}
|
||||
if (!info.isUnknownDomain() && info.isSuggestForCrawling()) {
|
||||
return "suggestForCrawling";
|
||||
}
|
||||
if (info.isInCrawlQueue()) {
|
||||
return "inCrawlQueue";
|
||||
}
|
||||
if (info.isUnknownDomain()) {
|
||||
return "unknownDomain";
|
||||
}
|
||||
else {
|
||||
return "indexed";
|
||||
}
|
||||
}
|
||||
|
||||
public String query() { return "site:" + domain; }
|
||||
|
||||
public boolean isKnown() {
|
||||
return domainId > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public record Docs(Map<String, Boolean> view,
|
||||
String domain,
|
||||
long domainId,
|
||||
List<UrlDetails> results) {
|
||||
public Docs(String domain, long domainId, List<UrlDetails> results) {
|
||||
this(Map.of("docs", true), domain, domainId, results);
|
||||
}
|
||||
|
||||
public String focusDomain() { return domain; }
|
||||
|
||||
public String query() { return "site:" + domain; }
|
||||
|
||||
public boolean isKnown() {
|
||||
return domainId > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public record Backlinks(Map<String, Boolean> view, String domain, long domainId, List<UrlDetails> results) {
|
||||
public Backlinks(String domain, long domainId, List<UrlDetails> results) {
|
||||
this(Map.of("links", true), domain, domainId, results);
|
||||
}
|
||||
|
||||
public String query() { return "links:" + domain; }
|
||||
|
||||
public boolean isKnown() {
|
||||
return domainId > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public record ReportDomain(
|
||||
Map<String, Boolean> view,
|
||||
String domain,
|
||||
int domainId,
|
||||
List<SearchFlagSiteService.FlagSiteComplaintModel> complaints,
|
||||
List<SearchFlagSiteService.CategoryItem> category,
|
||||
boolean submitted)
|
||||
{
|
||||
public ReportDomain(String domain,
|
||||
int domainId,
|
||||
List<SearchFlagSiteService.FlagSiteComplaintModel> complaints,
|
||||
List<SearchFlagSiteService.CategoryItem> category,
|
||||
boolean submitted) {
|
||||
this(Map.of("report", true), domain, domainId, complaints, category, submitted);
|
||||
}
|
||||
|
||||
public String query() { return "site:" + domain; }
|
||||
|
||||
public boolean isKnown() {
|
||||
return domainId > 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -25,10 +25,68 @@ body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#siteinfo-nav {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@extend .dialog;
|
||||
padding: 0.25ch !important;
|
||||
margin-top: 1.5ch;
|
||||
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1ch;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
padding: 1ch;
|
||||
background-color: $highlight-light2;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.link-unavailable {
|
||||
display: inline-block;
|
||||
text-decoration: line-through;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
li.current {
|
||||
background-color: $highlight-light;
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
border: 1px solid $border-color;
|
||||
box-shadow: 0 0 1ch $border-color;
|
||||
background-color: #fff;
|
||||
padding: 1ch;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
padding: 0.5ch;
|
||||
font-size: 12pt;
|
||||
background-color: $highlight-light;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: $nicotine-dark;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid $border-color;
|
||||
border: 1px solid #888;
|
||||
box-shadow: 0 0 0.5ch #888;
|
||||
margin-bottom: 1ch;
|
||||
|
||||
nav {
|
||||
@ -46,6 +104,7 @@ header {
|
||||
rgba(100,255,100,1) 50%,
|
||||
rgba(100,100,255,1) 100%);
|
||||
color: black;
|
||||
text-shadow: 0 0 0.25ch #ccc;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
@ -55,6 +114,119 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
#complaint {
|
||||
@extend .dialog;
|
||||
max-width: 60ch;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 2ch;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 10ch;
|
||||
}
|
||||
}
|
||||
|
||||
#siteinfo {
|
||||
margin-top: 1ch;
|
||||
display: flex;
|
||||
gap: 1ch;
|
||||
flex-grow: 0.5;
|
||||
flex-shrink: 0.5;
|
||||
flex-basis: 10ch 10ch;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: stretch;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
|
||||
#index-info, #link-info {
|
||||
width: 32ch;
|
||||
@extend .dialog;
|
||||
}
|
||||
#screenshot {
|
||||
@extend .dialog;
|
||||
}
|
||||
#screenshot img {
|
||||
width: 30ch;
|
||||
height: 22.5ch;
|
||||
}
|
||||
}
|
||||
|
||||
.infobox {
|
||||
background-color: #fff;
|
||||
padding: 1ch;
|
||||
margin: 1ch;
|
||||
border: 1px solid $border-color;
|
||||
box-shadow: 0 0 1ch $border-color;
|
||||
}
|
||||
|
||||
section.cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 1ch;
|
||||
gap: 2ch;
|
||||
justify-content: flex-start;
|
||||
|
||||
.card {
|
||||
border: 2px #ccc;
|
||||
background-color: #fff;
|
||||
border-left: 1px solid #ecb;
|
||||
border-top: 1px solid #ecb;
|
||||
box-shadow: #0008 0 0 5px;
|
||||
|
||||
h2 {
|
||||
color: #fff;
|
||||
background-color: $highlight-light;
|
||||
border-bottom: 1px solid $border-color;
|
||||
font-weight: normal;
|
||||
font-size: 12pt;
|
||||
padding: .5ch .5ch .5ch .5ch;
|
||||
margin: 0 0 0 0;
|
||||
word-break: break-word;
|
||||
font-family: $heading-fonts;
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h2 a {
|
||||
display: block !important;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:focus img {
|
||||
filter: sepia(100%);
|
||||
box-shadow: #444 0px 0px 20px;
|
||||
}
|
||||
a:focus:not(.nofocus) {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding-left: 1ch;
|
||||
padding-right: 1ch;
|
||||
overflow: auto;
|
||||
-webkit-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 28ch;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding-left: 1ch;
|
||||
padding-right: 1ch;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.positions {
|
||||
box-shadow: 0 0 2px #888;
|
||||
background-color: #e4e4e4;
|
||||
@ -258,30 +430,29 @@ footer {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.utils {
|
||||
display: flex;
|
||||
font-size: 10pt;
|
||||
padding: 1ch;
|
||||
background-color: #eee;
|
||||
|
||||
> * {
|
||||
margin-right: 1ch;
|
||||
margin-left: 1ch;
|
||||
}
|
||||
.meta {
|
||||
flex-grow: 2;
|
||||
text-align: right;
|
||||
}
|
||||
.meta > * {
|
||||
padding-left: 4px;
|
||||
}
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.utils {
|
||||
display: flex;
|
||||
font-size: 10pt;
|
||||
padding: 1ch;
|
||||
background-color: #eee;
|
||||
|
||||
> * {
|
||||
margin-right: 1ch;
|
||||
margin-left: 1ch;
|
||||
}
|
||||
.meta {
|
||||
flex-grow: 2;
|
||||
text-align: right;
|
||||
}
|
||||
.meta > * {
|
||||
padding-left: 4px;
|
||||
}
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
@media (max-device-width: 624px) {
|
||||
body[data-has-js="true"] {
|
||||
margin: 0 !important;
|
||||
|
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>Marginalia Search - {{query}}</title>
|
||||
|
||||
<link rel="stylesheet" href="/style-new.css" />
|
||||
<link rel="stylesheet" href="/serp.css" />
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex" />
|
||||
|
@ -1,80 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Marginalia Search - File complaint against {{domain}}</title>
|
||||
|
||||
<link rel="stylesheet" href="/style-new.css" />
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{{>search/parts/search-header}}
|
||||
|
||||
<article>
|
||||
{{>search/parts/search-form}}
|
||||
|
||||
<section class="form">
|
||||
|
||||
{{#if isSubmitted}}
|
||||
<h1>Your complaint against {{domain}} has been submitted</h1>
|
||||
<p>The review process is manual and may take a while.</p>
|
||||
{{/if}}
|
||||
|
||||
{{#unless isSubmitted}}
|
||||
<h1>Flag {{domain}} for review</h1>
|
||||
Note, this is not intended to police acceptable thoughts or ideas.
|
||||
<p>
|
||||
That said, offensive content in obvious bad faith is not tolerated, especially when designed
|
||||
to crop up when you didn't go looking for it. How and where it is said is more
|
||||
important than what is said.
|
||||
<p>
|
||||
This form can also be used to appeal unfairly blacklisted sites.
|
||||
<p>
|
||||
|
||||
<form method="POST" action="/site/flag-site/{{domainId}}">
|
||||
<fieldset>
|
||||
<legend>Flag for Review</legend>
|
||||
|
||||
<label for="category">Category</label><br>
|
||||
<select name="category" id="category">
|
||||
{{#each category}} <option value="{{categoryName}}">{{categoryDesc}}</option> {{/each}}
|
||||
</select>
|
||||
<br>
|
||||
<br>
|
||||
<label for="description">Description</label><br>
|
||||
<textarea type="text" name="description" id="description" rows=4></textarea><br>
|
||||
<br>
|
||||
<label for="samplequery">(Optional) Search Query </label><br>
|
||||
<input type="text" name="samplequery" id="samplequery" length=255 /><br>
|
||||
<br>
|
||||
<br/>
|
||||
<input type="submit" value="File complaint" />
|
||||
</fieldset>
|
||||
</form>
|
||||
<p>
|
||||
Communicating through forms and tables is a bit impersonal,
|
||||
you may also reach a human being through email at <tt>kontakt@marginalia.nu</tt>.
|
||||
{{/unless}}
|
||||
|
||||
{{#if complaints}}
|
||||
<hr>
|
||||
<h2> Complaints against {{domain}} </h2>
|
||||
<table border width=100%>
|
||||
<tr><th>Category</th><th>Submitted</th><th>Reviewed</th></tr>
|
||||
{{#each complaints}}
|
||||
<tr>
|
||||
<td>{{category}}</td>
|
||||
<td>{{submitTime}}</td>
|
||||
<td>{{#if reviewed}}✓{{/if}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
{{/if}}
|
||||
</section>
|
||||
|
||||
{{>search/parts/search-footer}}
|
||||
</body>
|
@ -1,4 +1,4 @@
|
||||
<form action="search" method="get">
|
||||
<form action="/search" method="get">
|
||||
<div id="search-box">
|
||||
<h1>
|
||||
Search The Internet
|
||||
|
@ -1,66 +0,0 @@
|
||||
<h2>Indexing Information</h2>
|
||||
<div class="description">
|
||||
<br>
|
||||
{{#if blacklisted}}
|
||||
This website is <em>blacklisted</em>. This excludes it from crawling and indexing.
|
||||
|
||||
<p>This is usually because of some form of misbehavior on the webmaster's end.
|
||||
Either annoying search engine spam, or tasteless content bad faith content.
|
||||
|
||||
<p>Occasionally this is done hastily and in error. If you would like the decision
|
||||
reviewed, you may use <a href="/site/flag-site/{{domainId}}">this form</a> to file a report.</tt>
|
||||
{{/if}}
|
||||
|
||||
{{#unless blacklisted}}
|
||||
<fieldset>
|
||||
<legend>Index</legend>
|
||||
State: {{state}}<br/>
|
||||
Node Affinity: {{nodeAffinity}} </br>
|
||||
Pages Known: {{pagesKnown}} <br/>
|
||||
Pages Crawled: {{pagesFetched}} <br/>
|
||||
Pages Indexed: {{pagesIndexed}} <br/>
|
||||
</fieldset>
|
||||
<br/>
|
||||
{{#if inCrawlQueue}}
|
||||
This website is in the queue for crawling.
|
||||
It may take up to a month before it is indexed.
|
||||
{{/if}}
|
||||
|
||||
{{#if suggestForCrawling}}
|
||||
{{#if unknownDomain}}
|
||||
|
||||
<fieldset>
|
||||
<legend>Crawling</legend>
|
||||
This website is not known to the search engine.
|
||||
|
||||
To submit the website for crawling, follow <a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://github.com/MarginaliaSearch/submit-site-to-marginalia-search">these instructions</a>.
|
||||
</fieldset>
|
||||
{{/if}}
|
||||
{{#unless unknownDomain}}
|
||||
<form method="POST" action="/site/suggest/">
|
||||
<fieldset>
|
||||
<legend>Crawling</legend>
|
||||
This website is not queued for crawling. If you would like it to be crawled,
|
||||
use the checkbox and button below.<p/>
|
||||
<input type="hidden" name="id" value="{{domainId}}" />
|
||||
<input type="checkbox" id="nomisclick" name="nomisclick" /> <label for="nomisclick"> This is not a mis-click </label>
|
||||
<br/>
|
||||
<br/>
|
||||
<input type="submit" value="Add {{domain}} to queue" />
|
||||
</fieldset>
|
||||
</form>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{#if pagesFetched}}
|
||||
<p>
|
||||
If you've found a reason why this website should not be indexed,
|
||||
you may use <a href="/site/flag-site/{{domainId}}">this form</a> to file a report.<p>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
@ -1,18 +0,0 @@
|
||||
<div class="card info">
|
||||
<h2>Links</h2>
|
||||
<div class="description">
|
||||
<br>
|
||||
<fieldset>
|
||||
<legend>Link Graph</legend>
|
||||
Ranking: {{ranking}}%<br/>
|
||||
Incoming Links: {{incomingLinks}} <br/>
|
||||
Outbound Links: {{outboundLinks}} <br/>
|
||||
</fieldset>
|
||||
<br>
|
||||
<fieldset>
|
||||
<legend>Explore</legend>
|
||||
<a href="/links/{{domain.domain}}">Which pages link here?</a><br/>
|
||||
<a href="/explore/{{domain}}">Explore similar domains</a><br/>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
@ -20,6 +20,12 @@
|
||||
|
||||
<section class="sidebar-narrow">
|
||||
<section id="results" class="sb-left">
|
||||
{{#if focusDomain}}
|
||||
<div class="infobox">
|
||||
Showing search results from <a href="/site/{{focusDomain}}">{{focusDomain}}</a>.
|
||||
</div>
|
||||
|
||||
{{/if}}
|
||||
{{#each results}}{{>search/parts/search-result}}{{/each}}
|
||||
</section>
|
||||
|
||||
|
@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Marginalia Search - {{query}}</title>
|
||||
|
||||
<link rel="stylesheet" href="/style-new.css" />
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{>search/parts/search-header}}
|
||||
|
||||
<article>
|
||||
{{>search/parts/search-form}}
|
||||
|
||||
<section class="cards">
|
||||
<div class="card">
|
||||
<h2>{{domain}}</h2>
|
||||
<a href="http://{{domain}}/"><img src="{{screenshot}}" alt="Thumbnail image of {{domain}}"/></a>
|
||||
</div>
|
||||
|
||||
<div class="card info">
|
||||
|
||||
{{>search/parts/site-info-index}}
|
||||
{{>search/parts/site-info-links}}
|
||||
|
||||
{{#each results}}{{>search/parts/search-result}}{{/each}}
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{{>search/parts/search-footer}}
|
||||
</body>
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
<p>This website is <em>blacklisted</em>. This excludes it from crawling and indexing.</p>
|
||||
|
||||
<p>This is usually because of some form of misbehavior on the webmaster's end.
|
||||
Either annoying search engine spam, or tasteless content bad faith content.</p>
|
||||
|
||||
<p>Occasionally this is done hastily and in error. If you would like the decision
|
||||
reviewed, you may use the <a href="?v=report">report form</a> to file an appeal.</tt>
|
||||
</p>
|
@ -0,0 +1,16 @@
|
||||
<fieldset>
|
||||
<legend>Index</legend>
|
||||
State: {{state}}<br/>
|
||||
Domain ID: {{domainId}} <br/>
|
||||
Node Affinity: {{nodeAffinity}} <br/>
|
||||
Pages Known: {{pagesKnown}} <br/>
|
||||
Pages Crawled: {{pagesFetched}} <br/>
|
||||
Pages Indexed: {{pagesIndexed}} <br/>
|
||||
</fieldset>
|
||||
<br/>
|
||||
|
||||
{{#if pagesFetched}}
|
||||
<p>
|
||||
If you've found a reason why this website should not be indexed,
|
||||
you may use <a href="/site/flag-site/{{domainId}}">this form</a> to file a report.<p>
|
||||
{{/if}}
|
@ -0,0 +1,12 @@
|
||||
<form method="POST" action="/site/suggest/">
|
||||
<fieldset>
|
||||
<legend>Crawling</legend>
|
||||
This website is not queued for crawling. If you would like it to be crawled,
|
||||
use the checkbox and button below.<p/>
|
||||
<input type="hidden" name="id" value="{{domainId}}" />
|
||||
<input type="checkbox" id="nomisclick" name="nomisclick" /> <label for="nomisclick"> This is not a mis-click </label>
|
||||
<br/>
|
||||
<br/>
|
||||
<input type="submit" value="Add {{domain}} to queue" />
|
||||
</fieldset>
|
||||
</form>
|
@ -0,0 +1,9 @@
|
||||
<fieldset>
|
||||
<legend>Crawling</legend>
|
||||
This website is not known to the search engine.
|
||||
|
||||
To submit the website for crawling, follow <a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://github.com/MarginaliaSearch/submit-site-to-marginalia-search">these instructions</a>.
|
||||
</fieldset>
|
@ -0,0 +1,25 @@
|
||||
<section id="index-info">
|
||||
<h2>Indexing Information</h2>
|
||||
{{#if domainState.blacklisted}}
|
||||
{{>search/site-info/site-info-index-blacklisted}}
|
||||
{{/if}}
|
||||
|
||||
{{#if domainState.unknownDomain}}
|
||||
{{>search/site-info/site-info-index-unknown}}
|
||||
{{/if}}
|
||||
|
||||
{{#if domainState.inCrawlQueue}}
|
||||
<p>
|
||||
This website is in the queue for crawling.
|
||||
It may take up to a month before it is indexed.
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if domainState.suggestForCrawling}}
|
||||
{{>search/site-info/site-info-index-suggest}}
|
||||
{{/if}}
|
||||
|
||||
{{#if domainState.indexed}}
|
||||
{{>search/site-info/site-info-index-indexed}}
|
||||
{{/if}}
|
||||
</section>
|
@ -0,0 +1,9 @@
|
||||
<section id="link-info">
|
||||
<h2>Links</h2>
|
||||
<fieldset>
|
||||
<legend>Link Graph</legend>
|
||||
Ranking: {{ranking}}%<br/>
|
||||
Incoming Links: {{incomingLinks}} <br/>
|
||||
Outbound Links: {{outboundLinks}} <br/>
|
||||
</fieldset>
|
||||
</section>
|
@ -0,0 +1,60 @@
|
||||
<section id="complaint">
|
||||
{{#if submitted}}
|
||||
<h2>Your complaint against {{domain}} has been submitted</h2>
|
||||
<p>The review process is manual and may take a while. If urgent action is necessary,
|
||||
reach me at kontakt@marginalia.nu!
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#unless submitted}}
|
||||
<h2>Flag {{domain}} for review</h2>
|
||||
<p>
|
||||
Note, this is not intended to police acceptable thoughts or ideas.
|
||||
<p>
|
||||
That said, offensive content in obvious bad faith is not tolerated, especially when designed
|
||||
to crop up when you didn't go looking for it. How and where it is said is more
|
||||
important than what is said.
|
||||
<p>
|
||||
This form can also be used to appeal unfairly blacklisted sites.
|
||||
<p>
|
||||
|
||||
<form method="POST">
|
||||
<fieldset>
|
||||
<legend>Flag for Review</legend>
|
||||
|
||||
<label for="category">Category</label><br>
|
||||
<select name="category" id="category">
|
||||
{{#each category}} <option value="{{categoryName}}">{{categoryDesc}}</option> {{/each}}
|
||||
</select>
|
||||
<br>
|
||||
<br>
|
||||
<label for="description">Description</label><br>
|
||||
<textarea type="text" name="description" id="description" rows=4></textarea><br>
|
||||
<br>
|
||||
<label for="samplequery">(Optional) Search Query </label><br>
|
||||
<input type="text" name="samplequery" id="samplequery" length=255 /><br>
|
||||
<br>
|
||||
<br/>
|
||||
<input type="submit" value="File complaint" />
|
||||
</fieldset>
|
||||
</form>
|
||||
<p>
|
||||
Communicating through forms and tables is a bit impersonal,
|
||||
you may also reach a human being through email at <tt>kontakt@marginalia.nu</tt>.
|
||||
{{/unless}}
|
||||
|
||||
{{#if complaints}}
|
||||
<hr>
|
||||
<h2> Complaints against {{domain}} </h2>
|
||||
<table border width=100%>
|
||||
<tr><th>Category</th><th>Submitted</th><th>Reviewed</th></tr>
|
||||
{{#each complaints}}
|
||||
<tr>
|
||||
<td>{{category}}</td>
|
||||
<td>{{submitTime}}</td>
|
||||
<td>{{#if reviewed}}✓{{/if}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
{{/if}}
|
||||
</section>
|
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Marginalia Search - {{domain}}</title>
|
||||
|
||||
<link rel="stylesheet" href="/serp.css" />
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Marginalia">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{>search/parts/search-header}}
|
||||
|
||||
{{>search/parts/search-form}}
|
||||
|
||||
{{#with view}}
|
||||
<nav id="siteinfo-nav">
|
||||
<h2>{{domain}}</h2>
|
||||
<ul>
|
||||
<li {{#if info}}class="current"{{/if}}><a href="?view=info">Info</a></li>
|
||||
<li {{#if report}}class="current"{{/if}}>{{#if known}}<a href="?view=report">Report</a>{{/if}}{{#unless known}}<a class="link-unavailable" title="This domain is not known by the search engine">Report</a>{{/unless}}</li>
|
||||
<li {{#if docs}}class="current"{{/if}}>{{#if known}}<a href="?view=docs">Docs</a>{{/if}}{{#unless known}}<a class="link-unavailable" title="This domain is not known by the search engine">Docs</a>{{/unless}}</li>
|
||||
<li {{#if links}}class="current"{{/if}}><a href="?view=links">Links</a></li>
|
||||
<li {{#if browse}}class="current"{{/if}}><a href="?view=similar">Similar</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{/with}}
|
||||
|
||||
{{#if view.info}}{{#with domainInformation}}
|
||||
<section id="siteinfo">
|
||||
{{> search/site-info/site-info-screenshot}}
|
||||
{{> search/site-info/site-info-index}}
|
||||
{{> search/site-info/site-info-links}}
|
||||
</section>
|
||||
{{/with}}{{/if}}
|
||||
|
||||
{{#if view.links}}
|
||||
<div class="infobox">
|
||||
Showing search results with links to {{domain}}.
|
||||
</div>
|
||||
{{#each results}}{{>search/parts/search-result}}{{/each}}
|
||||
{{/if}}
|
||||
{{#if view.docs}}
|
||||
<div class="infobox">
|
||||
Showing documents found in {{domain}}.
|
||||
</div>
|
||||
|
||||
{{#each results}}{{>search/parts/search-result}}{{/each}}
|
||||
{{/if}}
|
||||
{{#if view.report}}
|
||||
{{>search/site-info/site-info-report}}
|
||||
{{/if}}
|
||||
{{>search/parts/search-footer}}
|
||||
</body>
|
||||
|
||||
|
@ -1,38 +0,0 @@
|
||||
package nu.marginalia.search.command.commands;
|
||||
|
||||
import nu.marginalia.search.exceptions.RedirectException;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class BangCommandTest {
|
||||
|
||||
@Test
|
||||
public void testBang() {
|
||||
var bc = new BangCommand();
|
||||
|
||||
expectRedirectUrl("https://www.google.com/search?q=search+terms", () -> bc.process(null, null, "search terms !g"));
|
||||
expectNoRedirect(() -> bc.process(null, null, "search terms!g"));
|
||||
expectNoRedirect(() -> bc.process(null, null, "!gsearch terms"));
|
||||
expectRedirectUrl("https://www.google.com/search?q=search+terms", () -> bc.process(null, null, "!g search terms"));
|
||||
}
|
||||
|
||||
void expectNoRedirect(Runnable op) {
|
||||
try {
|
||||
op.run();
|
||||
}
|
||||
catch (RedirectException ex) {
|
||||
fail("Expected no redirection, but got " + ex.newUrl);
|
||||
}
|
||||
}
|
||||
void expectRedirectUrl(String expectedUrl, Runnable op) {
|
||||
try {
|
||||
op.run();
|
||||
fail("Didn't intercept exception");
|
||||
}
|
||||
catch (RedirectException ex) {
|
||||
Assertions.assertEquals(expectedUrl, ex.newUrl, "Unexpected redirect");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user