001/*
002 * Shredzone Commons
003 *
004 * Copyright (C) 2012 Richard "Shred" Körber
005 *   http://commons.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU Library General Public License as
009 * published by the Free Software Foundation, either version 3 of the
010 * License, or (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU Library General Public License
018 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019 */
020package org.shredzone.commons.text.filter;
021
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import edu.umd.cs.findbugs.annotations.Nullable;
026import org.shredzone.commons.text.TextFilter;
027
028/**
029 * A filter that detects links in a text, and creates an HTML &lt;a&gt; tag around each
030 * link. http, https and ftp protocols are detected.
031 *
032 * @author Richard "Shred" Körber
033 */
034public class LinkToUrlFilter implements TextFilter {
035
036    // TODO: allow trailing period, comma etc.
037    private static final Pattern URL_PATTERN = Pattern.compile(
038            "(.*?)((?:https?|ftp)://\\S+?)(?=[.,;:!)]?\\s|$)",
039            Pattern.CASE_INSENSITIVE
040            );
041
042    private boolean noFollow = false;
043    private boolean noReferrer = false;
044    private boolean noOpener = true;
045    private @Nullable String target = null;
046    private @Nullable String tag = null;
047
048    /**
049     * Creates a new {@link LinkToUrlFilter}.
050     */
051    public LinkToUrlFilter() {
052        updateTag();
053    }
054
055    /**
056     * Sets the way search engines evaluate the created link. If set to {@code false}, a
057     * {@code rel="nofollow"} attribute is added to the link, so web crawlers will not
058     * follow to the target.
059     *
060     * @param follow
061     *         {@code true} if links should be followed by web crawlers. Defaults to
062     *         {@code true}.
063     * @deprecated It is confusing that this property must be set to {@code false} in
064     * order to have a "nofollow" relationship. Use {@link #setNoFollow(boolean)}
065     * instead.
066     */
067    @Deprecated
068    public void setFollow(boolean follow) {
069        setNoFollow(!follow);
070    }
071
072    /**
073     * Sets the way search engines evaluate the created link. If set to {@code true}, a
074     * {@code rel="nofollow"} attribute is added to the link, so web crawlers will not
075     * follow to the target.
076     *
077     * @param noFollow
078     *         {@code true} if links should not be followed by web crawlers. Defaults to
079     *         {@code false}.
080     * @since 2.6
081     */
082    public void setNoFollow(boolean noFollow) {
083        this.noFollow = noFollow;
084        updateTag();
085    }
086
087    /**
088     * Sets whether links with target "_blank" should have a "noopener" relationship.
089     * Activated by default. Note that deactivation poses a security risk for your
090     * website, and should only be done for a very good reason!
091     *
092     * @param noOpener
093     *         {@code true} to set "noopener" relationships on all links with a "_blank"
094     *         target. This is the default.
095     * @since 2.6
096     */
097    public void setNoOpener(boolean noOpener) {
098        this.noOpener = noOpener;
099        updateTag();
100    }
101
102    /**
103     * Sets wheter a "noreferrer" relationship shall be used. If {@code true} (and
104     * supported by the browser), the browser won't send a "Referer" header when following
105     * a link.
106     *
107     * @param noReferrer
108     *         If {@code true}, a "noreferrer" relation is added to each link. {@code
109     *         false} by default.
110     * @since 2.6
111     */
112    public void setNoReferrer(boolean noReferrer) {
113        this.noReferrer = noReferrer;
114        updateTag();
115    }
116
117    /**
118     * Sets the link's target attribute.
119     *
120     * @param target
121     *            Link target, or {@code null} if no target is to be set.
122     */
123    public void setTarget(@Nullable String target) {
124        this.target = target;
125        updateTag();
126    }
127
128    /**
129     * Updates the tag template from the current settings.
130     */
131    private void updateTag() {
132        StringBuilder sb = new StringBuilder("$1<a href=\"$2\"");
133        StringBuilder relSb = new StringBuilder();
134        if (noFollow) {
135            relSb.append("nofollow ");
136        }
137        if (target != null) {
138            sb.append(" target=\"").append(target).append('"');
139            if (noOpener && "_blank".equals(target)) {
140                relSb.append("noopener ");
141            }
142        }
143        if (noReferrer) {
144            relSb.append("noreferrer ");
145        }
146        if (relSb.length() > 0) {
147            sb.append(" rel=\"").append(relSb.toString().trim()).append('"');
148        }
149        sb.append(">$2</a>");
150        tag = sb.toString();
151    }
152
153    @Override
154    public CharSequence apply(CharSequence text) {
155        Matcher m = URL_PATTERN.matcher(text);
156        if (!m.matches()) {
157            return text;
158        }
159        return m.replaceAll(tag);
160    }
161
162}