From c428a2de54229221c69d3e2973e58684afeed3f0 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sun, 5 Jan 2020 16:13:57 +0100 Subject: [PATCH 01/42] new structure --- pyhp.py | 452 -------------------------------------------------- src/pyhp.conf | 78 +++++++++ 2 files changed, 78 insertions(+), 452 deletions(-) delete mode 100644 pyhp.py create mode 100644 src/pyhp.conf diff --git a/pyhp.py b/pyhp.py deleted file mode 100644 index 0e087cc..0000000 --- a/pyhp.py +++ /dev/null @@ -1,452 +0,0 @@ -#!/usr/bin/python3 - -"""Interpreter for .pyhp Scripts (https://github.com/Deric-W/PyHP-Interpreter)""" - -# MIT License -# -# Copyright (c) 2019 Eric W. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import time -REQUEST_TIME = time.time() -import argparse -import configparser -import sys -import os -import marshal -import re -import cgi -import urllib.parse -import importlib -from collections import defaultdict - - -config = "/etc/pyhp.conf" - - -class pyhp: - def __init__(self): - parser = argparse.ArgumentParser(description="Interpreter for .pyhp Scripts (https://github.com/Deric-W/PyHP-Interpreter)") - parser.add_argument("-c", "--caching", help="enable caching (requires file)", action="store_true") - parser.add_argument("file", type=str, help="file to be interpreted (omit for reading from stdin)", nargs="?", default="") - args = parser.parse_args() - self.file_path = args.file - if args.file != "": # enable caching flag if file is not stdin - self.caching = args.caching - else: - self.caching = False - - self.config = configparser.ConfigParser(inline_comment_prefixes="#") - if config not in self.config.read(config): # failed to read file - raise ValueError("failed to read config file") - - self.print = print # backup for sending headers - self.exit = exit # backup for exit after shutdown_functions - self.response_code = [200, "OK"] - self.headers = [] - self.header_sent = False - self.header_callback = None - self.shutdown_functions = [] - - self.response_messages = { - 100: "Continue", - 101: "Switching Protocols", - 200: "OK", - 201: "Created", - 202: "Accepted", - 203: "Non-Authoritative Information", - 204: "No Content", - 205: "Reset Content", - 206: "Partial Content", - 300: "Multiple Choices", - 301: "Moved Permanently", - 302: "Found", - 303: "See Other", - 304: "Not Modified", - 305: "Use Proxy", - 307: "Temporary Redirect", - 308: "Permanent Redirect", - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Payload Too Large", - 414: "URI Too Long", - 415: "Unsupported Media Type", - 416: "Range Not Satisfiable", - 417: "Expectation Failed", - 418: "I’m a teapot", - 426: "Upgrade Required", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported" - } - - self.SERVER = { # incomplete (AUTH) - "PyHP_SELF": os.getenv("SCRIPT_NAME", default=""), - "argv": os.getenv("QUERY_STRING", default=sys.argv[2:]), - "argc": len(sys.argv) - 2, - "GATEWAY_INTERFACE": os.getenv("GATEWAY_INTERFACE", default=""), - "SERVER_ADDR": os.getenv("SERVER_ADDR", default=""), - "SERVER_NAME": os.getenv("SERVER_NAME", default=""), - "SERVER_SOFTWARE": os.getenv("SERVER_SOFTWARE", default=""), - "SERVER_PROTOCOL": os.getenv("SERVER_PROTOCOL", default=""), - "REQUEST_METHOD": os.getenv("REQUEST_METHOD", default=""), - "REQUEST_TIME": int(REQUEST_TIME), - "REQUEST_TIME_FLOAT": REQUEST_TIME, - "QUERY_STRING": os.getenv("QUERY_STRING", default=""), - "DOCUMENT_ROOT": os.getenv("DOCUMENT_ROOT", default=""), - "HTTP_ACCEPT": os.getenv("HTTP_ACCEPT", default=""), - "HTTP_ACCEPT_CHARSET": os.getenv("HTTP_ACCEPT_CHARSET", default=""), - "HTTP_ACCEPT_ENCODING": os.getenv("HTTP_ACCEPT_ENCODING", default=""), - "HTTP_ACCEPT_LANGUAGE": os.getenv("HTTP_ACCEPT_LANGUAGE", default=""), - "HTTP_CONNECTION": os.getenv("HTTP_CONNECTION", default=""), - "HTTP_HOST": os.getenv("HTTP_HOST", default=""), - "HTTP_REFERER": os.getenv("HTTP_REFERER", default=""), - "HTTP_USER_AGENT": os.getenv("HTTP_USER_AGENT", default=""), - "HTTPS": os.getenv("HTTPS", default=""), - "REMOTE_ADDR": os.getenv("REMOTE_ADDR", default=""), - "REMOTE_HOST": os.getenv("REMOTE_HOST", default=""), - "REMOTE_PORT": os.getenv("REMOTE_PORT", default=""), - "REMOTE_USER": os.getenv("REMOTE_USER", default=""), - "REDIRECT_REMOTE_USER": os.getenv("REDIRECT_REMOTE_USER", default=""), - "SCRIPT_FILENAME": self.file_path, - "SERVER_ADMIN": os.getenv("SERVER_ADMIN", default=""), - "SERVER_PORT": os.getenv("SERVER_PORT", default=""), - "SERVER_SIGNATURE": os.getenv("SERVER_SIGNATURE", default=""), - "PATH_TRANSLATED": os.getenv("PATH_TRANSLATED", default=self.file_path), - "SCRIPT_NAME": os.getenv("SCRIPT_NAME", default=os.path.basename(self.file_path)), - "REQUEST_URI": os.getenv("REQUEST_URI", default=""), - "PyHP_AUTH_DIGEST": "", - "PyHP_AUTH_USER": "", - "PyHP_AUTH_PW": "", - "AUTH_TYPE": os.getenv("AUTH_TYPE", default=""), - "PATH_INFO": os.getenv("PATH_INFO", default=""), - "ORIG_PATH_INFO": os.getenv("PATH_INFO", default="") - } - - data = cgi.FieldStorage() # build $_REQUEST array from PHP - self.REQUEST = defaultdict(lambda: "") - for key in data: - self.REQUEST[key] = data.getvalue(key) # to contain lists instead of multiple FieldStorages if key has multiple values - - data = urllib.parse.parse_qsl(self.SERVER["QUERY_STRING"], keep_blank_values=True) - self.GET = defaultdict(lambda: "") - for pair in data: # build $_GET - if not pair[0] in self.REQUEST: # if value is blank - self.REQUEST[pair[0]] = pair[1] - self.GET[pair[0]] = self.REQUEST[pair[0]] # copy value from REQUEST - - self.POST = defaultdict(lambda: "") - for key in self.REQUEST: # build $_POST - if key not in self.GET: # REQUEST - GET = POST - self.POST[key] = self.REQUEST[key] - - data = os.getenv("HTTP_COOKIE", default="") - self.COOKIE = defaultdict(lambda: "") - if data != "": # to avoid non existent blank cookies - for cookie in data.split(";"): # build $_COOKIE - cookie = cookie.split("=") - if len(cookie) > 2: # multiple = in cookie - cookie[1] = "=".join(cookie[1:]) - if len(cookie) == 1: # blank cookie - cookie.append("") - cookie[0] = cookie[0].strip(" ") - try: # to handle blank values - if cookie[1][0] == " ": # remove only potential space after = - cookie[1] = cookie[1][1:] - except IndexError: - pass - cookie[0] = urllib.parse.unquote_plus(cookie[0]) - cookie[1] = urllib.parse.unquote_plus(cookie[1]) - if cookie[0] in self.COOKIE: - if type(self.COOKIE[cookie[0]]) == str: - self.COOKIE[cookie[0]] = [self.COOKIE[cookie[0]], cookie[1]] # make new list - else: - self.COOKIE[cookie[0]].append(cookie[1]) # append to existing list - else: - self.COOKIE[cookie[0]] = cookie[1] # make new string - - for cookie in self.COOKIE: # merge COOKIE with REQUEST, prefer COOKIE - self.REQUEST[cookie] = self.COOKIE[cookie] - - if self.config.getboolean("caching", "allow_caching") and (self.caching or self.config.getboolean("caching", "auto_caching")): - handler_path = self.config.get("caching", "handler_path") - cache_path = self.config.get("caching", "cache_path") - sys.path.insert(0, handler_path) - handler = importlib.import_module(self.config.get("caching", "handler")).handler(cache_path, os.path.abspath(self.file_path)) - del sys.path[0] # cleanup for normal import behavior - if handler.is_outdated(): - self.file_content = self.prepare_file(self.file_path) - self.file_content, self.code_at_begin = self.split_code(self.file_content) - self.section_count = -1 - for self.section in self.file_content: - self.section_count += 1 - if self.section_count == 0: - if self.code_at_begin: # first section is code, exec - self.file_content[self.section_count][0] = compile(self.fix_indent(self.section[0], self.section_count), "", "exec") - else: # all sections after the first one are like [code, html until next code or eof] - self.file_content[self.section_count][0] = compile(self.fix_indent(self.section[0], self.section_count), "", "exec") - handler.save(self.file_content, self.code_at_begin) - self.cached = True - else: - self.file_content, self.code_at_begin = handler.load() - self.cached = True - handler.close() # to allow cleanup, like closing connections, etc - else: # no caching - self.file_content = self.prepare_file(self.file_path) - self.file_content, self.code_at_begin = self.split_code(self.file_content) - self.cached = False - - def prepare_file(self, file_path): # read file and handle shebang - if file_path != "": - with open(file_path, "r", encoding='utf-8') as file: - file_content = file.read().split("\n") - else: # file not given, read from stdin - file_content = input().split("\n") - - if file_content[0][:2] == "#!": # shebang support - file_content = "\n".join(file_content[1:]) - else: - file_content = "\n".join(file_content) - return file_content - - def split_code(self, code): # split file_content in sections like [code, html until next code or eof] with first section containing the html from the beginning if existing - opening_tag = self.config.get("parser", "opening_tag").encode("utf8").decode("unicode_escape") # process escape sequences like \n and \t - closing_tag = self.config.get("parser", "closing_tag").encode("utf8").decode("unicode_escape") - code = re.split(opening_tag, code) - if code[0] == "": - code_at_begin = True - code = code[1:] - else: - code_at_begin = False - index = 0 - for section in code: - if index == 0 and not code_at_begin: - code[index] = [section] - else: - code[index] = re.split(closing_tag, section, maxsplit=1) - index += 1 - return code, code_at_begin - - def mstrip(self, text, chars): # removes all chars in chars from start and end of text - while len(text) > 0 and text[0] in chars: - text = text[1:] - while len(text) > 0 and text[-1] in chars: - text = text[:-1] - return text - - def get_indent(self, line): # return string and index of indent - index = 0 - string = "" - for char in line: - if char in [" ", "\t"]: - index += 1 - string += char - else: - break - return [index, string] - - def is_comment(self, line): # return True if line is comment (first char == #) - comment = False - for char in line: - if char in [" ", "\t"]: - pass - elif char == "#": - comment = True - break - else: - comment = False - break - return comment - - def fix_indent(self, code, section): - fixed_code = "" - linecount = 0 - first_line = True - for line in code.split("\n"): - linecount += 1 - if line.replace(" ", "").replace("\t", "") != "": # not empthy - if not self.is_comment(line): - if first_line: - indent = self.get_indent(line) - first_line = False - if len(line) > indent[0] and line[:indent[0]] == indent[1]: # line is big enough for indent and indent is the same as first line - fixed_code += line[indent[0]:] + "\n" - else: - raise IndentationError("File: " + self.file_path + " line: " + str(linecount) + " section: " + str(section)) - return fixed_code - - def http_response_code(self, response_code=None): # set response code - old_response_code = self.response_code[0] - if response_code != None: - self.response_code = [int(response_code), self.response_messages[response_code]] - return old_response_code - - def headers_list(self): # list current header - headers = [] - for header in self.headers: - headers.append(str(header[0]) + ": " + str(header[1])) - return headers - - def header(self, header, replace=True, response_code=None): # add headers and set response code - if response_code != None: - self.http_response_code(response_code) # update response code if given - header = header.split("\n")[0] # to prevent Header-Injection - header = header.split(":", maxsplit=1) # to allow cookies - header = [header[0].strip(" "), header[1].strip(" ")] - if replace: - new_header = [] - for stored_header in self.headers: - if stored_header[0].lower() != header[0].lower(): - new_header.append(stored_header) # same header not in list - new_header.append(header) - self.headers = new_header - else: - self.headers.append(header) - - def header_remove(self, header): # remove header - header = header.split(":") - header = [header[0].strip(" "), header[1].strip(" ")] - new_header = [] - for stored_header in self.headers: - if stored_header[0].lower() != header[0].lower() or stored_header[1].lower() != header[1].lower(): - new_header.append(stored_header) # same headers not in list - self.headers = new_header - - def headers_sent(self): # true if headers already sent - return self.header_sent - - def sent_header(self): - if self.header_callback != None: - header_callback = self.header_callback - self.header_callback = None # to prevent recursion if output occurs - header_callback() # execute callback if set - self.print("Status: " + str(self.response_code[0]) + " " + self.response_code[1]) # print status code - mistake = True # no content-type header - for header in self.headers: - if header[0].lower() == "content-type": # check for content-type - mistake = False - self.print(str(header[0]) + ": " + str(header[1])) # sent header - if mistake: - self.print("Content-Type: text/html") # sent fallback Content-Type header - self.print() # end of headers - self.header_sent = True - - def header_register_callback(self, callback): - if self.header_sent: - return False # headers already send - else: - self.header_callback = callback - return True - - def setcookie(self, name, value="", expires=0, path="", domain="", secure=False, httponly=False): - name = urllib.parse.quote_plus(name) - value = urllib.parse.quote_plus(value) - return self.setrawcookie(name, value, expires, path, domain, secure, httponly) - - def setrawcookie(self, name, value="", expires=0, path="", domain="", secure=False, httponly=False): - if self.header_sent: - return False - else: - if type(expires) == dict: # options array - path = expires.get("path", "") - domain = expires.get("domain", "") - secure = expires.get("secure", False) - httponly = expires.get("httponly", False) - samesite = expires.get("samesite", "") - expires = expires.get("expires", 0) - else: - samesite = "" - cookie = "Set-Cookie:" - cookie += name + "=" + value - if expires != 0: - cookie += "; " + "Expires=" + time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time() + expires)) - if path != "": - cookie += "; " + "Path=" + path - if domain != "": - cookie += "; " + "Domain=" + domain - if secure: - cookie += "; " + "Secure" - if httponly: - cookie += "; " + "HttpOnly" - if samesite != "": - cookie += "; " + "SameSite=" + samesite - self.header(cookie, False) - return True - - def register_shutdown_function(self, callback, *args, **kwargs): - self.shutdown_functions.append([callback, args, kwargs]) - - -pyhp = pyhp() - - -def print(*args, **kwargs): # wrap print to auto sent headers - if not pyhp.header_sent: - pyhp.sent_header() - pyhp.print(*args, **kwargs) - -def exit(*args, **kwargs): # wrapper to exit shutdown functions - shutdown_functions = pyhp.shutdown_functions - pyhp.shutdown_functions = [] # to prevent recursion if exit is called - for func in shutdown_functions: - func[0](*func[1], **func[2]) - pyhp.exit(*args, **kwargs) - - -pyhp.section_count = -1 -for pyhp.section in pyhp.file_content: - pyhp.section_count += 1 - if pyhp.section_count == 0: - if pyhp.code_at_begin: # first section is code, exec - if pyhp.cached: - exec(pyhp.section[0]) - else: - exec(pyhp.fix_indent(pyhp.section[0], pyhp.section_count)) - try: - print(pyhp.section[1], end="") - except IndexError as err: # missing closing tag - raise SyntaxError("File: " + pyhp.file_path + " Section: " + str(pyhp.section_count) + " Cause: missing closing Tag") from err - else: # first section is just html, print - print(pyhp.section[0], end="") - else: # all sections after the first one are like [code, html until next code or eof] - if pyhp.cached: - exec(pyhp.section[0]) - else: - exec(pyhp.fix_indent(pyhp.section[0], pyhp.section_count)) - try: - print(pyhp.section[1], end="") - except IndexError as err: - raise SyntaxError("File: " + pyhp.file_path + " Section: " + str(pyhp.section_count) + " Cause: missing closing Tag") from err - -exit(0) diff --git a/src/pyhp.conf b/src/pyhp.conf new file mode 100644 index 0000000..907c1e1 --- /dev/null +++ b/src/pyhp.conf @@ -0,0 +1,78 @@ +# Configuration for the PyHP Interpreter (https://github.com/Deric-W/PyHP-Interpreter) +# This file uses the INI syntax + +[parser] +# regex to isolate the code +regex = \\<\\?pyhp[\\s](.*?)[\\s]\\?\\> + +[request] +# order to fill up REQUEST, starting left and missing methods are not filled in +# only seperate methods by one Withespace +request_order = GET POST COOKIE + +keep_blank_values = True + +# comment out if not wanted +fallback_value = + +# dont consume stdin and dont fill in POST +enable_post_data_reading = False + +# fallback content-type header +default_mimetype = text/html + +[caching] +enable = True + +# maximum size in MByte, -1 = infinite +max_size = 16 + +# time in seconds after a cached file is renewed, +# -1 to only renew if file is older than the original +ttl = -1 + +# ignore -c arg +auto_caching = False + +# path for caching +cache_path = ~/.pyhp/cache + +#handler + directory containing the handler +handler = files_mtime +handler_path = /lib/pyhp/cache_handlers + +[sessions] +enable = True +auto_start = False + +# path argument for handler +path = ~/.pyhp/sessions + +# session handler + directory containing the session handler +handler = files +handler_path = /lib/pyhp/session_handlers + +# lenght of the session id +sid_length = 32 + +# how to serialize/unserialize session data, pickle or json +serialize_handler = pickle + +# config for session cookie +name = PyHPSESSID +cookie_lifetime = 0 +cookie_path = / +cookie_domain = +cookie_secure = True +cookie_httponly = False +cookie_samesite = + +# probability/divisor = probability for carrying out a garbage collection at startup +gc_probability = 1 +gc_divisor = 100 + +# max lifetime of session since last use +gc_maxlifetime = 1440 + +# write only if data has changed +lazy_write = True From a6b28781ff00b3137913feaed023a24d74c53207 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sun, 5 Jan 2020 20:10:34 +0100 Subject: [PATCH 02/42] further cleanup and moving the code for isolation of python code to embed --- .gitignore | 1 + LICENSE | 2 +- pyhp.conf | 13 -------- src/embed.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 .gitignore delete mode 100644 pyhp.conf create mode 100644 src/embed.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..550d67d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +src/__pycache__ diff --git a/LICENSE b/LICENSE index 1bc0ee4..aecd78e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Eric W. +Copyright (c) 2020 Eric W. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/pyhp.conf b/pyhp.conf deleted file mode 100644 index 89063cc..0000000 --- a/pyhp.conf +++ /dev/null @@ -1,13 +0,0 @@ -# Configuration for the PyHP Interpreter (https://github.com/Deric-W/PyHP-Interpreter) -# This file uses the INI syntax - -[parser] -opening_tag = \<\?pyhp[\n \t] # regex -closing_tag = [\n \t]\?\> # regex - -[caching] -allow_caching = True -auto_caching = False # ignore -c arg -cache_path = ~/.pyhpcache # path to use -handler = files_mtime -handler_path = /lib/pyhp/cache_handlers # directory containing the handler diff --git a/src/embed.py b/src/embed.py new file mode 100644 index 0000000..bd1540b --- /dev/null +++ b/src/embed.py @@ -0,0 +1,92 @@ +#!/usr/bin/python3 + +# Module for processing strings embedded in text files, preferably Python code. +# This module is part of the PyHP interpreter (https://github.com/Deric-W/PyHP-Interpreter) + +import re +import marshal +import sys +from io import StringIO +from contextlib import redirect_stdout + +__all__ = [] + +# class for handling strings +class FromString: + # get string, regex to isolate code and optional flags for the regex (default for processing text files) + # the userdata is given to the processor function to allow state + def __init__(self, string, regex, flags=re.MULTILINE|re.DOTALL, userdata=None): + self.sections = re.split(regex, string, flags=flags) + self.userdata = userdata + + # process string with the code replaced by the output of the processor function + # this will modify self.sections + def process(self, processor): + code_sections = 0 + # the first section is always not code, and every code section has string sections as neighbors + for i in range(1, len(self.sections), 2): + code_sections += 1 + self.sections[i] = processor(self.sections[i], self.userdata) + return code_sections + + # process the string and write the string and replaced code parts to file (default is sys.stdout) + # this will not modify self.sections + def execute(self, processor, file=sys.stdout): + code_sections = 0 + for i in range(0, len(self.sections)): + code_sections += 1 + if i % 2 == 1: # uneven index --> code + file.write(processor(self.sections[i], self.userdata)) + else: # even index --> not code + file.write(self.sections[i]) + return code_sections + + def __str__(self): + return "".join(self.sections) + +# wrapper class for handling presplit strings +class FromIter(FromString): + # get presplit string as iterator + def __init__(self, iterator, userdata=None): + self.sections = list(iterator) + self.userdata = userdata + +# function for processing python code +def python_process(code, local_var={}): + stdout = StringIO() + with redirect_stdout(stdout): + # execute in seperatet namespace + exec(code, globals(), local_var) + output = stdout.getvalue() + stdout.close() + return output + +# function for aligning python code in case of a startindentation +def python_align(code, indentation=None): + line_num = 0 + code = code.split("\n") # split to lines + for line in code: + line_num += 1 + if not (not line or line.isspace() or python_is_comment(line)): # ignore non code lines + if indentation == None: # first line of code, get startindentation + indentation = python_get_indentation(line) + if line.startswith(indentation): # if line starts with startindentation + code[line_num - 1] = line[len(indentation):] # remove startindentation + else: + raise IndentationError("File: code processed by python_align Line: %s" % line_num) # raise Exception on bad indentation + return "\n".join(code) # join the lines back together + + +# function for getting the indentation of a line of python code +def python_get_indentation(line): + indentation = "" + for char in line: + if char in " \t": + indentation += char + else: + break + return indentation + +# check if complete line is a comment +def python_is_comment(line): + return line.strip(" \t").startswith("#") From 86f385c01e3a1a50e39f05e9c78d223836c4ce08 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sun, 5 Jan 2020 20:32:23 +0100 Subject: [PATCH 03/42] remove unused import --- src/embed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/embed.py b/src/embed.py index bd1540b..cd628b5 100644 --- a/src/embed.py +++ b/src/embed.py @@ -2,14 +2,14 @@ # Module for processing strings embedded in text files, preferably Python code. # This module is part of the PyHP interpreter (https://github.com/Deric-W/PyHP-Interpreter) +"""Module for processing strings embedded in text files""" import re -import marshal import sys from io import StringIO from contextlib import redirect_stdout -__all__ = [] +__all__ = ["FromString", "FromIter", "python_process", "python_align", "python_get_indentation", "python_is_comment"] # class for handling strings class FromString: From 8368848165e9f4ecd0d61b8ef5a4cd551e0eac12 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Wed, 8 Jan 2020 16:50:16 +0100 Subject: [PATCH 04/42] improve embed.py, tweak structure --- src/pyhp.conf => pyhp.conf | 2 +- src/embed.py | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) rename src/pyhp.conf => pyhp.conf (97%) diff --git a/src/pyhp.conf b/pyhp.conf similarity index 97% rename from src/pyhp.conf rename to pyhp.conf index 907c1e1..cf67e41 100644 --- a/src/pyhp.conf +++ b/pyhp.conf @@ -37,7 +37,7 @@ auto_caching = False # path for caching cache_path = ~/.pyhp/cache -#handler + directory containing the handler +# handler + directory containing the handler handler = files_mtime handler_path = /lib/pyhp/cache_handlers diff --git a/src/embed.py b/src/embed.py index cd628b5..dfbf9df 100644 --- a/src/embed.py +++ b/src/embed.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # Module for processing strings embedded in text files, preferably Python code. -# This module is part of the PyHP interpreter (https://github.com/Deric-W/PyHP-Interpreter) +# This module is part of the PyHP interpreter (https://github.com/Deric-W/PyHP) """Module for processing strings embedded in text files""" import re @@ -9,7 +9,7 @@ from io import StringIO from contextlib import redirect_stdout -__all__ = ["FromString", "FromIter", "python_process", "python_align", "python_get_indentation", "python_is_comment"] +__all__ = ["FromString", "FromIter", "python_process", "python_execute", "python_align", "python_get_indentation", "python_is_comment"] # class for handling strings class FromString: @@ -29,16 +29,16 @@ def process(self, processor): self.sections[i] = processor(self.sections[i], self.userdata) return code_sections - # process the string and write the string and replaced code parts to file (default is sys.stdout) - # this will not modify self.sections - def execute(self, processor, file=sys.stdout): + # process the string and write the string and replaced code parts to sys.stdout + # this will not modify self.sections an requires an processor to write the data himself + def execute(self, processor): code_sections = 0 for i in range(0, len(self.sections)): code_sections += 1 if i % 2 == 1: # uneven index --> code - file.write(processor(self.sections[i], self.userdata)) + processor(self.sections[i], self.userdata) else: # even index --> not code - file.write(self.sections[i]) + sys.stdout.write(self.sections[i]) return code_sections def __str__(self): @@ -56,11 +56,16 @@ def python_process(code, local_var={}): stdout = StringIO() with redirect_stdout(stdout): # execute in seperatet namespace - exec(code, globals(), local_var) + exec(python_align(code), globals(), local_var) output = stdout.getvalue() stdout.close() return output +# function for executing python code +# ignore file because python code will alway have stdout as stdout +def python_execute(code, local_var={}): + exec(python_align(code), globals(), local_var) + # function for aligning python code in case of a startindentation def python_align(code, indentation=None): line_num = 0 From 4ba135863aa598093eed32e64950116d2da2ec98 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Wed, 8 Jan 2020 21:03:03 +0100 Subject: [PATCH 05/42] fix typos in embed.py --- .gitignore | 1 + src/embed.py | 3 +-- src/libpyhp.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/libpyhp.py diff --git a/.gitignore b/.gitignore index 550d67d..d310dba 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ src/__pycache__ +notes.txt diff --git a/src/embed.py b/src/embed.py index dfbf9df..367ceb1 100644 --- a/src/embed.py +++ b/src/embed.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # Module for processing strings embedded in text files, preferably Python code. -# This module is part of the PyHP interpreter (https://github.com/Deric-W/PyHP) +# This module is part of PyHP (https://github.com/Deric-W/PyHP) """Module for processing strings embedded in text files""" import re @@ -62,7 +62,6 @@ def python_process(code, local_var={}): return output # function for executing python code -# ignore file because python code will alway have stdout as stdout def python_execute(code, local_var={}): exec(python_align(code), globals(), local_var) diff --git a/src/libpyhp.py b/src/libpyhp.py new file mode 100644 index 0000000..1ee899e --- /dev/null +++ b/src/libpyhp.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 + +# Module containing multiple Python implementations of functions from PHP and utilities +# This module is part of PyHP (https://github.com/Deric-W/PyHP) +"""Module containing multiple Python implementations of functions from PHP and utilities""" + +import time +REQUEST_TIME = time.time() # found no better solution +import sys +import os +import cgi +import http +import urllib.parse +from collections import defaultdict + +__all__ = ["PyHP", "dummy_cache_handler", "dummy_session_handler"] + +# class containing the implementations +class PyHP: + pass + + +# Class containing a fallback cache handler (with no function) +class dummy_cache_handler: + def __init__(self, cache_path, file_path, config): + pass + + def is_available(self): + return False # we are only a fallback + + def is_outdated(self): + return False + + def save(self, code): + pass + + def load(self): + return ("WARNING: This is the dummy cache handler of the libpyhp module, iam providing no useful functions and are just fallback", ) # return warning + + def close(self): + pass + + +# Class containing a fallback session handler (with no function) +class dummy_session_handler: + pass \ No newline at end of file From d3eb75bf4b523f697fb1783318a5fa9bed86aaff Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Wed, 8 Jan 2020 22:10:02 +0100 Subject: [PATCH 06/42] add SERVER, REQUEST, GET, POST and COOKIE to libpyhp.py --- src/libpyhp.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/src/libpyhp.py b/src/libpyhp.py index 1ee899e..86ca98e 100644 --- a/src/libpyhp.py +++ b/src/libpyhp.py @@ -13,11 +13,129 @@ import urllib.parse from collections import defaultdict -__all__ = ["PyHP", "dummy_cache_handler", "dummy_session_handler"] +__all__ = ["PyHP", "dummy_cache_handler", "dummy_session_handler", "parse_get", "parse_post", "parse_cookie", "dict2defaultdict"] # class containing the implementations class PyHP: - pass + def __init__(self, # build GET, POST, COOKIE, SERVER, REQUEST + request_order = ("GET", "POST", "COOKIE"), # order in wich REQUEST gets updated + keep_blank_values = True, # if to not remove "" values + fallback_value = None, # fallback value of GET, POST, REQUEST and COOKIE if not None + enable_post_data_reading = False, # if not to parse POST and consume stdin in the process + default_mimetype = "text/html" # Content-Type header if not been set + ): + self.__FILE__ = os.path.abspath(sys.argv[0]) # absolute path of script + + self.SERVER = { # incomplete (AUTH) + "PyHP_SELF": os.path.relpath(self.__FILE__, os.getenv("DOCUMENT_ROOT", default=os.curdir)), + "argv": os.getenv("QUERY_STRING", default=sys.argv), + "argc": len(sys.argv), + "GATEWAY_INTERFACE": os.getenv("GATEWAY_INTERFACE", default=""), + "SERVER_ADDR": os.getenv("SERVER_ADDR", default=""), + "SERVER_NAME": os.getenv("SERVER_NAME", default=""), + "SERVER_SOFTWARE": os.getenv("SERVER_SOFTWARE", default=""), + "SERVER_PROTOCOL": os.getenv("SERVER_PROTOCOL", default=""), + "REQUEST_METHOD": os.getenv("REQUEST_METHOD", default=""), + "REQUEST_TIME": int(REQUEST_TIME), + "REQUEST_TIME_FLOAT": REQUEST_TIME, + "QUERY_STRING": os.getenv("QUERY_STRING", default=""), + "DOCUMENT_ROOT": os.getenv("DOCUMENT_ROOT", default=""), + "HTTP_ACCEPT": os.getenv("HTTP_ACCEPT", default=""), + "HTTP_ACCEPT_CHARSET": os.getenv("HTTP_ACCEPT_CHARSET", default=""), + "HTTP_ACCEPT_ENCODING": os.getenv("HTTP_ACCEPT_ENCODING", default=""), + "HTTP_ACCEPT_LANGUAGE": os.getenv("HTTP_ACCEPT_LANGUAGE", default=""), + "HTTP_CONNECTION": os.getenv("HTTP_CONNECTION", default=""), + "HTTP_HOST": os.getenv("HTTP_HOST", default=""), + "HTTP_REFERER": os.getenv("HTTP_REFERER", default=""), + "HTTP_USER_AGENT": os.getenv("HTTP_USER_AGENT", default=""), + "HTTPS": os.getenv("HTTPS", default=""), + "REMOTE_ADDR": os.getenv("REMOTE_ADDR", default=""), + "REMOTE_HOST": os.getenv("REMOTE_HOST", default=""), + "REMOTE_PORT": os.getenv("REMOTE_PORT", default=""), + "REMOTE_USER": os.getenv("REMOTE_USER", default=""), + "REDIRECT_REMOTE_USER": os.getenv("REDIRECT_REMOTE_USER", default=""), + "SCRIPT_FILENAME": self.__FILE__, + "SERVER_ADMIN": os.getenv("SERVER_ADMIN", default=""), + "SERVER_PORT": os.getenv("SERVER_PORT", default=""), + "SERVER_SIGNATURE": os.getenv("SERVER_SIGNATURE", default=""), + "PATH_TRANSLATED": os.getenv("PATH_TRANSLATED", default=""), + "SCRIPT_NAME": os.getenv("SCRIPT_NAME", default=""), + "REQUEST_URI": os.getenv("REQUEST_URI", default=""), + "PyHP_AUTH_DIGEST": "", + "PyHP_AUTH_USER": "", + "PyHP_AUTH_PW": "", + "AUTH_TYPE": os.getenv("AUTH_TYPE", default=""), + "PATH_INFO": os.getenv("PATH_INFO", default=""), + "ORIG_PATH_INFO": os.getenv("PATH_INFO", default="") + } + + # start processing GET, POST and COOKIE + self.GET = dict2defaultdict(parse_get(keep_blank_values), fallback_value) + self.COOKIE = dict2defaultdict(parse_cookie(keep_blank_values), fallback_value) + if enable_post_data_reading: # dont consume stdin + self.POST = dict2defaultdict({}, fallback_value) + else: # parse POST and consume stdin + self.POST = dict2defaultdict(parse_post(keep_blank_values), fallback_value) + + # build REQUEST + self.REQUEST = dict2defaultdict({}, fallback_value) # empthy REQUEST + for request in request_order: # update REQUEST in the order given by request_order + if request == "GET": + self.REQUEST.update(self.GET) + elif request == "POST": + self.REQUEST.update(self.POST) + elif request == "COOKIE": + self.REQUEST.update(self.COOKIE) + else: # ignore unknown methods + pass + + + + +def parse_get(keep_blank_values=True): + return urllib.parse.parse_qs(os.getenv("QUERY_STRING", default=""), keep_blank_values=keep_blank_values) + +def parse_post(keep_blank_values=True): + environ = os.environ.copy() # dont modify original environ + environ["QUERY_STRING"] = "" # prevent th eparsing of GET + return cgi.parse(environ=environ, keep_blank_values=keep_blank_values) + +def parse_cookie(keep_blank_values=True): + cookie_string = os.getenv("HTTP_COOKIE", default="") + cookie_dict = {} + for cookie in cookie_string.split("; "): + cookie = cookie.split("=", maxsplit=1) # to allow multiple "=" in value + if len(cookie) == 1: # blank cookie + if keep_blank_values: + cookie.append("") + else: + continue # skip cookie + if cookie[1] == "" and not keep_blank_values: # skip cookie + pass + else: + cookie[0] = urllib.parse.unquote_plus(cookie[0]) # unquote name and value + cookie[1] = urllib.parse.unquote_plus(cookie[1]) + if cookie[0] in cookie_dict: + cookie_dict[cookie[0]].append(cookie[1]) # key already existing + else: + cookie_dict[cookie[0]] = [cookie[1]] # make new key + return cookie_dict + +# convert the dicts of parse_(get, post, cookie) to defaultdict +def dict2defaultdict(_dict, fallback=None): + if fallback is None: + output = {} # no fallback wanted, use normal dict + else: + output = defaultdict(lambda: fallback) + for key, value in _dict.items(): + if len(value) > 1: # multiple values, stays list + output[key] = value + elif len(value) == 1: # single element, free from list + output[key] = value[0] + else: # empthy list, use fallback if provided + pass + return output + # Class containing a fallback cache handler (with no function) From bed67ecb97fa264b2e6700e166108e71e80ce90d Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Thu, 9 Jan 2020 20:59:26 +0100 Subject: [PATCH 07/42] add header functios to libpyhp and fix __FILE__ --- src/libpyhp.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/src/libpyhp.py b/src/libpyhp.py index 86ca98e..97fe21b 100644 --- a/src/libpyhp.py +++ b/src/libpyhp.py @@ -18,13 +18,18 @@ # class containing the implementations class PyHP: def __init__(self, # build GET, POST, COOKIE, SERVER, REQUEST + file_path = sys.argv[0], # override if not directly executed request_order = ("GET", "POST", "COOKIE"), # order in wich REQUEST gets updated keep_blank_values = True, # if to not remove "" values fallback_value = None, # fallback value of GET, POST, REQUEST and COOKIE if not None enable_post_data_reading = False, # if not to parse POST and consume stdin in the process default_mimetype = "text/html" # Content-Type header if not been set ): - self.__FILE__ = os.path.abspath(sys.argv[0]) # absolute path of script + self.__FILE__ = os.path.abspath(file_path) # absolute path of script + self.response_code = 200 + self.headers = [["Content-Type", default_mimetype]] + self.header_sent = False + self.header_callback = lambda: None self.SERVER = { # incomplete (AUTH) "PyHP_SELF": os.path.relpath(self.__FILE__, os.getenv("DOCUMENT_ROOT", default=os.curdir)), @@ -89,8 +94,78 @@ def __init__(self, # build GET, POST, COOKIE, SERVER, REQUEST else: # ignore unknown methods pass + # set new response code return the old one + # if no code has been set it will return 200 + def http_response_code(self, response_code=None): + old_code = self.response_code + if response_code is not None: + self.response_code = response_code + return old_code # is the current one if no response code has been provided + + # set http header + # if replace=True replace existing headers of the same type, else simply add + def header(self, header, replace=True, http_response_code=None): + header = header.split("\n")[0] # prevent header injection + header = header.split(":", maxsplit=1) # split header into name and value + header = [part.strip() for part in header] # remove whitespace + if len(header) == 1: # no value provided + header.append("") # add empthy value + if replace: + repleaced = False # indicate if headers of the same type were found + for i in range(0, len(self.headers)): # need index for changing headers + if self.headers[i][0].lower() == header[0].lower(): # found same header + self.headers[i][1] = header[1] # repleace + repleaced = True + if not repleaced: # header not already set + self.headers.append(header) + else: + self.headers.append(header) + + # list set headers + def headers_list(self): + headers = [] + for header in self.headers: + headers.append(": ".join(header)) # add header like received by the client + return headers + + # remove header with matching name + def header_remove(self, name): + name = name.lower() + self.headers = [header for header in self.headers if header[0].lower() != name] # remove headers with same name + + # return if header have been sent + # unlike the PHP function it does not have file and line arguments + def headers_sent(self): + return self.header_sent + + # set calback to be executed just before headers are send + # callback gets no arguments and the return value is ignored + def header_register_callback(self, callback): + if not self.header_sent: + self.header_callback = callback + return True + else: + return False + + # send headers and execute callback + def send_headers(self): + if not self.header_sent: # send + self.header_sent = True # prevent recursion if callback prints output or headers set + self.header_callback() # execute callback + print("Status:" , self.response_code, http.HTTPStatus(self.response_code).phrase) + for header in self.headers: + print(": ".join(header)) + print() # end of headers + else: # already sent + pass - + # make wrapper for target function to call send_headers if wrapped function is used, like print + def make_header_wrapper(self, target=sys.stdout): + def wrapper(*args, **kwargs): + if not self.header_sent: + self.send_headers() + target(*args, **kwargs) # call target with arguments + return wrapper def parse_get(keep_blank_values=True): return urllib.parse.parse_qs(os.getenv("QUERY_STRING", default=""), keep_blank_values=keep_blank_values) @@ -153,7 +228,7 @@ def save(self, code): pass def load(self): - return ("WARNING: This is the dummy cache handler of the libpyhp module, iam providing no useful functions and are just fallback", ) # return warning + return ("WARNING: This is the dummy cache handler of the libpyhp module, iam providing no useful functions and are just a fallback", ) # return warning def close(self): pass From 1da83a6847d675bea08b1d92f42db04e7502bd08 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Thu, 9 Jan 2020 22:27:10 +0100 Subject: [PATCH 08/42] add cookie functions to libpyhp, fix default for make_header_wrapper --- src/libpyhp.py | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/libpyhp.py b/src/libpyhp.py index 97fe21b..2b4718f 100644 --- a/src/libpyhp.py +++ b/src/libpyhp.py @@ -160,13 +160,54 @@ def send_headers(self): pass # make wrapper for target function to call send_headers if wrapped function is used, like print - def make_header_wrapper(self, target=sys.stdout): - def wrapper(*args, **kwargs): + # use like print = PyHP.make_header_wrapper(print) + def make_header_wrapper(self, target=print): + def wrapper(*args, **kwargs): # wrapper forwards all args and kwargs to target function if not self.header_sent: self.send_headers() target(*args, **kwargs) # call target with arguments return wrapper + # set Set-Cookie header, but quote special characters in name and value + # same with expires as setrawcookie + def setcookie(self, name, value="", expires=0, path="", domain="", secure=False, httponly=False): + name = urllib.parse.quote_plus(name) + value = urllib.parse.quote_plus(value) + return self.setrawcookie(name, value, expires, path, domain, secure, httponly) + + # set Set-Cookie header + # if expires is a dict the arguments are read from it + def setrawcookie(self, name, value="", expires=0, path="", domain="", secure=False, httponly=False): + if self.header_sent: + return False + else: + if type(expires) == dict: # options array + path = expires.get("path", "") + domain = expires.get("domain", "") + secure = expires.get("secure", False) + httponly = expires.get("httponly", False) + samesite = expires.get("samesite", "") + expires = expires.get("expires", 0) # has to happen at the end because it overrides expires + else: + samesite = "" # somehow not as keyword argument in PHP + cookie = "Set-Cookie: %s=%s" % (name, value) + if expires != 0: + cookie += "; " + "Expires=%s" % time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time() + expires)) # add Expires and Max-Age just in case + cookie += "; " + "Max-Age=%d" % expires + if path != "": + cookie += "; " + "Path=%s" % path + if domain != "": + cookie += "; " + "Domain=%s" % domain + if secure: + cookie += "; " + "Secure" + if httponly: + cookie += "; " + "HttpOnly" + if samesite != "": + cookie += "; " + "SameSite=%s" % samesite + self.header(cookie, False) + return True + + def parse_get(keep_blank_values=True): return urllib.parse.parse_qs(os.getenv("QUERY_STRING", default=""), keep_blank_values=keep_blank_values) @@ -212,7 +253,6 @@ def dict2defaultdict(_dict, fallback=None): return output - # Class containing a fallback cache handler (with no function) class dummy_cache_handler: def __init__(self, cache_path, file_path, config): From c18c752d8c478a6a8d21397d3ad8be4805a1a24e Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Thu, 9 Jan 2020 22:50:18 +0100 Subject: [PATCH 09/42] add functions for shutdown functions --- src/libpyhp.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libpyhp.py b/src/libpyhp.py index 2b4718f..ab3d641 100644 --- a/src/libpyhp.py +++ b/src/libpyhp.py @@ -30,6 +30,7 @@ def __init__(self, # build GET, POST, COOKIE, SERVER, REQUEST self.headers = [["Content-Type", default_mimetype]] self.header_sent = False self.header_callback = lambda: None + self.shutdown_functions = [] self.SERVER = { # incomplete (AUTH) "PyHP_SELF": os.path.relpath(self.__FILE__, os.getenv("DOCUMENT_ROOT", default=os.curdir)), @@ -207,6 +208,21 @@ def setrawcookie(self, name, value="", expires=0, path="", domain="", secure=Fal self.header(cookie, False) return True + # register function to be run at shutdown + # multiple functions are run in the order they have been registerd + def register_shutdown_function(self, callback, *args, **kwargs): + self.shutdown_functions.append((callback, args, kwargs)) + + # run the shutdown functions in the order they have been registerd + # dont call run_shutdown_functions from a shutdown_function, it will cause infinite recursion + def run_shutdown_functions(self): + for function, args, kwargs in self.shutdown_functions: + function(*args, **kwargs) + + # destructor used to run shutdown functions + def __del__(self): + self.run_shutdown_functions() + def parse_get(keep_blank_values=True): return urllib.parse.parse_qs(os.getenv("QUERY_STRING", default=""), keep_blank_values=keep_blank_values) From e4105b6c4cc83fa87dc87b890cfb5ff4123814ee Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Thu, 9 Jan 2020 23:18:25 +0100 Subject: [PATCH 10/42] convert to package like structure --- .gitignore | 2 +- pyhp/__init__.py | 19 +++++++++++++++++++ pyhp/__main__.py | 9 +++++++++ {src => pyhp}/embed.py | 0 {src => pyhp}/libpyhp.py | 0 pyhp/main.py | 17 +++++++++++++++++ 6 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 pyhp/__init__.py create mode 100644 pyhp/__main__.py rename {src => pyhp}/embed.py (100%) rename {src => pyhp}/libpyhp.py (100%) create mode 100644 pyhp/main.py diff --git a/.gitignore b/.gitignore index d310dba..f49057b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -src/__pycache__ +pyhp/__pycache__ notes.txt diff --git a/pyhp/__init__.py b/pyhp/__init__.py new file mode 100644 index 0000000..c032793 --- /dev/null +++ b/pyhp/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/python3 + +"""Package for running PyHP Scripts""" + +# import all modules +from . import embed +from . import libpyhp +from . import main + +# for import * +__all__ = ["embed", "libpyhp", "main"] + +__version__ = "2.0" +__author__ = "Eric Wolf" +__maintainer__ = "Eric Wolf" +__license__ = "MIT" +__email__ = "robo-eric@gmx.de" # please dont use for spam :( +__contact__ = "https://github.com/Deric-W/PyHP" + diff --git a/pyhp/__main__.py b/pyhp/__main__.py new file mode 100644 index 0000000..9dcaf16 --- /dev/null +++ b/pyhp/__main__.py @@ -0,0 +1,9 @@ +#!/usr/bin/python3 + +# script to support python3 -m pyhp + +import sys +from . import main + +# exit with the return code of main +sys.exit(main.main()) diff --git a/src/embed.py b/pyhp/embed.py similarity index 100% rename from src/embed.py rename to pyhp/embed.py diff --git a/src/libpyhp.py b/pyhp/libpyhp.py similarity index 100% rename from src/libpyhp.py rename to pyhp/libpyhp.py diff --git a/pyhp/main.py b/pyhp/main.py new file mode 100644 index 0000000..aab791a --- /dev/null +++ b/pyhp/main.py @@ -0,0 +1,17 @@ +#!/usr/bin/python3 + +"""Module containing the main function(s) of the PyHP Interpreter""" +# This module is part of PyHP (https://github.com/Deric-W/PyHP) + +from . import embed +from . import libpyhp + +__all__ = ["main", "manual_main"] + +# start the PyHP Interpreter +def main(): + pass + +# start the PyHP Interpreter with predefined arguments +def manual_main(file, caching=False, config="/etc/pyhp.conf"): + pass \ No newline at end of file From e2a273bec47dd3f1cb732d66f22f3af3f61cad1f Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 10 Jan 2020 15:50:48 +0100 Subject: [PATCH 11/42] improved destructor --- pyhp/libpyhp.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index ab3d641..123196a 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -31,6 +31,7 @@ def __init__(self, # build GET, POST, COOKIE, SERVER, REQUEST self.header_sent = False self.header_callback = lambda: None self.shutdown_functions = [] + self.shutdown_functions_run = False self.SERVER = { # incomplete (AUTH) "PyHP_SELF": os.path.relpath(self.__FILE__, os.getenv("DOCUMENT_ROOT", default=os.curdir)), @@ -149,16 +150,14 @@ def header_register_callback(self, callback): return False # send headers and execute callback + # DO NOT call this function from a header callback to prevent infinite recursion def send_headers(self): - if not self.header_sent: # send - self.header_sent = True # prevent recursion if callback prints output or headers set - self.header_callback() # execute callback - print("Status:" , self.response_code, http.HTTPStatus(self.response_code).phrase) - for header in self.headers: - print(": ".join(header)) - print() # end of headers - else: # already sent - pass + self.header_sent = True # prevent recursion if callback prints output or headers set + self.header_callback() # execute callback + print("Status:" , self.response_code, http.HTTPStatus(self.response_code).phrase) + for header in self.headers: + print(": ".join(header)) + print() # end of headers # make wrapper for target function to call send_headers if wrapped function is used, like print # use like print = PyHP.make_header_wrapper(print) @@ -214,24 +213,31 @@ def register_shutdown_function(self, callback, *args, **kwargs): self.shutdown_functions.append((callback, args, kwargs)) # run the shutdown functions in the order they have been registerd - # dont call run_shutdown_functions from a shutdown_function, it will cause infinite recursion + # DO NOT call run_shutdown_functions from a shutdown_function, it will cause infinite recursion def run_shutdown_functions(self): + self.shutdown_functions_run = True for function, args, kwargs in self.shutdown_functions: function(*args, **kwargs) - # destructor used to run shutdown functions + # destructor used to run shutdown and header functions if needed def __del__(self): - self.run_shutdown_functions() + if not self.header_sent: + self.send_headers() + if not self.shutdown_functions_run: + self.run_shutdown_functions() +# parse get values from query string def parse_get(keep_blank_values=True): return urllib.parse.parse_qs(os.getenv("QUERY_STRING", default=""), keep_blank_values=keep_blank_values) +# parse only post data def parse_post(keep_blank_values=True): environ = os.environ.copy() # dont modify original environ environ["QUERY_STRING"] = "" # prevent th eparsing of GET return cgi.parse(environ=environ, keep_blank_values=keep_blank_values) +# parse cookie string def parse_cookie(keep_blank_values=True): cookie_string = os.getenv("HTTP_COOKIE", default="") cookie_dict = {} @@ -292,4 +298,4 @@ def close(self): # Class containing a fallback session handler (with no function) class dummy_session_handler: - pass \ No newline at end of file + pass From 54f194a3e18244cb6305a6f81dc7936661853a2f Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 10 Jan 2020 20:25:29 +0100 Subject: [PATCH 12/42] make embed.py ignore empthy non-code sections in execute --- pyhp/embed.py | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/pyhp/embed.py b/pyhp/embed.py index 367ceb1..82a904b 100644 --- a/pyhp/embed.py +++ b/pyhp/embed.py @@ -9,7 +9,7 @@ from io import StringIO from contextlib import redirect_stdout -__all__ = ["FromString", "FromIter", "python_process", "python_execute", "python_align", "python_get_indentation", "python_is_comment"] +__all__ = ["FromString", "FromIter", "python_execute", "python_compile", "python_execute_compiled", "python_align", "python_get_indentation", "python_is_comment"] # class for handling strings class FromString: @@ -38,12 +38,14 @@ def execute(self, processor): if i % 2 == 1: # uneven index --> code processor(self.sections[i], self.userdata) else: # even index --> not code - sys.stdout.write(self.sections[i]) + if self.sections[i]: # ignore empthy sections + sys.stdout.write(self.sections[i]) return code_sections def __str__(self): return "".join(self.sections) + # wrapper class for handling presplit strings class FromIter(FromString): # get presplit string as iterator @@ -51,19 +53,33 @@ def __init__(self, iterator, userdata=None): self.sections = list(iterator) self.userdata = userdata -# function for processing python code -def python_process(code, local_var={}): - stdout = StringIO() - with redirect_stdout(stdout): - # execute in seperatet namespace - exec(python_align(code), globals(), local_var) - output = stdout.getvalue() - stdout.close() - return output # function for executing python code -def python_execute(code, local_var={}): - exec(python_align(code), globals(), local_var) +# userdata = (locals, section_number), init with [{}, 0] +def python_execute(code, userdata): + userdata[1] += 1 + try: + exec(python_align(code), globals(), userdata[0]) + except Exception as e: + raise Exception("Exception during executing of section %d" % userdata[1]) from e + +# compile python code sections +# userdata = section_number, init 0 +def python_compile(code, userdata): + userdata += 1 + try: + return compile(python_align(code), "", "exec") + except Exception as e: + raise Exception("Exception during executing of section %d" % userdata) from e + +# execute compiled python sections +# userdata is the same as python_execute +def python_execute_compiled(code, userdata): + userdata[1] += 1 + try: + exec(code, globals(), userdata[0]) + except Exception as e: + raise Exception("Exception during executing of section %d" % userdata[1]) from e # function for aligning python code in case of a startindentation def python_align(code, indentation=None): @@ -77,7 +93,7 @@ def python_align(code, indentation=None): if line.startswith(indentation): # if line starts with startindentation code[line_num - 1] = line[len(indentation):] # remove startindentation else: - raise IndentationError("File: code processed by python_align Line: %s" % line_num) # raise Exception on bad indentation + raise IndentationError("File: code processed by python_align Line: %d" % line_num) # raise Exception on bad indentation return "\n".join(code) # join the lines back together From 83aa32a4ce986291926ffe3872869a242025821d Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 10 Jan 2020 20:28:14 +0100 Subject: [PATCH 13/42] remove __del__ because of bad controllable header sending --- pyhp/libpyhp.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index 123196a..713b02f 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -219,13 +219,6 @@ def run_shutdown_functions(self): for function, args, kwargs in self.shutdown_functions: function(*args, **kwargs) - # destructor used to run shutdown and header functions if needed - def __del__(self): - if not self.header_sent: - self.send_headers() - if not self.shutdown_functions_run: - self.run_shutdown_functions() - # parse get values from query string def parse_get(keep_blank_values=True): From 407cef72018c615d5f08d44d229a6eacea26b0fe Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 10 Jan 2020 20:29:51 +0100 Subject: [PATCH 14/42] add main module, change a few names in the caching section of pyhp.conf --- pyhp.conf | 10 +++--- pyhp/main.py | 96 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/pyhp.conf b/pyhp.conf index cf67e41..c14ae64 100644 --- a/pyhp.conf +++ b/pyhp.conf @@ -3,6 +3,7 @@ [parser] # regex to isolate the code +# escape sequences are processed regex = \\<\\?pyhp[\\s](.*?)[\\s]\\?\\> [request] @@ -32,14 +33,13 @@ max_size = 16 ttl = -1 # ignore -c arg -auto_caching = False +auto = False # path for caching -cache_path = ~/.pyhp/cache +path = ~/.pyhp/cache -# handler + directory containing the handler -handler = files_mtime -handler_path = /lib/pyhp/cache_handlers +# path to handler +handler_path = /lib/pyhp/cache_handlers/files-mtime.py [sessions] enable = True diff --git a/pyhp/main.py b/pyhp/main.py index aab791a..d911e27 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -3,15 +3,101 @@ """Module containing the main function(s) of the PyHP Interpreter""" # This module is part of PyHP (https://github.com/Deric-W/PyHP) +import sys +import os +import argparse +import configparser +import importlib +import atexit from . import embed from . import libpyhp -__all__ = ["main", "manual_main"] +__all__ = ["main", "manual_main", "prepare_file", "prepare_path", "import_path", "check_if_caching"] -# start the PyHP Interpreter +# start the PyHP Interpreter (wrapper for manual_main) def main(): - pass + parser = argparse.ArgumentParser(description="Interpreter for .pyhp Scripts (https://github.com/Deric-W/PyHP)") + parser.add_argument("-c", "--caching", help="enable caching (requires file)", action="store_true") + parser.add_argument("file", type=str, help="file to be interpreted (omit for reading from stdin)", nargs="?", default="") + parser.add_argument("--config", type=str, help="path to custom config file", nargs="?", const="/etc/pyhp.conf", default="/etc/pyhp.conf") + args = parser.parse_args() + manual_main(args.file, caching=args.caching, config_file=args.config) + # start the PyHP Interpreter with predefined arguments -def manual_main(file, caching=False, config="/etc/pyhp.conf"): - pass \ No newline at end of file +def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): + config = configparser.ConfigParser(inline_comment_prefixes="#") # allow inline comments + if config_file not in config.read(config_file): # reading file failed + raise ValueError("failed to read config file %s" % config_file) + + # prepare the PyHP Object + PyHP = libpyhp.PyHP(file_path=file_path, + request_order=config.get("request", "request_order", fallback="GET POST COOKIE").split(), + keep_blank_values=config.getboolean("request", "keep_blank_values", fallback=True), + fallback_value=config.get("request", "fallback_value", fallback=""), + enable_post_data_reading=config.getboolean("request", "enable_post_data_reading", fallback=False), + default_mimetype=config.get("request", "default_mimetype", fallback="text/html") + ) + sys.stdout.write = PyHP.make_header_wrapper(sys.stdout.write) #wrap stdout + atexit.register(PyHP.run_shutdown_functions) # just to be sure + + # handle caching + regex = config.get("parser", "regex", fallback="\\<\\?pyhp[\\s](.*?)[\\s]\\?\\>").encode("utf8").decode("unicode_escape") # process escape sequences like \n + caching_enabled = config.getboolean("caching", "enable", fallback=True) + caching_allowed = config.getboolean("caching", "auto", fallback=False) + # if file is not stdin and caching is enabled and wanted or auto_caching is enabled + if check_if_caching(file_path, caching, caching_enabled, caching_allowed): + handler_path = os.path.splitext(prepare_path(config.get("caching", "handler_path", fallback="/lib/pyhp/cache_handlers/files-mtime.py"))) # get neccesary data + cache_path = prepare_path(config.get("caching", "path", fallback="~/.pyhp/cache")) + handler = import_path(handler_path).Handler(cache_path, file_path, config["caching"]) # init handler + if handler.is_available(): # check if caching is possible + cached = True + if handler.is_outdated(): # update cache + code = embed.FromString(prepare_file(file_path), regex, userdata=0) # set userdata for python_compile + code.process(embed.python_compile) # compile python sections + code.userdata = [{"PyHP": PyHP}, 0] # set userdata for python_execute_compiled + handler.save(code) + else: # load cache + code = embed.FromIter(handler.load(), userdata=[{"PyHP": PyHP}, 0]) + else: # generate FromString Object + cached = False + code = embed.FromString(prepare_file(file_path), regex, userdata=[{"PyHP": PyHP}, 0]) + else: # same as above + cached = False + code = embed.FromString(prepare_file(file_path), regex, userdata=[{"PyHP": PyHP}, 0]) + + if cached: # run compiled code + code.execute(embed.python_execute_compiled) + else: # run normal code + code.execute(embed.python_execute) + + if not PyHP.headers_sent(): # prevent error if no output occured, but not if and exception occured + PyHP.send_headers() + +# prepare path for use +def prepare_path(path): + return os.path.expanduser(path) + +# import file at path +def import_path(path): + sys.path.insert(0, os.path.dirname(path)) + path = importlib.import_module(os.path.basename(path)) + del sys.path[0] + return path + +# check we should cache +def check_if_caching(file_path, caching, enabled, auto): + possible = file_path != "" # file is not stdin + allowed = (caching or auto) and enabled # if caching is wanted and enabled + return possible and allowed + +# get code and remove shebang +def prepare_file(path): + if path == "": + code = sys.stdin.read() + else: + with open(path, "r") as fd: + code = fd.read() + if code.startswith("#!"): # remove shebang + code = code.split("\n", maxsplit=1)[-1] # remove first line + return code From 1903ce09b9f39ddc753aa47172c7d872fef3d140 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 10 Jan 2020 20:32:24 +0100 Subject: [PATCH 15/42] modify fib.pyhp for tests, new ones coming soon --- examples/fib.pyhp | 10 +++++----- pyhp/main.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/fib.pyhp b/examples/fib.pyhp index 74fe769..394fd9a 100644 --- a/examples/fib.pyhp +++ b/examples/fib.pyhp @@ -24,11 +24,11 @@ stelle_2 = stelle_2 + stelle_1 print(stelle_2) print("
") - #pyhp.REQUEST = {"fib":"x"} - if "fib" in pyhp.REQUEST: - print("Dies die Fibonaccizahlen von der 0ten bis " + str(pyhp.REQUEST["fib"]) + "ten Stelle:
") + #PyHP.REQUEST = {"fib":"x"} + if "fib" in PyHP.REQUEST: + print("Dies die Fibonaccizahlen von der 0ten bis " + str(PyHP.REQUEST["fib"]) + "ten Stelle:
") try: - fib(int(pyhp.REQUEST["fib"])) + fib(int(PyHP.REQUEST["fib"])) except: print("Fehler während der Berechnung!
") else: @@ -39,4 +39,4 @@ - \ No newline at end of file + diff --git a/pyhp/main.py b/pyhp/main.py index d911e27..6bfc9ad 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -39,7 +39,7 @@ def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): default_mimetype=config.get("request", "default_mimetype", fallback="text/html") ) sys.stdout.write = PyHP.make_header_wrapper(sys.stdout.write) #wrap stdout - atexit.register(PyHP.run_shutdown_functions) # just to be sure + atexit.register(PyHP.run_shutdown_functions) # run shutdown functions even if a exception occured # handle caching regex = config.get("parser", "regex", fallback="\\<\\?pyhp[\\s](.*?)[\\s]\\?\\>").encode("utf8").decode("unicode_escape") # process escape sequences like \n From 750ddebd00cbf79d6bcb864a94d50b5aa32d1458 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 10 Jan 2020 22:20:04 +0100 Subject: [PATCH 16/42] make return code contain errno in case of Exception --- pyhp/embed.py | 2 +- pyhp/main.py | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pyhp/embed.py b/pyhp/embed.py index 82a904b..dda89bc 100644 --- a/pyhp/embed.py +++ b/pyhp/embed.py @@ -61,7 +61,7 @@ def python_execute(code, userdata): try: exec(python_align(code), globals(), userdata[0]) except Exception as e: - raise Exception("Exception during executing of section %d" % userdata[1]) from e + raise Exception("Exception during execution of section %d" % userdata[1]) from e # compile python code sections # userdata = section_number, init 0 diff --git a/pyhp/main.py b/pyhp/main.py index 6bfc9ad..d125ddf 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -9,6 +9,8 @@ import configparser import importlib import atexit +import errno +from traceback import print_exception from . import embed from . import libpyhp @@ -21,14 +23,22 @@ def main(): parser.add_argument("file", type=str, help="file to be interpreted (omit for reading from stdin)", nargs="?", default="") parser.add_argument("--config", type=str, help="path to custom config file", nargs="?", const="/etc/pyhp.conf", default="/etc/pyhp.conf") args = parser.parse_args() - manual_main(args.file, caching=args.caching, config_file=args.config) - + try: + manual_main(args.file, caching=args.caching, config_file=args.config) + except Exception as e: # catch all exceptions + print_exception(e, e, e.__traceback__) # print traceback and exception + if hasattr(e, "errno"): # if the exception provides a errno + return e.errno + else: # return standard error code + return 1 + else: # no exeption happend + return 0 # start the PyHP Interpreter with predefined arguments def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): config = configparser.ConfigParser(inline_comment_prefixes="#") # allow inline comments if config_file not in config.read(config_file): # reading file failed - raise ValueError("failed to read config file %s" % config_file) + raise FileNotFoundError(errno.ENOENT, "failed to read config file", config_file) # prepare the PyHP Object PyHP = libpyhp.PyHP(file_path=file_path, @@ -38,7 +48,7 @@ def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): enable_post_data_reading=config.getboolean("request", "enable_post_data_reading", fallback=False), default_mimetype=config.get("request", "default_mimetype", fallback="text/html") ) - sys.stdout.write = PyHP.make_header_wrapper(sys.stdout.write) #wrap stdout + sys.stdout.write = PyHP.make_header_wrapper(sys.stdout.write) # wrap stdout atexit.register(PyHP.run_shutdown_functions) # run shutdown functions even if a exception occured # handle caching @@ -71,7 +81,7 @@ def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): else: # run normal code code.execute(embed.python_execute) - if not PyHP.headers_sent(): # prevent error if no output occured, but not if and exception occured + if not PyHP.headers_sent(): # prevent error if no output occured, but not if an exception occured PyHP.send_headers() # prepare path for use From a5c54921b0761961feed440fdde2c095553e723c Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 10 Jan 2020 22:49:35 +0100 Subject: [PATCH 17/42] fix warning --- pyhp/embed.py | 4 ++-- pyhp/main.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyhp/embed.py b/pyhp/embed.py index dda89bc..efeed15 100644 --- a/pyhp/embed.py +++ b/pyhp/embed.py @@ -60,7 +60,7 @@ def python_execute(code, userdata): userdata[1] += 1 try: exec(python_align(code), globals(), userdata[0]) - except Exception as e: + except Exception as e: # tell the user the section of the Exception raise Exception("Exception during execution of section %d" % userdata[1]) from e # compile python code sections @@ -69,7 +69,7 @@ def python_compile(code, userdata): userdata += 1 try: return compile(python_align(code), "", "exec") - except Exception as e: + except Exception as e: # tell the user the section of the Exception raise Exception("Exception during executing of section %d" % userdata) from e # execute compiled python sections diff --git a/pyhp/main.py b/pyhp/main.py index d125ddf..ea0929c 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -28,10 +28,10 @@ def main(): except Exception as e: # catch all exceptions print_exception(e, e, e.__traceback__) # print traceback and exception if hasattr(e, "errno"): # if the exception provides a errno - return e.errno + return getattr(e, "errno") else: # return standard error code return 1 - else: # no exeption happend + else: # no exeption occured return 0 # start the PyHP Interpreter with predefined arguments From f2a5a8005e448dbc22b458f003fe5d585485c16b Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 17 Jan 2020 16:43:42 +0100 Subject: [PATCH 18/42] update files_mtime, fix caching --- .gitignore | 4 ++- cache_handlers/files_mtime.py | 53 +++++++++++++++++++++++------------ pyhp.conf | 2 +- pyhp/libpyhp.py | 7 +++++ pyhp/main.py | 7 +++-- 5 files changed, 50 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index f49057b..cb47942 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ -pyhp/__pycache__ +*/__pycache__ notes.txt +Test.html + diff --git a/cache_handlers/files_mtime.py b/cache_handlers/files_mtime.py index 39e4aef..274a1d1 100644 --- a/cache_handlers/files_mtime.py +++ b/cache_handlers/files_mtime.py @@ -2,34 +2,51 @@ """PyHP cache handler (files with modification time)""" -import marshal +import marshal # not pickle because only marshal supports code objects import os.path from os import makedirs +from time import time -class handler: - def __init__(self, cache_path, file_path): +class Handler: + def __init__(self, cache_path, file_path, config): + self.cache_prefix = cache_path self.cache_path = os.path.join(os.path.expanduser(cache_path), file_path.strip(os.path.sep) + ".cache") # use full path to allow indentical named files in different directories with cache_path as root self.file_path = file_path + self.ttl = config.getint("ttl") + self.max_size = config.getint("max_size") - def is_outdated(self): # return True if cache is not created or needs refresh - if not os.path.isfile(self.cache_path) or os.path.getmtime(self.cache_path) < os.path.getmtime(self.file_path): - return True + def get_cachedir_size(self): # get size of cache directory (with all sub directories) in Mbytes + size = 0 + for dirpath, dirnames, filenames in os.walk(self.cache_prefix, followlinks=False): + size += os.path.getsize(dirpath) # dont forget the size of the directory + for filename in filenames: + filepath = os.path.join(dirpath, filename) + if not os.path.islink(filepath): # dont count symlinks + size += os.path.getsize(filepath) + return size/(1000**2) # bytes --> Mbytes + + def is_available(self): # if cache directory has free space or the cached file is already existing or max_size < 0 + return self.max_size < 0 or os.path.isfile(self.cache_path) or self.get_cachedir_size() < self.max_size + + def is_outdated(self): # return True if cache is not created or needs refresh or exceeds ttl + if os.path.isfile(self.cache_path): # to prevent Exception if cache not existing + cache_mtime = os.path.getmtime(self.cache_path) + file_mtime = os.path.getmtime(self.file_path) + age = time() - cache_mtime + return cache_mtime < file_mtime or age > self.ttl > -1 # age > ttl > -1 ignores ttl if -1 or lower else: - return False + return True # file is not existing --> age = infinite - def load(self): + def load(self): # load sections with open(self.cache_path, "rb") as cache: - cache_content = marshal.load(cache) - if len(cache_content) != 2: - raise ValueError("corrupted cache at " + self.cache_path) - else: - return cache_content[0], cache_content[1] # file_content, code_at_begin + code = marshal.load(cache) + return code - def save(self, file_content, code_at_begin): - if not os.path.isdir(os.path.dirname(self.cache_path)): # directories not already created - os.makedirs(os.path.dirname(self.cache_path), exist_ok=True) + def save(self, code): # save sections + if not os.path.isdir(os.path.dirname(self.cache_path)): # directories not already created + makedirs(os.path.dirname(self.cache_path), exist_ok=True) # ignore already created directories with open(self.cache_path, "wb") as cache: - marshal.dump([file_content, code_at_begin], cache) + marshal.dump(code, cache) def close(self): - pass + pass # nothing to do diff --git a/pyhp.conf b/pyhp.conf index c14ae64..3372d3b 100644 --- a/pyhp.conf +++ b/pyhp.conf @@ -39,7 +39,7 @@ auto = False path = ~/.pyhp/cache # path to handler -handler_path = /lib/pyhp/cache_handlers/files-mtime.py +handler_path = /lib/pyhp/cache_handlers/files_mtime.py [sessions] enable = True diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index 713b02f..f877d41 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -270,21 +270,28 @@ def dict2defaultdict(_dict, fallback=None): # Class containing a fallback cache handler (with no function) class dummy_cache_handler: + # take the cache path, file path and the chache_handler section as arguments def __init__(self, cache_path, file_path, config): pass + # check if caching is possible def is_available(self): return False # we are only a fallback + # check if cache needs to be updated or created def is_outdated(self): return False + # save code, given as a iterator + # note that the code sections are replaced with code objects def save(self, code): pass + # get cached code as iterator def load(self): return ("WARNING: This is the dummy cache handler of the libpyhp module, iam providing no useful functions and are just a fallback", ) # return warning + # cleanup def close(self): pass diff --git a/pyhp/main.py b/pyhp/main.py index ea0929c..7ea9897 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -57,16 +57,17 @@ def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): caching_allowed = config.getboolean("caching", "auto", fallback=False) # if file is not stdin and caching is enabled and wanted or auto_caching is enabled if check_if_caching(file_path, caching, caching_enabled, caching_allowed): - handler_path = os.path.splitext(prepare_path(config.get("caching", "handler_path", fallback="/lib/pyhp/cache_handlers/files-mtime.py"))) # get neccesary data + handler_path = os.path.splitext(prepare_path(config.get("caching", "handler_path", fallback="/lib/pyhp/cache_handlers/files-mtime.py")))[0] # get neccesary data cache_path = prepare_path(config.get("caching", "path", fallback="~/.pyhp/cache")) - handler = import_path(handler_path).Handler(cache_path, file_path, config["caching"]) # init handler + handler = import_path(handler_path) + handler = handler.Handler(cache_path, os.path.abspath(file_path), config["caching"]) # init handler if handler.is_available(): # check if caching is possible cached = True if handler.is_outdated(): # update cache code = embed.FromString(prepare_file(file_path), regex, userdata=0) # set userdata for python_compile code.process(embed.python_compile) # compile python sections code.userdata = [{"PyHP": PyHP}, 0] # set userdata for python_execute_compiled - handler.save(code) + handler.save(code.sections) # just save the code sections else: # load cache code = embed.FromIter(handler.load(), userdata=[{"PyHP": PyHP}, 0]) else: # generate FromString Object From bd37c8fe700138989c3ecb9ea9cb4f7b8785edb1 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 17 Jan 2020 17:40:44 +0100 Subject: [PATCH 19/42] add filename to embed.python_compile --- TODO | 5 ++--- pyhp/embed.py | 10 +++++----- pyhp/main.py | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/TODO b/TODO index 83e2fb1..86a93a8 100644 --- a/TODO +++ b/TODO @@ -17,7 +17,6 @@ session_encode session_decode $_SESSION -add handler for memcached (if not already submitted) -add handler for redis (if not already submitted) +add handler for memcached +add handler for redis -wait for suggestions diff --git a/pyhp/embed.py b/pyhp/embed.py index efeed15..87da555 100644 --- a/pyhp/embed.py +++ b/pyhp/embed.py @@ -55,7 +55,7 @@ def __init__(self, iterator, userdata=None): # function for executing python code -# userdata = (locals, section_number), init with [{}, 0] +# userdata = [locals, section_number], init with [{}, 0] def python_execute(code, userdata): userdata[1] += 1 try: @@ -64,13 +64,13 @@ def python_execute(code, userdata): raise Exception("Exception during execution of section %d" % userdata[1]) from e # compile python code sections -# userdata = section_number, init 0 +# userdata = [file, section_number], init with [str, 0] def python_compile(code, userdata): - userdata += 1 + userdata[1] += 1 try: - return compile(python_align(code), "", "exec") + return compile(python_align(code), userdata[0], "exec") except Exception as e: # tell the user the section of the Exception - raise Exception("Exception during executing of section %d" % userdata) from e + raise Exception("Exception during executing of section %d" % userdata[1]) from e # execute compiled python sections # userdata is the same as python_execute diff --git a/pyhp/main.py b/pyhp/main.py index 7ea9897..58ae21c 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -57,14 +57,14 @@ def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): caching_allowed = config.getboolean("caching", "auto", fallback=False) # if file is not stdin and caching is enabled and wanted or auto_caching is enabled if check_if_caching(file_path, caching, caching_enabled, caching_allowed): - handler_path = os.path.splitext(prepare_path(config.get("caching", "handler_path", fallback="/lib/pyhp/cache_handlers/files-mtime.py")))[0] # get neccesary data + handler_path = os.path.splitext(prepare_path(config.get("caching", "handler_path", fallback="/lib/pyhp/cache_handlers/files_mtime.py")))[0] # get neccesary data cache_path = prepare_path(config.get("caching", "path", fallback="~/.pyhp/cache")) handler = import_path(handler_path) handler = handler.Handler(cache_path, os.path.abspath(file_path), config["caching"]) # init handler if handler.is_available(): # check if caching is possible cached = True if handler.is_outdated(): # update cache - code = embed.FromString(prepare_file(file_path), regex, userdata=0) # set userdata for python_compile + code = embed.FromString(prepare_file(file_path), regex, userdata=[file_path, 0]) # set userdata for python_compile code.process(embed.python_compile) # compile python sections code.userdata = [{"PyHP": PyHP}, 0] # set userdata for python_execute_compiled handler.save(code.sections) # just save the code sections From 793592be534337f6326c1cd192f880164a8cad13 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sat, 18 Jan 2020 15:36:00 +0100 Subject: [PATCH 20/42] fix handler not getting closed --- pyhp/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyhp/main.py b/pyhp/main.py index 58ae21c..abe6043 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -72,7 +72,8 @@ def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): code = embed.FromIter(handler.load(), userdata=[{"PyHP": PyHP}, 0]) else: # generate FromString Object cached = False - code = embed.FromString(prepare_file(file_path), regex, userdata=[{"PyHP": PyHP}, 0]) + code = embed.FromString(prepare_file(file_path), regex, userdata=[{"PyHP": PyHP}, 0]) + handler.close() else: # same as above cached = False code = embed.FromString(prepare_file(file_path), regex, userdata=[{"PyHP": PyHP}, 0]) From 1aafd38688e91f8bd92c2f187334687570d6b281 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Mon, 20 Jan 2020 21:37:15 +0100 Subject: [PATCH 21/42] change cache path to common .cache directory --- pyhp.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyhp.conf b/pyhp.conf index 3372d3b..83d9c1a 100644 --- a/pyhp.conf +++ b/pyhp.conf @@ -36,7 +36,7 @@ ttl = -1 auto = False # path for caching -path = ~/.pyhp/cache +path = ~/.cache/pyhp # path to handler handler_path = /lib/pyhp/cache_handlers/files_mtime.py From dd5ebb3e2a7b130995947f0f4ae228d828fff658 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Tue, 21 Jan 2020 21:53:38 +0100 Subject: [PATCH 22/42] fix errno != return code, improve IndentationError in embed.py --- pyhp/__main__.py | 5 ++--- pyhp/embed.py | 2 +- pyhp/main.py | 12 +----------- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/pyhp/__main__.py b/pyhp/__main__.py index 9dcaf16..6b552c6 100644 --- a/pyhp/__main__.py +++ b/pyhp/__main__.py @@ -2,8 +2,7 @@ # script to support python3 -m pyhp -import sys from . import main -# exit with the return code of main -sys.exit(main.main()) +# execute main +main.main() diff --git a/pyhp/embed.py b/pyhp/embed.py index 87da555..9afccb9 100644 --- a/pyhp/embed.py +++ b/pyhp/embed.py @@ -93,7 +93,7 @@ def python_align(code, indentation=None): if line.startswith(indentation): # if line starts with startindentation code[line_num - 1] = line[len(indentation):] # remove startindentation else: - raise IndentationError("File: code processed by python_align Line: %d" % line_num) # raise Exception on bad indentation + raise IndentationError("indentation not matching", ("embedded code section", line_num, len(indentation), line)) # raise Exception on bad indentation return "\n".join(code) # join the lines back together diff --git a/pyhp/main.py b/pyhp/main.py index abe6043..f56bc79 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -10,7 +10,6 @@ import importlib import atexit import errno -from traceback import print_exception from . import embed from . import libpyhp @@ -23,16 +22,7 @@ def main(): parser.add_argument("file", type=str, help="file to be interpreted (omit for reading from stdin)", nargs="?", default="") parser.add_argument("--config", type=str, help="path to custom config file", nargs="?", const="/etc/pyhp.conf", default="/etc/pyhp.conf") args = parser.parse_args() - try: - manual_main(args.file, caching=args.caching, config_file=args.config) - except Exception as e: # catch all exceptions - print_exception(e, e, e.__traceback__) # print traceback and exception - if hasattr(e, "errno"): # if the exception provides a errno - return getattr(e, "errno") - else: # return standard error code - return 1 - else: # no exeption occured - return 0 + manual_main(args.file, caching=args.caching, config_file=args.config) # start the PyHP Interpreter with predefined arguments def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): From f238f75bf2a582a6dd07ab5996e068b88395c3c9 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Wed, 22 Jan 2020 17:43:49 +0100 Subject: [PATCH 23/42] add automatic location response code setting, header_remove with no name --- pyhp/libpyhp.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index f877d41..0d7a80f 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -106,34 +106,34 @@ def http_response_code(self, response_code=None): # set http header # if replace=True replace existing headers of the same type, else simply add + # if http_response_code is not None set it as new response code def header(self, header, replace=True, http_response_code=None): - header = header.split("\n")[0] # prevent header injection - header = header.split(":", maxsplit=1) # split header into name and value - header = [part.strip() for part in header] # remove whitespace + header = header.splitlines()[0] # prevent header injection + header = [part.strip() for part in header.split(":", maxsplit=1)] # split in name and value and remove whitespace if len(header) == 1: # no value provided header.append("") # add empthy value if replace: - repleaced = False # indicate if headers of the same type were found - for i in range(0, len(self.headers)): # need index for changing headers - if self.headers[i][0].lower() == header[0].lower(): # found same header - self.headers[i][1] = header[1] # repleace - repleaced = True - if not repleaced: # header not already set - self.headers.append(header) + self.header_remove(header[0]) # remove headers with same name before adding header + self.headers.append(header) # add header + if http_response_code is not None: # set response code if given (higher priority than location headers) + self.response_code = http_response_code + elif header[0].lower() == "location" and not check_redirect(self.response_code): # set matching response code if code is not 201 or 3xx + self.response_code = 302 else: - self.headers.append(header) + pass # list set headers def headers_list(self): - headers = [] - for header in self.headers: - headers.append(": ".join(header)) # add header like received by the client - return headers + return [": ".join(header) for header in self.headers] # add header like received by the client # remove header with matching name - def header_remove(self, name): - name = name.lower() - self.headers = [header for header in self.headers if header[0].lower() != name] # remove headers with same name + # if name not given remove all headers (set-cookie and content-type too!) + def header_remove(self, name=None): + if name is not None: + name = name.lower() # header names are case-insensitive + self.headers = [header for header in self.headers if header[0].lower() != name] # remove headers with same name + else: + self.headers = [] # remove all headers # return if header have been sent # unlike the PHP function it does not have file and line arguments @@ -152,7 +152,7 @@ def header_register_callback(self, callback): # send headers and execute callback # DO NOT call this function from a header callback to prevent infinite recursion def send_headers(self): - self.header_sent = True # prevent recursion if callback prints output or headers set + self.header_sent = True # prevent recursion if callback prints output self.header_callback() # execute callback print("Status:" , self.response_code, http.HTTPStatus(self.response_code).phrase) for header in self.headers: @@ -267,6 +267,9 @@ def dict2defaultdict(_dict, fallback=None): pass return output +# check if the response code is redirecting (201 or 3xx) +def check_redirect(code): + return code == 201 or code // 100 == 3 # Class containing a fallback cache handler (with no function) class dummy_cache_handler: From f913bd803e1a1fa08d7bc8e92414441dc2012bf6 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Thu, 23 Jan 2020 16:38:57 +0100 Subject: [PATCH 24/42] rename manual_main, remove main, add get_args --- pyhp/__main__.py | 9 ++++++--- pyhp/libpyhp.py | 2 +- pyhp/main.py | 10 +++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pyhp/__main__.py b/pyhp/__main__.py index 6b552c6..0e8959b 100644 --- a/pyhp/__main__.py +++ b/pyhp/__main__.py @@ -2,7 +2,10 @@ # script to support python3 -m pyhp -from . import main +from .main import main, get_args -# execute main -main.main() +# get cli arguments +args = get_args() + +# execute main with file_path as normal argument and the rest as keyword arguments +main(args.pop("file_path"), **args) diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index 0d7a80f..77bc701 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -13,7 +13,7 @@ import urllib.parse from collections import defaultdict -__all__ = ["PyHP", "dummy_cache_handler", "dummy_session_handler", "parse_get", "parse_post", "parse_cookie", "dict2defaultdict"] +__all__ = ["PyHP", "dummy_cache_handler", "dummy_session_handler", "parse_get", "parse_post", "parse_cookie", "dict2defaultdict", "check_redirect"] # class containing the implementations class PyHP: diff --git a/pyhp/main.py b/pyhp/main.py index f56bc79..42c8336 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -13,19 +13,19 @@ from . import embed from . import libpyhp -__all__ = ["main", "manual_main", "prepare_file", "prepare_path", "import_path", "check_if_caching"] +__all__ = ["main", "get_args", "prepare_file", "prepare_path", "import_path", "check_if_caching"] -# start the PyHP Interpreter (wrapper for manual_main) -def main(): +# get cli arguments for main as dict +def get_args(): parser = argparse.ArgumentParser(description="Interpreter for .pyhp Scripts (https://github.com/Deric-W/PyHP)") parser.add_argument("-c", "--caching", help="enable caching (requires file)", action="store_true") parser.add_argument("file", type=str, help="file to be interpreted (omit for reading from stdin)", nargs="?", default="") parser.add_argument("--config", type=str, help="path to custom config file", nargs="?", const="/etc/pyhp.conf", default="/etc/pyhp.conf") args = parser.parse_args() - manual_main(args.file, caching=args.caching, config_file=args.config) + return {"file_path": args.file, "caching": args.caching, "config_file": args.config} # start the PyHP Interpreter with predefined arguments -def manual_main(file_path, caching=False, config_file="/etc/pyhp.conf"): +def main(file_path, caching=False, config_file="/etc/pyhp.conf"): config = configparser.ConfigParser(inline_comment_prefixes="#") # allow inline comments if config_file not in config.read(config_file): # reading file failed raise FileNotFoundError(errno.ENOENT, "failed to read config file", config_file) From 4f155a929ff2023ef856b80b40ad67d7bca06f99 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sat, 25 Jan 2020 17:28:36 +0100 Subject: [PATCH 25/42] update cookie functions --- pyhp/libpyhp.py | 59 +++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index 77bc701..5a7f00e 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -169,40 +169,40 @@ def wrapper(*args, **kwargs): # wrapper forwards all args and kwargs to target return wrapper # set Set-Cookie header, but quote special characters in name and value - # same with expires as setrawcookie - def setcookie(self, name, value="", expires=0, path="", domain="", secure=False, httponly=False): - name = urllib.parse.quote_plus(name) - value = urllib.parse.quote_plus(value) - return self.setrawcookie(name, value, expires, path, domain, secure, httponly) + # same behavior with expires as setrawcookie + # in contrast to php, the samesite keyword argument exists here + def setcookie(self, name, value="", expires=0, path=None, domain=None, secure=False, httponly=False, samesite=None): + name = urllib.parse.quote(name) + value = urllib.parse.quote(value) + return self.setrawcookie(name, value, expires, path, domain, secure, httponly, samesite) # set Set-Cookie header # if expires is a dict the arguments are read from it - def setrawcookie(self, name, value="", expires=0, path="", domain="", secure=False, httponly=False): + # in contrast to php, the samesite keyword argument exists here + def setrawcookie(self, name, value="", expires=0, path=None, domain=None, secure=False, httponly=False, samesite=None): if self.header_sent: return False else: - if type(expires) == dict: # options array - path = expires.get("path", "") - domain = expires.get("domain", "") + if type(expires) == dict: # options dict + path = expires.get("path", None) + domain = expires.get("domain", None) secure = expires.get("secure", False) httponly = expires.get("httponly", False) - samesite = expires.get("samesite", "") + samesite = expires.get("samesite", None) expires = expires.get("expires", 0) # has to happen at the end because it overrides expires - else: - samesite = "" # somehow not as keyword argument in PHP - cookie = "Set-Cookie: %s=%s" % (name, value) + cookie = "Set-Cookie: %s=%s" % (name, value) # initial header if expires != 0: cookie += "; " + "Expires=%s" % time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time() + expires)) # add Expires and Max-Age just in case cookie += "; " + "Max-Age=%d" % expires - if path != "": + if path is not None: cookie += "; " + "Path=%s" % path - if domain != "": + if domain is not None: cookie += "; " + "Domain=%s" % domain if secure: cookie += "; " + "Secure" if httponly: cookie += "; " + "HttpOnly" - if samesite != "": + if samesite is not None: cookie += "; " + "SameSite=%s" % samesite self.header(cookie, False) return True @@ -227,29 +227,30 @@ def parse_get(keep_blank_values=True): # parse only post data def parse_post(keep_blank_values=True): environ = os.environ.copy() # dont modify original environ - environ["QUERY_STRING"] = "" # prevent th eparsing of GET + environ["QUERY_STRING"] = "" # prevent the parsing of GET return cgi.parse(environ=environ, keep_blank_values=keep_blank_values) # parse cookie string def parse_cookie(keep_blank_values=True): cookie_string = os.getenv("HTTP_COOKIE", default="") cookie_dict = {} - for cookie in cookie_string.split("; "): - cookie = cookie.split("=", maxsplit=1) # to allow multiple "=" in value - if len(cookie) == 1: # blank cookie + for cookie in cookie_string.split(";"): + cookie = cookie.split("=", maxsplit=1) # to allow "=" in value + if len(cookie) == 1: # blank cookie (value is missing) if keep_blank_values: - cookie.append("") + cookie.append("") # add empthy value else: - continue # skip cookie - if cookie[1] == "" and not keep_blank_values: # skip cookie + continue # skip cookie + elif cookie[1] == "" and not keep_blank_values: # skip cookie if value is blank (value is "") + continue + else: pass + cookie[0] = urllib.parse.unquote(cookie[0].strip()) # unquote name and value and remove whitespace + cookie[1] = urllib.parse.unquote(cookie[1].strip()) + if cookie[0] in cookie_dict: + cookie_dict[cookie[0]].append(cookie[1]) # key already existing else: - cookie[0] = urllib.parse.unquote_plus(cookie[0]) # unquote name and value - cookie[1] = urllib.parse.unquote_plus(cookie[1]) - if cookie[0] in cookie_dict: - cookie_dict[cookie[0]].append(cookie[1]) # key already existing - else: - cookie_dict[cookie[0]] = [cookie[1]] # make new key + cookie_dict[cookie[0]] = [cookie[1]] # make new key return cookie_dict # convert the dicts of parse_(get, post, cookie) to defaultdict From 601ffa65ea0c9ecfec1d80edf6ac0df0fdab6135 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sat, 25 Jan 2020 19:21:12 +0100 Subject: [PATCH 26/42] increased use of builtins --- pyhp/__init__.py | 2 +- pyhp/embed.py | 4 ++-- pyhp/libpyhp.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyhp/__init__.py b/pyhp/__init__.py index c032793..3df35dc 100644 --- a/pyhp/__init__.py +++ b/pyhp/__init__.py @@ -2,7 +2,7 @@ """Package for running PyHP Scripts""" -# import all modules +# import all submodules from . import embed from . import libpyhp from . import main diff --git a/pyhp/embed.py b/pyhp/embed.py index 9afccb9..34d2138 100644 --- a/pyhp/embed.py +++ b/pyhp/embed.py @@ -84,7 +84,7 @@ def python_execute_compiled(code, userdata): # function for aligning python code in case of a startindentation def python_align(code, indentation=None): line_num = 0 - code = code.split("\n") # split to lines + code = code.splitlines() # split to lines for line in code: line_num += 1 if not (not line or line.isspace() or python_is_comment(line)): # ignore non code lines @@ -109,4 +109,4 @@ def python_get_indentation(line): # check if complete line is a comment def python_is_comment(line): - return line.strip(" \t").startswith("#") + return line.lstrip().startswith("#") diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index 5a7f00e..6206da3 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -9,8 +9,8 @@ import sys import os import cgi -import http import urllib.parse +from http import HTTPStatus from collections import defaultdict __all__ = ["PyHP", "dummy_cache_handler", "dummy_session_handler", "parse_get", "parse_post", "parse_cookie", "dict2defaultdict", "check_redirect"] @@ -154,7 +154,7 @@ def header_register_callback(self, callback): def send_headers(self): self.header_sent = True # prevent recursion if callback prints output self.header_callback() # execute callback - print("Status:" , self.response_code, http.HTTPStatus(self.response_code).phrase) + print("Status:" , self.response_code, HTTPStatus(self.response_code).phrase) for header in self.headers: print(": ".join(header)) print() # end of headers From 4d74e8a096356f98484976c6f95f14de1443ccc9 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sat, 25 Jan 2020 20:52:46 +0100 Subject: [PATCH 27/42] add setup.py --- .gitignore | 1 + LICENSE | 2 +- pyhp/__init__.py | 2 +- setup.py | 25 +++++++++++++++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index cb47942..33111af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ */__pycache__ +dist notes.txt Test.html diff --git a/LICENSE b/LICENSE index aecd78e..1bc0ee4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Eric W. +Copyright (c) 2019 Eric W. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/pyhp/__init__.py b/pyhp/__init__.py index 3df35dc..09796c0 100644 --- a/pyhp/__init__.py +++ b/pyhp/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -"""Package for running PyHP Scripts""" +"""Package for embedding and using python code like php""" # import all submodules from . import embed diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..649382c --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +#!/usr/bin/python3 + +import setuptools +import pyhp + +with open("README.md", "r") as fd: + long_description = fd.read() + +setuptools.setup( + name="pyhp-core", # pyhp was already taken + license="LICENSE", + version=pyhp.__version__, + author=pyhp.__author__, + author_email=pyhp.__email__, + description="application/package for embedding and using python code like php", + long_description=long_description, + long_description_content_type="text/markdown", + url=pyhp.__contact__, + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + ], + python_requires='>=3.5', +) \ No newline at end of file From 13b0fd0831b8170c3aa18e283c97b32fde40de72 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sun, 26 Jan 2020 21:50:36 +0100 Subject: [PATCH 28/42] add debian package --- debian/build_deb.sh | 58 +++++++++++++++++++++++++++++++++++++++++++ debian/changelog | 21 +++++++++++++++- debian/control | 12 ++++----- debian/copyright | 2 +- debian/deb_builder.sh | 55 ---------------------------------------- debian/pyhp | 11 ++++++++ pyhp/libpyhp.py | 6 ++--- setup.py | 4 +-- 8 files changed, 101 insertions(+), 68 deletions(-) create mode 100755 debian/build_deb.sh delete mode 100644 debian/deb_builder.sh create mode 100644 debian/pyhp diff --git a/debian/build_deb.sh b/debian/build_deb.sh new file mode 100755 index 0000000..8b1d80f --- /dev/null +++ b/debian/build_deb.sh @@ -0,0 +1,58 @@ +#!/bin/sh -e +# script for building the pyhp debian package +# you need to build the pyhp-core wheel first + +if [ "$1" = "" ] +then read -p "Name: " package +else package=$1 +fi + +if [ "$2" = "" ] +then read -p "pyhp-core Wheel: " wheel +else wheel=$2 +fi + +if [ "$3" = "" ] +then read -p "python executeable: " python +else python=$3 +fi + +mkdir "$package" + +# place config file, cache handlers and "executable" +mkdir -p "$package/lib/pyhp/cache_handlers" +cp ../cache_handlers/files_mtime.py "$package/lib/pyhp/cache_handlers" + +mkdir "$package/etc" +cp ../pyhp.conf "$package/etc" + +mkdir -p "$package/usr/bin" +cp pyhp "$package/usr/bin" +chmod +x "$package/usr/bin/pyhp" + +# place pyhp-core files +mkdir -p "$package/usr/lib/python3/dist-packages" +$python -m pip install --target "$package/usr/lib/python3/dist-packages" --ignore-installed $wheel + +# place metadata files +mkdir "$package/DEBIAN" +cp conffiles "$package/DEBIAN" +cp control "$package/DEBIAN" + +mkdir -p "$package/usr/share/doc/pyhp" +cp copyright "$package/usr/share/doc/pyhp" +cp changelog "$package/usr/share/doc/pyhp/changelog.Debian" +gzip -n --best "$package/usr/share/doc/pyhp/changelog.Debian" + +# generate md5sums file +chdir "$package" +md5sum $(find . -type d -name "DEBIAN" -prune -o -type f -print) > DEBIAN/md5sums # ignore metadata files +chdir ../ + +# build debian package +dpkg-deb --build "$package" + +# remove build directory +rm -r "$package" + +echo "Done" diff --git a/debian/changelog b/debian/changelog index d3527cc..35f4304 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,23 @@ +pyhp (2.0-1) stable; urgency=low + + * fourth release + * add max_size and ttl caching options + * add more customizable Request handling + * add --config argument + * add automatic response code setting to header + * add python wheel + * structural changes + * php functions now useable outside .pyhp files + * rename pyhp class to PyHP + * replace print wrapper with PyHP.make_header_wrapper + * changed cache handler interface + * rework register_shutdown_function to use atexit + * improve IndentationError message + * fix wrong directory size calculation in files_mtime + * fix crash of files_mtime.py if os not in namespace + + -- Eric Wolf Sun, 26 Jan 2020 18:11:00 +0100 + pyhp (1.2-1) stable; urgency=low * third release @@ -13,7 +33,6 @@ pyhp (1.1-1) stable; urgency=low * add header_register_callback * add config * reworked caching to use handlers (old code as files_mtime handler) - * reworked caching to use handlers (old code as files_mtime handler) * reworked prepare file * now using argparse * changed directory structure (see pyhp.conf) diff --git a/debian/control b/debian/control index 6a06f5e..d0f626d 100644 --- a/debian/control +++ b/debian/control @@ -1,13 +1,13 @@ Package: pyhp -Version: 1.2-1 +Version: 2.0-1 Architecture: all Maintainer: Eric Wolf -Installed-Size: 21 +Installed-Size: 31 Depends: python3:any (>= 3.5) Suggests: apache2 Section: web Priority: optional -Homepage: https://github.com/Deric-W/PyHP-Interpreter -Description: Interprets and executes pyhp files. - PyHP is a script for interpreting and executing pyhp files, with several PHP functions available. - pyhp files are (mostly) HTML files that have embedded Python source code and can be used for creating dynamic web pages. +Homepage: https://github.com/Deric-W/PyHP +Description: Application for embedding and using python code like php + PyHP is a application/python package for embedding python code in text files like HTML, + with several PHP functions available. diff --git a/debian/copyright b/debian/copyright index 01c4b07..f6c966e 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,7 +1,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: pyhp Upstream-Contact: Eric Wolf -Source: https://github.com/Deric-W/PyHP-Interpreter +Source: https://github.com/Deric-W/PyHP Copyright: 2019 Eric Wolf License: Expat diff --git a/debian/deb_builder.sh b/debian/deb_builder.sh deleted file mode 100644 index 46896d2..0000000 --- a/debian/deb_builder.sh +++ /dev/null @@ -1,55 +0,0 @@ -echo "Name of package?" -read package -mkdir $package - -mkdir $package/etc -wget -nv -O $package/etc/pyhp.conf --tries=3 https://raw.githubusercontent.com/Deric-W/PyHP-Interpreter/master/pyhp.conf -chown root $package/etc/pyhp.conf -chgrp root $package/etc/pyhp.conf - -mkdir $package/lib -mkdir $package/lib/pyhp -mkdir $package/lib/pyhp/cache_handlers -wget -nv -O $package/lib/pyhp/cache_handlers/files_mtime.py --tries=3 https://raw.githubusercontent.com/Deric-W/PyHP-Interpreter/master/cache_handlers/files_mtime.py -chown root $package/lib/pyhp/cache_handlers/files_mtime.py -chgrp root $package/lib/pyhp/cache_handlers/files_mtime.py - -mkdir $package/usr -mkdir $package/usr/bin -wget -nv -O $package/usr/bin/pyhp --tries=3 https://raw.githubusercontent.com/Deric-W/PyHP-Interpreter/master/pyhp.py -chown root $package/usr/bin/pyhp -chgrp root $package/usr/bin/pyhp -chmod +x $package/usr/bin/pyhp - -mkdir $package/DEBIAN -wget -nv -O $package/DEBIAN/control --tries=3 https://raw.githubusercontent.com/Deric-W/PyHP-Interpreter/master/debian/control -chown root $package/DEBIAN/control -chgrp root $package/DEBIAN/control - -wget -nv -O $package/DEBIAN/conffiles --tries=3 https://raw.githubusercontent.com/Deric-W/PyHP-Interpreter/master/debian/conffiles -chown root $package/DEBIAN/conffiles -chgrp root $package/DEBIAN/conffiles - -mkdir $package/usr/share -mkdir $package/usr/share/doc -mkdir $package/usr/share/doc/pyhp -wget -nv -O $package/usr/share/doc/pyhp/copyright --tries=3 https://raw.githubusercontent.com/Deric-W/PyHP-Interpreter/master/debian/copyright -chown root $package/usr/share/doc/pyhp/copyright -chgrp root $package/usr/share/doc/pyhp/copyright - -wget -nv -O $package/usr/share/doc/pyhp/changelog.Debian --tries=3 https://raw.githubusercontent.com/Deric-W/PyHP-Interpreter/master/debian/changelog -gzip -n --best $package/usr/share/doc/pyhp/changelog.Debian -chown root $package/usr/share/doc/pyhp/changelog.Debian.gz -chgrp root $package/usr/share/doc/pyhp/changelog.Debian.gz - -chdir $package -md5sum etc/pyhp.conf >> DEBIAN/md5sums -md5sum lib/pyhp/cache_handlers/files_mtime.py >> DEBIAN/md5sums -md5sum usr/bin/pyhp >> DEBIAN/md5sums -md5sum usr/share/doc/pyhp/copyright >> DEBIAN/md5sums -md5sum usr/share/doc/pyhp/changelog.Debian.gz >> DEBIAN/md5sums -chdir ../ - -dpkg-deb --build $package - -rm -rf $package diff --git a/debian/pyhp b/debian/pyhp new file mode 100644 index 0000000..cd16a21 --- /dev/null +++ b/debian/pyhp @@ -0,0 +1,11 @@ +#!/usr/bin/python3 + +# script to support the pyhp command + +from pyhp.main import main, get_args + +# get cli arguments +args = get_args() + +# execute main with file_path as normal argument and the rest as keyword arguments +main(args.pop("file_path"), **args) diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index 6206da3..6941bd7 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -27,9 +27,9 @@ def __init__(self, # build GET, POST, COOKIE, SERVER, REQUEST ): self.__FILE__ = os.path.abspath(file_path) # absolute path of script self.response_code = 200 - self.headers = [["Content-Type", default_mimetype]] + self.headers = [["Content-Type", default_mimetype]] # init with default mimetype header self.header_sent = False - self.header_callback = lambda: None + self.header_callback = lambda: None # dummy callback self.shutdown_functions = [] self.shutdown_functions_run = False @@ -124,7 +124,7 @@ def header(self, header, replace=True, http_response_code=None): # list set headers def headers_list(self): - return [": ".join(header) for header in self.headers] # add header like received by the client + return [": ".join(header) for header in self.headers] # list headers like received by the client # remove header with matching name # if name not given remove all headers (set-cookie and content-type too!) diff --git a/setup.py b/setup.py index 649382c..f4be260 100644 --- a/setup.py +++ b/setup.py @@ -12,11 +12,11 @@ version=pyhp.__version__, author=pyhp.__author__, author_email=pyhp.__email__, - description="application/package for embedding and using python code like php", + description="package for embedding and using python code like php", long_description=long_description, long_description_content_type="text/markdown", url=pyhp.__contact__, - packages=setuptools.find_packages(), + packages=["pyhp"], classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", From c93d6c7fce573af7c51a863a4768e430cc0ab806 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sun, 26 Jan 2020 22:47:58 +0100 Subject: [PATCH 29/42] fix shebang not getting removed --- pyhp/main.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyhp/main.py b/pyhp/main.py index 42c8336..3f416be 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -47,7 +47,7 @@ def main(file_path, caching=False, config_file="/etc/pyhp.conf"): caching_allowed = config.getboolean("caching", "auto", fallback=False) # if file is not stdin and caching is enabled and wanted or auto_caching is enabled if check_if_caching(file_path, caching, caching_enabled, caching_allowed): - handler_path = os.path.splitext(prepare_path(config.get("caching", "handler_path", fallback="/lib/pyhp/cache_handlers/files_mtime.py")))[0] # get neccesary data + handler_path = prepare_path(config.get("caching", "handler_path", fallback="/lib/pyhp/cache_handlers/files_mtime.py")) # get neccesary data cache_path = prepare_path(config.get("caching", "path", fallback="~/.pyhp/cache")) handler = import_path(handler_path) handler = handler.Handler(cache_path, os.path.abspath(file_path), config["caching"]) # init handler @@ -82,9 +82,10 @@ def prepare_path(path): # import file at path def import_path(path): - sys.path.insert(0, os.path.dirname(path)) - path = importlib.import_module(os.path.basename(path)) - del sys.path[0] + sys.path.insert(0, os.path.dirname(path)) # modify module search path + path = os.path.splitext(os.path.basename(path))[0] # get filename without .py + path = importlib.import_module(path) # import module + del sys.path[0] # cleanup module search path return path # check we should cache @@ -101,5 +102,9 @@ def prepare_file(path): with open(path, "r") as fd: code = fd.read() if code.startswith("#!"): # remove shebang - code = code.split("\n", maxsplit=1)[-1] # remove first line + code = code.split("\n", maxsplit=1) # split in first line, remaining lines + if len(code) == 1: # no lines except shebang + code = "" + else: # get all lines except the first line + code = code[1] return code From 80d191b7730940e0e01aa6fbd35ced4809c342b0 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Tue, 28 Jan 2020 22:05:21 +0100 Subject: [PATCH 30/42] add permission warning for build_deb.sh, add basic tests --- .travis.yaml | 19 +++++++++++++++++++ debian/build_deb.sh | 7 +++++++ 2 files changed, 26 insertions(+) create mode 100644 .travis.yaml diff --git a/.travis.yaml b/.travis.yaml new file mode 100644 index 0000000..c2de965 --- /dev/null +++ b/.travis.yaml @@ -0,0 +1,19 @@ +language: python +python: + - "3.5" + - "3.6" + - "3.7" + - "3.8" + - "pypy3" +env: + - QUERY_STRING="Test0=Hello&Test1==World!&Test2=&Test4" HTTP_COOKIE="Test0=Hello; Test1 == World!; Test2=Hello=World%21; Test3=; Test4" +sudo: required +install: + - python3 setup.py bdist_wheel + - version=$(python3 setup.py --version) + - cd debian + - sudo ./build_deb.sh pyhp-test ../dist/pyhp_core-$version-py3-none-any.whl python3 + - sudo dpkg -i pyhp-test.deb + - cd ../examples +script: + - echo TODO diff --git a/debian/build_deb.sh b/debian/build_deb.sh index 8b1d80f..fdf00f5 100755 --- a/debian/build_deb.sh +++ b/debian/build_deb.sh @@ -1,5 +1,6 @@ #!/bin/sh -e # script for building the pyhp debian package +# it is recommended to run this script as root or to set the owner and group of the files to root # you need to build the pyhp-core wheel first if [ "$1" = "" ] @@ -49,6 +50,12 @@ chdir "$package" md5sum $(find . -type d -name "DEBIAN" -prune -o -type f -print) > DEBIAN/md5sums # ignore metadata files chdir ../ +# if root set file permissions, else warn +if [ $(id -u) = 0 ] +then chown root:root -R "$package" +else echo "not running as root, permissions in package may be wrong" +fi + # build debian package dpkg-deb --build "$package" From ebeca7d709e7b222743d1f9f42279e53c064a644 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Tue, 28 Jan 2020 22:09:41 +0100 Subject: [PATCH 31/42] fix wrong filename for .travis.yml --- .travis.yaml => .travis.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .travis.yaml => .travis.yml (100%) diff --git a/.travis.yaml b/.travis.yml similarity index 100% rename from .travis.yaml rename to .travis.yml From 7ad6af810bb8083e167df224a8802f3a6e7a774c Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Tue, 28 Jan 2020 22:16:04 +0100 Subject: [PATCH 32/42] change third argument of build_deb to pip --- .travis.yml | 2 +- debian/build_deb.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c2de965..9fb7934 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: - python3 setup.py bdist_wheel - version=$(python3 setup.py --version) - cd debian - - sudo ./build_deb.sh pyhp-test ../dist/pyhp_core-$version-py3-none-any.whl python3 + - sudo ./build_deb.sh pyhp-test ../dist/pyhp_core-$version-py3-none-any.whl pip - sudo dpkg -i pyhp-test.deb - cd ../examples script: diff --git a/debian/build_deb.sh b/debian/build_deb.sh index fdf00f5..83ab606 100755 --- a/debian/build_deb.sh +++ b/debian/build_deb.sh @@ -14,8 +14,8 @@ else wheel=$2 fi if [ "$3" = "" ] -then read -p "python executeable: " python -else python=$3 +then read -p "pip executeable: " pip +else pip=$3 fi mkdir "$package" @@ -33,7 +33,7 @@ chmod +x "$package/usr/bin/pyhp" # place pyhp-core files mkdir -p "$package/usr/lib/python3/dist-packages" -$python -m pip install --target "$package/usr/lib/python3/dist-packages" --ignore-installed $wheel +$pip install --target "$package/usr/lib/python3/dist-packages" --ignore-installed $wheel # place metadata files mkdir "$package/DEBIAN" From 7a69fd2e6785ebc86ed78e9247a5190798fbe547 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Tue, 28 Jan 2020 22:33:44 +0100 Subject: [PATCH 33/42] add pip bootstrap --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9fb7934..85471d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,10 +9,11 @@ env: - QUERY_STRING="Test0=Hello&Test1==World!&Test2=&Test4" HTTP_COOKIE="Test0=Hello; Test1 == World!; Test2=Hello=World%21; Test3=; Test4" sudo: required install: + - wget -qO - https://bootstrap.pypa.io/get-pip.py|python3 - python3 setup.py bdist_wheel - version=$(python3 setup.py --version) - cd debian - - sudo ./build_deb.sh pyhp-test ../dist/pyhp_core-$version-py3-none-any.whl pip + - sudo ./build_deb.sh pyhp-test ../dist/pyhp_core-$version-py3-none-any.whl "python3 -m pip" - sudo dpkg -i pyhp-test.deb - cd ../examples script: From 4d64114e4a94b4fd11b2537086048e9d8de6edae Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Tue, 28 Jan 2020 23:02:58 +0100 Subject: [PATCH 34/42] fix issue with virtualenv --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 85471d7..888ec4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,11 +9,10 @@ env: - QUERY_STRING="Test0=Hello&Test1==World!&Test2=&Test4" HTTP_COOKIE="Test0=Hello; Test1 == World!; Test2=Hello=World%21; Test3=; Test4" sudo: required install: - - wget -qO - https://bootstrap.pypa.io/get-pip.py|python3 - python3 setup.py bdist_wheel - version=$(python3 setup.py --version) - cd debian - - sudo ./build_deb.sh pyhp-test ../dist/pyhp_core-$version-py3-none-any.whl "python3 -m pip" + - . ./build_deb.sh pyhp-test ../dist/pyhp_core-$version-py3-none-any.whl pip - sudo dpkg -i pyhp-test.deb - cd ../examples script: From 285b13fb2600056b574043e2de04e0bbb46c0729 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Tue, 28 Jan 2020 23:44:48 +0100 Subject: [PATCH 35/42] improved use of builtins --- pyhp/libpyhp.py | 22 ++++++++-------------- pyhp/main.py | 6 +----- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index 6941bd7..62e4380 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -109,9 +109,7 @@ def http_response_code(self, response_code=None): # if http_response_code is not None set it as new response code def header(self, header, replace=True, http_response_code=None): header = header.splitlines()[0] # prevent header injection - header = [part.strip() for part in header.split(":", maxsplit=1)] # split in name and value and remove whitespace - if len(header) == 1: # no value provided - header.append("") # add empthy value + header = [part.strip() for part in header.partition(":")[0:3:2]] # split in name and value and remove whitespace if replace: self.header_remove(header[0]) # remove headers with same name before adding header self.headers.append(header) # add header @@ -235,18 +233,10 @@ def parse_cookie(keep_blank_values=True): cookie_string = os.getenv("HTTP_COOKIE", default="") cookie_dict = {} for cookie in cookie_string.split(";"): - cookie = cookie.split("=", maxsplit=1) # to allow "=" in value - if len(cookie) == 1: # blank cookie (value is missing) - if keep_blank_values: - cookie.append("") # add empthy value - else: - continue # skip cookie - elif cookie[1] == "" and not keep_blank_values: # skip cookie if value is blank (value is "") + cookie = cookie.partition("=")[0:3:2] # split in name and value + if not keep_blank_values and (check_blank(cookie[0]) or check_blank(cookie[1])): continue - else: - pass - cookie[0] = urllib.parse.unquote(cookie[0].strip()) # unquote name and value and remove whitespace - cookie[1] = urllib.parse.unquote(cookie[1].strip()) + cookie = [urllib.parse.unquote(part.strip()) for part in cookie] # unquote name and value and remove whitespace if cookie[0] in cookie_dict: cookie_dict[cookie[0]].append(cookie[1]) # key already existing else: @@ -272,6 +262,10 @@ def dict2defaultdict(_dict, fallback=None): def check_redirect(code): return code == 201 or code // 100 == 3 +# check if string is empthy or just whitespace +def check_blank(string): + return string == "" or string.isspace() + # Class containing a fallback cache handler (with no function) class dummy_cache_handler: # take the cache path, file path and the chache_handler section as arguments diff --git a/pyhp/main.py b/pyhp/main.py index 3f416be..9290755 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -102,9 +102,5 @@ def prepare_file(path): with open(path, "r") as fd: code = fd.read() if code.startswith("#!"): # remove shebang - code = code.split("\n", maxsplit=1) # split in first line, remaining lines - if len(code) == 1: # no lines except shebang - code = "" - else: # get all lines except the first line - code = code[1] + code = code.partition("\n")[2] # get all lines except the first line return code From 000764b2d1ad1e885a0a74616cc041c60fe55201 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Wed, 29 Jan 2020 21:17:12 +0100 Subject: [PATCH 36/42] add --version, allow main.main to control return code --- debian/pyhp | 3 ++- pyhp/__init__.py | 14 ++++++-------- pyhp/__main__.py | 3 ++- pyhp/embed.py | 2 -- pyhp/libpyhp.py | 2 -- pyhp/main.py | 7 ++++--- 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/debian/pyhp b/debian/pyhp index cd16a21..04445aa 100644 --- a/debian/pyhp +++ b/debian/pyhp @@ -2,10 +2,11 @@ # script to support the pyhp command +import sys from pyhp.main import main, get_args # get cli arguments args = get_args() # execute main with file_path as normal argument and the rest as keyword arguments -main(args.pop("file_path"), **args) +sys.exit(main(args.pop("file_path"), **args)) diff --git a/pyhp/__init__.py b/pyhp/__init__.py index 09796c0..44991ab 100644 --- a/pyhp/__init__.py +++ b/pyhp/__init__.py @@ -2,14 +2,8 @@ """Package for embedding and using python code like php""" -# import all submodules -from . import embed -from . import libpyhp -from . import main - -# for import * -__all__ = ["embed", "libpyhp", "main"] - +# package metadata +# needs to be defined before .main is imported __version__ = "2.0" __author__ = "Eric Wolf" __maintainer__ = "Eric Wolf" @@ -17,3 +11,7 @@ __email__ = "robo-eric@gmx.de" # please dont use for spam :( __contact__ = "https://github.com/Deric-W/PyHP" +# import all submodules +from . import embed +from . import libpyhp +from . import main diff --git a/pyhp/__main__.py b/pyhp/__main__.py index 0e8959b..6ebe148 100644 --- a/pyhp/__main__.py +++ b/pyhp/__main__.py @@ -2,10 +2,11 @@ # script to support python3 -m pyhp +import sys from .main import main, get_args # get cli arguments args = get_args() # execute main with file_path as normal argument and the rest as keyword arguments -main(args.pop("file_path"), **args) +sys.exit(main(args.pop("file_path"), **args)) diff --git a/pyhp/embed.py b/pyhp/embed.py index 34d2138..7565c33 100644 --- a/pyhp/embed.py +++ b/pyhp/embed.py @@ -9,8 +9,6 @@ from io import StringIO from contextlib import redirect_stdout -__all__ = ["FromString", "FromIter", "python_execute", "python_compile", "python_execute_compiled", "python_align", "python_get_indentation", "python_is_comment"] - # class for handling strings class FromString: # get string, regex to isolate code and optional flags for the regex (default for processing text files) diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index 62e4380..1c6dee7 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -13,8 +13,6 @@ from http import HTTPStatus from collections import defaultdict -__all__ = ["PyHP", "dummy_cache_handler", "dummy_session_handler", "parse_get", "parse_post", "parse_cookie", "dict2defaultdict", "check_redirect"] - # class containing the implementations class PyHP: def __init__(self, # build GET, POST, COOKIE, SERVER, REQUEST diff --git a/pyhp/main.py b/pyhp/main.py index 9290755..1b85c6e 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -10,15 +10,15 @@ import importlib import atexit import errno +from . import __version__ from . import embed from . import libpyhp -__all__ = ["main", "get_args", "prepare_file", "prepare_path", "import_path", "check_if_caching"] - # get cli arguments for main as dict def get_args(): - parser = argparse.ArgumentParser(description="Interpreter for .pyhp Scripts (https://github.com/Deric-W/PyHP)") + parser = argparse.ArgumentParser(prog="pyhp", description="Interpreter for .pyhp Scripts (https://github.com/Deric-W/PyHP)") parser.add_argument("-c", "--caching", help="enable caching (requires file)", action="store_true") + parser.add_argument("-v", "--version", help="display version number", action="version", version="%(prog)s {version}".format(version=__version__)) parser.add_argument("file", type=str, help="file to be interpreted (omit for reading from stdin)", nargs="?", default="") parser.add_argument("--config", type=str, help="path to custom config file", nargs="?", const="/etc/pyhp.conf", default="/etc/pyhp.conf") args = parser.parse_args() @@ -75,6 +75,7 @@ def main(file_path, caching=False, config_file="/etc/pyhp.conf"): if not PyHP.headers_sent(): # prevent error if no output occured, but not if an exception occured PyHP.send_headers() + return 0 # return 0 on success # prepare path for use def prepare_path(path): From b598ba195a9a8c7123efeff77d8e6319fab381c3 Mon Sep 17 00:00:00 2001 From: Eric W Date: Thu, 30 Jan 2020 18:06:25 +0100 Subject: [PATCH 37/42] Update README.md --- README.md | 110 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index ba500bc..c4158c8 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,79 @@ -# PyHP-Interpreter +# PyHP-Interpreter [![Build Status](https://travis-ci.org/Deric-W/PyHP.svg?branch=master)](https://travis-ci.org/Deric-W/PyHP) -The PyHP Interpreter is a script that allows you to embed Python code like PHP code into HTML. +The PyHP Interpreter is a package that allows you to embed Python code like PHP code into HTML and other text files. The script is called either by the configuration of the web server or a shebang and communicates with the web server via CGI. ## Features: + - Parser for embedding python Code in HTML - - Encapsulation of the variables and functions of the interpreter in a separate class (to prevent accidental overwriting) + - a bunch of PHP features implemented in python + - modular structure to allow the use of features outside of the interpreter + - automatic code alignment for improved readability - caching - - PHP like header functions - - PHP like SERVER array (Dictionary) - - PHP like REQUEST,GET,POST and COOKIE array (Dictionary) - - PHP like setrawcookie and setcookie functions ## How it works: + - Python code is contained within the `` tags (like PHP) - - the Script is called like a interpreter, with the filepath as cli parameter - - if no filepath is given, the script is reading from stdin - - if "-c" is given, the file will be processed an cached in cache_path/absolute/path/filename.cache - (the file is also loaded or renewed with this option) - - python code can be away from the left site of the file for better optics --> Test4.pyhp, fib.pyhp - - the following PHP features are available as part of the `pyhp` class: - - `$_REQUEST` as REQUEST - - `$_GET`as GET - - `$_POST`as POST - - `$_COOKIE`as COOKIE - - `$_SERVER` as SERVER - - `http_response_code` - - `headers_list` - - `header` - - `header_remove` - - `headers_sent` - - `header_register_callback` - - `setrawcookie` - - `setcookie` - - `register_shutdown_function` - - automatic sending of headers with fallback: `Content-Type: text/html` + - the program is called like a interpreter, with the filepath as cli parameter + - if no filepath is given, the program is reading from stdin + - if the `-c` or `--caching` is given, the cache will be enabled and the file will additionally be preprocessed if needed + and cached in cache_path/absolute/path/of/filename.cache + - python code is allowed to have a starting indentation for better readability inside (for example) HTML files + - the following PHP features are available as methods of the `PyHP` class in pyhp.libpyhp: + - `$_SERVER` as `SERVER` + - `$_REQUEST` as `REQUEST` + - `$_GET` as `GET` + - `$_POST` as `POST` + - `$_COOKIE` as `COOKIE` + - `http_response_code` + - `header` + - `headers_list` + - `header_remove` + - `headers_sent` + - `header_register_callback` + - `setcookie` with an additional `samesite` keyword argument + - `setrawcookie` also with an additional `samesite` keyword argument + - `register_shutdown_function` + ## Cache Handlers - - are responsible for saving/loading/renewing caches - - are python scripts with the following contents: - - the `handler` class, wich takes the cache path and absolute file path as initialization parameters - - the method `is_outdated`, wich returns True or False - - the method `save`, wich returns nothing and saves the boolean code_at_begin and preprocessed code - - the method `load`, wich returns a tuble with the boolean code_at_begin and the code saved by `save` - - the method `close`, wich does cleanup tasks + - are responsible for saving/loading/renewing caches + - are python scripts with the following contents: + - the `Handler` class, wich takes the cache path, absolute file path and `caching` section of the config file as + initialization parameters and provides the following methods: + - `is_available`, wich returns a boolean indicating if the handler can be used + - `is_outdated`, wich returns a boolean indicating if the cache needs to be renewed + - `save`, wich takes an iterator as argument and saves it in the cache + - `load`, wich loads an iterator from the cache + - `close`, wich does cleanup tasks + - note that the iterator may contain code objects which can't be pickled + - examples are available in the *cache_handlers* directory + ## Installation - ### Debian - Use the Debian package - ### Other - 1. enable CGI for your web server - 2. drop pyhp.py somewhere and mark it as executable (make sure Python 3.5+ is installed) - 3. download pyhp.conf and move it to `/etc` - 4. create `/lib/pyhp/cache_handlers` and drop the choosen cache handler (and maybe others) in the cache handler directory - - Done! you can now use `.pyhp` files by adding a Shebang + + This section shows you how to install PyHP on your computer. + If you want to use *pyhp* scripts on your website by CGI you have to additionally enable CGI in your webserver. + + ### Just as python package + 1. build the *pyhp-core* python package with `python3 setup.py bdist_wheel` + 2. Done! You can now install the wheel contained in the *dist* directory with pip + + ### As application + If you just installed the python package, then you have to provide `--config` with every call of `python3 -m pyhp` + and can't use the caching feature. + To stop this, you can build a debian package or install PyHP manually. + + #### Debian package + 1. build the *pyhp-core* python package with `python3 setup.py bdist_wheel` + 2. go to the *debian* directory and execute `./build_deb.sh` + 3. enter a package name, the path of the *pyhp-core* wheel and the pip command you wish to use + 4. Done! You can now install the debian package with `sudo dpkg -i .deb` + + #### Manually + 1. install the *pyhp-core* python package + 2. copy *pyhp.conf* to */etc* + 3. copy *cache_handlers* to */lib/pyhp/* + 4. copy *debian/pyhp* to a directoy in your PATH + 5. Done! You can now use the `pyhp` command + From 952fa91b059b6eff9beb3337ca630e5cd99830a4 Mon Sep 17 00:00:00 2001 From: Eric W Date: Thu, 30 Jan 2020 18:08:29 +0100 Subject: [PATCH 38/42] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4158c8..7b6c0b4 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The script is called either by the configuration of the web server or a shebang - if the `-c` or `--caching` is given, the cache will be enabled and the file will additionally be preprocessed if needed and cached in cache_path/absolute/path/of/filename.cache - python code is allowed to have a starting indentation for better readability inside (for example) HTML files - - the following PHP features are available as methods of the `PyHP` class in pyhp.libpyhp: + - the following PHP features are available as methods of the `PyHP` class (available from the outside in pyhp.libpyhp): - `$_SERVER` as `SERVER` - `$_REQUEST` as `REQUEST` - `$_GET` as `GET` From 52a9a3b2c94afae037bb12756c723887205cc049 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Fri, 31 Jan 2020 23:18:42 +0100 Subject: [PATCH 39/42] add tests --- .travis.yml | 9 ++-- debian/changelog | 1 + examples/Test1.pyhp | 29 ----------- examples/Test2.pyhp | 17 ------- examples/Test3.pyhp | 20 -------- examples/Test4.pyhp | 14 ------ examples/caching.sh | 8 ++++ examples/cookie.sh | 7 +++ examples/cookie/setcookie.output | 14 ++++++ examples/cookie/setcookie.pyhp | 12 +++++ examples/cookie/setrawcookie.output | 16 +++++++ examples/cookie/setrawcookie.pyhp | 17 +++++++ examples/embedding.sh | 8 ++++ examples/embedding/indentation.output | 17 +++++++ examples/embedding/indentation.pyhp | 20 ++++++++ examples/embedding/shebang.output | 12 +++++ examples/embedding/shebang.pyhp | 9 ++++ examples/embedding/syntax.output | 18 +++++++ examples/embedding/syntax.pyhp | 15 ++++++ examples/fib.pyhp | 48 +++++++------------ examples/header.sh | 10 ++++ examples/header/header.output | 22 +++++++++ examples/header/header.pyhp | 25 ++++++++++ .../header/header_register_callback.output | 15 ++++++ examples/header/header_register_callback.pyhp | 18 +++++++ examples/header/header_remove.output | 16 +++++++ examples/header/header_remove.pyhp | 18 +++++++ examples/header/headers_list.output | 19 ++++++++ examples/header/headers_list.pyhp | 16 +++++++ examples/header/headers_sent.output | 13 +++++ examples/header/headers_sent.pyhp | 12 +++++ examples/request.sh | 9 ++++ examples/request/methods.output | 17 +++++++ examples/request/methods.pyhp | 14 ++++++ examples/request/request-order.conf | 5 ++ examples/request/request-order.output | 14 ++++++ examples/request/request-order.pyhp | 13 +++++ examples/shutdown_functions.sh | 6 +++ .../register_shutdown_function.output | 13 +++++ .../register_shutdown_function.pyhp | 18 +++++++ 40 files changed, 491 insertions(+), 113 deletions(-) delete mode 100644 examples/Test1.pyhp delete mode 100644 examples/Test2.pyhp delete mode 100644 examples/Test3.pyhp delete mode 100644 examples/Test4.pyhp create mode 100644 examples/caching.sh create mode 100644 examples/cookie.sh create mode 100644 examples/cookie/setcookie.output create mode 100644 examples/cookie/setcookie.pyhp create mode 100644 examples/cookie/setrawcookie.output create mode 100644 examples/cookie/setrawcookie.pyhp create mode 100644 examples/embedding.sh create mode 100644 examples/embedding/indentation.output create mode 100644 examples/embedding/indentation.pyhp create mode 100644 examples/embedding/shebang.output create mode 100644 examples/embedding/shebang.pyhp create mode 100644 examples/embedding/syntax.output create mode 100644 examples/embedding/syntax.pyhp create mode 100644 examples/header.sh create mode 100644 examples/header/header.output create mode 100644 examples/header/header.pyhp create mode 100644 examples/header/header_register_callback.output create mode 100644 examples/header/header_register_callback.pyhp create mode 100644 examples/header/header_remove.output create mode 100644 examples/header/header_remove.pyhp create mode 100644 examples/header/headers_list.output create mode 100644 examples/header/headers_list.pyhp create mode 100644 examples/header/headers_sent.output create mode 100644 examples/header/headers_sent.pyhp create mode 100644 examples/request.sh create mode 100644 examples/request/methods.output create mode 100644 examples/request/methods.pyhp create mode 100644 examples/request/request-order.conf create mode 100644 examples/request/request-order.output create mode 100644 examples/request/request-order.pyhp create mode 100644 examples/shutdown_functions.sh create mode 100644 examples/shutdown_functions/register_shutdown_function.output create mode 100644 examples/shutdown_functions/register_shutdown_function.pyhp diff --git a/.travis.yml b/.travis.yml index 888ec4f..2350ab8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,6 @@ python: - "3.7" - "3.8" - "pypy3" -env: - - QUERY_STRING="Test0=Hello&Test1==World!&Test2=&Test4" HTTP_COOKIE="Test0=Hello; Test1 == World!; Test2=Hello=World%21; Test3=; Test4" sudo: required install: - python3 setup.py bdist_wheel @@ -16,4 +14,9 @@ install: - sudo dpkg -i pyhp-test.deb - cd ../examples script: - - echo TODO + - . ./embedding.sh + - . ./caching.sh + - . ./request.sh + - . ./header.sh + - . ./cookie.sh + - . ./shutdown_functions.sh diff --git a/debian/changelog b/debian/changelog index 35f4304..15661a9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -4,6 +4,7 @@ pyhp (2.0-1) stable; urgency=low * add max_size and ttl caching options * add more customizable Request handling * add --config argument + * add --version argument * add automatic response code setting to header * add python wheel * structural changes diff --git a/examples/Test1.pyhp b/examples/Test1.pyhp deleted file mode 100644 index 5b23bad..0000000 --- a/examples/Test1.pyhp +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - Test1 - - -

Erster Test:

- -
- -

Ende

- - - \ No newline at end of file diff --git a/examples/Test2.pyhp b/examples/Test2.pyhp deleted file mode 100644 index e97bbca..0000000 --- a/examples/Test2.pyhp +++ /dev/null @@ -1,17 +0,0 @@ - - - - Test1 - - -

Erster Test:

- -
- -

Ende

- - - \ No newline at end of file diff --git a/examples/Test3.pyhp b/examples/Test3.pyhp deleted file mode 100644 index cd04661..0000000 --- a/examples/Test3.pyhp +++ /dev/null @@ -1,20 +0,0 @@ - - - Test3 - - -

Test3:

- - - \ No newline at end of file diff --git a/examples/Test4.pyhp b/examples/Test4.pyhp deleted file mode 100644 index 3aabc75..0000000 --- a/examples/Test4.pyhp +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/pyhp -c - - - Test4 - - - - - diff --git a/examples/caching.sh b/examples/caching.sh new file mode 100644 index 0000000..178132d --- /dev/null +++ b/examples/caching.sh @@ -0,0 +1,8 @@ +#!/bin/sh -e +# script for testing the caching features + +test -f /lib/pyhp/cache_handlers/files_mtime.py +test ! -f ~/.cache/pyhp/$(pwd)/embedding/syntax.pyhp.cache +pyhp --caching embedding/syntax.pyhp|diff embedding/syntax.output - +test -f ~/.cache/pyhp/$(pwd)/embedding/syntax.pyhp.cache +pyhp --caching embedding/syntax.pyhp|diff embedding/syntax.output - diff --git a/examples/cookie.sh b/examples/cookie.sh new file mode 100644 index 0000000..3428c30 --- /dev/null +++ b/examples/cookie.sh @@ -0,0 +1,7 @@ +#!/bin/sh -e +# script for testing the cookie functions + +cd cookie +pyhp setrawcookie.pyhp|diff setrawcookie.output - +pyhp setcookie.pyhp|diff setcookie.output - +cd .. diff --git a/examples/cookie/setcookie.output b/examples/cookie/setcookie.output new file mode 100644 index 0000000..bbf2f1d --- /dev/null +++ b/examples/cookie/setcookie.output @@ -0,0 +1,14 @@ +Status: 200 OK +Content-Type: text/html +Set-Cookie: Test0=test%20%21 + + + + + setcookie + + + This function is like setrawcookie except the fact that it urlencodes name and value. + + + diff --git a/examples/cookie/setcookie.pyhp b/examples/cookie/setcookie.pyhp new file mode 100644 index 0000000..3adf83f --- /dev/null +++ b/examples/cookie/setcookie.pyhp @@ -0,0 +1,12 @@ +#!/usr/bin/pyhp + + + + setcookie + + + + + diff --git a/examples/cookie/setrawcookie.output b/examples/cookie/setrawcookie.output new file mode 100644 index 0000000..ec75ec8 --- /dev/null +++ b/examples/cookie/setrawcookie.output @@ -0,0 +1,16 @@ +Status: 200 OK +Content-Type: text/html +Set-Cookie: Test0= +Set-Cookie: Test1=test !; Path=/test; Secure + + + + + setrawcookie + + + With this function you can set raw cookies. +Remember that this can't be done after the headers have been sent. + + + diff --git a/examples/cookie/setrawcookie.pyhp b/examples/cookie/setrawcookie.pyhp new file mode 100644 index 0000000..0090e75 --- /dev/null +++ b/examples/cookie/setrawcookie.pyhp @@ -0,0 +1,17 @@ +#!/usr/bin/pyhp + + + + setrawcookie + + + + + diff --git a/examples/embedding.sh b/examples/embedding.sh new file mode 100644 index 0000000..3593e29 --- /dev/null +++ b/examples/embedding.sh @@ -0,0 +1,8 @@ +#!/bin/sh -e +# script for testing the embedding features + +cd embedding +pyhp syntax.pyhp|diff syntax.output - +pyhp shebang.pyhp|diff shebang.output - +pyhp indentation.pyhp|diff indentation.output - +cd .. diff --git a/examples/embedding/indentation.output b/examples/embedding/indentation.output new file mode 100644 index 0000000..3b860e4 --- /dev/null +++ b/examples/embedding/indentation.output @@ -0,0 +1,17 @@ +Status: 200 OK +Content-Type: text/html + + + + Indentation + + + Normal python code would start with an indentation of 0 (like this). +But it looks a bit odd inside of an HTML document. + + Because of this, PyHP will calculate the starting indentation of each section +and remove it from all lines of it (12 spaces in this case) +If a line is not starting with the starting indentation a IndentationError will be raised + + + diff --git a/examples/embedding/indentation.pyhp b/examples/embedding/indentation.pyhp new file mode 100644 index 0000000..1e4b32e --- /dev/null +++ b/examples/embedding/indentation.pyhp @@ -0,0 +1,20 @@ +#!/usr/bin/pyhp + + + Indentation + + + + + + diff --git a/examples/embedding/shebang.output b/examples/embedding/shebang.output new file mode 100644 index 0000000..ce99070 --- /dev/null +++ b/examples/embedding/shebang.output @@ -0,0 +1,12 @@ +Status: 200 OK +Content-Type: text/html + + + + Shebang + + + If a Shebang is detected (first line starts with #!) the first line is removed before processing the file + + + diff --git a/examples/embedding/shebang.pyhp b/examples/embedding/shebang.pyhp new file mode 100644 index 0000000..e9597b5 --- /dev/null +++ b/examples/embedding/shebang.pyhp @@ -0,0 +1,9 @@ +#!/usr/bin/pyhp + + + Shebang + + + + + diff --git a/examples/embedding/syntax.output b/examples/embedding/syntax.output new file mode 100644 index 0000000..2ba792a --- /dev/null +++ b/examples/embedding/syntax.output @@ -0,0 +1,18 @@ +Status: 200 OK +Content-Type: text/html + + + + Syntax + + + + basic synatx test +With the default configuration, code needs to be contained +between the '' tags and one whitespace between +the code and the tags. +Dont forget that the parser ignores python syntax, so '?>' without the ' would +end this section. + + + diff --git a/examples/embedding/syntax.pyhp b/examples/embedding/syntax.pyhp new file mode 100644 index 0000000..a47c5c4 --- /dev/null +++ b/examples/embedding/syntax.pyhp @@ -0,0 +1,15 @@ + + + <?pyhp print("Syntax") ?> + + + ' tags and one whitespace between") + print("the code and the tags.") + print("Dont forget that the parser ignores python syntax, so '?>' without the ' would") + print("end this section.") + ?> + + diff --git a/examples/fib.pyhp b/examples/fib.pyhp index 394fd9a..cb09600 100644 --- a/examples/fib.pyhp +++ b/examples/fib.pyhp @@ -1,42 +1,30 @@ #!/usr/bin/pyhp - Fibonacci-Zahlen Test + Fibonacci test ") - if zahl > 1: - print(stelle_2) - print("
") - zahl = zahl - 3 - i = 0 - while i <= zahl: - i = i + 1 - if stelle_1 < stelle_2: - stelle_1 = stelle_1 + stelle_2 - print(stelle_1) - else: - stelle_2 = stelle_2 + stelle_1 - print(stelle_2) - print("
") - #PyHP.REQUEST = {"fib":"x"} - if "fib" in PyHP.REQUEST: - print("Dies die Fibonaccizahlen von der 0ten bis " + str(PyHP.REQUEST["fib"]) + "ten Stelle:
") - try: - fib(int(PyHP.REQUEST["fib"])) - except: - print("Fehler während der Berechnung!
") - else: - print("

Bitte geben Sie eine Zahl ein!

") + def fib(n): + a,b = 0, 1 + while a < n: + yield a + a, b = b, a + b + + if "fib" in PyHP.REQUEST: + if PyHP.REQUEST["fib"].isdecimal(): + n = int(PyHP.REQUEST["fib"]) + print("

These are the fibonacci numbers from 0 to %s

" % n) + for number in fib(n): + print("

%s

" % number) + else: + print("

Please enter a valid integer!

") + else: + print("

Enter a number

") ?>
- +
diff --git a/examples/header.sh b/examples/header.sh new file mode 100644 index 0000000..0582796 --- /dev/null +++ b/examples/header.sh @@ -0,0 +1,10 @@ +#!/bin/sh -e +# script for testing the header features + +cd header +pyhp header.pyhp|diff header.output - +pyhp headers_list.pyhp|diff headers_list.output - +pyhp header_remove.pyhp|diff header_remove.output - +pyhp headers_sent.pyhp|diff headers_sent.output - +pyhp header_register_callback.pyhp|diff header_register_callback.output - +cd .. diff --git a/examples/header/header.output b/examples/header/header.output new file mode 100644 index 0000000..2a7a6cf --- /dev/null +++ b/examples/header/header.output @@ -0,0 +1,22 @@ +Status: 404 Not Found +Content-Type: text/html +Test0: +Test1: +Test2: Hello +Test2: World! +test3: 1 +not_send: True + + + + + + header + + + + diff --git a/examples/header/header.pyhp b/examples/header/header.pyhp new file mode 100644 index 0000000..882fde3 --- /dev/null +++ b/examples/header/header.pyhp @@ -0,0 +1,25 @@ +#!/usr/bin/pyhp + + + + + header + + + + diff --git a/examples/header/header_register_callback.output b/examples/header/header_register_callback.output new file mode 100644 index 0000000..5ea12ad --- /dev/null +++ b/examples/header/header_register_callback.output @@ -0,0 +1,15 @@ +custom text +Status: 200 OK +Content-Type: text/html + + + + + header_register_callback + + + With this function you can set a callback to be executed just before the headers are being send. +Output from this callback will be send with the headers. + + + diff --git a/examples/header/header_register_callback.pyhp b/examples/header/header_register_callback.pyhp new file mode 100644 index 0000000..6195e03 --- /dev/null +++ b/examples/header/header_register_callback.pyhp @@ -0,0 +1,18 @@ +#!/usr/bin/pyhp + + + + header_register_callback + + + + + diff --git a/examples/header/header_remove.output b/examples/header/header_remove.output new file mode 100644 index 0000000..bb310a2 --- /dev/null +++ b/examples/header/header_remove.output @@ -0,0 +1,16 @@ +Status: 200 OK +Content-Type: text/html +Test1: test + + + + + header_remove + + + header_remove(name) removes all headers of the type name. +For example, instead of sending the headers Test0 and Test1, +only Test1 is being sent. + + + diff --git a/examples/header/header_remove.pyhp b/examples/header/header_remove.pyhp new file mode 100644 index 0000000..4952616 --- /dev/null +++ b/examples/header/header_remove.pyhp @@ -0,0 +1,18 @@ +#!/usr/bin/pyhp + + + + header_remove + + + + + diff --git a/examples/header/headers_list.output b/examples/header/headers_list.output new file mode 100644 index 0000000..f784876 --- /dev/null +++ b/examples/header/headers_list.output @@ -0,0 +1,19 @@ +Status: 200 OK +Content-Type: text/html +Test0: 1 + + + + + + headers_list + + + The headers_list function lists all headers like they would be send to the client. +The headers are: +Content-Type: text/html +Test0: 1 +Test1: 2 + + + diff --git a/examples/header/headers_list.pyhp b/examples/header/headers_list.pyhp new file mode 100644 index 0000000..9977663 --- /dev/null +++ b/examples/header/headers_list.pyhp @@ -0,0 +1,16 @@ +#!/usr/bin/pyhp + + + + + headers_list + + + + + diff --git a/examples/header/headers_sent.output b/examples/header/headers_sent.output new file mode 100644 index 0000000..4bef63b --- /dev/null +++ b/examples/header/headers_sent.output @@ -0,0 +1,13 @@ +Status: 200 OK +Content-Type: text/html + + + + headers_sent + + + This function tells you if the headers are already sent. +The return value after output is True. + + + diff --git a/examples/header/headers_sent.pyhp b/examples/header/headers_sent.pyhp new file mode 100644 index 0000000..8002bac --- /dev/null +++ b/examples/header/headers_sent.pyhp @@ -0,0 +1,12 @@ +#!/usr/bin/pyhp + + + headers_sent + + + + + diff --git a/examples/request.sh b/examples/request.sh new file mode 100644 index 0000000..4dee90b --- /dev/null +++ b/examples/request.sh @@ -0,0 +1,9 @@ +#!/bin/sh -e +# script for testing the request handling + +cd request +export QUERY_STRING='test0=Hello&Test1=World%21&Test2=&Test3&&test0=World!' +export HTTP_COOKIE='test0=Hello ; Test1 = World%21 = Hello; Test2 = ;Test3;;test0=World!; ;' +pyhp methods.pyhp|diff methods.output - +pyhp request-order.pyhp --config request-order.conf|diff request-order.output - +cd .. diff --git a/examples/request/methods.output b/examples/request/methods.output new file mode 100644 index 0000000..36ad81b --- /dev/null +++ b/examples/request/methods.output @@ -0,0 +1,17 @@ +Status: 200 OK +Content-Type: text/html + + + + Methods + + + GET, POST, COOKIE and REQUEST are available. +Their values are: +{'test0': ['Hello', 'World!'], 'Test1': 'World!', 'Test2': '', 'Test3': ''} +{} +{'test0': ['Hello', 'World!'], 'Test1': 'World! = Hello', 'Test2': '', 'Test3': '', '': ['', '', '']} +{'test0': ['Hello', 'World!'], 'Test1': 'World! = Hello', 'Test2': '', 'Test3': '', '': ['', '', '']} + + + diff --git a/examples/request/methods.pyhp b/examples/request/methods.pyhp new file mode 100644 index 0000000..3d38451 --- /dev/null +++ b/examples/request/methods.pyhp @@ -0,0 +1,14 @@ +#!/usr/bin/pyhp + + + Methods + + + + + diff --git a/examples/request/request-order.conf b/examples/request/request-order.conf new file mode 100644 index 0000000..44c08a3 --- /dev/null +++ b/examples/request/request-order.conf @@ -0,0 +1,5 @@ +# config file to test request_order and default_mimetype +[request] +request_order = GET POST unkown value +default_mimetype = test + diff --git a/examples/request/request-order.output b/examples/request/request-order.output new file mode 100644 index 0000000..873e77a --- /dev/null +++ b/examples/request/request-order.output @@ -0,0 +1,14 @@ +Status: 200 OK +Content-Type: test + + + + Request Order + + + The standart order to fill REQUEST is 'GET POST COOKIE'. +With the custom order 'GET POST unknown value', REQUEST will be like this: +{'test0': ['Hello', 'World!'], 'Test1': 'World!', 'Test2': '', 'Test3': ''} + + + diff --git a/examples/request/request-order.pyhp b/examples/request/request-order.pyhp new file mode 100644 index 0000000..8418f87 --- /dev/null +++ b/examples/request/request-order.pyhp @@ -0,0 +1,13 @@ +#!/usr/bin/pyhp + + + Request Order + + + + + diff --git a/examples/shutdown_functions.sh b/examples/shutdown_functions.sh new file mode 100644 index 0000000..336e9fa --- /dev/null +++ b/examples/shutdown_functions.sh @@ -0,0 +1,6 @@ +#!/bin/sh -e +# script for testing register_shutdown_function + +cd shutdown_functions +pyhp register_shutdown_function.pyhp|diff register_shutdown_function.output - +cd .. diff --git a/examples/shutdown_functions/register_shutdown_function.output b/examples/shutdown_functions/register_shutdown_function.output new file mode 100644 index 0000000..6ed41ed --- /dev/null +++ b/examples/shutdown_functions/register_shutdown_function.output @@ -0,0 +1,13 @@ +Status: 200 OK +Content-Type: text/html + + + + register_shutdown_function + + + This function can be used to register a function to be run at interpreter shutdown. +The functions are executed even if an Exception occured. +Furthermore, the functions are called with the additional args and kwargs of register_shutdown_function. +bb +Have a nice day! diff --git a/examples/shutdown_functions/register_shutdown_function.pyhp b/examples/shutdown_functions/register_shutdown_function.pyhp new file mode 100644 index 0000000..3924ecf --- /dev/null +++ b/examples/shutdown_functions/register_shutdown_function.pyhp @@ -0,0 +1,18 @@ +#!/usr/bin/pyhp + + + register_shutdown_function + + + + + From 62ef55af0e44af969051c9431194d7c28eda4c28 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sat, 1 Feb 2020 00:02:13 +0100 Subject: [PATCH 40/42] fix tests --- .travis.yml | 28 ++++++++++++++----- examples/caching.sh | 8 ------ examples/cookie.sh | 7 ----- examples/embedding.sh | 8 ------ examples/header.sh | 10 ------- examples/request.sh | 9 ------ examples/request/methods.output | 17 ----------- examples/request/request-order.pyhp | 13 --------- examples/shutdown_functions.sh | 6 ---- {examples => tests}/cookie/setcookie.output | 0 {examples => tests}/cookie/setcookie.pyhp | 0 .../cookie/setrawcookie.output | 0 {examples => tests}/cookie/setrawcookie.pyhp | 0 .../embedding/indentation.output | 0 .../embedding/indentation.pyhp | 0 {examples => tests}/embedding/shebang.output | 0 {examples => tests}/embedding/shebang.pyhp | 0 {examples => tests}/embedding/syntax.output | 0 {examples => tests}/embedding/syntax.pyhp | 0 {examples => tests}/fib.pyhp | 0 {examples => tests}/header/header.output | 0 {examples => tests}/header/header.pyhp | 0 .../header/header_register_callback.output | 0 .../header/header_register_callback.pyhp | 0 .../header/header_remove.output | 2 +- {examples => tests}/header/header_remove.pyhp | 0 .../header/headers_list.output | 2 +- {examples => tests}/header/headers_list.pyhp | 0 .../header/headers_sent.output | 0 {examples => tests}/header/headers_sent.pyhp | 0 tests/request/methods.output | 17 +++++++++++ {examples => tests}/request/methods.pyhp | 3 +- .../request/request-order.conf | 0 .../request/request-order.output | 2 +- tests/request/request-order.pyhp | 3 ++ .../register_shutdown_function.output | 0 .../register_shutdown_function.pyhp | 0 37 files changed, 46 insertions(+), 89 deletions(-) delete mode 100644 examples/caching.sh delete mode 100644 examples/cookie.sh delete mode 100644 examples/embedding.sh delete mode 100644 examples/header.sh delete mode 100644 examples/request.sh delete mode 100644 examples/request/methods.output delete mode 100644 examples/request/request-order.pyhp delete mode 100644 examples/shutdown_functions.sh rename {examples => tests}/cookie/setcookie.output (100%) rename {examples => tests}/cookie/setcookie.pyhp (100%) rename {examples => tests}/cookie/setrawcookie.output (100%) rename {examples => tests}/cookie/setrawcookie.pyhp (100%) rename {examples => tests}/embedding/indentation.output (100%) rename {examples => tests}/embedding/indentation.pyhp (100%) rename {examples => tests}/embedding/shebang.output (100%) rename {examples => tests}/embedding/shebang.pyhp (100%) rename {examples => tests}/embedding/syntax.output (100%) rename {examples => tests}/embedding/syntax.pyhp (100%) rename {examples => tests}/fib.pyhp (100%) rename {examples => tests}/header/header.output (100%) rename {examples => tests}/header/header.pyhp (100%) rename {examples => tests}/header/header_register_callback.output (100%) rename {examples => tests}/header/header_register_callback.pyhp (100%) rename {examples => tests}/header/header_remove.output (91%) rename {examples => tests}/header/header_remove.pyhp (100%) rename {examples => tests}/header/headers_list.output (91%) rename {examples => tests}/header/headers_list.pyhp (100%) rename {examples => tests}/header/headers_sent.output (100%) rename {examples => tests}/header/headers_sent.pyhp (100%) create mode 100644 tests/request/methods.output rename {examples => tests}/request/methods.pyhp (74%) rename {examples => tests}/request/request-order.conf (100%) rename {examples => tests}/request/request-order.output (78%) create mode 100644 tests/request/request-order.pyhp rename {examples => tests}/shutdown_functions/register_shutdown_function.output (100%) rename {examples => tests}/shutdown_functions/register_shutdown_function.pyhp (100%) diff --git a/.travis.yml b/.travis.yml index 2350ab8..6e49597 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ python: - "3.7" - "3.8" - "pypy3" +env: + - QUERY_STRING='test0=Hello&Test1=World%21&Test2=&Test3&&test0=World!' HTTP_COOKIE='test0=Hello ; Test1 = World%21 = Hello; Test2 = ;Test3;;test0=World!; ;' sudo: required install: - python3 setup.py bdist_wheel @@ -12,11 +14,23 @@ install: - cd debian - . ./build_deb.sh pyhp-test ../dist/pyhp_core-$version-py3-none-any.whl pip - sudo dpkg -i pyhp-test.deb - - cd ../examples + - cd ../tests script: - - . ./embedding.sh - - . ./caching.sh - - . ./request.sh - - . ./header.sh - - . ./cookie.sh - - . ./shutdown_functions.sh + - pyhp embedding/syntax.pyhp|diff embedding/syntax.output - + - pyhp embedding/shebang.pyhp|diff embedding/shebang.output - + - pyhp embedding/indentation.pyhp|diff embedding/indentation.output - + - test -f /lib/pyhp/cache_handlers/files_mtime.py + - test ! -f ~/.cache/pyhp/$(pwd)/embedding/syntax.pyhp.cache + - pyhp --caching embedding/syntax.pyhp|diff embedding/syntax.output - + - test -f ~/.cache/pyhp/$(pwd)/embedding/syntax.pyhp.cache + - pyhp --caching embedding/syntax.pyhp|diff embedding/syntax.output - + - pyhp request/methods.pyhp|diff request/methods.output - + - pyhp request/request-order.pyhp --config request/request-order.conf|diff request/request-order.output - + - pyhp header/header.pyhp|diff header/header.output - + - pyhp header/headers_list.pyhp|diff header/headers_list.output - + - pyhp header/header_remove.pyhp|diff header/header_remove.output - + - pyhp header/headers_sent.pyhp|diff header/headers_sent.output - + - pyhp header/header_register_callback.pyhp|diff header/header_register_callback.output - + - pyhp cookie/setrawcookie.pyhp|diff cookie/setrawcookie.output - + - pyhp cookie/setcookie.pyhp|diff cookie/setcookie.output - + - pyhp shutdown_functions/register_shutdown_function.pyhp|diff shutdown_functions/register_shutdown_function.output - diff --git a/examples/caching.sh b/examples/caching.sh deleted file mode 100644 index 178132d..0000000 --- a/examples/caching.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -e -# script for testing the caching features - -test -f /lib/pyhp/cache_handlers/files_mtime.py -test ! -f ~/.cache/pyhp/$(pwd)/embedding/syntax.pyhp.cache -pyhp --caching embedding/syntax.pyhp|diff embedding/syntax.output - -test -f ~/.cache/pyhp/$(pwd)/embedding/syntax.pyhp.cache -pyhp --caching embedding/syntax.pyhp|diff embedding/syntax.output - diff --git a/examples/cookie.sh b/examples/cookie.sh deleted file mode 100644 index 3428c30..0000000 --- a/examples/cookie.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -e -# script for testing the cookie functions - -cd cookie -pyhp setrawcookie.pyhp|diff setrawcookie.output - -pyhp setcookie.pyhp|diff setcookie.output - -cd .. diff --git a/examples/embedding.sh b/examples/embedding.sh deleted file mode 100644 index 3593e29..0000000 --- a/examples/embedding.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -e -# script for testing the embedding features - -cd embedding -pyhp syntax.pyhp|diff syntax.output - -pyhp shebang.pyhp|diff shebang.output - -pyhp indentation.pyhp|diff indentation.output - -cd .. diff --git a/examples/header.sh b/examples/header.sh deleted file mode 100644 index 0582796..0000000 --- a/examples/header.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -e -# script for testing the header features - -cd header -pyhp header.pyhp|diff header.output - -pyhp headers_list.pyhp|diff headers_list.output - -pyhp header_remove.pyhp|diff header_remove.output - -pyhp headers_sent.pyhp|diff headers_sent.output - -pyhp header_register_callback.pyhp|diff header_register_callback.output - -cd .. diff --git a/examples/request.sh b/examples/request.sh deleted file mode 100644 index 4dee90b..0000000 --- a/examples/request.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -e -# script for testing the request handling - -cd request -export QUERY_STRING='test0=Hello&Test1=World%21&Test2=&Test3&&test0=World!' -export HTTP_COOKIE='test0=Hello ; Test1 = World%21 = Hello; Test2 = ;Test3;;test0=World!; ;' -pyhp methods.pyhp|diff methods.output - -pyhp request-order.pyhp --config request-order.conf|diff request-order.output - -cd .. diff --git a/examples/request/methods.output b/examples/request/methods.output deleted file mode 100644 index 36ad81b..0000000 --- a/examples/request/methods.output +++ /dev/null @@ -1,17 +0,0 @@ -Status: 200 OK -Content-Type: text/html - - - - Methods - - - GET, POST, COOKIE and REQUEST are available. -Their values are: -{'test0': ['Hello', 'World!'], 'Test1': 'World!', 'Test2': '', 'Test3': ''} -{} -{'test0': ['Hello', 'World!'], 'Test1': 'World! = Hello', 'Test2': '', 'Test3': '', '': ['', '', '']} -{'test0': ['Hello', 'World!'], 'Test1': 'World! = Hello', 'Test2': '', 'Test3': '', '': ['', '', '']} - - - diff --git a/examples/request/request-order.pyhp b/examples/request/request-order.pyhp deleted file mode 100644 index 8418f87..0000000 --- a/examples/request/request-order.pyhp +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/pyhp - - - Request Order - - - - - diff --git a/examples/shutdown_functions.sh b/examples/shutdown_functions.sh deleted file mode 100644 index 336e9fa..0000000 --- a/examples/shutdown_functions.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -e -# script for testing register_shutdown_function - -cd shutdown_functions -pyhp register_shutdown_function.pyhp|diff register_shutdown_function.output - -cd .. diff --git a/examples/cookie/setcookie.output b/tests/cookie/setcookie.output similarity index 100% rename from examples/cookie/setcookie.output rename to tests/cookie/setcookie.output diff --git a/examples/cookie/setcookie.pyhp b/tests/cookie/setcookie.pyhp similarity index 100% rename from examples/cookie/setcookie.pyhp rename to tests/cookie/setcookie.pyhp diff --git a/examples/cookie/setrawcookie.output b/tests/cookie/setrawcookie.output similarity index 100% rename from examples/cookie/setrawcookie.output rename to tests/cookie/setrawcookie.output diff --git a/examples/cookie/setrawcookie.pyhp b/tests/cookie/setrawcookie.pyhp similarity index 100% rename from examples/cookie/setrawcookie.pyhp rename to tests/cookie/setrawcookie.pyhp diff --git a/examples/embedding/indentation.output b/tests/embedding/indentation.output similarity index 100% rename from examples/embedding/indentation.output rename to tests/embedding/indentation.output diff --git a/examples/embedding/indentation.pyhp b/tests/embedding/indentation.pyhp similarity index 100% rename from examples/embedding/indentation.pyhp rename to tests/embedding/indentation.pyhp diff --git a/examples/embedding/shebang.output b/tests/embedding/shebang.output similarity index 100% rename from examples/embedding/shebang.output rename to tests/embedding/shebang.output diff --git a/examples/embedding/shebang.pyhp b/tests/embedding/shebang.pyhp similarity index 100% rename from examples/embedding/shebang.pyhp rename to tests/embedding/shebang.pyhp diff --git a/examples/embedding/syntax.output b/tests/embedding/syntax.output similarity index 100% rename from examples/embedding/syntax.output rename to tests/embedding/syntax.output diff --git a/examples/embedding/syntax.pyhp b/tests/embedding/syntax.pyhp similarity index 100% rename from examples/embedding/syntax.pyhp rename to tests/embedding/syntax.pyhp diff --git a/examples/fib.pyhp b/tests/fib.pyhp similarity index 100% rename from examples/fib.pyhp rename to tests/fib.pyhp diff --git a/examples/header/header.output b/tests/header/header.output similarity index 100% rename from examples/header/header.output rename to tests/header/header.output diff --git a/examples/header/header.pyhp b/tests/header/header.pyhp similarity index 100% rename from examples/header/header.pyhp rename to tests/header/header.pyhp diff --git a/examples/header/header_register_callback.output b/tests/header/header_register_callback.output similarity index 100% rename from examples/header/header_register_callback.output rename to tests/header/header_register_callback.output diff --git a/examples/header/header_register_callback.pyhp b/tests/header/header_register_callback.pyhp similarity index 100% rename from examples/header/header_register_callback.pyhp rename to tests/header/header_register_callback.pyhp diff --git a/examples/header/header_remove.output b/tests/header/header_remove.output similarity index 91% rename from examples/header/header_remove.output rename to tests/header/header_remove.output index bb310a2..5057d05 100644 --- a/examples/header/header_remove.output +++ b/tests/header/header_remove.output @@ -10,7 +10,7 @@ Test1: test header_remove(name) removes all headers of the type name. For example, instead of sending the headers Test0 and Test1, -only Test1 is being sent. +only Test1 is being send. diff --git a/examples/header/header_remove.pyhp b/tests/header/header_remove.pyhp similarity index 100% rename from examples/header/header_remove.pyhp rename to tests/header/header_remove.pyhp diff --git a/examples/header/headers_list.output b/tests/header/headers_list.output similarity index 91% rename from examples/header/headers_list.output rename to tests/header/headers_list.output index f784876..aa7a5ce 100644 --- a/examples/header/headers_list.output +++ b/tests/header/headers_list.output @@ -9,7 +9,7 @@ Test0: 1 headers_list - The headers_list function lists all headers like they would be send to the client. + The headers_list function lists all headers like they would be sent to the client. The headers are: Content-Type: text/html Test0: 1 diff --git a/examples/header/headers_list.pyhp b/tests/header/headers_list.pyhp similarity index 100% rename from examples/header/headers_list.pyhp rename to tests/header/headers_list.pyhp diff --git a/examples/header/headers_sent.output b/tests/header/headers_sent.output similarity index 100% rename from examples/header/headers_sent.output rename to tests/header/headers_sent.output diff --git a/examples/header/headers_sent.pyhp b/tests/header/headers_sent.pyhp similarity index 100% rename from examples/header/headers_sent.pyhp rename to tests/header/headers_sent.pyhp diff --git a/tests/request/methods.output b/tests/request/methods.output new file mode 100644 index 0000000..116cb5e --- /dev/null +++ b/tests/request/methods.output @@ -0,0 +1,17 @@ +Status: 200 OK +Content-Type: text/html + + + + Methods + + + GET, POST, COOKIE and REQUEST are available. +Their values are: +OrderedDict([('Test1', 'World!'), ('Test2', ''), ('Test3', ''), ('test0', ['Hello', 'World!'])]) +OrderedDict() +OrderedDict([('', ['', '', '']), ('Test1', 'World! = Hello'), ('Test2', ''), ('Test3', ''), ('test0', ['Hello', 'World!'])]) +OrderedDict([('', ['', '', '']), ('Test1', 'World! = Hello'), ('Test2', ''), ('Test3', ''), ('test0', ['Hello', 'World!'])]) + + + diff --git a/examples/request/methods.pyhp b/tests/request/methods.pyhp similarity index 74% rename from examples/request/methods.pyhp rename to tests/request/methods.pyhp index 3d38451..592c308 100644 --- a/examples/request/methods.pyhp +++ b/tests/request/methods.pyhp @@ -5,10 +5,11 @@ diff --git a/examples/request/request-order.conf b/tests/request/request-order.conf similarity index 100% rename from examples/request/request-order.conf rename to tests/request/request-order.conf diff --git a/examples/request/request-order.output b/tests/request/request-order.output similarity index 78% rename from examples/request/request-order.output rename to tests/request/request-order.output index 873e77a..16e7d11 100644 --- a/examples/request/request-order.output +++ b/tests/request/request-order.output @@ -8,7 +8,7 @@ Content-Type: test The standart order to fill REQUEST is 'GET POST COOKIE'. With the custom order 'GET POST unknown value', REQUEST will be like this: -{'test0': ['Hello', 'World!'], 'Test1': 'World!', 'Test2': '', 'Test3': ''} +{'Test1': 'World!', 'Test2': '', 'Test3': '', 'test0': ['Hello', 'World!']} diff --git a/tests/request/request-order.pyhp b/tests/request/request-order.pyhp new file mode 100644 index 0000000..e4a5a57 --- /dev/null +++ b/tests/request/request-order.pyhp @@ -0,0 +1,3 @@ +Status: 200 OK +Content-Type: test + diff --git a/examples/shutdown_functions/register_shutdown_function.output b/tests/shutdown_functions/register_shutdown_function.output similarity index 100% rename from examples/shutdown_functions/register_shutdown_function.output rename to tests/shutdown_functions/register_shutdown_function.output diff --git a/examples/shutdown_functions/register_shutdown_function.pyhp b/tests/shutdown_functions/register_shutdown_function.pyhp similarity index 100% rename from examples/shutdown_functions/register_shutdown_function.pyhp rename to tests/shutdown_functions/register_shutdown_function.pyhp From a0a98b2675f605ff8be0564d51352948413b5a2a Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sat, 1 Feb 2020 00:10:33 +0100 Subject: [PATCH 41/42] fix request-order.pyhp being oberwritten --- tests/request/request-order.output | 2 +- tests/request/request-order.pyhp | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/request/request-order.output b/tests/request/request-order.output index 16e7d11..c56ee33 100644 --- a/tests/request/request-order.output +++ b/tests/request/request-order.output @@ -8,7 +8,7 @@ Content-Type: test The standart order to fill REQUEST is 'GET POST COOKIE'. With the custom order 'GET POST unknown value', REQUEST will be like this: -{'Test1': 'World!', 'Test2': '', 'Test3': '', 'test0': ['Hello', 'World!']} +OrderedDict([('Test1', 'World!'), ('Test2', ''), ('Test3', ''), ('test0', ['Hello', 'World!'])]) diff --git a/tests/request/request-order.pyhp b/tests/request/request-order.pyhp index e4a5a57..50bcf4e 100644 --- a/tests/request/request-order.pyhp +++ b/tests/request/request-order.pyhp @@ -1,3 +1,14 @@ -Status: 200 OK -Content-Type: test - +#!/usr/bin/pyhp + + + Request Order + + + + + From d723266fa5ce1e3c18a40f01b5441e5ffb1ebb48 Mon Sep 17 00:00:00 2001 From: Eric Wolf Date: Sat, 1 Feb 2020 00:34:37 +0100 Subject: [PATCH 42/42] tiny changes for readability --- cache_handlers/files_mtime.py | 5 +++-- pyhp/__init__.py | 2 +- pyhp/embed.py | 18 +++++++++--------- pyhp/libpyhp.py | 25 +++++++++++++------------ pyhp/main.py | 19 ++++++++++--------- setup.py | 2 +- 6 files changed, 37 insertions(+), 34 deletions(-) diff --git a/cache_handlers/files_mtime.py b/cache_handlers/files_mtime.py index 274a1d1..502ba23 100644 --- a/cache_handlers/files_mtime.py +++ b/cache_handlers/files_mtime.py @@ -7,6 +7,7 @@ from os import makedirs from time import time + class Handler: def __init__(self, cache_path, file_path, config): self.cache_prefix = cache_path @@ -23,7 +24,7 @@ def get_cachedir_size(self): # get size of cache directory (with all sub filepath = os.path.join(dirpath, filename) if not os.path.islink(filepath): # dont count symlinks size += os.path.getsize(filepath) - return size/(1000**2) # bytes --> Mbytes + return size / (1000 ** 2) # bytes --> Mbytes def is_available(self): # if cache directory has free space or the cached file is already existing or max_size < 0 return self.max_size < 0 or os.path.isfile(self.cache_path) or self.get_cachedir_size() < self.max_size @@ -37,7 +38,7 @@ def is_outdated(self): # return True if cache is not created or needs refre else: return True # file is not existing --> age = infinite - def load(self): # load sections + def load(self): # load sections with open(self.cache_path, "rb") as cache: code = marshal.load(cache) return code diff --git a/pyhp/__init__.py b/pyhp/__init__.py index 44991ab..b07fd3d 100644 --- a/pyhp/__init__.py +++ b/pyhp/__init__.py @@ -8,7 +8,7 @@ __author__ = "Eric Wolf" __maintainer__ = "Eric Wolf" __license__ = "MIT" -__email__ = "robo-eric@gmx.de" # please dont use for spam :( +__email__ = "robo-eric@gmx.de" # please dont use for spam :( __contact__ = "https://github.com/Deric-W/PyHP" # import all submodules diff --git a/pyhp/embed.py b/pyhp/embed.py index 7565c33..af7b99d 100644 --- a/pyhp/embed.py +++ b/pyhp/embed.py @@ -9,11 +9,12 @@ from io import StringIO from contextlib import redirect_stdout + # class for handling strings class FromString: # get string, regex to isolate code and optional flags for the regex (default for processing text files) # the userdata is given to the processor function to allow state - def __init__(self, string, regex, flags=re.MULTILINE|re.DOTALL, userdata=None): + def __init__(self, string, regex, flags=re.MULTILINE | re.DOTALL, userdata=None): self.sections = re.split(regex, string, flags=flags) self.userdata = userdata @@ -48,9 +49,8 @@ def __str__(self): class FromIter(FromString): # get presplit string as iterator def __init__(self, iterator, userdata=None): - self.sections = list(iterator) - self.userdata = userdata - + self.sections = list(iterator) + self.userdata = userdata # function for executing python code # userdata = [locals, section_number], init with [{}, 0] @@ -66,7 +66,7 @@ def python_execute(code, userdata): def python_compile(code, userdata): userdata[1] += 1 try: - return compile(python_align(code), userdata[0], "exec") + return compile(python_align(code), userdata[0], "exec") except Exception as e: # tell the user the section of the Exception raise Exception("Exception during executing of section %d" % userdata[1]) from e @@ -85,15 +85,15 @@ def python_align(code, indentation=None): code = code.splitlines() # split to lines for line in code: line_num += 1 - if not (not line or line.isspace() or python_is_comment(line)): # ignore non code lines - if indentation == None: # first line of code, get startindentation + if not (not line or line.isspace() or python_is_comment(line)): # ignore non code lines + if indentation is None: # first line of code, get startindentation indentation = python_get_indentation(line) - if line.startswith(indentation): # if line starts with startindentation + if line.startswith(indentation): # if line starts with startindentation code[line_num - 1] = line[len(indentation):] # remove startindentation else: raise IndentationError("indentation not matching", ("embedded code section", line_num, len(indentation), line)) # raise Exception on bad indentation return "\n".join(code) # join the lines back together - + # function for getting the indentation of a line of python code def python_get_indentation(line): diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py index 1c6dee7..e5d9790 100644 --- a/pyhp/libpyhp.py +++ b/pyhp/libpyhp.py @@ -13,15 +13,16 @@ from http import HTTPStatus from collections import defaultdict + # class containing the implementations class PyHP: def __init__(self, # build GET, POST, COOKIE, SERVER, REQUEST - file_path = sys.argv[0], # override if not directly executed - request_order = ("GET", "POST", "COOKIE"), # order in wich REQUEST gets updated - keep_blank_values = True, # if to not remove "" values - fallback_value = None, # fallback value of GET, POST, REQUEST and COOKIE if not None - enable_post_data_reading = False, # if not to parse POST and consume stdin in the process - default_mimetype = "text/html" # Content-Type header if not been set + file_path=sys.argv[0], # override if not directly executed + request_order=("GET", "POST", "COOKIE"), # order in wich REQUEST gets updated + keep_blank_values=True, # if to not remove "" values + fallback_value=None, # fallback value of GET, POST, REQUEST and COOKIE if not None + enable_post_data_reading=False, # if not to parse POST and consume stdin in the process + default_mimetype="text/html" # Content-Type header if not been set ): self.__FILE__ = os.path.abspath(file_path) # absolute path of script self.response_code = 200 @@ -81,7 +82,7 @@ def __init__(self, # build GET, POST, COOKIE, SERVER, REQUEST self.POST = dict2defaultdict({}, fallback_value) else: # parse POST and consume stdin self.POST = dict2defaultdict(parse_post(keep_blank_values), fallback_value) - + # build REQUEST self.REQUEST = dict2defaultdict({}, fallback_value) # empthy REQUEST for request in request_order: # update REQUEST in the order given by request_order @@ -113,12 +114,12 @@ def header(self, header, replace=True, http_response_code=None): self.headers.append(header) # add header if http_response_code is not None: # set response code if given (higher priority than location headers) self.response_code = http_response_code - elif header[0].lower() == "location" and not check_redirect(self.response_code): # set matching response code if code is not 201 or 3xx + elif header[0].lower() == "location" and not check_redirect(self.response_code): # set matching response code if code is not 201 or 3xx self.response_code = 302 else: pass - # list set headers + # list set headers def headers_list(self): return [": ".join(header) for header in self.headers] # list headers like received by the client @@ -127,7 +128,7 @@ def headers_list(self): def header_remove(self, name=None): if name is not None: name = name.lower() # header names are case-insensitive - self.headers = [header for header in self.headers if header[0].lower() != name] # remove headers with same name + self.headers = [header for header in self.headers if header[0].lower() != name] # remove headers with same name else: self.headers = [] # remove all headers @@ -150,7 +151,7 @@ def header_register_callback(self, callback): def send_headers(self): self.header_sent = True # prevent recursion if callback prints output self.header_callback() # execute callback - print("Status:" , self.response_code, HTTPStatus(self.response_code).phrase) + print("Status:", self.response_code, HTTPStatus(self.response_code).phrase) for header in self.headers: print(": ".join(header)) print() # end of headers @@ -189,7 +190,7 @@ def setrawcookie(self, name, value="", expires=0, path=None, domain=None, secure cookie = "Set-Cookie: %s=%s" % (name, value) # initial header if expires != 0: cookie += "; " + "Expires=%s" % time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time() + expires)) # add Expires and Max-Age just in case - cookie += "; " + "Max-Age=%d" % expires + cookie += "; " + "Max-Age=%d" % expires if path is not None: cookie += "; " + "Path=%s" % path if domain is not None: diff --git a/pyhp/main.py b/pyhp/main.py index 1b85c6e..b2619b7 100644 --- a/pyhp/main.py +++ b/pyhp/main.py @@ -14,6 +14,7 @@ from . import embed from . import libpyhp + # get cli arguments for main as dict def get_args(): parser = argparse.ArgumentParser(prog="pyhp", description="Interpreter for .pyhp Scripts (https://github.com/Deric-W/PyHP)") @@ -38,7 +39,7 @@ def main(file_path, caching=False, config_file="/etc/pyhp.conf"): enable_post_data_reading=config.getboolean("request", "enable_post_data_reading", fallback=False), default_mimetype=config.get("request", "default_mimetype", fallback="text/html") ) - sys.stdout.write = PyHP.make_header_wrapper(sys.stdout.write) # wrap stdout + sys.stdout.write = PyHP.make_header_wrapper(sys.stdout.write) # wrap stdout atexit.register(PyHP.run_shutdown_functions) # run shutdown functions even if a exception occured # handle caching @@ -47,33 +48,33 @@ def main(file_path, caching=False, config_file="/etc/pyhp.conf"): caching_allowed = config.getboolean("caching", "auto", fallback=False) # if file is not stdin and caching is enabled and wanted or auto_caching is enabled if check_if_caching(file_path, caching, caching_enabled, caching_allowed): - handler_path = prepare_path(config.get("caching", "handler_path", fallback="/lib/pyhp/cache_handlers/files_mtime.py")) # get neccesary data + handler_path = prepare_path(config.get("caching", "handler_path", fallback="/lib/pyhp/cache_handlers/files_mtime.py")) # get neccesary data cache_path = prepare_path(config.get("caching", "path", fallback="~/.pyhp/cache")) handler = import_path(handler_path) handler = handler.Handler(cache_path, os.path.abspath(file_path), config["caching"]) # init handler if handler.is_available(): # check if caching is possible cached = True if handler.is_outdated(): # update cache - code = embed.FromString(prepare_file(file_path), regex, userdata=[file_path, 0]) # set userdata for python_compile + code = embed.FromString(prepare_file(file_path), regex, userdata=[file_path, 0]) # set userdata for python_compile code.process(embed.python_compile) # compile python sections - code.userdata = [{"PyHP": PyHP}, 0] # set userdata for python_execute_compiled + code.userdata = [{"PyHP": PyHP}, 0] # set userdata for python_execute_compiled handler.save(code.sections) # just save the code sections else: # load cache code = embed.FromIter(handler.load(), userdata=[{"PyHP": PyHP}, 0]) else: # generate FromString Object cached = False - code = embed.FromString(prepare_file(file_path), regex, userdata=[{"PyHP": PyHP}, 0]) - handler.close() + code = embed.FromString(prepare_file(file_path), regex, userdata=[{"PyHP": PyHP}, 0]) + handler.close() else: # same as above cached = False - code = embed.FromString(prepare_file(file_path), regex, userdata=[{"PyHP": PyHP}, 0]) + code = embed.FromString(prepare_file(file_path), regex, userdata=[{"PyHP": PyHP}, 0]) if cached: # run compiled code code.execute(embed.python_execute_compiled) else: # run normal code code.execute(embed.python_execute) - if not PyHP.headers_sent(): # prevent error if no output occured, but not if an exception occured + if not PyHP.headers_sent(): # prevent error if no output occured, but not if an exception occured PyHP.send_headers() return 0 # return 0 on success @@ -92,7 +93,7 @@ def import_path(path): # check we should cache def check_if_caching(file_path, caching, enabled, auto): possible = file_path != "" # file is not stdin - allowed = (caching or auto) and enabled # if caching is wanted and enabled + allowed = (caching or auto) and enabled # if caching is wanted and enabled return possible and allowed # get code and remove shebang diff --git a/setup.py b/setup.py index f4be260..553688c 100644 --- a/setup.py +++ b/setup.py @@ -22,4 +22,4 @@ "License :: OSI Approved :: MIT License", ], python_requires='>=3.5', -) \ No newline at end of file +)