001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.utils;
019
020import com.fasterxml.jackson.core.JsonProcessingException;
021import io.stallion.Context;
022import io.stallion.dataAccess.Model;
023import io.stallion.services.Log;
024import io.stallion.users.Role;
025import io.stallion.utils.json.JSON;
026import io.stallion.utils.json.RestrictedViews;
027import org.apache.commons.lang3.StringEscapeUtils;
028import org.apache.commons.lang3.exception.ExceptionUtils;
029import org.owasp.html.HtmlPolicyBuilder;
030import org.owasp.html.PolicyFactory;
031
032
033import java.util.regex.Pattern;
034
035
036public class Sanitize {
037
038    public static final PolicyFactory
039            COMMENTS_BOX_POLICY = new HtmlPolicyBuilder()
040            .allowStandardUrlProtocols()
041            // Allow title="..." on any element.
042            .allowAttributes("title").globally()
043            // Allow href="..." on <a> elements.
044            .allowAttributes("href", "data-mentioned-contact").onElements("a")
045            // Defeat link spammers.
046            .requireRelNofollowOnLinks()
047            .allowAttributes("lang").matching(Pattern.compile("[a-zA-Z]{2,20}"))
048            .globally()
049            // The align attribute on <p> elements can have any value below.
050            .allowAttributes("align")
051            .matching(true, "center", "left", "right", "justify", "char")
052            .onElements("p")
053            // These elements are allowed.
054            .allowElements(
055                    "a", "p", "i", "b", "em", "blockquote", "code", "strong",
056                    "br", "ul", "ol", "li")
057            // Custom slashdot tags.
058            // These could be rewritten in the sanitizer using an ElementPolicy.
059            .allowElements("quote", "ecode")
060
061            .toFactory();
062
063
064    public static final  PolicyFactory
065            STANDARD_POLICY = new HtmlPolicyBuilder()
066            .allowTextIn("div")
067            .allowStandardUrlProtocols()
068                    // Allow title="..." on any element.
069            .allowAttributes("title").globally()
070                    // Allow href="..." on <a> elements.
071            .allowAttributes("href", "data-mentioned-contact").onElements("a")
072                    // Defeat link spammers.
073            .requireRelNofollowOnLinks()
074                    // Allow lang= with an alphabetic value on any element.
075            .allowAttributes("lang").matching(Pattern.compile("[a-zA-Z]{2,20}"))
076            .globally()
077                    // The align attribute on <p> elements can have any value below.
078            .allowAttributes("align")
079            .matching(true, "center", "left", "right", "justify", "char")
080            .onElements("p")
081                    // These elements are allowed.
082            .allowElements(
083                    "a", "p", "div", "i", "b", "em", "blockquote", "tt", "strong",
084                    "br", "ul", "ol", "li")
085                    // Custom slashdot tags.
086                    // These could be rewritten in the sanitizer using an ElementPolicy.
087            .allowElements("quote", "ecode")
088
089            .toFactory();
090
091    public static final  PolicyFactory
092            STANDARD_POLICY_WITH_IMAGES = new HtmlPolicyBuilder()
093
094            .allowStandardUrlProtocols()
095                    // Allow title="..." on any element.
096            .allowAttributes("title").globally()
097                    // Allow href="..." on <a> elements.
098            .allowAttributes("href", "data-mentioned-contact").onElements("a")
099                    // Defeat link spammers.
100            .requireRelNofollowOnLinks()
101                    // Allow lang= with an alphabetic value on any element.
102            .allowAttributes("lang").matching(Pattern.compile("[a-zA-Z]{2,20}"))
103            .globally()
104                    // The align attribute on <p> elements can have any value below.
105            .allowAttributes("align")
106            .matching(true, "center", "left", "right", "justify", "char")
107            .onElements("p")
108                    // These elements are allowed.
109            .allowElements(
110                    "a", "p", "div", "i", "b", "em", "blockquote", "tt", "strong",
111                    "br", "ul", "ol", "li", "img")
112                    // Custom slashdot tags.
113                    // These could be rewritten in the sanitizer using an ElementPolicy.
114            .allowAttributes("src", "alt").onElements("img")
115            .allowElements("quote", "ecode")
116            .toFactory();
117
118    public static final PolicyFactory BLOCK_ALL_POLICY = new HtmlPolicyBuilder()
119            .allowElements()
120            .toFactory();
121
122
123    public static Pattern stripTagsPattern = Pattern.compile("<[^>]*>");
124    /** Strips all HTML */
125    public static String stripAll(String s) {
126        if (s == null) {
127            return "";
128        }
129        return stripTagsPattern.matcher(s).replaceAll("");
130    }
131
132    /** Standard policy for a blog comment box. Mostly the same as the standard policy, except stripts out divs too.
133     *
134     *
135     * @param s
136     * @return
137     */
138    public static String commentSanitize(String s) {
139        if (s == null) {
140            return "";
141        }
142        return COMMENTS_BOX_POLICY.sanitize(s);
143    }
144
145
146    /** Strips all dangerous javascript, all block HTML that could ruin the page
147     *  Allows only a limited white list of tags
148     * @param s
149     * @return
150     */
151    public static String basicSanitize(String s) {
152        if (s == null) {
153            return "";
154        }
155        return STANDARD_POLICY.sanitize(s);
156    }
157
158
159    public static String basicSanitizeWithImages(String s) {
160        if (s == null) {
161            return "";
162        }
163        return STANDARD_POLICY_WITH_IMAGES.sanitize(s);
164    }
165
166    public static String escapeHtmlAttribute(String s) {
167        return s.replace("\"", "&quot;").replace("'", "&#39;").replace("<", "&lt;").replace(">", "&gt;");
168    }
169
170    public static String escapeXml(String s) {
171        return StringEscapeUtils.escapeXml11(s);
172    }
173
174
175    public static Object htmlSafeJson(Object obj) {
176
177        String out = JSON.stringify(obj);
178        out = out.replace("<", "\\u003c");
179        return out;
180    }
181
182    /**
183     * Gets the object in a JSON form that is safe for being outputted on a web page:
184     * &lt;script&gt;
185     *     var myObj = {{ utils.htmlSafeJson(obj, "member") }}
186     * &lt;/script&gt;
187     * @param obj
188     * @param restrictionLevel - Uses the JsonView annotation to determine which properties of the object
189     *                         should be outputed. Possible values are: unrestricted/public/member/owner/internal
190     * @return
191     */
192    public static Object htmlSafeJson(Object obj, String restrictionLevel) {
193        String out = "";
194        try {
195            restrictionLevel = restrictionLevel == null ? "public" : restrictionLevel.toLowerCase();
196            if ("public".equals(restrictionLevel)) {
197                out = JSON.stringify(obj, RestrictedViews.Public.class, true);
198            } else if ("unrestricted".equals(restrictionLevel)) {
199                out = JSON.stringify(obj, RestrictedViews.Unrestricted.class, false);
200            } else if ("member".equals(restrictionLevel)) {
201                out = JSON.stringify(obj, RestrictedViews.Member.class, true);
202            } else if ("owner".equals(restrictionLevel)) {
203                out = JSON.stringify(obj, RestrictedViews.Owner.class, true);
204            } else if ("internal".equals(restrictionLevel)) {
205                out = JSON.stringify(obj, RestrictedViews.Internal.class, true);
206            } else {
207                out = "Unknown restriction level: " + restrictionLevel;
208            }
209        } catch (JsonProcessingException ex) {
210            String objId = obj.toString();
211            if (obj instanceof Model) {
212                objId = ((Model)obj).getId().toString();
213            }
214            String msg = "Error JSON.stringifying object {0}" + obj.getClass().getSimpleName() + ":" + objId;
215            if (Context.getSettings().getDebug() || Context.getUser().isInRole(Role.ADMIN)) {
216                out = msg + "\n\nStacktrace-----\n\n" + ExceptionUtils.getStackTrace(ex);
217            }
218            Log.exception(ex, msg);
219        }
220        out = out.replace("<", "\\u003c");
221        return out;
222    }
223}