├── .gitignore ├── LICENSE.md ├── README.md └── burpscripterplus.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | THE BEER-WARE LICENSE" (Revision 42): ganapati (@G4N4P4T1) wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Burp Scripter Plus 2 | 3 | Burp Scripter Plus adds an extra proxy layer to Burp with a simple Python interface. 4 | 5 | Browser -> BURP -> BurpScripterPlus -> Website 6 | 7 | Browser <- BURP <- BurpScripterPlus <- Website 8 | 9 | This project is a fork of "Python Scripter" (https://github.com/portswigger/python-scripter) with some new features : 10 | - Response and Request are mapped into easy to use objects 11 | - Auto Content-Length generation 12 | - Auto message parser/rebuild for switching between string request and easy to use object 13 | - Documentation ! 14 | 15 | Request object has multiple attributes : 16 | - params : dictionnary of GET parameters/values 17 | - body : dictionnary of body key/values (or a simple string if other format like xml, json, etc.) 18 | - headers : dictionnary of headers parameters/values 19 | - is_request : boolean (True) 20 | - is_response : boolean (False) 21 | - is_in_scope : boolean 22 | - method : string HTTP method 23 | - path : string url 24 | - http_version : string like HTTP/1.1 25 | 26 | Response object has multiple attributes : 27 | - body : dictionnary of body key/values (or a simple string if other format like xml, json, html,etc.) 28 | - headers : dictionnary of headers parameters/values 29 | - is_request : boolean (False) 30 | - is_response : boolean (True) 31 | - is_in_scope : boolean 32 | - http_version : string (ex: HTTP/1.1) 33 | - response_code : int (ex: 200) 34 | - response_value : string (ex: OK) 35 | 36 | ## Setup 37 | - Add the Jython jarfile to Burp (https://www.jython.org/download) 38 | - Go to extender, click "Add" 39 | - Extension type : Python 40 | - Extension file : burpscripterplus.py 41 | - Click "Next" 42 | 43 | A new tab "Script+" is appended to the Burp interface; you can type your code in it. The message's object is directly accessible (following examples work with a simple copy/paste). 44 | 45 | ## Examples 46 | 47 | ### Change or add the "param1" GET parameter in outgoing request 48 | 49 | ``` 50 | if message.is_request: 51 | message.params["param1"] = "my_new_value" 52 | ``` 53 | 54 | ### Edit the outgoing request if "my_header" is in the headers 55 | 56 | ``` 57 | if message.is_request: 58 | if "my_header" in message.headers.keys(): 59 | message.headers["my_header"] = "my_new_header_value" 60 | ``` 61 | 62 | ### Change the outgoing request body as string 63 | 64 | ``` 65 | if message.is_request: 66 | message.body = "This is a new body" 67 | ``` 68 | 69 | ### Change the outgoing request body param 70 | 71 | ``` 72 | if message.is_request: 73 | message.body["myparam"] = "This is a new value" 74 | ``` 75 | 76 | ### Base64 encode the outgoing request body param 77 | 78 | ``` 79 | import base64 80 | 81 | if message.is_request: 82 | message.body["myparam"] = base64.b64encode(message.body["myparam"]) 83 | ``` 84 | 85 | ### Change response header 86 | 87 | ``` 88 | if message.is_response: 89 | if "my_header" in message.headers.keys(): 90 | message.headers["my_header"] = "my_new_header_value" 91 | ``` 92 | 93 | ### Modify response body 94 | 95 | ``` 96 | if message.is_response: 97 | message.body = message.body.replace("foo", "bar") 98 | ``` 99 | 100 | ## Documentation 101 | 102 | ``` 103 | Help on module burpscripterplus: 104 | 105 | NAME 106 | burpscripterplus 107 | 108 | CLASSES 109 | __builtin__.object 110 | Message 111 | Request 112 | Response 113 | 114 | class Message(__builtin__.object) 115 | | Methods defined here: 116 | | 117 | | __init__(self, message_info, helpers, callbacks) 118 | | Message constructor 119 | | 120 | | :param message_info: message_info from Burp extender 121 | | :param helpers: helpers from Burp extender 122 | | :param callbacks: callbacks from Burp extender 123 | | :return: Message instance (not usable, just a parent class for Request and Response) 124 | | :rtype: Message 125 | | 126 | | build_message(self) 127 | | Build a string message from parsed message (created by parse_message) 128 | | 129 | | Do not use this one, it's only for forcing childs to implement method 130 | | 131 | | parse_message(self) 132 | | Parse message input from Burp extender 133 | | 134 | | Do not use this one, it's only for forcing childs to implement method 135 | | 136 | | update_content_length(self, data) 137 | | Recalculate body length and set it in Content-Length header 138 | | 139 | | :param data: data is the body string used for re-calculating Content-Length header 140 | | :type data: string 141 | | 142 | | ---------------------------------------------------------------------- 143 | | Data descriptors defined here: 144 | | 145 | | __dict__ 146 | | dictionary for instance variables (if defined) 147 | | 148 | | __weakref__ 149 | | list of weak references to the object (if defined) 150 | 151 | class Request(Message) 152 | | Method resolution order: 153 | | Request 154 | | Message 155 | | __builtin__.object 156 | | 157 | | Methods defined here: 158 | | 159 | | __init__(self, message_info, helpers, callbacks) 160 | | Request constructor 161 | | 162 | | :param message_info: message_info from Burp extender 163 | | :param helpers: helpers from Burp extender 164 | | :param callbacks: callbacks from Burp extender 165 | | :return: Request instance 166 | | :rtype: Request 167 | | 168 | | build_body(self) 169 | | Transform dict body to string 170 | | 171 | | This method takes all key:values items from doby and construct a body string 172 | | 173 | | :return: full body string 174 | | :rtype: string 175 | | 176 | | build_message(self) 177 | | Build Complete message as string from attributes 178 | | 179 | | This method takes all Request attributes and build a response string 180 | | This method is auto-called by extension and you don't have to call build_message yourself 181 | | 182 | | build_parameters(self) 183 | | From params dict to string 184 | | 185 | | This method takes all key:values from parameters (GET) and build a string 186 | | 187 | | :return: GET parameters as string 188 | | :rtype: string 189 | | 190 | | parse_message(self) 191 | | Parse message input from Burp extender 192 | | 193 | | This method populate the Request object with parsed data 194 | | 195 | | parse_parameters(self, line) 196 | | Parse params string to dict 197 | | 198 | | This method takes the GET parameters as string and create a dictionnary in params attribute 199 | | 200 | | :param line: First line of request as string (ex: GET /foo?bar=baz HTTP/1.1) 201 | | :type line: string 202 | | 203 | | ---------------------------------------------------------------------- 204 | | Methods inherited from Message: 205 | | 206 | | update_content_length(self, data) 207 | | Recalculate body length and set it in Content-Length header 208 | | 209 | | :param data: data is the body string used for re-calculating Content-Length header 210 | | :type data: string 211 | | 212 | | ---------------------------------------------------------------------- 213 | | Data descriptors inherited from Message: 214 | | 215 | | __dict__ 216 | | dictionary for instance variables (if defined) 217 | | 218 | | __weakref__ 219 | | list of weak references to the object (if defined) 220 | 221 | class Response(Message) 222 | | Method resolution order: 223 | | Response 224 | | Message 225 | | __builtin__.object 226 | | 227 | | Methods defined here: 228 | | 229 | | __init__(self, message_info, helpers, callbacks) 230 | | Response constructor 231 | | 232 | | :param message_info: message_info from Burp extender 233 | | :param helpers: helpers from Burp extender 234 | | :param callbacks: callbacks from Burp extender 235 | | :return: Response instance 236 | | :rtype: Response 237 | | 238 | | build_message(self) 239 | | Build Complete message as string from attributes 240 | | 241 | | This method takes all Response attributes and build a response string 242 | | This method is auto-called by extension and you don't have to call build_message yourself 243 | | 244 | | parse_message(self) 245 | | Parse message input from Burp extender 246 | | 247 | | This method populate the Response object with parsed data 248 | | 249 | | ---------------------------------------------------------------------- 250 | | Methods inherited from Message: 251 | | 252 | | update_content_length(self, data) 253 | | Recalculate body length and set it in Content-Length header 254 | | 255 | | :param data: data is the body string used for re-calculating Content-Length header 256 | | :type data: string 257 | | 258 | | ---------------------------------------------------------------------- 259 | | Data descriptors inherited from Message: 260 | | 261 | | __dict__ 262 | | dictionary for instance variables (if defined) 263 | | 264 | | __weakref__ 265 | | list of weak references to the object (if defined) 266 | 267 | FUNCTIONS 268 | get_message(message_info, helpers, message_is_request, callbacks) 269 | Return Message or Request according to message 270 | 271 | :param message_info: message_info from Burp extender 272 | :param helpers: helpers from Burp extender 273 | :param message_is_request: message_is_request from Burp extender 274 | :param callbacks: callbacks from Burp extender 275 | :return: Request or Response instance 276 | :rtype: Message 277 | ``` 278 | 279 | ## License 280 | 281 | ``` 282 | THE BEER-WARE LICENSE" (Revision 42): ganapati (@G4N4P4T1) wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. 283 | ``` 284 | -------------------------------------------------------------------------------- /burpscripterplus.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Name: BurpScripter Plus 5 | Version: 0.1 6 | Author: Ganapati - @G4N4P4T1 7 | Github: https://github.com/Acceis/BurpScripterPlus 8 | Description: This extension provide a Python panel for writing custom proxy script. 9 | License : THE BEER-WARE LICENSE" (Revision 42): ganapati (@G4N4P4T1) wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. 10 | """ 11 | 12 | from java.awt import Font 13 | from java.io import PrintWriter 14 | from javax.swing import JScrollPane, JTextPane 15 | from javax.swing.text import SimpleAttributeSet 16 | 17 | from burp import ( 18 | IBurpExtender, 19 | IExtensionStateListener, 20 | IHttpListener, 21 | ITab, 22 | ) 23 | 24 | import base64 25 | import urllib 26 | import traceback 27 | 28 | VERSION = "0.1" 29 | 30 | 31 | def get_message( 32 | message_info, helpers, message_is_request, callbacks 33 | ): 34 | """ Return Message or Request according to message 35 | 36 | :param message_info: message_info from Burp extender 37 | :param helpers: helpers from Burp extender 38 | :param message_is_request: message_is_request from Burp extender 39 | :param callbacks: callbacks from Burp extender 40 | :return: Request or Response instance 41 | :rtype: Message 42 | 43 | """ 44 | if message_is_request: 45 | return Request(message_info, helpers, callbacks) 46 | else: 47 | return Response(message_info, helpers, callbacks) 48 | 49 | 50 | class Message(object): 51 | """ Generic Class for Request and Response 52 | 53 | Only used as parent class 54 | """ 55 | 56 | def __init__(self, message_info, helpers, callbacks): 57 | """ Message constructor 58 | 59 | :param message_info: message_info from Burp extender 60 | :param helpers: helpers from Burp extender 61 | :param callbacks: callbacks from Burp extender 62 | :return: Message instance (not usable, just a parent class for Request and Response) 63 | :rtype: Message 64 | """ 65 | self.message_info = message_info 66 | self.callbacks = callbacks 67 | self.helpers = helpers 68 | 69 | self.is_request = False 70 | self.is_response = False 71 | self.is_in_scope = callbacks.isInScope( 72 | message_info.getUrl() 73 | ) 74 | 75 | def parse_message(self): 76 | """ Parse message input from Burp extender 77 | 78 | Do not use this one, it's only for forcing childs to implement method 79 | """ 80 | raise NotImplementedError 81 | 82 | def build_message(self): 83 | """ Build a string message from parsed message (created by parse_message) 84 | 85 | Do not use this one, it's only for forcing childs to implement method 86 | """ 87 | raise NotImplementedError 88 | 89 | def update_content_length(self, data): 90 | """ Recalculate body length and set it in Content-Length header 91 | 92 | :param data: data is the body string used for re-calculating Content-Length header 93 | :type data: string 94 | """ 95 | if data is not None: 96 | self.headers["Content-Length"] = len(data) 97 | 98 | 99 | class Response(Message): 100 | """ Response class 101 | 102 | Map the entire Response into an object for easier manipulation 103 | """ 104 | 105 | def __init__(self, message_info, helpers, callbacks): 106 | """ Response constructor 107 | 108 | :param message_info: message_info from Burp extender 109 | :param helpers: helpers from Burp extender 110 | :param callbacks: callbacks from Burp extender 111 | :return: Response instance 112 | :rtype: Response 113 | """ 114 | Message.__init__( 115 | self, message_info, helpers, callbacks 116 | ) 117 | 118 | self.is_request = False 119 | self.is_response = True 120 | self.http_version = "" 121 | self.response_code = 200 122 | self.response_value = "" 123 | self.headers = {} 124 | self.body = "" 125 | 126 | self.parse_message() 127 | 128 | def parse_message(self): 129 | """ Parse message input from Burp extender 130 | 131 | This method populate the Response object with parsed data 132 | """ 133 | message = self.message_info.getResponse() 134 | parsed_message = self.helpers.analyzeResponse( 135 | message 136 | ) 137 | 138 | # Parse headers 139 | headers_dict = {} 140 | headers = parsed_message.getHeaders() 141 | 142 | # Reconstruct the headers as dict 143 | headers_list = list(headers) 144 | self.http_version, self.response_code, self.response_value = headers_list[ 145 | 0 146 | ].split( 147 | " ", 2 148 | ) 149 | for header in headers_list[1:]: 150 | k, v = header.split(": ") 151 | headers_dict[str(k)] = str(v) 152 | 153 | self.headers = headers_dict 154 | self.body = message[ 155 | (parsed_message.getBodyOffset()) : 156 | ].tostring() 157 | 158 | def build_message(self): 159 | """ Build Complete message as string from attributes 160 | 161 | This method takes all Response attributes and build a response string 162 | This method is auto-called by extension and you don't have to call build_message yourself 163 | """ 164 | self.update_content_length(self.body) 165 | 166 | message = "" 167 | message = "%s %s %s\r\n" % ( 168 | self.http_version, 169 | self.response_code, 170 | self.response_value, 171 | ) 172 | 173 | # Add headers 174 | for k, v in self.headers.items(): 175 | message = message + "%s: %s\r\n" % (k, v) 176 | message = message + "\r\n" 177 | 178 | # Add body 179 | message = message + self.body.decode( 180 | "utf-8", "replace" 181 | ) 182 | 183 | self.message_info.setResponse(message) 184 | 185 | 186 | class Request(Message): 187 | """ Request class 188 | 189 | Map the entire Request into an object for easier manipulation 190 | """ 191 | 192 | def __init__(self, message_info, helpers, callbacks): 193 | """ Request constructor 194 | 195 | :param message_info: message_info from Burp extender 196 | :param helpers: helpers from Burp extender 197 | :param callbacks: callbacks from Burp extender 198 | :return: Request instance 199 | :rtype: Request 200 | """ 201 | Message.__init__( 202 | self, message_info, helpers, callbacks 203 | ) 204 | 205 | self.is_request = True 206 | self.is_response = False 207 | self.method = "" 208 | self.path = "" 209 | self.http_version = "" 210 | self.params = {} 211 | self.headers = {} 212 | self.body = {} 213 | self.body_str = "" 214 | self.parse_message() 215 | 216 | def parse_parameters(self, line): 217 | """ Parse params string to dict 218 | 219 | This method takes the GET parameters as string and create a dictionnary in params attribute 220 | 221 | :param line: First line of request as string (ex: GET /foo?bar=baz HTTP/1.1) 222 | :type line: string 223 | """ 224 | self.method, path_params, self.http_version = line.split( 225 | " " 226 | ) 227 | path_params_array = path_params.split("?") 228 | self.path = path_params_array[0] 229 | if len(path_params_array) > 1: 230 | params = path_params_array[1] 231 | for _ in params.split("&"): 232 | try: 233 | k, v = _.split("=") 234 | self.params[k] = v 235 | except ValueError: 236 | k = _.split("=")[0] 237 | self.params[k] = "" 238 | 239 | def build_parameters(self): 240 | """ From params dict to string 241 | 242 | This method takes all key:values from parameters (GET) and build a string 243 | 244 | :return: GET parameters as string 245 | :rtype: string 246 | """ 247 | params = "" 248 | for k, v in self.params.items(): 249 | params = params + "%s=%s&" % ( 250 | k.strip(), 251 | v.strip(), 252 | ) 253 | if len(params) > 0: 254 | params = "?%s" % params[:-1] 255 | 256 | return params 257 | 258 | def parse_message(self): 259 | """ Parse message input from Burp extender 260 | 261 | This method populate the Request object with parsed data 262 | """ 263 | message = self.message_info.getRequest() 264 | parsed_message = self.helpers.analyzeRequest( 265 | message 266 | ) 267 | 268 | # Parse headers 269 | headers_dict = {} 270 | headers = parsed_message.getHeaders() 271 | 272 | # Reconstruct the headers as dict 273 | headers_list = list(headers) 274 | for header in headers_list[1:]: 275 | k, v = header.split(": ") 276 | headers_dict[str(k)] = str(v) 277 | 278 | self.headers = headers_dict 279 | 280 | self.parse_parameters(headers_list[0]) 281 | 282 | # Extract body from message 283 | body = message[(parsed_message.getBodyOffset()) :] 284 | self.body_str = "".join(chr(_) for _ in body) 285 | body_dict = {} 286 | if "Content-Length" in self.headers.keys(): 287 | try: 288 | if int(self.headers["Content-Length"]) > 0: 289 | for arg in self.body_str.split("&"): 290 | k, v = arg.split("=") 291 | body_dict[k] = v 292 | self.body = body_dict 293 | except: 294 | self.body = None 295 | 296 | def build_body(self): 297 | """ Transform dict body to string 298 | 299 | This method takes all key:values items from doby and construct a body string 300 | 301 | :return: full body string 302 | :rtype: string 303 | """ 304 | body = "" 305 | for k, v in self.body.items(): 306 | body = body + "%s=%s&" % (k.strip(), v.strip()) 307 | return body[:-1] 308 | 309 | def build_message(self): 310 | """ Build Complete message as string from attributes 311 | 312 | This method takes all Request attributes and build a response string 313 | This method is auto-called by extension and you don't have to call build_message yourself 314 | """ 315 | if isinstance(self.body, dict): 316 | self.body_str = self.build_body() 317 | else: 318 | if self.body is not None: 319 | self.body_str = self.body 320 | 321 | self.update_content_length(self.body_str) 322 | 323 | message = "" 324 | 325 | # Add method, path and params 326 | message = "%s %s%s %s\r\n" % ( 327 | self.method, 328 | self.path, 329 | self.build_parameters(), 330 | self.http_version, 331 | ) 332 | 333 | # Add headers 334 | for k, v in self.headers.items(): 335 | message = message + "%s: %s\r\n" % (k, v) 336 | message = message + "\r\n" 337 | 338 | # Add body 339 | message = message + self.body_str.decode( 340 | "utf-8", "replace" 341 | ) 342 | 343 | self.message_info.setRequest(message) 344 | 345 | 346 | class BurpExtender( 347 | IBurpExtender, 348 | IExtensionStateListener, 349 | IHttpListener, 350 | ITab, 351 | ): 352 | def registerExtenderCallbacks(self, callbacks): 353 | self.callbacks = callbacks 354 | self.helpers = callbacks.helpers 355 | 356 | callbacks.setExtensionName("Burp Scripter Plus") 357 | 358 | stdout = PrintWriter(callbacks.getStdout(), True) 359 | stdout.println( 360 | """Successfully loaded Burp Scripter Plus v""" 361 | + VERSION 362 | + """\n 363 | Repository @ https://github.com/Acceis/BurpScripterPlus 364 | Send feedback or bug reports on twitter @G4N4P4T1""" 365 | ) 366 | 367 | self.scriptpane = JTextPane() 368 | self.scriptpane.setFont( 369 | Font("Monospaced", Font.PLAIN, 12) 370 | ) 371 | 372 | self.scrollpane = JScrollPane() 373 | self.scrollpane.setViewportView(self.scriptpane) 374 | 375 | self._code = compile("", "", "exec") 376 | self._script = "" 377 | 378 | script = callbacks.loadExtensionSetting("script") 379 | 380 | if script: 381 | script = base64.b64decode(script) 382 | 383 | self.scriptpane.document.insertString( 384 | self.scriptpane.document.length, 385 | script, 386 | SimpleAttributeSet(), 387 | ) 388 | 389 | self._script = script 390 | try: 391 | self._code = compile( 392 | script, "", "exec" 393 | ) 394 | except Exception as e: 395 | traceback.print_exc( 396 | file=self.callbacks.getStderr() 397 | ) 398 | 399 | callbacks.registerExtensionStateListener(self) 400 | callbacks.registerHttpListener(self) 401 | callbacks.customizeUiComponent( 402 | self.getUiComponent() 403 | ) 404 | callbacks.addSuiteTab(self) 405 | 406 | self.scriptpane.requestFocus() 407 | 408 | def extensionUnloaded(self): 409 | try: 410 | self.callbacks.saveExtensionSetting( 411 | "script", 412 | base64.b64encode( 413 | self._script.replace( 414 | "\nmessage.build_message()", "" 415 | ) 416 | ), 417 | ) 418 | except Exception: 419 | traceback.print_exc( 420 | file=self.callbacks.getStderr() 421 | ) 422 | return 423 | 424 | def processHttpMessage( 425 | self, toolFlag, messageIsRequest, messageInfo 426 | ): 427 | try: 428 | globals_ = {} 429 | locals_ = { 430 | "extender": self, 431 | "toolFlag": toolFlag, 432 | "messageInfo": messageInfo, 433 | "message": get_message( 434 | messageInfo, 435 | self.helpers, 436 | messageIsRequest, 437 | self.callbacks, 438 | ), 439 | } 440 | exec(self.script, globals_, locals_) 441 | except Exception: 442 | traceback.print_exc( 443 | file=self.callbacks.getStderr() 444 | ) 445 | return 446 | 447 | def getTabCaption(self): 448 | return "Script+" 449 | 450 | def getUiComponent(self): 451 | return self.scrollpane 452 | 453 | @property 454 | def script(self): 455 | end = self.scriptpane.document.length 456 | _script = ( 457 | self.scriptpane.document.getText(0, end) 458 | + "\nmessage.build_message()" 459 | ) 460 | 461 | if _script == self._script: 462 | return self._code 463 | 464 | self._script = _script 465 | self._code = compile(_script, "", "exec") 466 | return self._code 467 | --------------------------------------------------------------------------------