/* * Copyright 2014-2017 Fukurou Mishiranu * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package chan.content; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.net.Uri; import chan.annotation.Extendable; import chan.annotation.Public; import chan.util.CommonUtils; import chan.util.StringUtils; import com.mishiranu.dashchan.C; import com.mishiranu.dashchan.preference.Preferences; @Extendable public class ChanLocator implements ChanManager.Linked { private final String chanName; private static final int HOST_TYPE_CONFIGURABLE = 0; private static final int HOST_TYPE_CONVERTABLE = 1; private static final int HOST_TYPE_SPECIAL = 2; private final LinkedHashMap<String, Integer> hosts = new LinkedHashMap<>(); private HttpsMode httpsMode = HttpsMode.NO_HTTPS; @Public public enum HttpsMode { @Public NO_HTTPS, @Public HTTPS_ONLY, @Public CONFIGURABLE } @Public public static final class NavigationData { @Public public static final int TARGET_THREADS = 0; @Public public static final int TARGET_POSTS = 1; @Public public static final int TARGET_SEARCH = 2; public final int target; public final String boardName; public final String threadNumber; public final String postNumber; public final String searchQuery; @Public public NavigationData(int target, String boardName, String threadNumber, String postNumber, String searchQuery) { this.target = target; this.boardName = boardName; this.threadNumber = threadNumber; this.postNumber = postNumber; this.searchQuery = searchQuery; if (target == TARGET_POSTS && StringUtils.isEmpty(threadNumber)) { throw new IllegalArgumentException("threadNumber must not be empty!"); } } } public static final ChanManager.Initializer INITIALIZER = new ChanManager.Initializer(); @Public public ChanLocator() { this(true); } ChanLocator(boolean useInitializer) { chanName = useInitializer ? INITIALIZER.consume().chanName : null; if (!useInitializer) { setHttpsMode(HttpsMode.CONFIGURABLE); } } @Override public final String getChanName() { return chanName; } @Override public final void init() { if (getChanHosts(true).size() == 0) { throw new RuntimeException("Chan hosts not defined"); } } public static <T extends ChanLocator> T get(String chanName) { return ChanManager.getInstance().getLocator(chanName, true); } @Public public static <T extends ChanLocator> T get(Object object) { ChanManager manager = ChanManager.getInstance(); return ChanManager.getInstance().getLocator(manager.getLinkedChanName(object), false); } public static ChanLocator getDefault() { return ChanManager.getInstance().getLocator(null, true); } @Public public final void addChanHost(String host) { hosts.put(host, HOST_TYPE_CONFIGURABLE); } @Public public final void addConvertableChanHost(String host) { hosts.put(host, HOST_TYPE_CONVERTABLE); } @Public public final void addSpecialChanHost(String host) { hosts.put(host, HOST_TYPE_SPECIAL); } @Public public final void setHttpsMode(HttpsMode httpsMode) { if (httpsMode == null) { throw new NullPointerException(); } this.httpsMode = httpsMode; } public final boolean isHttpsConfigurable() { return httpsMode == HttpsMode.CONFIGURABLE; } @Public public final boolean isUseHttps() { HttpsMode httpsMode = this.httpsMode; if (httpsMode == HttpsMode.CONFIGURABLE) { String chanName = getChanName(); return chanName != null ? Preferences.isUseHttps(chanName) : Preferences.isUseHttpsGeneral(); } return httpsMode == HttpsMode.HTTPS_ONLY; } public final ArrayList<String> getChanHosts(boolean confiruableOnly) { if (confiruableOnly) { ArrayList<String> hosts = new ArrayList<>(); for (LinkedHashMap.Entry<String, Integer> entry : this.hosts.entrySet()) { if (entry.getValue() == HOST_TYPE_CONFIGURABLE) { hosts.add(entry.getKey()); } } return hosts; } else { return new ArrayList<>(hosts.keySet()); } } public final boolean isChanHost(String host) { if (StringUtils.isEmpty(host)) { return false; } return hosts.containsKey(host) || host.equals(Preferences.getDomainUnhandled(getChanName())) || getHostTransition(getPreferredHost(), host) != null; } @Public public final boolean isChanHostOrRelative(Uri uri) { if (uri != null) { if (uri.isRelative()) { return true; } String host = uri.getHost(); return host != null && isChanHost(host); } return false; } public final boolean isConvertableChanHost(String host) { if (StringUtils.isEmpty(host)) { return false; } if (host.equals(Preferences.getDomainUnhandled(getChanName()))) { return true; } Integer hostType = hosts.get(host); return hostType != null && (hostType == HOST_TYPE_CONFIGURABLE || hostType == HOST_TYPE_CONVERTABLE); } public final Uri convert(Uri uri) { if (uri != null) { String preferredScheme = getPreferredScheme(); String host = uri.getHost(); String preferredHost = getPreferredHost(); boolean relative = uri.isRelative(); boolean webScheme = isWebScheme(uri); Uri.Builder builder = null; if (relative || webScheme && isConvertableChanHost(host)) { if (!StringUtils.equals(host, preferredHost)) { if (builder == null) { builder = uri.buildUpon().scheme(preferredScheme); } builder.authority(preferredHost); } } else if (webScheme) { String hostTransition = getHostTransition(preferredHost, host); if (hostTransition != null) { if (builder == null) { builder = uri.buildUpon().scheme(preferredScheme); } builder.authority(hostTransition); } } if (StringUtils.isEmpty(uri.getScheme()) || webScheme && !preferredScheme.equals(uri.getScheme()) && isChanHost(host)) { if (builder == null) { builder = uri.buildUpon().scheme(preferredScheme); } builder.scheme(preferredScheme); } if (builder != null) { return builder.build(); } } return uri; } public final Uri makeRelative(Uri uri) { if (isWebScheme(uri)) { String host = uri.getHost(); if (isConvertableChanHost(host)) { uri = uri.buildUpon().scheme(null).authority(null).build(); } } return uri; } @Extendable protected String getHostTransition(String chanHost, String requiredHost) { return null; } @Extendable protected boolean isBoardUri(Uri uri) { throw new UnsupportedOperationException(); } @Extendable protected boolean isThreadUri(Uri uri) { throw new UnsupportedOperationException(); } @Extendable protected boolean isAttachmentUri(Uri uri) { throw new UnsupportedOperationException(); } public final boolean isImageUri(Uri uri) { return uri != null && isImageExtension(uri.getPath()) && safe.isAttachmentUri(uri); } public final boolean isAudioUri(Uri uri) { return uri != null && isAudioExtension(uri.getPath()) && safe.isAttachmentUri(uri); } public final boolean isVideoUri(Uri uri) { return uri != null && isVideoExtension(uri.getPath()) && safe.isAttachmentUri(uri); } @Extendable protected String getBoardName(Uri uri) { throw new UnsupportedOperationException(); } @Extendable protected String getThreadNumber(Uri uri) { throw new UnsupportedOperationException(); } @Extendable protected String getPostNumber(Uri uri) { throw new UnsupportedOperationException(); } @Extendable protected Uri createBoardUri(String boardName, int pageNumber) { throw new UnsupportedOperationException(); } @Extendable protected Uri createThreadUri(String boardName, String threadNumber) { throw new UnsupportedOperationException(); } @Extendable protected Uri createPostUri(String boardName, String threadNumber, String postNumber) { throw new UnsupportedOperationException(); } @Extendable protected String createAttachmentForcedName(Uri fileUri) { return null; } public final String createAttachmentFileName(Uri fileUri) { return createAttachmentFileName(fileUri, safe.createAttachmentForcedName(fileUri)); } public final String createAttachmentFileName(Uri fileUri, String forcedName) { String fileName = forcedName != null ? forcedName : fileUri.getLastPathSegment(); if (fileName != null) { return StringUtils.escapeFile(fileName, false); } return null; } public final Uri validateClickedUriString(String uriString, String boardName, String threadNumber) { Uri uri = uriString != null ? Uri.parse(uriString) : null; if (uri != null && uri.isRelative()) { Uri baseUri = safe.createThreadUri(boardName, threadNumber); if (baseUri != null) { String query = StringUtils.nullIfEmpty(uri.getQuery()); String fragment = StringUtils.nullIfEmpty(uri.getFragment()); Uri.Builder builder = baseUri.buildUpon().encodedQuery(query).encodedFragment(fragment); String path = uri.getPath(); if (!StringUtils.isEmpty(path)) { builder.encodedPath(path); } return builder.build(); } } return uri; } @Extendable protected NavigationData handleUriClickSpecial(Uri uri) { return null; } public final boolean isWebScheme(Uri uri) { String scheme = uri.getScheme(); return "http".equals(scheme) || "https".equals(scheme); } @Public public final boolean isImageExtension(String path) { return C.IMAGE_EXTENSIONS.contains(getFileExtension(path)); } @Public public final boolean isAudioExtension(String path) { return C.AUDIO_EXTENSIONS.contains(getFileExtension(path)); } @Public public final boolean isVideoExtension(String path) { return C.VIDEO_EXTENSIONS.contains(getFileExtension(path)); } @Public public final String getFileExtension(String path) { return StringUtils.getFileExtension(path); } public final String getPreferredHost() { String host = Preferences.getDomainUnhandled(getChanName()); if (StringUtils.isEmpty(host)) { for (LinkedHashMap.Entry<String, Integer> entry : hosts.entrySet()) { if (entry.getValue() == HOST_TYPE_CONFIGURABLE) { host = entry.getKey(); break; } } } return host; } public final void setPreferredHost(String host) { if (host == null || getChanHosts(true).get(0).equals(host)) { host = ""; } Preferences.setDomainUnhandled(chanName, host); } private static String getPreferredScheme(boolean useHttps) { return useHttps ? "https" : "http"; } private String getPreferredScheme() { return getPreferredScheme(isUseHttps()); } @Public public final Uri buildPath(String... segments) { return buildPathWithHost(getPreferredHost(), segments); } @Public public final Uri buildPathWithHost(String host, String... segments) { return buildPathWithSchemeHost(isUseHttps(), host, segments); } @Public public final Uri buildPathWithSchemeHost(boolean useHttps, String host, String... segments) { Uri.Builder builder = new Uri.Builder().scheme(getPreferredScheme(useHttps)).authority(host); for (int i = 0; i < segments.length; i++) { String segment = segments[i]; if (segment != null) { builder.appendEncodedPath(segment.replaceFirst("^/+", "")); } } return builder.build(); } @Public public final Uri buildQuery(String path, String... alternation) { return buildQueryWithHost(getPreferredHost(), path, alternation); } @Public public final Uri buildQueryWithHost(String host, String path, String... alternation) { return buildQueryWithSchemeHost(isUseHttps(), host, path, alternation); } @Public public final Uri buildQueryWithSchemeHost(boolean useHttps, String host, String path, String... alternation) { Uri.Builder builder = new Uri.Builder().scheme(getPreferredScheme(useHttps)).authority(host); if (path != null) { builder.appendEncodedPath(path.replaceFirst("^/+", "")); } if (alternation.length % 2 != 0) { throw new IllegalArgumentException("Length of alternation must be a multiple of 2."); } for (int i = 0; i < alternation.length; i += 2) { builder.appendQueryParameter(alternation[i], alternation[i + 1]); } return builder.build(); } public final Uri setScheme(Uri uri) { if (uri != null && StringUtils.isEmpty(uri.getScheme())) { return uri.buildUpon().scheme(getPreferredScheme()).build(); } return uri; } private static final Pattern YOUTUBE_URI = Pattern.compile("(?:https?://)(?:www\\.)?(?:m\\.)?" + "youtu(?:\\.be/|be\\.com/(?:v/|embed/|(?:#/)?watch\\?(?:.*?|)v=))([\\w\\-]{11})"); private static final Pattern VIMEO_URI = Pattern.compile("(?:https?://)(?:player\\.)?vimeo.com/(?:video/)?" + "(?:channels/staffpicks/)?(\\d+)"); private static final Pattern VOCAROO_URI = Pattern.compile("(?:https?://)(?:www\\.)?vocaroo\\.com" + "/(?:player\\.swf\\?playMediaID=|i/|media_command\\.php\\?media=)([\\w\\-]{12})"); private static final Pattern SOUNDCLOUD_URI = Pattern.compile("(?:https?://)soundcloud\\.com/([\\w/_-]*)"); private boolean isMayContainEmbeddedCode(String text, String what) { return !StringUtils.isEmpty(text) && text.contains(what); } public final String getYouTubeEmbeddedCode(String text) { if (!isMayContainEmbeddedCode(text, "youtu")) { return null; } return getGroupValue(text, YOUTUBE_URI, 1); } public final String[] getYouTubeEmbeddedCodes(String text) { if (!isMayContainEmbeddedCode(text, "youtu")) { return null; } return getUniqueGroupValues(text, YOUTUBE_URI, 1); } public final String getVimeoEmbeddedCode(String text) { if (!isMayContainEmbeddedCode(text, "vimeo")) { return null; } return getGroupValue(text, VIMEO_URI, 1); } public final String[] getVimeoEmbeddedCodes(String text) { if (!isMayContainEmbeddedCode(text, "vimeo")) { return null; } return getUniqueGroupValues(text, VIMEO_URI, 1); } public final String getVocarooEmbeddedCode(String text) { if (!isMayContainEmbeddedCode(text, "vocaroo")) { return null; } return getGroupValue(text, VOCAROO_URI, 1); } public final String[] getVocarooEmbeddedCodes(String text) { if (!isMayContainEmbeddedCode(text, "vocaroo")) { return null; } return getUniqueGroupValues(text, VOCAROO_URI, 1); } public final String getSoundCloudEmbeddedCode(String text) { if (!isMayContainEmbeddedCode(text, "soundcloud")) { return null; } String value = getGroupValue(text, SOUNDCLOUD_URI, 1); if (value != null && value.contains("/")) { return value; } return null; } public final String[] getSoundCloudEmbeddedCodes(String text) { if (!isMayContainEmbeddedCode(text, "soundcloud")) { return null; } String[] embeddedCodes = getUniqueGroupValues(text, SOUNDCLOUD_URI, 1); if (embeddedCodes != null) { int deleteCount = 0; for (int i = 0; i < embeddedCodes.length; i++) { if (!embeddedCodes[i].contains("/")) { embeddedCodes[i] = null; deleteCount++; } } if (deleteCount > 0) { int newLength = embeddedCodes.length - deleteCount; if (newLength == 0) { return null; } String[] newEmbeddedCodes = new String[newLength]; for (int i = 0, j = 0; i < embeddedCodes.length; i++) { String embeddedCode = embeddedCodes[i]; if (embeddedCode != null) { newEmbeddedCodes[j++] = embeddedCode; } } return newEmbeddedCodes; } else { return embeddedCodes; } } return null; } @Public public final boolean isPathMatches(Uri uri, Pattern pattern) { if (uri != null) { String path = uri.getPath(); if (path != null) { return pattern.matcher(path).matches(); } } return false; } @Public public final String getGroupValue(String from, Pattern pattern, int groupIndex) { if (from == null) { return null; } Matcher matcher = pattern.matcher(from); if (matcher.find() && matcher.groupCount() > 0) { return matcher.group(groupIndex); } return null; } public final String[] getUniqueGroupValues(String from, Pattern pattern, int groupIndex) { if (from == null) { return null; } Matcher matcher = pattern.matcher(from); LinkedHashSet<String> data = new LinkedHashSet<>(); while (matcher.find() && matcher.groupCount() > 0) { data.add(matcher.group(groupIndex)); } return CommonUtils.toArray(data, String.class); } public static final class Safe { private final ChanLocator locator; private final boolean showToastOnError; private Safe(ChanLocator locator, boolean showToastOnError) { this.locator = locator; this.showToastOnError = showToastOnError; } public boolean isBoardUri(Uri uri) { try { return locator.isBoardUri(uri); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return false; } } public boolean isThreadUri(Uri uri) { try { return locator.isThreadUri(uri); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return false; } } public boolean isAttachmentUri(Uri uri) { try { return locator.isAttachmentUri(uri); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return false; } } public String getBoardName(Uri uri) { try { return locator.getBoardName(uri); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return null; } } public String getThreadNumber(Uri uri) { try { return locator.getThreadNumber(uri); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return null; } } public String getPostNumber(Uri uri) { try { return locator.getPostNumber(uri); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return null; } } public Uri createBoardUri(String boardName, int pageNumber) { try { return locator.createBoardUri(boardName, pageNumber); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return null; } } public Uri createThreadUri(String boardName, String threadNumber) { try { return locator.createThreadUri(boardName, threadNumber); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return null; } } public Uri createPostUri(String boardName, String threadNumber, String postNumber) { try { return locator.createPostUri(boardName, threadNumber, postNumber); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return null; } } public String createAttachmentForcedName(Uri fileUri) { try { return locator.createAttachmentForcedName(fileUri); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return null; } } public NavigationData handleUriClickSpecial(Uri uri) { try { return locator.handleUriClickSpecial(uri); } catch (LinkageError | RuntimeException e) { ExtensionException.logException(e, showToastOnError); return null; } } } private final Safe safeToast = new Safe(this, true); private final Safe safe = new Safe(this, false); public final Safe safe(boolean showToastOnError) { return showToastOnError ? safeToast : safe; } }