Merge remote-tracking branch 'sqlmapproject/master'

This commit is contained in:
cxh852456 2015-12-20 09:02:20 +08:00
commit 169c95921a
20 changed files with 490 additions and 163 deletions

View File

@ -1034,7 +1034,7 @@ def checkStability():
delay = max(0, min(1, delay)) delay = max(0, min(1, delay))
time.sleep(delay) time.sleep(delay)
secondPage, _ = Request.queryPage(content=True, raise404=False) secondPage, _ = Request.queryPage(content=True, noteResponseTime=False, raise404=False)
if kb.redirectChoice: if kb.redirectChoice:
return None return None

View File

@ -2700,7 +2700,7 @@ def parseSqliteTableSchema(value):
table = {} table = {}
columns = {} columns = {}
for match in re.finditer(r"(\w+)[\"'`]?\s+(INT|INTEGER|TINYINT|SMALLINT|MEDIUMINT|BIGINT|UNSIGNED BIG INT|INT2|INT8|INTEGER|CHARACTER|VARCHAR|VARYING CHARACTER|NCHAR|NATIVE CHARACTER|NVARCHAR|TEXT|CLOB|TEXT|BLOB|NONE|REAL|DOUBLE|DOUBLE PRECISION|FLOAT|REAL|NUMERIC|DECIMAL|BOOLEAN|DATE|DATETIME|NUMERIC)\b", value, re.I): for match in re.finditer(r"(\w+)[\"'`]?\s+(INT|INTEGER|TINYINT|SMALLINT|MEDIUMINT|BIGINT|UNSIGNED BIG INT|INT2|INT8|INTEGER|CHARACTER|VARCHAR|VARYING CHARACTER|NCHAR|NATIVE CHARACTER|NVARCHAR|TEXT|CLOB|LONGTEXT|BLOB|NONE|REAL|DOUBLE|DOUBLE PRECISION|FLOAT|REAL|NUMERIC|DECIMAL|BOOLEAN|DATE|DATETIME|NUMERIC)\b", value, re.I):
columns[match.group(1)] = match.group(2) columns[match.group(1)] = match.group(2)
table[conf.tbl] = columns table[conf.tbl] = columns
@ -3770,8 +3770,12 @@ def decodeHexValue(value, raw=False):
def _(value): def _(value):
retVal = value retVal = value
if value and isinstance(value, basestring) and len(value) % 2 == 0: if value and isinstance(value, basestring):
retVal = hexdecode(retVal) if len(value) % 2 != 0:
retVal = "%s?" % hexdecode(value[:-1])
singleTimeWarnMessage("there was a problem decoding value '%s' from expected hexadecimal form" % value)
else:
retVal = hexdecode(value)
if not kb.binaryField and not raw: if not kb.binaryField and not raw:
if Backend.isDbms(DBMS.MSSQL) and value.startswith("0x"): if Backend.isDbms(DBMS.MSSQL) and value.startswith("0x"):

View File

@ -8,10 +8,13 @@ See the file 'doc/COPYING' for copying permission
import base64 import base64
import json import json
import pickle import pickle
import StringIO
import sys import sys
import types
from lib.core.settings import IS_WIN from lib.core.settings import IS_WIN
from lib.core.settings import UNICODE_ENCODING from lib.core.settings import UNICODE_ENCODING
from lib.core.settings import PICKLE_REDUCE_WHITELIST
def base64decode(value): def base64decode(value):
""" """
@ -67,10 +70,23 @@ def base64unpickle(value):
retVal = None retVal = None
def _(self):
if len(self.stack) > 1:
func = self.stack[-2]
if func not in PICKLE_REDUCE_WHITELIST:
raise Exception, "abusing reduce() is bad, Mkay!"
self.load_reduce()
def loads(str):
file = StringIO.StringIO(str)
unpickler = pickle.Unpickler(file)
unpickler.dispatch[pickle.REDUCE] = _
return unpickler.load()
try: try:
retVal = pickle.loads(base64decode(value)) retVal = loads(base64decode(value))
except TypeError: except TypeError:
retVal = pickle.loads(base64decode(bytes(value))) retVal = loads(base64decode(bytes(value)))
return retVal return retVal

View File

@ -578,7 +578,8 @@ class Dump(object):
if not os.path.isdir(dumpDbPath): if not os.path.isdir(dumpDbPath):
os.makedirs(dumpDbPath, 0755) os.makedirs(dumpDbPath, 0755)
filepath = os.path.join(dumpDbPath, "%s-%d.bin" % (unsafeSQLIdentificatorNaming(column), randomInt(8))) _ = re.sub(r"[^\w]", "_", normalizeUnicode(unsafeSQLIdentificatorNaming(column)))
filepath = os.path.join(dumpDbPath, "%s-%d.bin" % (_, randomInt(8)))
warnMsg = "writing binary ('%s') content to file '%s' " % (mimetype, filepath) warnMsg = "writing binary ('%s') content to file '%s' " % (mimetype, filepath)
logger.warn(warnMsg) logger.warn(warnMsg)

View File

@ -1024,6 +1024,9 @@ def _setSocketPreConnect():
Makes a pre-connect version of socket.connect Makes a pre-connect version of socket.connect
""" """
if conf.disablePrecon:
return
def _(): def _():
while kb.threadContinue: while kb.threadContinue:
try: try:
@ -1373,7 +1376,7 @@ def _setHTTPExtraHeaders():
errMsg = "invalid header value: %s. Valid header format is 'name:value'" % repr(headerValue).lstrip('u') errMsg = "invalid header value: %s. Valid header format is 'name:value'" % repr(headerValue).lstrip('u')
raise SqlmapSyntaxException(errMsg) raise SqlmapSyntaxException(errMsg)
elif not conf.httpHeaders or len(conf.httpHeaders) == 1: elif not conf.requestFile and len(conf.httpHeaders or []) < 2:
conf.httpHeaders.append((HTTP_HEADER.ACCEPT_LANGUAGE, "en-us,en;q=0.5")) conf.httpHeaders.append((HTTP_HEADER.ACCEPT_LANGUAGE, "en-us,en;q=0.5"))
if not conf.charset: if not conf.charset:
conf.httpHeaders.append((HTTP_HEADER.ACCEPT_CHARSET, "ISO-8859-15,utf-8;q=0.7,*;q=0.7")) conf.httpHeaders.append((HTTP_HEADER.ACCEPT_CHARSET, "ISO-8859-15,utf-8;q=0.7,*;q=0.7"))
@ -1573,7 +1576,7 @@ def _cleanupOptions():
conf.progressWidth = width - 46 conf.progressWidth = width - 46
for key, value in conf.items(): for key, value in conf.items():
if value and any(key.endswith(_) for _ in ("Path", "File")): if value and any(key.endswith(_) for _ in ("Path", "File", "Dir")):
conf[key] = safeExpandUser(value) conf[key] = safeExpandUser(value)
if conf.testParameter: if conf.testParameter:
@ -1894,7 +1897,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.safeReq = AttribDict() kb.safeReq = AttribDict()
kb.singleLogFlags = set() kb.singleLogFlags = set()
kb.reduceTests = None kb.reduceTests = None
kb.tlsSNI = None kb.tlsSNI = {}
kb.stickyDBMS = False kb.stickyDBMS = False
kb.stickyLevel = None kb.stickyLevel = None
kb.storeCrawlingChoice = None kb.storeCrawlingChoice = None

View File

@ -227,6 +227,7 @@ optDict = {
}, },
"Hidden": { "Hidden": {
"dummy": "boolean", "dummy": "boolean",
"disablePrecon": "boolean",
"binaryFields": "string", "binaryFields": "string",
"profile": "boolean", "profile": "boolean",
"cpuThrottle": "integer", "cpuThrottle": "integer",

View File

@ -87,5 +87,4 @@ def profile(profileOutputFile=None, dotOutputFile=None, imageOutputFile=None):
win.connect('destroy', gtk.main_quit) win.connect('destroy', gtk.main_quit)
win.set_filter("dot") win.set_filter("dot")
win.open_file(dotOutputFile) win.open_file(dotOutputFile)
gobject.timeout_add(1000, win.update, dotOutputFile)
gtk.main() gtk.main()

View File

@ -11,7 +11,9 @@ import subprocess
import string import string
import sys import sys
import time import time
import types
from lib.core.datatype import AttribDict
from lib.core.enums import DBMS from lib.core.enums import DBMS
from lib.core.enums import DBMS_DIRECTORY_NAME from lib.core.enums import DBMS_DIRECTORY_NAME
from lib.core.enums import OS from lib.core.enums import OS
@ -427,6 +429,8 @@ HTML_TITLE_REGEX = "<title>(?P<result>[^<]+)</title>"
# Table used for Base64 conversion in WordPress hash cracking routine # Table used for Base64 conversion in WordPress hash cracking routine
ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
PICKLE_REDUCE_WHITELIST = (types.BooleanType, types.DictType, types.FloatType, types.IntType, types.ListType, types.LongType, types.NoneType, types.StringType, types.TupleType, types.UnicodeType, types.XRangeType, type(AttribDict()), type(set()))
# Chars used to quickly distinguish if the user provided tainted parameter values # Chars used to quickly distinguish if the user provided tainted parameter values
DUMMY_SQL_INJECTION_CHARS = ";()'" DUMMY_SQL_INJECTION_CHARS = ";()'"
@ -503,7 +507,7 @@ DEFAULT_COOKIE_DELIMITER = ';'
FORCE_COOKIE_EXPIRATION_TIME = "9999999999" FORCE_COOKIE_EXPIRATION_TIME = "9999999999"
# Github OAuth token used for creating an automatic Issue for unhandled exceptions # Github OAuth token used for creating an automatic Issue for unhandled exceptions
GITHUB_REPORT_OAUTH_TOKEN = "YzQzM2M2YzgzMDExN2I5ZDMyYjAzNTIzODIwZDA2MDFmMmVjODI1Ng==" GITHUB_REPORT_OAUTH_TOKEN = "YzNkYTgyMTdjYzdjNjZjMjFjMWE5ODI5OGQyNzk2ODM1M2M0MzUyOA=="
# Skip unforced HashDB flush requests below the threshold number of cached items # Skip unforced HashDB flush requests below the threshold number of cached items
HASHDB_FLUSH_THRESHOLD = 32 HASHDB_FLUSH_THRESHOLD = 32
@ -589,6 +593,12 @@ EVENTVALIDATION_REGEX = r'(?i)(?P<name>__EVENTVALIDATION[^"]*)[^>]+value="(?P<re
# Number of rows to generate inside the full union test for limited output (mustn't be too large to prevent payload length problems) # Number of rows to generate inside the full union test for limited output (mustn't be too large to prevent payload length problems)
LIMITED_ROWS_TEST_NUMBER = 15 LIMITED_ROWS_TEST_NUMBER = 15
# Default REST-JSON API server listen address
RESTAPI_DEFAULT_ADDRESS = "127.0.0.1"
# Default REST-JSON API server listen port
RESTAPI_DEFAULT_PORT = 8775
# Format used for representing invalid unicode characters # Format used for representing invalid unicode characters
INVALID_UNICODE_CHAR_FORMAT = r"\x%02x" INVALID_UNICODE_CHAR_FORMAT = r"\x%02x"

View File

@ -77,10 +77,6 @@ class Wordlist(object):
except StopIteration: except StopIteration:
self.adjust() self.adjust()
retVal = self.iter.next().rstrip() retVal = self.iter.next().rstrip()
try:
retVal = retVal.decode(UNICODE_ENCODING)
except UnicodeDecodeError:
continue
if not self.proc_count or self.counter % self.proc_count == self.proc_id: if not self.proc_count or self.counter % self.proc_count == self.proc_id:
break break
return retVal return retVal

View File

@ -754,6 +754,9 @@ def cmdLineParser(argv=None):
parser.add_option("--pickled-options", dest="pickledOptions", parser.add_option("--pickled-options", dest="pickledOptions",
help=SUPPRESS_HELP) help=SUPPRESS_HELP)
parser.add_option("--disable-precon", dest="disablePrecon", action="store_true",
help=SUPPRESS_HELP)
parser.add_option("--profile", dest="profile", action="store_true", parser.add_option("--profile", dest="profile", action="store_true",
help=SUPPRESS_HELP) help=SUPPRESS_HELP)
@ -780,9 +783,6 @@ def cmdLineParser(argv=None):
parser.add_option("--run-case", dest="runCase", help=SUPPRESS_HELP) parser.add_option("--run-case", dest="runCase", help=SUPPRESS_HELP)
parser.add_option("--nnc5ed", dest="nnc5ed", action="store_true",
help=SUPPRESS_HELP) # temporary hidden switch :)
parser.add_option_group(target) parser.add_option_group(target)
parser.add_option_group(request) parser.add_option_group(request)
parser.add_option_group(optimization) parser.add_option_group(optimization)

View File

@ -150,7 +150,7 @@ def checkCharEncoding(encoding, warn=True):
return encoding return encoding
# Reference: http://www.destructor.de/charsets/index.htm # Reference: http://www.destructor.de/charsets/index.htm
translate = {"windows-874": "iso-8859-11", "en_us": "utf8", "macintosh": "iso-8859-1", "euc_tw": "big5_tw", "th": "tis-620", "unicode": "utf8", "utc8": "utf8", "ebcdic": "ebcdic-cp-be", "iso-8859": "iso8859-1", "ansi": "ascii", "gbk2312": "gbk", "windows-31j": "cp932"} translate = {"windows-874": "iso-8859-11", "utf-8859-1": "utf8", "en_us": "utf8", "macintosh": "iso-8859-1", "euc_tw": "big5_tw", "th": "tis-620", "unicode": "utf8", "utc8": "utf8", "ebcdic": "ebcdic-cp-be", "iso-8859": "iso8859-1", "ansi": "ascii", "gbk2312": "gbk", "windows-31j": "cp932"}
for delimiter in (';', ',', '('): for delimiter in (';', ',', '('):
if delimiter in encoding: if delimiter in encoding:

View File

@ -343,6 +343,9 @@ class Connect(object):
# Prepare HTTP headers # Prepare HTTP headers
headers = forgeHeaders({HTTP_HEADER.COOKIE: cookie, HTTP_HEADER.USER_AGENT: ua, HTTP_HEADER.REFERER: referer, HTTP_HEADER.HOST: host}) headers = forgeHeaders({HTTP_HEADER.COOKIE: cookie, HTTP_HEADER.USER_AGENT: ua, HTTP_HEADER.REFERER: referer, HTTP_HEADER.HOST: host})
if HTTP_HEADER.COOKIE in headers:
cookie = headers[HTTP_HEADER.COOKIE]
if kb.authHeader: if kb.authHeader:
headers[HTTP_HEADER.AUTHORIZATION] = kb.authHeader headers[HTTP_HEADER.AUTHORIZATION] = kb.authHeader
@ -370,6 +373,12 @@ class Connect(object):
if boundary: if boundary:
headers[HTTP_HEADER.CONTENT_TYPE] = "%s; boundary=%s" % (headers[HTTP_HEADER.CONTENT_TYPE], boundary) headers[HTTP_HEADER.CONTENT_TYPE] = "%s; boundary=%s" % (headers[HTTP_HEADER.CONTENT_TYPE], boundary)
# Reset header values to original in case of provided request file
if target and conf.requestFile:
headers = OrderedDict(conf.httpHeaders)
if cookie:
headers[HTTP_HEADER.COOKIE] = cookie
if auxHeaders: if auxHeaders:
for key, value in auxHeaders.items(): for key, value in auxHeaders.items():
for _ in headers.keys(): for _ in headers.keys():

View File

@ -43,11 +43,14 @@ class HTTPSConnection(httplib.HTTPSConnection):
success = False success = False
if not kb.tlsSNI: # Reference(s): https://docs.python.org/2/library/ssl.html#ssl.SSLContext
for protocol in _protocols: # https://www.mnot.net/blog/2014/12/27/python_2_and_tls_sni
if kb.tlsSNI.get(self.host) != False and hasattr(ssl, "SSLContext"):
for protocol in filter(lambda _: _ >= ssl.PROTOCOL_TLSv1, _protocols):
try: try:
sock = create_sock() sock = create_sock()
_ = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=protocol) context = ssl.SSLContext(protocol)
_ = context.wrap_socket(sock, do_handshake_on_connect=True, server_hostname=self.host)
if _: if _:
success = True success = True
self.sock = _ self.sock = _
@ -60,16 +63,16 @@ class HTTPSConnection(httplib.HTTPSConnection):
self._tunnel_host = None self._tunnel_host = None
logger.debug("SSL connection error occurred ('%s')" % getSafeExString(ex)) logger.debug("SSL connection error occurred ('%s')" % getSafeExString(ex))
# Reference(s): https://docs.python.org/2/library/ssl.html#ssl.SSLContext if kb.tlsSNI.get(self.host) is None:
# https://www.mnot.net/blog/2014/12/27/python_2_and_tls_sni kb.tlsSNI[self.host] = success
if not success and hasattr(ssl, "SSLContext"):
for protocol in filter(lambda _: _ >= ssl.PROTOCOL_TLSv1, _protocols): if not success:
for protocol in _protocols:
try: try:
sock = create_sock() sock = create_sock()
context = ssl.SSLContext(protocol) _ = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=protocol)
_ = context.wrap_socket(sock, do_handshake_on_connect=False, server_hostname=self.host)
if _: if _:
kb.tlsSNI = success = True success = True
self.sock = _ self.sock = _
_protocols.remove(protocol) _protocols.remove(protocol)
_protocols.insert(0, protocol) _protocols.insert(0, protocol)

View File

@ -9,10 +9,9 @@ import os
import posixpath import posixpath
import re import re
import StringIO import StringIO
import tempfile
import urlparse import urlparse
from tempfile import mkstemp
from extra.cloak.cloak import decloak from extra.cloak.cloak import decloak
from lib.core.agent import agent from lib.core.agent import agent
from lib.core.common import arrayizeValue from lib.core.common import arrayizeValue
@ -257,10 +256,10 @@ class Web:
stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webApi) stagerName = "tmpu%s.%s" % (randomStr(lowercase=True), self.webApi)
self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName) self.webStagerFilePath = posixpath.join(ntToPosixSlashes(directory), stagerName)
handle, filename = mkstemp() handle, filename = tempfile.mkstemp()
os.fdopen(handle).close() # close low level handle (causing problems later) os.close(handle)
with open(filename, "w+") as f: with open(filename, "w+b") as f:
_ = decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stager.%s_" % self.webApi)) _ = decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stager.%s_" % self.webApi))
_ = _.replace("WRITABLE_DIR", utf8encode(directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory)) _ = _.replace("WRITABLE_DIR", utf8encode(directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory))
f.write(_) f.write(_)

View File

@ -56,7 +56,7 @@ from lib.utils.progress import ProgressBar
from thirdparty.odict.odict import OrderedDict from thirdparty.odict.odict import OrderedDict
def _oneShotUnionUse(expression, unpack=True, limited=False): def _oneShotUnionUse(expression, unpack=True, limited=False):
retVal = hashDBRetrieve("%s%s" % (conf.hexConvert, expression), checkConf=True) # as union data is stored raw unconverted retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True) # as union data is stored raw unconverted
threadData = getCurrentThreadData() threadData = getCurrentThreadData()
threadData.resumed = retVal is not None threadData.resumed = retVal is not None
@ -102,7 +102,7 @@ def _oneShotUnionUse(expression, unpack=True, limited=False):
if Backend.isDbms(DBMS.MSSQL) and wasLastResponseDBMSError(): if Backend.isDbms(DBMS.MSSQL) and wasLastResponseDBMSError():
retVal = htmlunescape(retVal).replace("<br>", "\n") retVal = htmlunescape(retVal).replace("<br>", "\n")
hashDBWrite("%s%s" % (conf.hexConvert, expression), retVal) hashDBWrite("%s%s" % (conf.hexConvert or False, expression), retVal)
else: else:
trimmed = _("%s(?P<result>.*?)<" % (kb.chars.start)) trimmed = _("%s(?P<result>.*?)<" % (kb.chars.start))
@ -111,6 +111,9 @@ def _oneShotUnionUse(expression, unpack=True, limited=False):
warnMsg += "(probably due to its length and/or content): " warnMsg += "(probably due to its length and/or content): "
warnMsg += safecharencode(trimmed) warnMsg += safecharencode(trimmed)
logger.warn(warnMsg) logger.warn(warnMsg)
else:
vector = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector
kb.unionDuplicates = vector[7]
return retVal return retVal

View File

@ -10,6 +10,7 @@ import logging
import os import os
import re import re
import shlex import shlex
import socket
import sqlite3 import sqlite3
import sys import sys
import tempfile import tempfile
@ -35,6 +36,8 @@ from lib.core.exception import SqlmapConnectionException
from lib.core.log import LOGGER_HANDLER from lib.core.log import LOGGER_HANDLER
from lib.core.optiondict import optDict from lib.core.optiondict import optDict
from lib.core.settings import IS_WIN from lib.core.settings import IS_WIN
from lib.core.settings import RESTAPI_DEFAULT_ADDRESS
from lib.core.settings import RESTAPI_DEFAULT_PORT
from lib.core.subprocessng import Popen from lib.core.subprocessng import Popen
from lib.parse.cmdline import cmdLineParser from lib.parse.cmdline import cmdLineParser
from thirdparty.bottle.bottle import error as return_error from thirdparty.bottle.bottle import error as return_error
@ -45,9 +48,6 @@ from thirdparty.bottle.bottle import request
from thirdparty.bottle.bottle import response from thirdparty.bottle.bottle import response
from thirdparty.bottle.bottle import run from thirdparty.bottle.bottle import run
RESTAPI_SERVER_HOST = "127.0.0.1"
RESTAPI_SERVER_PORT = 8775
# global settings # global settings
class DataStore(object): class DataStore(object):
@ -637,7 +637,7 @@ def download(taskid, target, filename):
return jsonize({"success": False, "message": "File does not exist"}) return jsonize({"success": False, "message": "File does not exist"})
def server(host="0.0.0.0", port=RESTAPI_SERVER_PORT): def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT):
""" """
REST-JSON API server REST-JSON API server
""" """
@ -654,7 +654,13 @@ def server(host="0.0.0.0", port=RESTAPI_SERVER_PORT):
DataStore.current_db.init() DataStore.current_db.init()
# Run RESTful API # Run RESTful API
try:
run(host=host, port=port, quiet=True, debug=False) run(host=host, port=port, quiet=True, debug=False)
except socket.error, ex:
if "already in use" in getSafeExString(ex):
logger.error("Address already in use ('%s:%s')" % (host, port))
else:
raise
def _client(url, options=None): def _client(url, options=None):
@ -673,7 +679,7 @@ def _client(url, options=None):
return text return text
def client(host=RESTAPI_SERVER_HOST, port=RESTAPI_SERVER_PORT): def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT):
""" """
REST-JSON API client REST-JSON API client
""" """

View File

@ -209,6 +209,9 @@ def oracle_old_passwd(password, username, uppercase=True): # prior to version '
if isinstance(username, unicode): if isinstance(username, unicode):
username = unicode.encode(username, UNICODE_ENCODING) # pyDes has issues with unicode strings username = unicode.encode(username, UNICODE_ENCODING) # pyDes has issues with unicode strings
if isinstance(password, unicode):
password = unicode.encode(password, UNICODE_ENCODING)
unistr = "".join("\0%s" % c for c in (username + password).upper()) unistr = "".join("\0%s" % c for c in (username + password).upper())
cipher = des(hexdecode("0123456789ABCDEF"), CBC, IV, pad) cipher = des(hexdecode("0123456789ABCDEF"), CBC, IV, pad)
@ -327,6 +330,7 @@ def wordpress_passwd(password, salt, count, prefix, uppercase=False):
return output return output
if isinstance(password, unicode):
password = password.encode(UNICODE_ENCODING) password = password.encode(UNICODE_ENCODING)
cipher = md5(salt) cipher = md5(salt)

View File

@ -64,15 +64,19 @@ def pivotDumpTable(table, colList, count=None, blind=True):
colList = filter(None, sorted(colList, key=lambda x: len(x) if x else MAX_INT)) colList = filter(None, sorted(colList, key=lambda x: len(x) if x else MAX_INT))
if conf.pivotColumn: if conf.pivotColumn:
if any(re.search(r"(.+\.)?%s" % re.escape(conf.pivotColumn), _, re.I) for _ in colList): for _ in colList:
if re.search(r"(.+\.)?%s" % re.escape(conf.pivotColumn), _, re.I):
infoMsg = "using column '%s' as a pivot " % conf.pivotColumn infoMsg = "using column '%s' as a pivot " % conf.pivotColumn
infoMsg += "for retrieving row data" infoMsg += "for retrieving row data"
logger.info(infoMsg) logger.info(infoMsg)
colList.remove(_)
colList.insert(0, _)
validPivotValue = True validPivotValue = True
colList.remove(conf.pivotColumn) break
colList.insert(0, conf.pivotColumn)
else: if not validPivotValue:
warnMsg = "column '%s' not " % conf.pivotColumn warnMsg = "column '%s' not " % conf.pivotColumn
warnMsg += "found in table '%s'" % table warnMsg += "found in table '%s'" % table
logger.warn(warnMsg) logger.warn(warnMsg)

View File

@ -14,12 +14,11 @@ from sqlmap import modulePath
from lib.core.common import setPaths from lib.core.common import setPaths
from lib.core.data import paths from lib.core.data import paths
from lib.core.data import logger from lib.core.data import logger
from lib.core.settings import RESTAPI_DEFAULT_ADDRESS
from lib.core.settings import RESTAPI_DEFAULT_PORT
from lib.utils.api import client from lib.utils.api import client
from lib.utils.api import server from lib.utils.api import server
RESTAPI_SERVER_HOST = "127.0.0.1"
RESTAPI_SERVER_PORT = 8775
if __name__ == "__main__": if __name__ == "__main__":
""" """
REST-JSON API main function REST-JSON API main function
@ -33,10 +32,10 @@ if __name__ == "__main__":
# Parse command line options # Parse command line options
apiparser = optparse.OptionParser() apiparser = optparse.OptionParser()
apiparser.add_option("-s", "--server", help="Act as a REST-JSON API server", default=RESTAPI_SERVER_PORT, action="store_true") apiparser.add_option("-s", "--server", help="Act as a REST-JSON API server", default=RESTAPI_DEFAULT_PORT, action="store_true")
apiparser.add_option("-c", "--client", help="Act as a REST-JSON API client", default=RESTAPI_SERVER_PORT, action="store_true") apiparser.add_option("-c", "--client", help="Act as a REST-JSON API client", default=RESTAPI_DEFAULT_PORT, action="store_true")
apiparser.add_option("-H", "--host", help="Host of the REST-JSON API server", default=RESTAPI_SERVER_HOST, action="store") apiparser.add_option("-H", "--host", help="Host of the REST-JSON API server", default=RESTAPI_DEFAULT_ADDRESS, action="store")
apiparser.add_option("-p", "--port", help="Port of the the REST-JSON API server", default=RESTAPI_SERVER_PORT, type="int", action="store") apiparser.add_option("-p", "--port", help="Port of the the REST-JSON API server", default=RESTAPI_DEFAULT_PORT, type="int", action="store")
(args, _) = apiparser.parse_args() (args, _) = apiparser.parse_args()
# Start the client or the server # Start the client or the server

View File

@ -16,11 +16,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
'''Visualize dot graphs via the xdot Format.''' '''Visualize dot graphs via the xdot format.'''
__author__ = "Jose Fonseca" __author__ = "Jose Fonseca et al"
__version__ = "0.4"
import os import os
@ -30,6 +28,7 @@ import math
import colorsys import colorsys
import time import time
import re import re
import optparse
import gobject import gobject
import gtk import gtk
@ -90,13 +89,12 @@ class Shape:
else: else:
return self.pen return self.pen
def search_text(self, regexp):
return False
class TextShape(Shape): class TextShape(Shape):
#fontmap = pangocairo.CairoFontMap()
#fontmap.set_resolution(72)
#context = fontmap.create_context()
LEFT, CENTER, RIGHT = -1, 0, 1 LEFT, CENTER, RIGHT = -1, 0, 1
def __init__(self, pen, x, y, j, w, t): def __init__(self, pen, x, y, j, w, t):
@ -191,6 +189,33 @@ class TextShape(Shape):
cr.line_to(x+self.w, self.y) cr.line_to(x+self.w, self.y)
cr.stroke() cr.stroke()
def search_text(self, regexp):
return regexp.search(self.t) is not None
class ImageShape(Shape):
def __init__(self, pen, x0, y0, w, h, path):
Shape.__init__(self)
self.pen = pen.copy()
self.x0 = x0
self.y0 = y0
self.w = w
self.h = h
self.path = path
def draw(self, cr, highlight=False):
cr2 = gtk.gdk.CairoContext(cr)
pixbuf = gtk.gdk.pixbuf_new_from_file(self.path)
sx = float(self.w)/float(pixbuf.get_width())
sy = float(self.h)/float(pixbuf.get_height())
cr.save()
cr.translate(self.x0, self.y0 - self.h)
cr.scale(sx, sy)
cr2.set_source_pixbuf(pixbuf, 0, 0)
cr2.paint()
cr.restore()
class EllipseShape(Shape): class EllipseShape(Shape):
@ -304,6 +329,12 @@ class CompoundShape(Shape):
for shape in self.shapes: for shape in self.shapes:
shape.draw(cr, highlight=highlight) shape.draw(cr, highlight=highlight)
def search_text(self, regexp):
for shape in self.shapes:
if shape.search_text(regexp):
return True
return False
class Url(object): class Url(object):
@ -332,6 +363,9 @@ class Element(CompoundShape):
def __init__(self, shapes): def __init__(self, shapes):
CompoundShape.__init__(self, shapes) CompoundShape.__init__(self, shapes)
def is_inside(self, x, y):
return False
def get_url(self, x, y): def get_url(self, x, y):
return None return None
@ -341,9 +375,10 @@ class Element(CompoundShape):
class Node(Element): class Node(Element):
def __init__(self, x, y, w, h, shapes, url): def __init__(self, id, x, y, w, h, shapes, url):
Element.__init__(self, shapes) Element.__init__(self, shapes)
self.id = id
self.x = x self.x = x
self.y = y self.y = y
@ -360,7 +395,6 @@ class Node(Element):
def get_url(self, x, y): def get_url(self, x, y):
if self.url is None: if self.url is None:
return None return None
#print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2)
if self.is_inside(x, y): if self.is_inside(x, y):
return Url(self, self.url) return Url(self, self.url)
return None return None
@ -370,6 +404,9 @@ class Node(Element):
return Jump(self, self.x, self.y) return Jump(self, self.x, self.y)
return None return None
def __repr__(self):
return "<Node %s>" % self.id
def square_distance(x1, y1, x2, y2): def square_distance(x1, y1, x2, y2):
deltax = x2 - x1 deltax = x2 - x1
@ -387,13 +424,29 @@ class Edge(Element):
RADIUS = 10 RADIUS = 10
def is_inside_begin(self, x, y):
return square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS
def is_inside_end(self, x, y):
return square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS
def is_inside(self, x, y):
if self.is_inside_begin(x, y):
return True
if self.is_inside_end(x, y):
return True
return False
def get_jump(self, x, y): def get_jump(self, x, y):
if square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS: if self.is_inside_begin(x, y):
return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst])) return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst]))
if square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS: if self.is_inside_end(x, y):
return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src])) return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src]))
return None return None
def __repr__(self):
return "<Edge %s -> %s>" % (self.src, self.dst)
class Graph(Shape): class Graph(Shape):
@ -424,6 +477,14 @@ class Graph(Shape):
for node in self.nodes: for node in self.nodes:
node.draw(cr, highlight=(node in highlight_items)) node.draw(cr, highlight=(node in highlight_items))
def get_element(self, x, y):
for node in self.nodes:
if node.is_inside(x, y):
return node
for edge in self.edges:
if edge.is_inside(x, y):
return edge
def get_url(self, x, y): def get_url(self, x, y):
for node in self.nodes: for node in self.nodes:
url = node.get_url(x, y) url = node.get_url(x, y)
@ -443,6 +504,14 @@ class Graph(Shape):
return None return None
BOLD = 1
ITALIC = 2
UNDERLINE = 4
SUPERSCRIPT = 8
SUBSCRIPT = 16
STRIKE_THROUGH = 32
class XDotAttrParser: class XDotAttrParser:
"""Parser for xdot drawing attributes. """Parser for xdot drawing attributes.
See also: See also:
@ -451,7 +520,7 @@ class XDotAttrParser:
def __init__(self, parser, buf): def __init__(self, parser, buf):
self.parser = parser self.parser = parser
self.buf = self.unescape(buf) self.buf = buf
self.pos = 0 self.pos = 0
self.pen = Pen() self.pen = Pen()
@ -460,11 +529,6 @@ class XDotAttrParser:
def __nonzero__(self): def __nonzero__(self):
return self.pos < len(self.buf) return self.pos < len(self.buf)
def unescape(self, buf):
buf = buf.replace('\\"', '"')
buf = buf.replace('\\n', '\n')
return buf
def read_code(self): def read_code(self):
pos = self.buf.find(" ", self.pos) pos = self.buf.find(" ", self.pos)
res = self.buf[self.pos:pos] res = self.buf[self.pos:pos]
@ -473,19 +537,19 @@ class XDotAttrParser:
self.pos += 1 self.pos += 1
return res return res
def read_number(self): def read_int(self):
return int(self.read_code()) return int(self.read_code())
def read_float(self): def read_float(self):
return float(self.read_code()) return float(self.read_code())
def read_point(self): def read_point(self):
x = self.read_number() x = self.read_float()
y = self.read_number() y = self.read_float()
return self.transform(x, y) return self.transform(x, y)
def read_text(self): def read_text(self):
num = self.read_number() num = self.read_int()
pos = self.buf.find("-", self.pos) + 1 pos = self.buf.find("-", self.pos) + 1
self.pos = pos + num self.pos = pos + num
res = self.buf[pos:self.pos] res = self.buf[pos:self.pos]
@ -494,7 +558,7 @@ class XDotAttrParser:
return res return res
def read_polygon(self): def read_polygon(self):
n = self.read_number() n = self.read_int()
p = [] p = []
for i in range(n): for i in range(n):
x, y = self.read_point() x, y = self.read_point()
@ -521,6 +585,9 @@ class XDotAttrParser:
r, g, b = colorsys.hsv_to_rgb(h, s, v) r, g, b = colorsys.hsv_to_rgb(h, s, v)
a = 1.0 a = 1.0
return r, g, b, a return r, g, b, a
elif c1 == "[":
sys.stderr.write('warning: color gradients not supported yet\n')
return None
else: else:
return self.lookup_color(c) return self.lookup_color(c)
@ -550,7 +617,7 @@ class XDotAttrParser:
a = 1.0 a = 1.0
return r, g, b, a return r, g, b, a
sys.stderr.write("unknown color '%s'\n" % c) sys.stderr.write("warning: unknown color '%s'\n" % c)
return None return None
def parse(self): def parse(self):
@ -573,7 +640,7 @@ class XDotAttrParser:
lw = style.split("(")[1].split(")")[0] lw = style.split("(")[1].split(")")[0]
lw = float(lw) lw = float(lw)
self.handle_linewidth(lw) self.handle_linewidth(lw)
elif style in ("solid", "dashed"): elif style in ("solid", "dashed", "dotted"):
self.handle_linestyle(style) self.handle_linestyle(style)
elif op == "F": elif op == "F":
size = s.read_float() size = s.read_float()
@ -581,19 +648,22 @@ class XDotAttrParser:
self.handle_font(size, name) self.handle_font(size, name)
elif op == "T": elif op == "T":
x, y = s.read_point() x, y = s.read_point()
j = s.read_number() j = s.read_int()
w = s.read_number() w = s.read_float()
t = s.read_text() t = s.read_text()
self.handle_text(x, y, j, w, t) self.handle_text(x, y, j, w, t)
elif op == "t":
f = s.read_int()
self.handle_font_characteristics(f)
elif op == "E": elif op == "E":
x0, y0 = s.read_point() x0, y0 = s.read_point()
w = s.read_number() w = s.read_float()
h = s.read_number() h = s.read_float()
self.handle_ellipse(x0, y0, w, h, filled=True) self.handle_ellipse(x0, y0, w, h, filled=True)
elif op == "e": elif op == "e":
x0, y0 = s.read_point() x0, y0 = s.read_point()
w = s.read_number() w = s.read_float()
h = s.read_number() h = s.read_float()
self.handle_ellipse(x0, y0, w, h, filled=False) self.handle_ellipse(x0, y0, w, h, filled=False)
elif op == "L": elif op == "L":
points = self.read_polygon() points = self.read_polygon()
@ -610,9 +680,15 @@ class XDotAttrParser:
elif op == "p": elif op == "p":
points = self.read_polygon() points = self.read_polygon()
self.handle_polygon(points, filled=False) self.handle_polygon(points, filled=False)
elif op == "I":
x0, y0 = s.read_point()
w = s.read_float()
h = s.read_float()
path = s.read_text()
self.handle_image(x0, y0, w, h, path)
else: else:
sys.stderr.write("unknown xdot opcode '%s'\n" % op) sys.stderr.write("error: unknown xdot opcode '%s'\n" % op)
break sys.exit(1)
return self.shapes return self.shapes
@ -633,11 +709,18 @@ class XDotAttrParser:
self.pen.dash = () self.pen.dash = ()
elif style == "dashed": elif style == "dashed":
self.pen.dash = (6, ) # 6pt on, 6pt off self.pen.dash = (6, ) # 6pt on, 6pt off
elif style == "dotted":
self.pen.dash = (2, 4) # 2pt on, 4pt off
def handle_font(self, size, name): def handle_font(self, size, name):
self.pen.fontsize = size self.pen.fontsize = size
self.pen.fontname = name self.pen.fontname = name
def handle_font_characteristics(self, flags):
# TODO
if flags != 0:
sys.stderr.write("warning: font characteristics not supported yet\n" % op)
def handle_text(self, x, y, j, w, t): def handle_text(self, x, y, j, w, t):
self.shapes.append(TextShape(self.pen, x, y, j, w, t)) self.shapes.append(TextShape(self.pen, x, y, j, w, t))
@ -647,6 +730,9 @@ class XDotAttrParser:
self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True)) self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True))
self.shapes.append(EllipseShape(self.pen, x0, y0, w, h)) self.shapes.append(EllipseShape(self.pen, x0, y0, w, h))
def handle_image(self, x0, y0, w, h, path):
self.shapes.append(ImageShape(self.pen, x0, y0, w, h, path))
def handle_line(self, points): def handle_line(self, points):
self.shapes.append(LineShape(self.pen, points)) self.shapes.append(LineShape(self.pen, points))
@ -922,10 +1008,11 @@ class DotLexer(Lexer):
text = text.replace('\\\r', '') text = text.replace('\\\r', '')
text = text.replace('\\\n', '') text = text.replace('\\\n', '')
text = text.replace('\\r', '\r') # quotes
text = text.replace('\\n', '\n') text = text.replace('\\"', '"')
text = text.replace('\\t', '\t')
text = text.replace('\\', '') # layout engines recognize other escape codes (many non-standard)
# but we don't translate them here
type = ID type = ID
@ -1059,6 +1146,8 @@ class DotParser(Parser):
class XDotParser(DotParser): class XDotParser(DotParser):
XDOTVERSION = '1.6'
def __init__(self, xdotcode): def __init__(self, xdotcode):
lexer = DotLexer(buf = xdotcode) lexer = DotLexer(buf = xdotcode)
DotParser.__init__(self, lexer) DotParser.__init__(self, lexer)
@ -1071,14 +1160,22 @@ class XDotParser(DotParser):
def handle_graph(self, attrs): def handle_graph(self, attrs):
if self.top_graph: if self.top_graph:
# Check xdot version
try:
xdotversion = attrs['xdotversion']
except KeyError:
pass
else:
if float(xdotversion) > float(self.XDOTVERSION):
sys.stderr.write('warning: xdot version %s, but supported is %s\n' % (xdotversion, self.XDOTVERSION))
# Parse bounding box
try: try:
bb = attrs['bb'] bb = attrs['bb']
except KeyError: except KeyError:
return return
if not bb: if bb:
return
xmin, ymin, xmax, ymax = map(float, bb.split(",")) xmin, ymin, xmax, ymax = map(float, bb.split(","))
self.xoffset = -xmin self.xoffset = -xmin
@ -1087,8 +1184,8 @@ class XDotParser(DotParser):
self.yscale = -1.0 self.yscale = -1.0
# FIXME: scale from points to pixels # FIXME: scale from points to pixels
self.width = xmax - xmin self.width = max(xmax - xmin, 1)
self.height = ymax - ymin self.height = max(ymax - ymin, 1)
self.top_graph = False self.top_graph = False
@ -1104,15 +1201,15 @@ class XDotParser(DotParser):
return return
x, y = self.parse_node_pos(pos) x, y = self.parse_node_pos(pos)
w = float(attrs['width'])*72 w = float(attrs.get('width', 0))*72
h = float(attrs['height'])*72 h = float(attrs.get('height', 0))*72
shapes = [] shapes = []
for attr in ("_draw_", "_ldraw_"): for attr in ("_draw_", "_ldraw_"):
if attr in attrs: if attr in attrs:
parser = XDotAttrParser(self, attrs[attr]) parser = XDotAttrParser(self, attrs[attr])
shapes.extend(parser.parse()) shapes.extend(parser.parse())
url = attrs.get('URL', None) url = attrs.get('URL', None)
node = Node(x, y, w, h, shapes, url) node = Node(id, x, y, w, h, shapes, url)
self.node_by_name[id] = node self.node_by_name[id] = node
if shapes: if shapes:
self.nodes.append(node) self.nodes.append(node)
@ -1399,6 +1496,9 @@ class DotWidget(gtk.DrawingArea):
self.connect("size-allocate", self.on_area_size_allocate) self.connect("size-allocate", self.on_area_size_allocate)
self.connect('key-press-event', self.on_key_press_event) self.connect('key-press-event', self.on_key_press_event)
self.last_mtime = None
gobject.timeout_add(1000, self.update)
self.x, self.y = 0.0, 0.0 self.x, self.y = 0.0, 0.0
self.zoom_ratio = 1.0 self.zoom_ratio = 1.0
@ -1411,9 +1511,9 @@ class DotWidget(gtk.DrawingArea):
def set_filter(self, filter): def set_filter(self, filter):
self.filter = filter self.filter = filter
def set_dotcode(self, dotcode, filename='<stdin>'): def run_filter(self, dotcode):
if isinstance(dotcode, unicode): if not self.filter:
dotcode = dotcode.encode('utf8') return dotcode
p = subprocess.Popen( p = subprocess.Popen(
[self.filter, '-Txdot'], [self.filter, '-Txdot'],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
@ -1423,6 +1523,7 @@ class DotWidget(gtk.DrawingArea):
universal_newlines=True universal_newlines=True
) )
xdotcode, error = p.communicate(dotcode) xdotcode, error = p.communicate(dotcode)
sys.stderr.write(error)
if p.returncode != 0: if p.returncode != 0:
dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=error, message_format=error,
@ -1430,10 +1531,19 @@ class DotWidget(gtk.DrawingArea):
dialog.set_title('Dot Viewer') dialog.set_title('Dot Viewer')
dialog.run() dialog.run()
dialog.destroy() dialog.destroy()
return None
return xdotcode
def set_dotcode(self, dotcode, filename=None):
self.openfilename = None
if isinstance(dotcode, unicode):
dotcode = dotcode.encode('utf8')
xdotcode = self.run_filter(dotcode)
if xdotcode is None:
return False return False
try: try:
self.set_xdotcode(xdotcode) self.set_xdotcode(xdotcode)
except ParseError, ex: except ParseError as ex:
dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=str(ex), message_format=str(ex),
buttons=gtk.BUTTONS_OK) buttons=gtk.BUTTONS_OK)
@ -1442,11 +1552,14 @@ class DotWidget(gtk.DrawingArea):
dialog.destroy() dialog.destroy()
return False return False
else: else:
if filename is None:
self.last_mtime = None
else:
self.last_mtime = os.stat(filename).st_mtime
self.openfilename = filename self.openfilename = filename
return True return True
def set_xdotcode(self, xdotcode): def set_xdotcode(self, xdotcode):
#print xdotcode
parser = XDotParser(xdotcode) parser = XDotParser(xdotcode)
self.graph = parser.parse() self.graph = parser.parse()
self.zoom_image(self.zoom_ratio, center=True) self.zoom_image(self.zoom_ratio, center=True)
@ -1460,6 +1573,14 @@ class DotWidget(gtk.DrawingArea):
except IOError: except IOError:
pass pass
def update(self):
if self.openfilename is not None:
current_mtime = os.stat(self.openfilename).st_mtime
if current_mtime != self.last_mtime:
self.last_mtime = current_mtime
self.reload()
return True
def do_expose_event(self, event): def do_expose_event(self, event):
cr = self.window.cairo_create() cr = self.window.cairo_create()
@ -1500,6 +1621,10 @@ class DotWidget(gtk.DrawingArea):
self.queue_draw() self.queue_draw()
def zoom_image(self, zoom_ratio, center=False, pos=None): def zoom_image(self, zoom_ratio, center=False, pos=None):
# Constrain zoom ratio to a sane range to prevent numeric instability.
zoom_ratio = min(zoom_ratio, 1E4)
zoom_ratio = max(zoom_ratio, 1E-6)
if center: if center:
self.x = self.graph.width/2 self.x = self.graph.width/2
self.y = self.graph.height/2 self.y = self.graph.height/2
@ -1518,6 +1643,9 @@ class DotWidget(gtk.DrawingArea):
rect = self.get_allocation() rect = self.get_allocation()
width = abs(x1 - x2) width = abs(x1 - x2)
height = abs(y1 - y2) height = abs(y1 - y2)
if width == 0 and height == 0:
self.zoom_ratio *= self.ZOOM_INCREMENT
else:
self.zoom_ratio = min( self.zoom_ratio = min(
float(rect.width)/float(width), float(rect.width)/float(width),
float(rect.height)/float(height) float(rect.height)/float(height)
@ -1574,11 +1702,16 @@ class DotWidget(gtk.DrawingArea):
self.y += self.POS_INCREMENT/self.zoom_ratio self.y += self.POS_INCREMENT/self.zoom_ratio
self.queue_draw() self.queue_draw()
return True return True
if event.keyval == gtk.keysyms.Page_Up: if event.keyval in (gtk.keysyms.Page_Up,
gtk.keysyms.plus,
gtk.keysyms.equal,
gtk.keysyms.KP_Add):
self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT) self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
self.queue_draw() self.queue_draw()
return True return True
if event.keyval == gtk.keysyms.Page_Down: if event.keyval in (gtk.keysyms.Page_Down,
gtk.keysyms.minus,
gtk.keysyms.KP_Subtract):
self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT) self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
self.queue_draw() self.queue_draw()
return True return True
@ -1589,11 +1722,49 @@ class DotWidget(gtk.DrawingArea):
if event.keyval == gtk.keysyms.r: if event.keyval == gtk.keysyms.r:
self.reload() self.reload()
return True return True
if event.keyval == gtk.keysyms.f:
win = widget.get_toplevel()
find_toolitem = win.uimanager.get_widget('/ToolBar/Find')
textentry = find_toolitem.get_children()
win.set_focus(textentry[0])
return True
if event.keyval == gtk.keysyms.q: if event.keyval == gtk.keysyms.q:
gtk.main_quit() gtk.main_quit()
return True return True
if event.keyval == gtk.keysyms.p:
self.on_print()
return True
return False return False
print_settings = None
def on_print(self, action=None):
print_op = gtk.PrintOperation()
if self.print_settings != None:
print_op.set_print_settings(self.print_settings)
print_op.connect("begin_print", self.begin_print)
print_op.connect("draw_page", self.draw_page)
res = print_op.run(gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG, self.parent.parent)
if res == gtk.PRINT_OPERATION_RESULT_APPLY:
print_settings = print_op.get_print_settings()
def begin_print(self, operation, context):
operation.set_n_pages(1)
return True
def draw_page(self, operation, context, page_nr):
cr = context.get_cairo_context()
rect = self.get_allocation()
cr.translate(0.5*rect.width, 0.5*rect.height)
cr.scale(self.zoom_ratio, self.zoom_ratio)
cr.translate(-self.x, -self.y)
self.graph.draw(cr, highlight_items=self.highlight)
def get_drag_action(self, event): def get_drag_action(self, event):
state = event.state state = event.state
if event.button in (1, 2): # left or middle button if event.button in (1, 2): # left or middle button
@ -1628,11 +1799,22 @@ class DotWidget(gtk.DrawingArea):
return (time.time() < self.presstime + click_timeout return (time.time() < self.presstime + click_timeout
and math.hypot(deltax, deltay) < click_fuzz) and math.hypot(deltax, deltay) < click_fuzz)
def on_click(self, element, event):
"""Override this method in subclass to process
click events. Note that element can be None
(click on empty space)."""
return False
def on_area_button_release(self, area, event): def on_area_button_release(self, area, event):
self.drag_action.on_button_release(event) self.drag_action.on_button_release(event)
self.drag_action = NullAction(self) self.drag_action = NullAction(self)
if event.button == 1 and self.is_click(event):
x, y = int(event.x), int(event.y) x, y = int(event.x), int(event.y)
if self.is_click(event):
el = self.get_element(x, y)
if self.on_click(el, event):
return True
if event.button == 1:
url = self.get_url(x, y) url = self.get_url(x, y)
if url is not None: if url is not None:
self.emit('clicked', unicode(url.url), event) self.emit('clicked', unicode(url.url), event)
@ -1642,6 +1824,7 @@ class DotWidget(gtk.DrawingArea):
self.animate_to(jump.x, jump.y) self.animate_to(jump.x, jump.y)
return True return True
if event.button == 1 or event.button == 2: if event.button == 1 or event.button == 2:
return True return True
return False return False
@ -1679,6 +1862,10 @@ class DotWidget(gtk.DrawingArea):
y += self.y y += self.y
return x, y return x, y
def get_element(self, x, y):
x, y = self.window2graph(x, y)
return self.graph.get_element(x, y)
def get_url(self, x, y): def get_url(self, x, y):
x, y = self.window2graph(x, y) x, y = self.window2graph(x, y)
return self.graph.get_url(x, y) return self.graph.get_url(x, y)
@ -1688,6 +1875,14 @@ class DotWidget(gtk.DrawingArea):
return self.graph.get_jump(x, y) return self.graph.get_jump(x, y)
class FindMenuToolAction(gtk.Action):
__gtype_name__ = "FindMenuToolAction"
def __init__(self, *args, **kw):
gtk.Action.__init__(self, *args, **kw)
self.set_tool_item_type(gtk.ToolItem)
class DotWindow(gtk.Window): class DotWindow(gtk.Window):
ui = ''' ui = '''
@ -1695,28 +1890,33 @@ class DotWindow(gtk.Window):
<toolbar name="ToolBar"> <toolbar name="ToolBar">
<toolitem action="Open"/> <toolitem action="Open"/>
<toolitem action="Reload"/> <toolitem action="Reload"/>
<toolitem action="Print"/>
<separator/> <separator/>
<toolitem action="ZoomIn"/> <toolitem action="ZoomIn"/>
<toolitem action="ZoomOut"/> <toolitem action="ZoomOut"/>
<toolitem action="ZoomFit"/> <toolitem action="ZoomFit"/>
<toolitem action="Zoom100"/> <toolitem action="Zoom100"/>
<separator/>
<toolitem name="Find" action="Find"/>
</toolbar> </toolbar>
</ui> </ui>
''' '''
def __init__(self): base_title = 'Dot Viewer'
def __init__(self, widget=None):
gtk.Window.__init__(self) gtk.Window.__init__(self)
self.graph = Graph() self.graph = Graph()
window = self window = self
window.set_title('Dot Viewer') window.set_title(self.base_title)
window.set_default_size(512, 512) window.set_default_size(512, 512)
vbox = gtk.VBox() vbox = gtk.VBox()
window.add(vbox) window.add(vbox)
self.widget = DotWidget() self.widget = widget or DotWidget()
# Create a UIManager instance # Create a UIManager instance
uimanager = self.uimanager = gtk.UIManager() uimanager = self.uimanager = gtk.UIManager()
@ -1733,12 +1933,17 @@ class DotWindow(gtk.Window):
actiongroup.add_actions(( actiongroup.add_actions((
('Open', gtk.STOCK_OPEN, None, None, None, self.on_open), ('Open', gtk.STOCK_OPEN, None, None, None, self.on_open),
('Reload', gtk.STOCK_REFRESH, None, None, None, self.on_reload), ('Reload', gtk.STOCK_REFRESH, None, None, None, self.on_reload),
('Print', gtk.STOCK_PRINT, None, None, "Prints the currently visible part of the graph", self.widget.on_print),
('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in), ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in),
('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out), ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out),
('ZoomFit', gtk.STOCK_ZOOM_FIT, None, None, None, self.widget.on_zoom_fit), ('ZoomFit', gtk.STOCK_ZOOM_FIT, None, None, None, self.widget.on_zoom_fit),
('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100), ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100),
)) ))
find_action = FindMenuToolAction("Find", None,
"Find a node by name", None)
actiongroup.add_action(find_action)
# Add the actiongroup to the uimanager # Add the actiongroup to the uimanager
uimanager.insert_action_group(actiongroup, 0) uimanager.insert_action_group(actiongroup, 0)
@ -1751,45 +1956,82 @@ class DotWindow(gtk.Window):
vbox.pack_start(self.widget) vbox.pack_start(self.widget)
self.last_open_dir = "."
self.set_focus(self.widget) self.set_focus(self.widget)
# Add Find text search
find_toolitem = uimanager.get_widget('/ToolBar/Find')
self.textentry = gtk.Entry(max=20)
self.textentry.set_icon_from_stock(0, gtk.STOCK_FIND)
find_toolitem.add(self.textentry)
self.textentry.set_activates_default(True)
self.textentry.connect ("activate", self.textentry_activate, self.textentry);
self.textentry.connect ("changed", self.textentry_changed, self.textentry);
self.show_all() self.show_all()
def update(self, filename): def find_text(self, entry_text):
import os found_items = []
if not hasattr(self, "last_mtime"): dot_widget = self.widget
self.last_mtime = None regexp = re.compile(entry_text)
for node in dot_widget.graph.nodes:
if node.search_text(regexp):
found_items.append(node)
return found_items
current_mtime = os.stat(filename).st_mtime def textentry_changed(self, widget, entry):
if current_mtime != self.last_mtime: entry_text = entry.get_text()
self.last_mtime = current_mtime dot_widget = self.widget
self.open_file(filename) if not entry_text:
dot_widget.set_highlight(None)
return
return True found_items = self.find_text(entry_text)
dot_widget.set_highlight(found_items)
def textentry_activate(self, widget, entry):
entry_text = entry.get_text()
dot_widget = self.widget
if not entry_text:
dot_widget.set_highlight(None)
return;
found_items = self.find_text(entry_text)
dot_widget.set_highlight(found_items)
if(len(found_items) == 1):
dot_widget.animate_to(found_items[0].x, found_items[0].y)
def set_filter(self, filter): def set_filter(self, filter):
self.widget.set_filter(filter) self.widget.set_filter(filter)
def set_dotcode(self, dotcode, filename='<stdin>'): def set_dotcode(self, dotcode, filename=None):
if self.widget.set_dotcode(dotcode, filename): if self.widget.set_dotcode(dotcode, filename):
self.set_title(os.path.basename(filename) + ' - Dot Viewer') self.update_title(filename)
self.widget.zoom_to_fit() self.widget.zoom_to_fit()
def set_xdotcode(self, xdotcode, filename='<stdin>'): def set_xdotcode(self, xdotcode, filename=None):
if self.widget.set_xdotcode(xdotcode): if self.widget.set_xdotcode(xdotcode):
self.set_title(os.path.basename(filename) + ' - Dot Viewer') self.update_title(filename)
self.widget.zoom_to_fit() self.widget.zoom_to_fit()
def update_title(self, filename=None):
if filename is None:
self.set_title(self.base_title)
else:
self.set_title(os.path.basename(filename) + ' - ' + self.base_title)
def open_file(self, filename): def open_file(self, filename):
try: try:
fp = file(filename, 'rt') fp = file(filename, 'rt')
self.set_dotcode(fp.read(), filename) self.set_dotcode(fp.read(), filename)
fp.close() fp.close()
except IOError, ex: except IOError as ex:
dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
message_format=str(ex), message_format=str(ex),
buttons=gtk.BUTTONS_OK) buttons=gtk.BUTTONS_OK)
dlg.set_title('Dot Viewer') dlg.set_title(self.base_title)
dlg.run() dlg.run()
dlg.destroy() dlg.destroy()
@ -1801,6 +2043,7 @@ class DotWindow(gtk.Window):
gtk.STOCK_OPEN, gtk.STOCK_OPEN,
gtk.RESPONSE_OK)) gtk.RESPONSE_OK))
chooser.set_default_response(gtk.RESPONSE_OK) chooser.set_default_response(gtk.RESPONSE_OK)
chooser.set_current_folder(self.last_open_dir)
filter = gtk.FileFilter() filter = gtk.FileFilter()
filter.set_name("Graphviz dot files") filter.set_name("Graphviz dot files")
filter.add_pattern("*.dot") filter.add_pattern("*.dot")
@ -1811,6 +2054,7 @@ class DotWindow(gtk.Window):
chooser.add_filter(filter) chooser.add_filter(filter)
if chooser.run() == gtk.RESPONSE_OK: if chooser.run() == gtk.RESPONSE_OK:
filename = chooser.get_filename() filename = chooser.get_filename()
self.last_open_dir = chooser.get_current_folder()
chooser.destroy() chooser.destroy()
self.open_file(filename) self.open_file(filename)
else: else:
@ -1820,17 +2064,41 @@ class DotWindow(gtk.Window):
self.widget.reload() self.widget.reload()
def main(): class OptionParser(optparse.OptionParser):
import optparse
parser = optparse.OptionParser( def format_epilog(self, formatter):
# Prevent stripping the newlines in epilog message
# http://stackoverflow.com/questions/1857346/python-optparse-how-to-include-additional-info-in-usage-output
return self.epilog
def main():
parser = OptionParser(
usage='\n\t%prog [file]', usage='\n\t%prog [file]',
version='%%prog %s' % __version__) epilog='''
Shortcuts:
Up, Down, Left, Right scroll
PageUp, +, = zoom in
PageDown, - zoom out
R reload dot file
F find
Q quit
P print
Escape halt animation
Ctrl-drag zoom in/out
Shift-drag zooms an area
'''
)
parser.add_option( parser.add_option(
'-f', '--filter', '-f', '--filter',
type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'), type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'),
dest='filter', default='dot', dest='filter', default='dot',
help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]') help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]')
parser.add_option(
'-n', '--no-filter',
action='store_const', const=None, dest='filter',
help='assume input is already filtered into xdot format (use e.g. dot -Txdot)')
(options, args) = parser.parse_args(sys.argv[1:]) (options, args) = parser.parse_args(sys.argv[1:])
if len(args) > 1: if len(args) > 1:
@ -1839,12 +2107,14 @@ def main():
win = DotWindow() win = DotWindow()
win.connect('destroy', gtk.main_quit) win.connect('destroy', gtk.main_quit)
win.set_filter(options.filter) win.set_filter(options.filter)
if len(args) >= 1: if len(args) == 0:
if not sys.stdin.isatty():
win.set_dotcode(sys.stdin.read())
else:
if args[0] == '-': if args[0] == '-':
win.set_dotcode(sys.stdin.read()) win.set_dotcode(sys.stdin.read())
else: else:
win.open_file(args[0]) win.open_file(args[0])
gobject.timeout_add(1000, win.update, args[0])
gtk.main() gtk.main()