Daniel Plohmann avatar Daniel Plohmann committed 080962b

added WinAPI online mode.

Comments (0)

Files changed (16)

         },
     "winapi": {
         "search_hotkey": "ctrl+y",
-        "load_keyword_database": true
+        "load_keyword_database": true,
+        "online_enabled": true
         }
 }

idascope/core/AnnotationsProvider.py

 import idaapi
 import idautils
 
-import JsonHelper
+from helpers import JsonHelper
 
 annotations = {
                0x401000:

idascope/core/CryptoIdentifier.py

 
 from IdaProxy import IdaProxy
 from PatternManager import PatternManager
-from Tarjan import Tarjan
+from helpers.Tarjan import Tarjan
 
 from idascope.core.structures.Segment import Segment
 from idascope.core.structures.AritlogBasicBlock import AritlogBasicBlock

idascope/core/DocumentationHelper.py

 
 from IdaProxy import IdaProxy
 
-import JsonHelper
+from helpers import JsonHelper
 
 
     ## 6 nice opposing colors as used in IDAscope standard config

idascope/core/JsonHelper.py

-#!/usr/bin/python
-# JSON decoding hooks by Mike Brennan / Stack Overflow
-# http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-unicode-ones-from-json-in-python
-
-"""
-This module allows using the Python json module for parsing JSON data and
-decoding strings in UTF-8 instead of Unicode, resulting in ability to use str functions (e.g. for sorting)
-"""
-
-
-def decode_list(data):
-    rv = []
-    for item in data:
-        if isinstance(item, unicode):
-            item = item.encode('utf-8')
-        elif isinstance(item, list):
-            item = decode_list(item)
-        elif isinstance(item, dict):
-            item = decode_dict(item)
-        rv.append(item)
-    return rv
-
-
-def decode_dict(data):
-    rv = {}
-    for key, value in data.iteritems():
-        if isinstance(key, unicode):
-            key = key.encode('utf-8')
-        if isinstance(value, unicode):
-            value = value.encode('utf-8')
-        elif isinstance(value, list):
-            value = decode_list(value)
-        elif isinstance(value, dict):
-            value = decode_dict(value)
-        rv[key] = value
-    return rv

idascope/core/SemanticIdentifier.py

 import json
 import re
 
-import JsonHelper
+from helpers import JsonHelper
 
 from IdaProxy import IdaProxy
 from idascope.core.structures.FunctionContext import FunctionContext

idascope/core/Tarjan.py

-#!/usr/bin/python
-class Tarjan():
-    """
-    Tarjan's Algorithm (named for its discoverer, Robert Tarjan) is a graph theory algorithm
-    for finding the strongly connected components of a graph.
-    This can be used to find loops.
-    Based on: http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
-
-    Implementation by Dries Verdegem:
-    http://www.logarithmic.net/pfh-files/blog/01208083168/tarjan.py
-    Taken from Dr. Paul Harrison Blog:
-    http://www.logarithmic.net/pfh/blog/01208083168
-    """
-
-    def __init__(self):
-        pass
-
-    def calculate_strongly_connected_components(self, graph):
-        """
-        @param graph: a dictionary describing a directed graph, with keys as nodes and values as successors.
-        @type graph: (dict)
-        @return: (a list of tuples) describing the SCCs
-        """
-
-        index_counter = [0]
-        stack = []
-        lowlinks = {}
-        index = {}
-        result = []
-
-        def calculate_scc_for_node(node):
-            # set the depth index for this node to the smallest unused index
-            index[node] = index_counter[0]
-            lowlinks[node] = index_counter[0]
-            index_counter[0] += 1
-            stack.append(node)
-
-            # Consider successors of `node`
-            try:
-                successors = graph[node]
-            except:
-                successors = []
-            for successor in successors:
-                if successor not in lowlinks:
-                    # Successor has not yet been visited; recurse on it
-                    calculate_scc_for_node(successor)
-                    lowlinks[node] = min(lowlinks[node],lowlinks[successor])
-                elif successor in stack:
-                    # the successor is in the stack and hence in the current strongly connected component (SCC)
-                    lowlinks[node] = min(lowlinks[node],index[successor])
-
-            # If `node` is a root node, pop the stack and generate an SCC
-            if lowlinks[node] == index[node]:
-                connected_component = []
-
-                while True:
-                    successor = stack.pop()
-                    connected_component.append(successor)
-                    if successor == node: break
-                component = tuple(connected_component)
-                # storing the result
-                result.append(component)
-
-        for node in graph:
-            if node not in lowlinks:
-                calculate_scc_for_node(node)
-
-        return result

idascope/core/WinApiProvider.py

 
 import json
 import os
+import string
 
-import JsonHelper
+from helpers import JsonHelper
+from helpers.Downloader import Downloader
 
 
 class WinApiProvider():
     def __init__(self, idascope_config):
         print ("[|] Loading WinApiProvider")
         self.os = os
+        self.string = string
+        self.downloader = Downloader()
+        self.downloader.downloadFinished.connect(self.onDownloadFinished)
         self.idascope_config = idascope_config
         self.winapi_data = {}
         if self.idascope_config.winapi_load_keyword_database:
             self._load_keywords()
+        self.online_msdn_enabled = self.idascope_config.winapi_online_enabled
         self.last_delivered_filepath = self.idascope_config.winapi_rootdir
         self.backward_history = []
         self.forward_history = []
         self.is_appending_to_history = True
+        self.download_receivers = []
 
     def _load_keywords(self):
         """
         keywords_file = open(self.idascope_config.winapi_keywords_file, "r")
         self.winapi_data = json.loads(keywords_file.read(), object_hook=JsonHelper.decode_dict)
 
+    def register_data_receiver(self, receiving_function):
+        """
+        (Observer Pattern) Register a data receiver for downloaded data. Each time an
+        online lookup is performed, the data receiver is provided with the downloaded content.
+        @param receiving_function: the function to receive the downloaded data
+        @type: receiveing_function: a function that receives one (str) parameter.
+        """
+        self.download_receivers.append(receiving_function)
+
+    def onDownloadFinished(self):
+        """
+        When a download of MSDN data is finished, notice all receivers.
+        """
+        data = self.downloader.get_data()
+        if not data:
+            data = "Download failed! Try again or check your Internet connection."
+        for receiver in self.download_receivers:
+            receiver(data)
+
     def has_offline_msdn_available(self):
         """
-        Determines wther the offline MSDN database is available or not.
+        Determines whether the offline MSDN database is available or not.
         This is evaluated based on whether the keywords database has been loaded or not.
         @return: (bool) availablity of the MSDN database
         """
             return True
         return False
 
+    def has_online_msdn_available(self):
+        """
+        Determines whether the online MSDN database is available or not.
+        @return: (bool) setting of the online lookup flag
+        """
+        return self.online_msdn_enabled
+
+    def set_online_msdn_enabled(self, enabled):
+        """
+        Change the state of the online lookup availability.
+        """
+        self.online_msdn_enabled = enabled
+
     def get_keywords_for_initial(self, keyword_initial):
         """
         Get all keywords that start with the given initial character.
         api_filenames = self._get_api_filenames(keyword)
         if len(api_filenames) == 1:
             api_filenames = [self.idascope_config.winapi_rootdir + api_filenames[0]]
+        elif self.online_msdn_enabled and len(api_filenames) == 0:
+            return self._get_online_msdn_content(keyword)
         return self._get_document_content(api_filenames)
 
     def get_linked_document_content(self, url):
         else:
             return self._get_single_document_content(url.toString()), anchor
 
-    def get_history_states(self):
+    def has_backward_history(self):
         """
-        Get information about whether history stepping (backward, forward) is available or not.
-        @return: a tuple (boolean, boolean) telling about availability of history stepping.
+        Get information about whether backward history stepping is available or not.
+        @return: (boolean) telling about availability of history stepping.
         """
-        return (len(self.backward_history) > 1, len(self.forward_history) > 0)
+        return len(self.backward_history) > 1
+
+    def has_forward_history(self):
+        """
+        Get information about whether forward history stepping is available or not.
+        @return: (boolean) telling about availability of history stepping.
+        """
+        return len(self.forward_history) > 1
 
     def get_previous_document_content(self):
         """
             document_content = "<html><head /><body>Well, something has gone wrong here. Try again with some" \
                 + " proper API name.<hr /><p>Exception: %s</p></body></html>" % exc
         return document_content
+
+    def _get_online_msdn_content(self, keyword):
+        """
+        This functions downloads content from the MSDN website. It does not return the
+        information instantly but provides it through a callback that can be registered
+        with the function I{register_data_receiver}.
+        @param keyword: the keyword to look up in MSDN
+        @type keyword: str
+        @return: (str) a waiting message if the keyword has been queried or a negative answer if
+            there are no entries in MSDN
+        """
+        feed_url = "http://social.msdn.microsoft.com/Search/en-us/feed?format=RSS&Query=%s" % keyword
+        feed_content = self.downloader.download(feed_url)
+        if not feed_content:
+            return "<p>Could not access the MSDN feed. Check your Internet connection.</p>"
+        msdn_url = self._get_first_msdn_link(feed_content)
+        if msdn_url != "":
+            return self.cleanup_downloaded_html(self.downloader.download(msdn_url))
+            # FIXME: should threading in IDA ever work, use this...
+            # self.downloader.download_threaded(msdn_url)
+            # return "<p>Fetching information from online MSDN, please wait...</p>"
+        else:
+            return "<p>Even MSDN can't help you on this one.</p>"
+
+    def _get_first_msdn_link(self, feed_content):
+        """
+        Parses the first MSDN URL from a RSS feed.
+        @param feed_content: a rss feed output
+        @type feed_content: str
+        @return: (str) the first MSDN url if existing.
+        """
+        while feed_content.find("<link>") > -1:
+            start_index = feed_content.find("<link>")
+            end_index = feed_content.find("</link>")
+            link_url = feed_content[len("<link>") + start_index:end_index]
+            if "msdn.microsoft.com" in link_url:
+                return link_url
+            else:
+                feed_content = feed_content[feed_content.find("</link>") + 7:]
+            return ""
+
+    def cleanup_downloaded_html(self, content):
+        content = content[content.find("<div class=\"topic\""):content.find("<div id=\"contentFeedback\"")]
+        content = "".join(s for s in content if s in self.string.printable)
+        return content

idascope/core/helpers/Downloader.py

+#!/usr/bin/python
+########################################################################
+# Copyright (c) 2012
+# Daniel Plohmann <daniel.plohmann<at>gmail<dot>com>
+# Alexander Hanel <alexander.hanel<at>gmail<dot>com>
+# All rights reserved.
+########################################################################
+#
+#  a QThread for downloading web content.
+#
+########################################################################
+#
+#  This file is part of IDAscope
+#
+#  IDAscope is free software: you can redistribute it and/or modify it
+#  under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful, but
+#  WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see
+#  <http://www.gnu.org/licenses/>.
+#
+########################################################################
+
+from PySide import QtCore
+import httplib
+
+from ThreadedDownloader import ThreadedDownloader
+
+
+class TempQThread(QtCore.QThread):
+
+    def __init__(self, parent=None):
+        QtCore.QThread.__init__(self, parent)
+
+    def run(self):
+        self.exec_()
+
+    def __str__(self):
+        return "0x%08X" % id(self)
+
+
+class Downloader(QtCore.QObject):
+    """
+    A class to download web content. Works both blocking and non-blocking (threaded with callback).
+    """
+
+    downloadFinished = QtCore.Signal()
+
+    def __init__(self):
+        super(Downloader, self).__init__()
+        self.httplib = httplib
+        self.TempQThread = TempQThread
+        self.ThreadedDownloader = ThreadedDownloader
+        self._data = None
+        self.download_thread = None
+
+    def download_threaded(self, url):
+        """
+        Start a new download thread. Will notify via signal "downloadFinished" when done.
+        @param url: The URL to download from.
+        @type url: str
+        """
+        self.download_thread = self.TempQThread()
+        self.download_worker = self.ThreadedDownloader(url)
+        self.download_worker.moveToThread(self.download_thread)
+        self.download_worker.threadFinished.connect(self._onThreadFinished)
+        self.download_thread.started.connect(self.download_worker.setup)
+        # starts thread event loop and triggers my_thread.started (and in turn, setup)
+        self.download_thread.start()
+        # in the end
+        # quit thread's event loop (thread will end)
+        if self.download_thread.isRunning():
+            self.download_thread.quit()
+            # self.download_thread.wait(5000)
+        # clean up thread object
+        self.download_worker.shutdown()
+
+    def download(self, url):
+        """
+        Start a blocking download. Will return the downloaded content when done.
+        @param url: The URL to download from.
+        @type url: str
+        @return: (str) the downloaded content.
+        """
+        host = url[7:url.find("/", 7)]
+        path = url[url.find("/", 7):]
+        try:
+            conn = self.httplib.HTTPConnection(host)
+            conn.request("GET", path)
+            response = conn.getresponse()
+            if response.status == 200:
+                print "[+] Downloaded from: %s" % (url)
+                self._data = response.read()
+            else:
+                print "[-] Download failed: %s (%s %s)" % (url, response.status, response.reason)
+                self._data = "Download failed (%s %s)!" % (response.status, response.reason)
+            conn.close()
+        except Exception as exc:
+            print ("[!] Downloader.download: Exception while downloading: %s" % exc)
+            self._data = None
+        return self._data
+
+    def download_signalled(self, url):
+        self.download(url)
+        self.downloadFinished.emit()
+
+    def get_data(self):
+        """
+        Returns the previously downloaded data.
+        """
+        return self._data
+
+    def _onThreadFinished(self):
+        if self.download_thread:
+            self._data = self.download_thread.get_data()
+            self.download_thread = None
+        self.downloadFinished.emit()

idascope/core/helpers/JsonHelper.py

+#!/usr/bin/python
+# JSON decoding hooks by Mike Brennan / Stack Overflow
+# http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-unicode-ones-from-json-in-python
+
+"""
+This module allows using the Python json module for parsing JSON data and
+decoding strings in UTF-8 instead of Unicode, resulting in ability to use str functions (e.g. for sorting)
+"""
+
+
+def decode_list(data):
+    rv = []
+    for item in data:
+        if isinstance(item, unicode):
+            item = item.encode('utf-8')
+        elif isinstance(item, list):
+            item = decode_list(item)
+        elif isinstance(item, dict):
+            item = decode_dict(item)
+        rv.append(item)
+    return rv
+
+
+def decode_dict(data):
+    rv = {}
+    for key, value in data.iteritems():
+        if isinstance(key, unicode):
+            key = key.encode('utf-8')
+        if isinstance(value, unicode):
+            value = value.encode('utf-8')
+        elif isinstance(value, list):
+            value = decode_list(value)
+        elif isinstance(value, dict):
+            value = decode_dict(value)
+        rv[key] = value
+    return rv

idascope/core/helpers/Tarjan.py

+#!/usr/bin/python
+class Tarjan():
+    """
+    Tarjan's Algorithm (named for its discoverer, Robert Tarjan) is a graph theory algorithm
+    for finding the strongly connected components of a graph.
+    This can be used to find loops.
+    Based on: http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
+
+    Implementation by Dries Verdegem:
+    http://www.logarithmic.net/pfh-files/blog/01208083168/tarjan.py
+    Taken from Dr. Paul Harrison Blog:
+    http://www.logarithmic.net/pfh/blog/01208083168
+    """
+
+    def __init__(self):
+        pass
+
+    def calculate_strongly_connected_components(self, graph):
+        """
+        @param graph: a dictionary describing a directed graph, with keys as nodes and values as successors.
+        @type graph: (dict)
+        @return: (a list of tuples) describing the SCCs
+        """
+
+        index_counter = [0]
+        stack = []
+        lowlinks = {}
+        index = {}
+        result = []
+
+        def calculate_scc_for_node(node):
+            # set the depth index for this node to the smallest unused index
+            index[node] = index_counter[0]
+            lowlinks[node] = index_counter[0]
+            index_counter[0] += 1
+            stack.append(node)
+
+            # Consider successors of `node`
+            try:
+                successors = graph[node]
+            except:
+                successors = []
+            for successor in successors:
+                if successor not in lowlinks:
+                    # Successor has not yet been visited; recurse on it
+                    calculate_scc_for_node(successor)
+                    lowlinks[node] = min(lowlinks[node],lowlinks[successor])
+                elif successor in stack:
+                    # the successor is in the stack and hence in the current strongly connected component (SCC)
+                    lowlinks[node] = min(lowlinks[node],index[successor])
+
+            # If `node` is a root node, pop the stack and generate an SCC
+            if lowlinks[node] == index[node]:
+                connected_component = []
+
+                while True:
+                    successor = stack.pop()
+                    connected_component.append(successor)
+                    if successor == node: break
+                component = tuple(connected_component)
+                # storing the result
+                result.append(component)
+
+        for node in graph:
+            if node not in lowlinks:
+                calculate_scc_for_node(node)
+
+        return result

idascope/core/helpers/ThreadedDownloader.py

+#!/usr/bin/python
+########################################################################
+# Copyright (c) 2012
+# Daniel Plohmann <daniel.plohmann<at>gmail<dot>com>
+# Alexander Hanel <alexander.hanel<at>gmail<dot>com>
+# All rights reserved.
+########################################################################
+#
+#  a QThread for downloading web content.
+#
+########################################################################
+#
+#  This file is part of IDAscope
+#
+#  IDAscope is free software: you can redistribute it and/or modify it
+#  under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful, but
+#  WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see
+#  <http://www.gnu.org/licenses/>.
+#
+########################################################################
+
+from PySide import QtCore
+# from PySide.QtCore import QThread
+import httplib
+
+import idascope.core.helpers.ThreadedDownloader
+
+
+class ThreadedDownloader(QtCore.QObject):
+
+    threadFinished = QtCore.Signal()
+
+    def __init__(self, url):
+        super(ThreadedDownloader, self).__init__()
+        self.httplib = httplib
+        self.QtCore = QtCore
+        self.url = url
+        self._data = None
+
+    def setup(self):
+        # loop setup (0 timer): optional
+        self.loop_timer = self.QtCore.QTimer()
+        self.loop_timer.timeout.connect(self.loop)
+        self.loop_timer.start(0)
+        self.run()
+        self.threadFinished.emit()
+
+    def shutdown(self):
+        # do clean-up
+        pass
+
+    def loop(self):
+        pass
+
+    def run(self):
+        host = self.url[7:self.url.find("/", 7)]
+        path = self.url[self.url.find("/", 7):]
+        try:
+            conn = self.httplib.HTTPConnection(host)
+            conn.request("GET", path)
+            response = conn.getresponse()
+            print response.status, response.reason
+            self._data = response.read()
+            conn.close()
+        except Exception as exc:
+            print ("[!] ThreadedDownloader.run: Exception while downloading: %s" % exc)
+            self._data = None
+        finally:
+            self.threadFinished.emit()
+
+    def get_data(self):
+        return self._data
Add a comment to this file

idascope/core/helpers/__init__.py

Empty file added.

idascope/core/structures/IDAscopeConfiguration.py

 import os
 import json
 
-from idascope.core import JsonHelper
+from idascope.core.helpers import JsonHelper
 
 
 class IDAscopeConfiguration():
         self.winapi_rootdir = ""
         self.winapi_shortcut = "ctrl+y"
         self.winapi_load_keyword_database = False
+        self.winapi_online_enabled = False
         self._load_config(config_filename)
 
     def _load_config(self, config_filename):
         # widget related configurations
         self.winapi_shortcut = config["winapi"]["search_hotkey"]
         self.winapi_load_keyword_database = config["winapi"]["load_keyword_database"]
+        self.winapi_online_enabled = config["winapi"]["online_enabled"]
 
     def _normalize_path(self, path):
         if self.os_path_normpath is None:
Add a comment to this file

idascope/icons/online.png

Added
New image

idascope/widgets/WinApiWidget.py

     def __init__(self, parent):
         QtGui.QWidget.__init__(self)
         print "[|] loading WinApiWidget"
-        # enable access to shared IDAscope modules
         self.parent = parent
         self.name = "WinAPI Browsing"
         self.icon = QIcon(self.parent.config.icon_file_path + "winapi.png")
         self.search_icon = QIcon(self.parent.config.icon_file_path + "search.png")
         self.back_icon = QIcon(self.parent.config.icon_file_path + "back.png")
         self.forward_icon = QIcon(self.parent.config.icon_file_path + "forward.png")
-        # This widget may relay on IDAproxy
+        self.online_icon = QIcon(self.parent.config.icon_file_path + "online.png")
         self.ida_proxy = self.parent.ida_proxy
-        self.winapi = self.parent.winapi_provider
-        # references to Qt-specific modules
         self.QtGui = QtGui
         self.QtCore = QtCore
+        self.winapi = self.parent.winapi_provider
         self.old_keyword_initial = ""
+        self.winapi.register_data_receiver(self.populate_browser_window)
         self.createGui()
-        self.set_availability()
+        self.update_availability()
         self.register_hotkeys()
 
-    def set_availability(self):
+    def update_availability(self):
         """
-        Adjust the availability of this widget by checking if the keyword database has been loaded.
-        If the database has not been loaded, deactivate the search functionality.
+        Adjust the availability of this widget by checking if the keyword database has been loaded or
+        online mode is enabled.
         """
-        if not self.winapi.has_offline_msdn_available():
-            self.browser_window.setHtml("<font color=\"#FF0000\">Offline MSDN database is not available. To use " \
+        if not self.winapi.has_offline_msdn_available() and \
+            not self.winapi.has_online_msdn_available():
+            self.browser_window.setHtml("<p><font color=\"#FF0000\">Offline MSDN database is not available. To use " \
                 + "it, have a look at the installation instructions located in the manual: " \
-                + "IDAscope/documentation/manual.html.</font>")
+                + "IDAscope/documentation/manual.html. Online mode is deactivated as well.</font></p>")
             self.search_button.setEnabled(False)
             self.api_chooser_lineedit.setEnabled(False)
+        else:
+            self.browser_window.setHtml("<p>Enter a search term in the above field to search offline/online MSDN.</p>")
+            self.search_button.setEnabled(True)
+            self.api_chooser_lineedit.setEnabled(True)
 
     def register_hotkeys(self):
         """
         """
         self.create_back_button()
         self.create_next_button()
+        self.create_online_button()
         self.create_api_chooser_lineedit()
         self.create_search_button()
         self.create_browser_window()
         winapi_layout = QtGui.QVBoxLayout()
         selection_widget = QtGui.QWidget()
         selection_layout = QtGui.QHBoxLayout()
+        selection_layout.addWidget(self.online_button)
         selection_layout.addWidget(self.back_button)
         selection_layout.addWidget(self.next_button)
         selection_layout.addWidget(self.api_chooser_lineedit)
         self.next_button.setEnabled(False)
         self.next_button.clicked.connect(self.onNextButtonClicked)
 
+    def create_online_button(self):
+        """
+        Create a next button to allow easier browsing
+        """
+        self.online_button = QtGui.QPushButton(self.online_icon, "", self)
+        self.online_button.setCheckable(True)
+        if self.winapi.has_online_msdn_available():
+            self.online_button.setChecked(QtCore.Qt.Checked)
+        self.online_button.setToolTip("Enable/disable MSDN online lookup.")
+        self.online_button.resize(self.online_button.sizeHint())
+        self.online_button.clicked.connect(self.onOnlineButtonClicked)
+
     def create_api_chooser_lineedit(self):
         """
         Create the I{QLineEdit }used for selecting API names. This includes a QCompleter to make suggestions based on
                 keyword_data = self.winapi.get_keywords_for_initial(keyword_initial)
                 self.completer_model.setStringList(keyword_data)
 
-    def populate_browser_window(self):
+    def populate_browser_window(self, content=""):
         """
         Populate the browser window based upon the entered term in the search line.
-        """
-        api_chooser_text = self.api_chooser_lineedit.text()
-        if len(api_chooser_text) > 0:
-            api_document = self.winapi.get_keyword_content(api_chooser_text)
-            self.browser_window.setHtml(api_document)
+        @param content: the content to render in the browser
+        @type content: str
+        """
+        if content == "":
+            api_chooser_text = self.api_chooser_lineedit.text()
+            if len(api_chooser_text) > 0:
+                content = self.winapi.get_keyword_content(api_chooser_text)
+        self.browser_window.setHtml(content)
         self.update_history_button_state()
 
     def onSearchButtonClicked(self):
         self.browser_window.scrollToAnchor(anchor)
         self.update_history_button_state()
 
+    def onOnlineButtonClicked(self):
+        """
+        Action that is performed when the search button is clicked. This will populate the browser window.
+        """
+        self.winapi.set_online_msdn_enabled(not self.winapi.has_online_msdn_available())
+        self.update_availability()
+
     def browserAnchorClicked(self, url):
         """
         Callback for the case an anchor (or any link) within the browser window is clicked. This will fetch
         Update the button state (enabled/disabled) according to availability of history information from the
         WinApiProvider
         """
-        history_button_states = self.winapi.get_history_states()
-        self.back_button.setEnabled(history_button_states[0])
-        self.next_button.setEnabled(history_button_states[1])
+        self.back_button.setEnabled(self.winapi.has_backward_history())
+        self.next_button.setEnabled(self.winapi.has_forward_history())
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.