diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33111af --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*/__pycache__ +dist +notes.txt +Test.html + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6e49597 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,36 @@ +language: python +python: + - "3.5" + - "3.6" + - "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 + - version=$(python3 setup.py --version) + - cd debian + - . ./build_deb.sh pyhp-test ../dist/pyhp_core-$version-py3-none-any.whl pip + - sudo dpkg -i pyhp-test.deb + - cd ../tests +script: + - 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/README.md b/README.md index ba500bc..7b6c0b4 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 (available from the outside 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 + 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/cache_handlers/files_mtime.py b/cache_handlers/files_mtime.py index 39e4aef..502ba23 100644 --- a/cache_handlers/files_mtime.py +++ b/cache_handlers/files_mtime.py @@ -2,34 +2,52 @@ """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 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_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 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/debian/build_deb.sh b/debian/build_deb.sh new file mode 100755 index 0000000..83ab606 --- /dev/null +++ b/debian/build_deb.sh @@ -0,0 +1,65 @@ +#!/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" = "" ] +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 "pip executeable: " pip +else pip=$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" +$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 ../ + +# 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" + +# remove build directory +rm -r "$package" + +echo "Done" diff --git a/debian/changelog b/debian/changelog index d3527cc..15661a9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,24 @@ +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 --version 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 +34,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..04445aa --- /dev/null +++ b/debian/pyhp @@ -0,0 +1,12 @@ +#!/usr/bin/python3 + +# 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 +sys.exit(main(args.pop("file_path"), **args)) 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/fib.pyhp b/examples/fib.pyhp deleted file mode 100644 index 74fe769..0000000 --- a/examples/fib.pyhp +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/pyhp - - - Fibonacci-Zahlen 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!

") - ?> -
- - -
- - \ No newline at end of file diff --git a/pyhp.conf b/pyhp.conf index 89063cc..83d9c1a 100644 --- a/pyhp.conf +++ b/pyhp.conf @@ -2,12 +2,77 @@ # This file uses the INI syntax [parser] -opening_tag = \<\?pyhp[\n \t] # regex -closing_tag = [\n \t]\?\> # regex +# regex to isolate the code +# escape sequences are processed +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] -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 +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 = False + +# path for caching +path = ~/.cache/pyhp + +# path to handler +handler_path = /lib/pyhp/cache_handlers/files_mtime.py + +[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 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/pyhp/__init__.py b/pyhp/__init__.py new file mode 100644 index 0000000..b07fd3d --- /dev/null +++ b/pyhp/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python3 + +"""Package for embedding and using python code like php""" + +# package metadata +# needs to be defined before .main is imported +__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" + +# import all submodules +from . import embed +from . import libpyhp +from . import main diff --git a/pyhp/__main__.py b/pyhp/__main__.py new file mode 100644 index 0000000..6ebe148 --- /dev/null +++ b/pyhp/__main__.py @@ -0,0 +1,12 @@ +#!/usr/bin/python3 + +# 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 +sys.exit(main(args.pop("file_path"), **args)) diff --git a/pyhp/embed.py b/pyhp/embed.py new file mode 100644 index 0000000..af7b99d --- /dev/null +++ b/pyhp/embed.py @@ -0,0 +1,110 @@ +#!/usr/bin/python3 + +# Module for processing strings embedded in text files, preferably Python code. +# This module is part of PyHP (https://github.com/Deric-W/PyHP) +"""Module for processing strings embedded in text files""" + +import re +import sys +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): + 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 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 + processor(self.sections[i], self.userdata) + else: # even index --> not code + 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 + def __init__(self, iterator, userdata=None): + self.sections = list(iterator) + self.userdata = userdata + +# function for executing python code +# 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: # tell the user the section of the Exception + raise Exception("Exception during execution of section %d" % userdata[1]) from e + +# compile python code sections +# userdata = [file, section_number], init with [str, 0] +def python_compile(code, userdata): + userdata[1] += 1 + try: + 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 + +# 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): + line_num = 0 + 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 is 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("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): + 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.lstrip().startswith("#") diff --git a/pyhp/libpyhp.py b/pyhp/libpyhp.py new file mode 100644 index 0000000..e5d9790 --- /dev/null +++ b/pyhp/libpyhp.py @@ -0,0 +1,298 @@ +#!/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 urllib.parse +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 + ): + self.__FILE__ = os.path.abspath(file_path) # absolute path of script + self.response_code = 200 + self.headers = [["Content-Type", default_mimetype]] # init with default mimetype header + self.header_sent = False + self.header_callback = lambda: None # dummy callback + 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)), + "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 + + # 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 + # 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.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 + 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: + pass + + # list set headers + def headers_list(self): + 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!) + 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 + 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 + # 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 + self.header_callback() # execute callback + print("Status:", self.response_code, 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) + 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 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 + # 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 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", None) + expires = expires.get("expires", 0) # has to happen at the end because it overrides expires + 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 is not None: + cookie += "; " + "Path=%s" % path + if domain is not None: + cookie += "; " + "Domain=%s" % domain + if secure: + cookie += "; " + "Secure" + if httponly: + cookie += "; " + "HttpOnly" + if samesite is not None: + cookie += "; " + "SameSite=%s" % samesite + 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 + # 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) + + +# 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 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.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 + 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: + 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 + +# check if the response code is redirecting (201 or 3xx) +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 + 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 + + +# Class containing a fallback session handler (with no function) +class dummy_session_handler: + pass diff --git a/pyhp/main.py b/pyhp/main.py new file mode 100644 index 0000000..b2619b7 --- /dev/null +++ b/pyhp/main.py @@ -0,0 +1,108 @@ +#!/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) + +import sys +import os +import argparse +import configparser +import importlib +import atexit +import errno +from . import __version__ +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)") + 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() + return {"file_path": args.file, "caching": args.caching, "config_file": args.config} + +# start the PyHP Interpreter with predefined arguments +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) + + # 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) # 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 + 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 = 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.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 + 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() + 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 an exception occured + PyHP.send_headers() + return 0 # return 0 on success + +# 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)) # 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 +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.partition("\n")[2] # get all lines except the first line + return code diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..553688c --- /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="package for embedding and using python code like php", + long_description=long_description, + long_description_content_type="text/markdown", + url=pyhp.__contact__, + packages=["pyhp"], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + ], + python_requires='>=3.5', +) diff --git a/tests/cookie/setcookie.output b/tests/cookie/setcookie.output new file mode 100644 index 0000000..bbf2f1d --- /dev/null +++ b/tests/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/tests/cookie/setcookie.pyhp b/tests/cookie/setcookie.pyhp new file mode 100644 index 0000000..3adf83f --- /dev/null +++ b/tests/cookie/setcookie.pyhp @@ -0,0 +1,12 @@ +#!/usr/bin/pyhp + + + + setcookie + + + + + diff --git a/tests/cookie/setrawcookie.output b/tests/cookie/setrawcookie.output new file mode 100644 index 0000000..ec75ec8 --- /dev/null +++ b/tests/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/tests/cookie/setrawcookie.pyhp b/tests/cookie/setrawcookie.pyhp new file mode 100644 index 0000000..0090e75 --- /dev/null +++ b/tests/cookie/setrawcookie.pyhp @@ -0,0 +1,17 @@ +#!/usr/bin/pyhp + + + + setrawcookie + + + + + diff --git a/tests/embedding/indentation.output b/tests/embedding/indentation.output new file mode 100644 index 0000000..3b860e4 --- /dev/null +++ b/tests/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/tests/embedding/indentation.pyhp b/tests/embedding/indentation.pyhp new file mode 100644 index 0000000..1e4b32e --- /dev/null +++ b/tests/embedding/indentation.pyhp @@ -0,0 +1,20 @@ +#!/usr/bin/pyhp + + + Indentation + + + + + + diff --git a/tests/embedding/shebang.output b/tests/embedding/shebang.output new file mode 100644 index 0000000..ce99070 --- /dev/null +++ b/tests/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/tests/embedding/shebang.pyhp b/tests/embedding/shebang.pyhp new file mode 100644 index 0000000..e9597b5 --- /dev/null +++ b/tests/embedding/shebang.pyhp @@ -0,0 +1,9 @@ +#!/usr/bin/pyhp + + + Shebang + + + + + diff --git a/tests/embedding/syntax.output b/tests/embedding/syntax.output new file mode 100644 index 0000000..2ba792a --- /dev/null +++ b/tests/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/tests/embedding/syntax.pyhp b/tests/embedding/syntax.pyhp new file mode 100644 index 0000000..a47c5c4 --- /dev/null +++ b/tests/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/tests/fib.pyhp b/tests/fib.pyhp new file mode 100644 index 0000000..cb09600 --- /dev/null +++ b/tests/fib.pyhp @@ -0,0 +1,30 @@ +#!/usr/bin/pyhp + + + Fibonacci test + + + 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/tests/header/header.output b/tests/header/header.output new file mode 100644 index 0000000..2a7a6cf --- /dev/null +++ b/tests/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/tests/header/header.pyhp b/tests/header/header.pyhp new file mode 100644 index 0000000..882fde3 --- /dev/null +++ b/tests/header/header.pyhp @@ -0,0 +1,25 @@ +#!/usr/bin/pyhp + + + + + header + + + + diff --git a/tests/header/header_register_callback.output b/tests/header/header_register_callback.output new file mode 100644 index 0000000..5ea12ad --- /dev/null +++ b/tests/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/tests/header/header_register_callback.pyhp b/tests/header/header_register_callback.pyhp new file mode 100644 index 0000000..6195e03 --- /dev/null +++ b/tests/header/header_register_callback.pyhp @@ -0,0 +1,18 @@ +#!/usr/bin/pyhp + + + + header_register_callback + + + + + diff --git a/tests/header/header_remove.output b/tests/header/header_remove.output new file mode 100644 index 0000000..5057d05 --- /dev/null +++ b/tests/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 send. + + + diff --git a/tests/header/header_remove.pyhp b/tests/header/header_remove.pyhp new file mode 100644 index 0000000..4952616 --- /dev/null +++ b/tests/header/header_remove.pyhp @@ -0,0 +1,18 @@ +#!/usr/bin/pyhp + + + + header_remove + + + + + diff --git a/tests/header/headers_list.output b/tests/header/headers_list.output new file mode 100644 index 0000000..aa7a5ce --- /dev/null +++ b/tests/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 sent to the client. +The headers are: +Content-Type: text/html +Test0: 1 +Test1: 2 + + + diff --git a/tests/header/headers_list.pyhp b/tests/header/headers_list.pyhp new file mode 100644 index 0000000..9977663 --- /dev/null +++ b/tests/header/headers_list.pyhp @@ -0,0 +1,16 @@ +#!/usr/bin/pyhp + + + + + headers_list + + + + + diff --git a/tests/header/headers_sent.output b/tests/header/headers_sent.output new file mode 100644 index 0000000..4bef63b --- /dev/null +++ b/tests/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/tests/header/headers_sent.pyhp b/tests/header/headers_sent.pyhp new file mode 100644 index 0000000..8002bac --- /dev/null +++ b/tests/header/headers_sent.pyhp @@ -0,0 +1,12 @@ +#!/usr/bin/pyhp + + + headers_sent + + + + + 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/tests/request/methods.pyhp b/tests/request/methods.pyhp new file mode 100644 index 0000000..592c308 --- /dev/null +++ b/tests/request/methods.pyhp @@ -0,0 +1,15 @@ +#!/usr/bin/pyhp + + + Methods + + + + + diff --git a/tests/request/request-order.conf b/tests/request/request-order.conf new file mode 100644 index 0000000..44c08a3 --- /dev/null +++ b/tests/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/tests/request/request-order.output b/tests/request/request-order.output new file mode 100644 index 0000000..c56ee33 --- /dev/null +++ b/tests/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: +OrderedDict([('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..50bcf4e --- /dev/null +++ b/tests/request/request-order.pyhp @@ -0,0 +1,14 @@ +#!/usr/bin/pyhp + + + Request Order + + + + + diff --git a/tests/shutdown_functions/register_shutdown_function.output b/tests/shutdown_functions/register_shutdown_function.output new file mode 100644 index 0000000..6ed41ed --- /dev/null +++ b/tests/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/tests/shutdown_functions/register_shutdown_function.pyhp b/tests/shutdown_functions/register_shutdown_function.pyhp new file mode 100644 index 0000000..3924ecf --- /dev/null +++ b/tests/shutdown_functions/register_shutdown_function.pyhp @@ -0,0 +1,18 @@ +#!/usr/bin/pyhp + + + register_shutdown_function + + + + +