/* * Copyright (C) 2013 The Android Open Source Project * * 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 com.android.tools.lint.checks; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation; import com.android.tools.lint.client.api.JavaParser.ResolvedClass; import com.android.tools.lint.client.api.JavaParser.ResolvedMethod; import com.android.tools.lint.client.api.JavaParser.ResolvedNode; import com.android.tools.lint.client.api.JavaParser.ResolvedVariable; import com.android.tools.lint.client.api.JavaParser.TypeDescriptor; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Implementation; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.JavaContext; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Scope; import com.android.tools.lint.detector.api.Severity; import com.android.tools.lint.detector.api.Speed; import com.google.common.collect.Maps; import java.util.Collections; import java.util.List; import java.util.Map; import lombok.ast.AstVisitor; import lombok.ast.BinaryExpression; import lombok.ast.BinaryOperator; import lombok.ast.Cast; import lombok.ast.ConstructorInvocation; import lombok.ast.Expression; import lombok.ast.ForwardingAstVisitor; import lombok.ast.InlineIfExpression; import lombok.ast.MethodInvocation; import lombok.ast.Node; import lombok.ast.VariableDefinitionEntry; import lombok.ast.VariableReference; /** * Looks for addJavascriptInterface calls on interfaces have been properly annotated * with {@code @JavaScriptInterface} */ public class JavaScriptInterfaceDetector extends Detector implements Detector.JavaScanner { /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "JavascriptInterface", //$NON-NLS-1$ "Missing @JavascriptInterface on methods", "As of API 17, you must annotate methods in objects registered with the " + "`addJavascriptInterface` method with a `@JavascriptInterface` annotation.", Category.SECURITY, 8, Severity.ERROR, new Implementation( JavaScriptInterfaceDetector.class, Scope.JAVA_FILE_SCOPE)) .addMoreInfo( "http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)"); //$NON-NLS-1$ private static final String ADD_JAVASCRIPT_INTERFACE = "addJavascriptInterface"; //$NON-NLS-1$ private static final String JAVASCRIPT_INTERFACE_CLS = "android.webkit.JavascriptInterface"; //$NON-NLS-1$ private static final String WEB_VIEW_CLS = "android.webkit.WebView"; //$NON-NLS-1$ /** Constructs a new {@link JavaScriptInterfaceDetector} check */ public JavaScriptInterfaceDetector() { } @NonNull @Override public Speed getSpeed() { return Speed.SLOW; // because it relies on class loading referenced javascript interface } // ---- Implements JavaScanner ---- @Nullable @Override public List<String> getApplicableMethodNames() { return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE); } @Override public void visitMethod( @NonNull JavaContext context, @Nullable AstVisitor visitor, @NonNull MethodInvocation call) { if (context.getMainProject().getTargetSdk() < 17) { return; } if (call.astArguments().size() != 2) { return; } if (!isCallOnWebView(context, call)) { return; } Expression first = call.astArguments().first(); ResolvedNode resolved = context.resolve(first); if (resolved instanceof ResolvedVariable) { // We're passing in a variable to the addJavaScriptInterface method; // the variable may be of a more generic type than the actual // value assigned to it. For example, we may have a scenario like this: // Object object = new SpecificType(); // addJavaScriptInterface(object, ...) // Here the type of the variable is Object, but we know that it can // contain objects of type SpecificType, so we should check that type instead. Node method = JavaContext.findSurroundingMethod(call); if (method != null) { ConcreteTypeVisitor v = new ConcreteTypeVisitor(context, call); method.accept(v); resolved = v.getType(); if (resolved == null) { return; } } else { return; } } else if (resolved instanceof ResolvedMethod) { ResolvedMethod method = (ResolvedMethod) resolved; if (method.isConstructor()) { resolved = method.getContainingClass(); } else { TypeDescriptor returnType = method.getReturnType(); if (returnType != null) { resolved = returnType.getTypeClass(); } } } else { TypeDescriptor type = context.getType(first); if (type != null) { resolved = type.getTypeClass(); } } if (resolved instanceof ResolvedClass) { ResolvedClass cls = (ResolvedClass) resolved; if (isJavaScriptAnnotated(cls)) { return; } Location location = context.getLocation(call.astName()); String message = String.format( "None of the methods in the added interface (%1$s) have been annotated " + "with `@android.webkit.JavascriptInterface`; they will not " + "be visible in API 17", cls.getSimpleName()); context.report(ISSUE, call, location, message); } } private static boolean isCallOnWebView(JavaContext context, MethodInvocation call) { ResolvedNode resolved = context.resolve(call); if (!(resolved instanceof ResolvedMethod)) { return false; } ResolvedMethod method = (ResolvedMethod) resolved; return method.getContainingClass().matches(WEB_VIEW_CLS); } private static boolean isJavaScriptAnnotated(ResolvedClass clz) { while (clz != null) { for (ResolvedAnnotation annotation : clz.getAnnotations()) { if (annotation.getType().matchesSignature(JAVASCRIPT_INTERFACE_CLS)) { return true; } } for (ResolvedMethod method : clz.getMethods(false)) { for (ResolvedAnnotation annotation : method.getAnnotations()) { if (annotation.getType().matchesSignature(JAVASCRIPT_INTERFACE_CLS)) { return true; } } } clz = clz.getSuperClass(); } return false; } private static class ConcreteTypeVisitor extends ForwardingAstVisitor { private final JavaContext mContext; private final MethodInvocation mTargetCall; private boolean mFoundCall; private Map<Node, ResolvedClass> mTypes = Maps.newIdentityHashMap(); private Map<ResolvedVariable, ResolvedClass> mVariableTypes = Maps.newHashMap(); public ConcreteTypeVisitor(JavaContext context, MethodInvocation call) { mContext = context; mTargetCall = call; } public ResolvedClass getType() { Expression first = mTargetCall.astArguments().first(); ResolvedClass resolvedClass = mTypes.get(first); if (resolvedClass == null) { ResolvedNode resolved = mContext.resolve(first); if (resolved instanceof ResolvedVariable) { resolvedClass = mVariableTypes.get(resolved); if (resolvedClass == null) { return ((ResolvedVariable)resolved).getType().getTypeClass(); } } } return resolvedClass; } @Override public boolean visitNode(Node node) { return mFoundCall || super.visitNode(node); } @Override public void afterVisitMethodInvocation(MethodInvocation node) { if (node == mTargetCall) { mFoundCall = true; } } @Override public void afterVisitConstructorInvocation(@NonNull ConstructorInvocation node) { ResolvedNode resolved = mContext.resolve(node); if (resolved instanceof ResolvedMethod) { ResolvedMethod method = (ResolvedMethod) resolved; mTypes.put(node, method.getContainingClass()); } else { // Implicit constructor? TypeDescriptor type = mContext.getType(node); if (type != null) { ResolvedClass typeClass = type.getTypeClass(); if (typeClass != null) { mTypes.put(node, typeClass); } } } } @Override public void afterVisitVariableReference(VariableReference node) { if (mTypes.get(node) == null) { ResolvedNode resolved = mContext.resolve(node); if (resolved instanceof ResolvedVariable) { ResolvedClass resolvedClass = mVariableTypes.get(resolved); if (resolvedClass != null) { mTypes.put(node, resolvedClass); } } } } @Override public void afterVisitBinaryExpression(BinaryExpression node) { if (node.astOperator() == BinaryOperator.ASSIGN) { Expression rhs = node.astRight(); ResolvedClass resolvedClass = mTypes.get(rhs); if (resolvedClass != null) { Expression lhs = node.astLeft(); mTypes.put(lhs, resolvedClass); ResolvedNode variable = mContext.resolve(lhs); if (variable instanceof ResolvedVariable) { mVariableTypes.put((ResolvedVariable) variable, resolvedClass); } } } } @Override public void afterVisitInlineIfExpression(InlineIfExpression node) { ResolvedClass resolvedClass = mTypes.get(node.astIfTrue()); if (resolvedClass == null) { resolvedClass = mTypes.get(node.astIfFalse()); } if (resolvedClass != null) { mTypes.put(node, resolvedClass); } } @Override public void afterVisitVariableDefinitionEntry(VariableDefinitionEntry node) { Expression initializer = node.astInitializer(); if (initializer != null) { ResolvedClass resolvedClass = mTypes.get(initializer); if (resolvedClass != null) { mTypes.put(node, resolvedClass); ResolvedNode variable = mContext.resolve(node); if (variable instanceof ResolvedVariable) { mVariableTypes.put((ResolvedVariable) variable, resolvedClass); } } } } @Override public void afterVisitCast(Cast node) { ResolvedClass resolvedClass = mTypes.get(node); if (resolvedClass != null) { mTypes.put(node, resolvedClass); } } } }