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 @@
+
+
+
+
+
+ ' 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
+
+
+
+
+